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
Member

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
Member

✅ @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
Member

✅ 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.