diff --git a/.github/workflows/rainix-copy-artifacts.yaml b/.github/workflows/rainix-copy-artifacts.yaml index 1157a67..0f8c1d6 100644 --- a/.github/workflows/rainix-copy-artifacts.yaml +++ b/.github/workflows/rainix-copy-artifacts.yaml @@ -22,8 +22,9 @@ jobs: if: hashFiles('soldeer.lock') != '' run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge soldeer install # Currency-check every committed generated artifact by re-running each - # consumer-provided codegen step. The final git diff fails if any - # committed file has drifted from its source. The build-meta.sh hook is + # consumer-provided codegen step. The final currency check fails if any + # committed file has drifted from its source, or if regeneration wrote a + # file the repo does not track. The build-meta.sh hook is # consumer-supplied because rain meta build's invocation (input/output # filenames, meta type) varies per repo. - name: Regenerate meta artifacts @@ -55,9 +56,17 @@ jobs: run: ./script/build.sh - name: Format (so generated artifacts match committed style) run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge fmt + # Staging first is what makes this a whole-tree comparison. `git diff` + # alone reads tracked content only, so a regeneration that writes to a + # path nothing has committed yet — a renamed contract, an added artifact — + # is invisible to it, while the artifact it supersedes stays committed and + # frozen at whatever the consumer's `src/**` still imports. `git add --all` + # respects .gitignore, so out/, cache/ and dependencies/ contribute + # nothing, and every addition, deletion and edit reaches the diff. - name: Assert committed artifacts match freshly built run: | - if ! git diff --exit-code; then - echo "::error::Committed artifacts are stale. Regenerate (script/build-meta.sh, script/Build.sol, script/CopyArtifacts.sol, script/build.sh, forge fmt) and commit." + git add --all + if ! git diff --cached --exit-code; then + echo "::error::Committed artifacts are stale, or regeneration wrote a file that is not committed. Regenerate (script/build-meta.sh, script/Build.sol, script/CopyArtifacts.sol, script/build.sh, forge fmt) and commit." exit 1 fi diff --git a/flake.nix b/flake.nix index c390c0b..c89f1c3 100644 --- a/flake.nix +++ b/flake.nix @@ -448,6 +448,7 @@ bats test/bats/devshell/default/prettier-bundle.test.bats bats test/bats/action/rpc-preflight.test.bats bats test/bats/action/prompt-cap.test.bats + bats test/bats/workflow/copy-artifacts-currency.test.bats bats test/bats/task/skip-simulation.test.bats bats test/bats/task/subgraph-build.test.bats bats test/bats/task/subgraph-deploy-version.test.bats diff --git a/test/bats/workflow/copy-artifacts-currency.test.bats b/test/bats/workflow/copy-artifacts-currency.test.bats new file mode 100644 index 0000000..4f76dae --- /dev/null +++ b/test/bats/workflow/copy-artifacts-currency.test.bats @@ -0,0 +1,76 @@ +setup() { + repo_root="$BATS_TEST_DIRNAME/../../.." + workflow="$repo_root/.github/workflows/rainix-copy-artifacts.yaml" + assert_script="$(yq -r '.jobs["copy-artifacts"].steps[] | select(.name == "Assert committed artifacts match freshly built") | .run' "$workflow")" + + consumer="$(mktemp -d)" + # A consumer checkout is the only git state the step reads, so the fixture + # owns its whole git environment: no ambient config, no ambient excludes. + export GIT_CONFIG_NOSYSTEM=1 + export HOME="$consumer" + cd "$consumer" || return 1 + git init -q -b main . + git config user.email rainix@example.com + git config user.name rainix + printf 'out/\ncache/\ndependencies/\n' >.gitignore + mkdir -p src/generated + printf 'library CodeGennable {}\n' >src/generated/CodeGennable.sol + git add --all + git commit -qm 'committed artifacts' +} + +teardown() { + cd / || return 0 + rm -rf "$consumer" +} + +run_assert() { + bash -c "$assert_script" +} + +@test "a checkout whose regeneration changed nothing passes" { + run run_assert + + [ "$status" -eq 0 ] +} + +@test "a committed artifact whose content drifted fails" { + printf 'library CodeGennable { uint256 constant X = 1; }\n' >src/generated/CodeGennable.sol + + run run_assert + + [ "$status" -eq 1 ] + [[ "$output" == *"CodeGennable.sol"* ]] + [[ "$output" == *"::error::"* ]] +} + +@test "an artifact regenerated under a new name, leaving the old one committed, fails" { + printf 'library CodeGennableRenamed {}\n' >src/generated/CodeGennableRenamed.sol + + run run_assert + + [ "$status" -eq 1 ] + [[ "$output" == *"CodeGennableRenamed.sol"* ]] + [[ "$output" == *"::error::"* ]] +} + +@test "a committed artifact that regeneration no longer emits fails" { + rm src/generated/CodeGennable.sol + + run run_assert + + [ "$status" -eq 1 ] + [[ "$output" == *"CodeGennable.sol"* ]] + [[ "$output" == *"::error::"* ]] +} + +@test "build output the consumer gitignores passes" { + mkdir -p out cache dependencies + printf '{}\n' >out/CodeGennable.json + printf '{}\n' >cache/solidity-files-cache.json + printf 'library Dep {}\n' >dependencies/Dep.sol + + run run_assert + + [ "$status" -eq 0 ] +}