Skip to content

ci: currency check sees files regeneration wrote but nothing committed - #315

Open
thedavidmeister wants to merge 1 commit into
mainfrom
2026-08-16-copy-artifacts-untracked
Open

ci: currency check sees files regeneration wrote but nothing committed#315
thedavidmeister wants to merge 1 commit into
mainfrom
2026-08-16-copy-artifacts-untracked

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

What the check missed

The last step of rainix-copy-artifacts.yaml asserted artifact currency with
git diff --exit-code. git diff compares tracked working-tree content against
the index; untracked paths are outside what it reads. So the step caught "a
committed artifact's content drifted" and never caught "regeneration now writes
somewhere else, and the artifact it replaced is still committed".

Measured in a rain.sol.codegen checkout at 7aa85a4 — this comes from the
whole-repo audit at that sha, not from a filed issue. Renaming the generated
contract in script/Build.sol and replaying the copy-artifacts step sequence:

git status --porcelain   ->  ?? src/generated/CodeGennableRenamed.sol
git diff --exit-code     ->  exit 0     <- step reports GREEN
forge test               ->  134 passed

src/generated/CodeGennable.sol stays committed, untouched, and frozen at
whatever bytecode it last held. In a consumer that file is where src/**
imports OPCODE_FUNCTION_POINTERS, INTEGRITY_FUNCTION_POINTERS and
BYTECODE_HASH from, so the currency check's whole purpose is defeated in
exactly the case it exists for.

What the step does now

git add --all
if ! git diff --cached --exit-code; then

Staging first makes the comparison whole-tree. git add --all respects
.gitignore, so out/, cache/ and dependencies/ still contribute nothing,
while additions, deletions and edits all reach the diff. The step is still the
last one in the job, so mutating the index affects nothing downstream.

Deviation from the proposed fix

The audit proposed if [ -n "$(git status --porcelain)" ]. Same detection set,
but staging and diffing is better on two counts, and I took it instead:

  • the failure output stays a real diff — a renamed artifact prints its whole
    body, where --porcelain prints one path and leaves the reader guessing;
  • the step keeps branching on git's own exit code rather than on captured
    command output, which is the shape CLAUDE.md asks for ("bash may
    orchestrate… the moment it branches over data, that logic is Rust").

Test

New: test/bats/workflow/copy-artifacts-currency.test.bats, registered in
default-shell-test so check-shell.yml runs it. It extracts the step's run
body straight out of the workflow YAML with yq — the same technique
test/bats/action/rpc-preflight.test.bats already uses — and executes it against
throwaway git repos that stand in for a consumer checkout after regeneration.

Before the fix (the untracked case is the defect; the other four already held):

1..5
ok 1 a checkout whose regeneration changed nothing passes
ok 2 a committed artifact whose content drifted fails
not ok 3 an artifact regenerated under a new name, leaving the old one committed, fails
# (in test file test/bats/workflow/copy-artifacts-currency.test.bats, line 52)
#   `[ "$status" -eq 1 ]' failed
ok 4 a committed artifact that regeneration no longer emits fails
ok 5 build output the consumer gitignores passes

After:

1..5
ok 1 a checkout whose regeneration changed nothing passes
ok 2 a committed artifact whose content drifted fails
ok 3 an artifact regenerated under a new name, leaving the old one committed, fails
ok 4 a committed artifact that regeneration no longer emits fails
ok 5 build output the consumer gitignores passes

Mutation matrix

Each mutant edits the step in the workflow YAML, runs the bats file, and is
reverted. Every run printed the 1..5 plan line and five result lines, so the
suite demonstrably ran rather than erroring out into a false "survived".

mutant killed by result
drop git add --all tests 2, 3, 4 KILLED
git diff --cached -> git diff tests 2, 3, 4 KILLED
git add --all -> git add --all --force (stop honouring .gitignore) test 5 KILLED

No mutant survived.

Consumer verification

The stricter check only earns its place if it does not red existing consumers
for unrelated reasons. Every repo in the org that calls this reusable was
enumerated by sweeping .github/workflows/** across 229 non-archived repos in
rainlanguage, cyclofinance, S01-Issuer, ST0x-Technology, h20liquidity,
gildlab and raincommercial — ten consumers, listed below.

All ten were cloned fresh and driven through the job's step sequence locally in
nix develop github:rainlanguage/rainix/53e96a7…#sol-shell, then both the old
and the new check were run on the resulting tree. Nine reach the check; the
tenth fails an earlier gate.

consumer old check git status --porcelain after regeneration new check
rain.sol.codegen @ 935c725 0 empty 0
rain.flare @ b634aad 0 empty 0
rain.erc4626.words @ e02d60e 0 empty 0
rain.metadata @ 382d44e 0 empty 0
rainlang @ a6b7ad7 0 empty 0
raindex @ 16049a3 0 empty 0
rain.dia @ 35cf7a5 0 ?? .pre-commit-config.yaml 1
rain.merkle @ ca6d324 0 ?? .pre-commit-config.yaml 1
rain.pyth @ 9f9cd8c 0 ?? .pre-commit-config.yaml 1
rain.math.float @ d3fb611 job already fails earlier, see below

raindex is the strongest of the clean results: 76 committed files under
src/generated, plus both the script/build-meta.sh and script/build.sh
hooks, and the tree comes out with nothing untracked at all.

The three that go red, and why the file is ours

The three failures are one file, and it is rainix's own. Entering any rainix devshell
runs the git-hooks installation script, which writes .pre-commit-config.yaml
into whatever repo is the working directory. Proven directly, in a rain.merkle
checkout that had never seen the file:

$ nix develop github:rainlanguage/rainix/53e96a7…#sol-shell -c true
git-hooks.nix: updating …/rain.merkle repo
…/rain.merkle/.pre-commit-config.yaml
pre-commit installed at .git/hooks/pre-commit
$ git status --porcelain
?? .pre-commit-config.yaml

The job's very first step enters that shell, so every consumer's checkout gets
the file. Seven of the ten consumers list it in .gitignore and are unaffected.
Three do not — rain.dia, rain.merkle, rain.pyth — and those are exactly
the three that go red in the table, with nothing else in their staged diff.

This is not new behaviour and this PR does not cause it — the pollution has
happened on every copy-artifacts run all along, and git diff simply could not
see it. Making the check honest is what surfaces it.

I have deliberately not weakened the check to step around this, and have not
touched the consumer repos. The fix belongs at the source, and there are two
places it could sit — one line of .gitignore in each of the three repos, which
is what the other seven already do, or rainix's devshells not installing hooks
into a tree they were invoked in non-interactively. That call, and the ordering
against this PR, is a human's.

rain.math.float

Not a regression from this PR either: its job never reaches the currency check.
It commits src/generated/LogTables.pointers.sol with script/BuildPointers.sol
rather than script/Build.sol, so it fails the existing script/Build.sol gate.
Its last green copy-artifacts run was 2026-07-15T13:17:50Z; the gate reached
rainix main at 2026-07-15T13:51Z, 34 minutes later. That green predates the
gate and will not survive the repo's next push, with or without this change.

Enumerated consumers

raindex, rain.dia, rain.erc4626.words, rain.flare, rainlang,
rain.math.float, rain.merkle, rain.metadata, rain.pyth,
rain.sol.codegen.

QA

  • Discriminating tests: test/bats/workflow/copy-artifacts-currency.test.bats — "an artifact regenerated under a new name, leaving the old one committed, fails" fails on base (run against the unmodified workflow via nix develop -c bats …: not ok 3 … [ "$status" -eq 1 ]' failed, the other four ok); it passes after the change, 1..5 all ok. The other four tests pin the behaviour the step already had — drift, deletion, clean tree, gitignored build output — so the widening cannot quietly cost any of it.
  • Mutations applied: git add --all deleted -> killed by tests 2, 3, 4; git diff --cached --exit-code -> git diff --exit-code -> killed by tests 2, 3, 4; git add --all -> git add --all --force -> killed by test 5. Three mutants, zero survivors, each applied to the workflow YAML and reverted. Every mutant run printed the 1..5 plan and five result lines, so no run was a harness error masquerading as a survivor.
  • Oracle: git's own documented semantics, not the step's. git diff reads tracked content against the index and does not consider untracked paths; git add --all stages additions, deletions and edits while honouring .gitignore. Each test asserts the outcome derived from that, and the expectations were confirmed independently against scratch repositories before the workflow was touched. The orphaned-artifact scenario itself is the one measured in rain.sol.codegen at 7aa85a4, where git diff --exit-code returned 0 on an untracked regenerated file.
  • Category check: the audit finding names one defect — the currency check is blind to untracked files — and asks that additions and deletions both be caught while .gitignore is still respected, and that the stricter check be verified against real consumers before pushing. Covered: additions (test 3), deletions (test 4), edits (test 2), .gitignore respected (test 5), and all ten consumers enumerated and replayed end to end (table above). The four consumers that do not come out clean are diagnosed above and none of it is caused by this change. The finding's adjacent note that rainix#313 covers hardcoded src/generated in this same file is out of scope here and untouched.
  • Consumer replay: all ten consumers cloned fresh and driven through the job's step sequence in the pinned sol-shell, recording the old check, git status --porcelain and the new check for each — six clean, three carrying only rainix's own .pre-commit-config.yaml, one failing an earlier pre-existing gate.
  • Static/format: nix develop -c pre-commit run --all-files — nixfmt, no-consumer-prettier, prettier-rainix, shellcheck, statix, taplo and yamlfmt all pass, and no hook rewrote a file (git status --porcelain empty afterwards). rustfmt-conditional fails with could not find Cargo.toml in …/rainix; it fails identically on a clean origin/main at 7f223b4, so it is pre-existing and unrelated to this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Aug 16, 2026
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d83fc6e7-4179-4a7d-a2bd-570e8f40a844

📥 Commits

Reviewing files that changed from the base of the PR and between 7f223b4 and d57f7fc.

📒 Files selected for processing (3)
  • .github/workflows/rainix-copy-artifacts.yaml
  • flake.nix
  • test/bats/workflow/copy-artifacts-currency.test.bats

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The workflow now stages non-ignored changes before comparing generated artifacts. New Bats tests cover modified, added, deleted, unchanged, and ignored artifacts. The test runs in the default shell test suite.

Changes

Artifact currency verification

Layer / File(s) Summary
Stage and compare generated artifacts
.github/workflows/rainix-copy-artifacts.yaml
The workflow stages non-ignored changes and checks the staged diff for modified, added, and deleted generated artifacts. Its documentation and failure message cover committed and untracked artifacts.
Validate currency-check scenarios
test/bats/workflow/copy-artifacts-currency.test.bats, flake.nix
The isolated Bats fixture tests unchanged, modified, renamed, deleted, and ignored artifacts. The test is included in the default shell test task.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to d57f7

The change makes artifact currency checks include added and deleted generated files while preserving ignored build output, with focused tests covering the behavior. No actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: the currency check detects regenerated files that are not committed.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-16-copy-artifacts-untracked

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant