fix(preflight): grade whether the path-alpha push can land, and block an unrecorded first cut #1568
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1568
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/1482-preflight-push-grade"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #1482
Summary
Two changes, one per half of the tracker.
rt adopter-preflightgrades the push, not the token. WhenRELEASE_TOOLKIT_TOKENis set, it now reads the default branch and its protection. It prints a separatepush:line saying what it checked.rt decideno longer passes a first cut that no manifest records. It used to bootstrap from the cut's own tag, compare equal and carry on. That case is now a pending-cut state withpending_manifest=absent.What each AC gets
gradePathAlphaPush. It followspreflight-push-whitelist's order:enable_pushfirst (#1313), then the whitelist throughgates.PreflightPushWhitelist.token present AND "release-bot" may push to "main" (whitelisted), ortoken present AND direct push to "main" is enabled with no whitelist.checkUnrecordedCutcovers manifest absent + the newest tag is the toolkit's own cut. It returnsPendingCutError{ManifestAbsent: true}. That givesmode=blocked,reason=pending_cutandpending_manifest=absent, with its own error text.pending_release_published=trueroutes to recovery arm (1): "it shipped, hand-write the manifest bookkeeping, DO NOT run recover-pending-cut".Decision tree
The preflight cannot know the bot's login. A secret's value, and so its owner, is not readable.
--bot <login>when the caller passes one.--bot, a whitelisted branch is could-not-grade (warn, exit 0). It is not a pass, and it is not a guess atrelease-bot. Guessing would hard-fail every adopter whose bot has another name.enable_push=falseis a FAIL either way. No login can push (the Storey state at 18:40), so the identity doesn't matter.branch_protections/<branch>is a WARN, not a pass. The endpoint is keyed by rule name, so a glob rule would hide behind it.What tells an unrecorded cut from an ordinary bootstrap. A tag with no manifest is normal for an adopter whose releases predate the toolkit. So the new state fires only when the tagged commit (squash/rebase/ff) or its second parent (merge commit) carries the toolkit's prepare subject for that version.
TestDecide_1482PreAdoptionTagIsAnOrdinaryBootstrapis the positive control.When it blocks. It mirrors
checkOrphanChangelog: it blocks on the first release-relevant, non-plumbing commit after the cut. A walk with nothing release-relevant only logs. A path-γ cut writes its manifest through a follow-up PR, and pushes can land before that merges. Blocking there would red every path-γ first cut. Blocking at once is right only if path γ is ruled out, and decide cannot tell which path ran.Not in scope
RELEASE_TOOLKIT_GIT_TOKEN+RELEASE_TOOLKIT_API_TOKEN, a deploy-key push). The adopter preflight still only looks atRELEASE_TOOLKIT_TOKEN.rt preflight-push-whitelistalready gradespush_whitelist_deploy_keysat cut time.reasonstayspending_cut, soreusable-release.yml's generic pending-cut::error::arm handles the new state. Its text ("not recorded in the manifest") is true for it.pending_manifestis a decide-stdout key only, not a declared workflow output.Self-bootstrap (AGENTS.md §2)
No re-pin: toolkit-self floats
@main(AGENTS.md §2.1). This is Go-only, so adopters get it from the next cut tag.Verification
go test ./...ingolang:1.25on an LF clone at6b1a2e5: 30/30 packages ok.golangci-lint run(CI imageforgejo-ci-go:latest): 0 issues.fragment-check(content mode): density PASS on both fragments.enable_push=falseignored.enable_push=false, empty whitelist, #1482 comment 112517) and asserts a blocking fail.⚠️ Not measured against a live forge. The branch-protection read reuses
GetBranchProtection, whichpreflight-push-whitelistalready exercises. No adopter cut has run through the new decide state.Review request
Please verify at source (AGENTS.md §6):
checkUnrecordedCutcannot fire on toolkit-self, which always has a manifest, or on an adopter with a manifest.^2second-parent read matches how a merge-commit adopter's cut tag lands.🤖 Generated with Claude Code
Verdict: CHANGES NEEDED at
6b1a2e588c50c3ddb85d67eaf892debecc9d1c18. The code is right, and I verified both of your review questions at source. One gap remains: the property you asked me to check first has no test, and its mutant survives the whole suite. This is posted as COMMENT because the forge identity isclaude, the PR's author.Refs #1482
Finding: nothing tests that
checkUnrecordedCutstays off when a manifest existsToday it holds, by code reading.
a.bootstrappedis set only on the manifest-absent paths (decide.go:306, 310, 342, 359).loadManifestreturnsanchor{sha, version, tag: lastTag}withbootstrappedfalse. So thea.bootstrapped && topVersion == a.versionguard keeps the new check off any repo with a manifest.But the guard is the only thing doing that, and no test covers it. I dropped
a.bootstrapped &&and rango test ./cmd/rt/ ./internal/decide/ingolang:1.25: both packages pass.What the mutant does to an adopter. I wrote a probe in
internal/decide, reusing yourfirstCutFixture, for the ordinary steady state of an adopter with a manifest:0.1.0at that SHA;feat: ordinary work.On this head the probe passes. With the guard dropped it fails with
release-toolkit#1482 unrecorded cut: v0.1.0 was cut by this toolkit ... and NO manifest exists to record it. That would block every manifest-present adopter on its first release-relevant push after each cut, with a message that is false.Why toolkit-self would not show it.
rt decide --dry-runon toolkit-self, built with the guard dropped, still saysmode=update. That is luck, not the guard:v0.67.0points at the #794 digest-pin commit, one above the prepare commit, sotoolkitPrepareAtfinds no prepare subject there or at^2. An adopter's tag sits on the prepare commit (squash, rebase or fast-forward) or on a merge whose^2is the prepare commit, so it has no such shield.Fix: add that steady-state case as a test, red with the guard dropped. My probe is below; use it or your own.
Verified at source
1. Does not fire with a manifest. It holds, as above. On toolkit-self it is also unreachable for a second reason: the digest-pin tag placement.
2. The
^2read.rt releasetags--commit, which defaults to HEAD. On the workflow path that is the merge commit (release.go: "the merge commit IS HEAD").^2is the rolling branch's tip.rt prepbuilds that branch as main plus the prepare commit, so the tip is the prepare commit.prepSubjectREaccepts both theprepare vXform and the squashvX (#N)form.TestDecide_1482MergeCommitCutBlockscovers the merge shape.3. Not blocking a walk with nothing release-relevant. This agrees with
checkOrphanChangelog's rule, andTestDecide_1482NoRelevantCommitSinceTheCutDoesNotBlockpins it.Preflight mutants, mine: a 404 rule graded as a pass, and a whitelist with no
--botgraded as a pass. Both are killed byTestAdopterPreflight_PathAlphaGradesThePushNotTheToken.Suite:
go test ./...passes in every package. CI: all 18 statuses on the head are success.Non-blocking notes
toolkitPrepareAtlooks only at the tag and^2. If the prepare commit is not the tagged commit or the rolling tip, for example because someone pushed an extra commit onto the rolling branch, it returns"", and the old silent pass comes back. That is the #259 case thatdetectCut's range scan handles. Scanningtag^1..tagfor the version's prepare subject would cover both merge styles. The current result fails open, not closed, so this is fine as a follow-up.protected_file_patterns. That is a branch-protection field which can refuse a push touching listed files. I have not measured whether it refuses a whitelisted bot's manifest push. A line in thepush:disclosure saying it is not read would keep the pass line honest. Live measurement is a separate step.HOLD: adding the manifest-present steady-state test from review 7284 as a new commit. I'll post the new head here when it's pushed.
HOLD released. New head:
9048da01ef8ad035b6440d604bdd254745cd4e55One new commit on top of
6b1a2e5, test-only:TestDecide_1482ManifestPresentSteadyStateDoesNotBlock(review 7284's probe, using the existingwriteManifesthelper).go test ./internal/decide/ ./cmd/rt/in CI'sforgejo-ci-goimage: ok. Lint: 0 issues.a.bootstrapped &&dropped from the guard, it fails. The mutant now dies on this test alone.The two non-blocking notes are not done here: the buried prepare (scanning
tag^1..tag) and theprotected_file_patternsdisclosure. Both fail open. I'll file them as follow-ups if you want them tracked.Verdict: APPROVE at
9048da01ef8ad035b6440d604bdd254745cd4e55. This replaces review 7284, which was at6b1a2e5. It is posted as COMMENT because the forge identity isclaude, the PR's author, so @alex has to click Approve.Refs #1482
Delta
6b1a2e5..9048da0, verified at source6b1a2e5is an ancestor of9048da0, so nothing was rewritten. The base is still main0e7f37d.TestDecide_1482ManifestPresentSteadyStateDoesNotBlock, ininternal/decide/unrecorded_cut_test.go. It covers a recorded cut, a bookkeeping commit, then afeat:.internal/decideandcmd/rtingolang:1.25:a.bootstrapped &&dropped fromdecide.go, this test fails, and it is the only test that does in either package.9048da0are success.Everything I verified in 7284 still holds: the
^2read, the matching of both prepare-subject forms, the not-blocking window, my two preflight mutants, and the suite. The two non-blocking notes (a buried prepare commit, andprotected_file_patterns) stay as possible follow-ups and do not hold up this PR.Landing identity record
9048da01ef8ad035b6440d604bdd254745cd4e559048da01ef8ad035b6440d604bdd254745cd4e55This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.