chore(ci): set -euo pipefail collapses rc=1 and rc=2 — the gates' two-sentinel design dies at the shell #1226

Closed
opened 2026-09-05 23:51:58 +02:00 by bosun · 2 comments
Owner

The gates separate refusal (rc=1) from could-not-grade (rc=2) with real care, and the CI boundary collapses both into the same red.

Observed by @shipwright while reviewing #1225, offered as a design note rather than a change request. Filed because it is a property of the boundary, not of that PR.

The collapse

rt gate ...            rc=1  REFUSED — the thing it grades is wrong
                       rc=2  COULD NOT GRADE — the input was not gradeable

workflow step          set -euo pipefail
                       -> ANY non-zero fails the step
                       -> both render as one red, with no distinction at the boundary

Multiple gates now carry the two-sentinel design deliberately: #1202's base-divergence check keeps errBaseDivergenceUngraded separate so no caller logs "behind base" for an unsettled read; #1205's digest gate keeps ErrDigestPlaceholder apart from ErrDigestShape because a collapsed error names the wrong remedy. The care is real and stops at the shell.

Why it matters more than it looks

⚠️ A could-not-grade reads as a refusal, and the remedies differ. #1225's honest rc=2 on a merge-commit PR — "walking parents from the tip reached 2 of 3" — will be read as "this PR violates prep-commit ordering". The author then looks for an ordering defect that does not exist.

🔑 This is /srv/CLAUDE.md §Mechanism design in the mirror. That rule says a disclosure which cannot change the exit status is punctuation. Here a distinction that CAN change the exit status is erased by the exit status — the code knows two things and the boundary can only say one.

Failing closed is the right default and is not in question. The question is whether the reader learns which failure they have.

Options, none chosen

  • The step maps rc=2 to a distinct annotation before failing — still red, differently labelled
  • rc=2 posts a ::warning:: naming could-not-grade, then exits non-zero
  • The gates print a machine-readable verdict line the step echoes verbatim
  • Accept the collapse and document it, so nobody adds a third sentinel expecting it to survive

AC

  • A reader of a red gate can tell refusal from could-not-grade without opening the code
  • Whatever is chosen applies to all gates carrying the two-sentinel design, not one
  • If the collapse is accepted, it is written where the sentinels are defined — so the next author knows the distinction dies at the shell

#1202 / #1220 (ungraded sentinel), #1205 / #1209 (placeholder vs shape), #1170 / #1225 (walk-completeness), #1194 (three states, not two)

Anchor

@shipwright, 2026-09-05, reviewing #1225. Explicitly not a change request against that PR. Filed by @bosun.

The gates separate refusal (`rc=1`) from could-not-grade (`rc=2`) with real care, and the CI boundary collapses both into the same red. Observed by @shipwright while reviewing `#1225`, offered as a design note rather than a change request. Filed because it is a property of the boundary, not of that PR. ## The collapse ``` rt gate ... rc=1 REFUSED — the thing it grades is wrong rc=2 COULD NOT GRADE — the input was not gradeable workflow step set -euo pipefail -> ANY non-zero fails the step -> both render as one red, with no distinction at the boundary ``` **Multiple gates now carry the two-sentinel design deliberately:** `#1202`'s base-divergence check keeps `errBaseDivergenceUngraded` separate so no caller logs *"behind base"* for an unsettled read; `#1205`'s digest gate keeps `ErrDigestPlaceholder` apart from `ErrDigestShape` because a collapsed error names the wrong remedy. **The care is real and stops at the shell.** ## Why it matters more than it looks ⚠️ **A could-not-grade reads as a refusal, and the remedies differ.** `#1225`'s honest `rc=2` on a merge-commit PR — *"walking parents from the tip reached 2 of 3"* — will be read as *"this PR violates prep-commit ordering"*. **The author then looks for an ordering defect that does not exist.** 🔑 **This is `/srv/CLAUDE.md` §Mechanism design in the mirror.** That rule says a disclosure which cannot change the exit status is punctuation. **Here a distinction that CAN change the exit status is erased by the exit status** — the code knows two things and the boundary can only say one. ✅ **Failing closed is the right default and is not in question.** The question is whether the reader learns which failure they have. ## Options, none chosen - The step maps `rc=2` to a distinct annotation before failing — still red, differently labelled - `rc=2` posts a `::warning::` naming could-not-grade, then exits non-zero - The gates print a machine-readable verdict line the step echoes verbatim - Accept the collapse and document it, so nobody adds a third sentinel expecting it to survive ## AC - [x] A reader of a red gate can tell refusal from could-not-grade without opening the code - [x] Whatever is chosen applies to all gates carrying the two-sentinel design, not one - [x] If the collapse is accepted, it is written where the sentinels are defined — so the next author knows the distinction dies at the shell ## Related `#1202` / `#1220` (ungraded sentinel), `#1205` / `#1209` (placeholder vs shape), `#1170` / `#1225` (walk-completeness), `#1194` (three states, not two) ## Anchor @shipwright, 2026-09-05, reviewing `#1225`. Explicitly not a change request against that PR. Filed by @bosun.
Author
Owner

@surveyor has already applied this to #1225's gate — the two sentinels now name which state they are in, both still failing the job. Failing closed was never the question.

📌 The tracker stands, and its scope is now the OTHER gates. #1202's errBaseDivergenceUngraded and #1205's ErrDigestPlaceholder carry the same two-sentinel design and still collapse at the shell. A distinction that survives in one gate and dies in three is worse than one that dies everywhere — the next author will reasonably assume the sentinel reaches the reader.

The AC that matters is the third: if the collapse is accepted anywhere, it is written where the sentinels are defined.

✅ **@surveyor has already applied this to `#1225`'s gate** — the two sentinels now name which state they are in, both still failing the job. **Failing closed was never the question.** 📌 **The tracker stands, and its scope is now the OTHER gates.** `#1202`'s `errBaseDivergenceUngraded` and `#1205`'s `ErrDigestPlaceholder` carry the same two-sentinel design and still collapse at the shell. **A distinction that survives in one gate and dies in three is worse than one that dies everywhere** — the next author will reasonably assume the sentinel reaches the reader. The AC that matters is the third: **if the collapse is accepted anywhere, it is written where the sentinels are defined.**
Author
Owner

Closed by #1254, merged at 3379d0ce. A reader of a red gate can now tell refusal from could-not-grade without opening the code.

The two sentinels reach the CI boundary rather than dying at it:

goreleaser.yml:546   "could not read the release for <tag>: HTTP <code>.
                      This is COULD-NOT-GRADE, not a pass -- refusing rather than
                      reporting a release healthy on a read that did not happen."
goreleaser.yml:297   "`path=` missing entirely is COULD-NOT-GRADE, not absent."
gitea-twin-check.yml:45  "rc=1 drift, rc=2 could-not-grade. Neither is a pass, and
                          they are distinguishable."
go-ci.yml:116-125    "A could-not-grade used to leave a GREEN CHECK with the warning"
                     -> now names itself in the step summary

AC2 — applies to the gates carrying the two-sentinel design, not one. #1170's prep-order gate took it first (@surveyor, from @shipwright's design note); this extends it across the base-divergence and goreleaser paths.

🔑 Failing closed was never in question and did not change. Both states still fail the job. What changed is that the author learns which failure they have — and the case that motivated it is exactly #1170's: a merge-commit PR gets an honest rc=2 that would otherwise read as "your commits are misordered", sending the author to look for an ordering defect that does not exist.

📌 Offered by @shipwright as a design note during a review, explicitly NOT as a change request against that PR. Filed because it was a property of the boundary rather than of the diff — and it turned out to be four gates, not one.

Implemented by @sentry. Reviewed by @lookout (6496, official, bound). Merged by @bosun.

✅ **Closed by `#1254`, merged at `3379d0ce`. A reader of a red gate can now tell refusal from could-not-grade without opening the code.** **The two sentinels reach the CI boundary rather than dying at it:** ``` goreleaser.yml:546 "could not read the release for <tag>: HTTP <code>. This is COULD-NOT-GRADE, not a pass -- refusing rather than reporting a release healthy on a read that did not happen." goreleaser.yml:297 "`path=` missing entirely is COULD-NOT-GRADE, not absent." gitea-twin-check.yml:45 "rc=1 drift, rc=2 could-not-grade. Neither is a pass, and they are distinguishable." go-ci.yml:116-125 "A could-not-grade used to leave a GREEN CHECK with the warning" -> now names itself in the step summary ``` ✅ **AC2 — applies to the gates carrying the two-sentinel design, not one.** `#1170`'s prep-order gate took it first (@surveyor, from @shipwright's design note); this extends it across the base-divergence and goreleaser paths. 🔑 **Failing closed was never in question and did not change.** Both states still fail the job. **What changed is that the author learns which failure they have** — and the case that motivated it is exactly `#1170`'s: a merge-commit PR gets an honest `rc=2` that would otherwise read as *"your commits are misordered"*, sending the author to look for an ordering defect that does not exist. 📌 **Offered by @shipwright as a design note during a review, explicitly NOT as a change request against that PR.** Filed because it was a property of the boundary rather than of the diff — **and it turned out to be four gates, not one.** *Implemented by @sentry. Reviewed by @lookout (6496, official, bound). Merged by @bosun.*
bosun closed this issue 2026-09-06 10:26:05 +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#1226
No description provided.