Skip to content

Host the BuildScript base: run() and cutRelease() concrete - #138

Merged
thedavidmeister merged 5 commits into
mainfrom
2026-08-18-issue-132-buildscript-base
Aug 18, 2026
Merged

Host the BuildScript base: run() and cutRelease() concrete#138
thedavidmeister merged 5 commits into
mainfrom
2026-08-18-issue-132-buildscript-base

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Closes #132

Items 1 and 2 of the issue: src/abstract/BuildScript.sol hosts run() and
cutRelease() concrete, and script/Build.sol inherits it, so the split this
repo proved out is now the one every deploy repo gets by inheriting rather than
by copying.

The base

abstract contract BuildScript is Script {
    function regenerateSnapshots() internal virtual;    // consumer
    function regenerateLibs() internal virtual;         // consumer
    function snapshotContractNames() internal view virtual returns (string[] memory);  // consumer
    function recordRoot() internal view virtual returns (string memory);  // defaulted

    function run() external { regenerateSnapshots(); regenerateLibs(); }
    function cutRelease() external {
        LibRainDeploySnapshot.freeze(vm, recordRoot(), regenerateSnapshots, snapshotContractNames());
        regenerateLibs();
    }
}

Neither entry point is virtual, so a consumer implements hooks and has no
entry point to cut a release from other than cutRelease().

Three regeneration hooks rather than the issue's one, because freeze takes the
regeneration as a function pointer and runs it between its guards and its
writes, while anything emitted from the record has to be written after the
record exists. One build() hook would have to be called twice — once inside
freeze and once after — and its first call would write libs describing a
record that does not yet hold the release being cut.

recordRoot() is defaulted to LibRainDeploySnapshot.LIB_FS_ROOT and
overridable for the same reason freeze takes a root rather than assuming
one: src/generated/ is append-only, so a cutRelease() that could only be
pointed there could only be tested by cutting this repo's real release.

Behaviour preserved

nix develop -c forge script ./script/Build.sol exits 0 on this branch and,
after forge fmt, leaves the tree clean — git status --porcelain and
git diff --stat both empty. The refactor emits the same bytes the old Build
did. Re-run on the merge commit, so it holds with #126's aggregate
writer in the loop too.

Merged main in

main moved twice under this branch — #126 generates the released-suites
aggregate, #136 moved the release version to foundry's [external] section.
Merged in at 74260ae, never rebased.

One semantic conflict, in script/Build.sol. #126 added
generatedContractNames(), which is the same list this base needs as
snapshotContractNames(), and a third lib writer that reads it for the
aggregate's declaration order. Resolved by making the base's hook that one
function: snapshotContractNames() is both what freeze is handed and what the
aggregate is emitted from, so the frozen record and the aggregate cannot name
different contracts. main's testTheCommittedAggregateIsInDeclarationOrder
and this PR's testSnapshotContractNamesAreTheGeneratedContracts both run in
the merged suite.

What the other deploy repos would have to change

Verified against each repo's live main on 2026-08-18. Issue items 3–5 are
cross-repo and are on the record where the work has to happen:

repo build script entry points release lifecycle rain-deploy regenerate-and-diff in CI
rainlanguage/rain.deploy script/Build.sol run() + cutRelease() rainix-tag-release, --sig "cutRelease()" none — asserted in-suite instead
rainlanguage/rain.factory.deploy script/BuildPointers.sol run() only rainix-tag-release, plain forge script 0.1.3 none
S01-Issuer/st0x.deploy script/BuildPointers.sol run() only rainix-tag-release, bash script/cut-release.sh 0.1.4 hand-rolled git-clean.yaml
rainlanguage/dvin.deploy script/BuildPointers.sol run() only none — ARCHIVED, read-only 0.1.2 rainix-copy-artifacts@main, unreachable

Common to the two live consumers: bump rain-deploy to the release that carries
this base (and rain-sol-codegen to the version it pins), rename
script/BuildPointers.sol to script/Build.sol, and split the current single
run() into the three hooks.

  • rain.factory.deploy — its run() calls
    vm.createDir(string.concat("src/generated/", deployTag())) and writes the
    release snapshot there, so every CI push rewrites the frozen release
    directory. This is the freeze-on-the-CI-path case rainix#273 is about, live.
    It has no candidate/ at all, so adopting the base is also adopting the
    rolling-snapshot layout. No regenerate-and-diff runs in its CI at all.
  • st0x.deploy — already has candidate/ and frozen tags; the freeze lives
    in script/cut-release.sh, a bash reimplementation of
    LibRainDeploySnapshot.freeze with the same three guards. It copies
    candidate/ first and re-runs the generator afterwards, so it freezes what was
    committed rather than what the release compiles — the ordering freeze
    inverts. Moving over deletes that script and points snapshot-generate-cmd at
    --sig "cutRelease()".
  • dvin.deploy — issue item 5 asks whether it is in scope. It is not: the
    repo is ARCHIVED and read-only, last pushed 2026-06-05. gh issue create
    there returns Repository was archived so is read-only, and no push can reach
    its git-clean.yaml. It also has no package-release.yaml and a flat
    src/generated/ with no tag directories, so it has no release record for a
    cutRelease() to append to even in principle.

Both consumers on rainix-tag-release also repoint snapshot-generate-cmd at
forge script ./script/Build.sol --sig "cutRelease()" && forge fmt.

Adversarial mutation pass

Committed before mutating. Filter --match-contract 'BuildTest|BuildScriptTest';
every run reports 10 total tests, printed in the table so a suite that ran
nothing cannot pass for a kill. Neither contract pins a deployed address or a
code hash, so nothing here is killed by a bytecode pin that fails under any
mutation. Logs and diffs per mutant were kept for the run. The whole table was
re-run on the merge commit, against main's aggregate writer, with identical
results.

# mutant result killed by
M1 run(): drop regenerateSnapshots() KILLED (10 ran) testRunRegeneratesAndFreezesNothing
M2 run(): drop regenerateLibs() KILLED (10 ran) testRunRegeneratesAndFreezesNothing
M3 run(): swap the two hooks KILLED (10 ran) testRunRegeneratesAndFreezesNothing
M4 cutRelease(): drop regenerateLibs() KILLED (10 ran) testCutReleaseRegeneratesLibsFromTheRecordJustCut
M5 cutRelease(): regenerate libs BEFORE the freeze KILLED (10 ran) testCutReleaseRegeneratesLibsFromTheRecordJustCut
M6 cutRelease(): hand freeze regenerateLibs as the regeneration KILLED (10 ran) testCutReleaseFreezesTheRegeneratedSnapshot, testCutReleaseRegeneratesLibsFromTheRecordJustCut
M7 recordRoot(): default to src/generated-mutant KILLED (10 ran) testRecordRootDefaultsToTheRepoRecord
M8 cutRelease(): freeze into LIB_FS_ROOT instead of recordRoot() KILLED (10 ran) testCutReleaseFreezesTheRegeneratedSnapshot, testCutReleaseRegeneratesLibsFromTheRecordJustCut
M9 Build.snapshotContractNames(): drop the last contract KILLED (10 ran) testSnapshotContractNamesAreTheGeneratedContracts
M10 Build.regenerateLibs(): LIB_FS_ROOT for recordRoot() SURVIVED — equivalent
M11 Build.regenerateSnapshots(): drop the last contract from the loop SURVIVED

M10 is equivalent: Build does not override recordRoot(), so the two
expressions are the same string. Only a subclass that overrides the root can
tell them apart, and none exists.

M11 survives because nothing in the suite runs Build's real generators —
BuildTest says why: run() and cutRelease() write src/lib/Lib*Released.sol,
which LibRainDeploySnapshotTest also writes, and forge runs test contracts in
parallel. It is caught one step later by the source anchor: a candidate whose
snapshot stops being regenerated fails CandidateSourceMismatch in
RegistryDeploySnapshotTest as soon as that contract's source moves.

The first pass of this mutation run reported M2 and M7 killed by an assertion
against a fixture a previous failing mutant had left on disk — a cheatcode write
is not undone by a revert. The tests now clear their fixture root before they
write to it, and the table above is a rerun with that in place.

QA

  • Discriminating tests: testRunRegeneratesAndFreezesNothing,
    testCutReleaseFreezesTheRegeneratedSnapshot,
    testCutReleaseRegeneratesLibsFromTheRecordJustCut,
    testRecordRootDefaultsToTheRepoRecord,
    testSnapshotContractNamesAreTheGeneratedContracts — none can fail on base,
    because base has no BuildScript and no snapshotContractNames() for them to
    compile against; discrimination is verified against mutants of the shipped
    code instead, in the table above, where each named test fails when its one
    line is broken and passes otherwise.
  • Mutations applied: 11, M1–M11 in the table above — run() drop/swap of each
    hook -> testRunRegeneratesAndFreezesNothing; cutRelease() drop/reorder of
    regenerateLibs() -> testCutReleaseRegeneratesLibsFromTheRecordJustCut;
    freeze handed the wrong regeneration, and freeze pointed at LIB_FS_ROOT
    instead of recordRoot() -> testCutReleaseFreezesTheRegeneratedSnapshot +
    testCutReleaseRegeneratesLibsFromTheRecordJustCut; recordRoot() default
    changed -> testRecordRootDefaultsToTheRepoRecord;
    Build.snapshotContractNames() dropping a contract ->
    testSnapshotContractNamesAreTheGeneratedContracts. 9 killed, 2 survived
    (M10 equivalent, M11 accepted with its compensating check named). Every run
    reports 10 total tests.
  • Oracle: the ordering contract stated in issue Host the BuildScript base here: run() and cutRelease() concrete, and move the other deploy repos onto it #132 and in
    LibRainDeploySnapshot.freeze's own NatSpec — guards, then regeneration, then
    the copy; anything emitted from the record written after the record exists.
    The harness asserts what the record held AT THE MOMENT each hook ran, which is
    derived from that contract rather than from reading the new code. Behaviour
    preservation has a second, independent oracle: main's committed generated
    files, which forge script ./script/Build.sol on this branch reproduces
    byte-for-byte.
  • Category check: issue asks 1 base here, 2 script/Build.sol inherits it,
    3 move rain.factory.deploy + st0x.deploy, 4 rename their
    script/BuildPointers.sol, 5 decide on dvin.deploy. Covered 1 and 2 in this
    diff; 3 and 4 are edits to other repositories and are on the record there
    (rain.factory.deploy#14, st0x.deploy#251); 5 is answered here —
    dvin.deploy is archived and read-only, so it is out of scope.

Suite

nix develop -c forge test on the merge commit: 273 passed, 51 failed, all 51
*_RPC_URL not found; grep '[FAIL' | grep -vc '_RPC_URL. not found' is 0.
The five tests this PR adds are in that count.
nix develop -c forge fmt --check exits 0.

slither ., as rainix-sol-static runs it, needs BuildScript named in
slither.config.json: its unimplemented-functions detector fires on an
abstract with virtual hooks, which is why every other src/abstract/ file is
already in that filter. Added by exact filename, per CLAUDE.md — never by the
src/abstract/ prefix, which would exempt a deployable file added there later.

claude added 2 commits August 18, 2026 12:43
Closes #132

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

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f6eee8b2-e118-4989-9a4a-b61063cd2226

📥 Commits

Reviewing files that changed from the base of the PR and between cc83fe0 and 1259a48.

📒 Files selected for processing (8)
  • README.md
  • script/Build.sol
  • slither.config.json
  • src/abstract/BuildScript.sol
  • test/concrete/BuildHarness.sol
  • test/concrete/BuildScriptHarness.sol
  • test/script/Build.t.sol
  • test/src/abstract/BuildScript.t.sol

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.

…buildscript-base

#140 swept named returns out of the repo (#129: this repo does not name return
values), and #139 pinned the name list against the declaration. Both landed on
the functions this branch renames.

Conflicts, and how they were taken:

* script/Build.sol - main unnamed generatedContractNames()'s return; this
  branch renamed that function to snapshotContractNames() as BuildScript's
  hook. Kept the rename, took main's style: unnamed `returns (string[] memory)`
  with an explicit `return names;`. generatedContracts() was already main's
  unnamed form and came through the automatic merge unchanged.
* test/concrete/BuildHarness.sol - main's externalGeneratedContractNames()
  wrapper and this branch's externalSnapshotContractNames() are the same seam
  onto the renamed function. Kept this branch's, dropped main's: the function
  it called no longer exists.
* test/script/Build.t.sol - merged without markers, but main's
  testGeneratedContractNamesAreTheDeclarationInOrder called the wrapper that
  went. Repointed at externalSnapshotContractNames() and renamed to match, so
  main's positional assertion is not lost. Its doc reference, and the one in
  testTheCommittedAggregateIsInDeclarationOrder, follow the rename.

test/concrete/BuildScriptHarness.sol is not a conflict but its
snapshotContractNames() is added by this branch with a named return, so it is
unnamed here too, with an explicit return.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister
thedavidmeister merged commit 81b619a into main Aug 18, 2026
4 checks passed
thedavidmeister pushed a commit that referenced this pull request Aug 18, 2026
#138 landed after the previous merge, replacing `LIB_FS_ROOT` with the
`recordRoot()` hook while this branch adds `LIB_DIR`. The one conflict, in
`script/Build.sol`, passes both: the writer takes the directory this branch
hands it AND the record root #138 made a hook, matching the five-argument
`writeReleasedSuitesLib(Vm, libDir, recordRoot, contractName, template)`
overload.

No `libDir()` hook was added to `BuildScript` — `regenerateLibs()` is fully
abstract, so the concrete repo picks `LIB_DIR`. That would be a design change
rather than a merge resolution.

Verified: build clean, `forge fmt --check` exit 0, 280 passed / 52 failed with
every failure `*_RPC_URL not found`, `git status` clean and `fixture-lib/` empty
before and after.
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.

Host the BuildScript base here: run() and cutRelease() concrete, and move the other deploy repos onto it

2 participants