Skip to content

fix(apply): refuse an unusable build id before tofu apply - #803

Merged
fas89 merged 3 commits into
mainfrom
claude/eloquent-merkle-e755fe
Nov 3, 2026
Merged

fas89 merged 3 commits into
mainfrom
claude/eloquent-merkle-e755fe

Conversation

@fas89

@fas89 fas89 commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Description

fluid apply --mode amend-and-build (and replace-and-build) checked contract.id and builds[].id only in the build runner. On the OpenTofu engine the runner starts after tofu apply. So an id the schema accepts but the build phase refuses was refused only after the product had been provisioned, and it printed as "Unexpected error" (exit 2).

The two rules differ in three ways:

  • The schema's $defs.identifier allows a leading digit (1orders) and any length.
  • The build phase (build_runners/_ids.py) wants a letter or _ first and at most 128 characters.
  • Since security: refuse a trailing newline in whole-string validators #797 the build phase also refuses a trailing newline, which the schema's pattern still lets through.

This PR runs the runner's own check at the start of the apply, right after the schema gate. It runs in the build modes only, before the apply hooks, tofu and the runner, on both engines:

  • build_runners/_ids.py::validate_build_ids(contract) is the runner's existing rule as one function. The runner and fluid apply both call it. IdentifierViolation now carries kind and value.
  • cli/apply.py::_gate_build_ids_for_apply turns a refusal into CLIError apply_build_identifier_invalid (ERR_APPLY_BUILD_IDENTIFIER_INVALID). The error shows the field, the value, a hint with the rule and "Nothing was applied", and catalog suggestions.
  • A mode without a build phase is not checked, and neither is a contract without builds, as before. The runner keeps its own check as a backstop.

Before and after, on the OpenTofu engine with id: 1orders (which fluid validate accepts):

before: tofu init/plan/apply ran, then
        ❌ Unexpected error: contract.id '1orders' is not a valid identifier (...)    exit 2

after:  ❌ apply_build_identifier_invalid  [ERR_APPLY_BUILD_IDENTIFIER_INVALID]
          field: contract.id
          value: '1orders'
          hint: The build phase keeps each build's run records, cursors and watermarks under
                .fluid/runs/<contract.id>/<build.id>/, so both ids must start with a letter or
                underscore, use only letters, digits, '_', '.' and '-', and be at most 128
                characters. ... Nothing was applied.
        exit 1, and tofu is never called

The founder decided that the two id grammars stay different. $defs.identifier backs 20 schema fields, and _ids is a security guard. Instead, fluid validate and fluid plan will warn about such ids, in a follow-up PR.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would break existing functionality)
  • Documentation update
  • Refactor / code cleanup
  • New provider or provider enhancement

Documentation

Checklist

  • I have read the Contributing Guide
  • My code follows the project's coding standards
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (pytest)
  • I have run ruff check and black with no errors
  • New maintained Python files include license headers (python scripts/add_license_headers.py; examples/** exempt)

Testing

  • New test file: tests/cli/test_apply_build_ids_before_tofu.py has 47 cases. On main 9260686, 34 fail and 13 pass (the 13 are controls). All 47 pass on the branch.
    • It drives the real argparse surface. tofu and the apply hooks are recorders, and the real build runner runs behind a spy.
    • It covers both engines, the YAML and plan.json inputs, amend-and-build and replace-and-build, a leading digit, 129 characters, a trailing newline, the --build-id filter, and the CLI rendering through main().
  • Suites for the touched modules: 2142 + 1731 passed. Two failures in tests/providers/test_duckdb_sandbox_workspace_root.py fail the same way on main; they depend on the macOS TMPDIR.
  • Live run of the real fluid CLI, with a recording stand-in tofu first on PATH and no AWS credentials:
    • AWS contract with 1orders: fluid validate passes, then fluid apply --mode amend-and-build is refused. There are 0 tofu calls and no .fluid/iac directory.
    • With orders, the apply reaches tofu --version and tofu init.
    • Native engine: 1orders is refused, and orders runs its build (exit 0).
    • fluid plan writes a plan.json for 1orders; applying that plan is refused before tofu.
  • Lint and policy gates: ruff (whole tree), the dead-code gate, black 24.10.0, license headers, product guardrail, lint-imports and detect-secrets 1.5.0 all pass.
  • Startup budget: tests/perf/test_startup_budget.py gives the same result on main and on the branch, with the same module counts. Its 2 module-count failures in this venv predate this change.
  • Reviews of the diff: a defect scan (one logic and runtime hunter, one security and cross-file-contract hunter) and an independent security review found nothing. They checked that the gate and the runner see the same contract copy, the id boundaries, ids made of escape sequences or Rich markup, redaction, and every IdentifierViolation raiser and handler.
  • After rebasing onto 3a727c9: 371 tests across the apply, id and security suites pass, along with ruff, black and lint-imports.

fas89 added 2 commits November 3, 2026 23:32
`fluid apply --mode amend-and-build` (and replace-and-build) checked
contract.id and builds[].id only in the build runner. On the OpenTofu
engine the runner starts after `tofu apply`, so an id the schema admits
but the build phase refuses (1orders, more than 128 characters, a
trailing newline) was refused after the product was provisioned, and
rendered as "Unexpected error".

The runner's rule is now one function, build_runners/_ids.py
validate_build_ids, and apply runs it right after the schema gate in a
build mode, ahead of the apply hooks, tofu and the runner, on both
engines. The refusal is a typed CLIError, apply_build_identifier_invalid,
that names the field and the value and says nothing was applied. The
runner keeps its own check as the backstop.
@github-actions github-actions Bot added cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes labels Nov 3, 2026
@fas89
fas89 merged commit 8755906 into main Nov 3, 2026
37 checks passed
fas89 added a commit that referenced this pull request Nov 4, 2026
* feat(validate,plan): warn about an id the build phase refuses

The schema's $defs.identifier admits ids the build phase refuses: a
leading digit (1orders) or more than 128 characters. fluid apply in a
build mode refuses such an id before it applies anything (#803), but
fluid validate and fluid plan said nothing, so the first sign was a
failed apply. The two grammars stay different by decision.

For a contract with builds, both now warn and name the id, using the
rule apply uses (build_runners._ids.validate_build_ids):

- validate adds a ValidationResult warning, so plain validate passes
  and --strict fails (founder's call). It runs with the other 0.7.x
  contract rules, on a contract file and on a bundle's resolved
  contract. If the check itself crashes, that is reported as a warning.
- plan prints the warning and still writes plan.json, whatever its
  --mode (founder's call). It runs after the gate plan shares with
  apply, not inside it, so apply keeps its typed CLIError.

IDENT_RULE states the grammar in words, and both the new warning and
apply's hint use it. The hint said validate and plan "accept" such an
id; it now says they only warn about it.

* fix(validate,plan): keep the build-id warning true on a schema-refused id

Review follow-ups to the build-id warning:

- validate runs its contract rules on a contract the schema refused too,
  so the warning could say "The contract schema allows more" about an id
  the schema had just refused (id: 123). The text now only says what the
  build phase needs and that fluid apply in a build mode refuses the id.
- plan prints the warning the way validate prints its warnings: literal,
  soft_wrap (the terminal wraps, so Rich never breaks a line inside the
  id) and no highlighter.
@fas89 fas89 mentioned this pull request Nov 4, 2026
2 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant