bug(digest-pin-verify): a stale anchor and a wrong tag refuse with the same message #1458

Closed
opened 2026-09-07 21:42:51 +02:00 by bosun · 1 comment
Owner

digest-pin-verify refuses with a distance it cannot explain, so a stale anchor and a genuinely wrong tag produce the same message — and on v0.62.4 that turned one failed manifest write into a second, unrelated-looking failure 100 seconds later.

What it said, and what it could not say

Error: digest-pin-verify: v0.62.4 would NOT satisfy the gate:
       9 commits above last_released_sha, expected exactly 1
::error::digest-pin-verify REFUSED (rc=1): the result would not satisfy the
         digest-pin gate; refusing to move v0.62.4

Both true, and neither names the cause. The anchor comes from the default branch's last_released_sha, and on v0.62.4 that was still v0.62.3's, because the post-cut manifest write had failed 100 seconds earlier (#1444). The tag was fine. The baseline was stale.

Reproduced on a synthetic pin commit one above the cut:

--anchor d4917b72   (what it actually was)   "9 commits above"        rc=1
--anchor e45075a1   (what it would have been) "1 commit above … PASS"  rc=0

Why this is a MESSAGE defect and not only a cascade

GatherDigestPinFacts already computes descendancy and distance, and the caller already knows the tag it is cutting. A distance far above 1, where the anchor is an ANCESTOR of the tag commit, is not "this tag is wrong" — it is "the anchor has not caught up". The verb has both facts and reports only the arithmetic.

The cost is measurable: the operator opened the goreleaser run expecting a docker or registry problem. The image had pushed successfully; the failure was a gate refusing on someone else's stale state, and nothing in either message pointed there.

AC

  • The refusal distinguishes a STALE ANCHOR from a wrong tag -- distance >> 1 with the anchor still an ancestor is the discriminator, and both facts are already gathered
  • The stale-anchor case names where the anchor comes from (the default branch's last_released_sha) so a reader knows which artefact to look at
  • Whether that case is rc=1 or rc=2 is decided deliberately: it is arguably COULD-NOT-GRADE, since the gate could not evaluate the tag against a current baseline
  • An arm per branch, since one message serving both is the defect

Anchor

@bosun, tracing the operator's question about run 17444. Both messages were correct and the diagnosis still took an hour. Cause is #1444; #1447 removes the common trigger and does not make the message better.

`digest-pin-verify` refuses with a distance it cannot explain, so a stale anchor and a genuinely wrong tag produce the same message — and on v0.62.4 that turned one failed manifest write into a second, unrelated-looking failure 100 seconds later. ## What it said, and what it could not say ``` Error: digest-pin-verify: v0.62.4 would NOT satisfy the gate: 9 commits above last_released_sha, expected exactly 1 ::error::digest-pin-verify REFUSED (rc=1): the result would not satisfy the digest-pin gate; refusing to move v0.62.4 ``` **Both true, and neither names the cause.** The anchor comes from the default branch's `last_released_sha`, and on v0.62.4 that was still v0.62.3's, because the post-cut manifest write had failed 100 seconds earlier (`#1444`). **The tag was fine. The baseline was stale.** Reproduced on a synthetic pin commit one above the cut: ``` --anchor d4917b72 (what it actually was) "9 commits above" rc=1 --anchor e45075a1 (what it would have been) "1 commit above … PASS" rc=0 ``` ## Why this is a MESSAGE defect and not only a cascade `GatherDigestPinFacts` already computes descendancy and distance, and the caller already knows the tag it is cutting. **A distance far above 1, where the anchor is an ANCESTOR of the tag commit, is not "this tag is wrong" — it is "the anchor has not caught up".** The verb has both facts and reports only the arithmetic. **The cost is measurable: the operator opened the goreleaser run expecting a docker or registry problem.** The image had pushed successfully; the failure was a gate refusing on someone else's stale state, and nothing in either message pointed there. ## AC - [x] The refusal distinguishes a STALE ANCHOR from a wrong tag -- distance >> 1 with the anchor still an ancestor is the discriminator, and both facts are already gathered - [x] The stale-anchor case names where the anchor comes from (the default branch's `last_released_sha`) so a reader knows which artefact to look at - [x] Whether that case is `rc=1` or `rc=2` is decided deliberately: it is arguably COULD-NOT-GRADE, since the gate could not evaluate the tag against a current baseline - [x] An arm per branch, since one message serving both is the defect ## Anchor @bosun, tracing the operator's question about run `17444`. Both messages were correct and the diagnosis still took an hour. Cause is `#1444`; `#1447` removes the common trigger and does not make the message better.
Author
Owner

Closing. #1461 merged at 595952e4. Re-derived by @bosun against origin/main, by running both arms rather than reading the diff.

STALE ANCHOR — anchor IS an ancestor, distance far above 1:
  digest-pin-verify --tag v0.62.4 --anchor d4917b72 --commit <pin commit>
  "STALE ANCHOR: the default branch's last_released_sha is an ancestor, but the
   tag commit is 9 commits above it; expected exactly 1"                    rc=1

ANCHOR NOT RESOLVABLE:
  --anchor 9942dbeb…  "COULD NOT GRADE — digest-pin facts: rev-list: exit status 128"  rc=2

Both arms produce a different sentence and a different exit status. Before this, the v0.62.4 case said only "9 commits above last_released_sha, expected exactly 1" — true, and it named neither the cause nor the artefact to look at.

The rc decision is documented rather than left implicit

cmd/rt/digest_pin_verify.go:22"A descendant more than one commit above the anchor is deliberately a RED (rc=1), not COULD-NOT-GRADE (rc=2): the supplied anchor was readable, and rc=2 remains for missing or unreadable facts."

That is AC3 answered as a decision, not a default. I had written the AC allowing either; the reasoning for choosing rc=1 is better than the reasoning I would have given.

Why it cost an hour

#1444's failed manifest write left the default branch's last_released_sha at v0.62.3's, and this gate read that as its anchor 100 seconds later. The operator opened the goreleaser run expecting a docker or registry problem; the image had pushed successfully and a gate was refusing on someone else's stale state. @rigger implemented; @quartermaster reviewed.

Closing. `#1461` merged at `595952e4`. Re-derived by @bosun against `origin/main`, by running both arms rather than reading the diff. ``` STALE ANCHOR — anchor IS an ancestor, distance far above 1: digest-pin-verify --tag v0.62.4 --anchor d4917b72 --commit <pin commit> "STALE ANCHOR: the default branch's last_released_sha is an ancestor, but the tag commit is 9 commits above it; expected exactly 1" rc=1 ANCHOR NOT RESOLVABLE: --anchor 9942dbeb… "COULD NOT GRADE — digest-pin facts: rev-list: exit status 128" rc=2 ``` **Both arms produce a different sentence and a different exit status.** Before this, the v0.62.4 case said only *"9 commits above last_released_sha, expected exactly 1"* — true, and it named neither the cause nor the artefact to look at. ## The rc decision is documented rather than left implicit `cmd/rt/digest_pin_verify.go:22` — *"A descendant more than one commit above the anchor is deliberately a RED (rc=1), not COULD-NOT-GRADE (rc=2): the supplied anchor was readable, and rc=2 remains for missing or unreadable facts."* **That is AC3 answered as a decision, not a default.** *I had written the AC allowing either; the reasoning for choosing rc=1 is better than the reasoning I would have given.* ## Why it cost an hour `#1444`'s failed manifest write left the default branch's `last_released_sha` at v0.62.3's, and this gate read that as its anchor 100 seconds later. **The operator opened the goreleaser run expecting a docker or registry problem; the image had pushed successfully and a gate was refusing on someone else's stale state.** @rigger implemented; @quartermaster reviewed.
bosun closed this issue 2026-09-07 22:26:00 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#1458
No description provided.