rainix owns the deploy-repo release freeze: rainix-static cut-release - #302
rainix owns the deploy-repo release freeze: rainix-static cut-release#302thedavidmeister wants to merge 4 commits into
Conversation
|
Warning Review limit reached
Next review available in: 36 minutes 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: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
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 |
|
@coderabbitai review The earlier run on this PR was rate limited and reported |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== changed files ==='
git diff --name-status HEAD^..HEAD
echo '=== diff summary ==='
git diff --stat HEAD^..HEAD
echo '=== relevant changed diff ==='
git diff --find-renames -- \
rainix-static/src/cut_release.rs \
rainix-static/src/main.rs \
rainix-tag-release.yaml \
.github/actions/cut-release \
'*cut-release*' \
'*rainix-tag-release*' || true
echo '=== candidate file locations ==='
fd -a 'cut_release\.rs|main\.rs|rainix-tag-release\.yaml|cut-release' . \
-E .git -E target -E node_modules
echo '=== Rust module outlines ==='
if [ -f rainix-static/src/cut_release.rs ]; then
ast-grep outline rainix-static/src/cut_release.rs --items all --view expanded
fi
if [ -f rainix-static/src/main.rs ]; then
ast-grep outline rainix-static/src/main.rs --items all --view expanded
fi
echo '=== workflow references and input declarations ==='
rg -n -C 4 --glob '*.yaml' --glob '*.yml' \
'snapshot-generate-cmd|pointers-generate-cmd|cut-release|rainix-static' .Length of output: 3817 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== pull request metadata ==='
gh pr view 302 --repo rainlanguage/rainix --json baseRefName,headRefName,commits \
--jq '{baseRefName, headRefName, commits: [.commits[].oid]}'
base_ref="$(gh pr view 302 --repo rainlanguage/rainix --json baseRefName --jq .baseRefName)"
echo "=== merge base with origin/${base_ref} ==="
merge_base="$(git merge-base "origin/${base_ref}" HEAD)"
printf '%s\n' "$merge_base"
echo '=== complete PR changed files ==='
git diff --name-status "$merge_base"...HEAD
echo '=== complete PR diff ==='
git diff --find-renames "$merge_base"...HEAD -- \
rainix-static/src/cut_release.rs \
rainix-static/src/main.rs \
rainix-static/src/frozen_snapshots.rs \
rainix-static/src/soldeer_gate.rs \
.github/workflows/rainix-tag-release.yaml \
.github/actions/cut-release/action.yaml
echo '=== cut_release implementation ==='
sed -n '1,540p' rainix-static/src/cut_release.rs
echo '=== command dispatch ==='
sed -n '1,180p' rainix-static/src/main.rs
echo '=== helper contracts ==='
sed -n '1,260p' rainix-static/src/frozen_snapshots.rs
sed -n '1,260p' rainix-static/src/soldeer_gate.rs
echo '=== release workflow ==='
sed -n '1,280p' .github/workflows/rainix-tag-release.yaml
echo '=== cut-release composite action ==='
sed -n '1,240p' .github/actions/cut-release/action.yaml
echo '=== action and workflow files in full PR ==='
git diff --name-only "$merge_base"...HEAD | grep -E '(^|/)(action|[^/]+)\.ya?ml$' || trueLength of output: 90614 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== cut-release composite action ==='
cat -n .github/actions/cut-release/action.yml
echo '=== comparable composite actions ==='
for action in \
.github/actions/frozen-snapshots-append-only/action.yml \
.github/actions/rpc-preflight/action.yml \
.github/actions/no-submodules/action.yml
do
if [ -f "$action" ]; then
echo "--- $action ---"
cat -n "$action"
fi
done
echo '=== composite action references ==='
rg -n -C 3 --glob 'action.y*ml' \
'path:|rainix-static|nix develop|generate-cmd|GITHUB_ACTION_PATH' .github/actions
echo '=== Cargo package and test configuration ==='
if [ -f rainix-static/Cargo.toml ]; then
cat -n rainix-static/Cargo.toml
fi
rg -n -C 3 --glob '*.nix' --glob 'Cargo.toml' --glob 'flake.nix' \
'rainix-static|cargo test|doCheck|cut_release' .
echo '=== numbered source lines ==='
nl -ba rainix-static/src/cut_release.rs | sed -n '45,235p'
nl -ba .github/workflows/rainix-tag-release.yaml | sed -n '45,225p'Length of output: 22555
I found no review findings. The release workflow removes
The command also reuses
|
|
HELD — do not merge. The design this PR sits in is unresolved, and merging now ships something we would immediately undo. Specifically: this PR deletes
What stands regardless of both: the Rust The two caller PRs are held with it, since they were sequenced behind this one: rainlanguage/rain.factory.deploy#9 and S01-Issuer/st0x.deploy#305. Neither is a no-op — |
|
Caller count correction: there are three, not two. The sweep behind this PR found two callers passing That PR is correct as it stands — the input is Note also that #26 uses |
Closes #301.
rainix-tag-releaseowned the release lifecycle but delegated the step where therelease's correctness lives — the snapshot freeze — to a required free-text
shell input,
snapshot-generate-cmd. Every deploy repo hand-rolled it, and thefirst consumer to write it inverted the ordering.
This moves the freeze into
rainix-staticas acut-releasesubcommand, so itlands once for every deploy repo and the ordering is a property of the tool
rather than a comment in a script.
Where the logic lives
rainix-static/src/cut_release.rs— Rust, alongsidesoldeer-gateandfrozen_snapshots, invoked from the new.github/actions/cut-releasecomposite, whichrainix-tag-releaseuses in placeof its old "Regenerate the deploy-pin snapshot" step.
It does what
rain.factory.deploy'sscript/cut-release.shdid, generically:[package].versionfromfoundry.toml— throughsoldeer_gate::read_local_version, the parser the publish lifecycle alreadyuses, so the two can never disagree about what that line is;
X.Y.Z— checked againstfrozen_snapshots::is_tag, the append-only gate's own predicate, ratherthan a restatement of it.
0.1.7-rc1maps to0_1_7-rc1, which that gateignores forever: an orphan snapshot nothing protects. The rejection criterion
is literally "the gate would not recognise this dir";
src/generated/candidate/is missing (or empty — the property is"there is something to freeze", not "the dir exists");
src/generated/<TAG>/, twice: as a pre-flightcheck with a readable message, and again at the point of writing, because the
copy uses
create_dirrather thancreate_dir_all;candidate,forge fmt, then copy;candidateare byte-identical afterwards, bothsides re-read from disk (the
diff -rthe shell version ended with).How the wrong ordering is made unrepresentable
The consumer no longer supplies a freeze script; it supplies a generate
command, and
cut()is the only thing that sequences the release:There is no input that runs after the copy, so "freeze, then regenerate" has
nowhere to be written. And the one way it could sneak back — a consumer passing a
leftover
cut-release.shas the generate command — is refused explicitly: if thegenerate step created the numbered dir itself,
cut()fails rather thanproceeding.
generateis a&mut dyn FnMut() -> Result<(), String>purely so the sequenceis unit-testable without
forge; production passes the consumer command followedby
forge fmt.What consumer input survives, and why that shape
One optional input:
pointers-generate-cmd, e.g.forge script ./script/BuildPointers.sol— which is also the default when it isempty, so a repo on the convention passes nothing at all.
Both existing deploy repos, and every repo #296 migrates, generate pins with
exactly that invocation, so the default is the convention rather than a guess.
Keeping the override rather than hard-coding it is deliberate: rainix does not own
the consumer's script filename, and the failure mode of a wrong guess (
forge script: file not found, mid-release) is worse than one input. The default stringlives in exactly one place —
cut_release::DEFAULT_GENERATE_CMD— and theworkflow input defaults to
''rather than restating it.What it is not is as important: it is a generate command, so its only
position in the lifecycle is "before the freeze".
snapshot-generate-cmdis deleted, not deprecatedGone from the inputs entirely — no note, no ignored knob, no default. An earlier
revision of this PR deprecated it the way
rainix-autopublishdeprecates itssoldeer-generate-cmd; that reasoning was circular, since that input is aninstance of the same debt rather than a precedent for more of it. A permanent
no-op input in a shared reusable propagates by copy-paste: #296
is about to create eight or more deploy repos whose authors will start from an
existing caller's
package-release.yaml, and each would inherit a knob that doesnothing and has to be discovered as such.
The cost of deleting is one line in each of two repos, both already open:
Merge order: this PR FIRST, then the two caller PRs
This matters, so here is the actual constraint rather than an assumption.
snapshot-generate-cmdisrequired: trueonmaintoday, so bothdirections fail inside the window — there is no free ordering:
required: truesnapshot-generate-cmdis not defined in the referenced workflow"So the callers cannot land first "harmlessly"; that would only hold if this PR
left the input declared-but-optional, which is precisely the dead knob being
deleted. Both windows being the same class of failure, this PR first is the
better order for one reason: the dependency then runs from a finished, reviewed
PR to two one-line follow-ups that merge in seconds. The reverse couples two
production release paths to whether this PR survives review unchanged — if it is
sent back, both callers sit broken for as long as that takes.
Neither failure can corrupt a release. Both are input validation, raised before
any step runs: nothing is generated, published, tagged or committed, and recovery
is re-pushing the tag once the set is complete. The one real precaution is that no
sol-v*tag should be pushed on either caller between the merges.(For completeness: a genuinely zero-window sequence exists — land this with the
input optional-and-ignored, merge the callers, then a third PR deleting it. Three
merges and a transient dead knob, to buy a window that only matters if someone
tags during it. Not taken; recorded so the trade is explicit rather than implied.)
Why a composite action, not a
run:stepRAINIX_SHA(53e96a7) predatesfrozen_snapshotsandrpc_preflight—git ls-tree 53e96a7 rainix-static/src/is three files. Sonix develop …/$RAINIX_SHA#sol-shell -c rainix-static cut-releasewould be"unknown subcommand" on merge and stay broken until a separate sha bump landed. The
composite runs the tool out of its own checkout via a
path:flake ref — thepattern
no-submodules,frozen-snapshots-append-onlyandrpc-preflightalreadyuse, and for this same reason.
path:also makes noapi.github.comcall, so itcannot hit the 429 that sha-pinning exists to avoid. It enters that checkout's
sol-shell(which carries bothforgeandrainix-static), so the tool and theforgeit drives are the same commit;flake.lockis byte-identical between53e96a7andmain, so there is no toolchain skew today either.Blast radius: the two live callers
Consumers pin
@main, so this is live on merge. Both callers, checked on theirdefault branches just now:
S01-Issuer/st0x.deploysnapshot-generate-cmd: bash script/cut-release.shscript/cut-release.sh— which freezescandidateand only then runsforge script+forge fmt, i.e. the live inverted order this issue is about — becomes dead coderainlanguage/rain.factory.deploysnapshot-generate-cmd: forge script ./script/BuildPointers.sol && forge fmtmainhas nosrc/generated/candidate/(itsBuildPointersstill writessrc/generated/<version>/straight fromfoundry.toml), so asol-v*tag fails loud at the candidate guard, publishing nothing, until #5 lands too. That repo has never published a Soldeer revisionStating it plainly rather than hedging: this makes
rain.factory.deploy's releasepath depend on #5 being merged. The alternative — a second code path for repos not
yet on the rolling-candidate model — would preserve exactly the mode #296 exists to
delete, and #301's guard list is explicit that a missing
candidate/is a refusal.What
rain.factory.deploy#5drops once this landsNot touched here; that is its own PR.
script/cut-release.sh— all 72 lines, entirely.package-release.yaml'ssnapshot-generate-cmd: bash script/cut-release.sh—the whole
with:entry, since that repo is on the conventional script path andneeds no
pointers-generate-cmdeither. (add gmp dep for rust #9 removesmain's copy of that line;debug deploy #5 carries its own and will need the same edit, whichever lands second.)
forgeexercise of that bash, and the "still open forthe human:
cut-release.shhand-rolls the freeze in bash rather than a sharedmechanism" note, which this closes.
script/BuildPointers.sol's docstringpoints readers at
script/cut-release.shfor what creates a numbered dir; thatbecomes
rainix-static cut-release.S01-Issuer/st0x.deployshould drop itsscript/cut-release.shtoo — its copy isthe one with the inverted order — as a follow-up to #305, which is deliberately
just the one release-call line.
Out of scope, found while here
pre-commit run --all-filescannot pass onmain. Therustfmt-conditionalhook's guard is
[ -f Cargo.toml ] || [ -f */Cargo.toml ], which matchesrainix-static/Cargo.toml, and it then runscargo-fmtfrom the repo root:`cargo metadata` exited with an error: could not find `Cargo.toml`.Reproduced on clean
main(8af258f) before touching anything, and unchanged bythis PR. Not fixed here.
S01-Issuer/st0x.deploy's inverted freeze is live right now. Until this (ora change on its side) lands, any
sol-v*tag there can freeze a driftedcandidateinto an append-only dir. It already carries one frozen tag,src/generated/0_1_1/.rainix-autopublish'ssoldeer-generate-cmdis the same dead input, and hasZERO live callers. All 29 repos calling
rainix-autopublish.yaml@mainwerefetched and grepped: not one passes it, and the only occurrence anywhere is its
own declaration. So it can be deleted outright with no caller PRs and no
ordering window at all — strictly cheaper than this deletion. Not touched here;
it wants its own one-line PR.
RAINIX_SHAis two rainix-static subcommands behindmain. Anyfuture workflow step that reaches for a new subcommand from
github:rainlanguage/rainix/$RAINIX_SHA#…will be an unknown-subcommand failureon merge. Only the
path:composites are immune. Worth a lint or a bump policy;not this PR.
End to end, not just unit tests
A minimal deploy-repo shape (a
foundry.tomlat0.1.5and a deliberatelyunformatted
src/generated/candidate/X.pointers.solpinning0xAAAA), runthrough the composite's exact invocation —
nix develop "path:<rainix>#sol-shell" -c rainix-static cut-release …— with agenerate command that rewrites the pin to
0xBBBB:Three things that transcript shows, which the unit tests assert separately: the
frozen dir holds
0xBBBB, the address the generator produced, not the0xAAAAthat was committed — freeze-first would have recorded the wrong one; the frozen
bytes are the formatted form, so
forge fmtran before the copy, not after;and a second cut refuses rather than clobbering.
QA
Discriminating tests: 13 new tests in
rainix-static/src/cut_release.rs(suite: 53 passed, 0 failed — they run inside the nix build via
doCheck, soevery job that enters a shell runs them).
regenerates_before_freezing— the generator rewritescandidatefrom0xAAAto0xBBBand adds a file; the frozen dir must hold0xBBBand thenew file. Freezing first records the address the release does not publish.
nothing_is_frozen_while_the_generator_runs— the same ordering from thegenerator's side: the numbered dir does not exist while it runs.
refuses_when_the_generator_freezes_the_numbered_dir_itself— a leftoverconsumer
cut-release.shpassed as the generate command is refused, and itsbytes are not adopted as the release.
a_failing_generator_freezes_nothing, anda_command_that_fails_anywhere_fails_the_cut(false,false; true,false | true, unset var —-e,-o pipefail,-urespectively).rejects_versions_the_append_only_gate_would_ignore—0.1.7-rc1,0.1,1.2.3.4,0.1.5+build,v0.1.5,""; each must refuse, must not run thegenerator, and must leave
src/generatedholding onlycandidate.accepts_every_strict_version—0.1.5,1.0.0,12.0.255,0.1.10.refuses_without_a_candidate,refuses_an_empty_candidate,refuses_to_overwrite_a_frozen_snapshot(asserts the pre-existing frozenbytes are still there afterwards),
refuses_without_a_package_version.freezes_candidate_into_the_numbered_dir,freezes_nested_files_too,write_tree_refuses_an_existing_dir,read_tree_is_relative_and_recursive.Mutations applied: nine, one at a time against the committed baseline, whole
suite each time, tree restored and re-verified green (53 passed) at the end.
Seven killed, one killed after adding a test, one survivor kept deliberately.
regenerates_before_freezing,nothing_is_frozen_while_the_generator_runs,refuses_when_the_generator_freezes_the_numbered_dir_itself,refuses_an_empty_candidate,a_failing_generator_freezes_nothingX.Y.Zguardrejects_versions_the_append_only_gate_would_ignorecreate_dir->create_dir_allin the copywrite_tree_refuses_an_existing_dirrefuses_to_overwrite_a_frozen_snapshotrefuses_without_a_candidaterefuses_when_the_generator_freezes_the_numbered_dir_itselfrefuses_an_empty_candidatebash -euo pipefaila_command_that_fails_anywhere_fails_the_cutwas written for it and now kills itdiff -rthe reference script ended with, kept as defence in depth and reported rather than papered over with a test that cannot failOracle:
rainlanguage/rain.factory.deploy@2026-07-25-rolling-candidate-model'sscript/cut-release.shread as the behaviour being generalised;rainix-static/src/frozen_snapshots.rsis_tag()read directly for what theappend-only gate does and does not recognise (and then called, not restated);
rainix-autopublish.yaml'ssoldeer-generate-cmdread for the deprecationwording; both callers'
package-release.yamlandsrc/generated/layouts readlive from their default branches, not assumed. All independent of this diff.
Category check: rainix-tag-release delegates cut-release to consumers; the freeze ordering is a silent-corruption hazard #301's six boxes. Freeze implemented in rainix as a
rainix-staticsubcommand on thesoldeer-gateprecedent, invoked byrainix-tag-release— yes. Regenerate -> fmt -> freeze enforced in the tool —yes, and unrepresentable otherwise. Guards kept (strict
X.Y.Z,candidate/must exist, refuse overwriting a frozen dir) — yes, plus empty-
candidateandgenerator-created-the-dir, which are the same properties widened.
Consumer input reduced to the pointer-generation command, defaulted by
convention — yes.
snapshot-generate-cmddeprecated the autopublish way — yes.rain.factory.deploydropsscript/cut-release.sh— enumerated above, left forits own PR as instructed.
CI:
cargo fmt --all -- --checkandcargo clippy --all-targets --all-features -- -D warnings -D clippy::all(CI'sexact invocation, after touching the changed files so a warm target cannot mask a
lint) both clean;
pre-commit run --all-filesmatchesmain— same singlepre-existing
rustfmt-conditionalfailure, nothing new.🤖 Generated with Claude Code