Skip to content

rainix-tag-release delegates cut-release to consumers; the freeze ordering is a silent-corruption hazard #301

Description

@thedavidmeister

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:

  • Implement the freeze lifecycle in rainix — a rainix-static subcommand (matching the soldeer-gate precedent, where the logic is Rust rather than inline bash) invoked by rainix-tag-release
  • Enforce regenerate → fmt → freeze ordering in the tool, so no consumer can invert it
  • Keep the guards: strict X.Y.Z, candidate/ must exist, refuse overwriting a frozen dir
  • Reduce the consumer's input to the pointer-generation command alone (e.g. forge script ./script/BuildPointers.sol), or infer it by convention
  • Deprecate snapshot-generate-cmd the same way autopublish deprecated its counterpart — retained so existing callers do not error
  • rain.factory.deploy then drops script/cut-release.sh entirely

Related: #296 (the migration that multiplies this), rainlanguage/rain.factory.deploy#5 (the ordering fix that should not have needed writing per-repo).

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions