Problem
rainix-tag-release requires consumers to supply snapshot-generate-cmd — a free-text shell string — for the step where the release's correctness lives:
snapshot-generate-cmd (required): Command that regenerates the deploy-pin snapshot from the (deterministic) bytecode into src/generated/<tag>/, DEPLOY_TAG and any pointer libs, then formats. e.g. forge script ./script/BuildPointers.sol && forge fmt.
So rainix owns the release lifecycle but delegates the freeze mechanics to every consumer. rain.factory.deploy's implementation (script/cut-release.sh, 72 lines, in rainlanguage/rain.factory.deploy#5) is entirely generic:
- read
[package].version from foundry.toml
- refuse anything that is not strict
X.Y.Z — a version like 0.1.7-rc1 freezes a dir the append-only gate's numeric filter ignores forever, an orphan snapshot nothing protects
- refuse when
src/generated/candidate/ is missing
- refuse to overwrite an existing frozen
src/generated/<TAG>/
- regenerate
candidate, forge fmt, then copy to the numbered dir
Every one of those is the rolling-candidate model #296 has every deploy repo adopting. Exactly one line is repo-specific: which script generates the pointers.
Why this is not merely duplication
The ordering is a silent-corruption hazard, and the first consumer to write it got it wrong. rain.factory.deploy#5 exists partly to fix its own script: it froze the committed candidate and only then regenerated. A drifted candidate gets frozen stale into an append-only dir while regeneration moves candidate on — so the release publishes one address and permanently records another.
Nothing downstream catches it: testCandidateSelfConsistent checks the regenerated candidate against source, and no test compares a numbered dir to candidate. The frozen record is wrong forever, by design of the append-only gate.
Regenerate-before-freeze is the non-obvious part, it is unenforced, and #296 queues eight or more repos to grow deploy halves. Each hand-rolled copy is another chance to invert it, and the failure is invisible.
rainix already reached this conclusion on the library side
rainix-autopublish's equivalent input is marked:
DEPRECATED / no-op. The version bump no longer generates per-release artifacts — a version's deploy-pin snapshot is built and committed by the PR that defines that version's content, never pre-generated onto main at bump time. Retained only so existing callers that still pass it do not error.
The library lifecycle stopped taking a consumer-supplied generate command. The deploy lifecycle still requires one.
Ask
Move cut-release into rainix so it lands once for everyone:
Related: #296 (the migration that multiplies this), rainlanguage/rain.factory.deploy#5 (the ordering fix that should not have needed writing per-repo).
Problem
rainix-tag-releaserequires consumers to supplysnapshot-generate-cmd— a free-text shell string — for the step where the release's correctness lives:So rainix owns the release lifecycle but delegates the freeze mechanics to every consumer. rain.factory.deploy's implementation (
script/cut-release.sh, 72 lines, in rainlanguage/rain.factory.deploy#5) is entirely generic:[package].versionfromfoundry.tomlX.Y.Z— a version like0.1.7-rc1freezes a dir the append-only gate's numeric filter ignores forever, an orphan snapshot nothing protectssrc/generated/candidate/is missingsrc/generated/<TAG>/candidate,forge fmt, then copy to the numbered dirEvery one of those is the rolling-candidate model #296 has every deploy repo adopting. Exactly one line is repo-specific: which script generates the pointers.
Why this is not merely duplication
The ordering is a silent-corruption hazard, and the first consumer to write it got it wrong. rain.factory.deploy#5 exists partly to fix its own script: it froze the committed
candidateand only then regenerated. A driftedcandidategets frozen stale into an append-only dir while regeneration movescandidateon — so the release publishes one address and permanently records another.Nothing downstream catches it:
testCandidateSelfConsistentchecks the regenerated candidate against source, and no test compares a numbered dir tocandidate. The frozen record is wrong forever, by design of the append-only gate.Regenerate-before-freeze is the non-obvious part, it is unenforced, and #296 queues eight or more repos to grow deploy halves. Each hand-rolled copy is another chance to invert it, and the failure is invisible.
rainix already reached this conclusion on the library side
rainix-autopublish's equivalent input is marked:The library lifecycle stopped taking a consumer-supplied generate command. The deploy lifecycle still requires one.
Ask
Move cut-release into rainix so it lands once for everyone:
rainix-staticsubcommand (matching thesoldeer-gateprecedent, where the logic is Rust rather than inline bash) invoked byrainix-tag-releaseX.Y.Z,candidate/must exist, refuse overwriting a frozen dirforge script ./script/BuildPointers.sol), or infer it by conventionsnapshot-generate-cmdthe same way autopublish deprecated its counterpart — retained so existing callers do not errorscript/cut-release.shentirelyRelated: #296 (the migration that multiplies this), rainlanguage/rain.factory.deploy#5 (the ordering fix that should not have needed writing per-repo).