ci: run codegen to a fixed point, with a bound, instead of once - #320
ci: run codegen to a fixed point, with a bound, instead of once#320thedavidmeister wants to merge 2 commits into
Conversation
Generated sources are inputs to their own generation, so one pass of the regeneration pipeline applies the generation function rather than reaching its fixed point. Run once, a tree several passes behind and a generation cycle that never settles both reach the currency check as "stale". `rainix-static codegen-fixed-point` loops the pipeline until the working tree stops changing, observing the tree as a git tree object built in a scratch index so .gitignore applies, untracked output counts, and the repo's own index is left for the currency check. `max-codegen-passes` bounds it; exhausting the bound fails with its own error. Closes #314 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The loop's own semantics were unit-tested, but nothing exercised the wiring between the workflow input, the pinned flake refs and `rainix-static codegen-fixed-point`. The new bats suite drives the step's own `run:` text, read out of the workflow with `yq`, against a consumer checkout with `nix` and `forge` stubbed as files on PATH so the real binary reaches them. A single-shot pipeline, a hardcoded bound, `forge fmt` hoisted out of the loop, an unpinned flake ref, a loop that leaves the tree staged, a skipped optional hook and a changed yaml default are each killed by it. Observing the tree reused the scratch index rather than deleting it per pass. `git add --all` is defined to reconcile removals as well as content, so the tree it writes describes the working tree as it is now; deleting the index each pass was doing the same work at the cost of the guarantee being restated. A pass that only deletes a file now has a test, because that is the case a `git add --no-all .` regression silently converges on pass 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 3 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Closing in favour of #319, which closes #314 and subsumes this. Verified against both trees rather than taken on the claim:
This branch has a fatal defect that #319 fixes. The step runs with My 11th test actively enshrined that defect: "every flake ref in the step is pinned to the workflow's sha" asserts the broken invariant, and the suite structurally cannot catch it — the The one thing here that #319 lacks on the remote at The mutation matrix in the body above was measured on this branch and mostly transfers; the row for the unpinned-flake-ref mutant does not, since #319 deliberately changes that invariant. Branch |
Closes #314
Problem
Generated sources are inputs to their own generation — a pointer table is imported by
the contract whose codehash that same table records — so one pass of the regeneration
pipeline applies the generation function rather than reaching its fixed point.
rainix-copy-artifactsran the pipeline exactly once and diffed, so a tree severalpasses behind and a generation cycle that never settles both arrived at the currency
check as the same thing: "stale, regenerate and commit". Followed literally once, that
message produces a still-stale tree and a still-red run, and the tempting resolution —
commit whichever pass happened to diff clean — is exactly the inconsistent state where
BYTECODE_HASHnames a contract compiled against a different pass of the same file.Change
rainix-static codegen-fixed-pointrepeats the whole pipeline until the working treestops changing.
max-codegen-passes(default5) bounds it; exhausting the bound failswith its own error, distinct from the currency check's. A repo already at its fixed point
pays exactly one pass.
The tree is observed as a git tree object written through a scratch index, not a
git diff. That compares content rather than paths, respects.gitignore, countsuntracked output, and leaves the repo's own index alone for the currency check that
follows. The scratch index is reused across observations rather than deleted per pass,
because
git add --allis defined to reconcile removals as well as content — the tree itwrites describes the working tree as it is now, not the union of every pass so far.
Deviation from the issue's sketch
The issue proposes inline bash:
git add -Athen[ -z "$(git status --porcelain)" ].That never converges —
git status --porcelainreports staged changes against HEAD, soonce
git add -Ahas staged anything the emptiness test can no longer come back true. Italso stages the consumer's tree, which is the input the currency check downstream needs
left alone. Hence the Rust binary, which
CLAUDE.mdalready binds real work to.QA
Discriminating tests: the 11-test bats suite
test/bats/workflow/copy-artifacts-fixed-point.test.bats, plus Rustcodegen_fixed_point::tests::{a_pass_that_deletes_a_file_counts_as_a_change, the_repos_own_index_is_left_for_the_currency_check_to_stage, gitignored_build_output_does_not_look_like_a_moving_tree, a_directory_that_is_not_a_repo_is_an_error_not_a_pass}— each fails on base (verified bygit checkout origin/main -- .github/workflows/rainix-copy-artifacts.yamland re-running: all 11 bats reportnot ok, the step the suite reads withyqnot existing onorigin/main; the Rust tests fail on base because thecodegen_fixed_pointmodule does not exist there at all). Harness honesty was checked first by stubbing the binary under test toexit 77: 9 of 11 bats fail, and the two that pass (6 and 11) are the pure workflow-text assertions that never invoke it — so the suite is really executing the binary, not asserting against a shell that silently no-ops.Mutations applied: every mutant below was introduced, run, and reverted by me; each was asserted to actually change the file first, and each run was asserted to have executed a non-zero number of tests, so "survived" cannot be a no-op or an empty filter.
codegen-fixed-pointwrapper → single-shot pipeline (the issue's exact defect)--max-passes ${{ inputs.max-codegen-passes }}→--max-passes 5forge fmthoisted out of the--runbody.../${{ env.RAINIX_SHA }}#sol-shell -c forge build→.../main#sol-shellif [ -f script/build.sh ]→if truemax-codegen-passes.default: 5→9git()drops.env("GIT_INDEX_FILE", index)→ loop uses the real indexthe_repos_own_index_is_left_for_the_currency_check_to_stage; bats 8git add --all→git add --all --forcegitignored_build_output_does_not_look_like_a_moving_treegit add --all→git add --no-all .a_pass_that_deletes_a_file_counts_as_a_change(only this one)root.display()a_directory_that_is_not_a_repo_is_an_error_not_a_passlet _ = std::fs::remove_file(index);insnapshot()The last row is not a coverage gap. That line surviving is the proof it was dead: with
the scratch index reused,
--allalready reconciles removals, so deleting the indexdid nothing. It is dropped by this PR rather than covered by a test. The
--no-all .row is why the deletion test exists — it is the regression that silently converges on
pass 1 and hands the currency check a tree it never watched settle, and nothing else
in the suite catches it.
git add --all --ignore-removaland baregit add --no-allwere tried and rejected as invalid mutants: git errors or no-ops, killing tests for
the wrong reason.
Oracle: the invariant is
rain.sol.codegen'sREADME.md:18-21and this issue — pointers feed back into the contracts whose codehash the same file records, so generation must repeat to a fixed point — derived independently of this implementation. The bats suite reads its expected values out of the workflow withyq(the step's ownrun:text, andmax-codegen-passes.default) rather than restating them, so the bound under test is the one a consumer gets when they pass nowith:at all; test 6 then pins that read value to 5. Tree equality is git's own tree-object id, not a hand-rolled directory walk, and pass counting is done outside the checkout so counting cannot itself look like a tree that keeps moving.Category check: the issue asks for (A) the loop in the machine, (B) with a bound, (C) a non-converging build that says so rather than blaming the developer, and (D) a currency check comparing the committed tree against the converged output rather than one iteration. Covered A (bats 1-3), B (bats 5, 6), C (bats 4 asserts
::error::and the "cycle that does not settle" wording, distinct from the staleness message), D (bats 8 pins that the loop leaves the index for the currency check). The issue's yaml sketch is deliberately not followed; that deviation and its reason are stated above.Also run:
cargo test121 passed / 0 failed;cargo clippy --all-targets --all-features -- -D warnings -D clippy::allandcargo fmt --all -- --checkclean. Pre-existing and confirmed pre-existing by running them against a cleanorigin/main@7f223b4rather than assumed: threeprettier-bundlefailures indefault-shell-test, andpre-commit run --all-filesfailingrustfmt(its glob matches*/Cargo.tomlthen runscargo-fmtfrom the repo root, where there is none).Interaction with open PRs
git add --all+git diff --cached --exit-code. Semantically compatible: this loop deliberately leaves the real indexuntouched, and bats test 8 pins exactly that. Expect a textual conflict only in
flake.nix'sdefault-shell-testlist, where ci: currency check sees files regeneration wrote but nothing committed #315 addscopy-artifacts-currency.test.batsone line from this one — keep both.
rainix-copy-artifacts.yamlandflake.nix; no README overlap.The README hunk here correcting
./script/BuildPointers.sol→./script/Build.solandsrc/generated/*.solis a drive-by correction of text that was already wrong on main,and ci: one canonical generated-sources dir for all three mechanisms #318 may want to fold the directory name into its own rename.
🤖 Generated with Claude Code