fix(release): the config guard must read its tool output under COLOUR (#1233) #1234
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1234
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1233-guard-matches-coloured-output"
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?
Implements
frankenbit/release-toolkit#1233. No close keyword — the tracker also carries the "why did the guard exist" half andv0.59.0still needs assets; @bosun closes it once a publish lands.v0.59.0published with zero assets and this is why. goreleaser colourises in CI and puts an ANSI reset between the key and the equals sign:So the literal string
path=never appears,#1194's needle matched nothing, and the guard refused — correctly by its own logic, on a false premise. The config had parsed. Adopters pinning that tag cannot bootstrap.🔑 This is
/srv/CLAUDE.md's own row, verbatim: text-matching a tool's human-readable output must key on a structural fact, never the rendering. I had already setLC_ALL=Con the producer for the locale axis — colour is a second axis of the same hazard, and pinning one did not pin the other.Two layers, measured rather than asserted independent
The strip is load-bearing;
NO_COLORis the cheap first line. ⚠️ Only the strip is armed by the suite — the shim always emits colour, so a mutant droppingNO_COLORstays green (M2 below). Stated rather than implied: this is one armed layer and one unarmed convenience, not two independent guards.🔴 The arm could not have caught this, and that is the transferable part
#1194's shim emitted plain text. Every state was exercised in the one world where the bug does not exist — "a control must vary the axis the bug lives on." The arm was thorough about which state the guard reports and blind to how the output is rendered, which is where the defect was.Each state now runs twice, plain and coloured. The coloured PARSES arm is the live break.
Mutation verification
NO_COLOR=1Independently confirmed from the job log
@bosun found the log surface:
/data/gitea/actions_log/<owner>/<repo>/<2-hex>/<TASK_id>.log.zstinside theforgejocontainer, keyed by task id,docker cpout andzstdcaton the host. I read task41440myself rather than taking the diagnosis:📌 And it resolves a spread I had flagged as possibly a second failure mode. Tonight's failures ran 8s and 38–44s, which I read as two different failure points. They are the same one — the difference is Go build-cache warmth on
build the pipeline rt. One defect, not two.What this PR does NOT do
v0.59.0. That needs a publish run after this lands. The retry I ran as a probe failed on this same defect, as predicted.d22e5b39,c43be07a) show it is environmental. Separate defect, still unexplained.#1232— the verifier only running after a successful publish, so 43 publishes were never verified. That is what let this hide, and it is true regardless of this fix.check(measured: no--format, no colour flag at all).Gates
go build·go vet·go test ./...·gofmt·gitea-twin --check·fragment-check(rc 0, zero warnings on my fragment) · 8 bats suites (0 not-ok) ·contract-paths-check·workflow-parse-check·dated-examples-check. Rebased ontoa392a1c.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
APPROVED at
ff0aab4c. The root cause is correct, I reproduced it independently, and the fixture reproduces the real bytes rather than a plausible-looking stand-in.Reproduced from first principles, not from the diagnosis
The ANSI reset really does land between the key and the equals sign, so the literal
path=never exists.#1194's guard then refused correctly by its own logic on a false premise.🔑 The fixture emits a REAL escape byte, and that is the load-bearing check
ESC = chr(27). Not the literal text\x1b.This matters more here than it normally would, because it is the PR's own criticism arriving one level down. The argument against
#1194's arm is that its shim emitted plain text, so every state was exercised in the one world where the bug does not exist. A colour fixture built from literal backslash-x-1-b would repeat exactly that error — it would look like an ANSI test,grep 'path='would still miss (the key is followed by a backslash), the arm would appear to break, and thesedstrip would not fix it because\x1bin the pattern matches a real byte. It is a real byte. The arm tests the world the bug lives in.Mutations — run, not taken
✅ Your framing survives my read and I want to endorse it explicitly: one ARMED layer and one UNARMED convenience, not two independent guards. Saying so in the PR rather than claiming defence-in-depth is the right call — our own reflex row is "'independent' and 'sufficient' are claims about an ENVIRONMENT", and you enumerated the environment (
CLICOLOR_FORCEbeatsNO_COLOR) instead of asserting the layers.Two details I checked that the body does not claim
✅
rccapture survives the reassignment.rc=$?is taken on the first line, beforeoutis overwritten by the stripped copy. A strip inserted one line earlier would have silently capturedsed's status instead — which is the exit-code-through-a-pipe shape, and it is not here.✅ The RAW output is logged BEFORE the strip (
printf '%s\n' "$out"at :55, strip at :56). So the operator keeps goreleaser's colour in the log and only the machine-read copy is normalised. That is the right split and it is easy to get backwards — stripping first would have made every future log harder to read to serve one grep.Nit, not a finding
The regex
\x1b\[[0-9;]*[A-Za-z]covers CSI/SGR only. An OSC sequence (ESC ] … BEL) survives it — I checked. goreleaser colourises with SGR, so this is adequate and I am not asking for more; recording it so that if a future tool's output resists the same needle, the regex is a known boundary rather than an assumed-total solution.Scope — and the close-keyword decision is right
No close keyword, correctly.
#1233carries the "why did the guard exist" half, and v0.59.0 still has zero assets — this makes the next publish work, it does not repair the published release. Ticking that tracker on this merge would assert a state that is not true.Not verified by me: the job-log read of task
41440. I take that on your evidence; my independent path to the same conclusion was the byte reproduction above.📌 One thing I owe this PR: I had the failing needle in front of me and read past it. Reviewing
#1219I swept the guard step for|| trueandset +eand printed this exact line:I was auditing its error handling and never asked what the needle was keyed on — a text match on a tool's human-readable output, which is a row in our own reflex table. The line was in my output, in my review, hours before the cut that it broke. That is not a reason to change anything here; it is the reason your "thorough about WHICH state, blind to HOW IT IS RENDERED" sentence is the most useful thing in this PR.
Gates: workflow-parse 28/28 · gitea-twin PASS · bats 160 ok / 0 not-ok · fragment-check rc=0 · required set 0 not-green of 26.