Migrate to the rolling-candidate snapshot model - #5
Conversation
BuildPointers now writes src/generated/candidate/ unconditionally instead of a dir named after [package].version, and LibCloneFactoryDeploy aliases it, so the pins consumers import always describe the current source. script/cut-release.sh freezes candidate as src/generated/<tag>/ at release time and is the only thing that creates a numbered snapshot. LibCloneFactoryDeployCandidateTest ties the candidate snapshot to the source, to its own recorded address, and to the lib. The three existing frozen snapshots are untouched: 0_1_5 is the live headline pin and 0_1_3/0_1_4 hold the audited bytecode, all live on all five chains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 29 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. 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 ignored due to path filters (1)
📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
|
CI landed exactly as predicted at
That is an RPC quota on the |
|
🤖 ai:vetter |
|
🤖 ai:producer |
Migrates this repo to the rolling-candidate model that
S01-Issuer/st0x.deploy#283
adopted, mirroring it rather than inventing a variant. Adapted to this repo's
shape: one contract, one generated pin lib, no multi-contract alias machinery.
What changes
src/generated/candidate/is new and rolling.script/BuildPointers.solnow writes it unconditionally —
deployTag()went fromvm.parseTomlString(vm.readFile("foundry.toml"), ".package.version")to apurefunction returning the constant
"candidate". It is regenerated in full on everyrun and committed, so it always describes what the current source compiles to.
src/lib/LibCloneFactoryDeploy.solaliasescandidate/instead of0_1_5/.This is a semantic no-op today:
candidate/CloneFactory.pointers.solisbyte-identical to
0_1_5/CloneFactory.pointers.sol(diffis empty), because0_1_5is what the current source compiles to. Same address0x7053c1D000192f1FeC1fbe20350139F3300dBBaD, same codehash. What changes is therule: the consumer-facing pin now tracks the source instead of tracking a
version number.
script/cut-release.shis new — the direct analogue of st0x's. Itcp -rscandidatetosrc/generated/<tag>/, refusing on a non-X.Y.Zversion (theappend-only gate's tag filter is numeric-only, so a
0_1_7-rc1dir would be anorphan snapshot nothing protects) and refusing to overwrite an existing tag dir.
package-release.yaml'ssnapshot-generate-cmdbecomesbash script/cut-release.sh; the rest of the release wiring was already correct(
on: push: tags: sol-v*→rainix-tag-release) and is untouched.testCandidateSelfConsistentreplaces the version↔snapshot coupling. Newtest/src/lib/LibCloneFactoryDeployCandidate.t.solpins the whole chain:[package].versionis no longer read by any Solidity in this repo, so the{ access = "read", path = "./foundry.toml" }fs permission is dropped too.Frozen snapshots: all three kept
0_1_5MUST survive and does. It is the repo's headline pin, live on allfive supported chains, and the only implementation the current source can build
(
ICloneableFactoryV3—cloneDeterministic,keccak256(abi.encode(deployer, salt))namespacing, 5-argNewClone). It is unaudited, and st0x#283's "keeponly the audited snapshot" move would have deleted it. Deleting it would delete
the reason this repo exists. It stays, unaudited, on purpose.
0_1_3/0_1_4also stay. They hold the Protofire-auditedICloneableFactoryV2bytecode (0x444acC29d63fa643E8adCC35FD9aa6DE111dCb39),which is also live on all five chains and is the pin
S01-Issuer/st0x.deployconsumes in production (via the
rain-factory0.1.1 Soldeer package).So this migration touches nothing under
src/generated/0_1_*. Simulating thegate's own diff against
main:candidateis invisible to the gate by construction —rainix-static/src/frozen_snapshots.rs:19-25is_tag()requires exactly three_-separated all-numeric parts, and"candidate".split('_')has one. So it canroll forever without any rainix change.
candidate/was generated fresh, nevergit mv'd from a numbered dir (the gate passes--no-renames, so a move wouldhave been recorded as a deletion and flagged).
0_1_4is a byte-identical duplicate — and I am leaving it0_1_3and0_1_4are the same file (sha256fa92b0caa1084172240189245d1f6f7f018a56bec1f2c3d884ffe0ee358d225afor both) —one bytecode snapshotted twice by autopublish version bumps, exactly the noise
#283 deleted from st0x. Dropping it would break nothing functional: no Soldeer
consumer exists (this repo has zero published revisions), no git tag names
it, its address is identical to
0_1_3's so no deployment record would be lost,and the only code referencing it is its own test pair in
LibCloneFactoryDeployTaggedConstants.t.sol.But the
frozen-snapshots-append-onlygate would block it.parse_offendersflags statusDas well asM, so deletingsrc/generated/0_1_4/CloneFactory.pointers.solproduces an offender and turnsrainix-sol / staticred. st0x#283 got past exactly this by being mergedred under a one-off human sign-off. I am not doing that here: it is a separate
decision from the model migration, and it would trade a green PR for a cosmetic
cleanup.
0_1_4stays. If you want it collapsed, that is its owndeliberately-red PR with its own sign-off.
Supersedes #4
#4 (
17b69fb,"Enforce that [package].version, its snapshot and the deploy lib agree")
implements the model this PR removes. It should be closed when this merges.
Its three assertions, disposed of individually:
testDeployTag—DEPLOY_TAG == foundry.toml [package].version. Dropped,not folded. The candidate model deliberately abolishes that invariant:
DEPLOY_TAGis now the literal"candidate"and the version names nothing ondisk, so the assertion would be false by design. This is also the weakness
that motivates the change — it compares a name to a name, so editing
CloneFactorywithout regenerating leaves it green while the pins silentlymisdescribe the source.
testDeployTagSnapshotExists— folded as a compile-time guarantee. Thecandidate test imports
../../../src/generated/candidate/CloneFactory.pointers.soldirectly, so amissing snapshot is a compile error, not a runtime assertion. Enforce that [package].version, its snapshot and the deploy lib agree #4 needed
vm.existsonly because itsDEPLOY_TAGstring was decoupled from anyimport.
testDeployTagSnapshotHoldsTheAliasedPins— folded, and strengthened.Enforce that [package].version, its snapshot and the deploy lib agree #4 grepped the snapshot's file text for the rendered literals. The
equivalent here is
testCandidateIsTheAliasedSnapshot, which asserts theidentity in Solidity against the imported constants — no file parsing — and it
catches the real hazard Enforce that [package].version, its snapshot and the deploy lib agree #4 was aiming at: the generated lib left pointing at a
frozen dir while
candidatemoves on. Mutation-proven below.The
DEPLOY_TAGconstant itself — #4's genuinely good idea, making the aliasedsnapshot readable without reading an import path — is kept, generated as
"candidate".Mutation proof
A self-consistency test nobody has seen fail is worthless, so both directions
were broken and restored.
Mutant 1 — candidate disagrees with current source. One nibble flipped in
candidate/CloneFactory.pointers.solline 21 col 300(
…90f35b61004b6100…→…90f35b61f04b6100…), i.e. the committed pin no longerrecords what the source compiles to:
Mutant 2 — generated lib left pointing at a frozen dir. Import repointed from
../generated/candidate/to../generated/0_1_3/— the stale-headline-pinscenario #4's third assertion was written for:
Both restored with
git checkout -- <file>, green again:Generator is reproducible
From the committed tree, regenerating produces no diff:
Docs
README.mdsaid[package].versionand the snapshot "only ever move together"and implied a release history. Neither was true — Soldeer has zero
rain-factory-deployrevisions; nothing has ever been released from this repo.(
sol-v0.1.6exists as a tag but its run died before publishing anything.)README and
CLAUDE.mdnow describe the candidate model, andfoundry.tomlcarries a note that
version = "0.1.5"coincides withsrc/generated/0_1_5/byhistory, not by rule.
I deliberately did not bump the version: which number the first real release
carries is yours to pick, and it is worth knowing
sol-v0.1.6is already takenas a tag even though it published nothing.
Open ruling for you — not decided here
st0x#283 could delete every unaudited snapshot because there audited == live ==
depended-on. Here they diverge:
0x444acC29…(0_1_3/0_1_4, V2)1a92a86+v0.1.1)rain-factory0.1.1)0x7053c1D0…(0_1_5, V3)The repo's headline pin and only buildable implementation is unaudited, and the
audited implementation is not in the current source at all. Whether to audit V3
or revert to V2 is your call and this PR deliberately does not touch it — which
is a further argument for the add-only shape: the migration is orthogonal to that
ruling and pre-empts it in neither direction.
One adjacent fact worth seeing: the split is incomplete.
rain.factorymainstill carries the entire deploy half (
src/concrete/CloneFactory.sol,src/generated/0_1_3|0_1_4|0_1_5,src/lib/LibCloneFactoryDeploy.sol,script/BuildPointers.sol,script/Deploy.soland all four test files), so bothpins currently have two owners.
Behaviour change to be aware of
Because the pin lib now tracks
candidate, any change toCloneFactory'sbytecode makes the five
LibCloneFactoryDeployProdTestfork tests red until thatbytecode is deployed. Previously they asserted "prod matches
0_1_5"; now theyassert "prod matches HEAD". That is the deploy-before-merge coupling made
explicit, and it is documented in
CLAUDE.md.Verification
forge build,forge fmt --checkandreuse lintall pass.forge testis24/24 green locally, including all five fork tests run against public RPC
endpoints — which also confirms
candidate(==0_1_5) matches prod on everysupported chain.
In CI expect exactly one red:
testProdDeployArbitrum, failing withlb.drpc.live … error code -32001— an exhausted drpc quota on theRPC_URL_ARBITRUM_FORKsecret. It is pre-existing onmainand unrelated tothis change; Arbitrum is confirmed green on chain by direct
cast codeprobe, sothe red is purely the RPC.
rainix-sol / staticshould be green — this PRadds only, so the append-only gate has nothing to flag.
🤖 Generated with Claude Code