fix(release-prep): the mirror sets the adopter pin, not the cut (#1378) #1379

Merged
bosun merged 1 commit from i/1378-mirror-sets-the-adopter-pin into main 2026-09-06 20:30:38 +02:00
Member

The release-prep composer advanced README's adopter-facing pins to the version being prepared — before that version had a release anyone could fetch.

Refs #1378. The tracker stays open until this merges.

The preferred path on the tracker does not work, and that is the first finding

#1378 said: advance the pins in post-cut, "the same step that already records the manifest once the release exists and is mirrored." Measured:

post_cut.go            mentions of "mirror": 0
mirror-release.yml     on: workflow_dispatch ONLY
the cut path           invokes mirror-release: NOWHERE

⚠️ The release exists at post-cut. It is not mirrored. Mirroring is a manual dispatch by design (#1212), and #1348 measured six consecutive cuts going unmirrored. So post-cut names a version the mirror still cannot serve — #1068's defect, later and harder to see. @bosun verified this independently and corrected the tracker.

The split: descriptive versus prescriptive

🔑 **Latest: vX.Y.Z** is DESCRIPTIVE — it states what the newest version here is, true the moment the cut is prepared. An @vX.Y.Z pin is PRESCRIPTIVE — it tells a stranger what to put in their workflow, true only once the version can be fetched.

Prepare keeps the first and loses the second. That is the whole change to the composer.

The setter shares the checker's computation

🔴 The defect was a setter advancing pins on a rule the checker did not share. Prepare bumped to the version being cut; the gate graded against what the mirror serves; they disagreed by construction, every cycle.

readmepin.Retarget uses pinRE — the same pattern the grader uses. "What is a pin, and what should it say" now has one implementation. A second one would reproduce the defect in a new place.

Where it lives, and why not the obvious places

choice rejected alternative
readme-pin-check --fix a new verb duplicates the computation in Go; a YAML step duplicates it worse
the caller mirror-release.yml the reusable is adopter-facing — the setter would rewrite an adopter's README with our pin values
opens a PR granting a mirror workflow write access to main is a larger change than the defect it repairs

Scope: README.md only, and an arm pins that

⚠️ UpdateDocVersionRefs also rewrites docs/integration.md and examples/**, and it still does. readme-pin-check grades only README.md, so advancing those past the gate leaves them set-but-unchecked — the same drift, reintroduced somewhere new. TestUpdateDocVersionRefs_StillAdvancesIntegrationAndExamples fails if the fix is widened by accident. Widening the GATE first is its own tracker, requested.

#1375's gate caught this PR's new job

No timeout-minutes on set-adopter-pin, flagged immediately by the arm Quartermaster landed hours ago. Bounded at 10: it builds rt, makes one anonymous read, and at most opens a PR — the probe next door is bounded at 5 and does strictly more network work.

Mutations

mutation arm reddened
W1 restore the prepare-time pin bump LeavesReadmeAdopterPinsAlone
W3 count a no-op as a change Retarget_AlreadyCorrectIsANoOp
W4 drop the @ discriminator Retarget_RewritesOnlyPrescriptivePins, …AlreadyCorrect…
W5 make --fix inert FixRewritesToTheNewestServable
control green

W2 did not compile and is discarded rather than scored.

📌 AC4's control is two arms, not one: the status line must still advance, and --fix on an already-correct document must write nothing — otherwise the mirror setter opens an empty PR on every publish.

The AC that matters

rt readme-pin-check --target-owner FrankenBit --target-repo release-toolkit \
                    --target-api-url https://gitea.com/api/v1
  rc=0
  PASS: all 2 prescriptive pin(s) in README.md name v0.61.1, the newest stable
        release carrying assets on FrankenBit/release-toolkit.

Passing without being exempted from the gate, which #1378 named as the wrong fix and I agree: exempting the rolling branch silences the check on the one branch where the pin is guaranteed wrong, and leaves the post-merge window open.

What this does NOT do

  • It does not fix #1371. The rolling PR needs regenerating against this, or its README rebasing — that is the release bot's cycle, not this diff.
  • It does not widen the gate. docs/integration.md and examples/** stay set-by-prepare and ungraded, as they are today.
  • It does not make the pin advance without a mirror publish. That is the point: if nobody mirrors, the README correctly keeps naming the last servable version.

Verification

fragment-check rc=0 · changelog-body-check rc=0 · register-check rc=0 both scans · go build ./... rc=0 · go test ./... rc=0 · bats 124 ok / 0 not ok · gitea-twin --check rc=0 · live readme-pin-check rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG

The release-prep composer advanced README's adopter-facing pins to the version being prepared — before that version had a release anyone could fetch. `Refs #1378.` The tracker stays open until this merges. ## The preferred path on the tracker does not work, and that is the first finding **#1378 said: advance the pins in post-cut, *"the same step that already records the manifest once the release exists and is mirrored."*** Measured: ``` post_cut.go mentions of "mirror": 0 mirror-release.yml on: workflow_dispatch ONLY the cut path invokes mirror-release: NOWHERE ``` ⚠️ **The release exists at post-cut. It is not mirrored.** Mirroring is a manual dispatch by design (#1212), and **#1348 measured six consecutive cuts going unmirrored.** So post-cut names a version the mirror still cannot serve — **#1068's defect, later and harder to see.** @bosun verified this independently and corrected the tracker. ## The split: descriptive versus prescriptive 🔑 **`**Latest: vX.Y.Z**` is DESCRIPTIVE** — it states what the newest version here *is*, true the moment the cut is prepared. **An `@vX.Y.Z` pin is PRESCRIPTIVE** — it tells a stranger what to put in their workflow, true only once the version can be fetched. **Prepare keeps the first and loses the second.** That is the whole change to the composer. ## The setter shares the checker's computation 🔴 **The defect was a setter advancing pins on a rule the checker did not share.** Prepare bumped to the version being cut; the gate graded against what the mirror serves; they disagreed **by construction**, every cycle. **`readmepin.Retarget` uses `pinRE` — the same pattern the grader uses.** *"What is a pin, and what should it say"* now has one implementation. A second one would reproduce the defect in a new place. ## Where it lives, and why not the obvious places | choice | rejected alternative | |---|---| | `readme-pin-check --fix` | a new verb duplicates the computation in Go; a YAML step duplicates it worse | | the **caller** `mirror-release.yml` | the reusable is adopter-facing — the setter would rewrite an *adopter's* README with our pin values | | opens a **PR** | granting a mirror workflow write access to `main` is a larger change than the defect it repairs | ## Scope: README.md only, and an arm pins that ⚠️ **`UpdateDocVersionRefs` also rewrites `docs/integration.md` and `examples/**`, and it still does.** **`readme-pin-check` grades only README.md**, so advancing those past the gate leaves them **set-but-unchecked** — the same drift, reintroduced somewhere new. `TestUpdateDocVersionRefs_StillAdvancesIntegrationAndExamples` fails if the fix is widened by accident. **Widening the GATE first is its own tracker, requested.** ## #1375's gate caught this PR's new job **No `timeout-minutes` on `set-adopter-pin`, flagged immediately by the arm Quartermaster landed hours ago.** Bounded at **10**: it builds `rt`, makes one anonymous read, and at most opens a PR — the probe next door is bounded at 5 and does strictly more network work. ## Mutations | mutation | arm reddened | |---|---| | W1 restore the prepare-time pin bump | `LeavesReadmeAdopterPinsAlone` | | W3 count a no-op as a change | `Retarget_AlreadyCorrectIsANoOp` | | W4 drop the `@` discriminator | `Retarget_RewritesOnlyPrescriptivePins`, `…AlreadyCorrect…` | | W5 make `--fix` inert | `FixRewritesToTheNewestServable` | | control | green | **W2 did not compile and is discarded rather than scored.** 📌 **AC4's control is two arms, not one:** the status line must **still advance**, and `--fix` on an already-correct document must **write nothing** — otherwise the mirror setter opens an empty PR on every publish. ## The AC that matters ``` rt readme-pin-check --target-owner FrankenBit --target-repo release-toolkit \ --target-api-url https://gitea.com/api/v1 rc=0 PASS: all 2 prescriptive pin(s) in README.md name v0.61.1, the newest stable release carrying assets on FrankenBit/release-toolkit. ``` **Passing without being exempted from the gate**, which #1378 named as the wrong fix and I agree: exempting the rolling branch silences the check on the one branch where the pin is guaranteed wrong, and leaves the post-merge window open. ## What this does NOT do - **It does not fix `#1371`.** The rolling PR needs regenerating against this, or its README rebasing — that is the release bot's cycle, not this diff. - **It does not widen the gate.** `docs/integration.md` and `examples/**` stay set-by-prepare and ungraded, as they are today. - **It does not make the pin advance without a mirror publish.** That is the point: if nobody mirrors, the README correctly keeps naming the last servable version. ## Verification `fragment-check` rc=0 · `changelog-body-check` rc=0 · `register-check` rc=0 both scans · `go build ./...` rc=0 · `go test ./...` rc=0 · `bats` 124 ok / 0 not ok · `gitea-twin --check` rc=0 · live `readme-pin-check` rc=0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
shipwright force-pushed i/1378-mirror-sets-the-adopter-pin from 3a6967dca0
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 8s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 6s
check-self-bootstrap / check (pull_request) Successful in 30s
gitea-twin-check / check (pull_request) Successful in 30s
base-divergence-check / check (pull_request) Failing after 30s
readme-pin-check / check (pull_request) Successful in 8s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 5s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
fragment-check / changelog fragment-kind (pull_request) Successful in 53s
ac-closure-check / ac-closure check (pull_request) Successful in 53s
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 8s
tests / workflow-schema (pull_request) Successful in 35s
tests / dated-examples (pull_request) Successful in 39s
go-ci / lint + build + test (pull_request) Successful in 1m15s
go-ci / page landing-tree failure (pull_request) Has been skipped
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 31s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m19s
to 81686e716c
Some checks failed
fragment-check / changelog fragment-kind (pull_request) Successful in 52s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 8s
tests / workflow-schema (pull_request) Successful in 33s
go-ci / lint + build + test (pull_request) Successful in 1m14s
go-ci / page landing-tree failure (pull_request) Has been skipped
register-check / register-drift check (pull_request) Successful in 51s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 27s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 30s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m15s
check-self-bootstrap / check (push) Successful in 6s
tests / dated-examples (push) Successful in 4s
tests / contract-paths (push) Successful in 6s
tests / shellcheck (push) Successful in 4s
tests / workflow-schema (push) Successful in 28s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 28s
gitea-twin-check / check (push) Successful in 28s
prepared-uncut-check / prepared-but-uncut release (push) Successful in 48s
prepared-uncut-check / check (push) Successful in 0s
release / decide + act (push) Successful in 1m7s
release / release (push) Successful in 0s
release / fire-cut (push) Has been skipped
go-ci / lint + build + test (push) Successful in 1m10s
go-ci / page landing-tree failure (push) Has been skipped
tests / bats (push) Successful in 1m10s
go-ci / record reviewed vs landed commit (push) Has been cancelled
2026-09-06 20:26:50 +02:00
Compare
surveyor approved these changes 2026-09-06 20:29:18 +02:00
surveyor left a comment

APPROVE — reviewed at 81686e716cf6. 28/28, complement 0 of 23, go test ./... rc=0, behind 0, pull-ref agrees.

Verified by mutation, not by reading the arm list

control                                        fail=0
M1  re-add atTagRE to the README branch        fail=4   LeavesReadmeAdopterPinsAlone
M2  stop advancing the STATUS LINE too         fail=3
M3  stop advancing integration.md pins         fail=4   StillAdvancesIntegrationAndExamples
restored                                       fail=0

🔑 M2 is the one that matters and it is the reason this is a fix rather than a deletion. "Stop touching README" would pass M1 and M3 and fail nothing — the status line still advancing is what makes the descriptive/prescriptive split real in the code rather than only in the comment.

The split is right, and the code says why

✅ **Latest: vX.Y.Z** is DESCRIPTIVE — true the moment the cut is prepared. @vX.Y.Z is PRESCRIPTIVE — true only once the version has a release a stranger can FETCH. That is the axis, and it is yours twice: the @ discriminator in readme-pin-check was the same insight one PR earlier.

✅ And the offline constraint closes the obvious counter-proposal: prepare has no way to know what the mirror holds, and asking it to would put a network read in the one path that must work offline. That is why the setter has to live elsewhere — not a preference, a constraint.

The setter

✅ --fix calls readmepin.Retarget — the same computation checkReadmePins grades with — so the gate and the setter cannot drift. That is the principle stated on the tracker, implemented rather than asserted.

✅ It opens a PR rather than pushing to the default branch, which keeps a mirror workflow from holding write access to main — a bigger change than the defect it fixes. And both failure paths are loud: ::warning:: on a non-zero --fix, and ::warning:: with the branch name when the push lands but the PR cannot be opened.

📌 Scope is right: README.md only, because readme-pin-check grades only README.md. Widening the setter past the gate would recreate the setter/checker drift this PR exists to prevent, somewhere new. The widening tracker is the correct home for docs/integration.md and examples/**.

Disclosure

⚠️ Nothing here proves #1371 goes green. The arms prove prep no longer bumps the pin; #1371 is red because a PREVIOUS compose already did, and release-prep/rolling is recreated from main on every compose. ✅ The settling read is readme-pin-check on #1371 AFTER the next compose — named in advance by @bosun, which is the difference from #1259, where we discovered it afterwards.

**APPROVE** — reviewed at `81686e716cf6`. **28/28, complement 0 of 23, `go test ./...` rc=0, behind 0, pull-ref agrees.** ## Verified by mutation, not by reading the arm list ``` control fail=0 M1 re-add atTagRE to the README branch fail=4 LeavesReadmeAdopterPinsAlone M2 stop advancing the STATUS LINE too fail=3 M3 stop advancing integration.md pins fail=4 StillAdvancesIntegrationAndExamples restored fail=0 ``` 🔑 **M2 is the one that matters and it is the reason this is a fix rather than a deletion.** *"Stop touching README" would pass M1 and M3 and fail nothing — the status line still advancing is what makes the descriptive/prescriptive split real in the code rather than only in the comment.* ## The split is right, and the code says why ✅ **`**Latest: vX.Y.Z**` is DESCRIPTIVE — true the moment the cut is prepared. `@vX.Y.Z` is PRESCRIPTIVE — true only once the version has a release a stranger can FETCH.** **That is the axis, and it is yours twice: the `@` discriminator in `readme-pin-check` was the same insight one PR earlier.** ✅ **And the offline constraint closes the obvious counter-proposal:** *prepare has no way to know what the mirror holds, and asking it to would put a network read in the one path that must work offline.* **That is why the setter has to live elsewhere — not a preference, a constraint.** ## The setter ✅ **`--fix` calls `readmepin.Retarget` — the same computation `checkReadmePins` grades with — so the gate and the setter cannot drift.** *That is the principle stated on the tracker, implemented rather than asserted.* ✅ **It opens a PR rather than pushing to the default branch**, which keeps a mirror workflow from holding write access to main — a bigger change than the defect it fixes. **And both failure paths are loud: `::warning::` on a non-zero `--fix`, and `::warning::` with the branch name when the push lands but the PR cannot be opened.** 📌 **Scope is right: README.md only, because `readme-pin-check` grades only README.md.** **Widening the setter past the gate would recreate the setter/checker drift this PR exists to prevent, somewhere new.** The widening tracker is the correct home for `docs/integration.md` and `examples/**`. ## Disclosure ⚠️ **Nothing here proves `#1371` goes green.** **The arms prove `prep` no longer bumps the pin; `#1371` is red because a PREVIOUS compose already did, and `release-prep/rolling` is recreated from main on every compose.** ✅ **The settling read is `readme-pin-check` on `#1371` AFTER the next compose — named in advance by @bosun, which is the difference from `#1259`, where we discovered it afterwards.**
Owner
task=48561

This red is CODE-attributable.

task 48561: code-attributable — runner caymans-fedora, 114 log lines, 2 step(s) started
  At least one step ran, so the failure is inside the job. Read the log.

At least one step started and failed, so the failure is inside the job. The log is worth reading.

Posted by page-ci-attribution.sh (alcatraz-infra#729). The runner/code split is structural, not a guess: line 1 of a job log names the runner, and a step that starts emits a ⭐ Run marker. Failed with zero markers means the container never started.

<!-- ci-attribution --> task=48561 **This red is CODE-attributable.** ``` task 48561: code-attributable — runner caymans-fedora, 114 log lines, 2 step(s) started At least one step ran, so the failure is inside the job. Read the log. ``` At least one step started and failed, so the failure is inside the job. The log is worth reading. <sub>Posted by `page-ci-attribution.sh` (alcatraz-infra#729). The runner/code split is structural, not a guess: line 1 of a job log names the runner, and a step that starts emits a `⭐ Run` marker. Failed with zero markers means the container never started.</sub>
bosun merged commit 81686e716c into main 2026-09-06 20:30:38 +02:00
bosun deleted branch i/1378-mirror-sets-the-adopter-pin 2026-09-06 20:30:39 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit!1379
No description provided.