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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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