Host the BuildScript base: run() and cutRelease() concrete - #138
Conversation
Closes #132 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
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 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: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
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 |
…buildscript-base # Conflicts: # script/Build.sol
…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>
#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.
Closes #132
Items 1 and 2 of the issue:
src/abstract/BuildScript.solhostsrun()andcutRelease()concrete, andscript/Build.solinherits it, so the split thisrepo proved out is now the one every deploy repo gets by inheriting rather than
by copying.
The base
Neither entry point is
virtual, so a consumer implements hooks and has noentry point to cut a release from other than
cutRelease().Three regeneration hooks rather than the issue's one, because
freezetakes theregeneration 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 insidefreezeand once after — and its first call would write libs describing arecord that does not yet hold the release being cut.
recordRoot()is defaulted toLibRainDeploySnapshot.LIB_FS_ROOTandoverridable for the same reason
freezetakes arootrather than assumingone:
src/generated/is append-only, so acutRelease()that could only bepointed there could only be tested by cutting this repo's real release.
Behaviour preserved
nix develop -c forge script ./script/Build.solexits 0 on this branch and,after
forge fmt, leaves the tree clean —git status --porcelainandgit diff --statboth empty. The refactor emits the same bytes the oldBuilddid. Re-run on the merge commit, so it holds with #126's aggregate
writer in the loop too.
Merged
maininmainmoved twice under this branch — #126 generates the released-suitesaggregate, #136 moved the release version to foundry's
[external]section.Merged in at
74260ae, never rebased.One semantic conflict, in
script/Build.sol. #126 addedgeneratedContractNames(), which is the same list this base needs assnapshotContractNames(), and a third lib writer that reads it for theaggregate's declaration order. Resolved by making the base's hook that one
function:
snapshotContractNames()is both whatfreezeis handed and what theaggregate is emitted from, so the frozen record and the aggregate cannot name
different contracts.
main'stestTheCommittedAggregateIsInDeclarationOrderand this PR's
testSnapshotContractNamesAreTheGeneratedContractsboth run inthe merged suite.
What the other deploy repos would have to change
Verified against each repo's live
mainon 2026-08-18. Issue items 3–5 arecross-repo and are on the record where the work has to happen:
rain.factory.deploy— Move script/BuildPointers.sol onto rain.deploy's BuildScript, as script/Build.sol rain.factory.deploy#14st0x.deploy— Convert BuildPointers to the shared LibSnapshot (drop the local copy + reconcile deployTags divergence) S01-Issuer/st0x.deploy#251, which alreadyasked for this against
rain-sol-codegen's deletedLibSnapshot; redirectedthere rather than duplicated.
dvin.deploy— item 5, answered below: the repo is archived.rain-deployrainlanguage/rain.deployscript/Build.solrun()+cutRelease()rainix-tag-release,--sig "cutRelease()"rainlanguage/rain.factory.deployscript/BuildPointers.solrun()onlyrainix-tag-release, plainforge scriptS01-Issuer/st0x.deployscript/BuildPointers.solrun()onlyrainix-tag-release,bash script/cut-release.shgit-clean.yamlrainlanguage/dvin.deployscript/BuildPointers.solrun()onlyrainix-copy-artifacts@main, unreachableCommon to the two live consumers: bump
rain-deployto the release that carriesthis base (and
rain-sol-codegento the version it pins), renamescript/BuildPointers.soltoscript/Build.sol, and split the current singlerun()into the three hooks.rain.factory.deploy— itsrun()callsvm.createDir(string.concat("src/generated/", deployTag()))and writes therelease 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 therolling-snapshot layout. No regenerate-and-diff runs in its CI at all.
st0x.deploy— already hascandidate/and frozen tags; the freeze livesin
script/cut-release.sh, a bash reimplementation ofLibRainDeploySnapshot.freezewith the same three guards. It copiescandidate/first and re-runs the generator afterwards, so it freezes what wascommitted rather than what the release compiles — the ordering
freezeinverts. Moving over deletes that script and points
snapshot-generate-cmdat--sig "cutRelease()".dvin.deploy— issue item 5 asks whether it is in scope. It is not: therepo is ARCHIVED and read-only, last pushed 2026-06-05.
gh issue createthere returns
Repository was archived so is read-only, and no push can reachits
git-clean.yaml. It also has nopackage-release.yamland a flatsrc/generated/with no tag directories, so it has no release record for acutRelease()to append to even in principle.Both consumers on
rainix-tag-releasealso repointsnapshot-generate-cmdatforge 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 rannothing 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 identicalresults.
run(): dropregenerateSnapshots()testRunRegeneratesAndFreezesNothingrun(): dropregenerateLibs()testRunRegeneratesAndFreezesNothingrun(): swap the two hookstestRunRegeneratesAndFreezesNothingcutRelease(): dropregenerateLibs()testCutReleaseRegeneratesLibsFromTheRecordJustCutcutRelease(): regenerate libs BEFORE the freezetestCutReleaseRegeneratesLibsFromTheRecordJustCutcutRelease(): handfreezeregenerateLibsas the regenerationtestCutReleaseFreezesTheRegeneratedSnapshot,testCutReleaseRegeneratesLibsFromTheRecordJustCutrecordRoot(): default tosrc/generated-mutanttestRecordRootDefaultsToTheRepoRecordcutRelease(): freeze intoLIB_FS_ROOTinstead ofrecordRoot()testCutReleaseFreezesTheRegeneratedSnapshot,testCutReleaseRegeneratesLibsFromTheRecordJustCutBuild.snapshotContractNames(): drop the last contracttestSnapshotContractNamesAreTheGeneratedContractsBuild.regenerateLibs():LIB_FS_ROOTforrecordRoot()Build.regenerateSnapshots(): drop the last contract from the loopM10 is equivalent:
Builddoes not overriderecordRoot(), so the twoexpressions 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 —BuildTestsays why:run()andcutRelease()writesrc/lib/Lib*Released.sol,which
LibRainDeploySnapshotTestalso writes, and forge runs test contracts inparallel. It is caught one step later by the source anchor: a candidate whose
snapshot stops being regenerated fails
CandidateSourceMismatchinRegistryDeploySnapshotTestas 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
testRunRegeneratesAndFreezesNothing,testCutReleaseFreezesTheRegeneratedSnapshot,testCutReleaseRegeneratesLibsFromTheRecordJustCut,testRecordRootDefaultsToTheRepoRecord,testSnapshotContractNamesAreTheGeneratedContracts— none can fail on base,because base has no
BuildScriptand nosnapshotContractNames()for them tocompile 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.
run()drop/swap of eachhook ->
testRunRegeneratesAndFreezesNothing;cutRelease()drop/reorder ofregenerateLibs()->testCutReleaseRegeneratesLibsFromTheRecordJustCut;freezehanded the wrong regeneration, andfreezepointed atLIB_FS_ROOTinstead of
recordRoot()->testCutReleaseFreezesTheRegeneratedSnapshot+testCutReleaseRegeneratesLibsFromTheRecordJustCut;recordRoot()defaultchanged ->
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.LibRainDeploySnapshot.freeze's own NatSpec — guards, then regeneration, thenthe 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 generatedfiles, which
forge script ./script/Build.solon this branch reproducesbyte-for-byte.
script/Build.solinherits it,3 move
rain.factory.deploy+st0x.deploy, 4 rename theirscript/BuildPointers.sol, 5 decide ondvin.deploy. Covered 1 and 2 in thisdiff; 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.deployis archived and read-only, so it is out of scope.Suite
nix develop -c forge teston 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 --checkexits 0.slither ., asrainix-sol-staticruns it, needsBuildScriptnamed inslither.config.json: itsunimplemented-functionsdetector fires on anabstract with virtual hooks, which is why every other
src/abstract/file isalready in that filter. Added by exact filename, per
CLAUDE.md— never by thesrc/abstract/prefix, which would exempt a deployable file added there later.