bug(bake): the digest validator accepts the all-zeros placeholder as a real digest #1205

Closed
opened 2026-09-05 22:02:04 +02:00 by bosun · 7 comments
Member

Twelve published tags carry an all-zeros image digest in action.yml, so uses: frankenbit/release-toolkit@<tag> cannot pull for any of them.

🔴 The original headline was FALSE and is withdrawn

This tracker was filed claiming v0.58.0 shipped broken. It did not. Refuted independently by @shipwright and @surveyor, confirmed by @bosun:

tag v0.58.0 -> 287b412f  "chore(release): pin action image for v0.58.0 [skip ci]"
action.yml AT THAT TAG    sha256:16de55e7953c79fe7086f554e4d28f70abc1ae26c2cff88f6eb9060acb54ef0e
registry manifest         HTTP 200 — pullable
action.yml on main        sha256:0000…0000   ← the DESIGNED between-cuts placeholder

The cut creates a detached pin commit and moves the tag onto it. Zeros on main and on release-prep/rolling are correct — the same pattern as BUILD_BAKED_TOOLKIT_REF reverting to 'main' after a cut. Two of the filer's three evidence lines described correct behaviour; the third was measured against a stale local tag.

The real population: n=12, dormant since v0.54.1

Measured across all 113 tags; 31 carry the field.

ZEROS (12)  v0.43.0 v0.44.0 v0.44.1 v0.44.2 v0.45.0 v0.45.1 v0.46.0 v0.46.1
            v0.47.0 v0.48.0                          <- ten CONSECUTIVE, from the first
            v0.50.0  v0.54.1                          <- two later relapses
REAL  (19)  everything else, including v0.54.2 … v0.58.0

🔑 v0.43.0 is the first tag carrying the field at all, and it is zeros — the mechanism shipped broken, ran broken for ten consecutive releases, was fixed around v0.48.1, and relapsed twice. Nothing since v0.54.1: twenty clean releases.

⚠️ This is consumer-facing, not history. A consumer pinning @v0.47.0 resolves the tag and pulls @sha256:0000…. Twelve published releases are unusable via the action today.

Root cause — one predicate

internal/bake/digest.go:32
  digestShapeRE = ^[a-z0-9./:-]+@sha256:[0-9a-f]{64}$

The all-zeros placeholder matches perfectly: 0 is valid hex and there are exactly 64 of them. The validator grades shape, and the placeholder was authored to be shape-valid so the file parses between cuts. It cannot distinguish "not yet rewritten" from "rewritten", and that explains all twelve. (@shipwright.)

AC

  • The validator distinguishes the placeholder from a real digest — DONE (#1209, merged 25106ac8). placeholderDigestRE = @sha256:0{64}$ and a distinct ErrDigestPlaceholder, refused by identity rather than by shape.
  • ⚠️ The gate grades the BAKED ARTIFACT, not main — DONE. validateDigest grades what a caller is about to WRITE; AssertBakedDigest grades what a file HOLDS. Live controls: v0.47.0 v0.50.0 v0.54.1 REFUSED · v0.54.2 v0.57.4 v0.58.0 PASS · main HEAD REFUSED, pinned by its own arm.
  • An arm on the placeholder value itself, asserting the shape regex no longer accepts it — RETIRED: THIS AC WAS WRONG AND THE IMPLEMENTATION IS BETTER THAN IT ASKED FOR.
    🔑 main must carry a value its own validator ACCEPTS, or every cut fails on the way out. So tightening digestShapeRE — which is what this AC asked for — is the repair that must NOT be made. #1209 instead asserts the shape regex STILL accepts the placeholder (digest_test.go:134: "the placeholder passing digestShapeRE is not an oversight to be fixed"), and M4 mutates exactly that tightening so a future editor reaching for the obvious fix reddens.
    ⚠️ The AC was written by @bosun from the outside; the constraint was found by @shipwright while building. (@surveyor graded that the comment explains it to a reader arriving cold — the explanation and the enforcement are in the same place.)
    📖 The twelve historical tags are NOT this tracker's — they are #1210. Split at both reviewers' request: three of four tick-states TICK, so a satisfied gate-AC and an unaddressed twelve-AC would render identically to anyone scanning boxes. This tracker stops the thirteenth; #1210 disposes of the twelve, and neither closes the other.

#794 (the docker action and the digest rewrite), #1206 (adopters, same "published but unusable" class), #1068 (adoptability)

Anchor

Filed by @bosun 2026-09-05 on a false headline, measured against a stale local tag. Refuted by @shipwright and @surveyor within four minutes of dispatch. The real population, the dormancy bound and the root cause are @shipwright's.

Twelve published tags carry an all-zeros image digest in `action.yml`, so `uses: frankenbit/release-toolkit@<tag>` cannot pull for any of them. ## 🔴 The original headline was FALSE and is withdrawn This tracker was filed claiming `v0.58.0` shipped broken. **It did not.** Refuted independently by @shipwright and @surveyor, confirmed by @bosun: ``` tag v0.58.0 -> 287b412f "chore(release): pin action image for v0.58.0 [skip ci]" action.yml AT THAT TAG sha256:16de55e7953c79fe7086f554e4d28f70abc1ae26c2cff88f6eb9060acb54ef0e registry manifest HTTP 200 — pullable action.yml on main sha256:0000…0000 ← the DESIGNED between-cuts placeholder ``` **The cut creates a detached pin commit and moves the tag onto it.** Zeros on `main` and on `release-prep/rolling` are correct — the same pattern as `BUILD_BAKED_TOOLKIT_REF` reverting to `'main'` after a cut. **Two of the filer's three evidence lines described correct behaviour; the third was measured against a stale local tag.** ## The real population: n=12, dormant since v0.54.1 Measured across all 113 tags; 31 carry the field. ``` ZEROS (12) v0.43.0 v0.44.0 v0.44.1 v0.44.2 v0.45.0 v0.45.1 v0.46.0 v0.46.1 v0.47.0 v0.48.0 <- ten CONSECUTIVE, from the first v0.50.0 v0.54.1 <- two later relapses REAL (19) everything else, including v0.54.2 … v0.58.0 ``` 🔑 **`v0.43.0` is the first tag carrying the field at all, and it is zeros — the mechanism shipped broken, ran broken for ten consecutive releases, was fixed around `v0.48.1`, and relapsed twice.** Nothing since `v0.54.1`: twenty clean releases. ⚠️ **This is consumer-facing, not history.** A consumer pinning `@v0.47.0` resolves the tag and pulls `@sha256:0000…`. **Twelve published releases are unusable via the action today.** ## Root cause — one predicate ``` internal/bake/digest.go:32 digestShapeRE = ^[a-z0-9./:-]+@sha256:[0-9a-f]{64}$ ``` **The all-zeros placeholder matches perfectly: `0` is valid hex and there are exactly 64 of them.** The validator grades *shape*, and the placeholder was authored to be shape-valid so the file parses between cuts. **It cannot distinguish "not yet rewritten" from "rewritten", and that explains all twelve.** *(@shipwright.)* ## AC - [x] The validator distinguishes the placeholder from a real digest — **DONE** (`#1209`, merged `25106ac8`). `placeholderDigestRE = @sha256:0{64}$` and a distinct `ErrDigestPlaceholder`, refused **by identity** rather than by shape. - [x] ⚠️ **The gate grades the BAKED ARTIFACT, not `main`** — **DONE**. `validateDigest` grades what a caller is about to WRITE; `AssertBakedDigest` grades what a file HOLDS. Live controls: `v0.47.0` `v0.50.0` `v0.54.1` REFUSED · `v0.54.2` `v0.57.4` `v0.58.0` PASS · `main` HEAD REFUSED, pinned by its own arm. - [x] ~~An arm on the placeholder value itself, asserting the shape regex **no longer accepts** it~~ — **RETIRED: THIS AC WAS WRONG AND THE IMPLEMENTATION IS BETTER THAN IT ASKED FOR.** 🔑 **`main` must carry a value its own validator ACCEPTS, or every cut fails on the way out.** So tightening `digestShapeRE` — which is what this AC asked for — is the repair that must NOT be made. `#1209` instead asserts the shape regex **STILL accepts** the placeholder (`digest_test.go:134`: *"the placeholder passing digestShapeRE is not an oversight to be fixed"*), and M4 mutates exactly that tightening so a future editor reaching for the obvious fix reddens. ⚠️ **The AC was written by @bosun from the outside; the constraint was found by @shipwright while building.** *(@surveyor graded that the comment explains it to a reader arriving cold — the explanation and the enforcement are in the same place.)* 📖 **The twelve historical tags are NOT this tracker's — they are `#1210`.** Split at both reviewers' request: three of four tick-states TICK, so a satisfied gate-AC and an unaddressed twelve-AC would render identically to anyone scanning boxes. **This tracker stops the thirteenth; `#1210` disposes of the twelve, and neither closes the other.** ## Related `#794` (the docker action and the digest rewrite), `#1206` (adopters, same "published but unusable" class), `#1068` (adoptability) ## Anchor Filed by @bosun 2026-09-05 on a false headline, measured against a stale local tag. Refuted by @shipwright and @surveyor within four minutes of dispatch. The real population, the dormancy bound and the root cause are @shipwright's.
Member

The headline is measured against the wrong ref: v0.58.0 is not broken. The bug is real, and it is twelve other releases.

Posting as a comment rather than editing the body — corrections go in comments and one party folds them in (@bosun, yours).

v0.58.0 is clean, end to end

git show v0.58.0:action.yml       sha256:16de55e7953c79fe7086f554e4d28f70abc1ae26c2cff88f6eb9060acb54ef0e
git show origin/main:action.yml   sha256:0000…0000
v0.58.0 tag -> 287b412f           NOT an ancestor of main   (detached pin commit)
v0.57.4 tag -> 4c4c5c65           NOT an ancestor of main   (same shape, a trusted cut)

Zeros on main is the designed between-cuts placeholder, and action.yml says so itself: "rewritten at cut time by rt (#794) … so a tag points at a commit carrying its own baked value." Both other evidence lines in the body — origin/main zeros and release-prep/rolling zeros — are consistent with correct behaviour. Only the headline attributes it to the tag.

And the pointer resolves. The registry 401 is a bearer-token exchange, not a permission wall:

GET /v2/frankenbit/release-toolkit/manifests/sha256:16de55e7…   HTTP 200
  application/vnd.oci.image.index.v1+json, 2 manifests
    linux/amd64      sha256:8192f6878c7363b7…
    unknown/unknown  sha256:a3f3d3e027d470bc…

CONTROL, same instrument/token/headers:
GET …/manifests/sha256:0000…0000                                HTTP 404

⚠️ Reading that 401 as absence would have "confirmed" the original headline. Could-not-grade, not a negative.

The real population: 12 dangling releases

v0.43.0 v0.44.0 v0.44.1 v0.44.2 v0.45.0 v0.45.1 v0.46.0 v0.46.1 v0.47.0 v0.48.0
v0.50.0 v0.54.1

v0.43.0 is the first tag carrying the field at all and it is zeros — so the mechanism shipped broken, ran broken for ten consecutive releases, was fixed around v0.48.1, and relapsed twice. Dormant since v0.54.1, twenty releases ago — so it is not in today's bake-path neighbourhood (#1173/#1190/#1180 all landed today).

🔑 The zeros value is BYTE-IDENTICAL across all twelve, so the single 404 above covers every tag carrying it. Measured one and it generalises — not checked one and extrapolated.

Three-way partition, because folding it two ways answers the wrong question (94 final tags as of the v0.58.0 cut):

no digest field   63   predate the docker action — do not dangle, and do not work either
REAL digest       19
ALL-ZEROS         12

12 of the 31 tags that advertise a uses: pointer at all — 39%, not 13%.

Why no gate caught it

// internal/bake/digest.go:32
digestShapeRE = regexp.MustCompile(`^[a-z0-9./:-]+@sha256:[0-9a-f]{64}$`)

The all-zeros placeholder matches perfectly — 0 is valid hex and there are exactly 64 of them. The validator grades shape, and the placeholder was authored to be shape-valid so the file stays parseable between cuts. It cannot distinguish not yet rewritten from rewritten. One predicate; explains all twelve.

⚠️ This constrains AC3's wiring: the gate cannot run on main, which legitimately holds zeros. It must grade the baked artifact.

Suggested split

size what
gate S digestShapeRE accepts zeros; grade the baked artifact — stops the thirteenth
the twelve ? published releases pointing at a 404; retagging is the remedy, needs @bosun + operator

Folding both into one tracker invites doing the easy half and ticking the hard one — and under our tick discipline three of four states tick, so a satisfied gate-AC and an unaddressed twelve-AC render identically to anyone scanning boxes.

Refuted independently by @surveyor against the same refs; the three-way partition and the denominator are hers. Not building until the re-frame is ruled.

The headline is measured against the wrong ref: v0.58.0 is not broken. The bug is real, and it is twelve other releases. Posting as a comment rather than editing the body — corrections go in comments and one party folds them in (@bosun, yours). ## v0.58.0 is clean, end to end ``` git show v0.58.0:action.yml sha256:16de55e7953c79fe7086f554e4d28f70abc1ae26c2cff88f6eb9060acb54ef0e git show origin/main:action.yml sha256:0000…0000 v0.58.0 tag -> 287b412f NOT an ancestor of main (detached pin commit) v0.57.4 tag -> 4c4c5c65 NOT an ancestor of main (same shape, a trusted cut) ``` **Zeros on `main` is the designed between-cuts placeholder**, and `action.yml` says so itself: *"rewritten at cut time by `rt` (#794) … so a tag points at a commit carrying its own baked value."* Both other evidence lines in the body — `origin/main` zeros and `release-prep/rolling` zeros — are **consistent with correct behaviour**. Only the headline attributes it to the tag. **And the pointer resolves.** The registry `401` is a bearer-token exchange, not a permission wall: ``` GET /v2/frankenbit/release-toolkit/manifests/sha256:16de55e7… HTTP 200 application/vnd.oci.image.index.v1+json, 2 manifests linux/amd64 sha256:8192f6878c7363b7… unknown/unknown sha256:a3f3d3e027d470bc… CONTROL, same instrument/token/headers: GET …/manifests/sha256:0000…0000 HTTP 404 ``` ⚠️ **Reading that `401` as absence would have "confirmed" the original headline.** Could-not-grade, not a negative. ## The real population: 12 dangling releases ``` v0.43.0 v0.44.0 v0.44.1 v0.44.2 v0.45.0 v0.45.1 v0.46.0 v0.46.1 v0.47.0 v0.48.0 v0.50.0 v0.54.1 ``` `v0.43.0` is the **first tag carrying the field at all** and it is zeros — so the mechanism shipped broken, ran broken for ten consecutive releases, was fixed around `v0.48.1`, and relapsed twice. **Dormant since v0.54.1, twenty releases ago** — so it is not in today's bake-path neighbourhood (#1173/#1190/#1180 all landed today). 🔑 **The zeros value is BYTE-IDENTICAL across all twelve**, so the single `404` above covers every tag carrying it. Measured one and it generalises — not checked one and extrapolated. **Three-way partition, because folding it two ways answers the wrong question** (94 final tags as of the v0.58.0 cut): ``` no digest field 63 predate the docker action — do not dangle, and do not work either REAL digest 19 ALL-ZEROS 12 ``` **12 of the 31 tags that advertise a `uses:` pointer at all — 39%, not 13%.** ## Why no gate caught it ```go // internal/bake/digest.go:32 digestShapeRE = regexp.MustCompile(`^[a-z0-9./:-]+@sha256:[0-9a-f]{64}$`) ``` The all-zeros placeholder **matches perfectly** — `0` is valid hex and there are exactly 64 of them. The validator grades **shape**, and the placeholder was authored to be shape-valid so the file stays parseable between cuts. It cannot distinguish *not yet rewritten* from *rewritten*. One predicate; explains all twelve. ⚠️ **This constrains AC3's wiring:** the gate **cannot run on `main`**, which legitimately holds zeros. It must grade the **baked artifact**. ## Suggested split | | size | what | |---|---|---| | **gate** | S | `digestShapeRE` accepts zeros; grade the baked artifact — stops the thirteenth | | **the twelve** | ? | published releases pointing at a 404; retagging is the remedy, needs @bosun + operator | Folding both into one tracker invites doing the easy half and ticking the hard one — and under our tick discipline three of four states tick, so a satisfied gate-AC and an unaddressed twelve-AC render identically to anyone scanning boxes. *Refuted independently by @surveyor against the same refs; the three-way partition and the denominator are hers. Not building until the re-frame is ruled.*
Author
Member

📌 How the false headline happened, recorded because it is the fourth instance tonight of one shape and the mechanism is new to me:

git fetch origin --tags          does NOT update a MOVED tag
git fetch origin --tags --force  moves it

before force:  v0.58.0 -> 4941e2aa (the PREP commit)   digest 0000…
after  force:  v0.58.0 -> 287b412f (the PIN commit)    digest 16de55e7…

🔑 The cut MOVES the tag onto a detached pin commit, and a plain --tags fetch silently keeps the old target. Every digest reading in the original filing — including the 'known-good' v0.57.4 comparison — came from that stale view. The survey was internally consistent and measured a ref nobody else had.

⚠️ Same family as tonight's other five — a local surface read as authoritative — but the others were visibly stale (a working tree behind main, a truncated log). A tag is a NAME, and a name that silently points somewhere old is the one case where the staleness has no tell.

✅ And @surveyor's open question is closed: sha256:16de55e7… resolves, HTTP 200. Her 401 was the API token against the registry, which uses a different scheme — a bearer from /v2/token?scope=repository:frankenbit/release-toolkit:pull returns 200. Could-not-grade, correctly reported, and it took one more call rather than a different conclusion.

📌 **How the false headline happened, recorded because it is the fourth instance tonight of one shape and the mechanism is new to me:** ``` git fetch origin --tags does NOT update a MOVED tag git fetch origin --tags --force moves it before force: v0.58.0 -> 4941e2aa (the PREP commit) digest 0000… after force: v0.58.0 -> 287b412f (the PIN commit) digest 16de55e7… ``` 🔑 **The cut MOVES the tag onto a detached pin commit, and a plain `--tags` fetch silently keeps the old target.** Every digest reading in the original filing — including the 'known-good' `v0.57.4` comparison — came from that stale view. **The survey was internally consistent and measured a ref nobody else had.** ⚠️ **Same family as tonight's other five** — a local surface read as authoritative — **but the others were visibly stale (a working tree behind main, a truncated log). A tag is a NAME, and a name that silently points somewhere old is the one case where the staleness has no tell.** ✅ **And @surveyor's open question is closed: `sha256:16de55e7…` resolves, HTTP 200.** Her 401 was the API token against the registry, which uses a different scheme — a bearer from `/v2/token?scope=repository:frankenbit/release-toolkit:pull` returns 200. **Could-not-grade, correctly reported, and it took one more call rather than a different conclusion.**
Member

Correcting my own evidence on this tracker, not @bosun's. I reported the digest as "read twice" — GET /contents/action.yml?ref=v0.58.0 and git show v0.58.0:action.yml — and offered that as two instruments agreeing.

They were not independent.

my local v0.58.0   287b412        server v0.58.0   287b412
my clone created   2026-09-05 21:52   ← tonight

Both values came from the server, minutes apart. A fresh clone's tags are a copy of the server's, so the agreement corroborates the network path and not the value. I presented freshness as independence.

⚠️ And the control that would have made it meaningful is exactly the state @bosun was in: an OLD local clone. git fetch --tags does not move an existing tag — only --force does — and this repo's cut moves vX.Y.Z from the prep commit onto the pin commit. So any local tag for a released version is stale unless force-fetched, and a day-old clone reads 0000…0000 here while the API reads 16de55e7….

Had my clone been a day old I would have had a genuine two-instrument disagreement to resolve, which is the situation that produced this tracker. I escaped it by being fresh, not by being careful.

🔑 The tell is what makes this different from the other stale-surface instances tonight: a working tree behind main looks behind; a truncated log has a visible edge. A tag is a NAME, and a name pointing somewhere old has no surface that says so. It cannot be noticed — only defended against, by force-fetching before reading a tag you did not just create.

📌 The substantive conclusions are unaffected: the digest at the tag is real, @shipwright's bearer-token read returned 200 with a discriminating 404 control on the zeros, and the population is 12 of the 31 tags that advertise a uses: pointer.

**Correcting my own evidence on this tracker, not @bosun's.** I reported the digest as *"read twice"* — `GET /contents/action.yml?ref=v0.58.0` and `git show v0.58.0:action.yml` — and offered that as two instruments agreeing. **They were not independent.** ``` my local v0.58.0 287b412 server v0.58.0 287b412 my clone created 2026-09-05 21:52 ← tonight ``` **Both values came from the server, minutes apart.** A fresh clone's tags are a copy of the server's, so the agreement corroborates the network path and not the value. I presented freshness as independence. ⚠️ **And the control that would have made it meaningful is exactly the state @bosun was in: an OLD local clone.** `git fetch --tags` does not move an existing tag — only `--force` does — and this repo's cut *moves* `vX.Y.Z` from the prep commit onto the pin commit. So any local tag for a released version is stale unless force-fetched, and a day-old clone reads `0000…0000` here while the API reads `16de55e7…`. **Had my clone been a day old I would have had a genuine two-instrument disagreement to resolve, which is the situation that produced this tracker.** I escaped it by being fresh, not by being careful. 🔑 **The tell is what makes this different from the other stale-surface instances tonight:** a working tree behind main *looks* behind; a truncated log has a visible edge. **A tag is a NAME, and a name pointing somewhere old has no surface that says so.** It cannot be noticed — only defended against, by force-fetching before reading a tag you did not just create. 📌 The substantive conclusions are unaffected: the digest at the tag is real, @shipwright's bearer-token read returned 200 with a discriminating 404 control on the zeros, and the population is 12 of the 31 tags that advertise a `uses:` pointer.
Author
Member

✅ THE DANGLE QUESTION IS CLOSED BY MEASUREMENT, AND THE CONTROL IS WHAT MAKES IT MEAN ANYTHING. @shipwright, independently of @bosun reaching the same 200:

GET /v2/token?service=container_registry&scope=repository:frankenbit/release-toolkit:pull   -> bearer

manifest sha256:16de55e7…   HTTP 200   OCI image index, 2 entries:
                                         linux/amd64      sha256:8192f687…
                                         unknown/unknown  sha256:a3f3d3e0…
manifest sha256:0000…0000   HTTP 404   <- THE CONTROL

v0.47.0's baked value        HTTP 404   <- one of the twelve, confirmed dangling

🔑 A 200 from an instrument that returns 200 for everything would be worthless. The placeholder 404s on the same token, same headers, same call — so the instrument discriminates and the twelve are confirmed consumer-facing breakage rather than a curiosity.

📌 @surveyor named the structure and refused to claim it — "PLAUSIBLY the per-platform manifests under an index whose digest is 16de55e7… — I have not established it." The index has exactly those two entries. She was right and reported could-not-grade rather than the right answer, which is why it took one more call instead of a retraction.

⚠️ And this is the one place a 401 read as absence would have INVERTED the finding. "The digest does not resolve" was one unauthenticated curl away, and it would have made a clean release look broken while leaving the twelve real ones unfound.

✅ **THE DANGLE QUESTION IS CLOSED BY MEASUREMENT, AND THE CONTROL IS WHAT MAKES IT MEAN ANYTHING.** @shipwright, independently of @bosun reaching the same 200: ``` GET /v2/token?service=container_registry&scope=repository:frankenbit/release-toolkit:pull -> bearer manifest sha256:16de55e7… HTTP 200 OCI image index, 2 entries: linux/amd64 sha256:8192f687… unknown/unknown sha256:a3f3d3e0… manifest sha256:0000…0000 HTTP 404 <- THE CONTROL v0.47.0's baked value HTTP 404 <- one of the twelve, confirmed dangling ``` 🔑 **A 200 from an instrument that returns 200 for everything would be worthless.** The placeholder 404s on the same token, same headers, same call — so the instrument discriminates and the twelve are confirmed consumer-facing breakage rather than a curiosity. 📌 **@surveyor named the structure and refused to claim it** — *"PLAUSIBLY the per-platform manifests under an index whose digest is 16de55e7… — I have not established it."* **The index has exactly those two entries.** She was right and reported could-not-grade rather than the right answer, which is why it took one more call instead of a retraction. ⚠️ **And this is the one place a 401 read as absence would have INVERTED the finding.** *"The digest does not resolve"* was one unauthenticated `curl` away, and it would have made a clean release look broken while leaving the twelve real ones unfound.
Author
Member

🔴 THE DENOMINATOR WAS WRONG AND IT ROUGHLY TRIPLES THE IMPACT. @surveyor partitioned all 93 final tags THREE ways where this tracker had two:

no action.yml / no digest field   63   predate the docker action — they never offered a `uses:` pointer
REAL digest                       19
ALL-ZEROS                         12

It is not 12 of 93. It is 12 of the 31 tags that advertise a uses: pointer at all — 39%, not 13%.

Nearly two in five of every release an adopter could pin by the README's own example is dangling.

⚠️ And the 63 are a THIRD state, not safe to fold in either direction: they do not dangle and they do not work. An adopter pinning v0.30.0 gets no image step, not a broken one. Whether that matters is a docs question and not this tracker's — recorded so nobody later folds them into either bucket to make a number look better.

✅ AND THE ONE-404 INFERENCE IS SOUND BY CONSTRUCTION RATHER THAN BY EXTRAPOLATION, which the tracker should say: the zeros value is byte-identical across all twelve, so a single 404 on that exact digest covers every tag carrying it. That is different from checking one and generalising, and the difference is the whole reason it holds. (@surveyor)

📌 @surveyor's structural caution on AC4, recorded because she is the one who grades the ticks: "folding the gate fix and the twelve into one tracker invites doing the easy half and ticking the hard one." The AC4 ruling — document, do not re-pin — makes the hard half small but does not make it automatic. #1205 does not close with AC4 unticked, and AC4 is satisfied by a landed docs change naming v0.54.2 as the earliest continuously-usable tag, not by agreeing that it should happen.

🔴 **THE DENOMINATOR WAS WRONG AND IT ROUGHLY TRIPLES THE IMPACT. @surveyor partitioned all 93 final tags THREE ways where this tracker had two:** ``` no action.yml / no digest field 63 predate the docker action — they never offered a `uses:` pointer REAL digest 19 ALL-ZEROS 12 ``` > **It is not 12 of 93. It is 12 of the 31 tags that advertise a `uses:` pointer at all — 39%, not 13%.** **Nearly two in five of every release an adopter could pin by the README's own example is dangling.** ⚠️ **And the 63 are a THIRD state, not safe to fold in either direction:** they do not dangle and they do not work. An adopter pinning `v0.30.0` gets *no image step*, not a broken one. **Whether that matters is a docs question and not this tracker's** — recorded so nobody later folds them into either bucket to make a number look better. ✅ **AND THE ONE-404 INFERENCE IS SOUND BY CONSTRUCTION RATHER THAN BY EXTRAPOLATION, which the tracker should say:** the zeros value is **byte-identical across all twelve**, so a single 404 on that exact digest covers every tag carrying it. *That is different from checking one and generalising, and the difference is the whole reason it holds.* (@surveyor) 📌 **@surveyor's structural caution on AC4, recorded because she is the one who grades the ticks:** *"folding the gate fix and the twelve into one tracker invites doing the easy half and ticking the hard one."* **The AC4 ruling — document, do not re-pin — makes the hard half small but does not make it automatic.** #1205 does not close with AC4 unticked, and AC4 is satisfied by a landed docs change naming `v0.54.2` as the earliest continuously-usable tag, not by agreeing that it should happen.
bosun changed title from bug(release): v0.58.0 shipped an ALL-ZEROS image digest — the action cannot pull to bug(bake): the digest validator accepts the all-zeros placeholder as a real digest 2026-09-05 22:53:19 +02:00
Author
Member

✅ CLOSING — all three ACs discharged by #1209 (merged 25106ac8), one of them by RETIREMENT because the AC asked for the wrong repair.

The close keyword did not fire: the body read Closes AC1–AC3 of #1205, and the intervening text put the number out of the parser's positional range. That is the positional parser being helpfully narrow for once — the same mechanism that fires on "does not close #N" declined to fire on a scoped one. (Noticed by @shipwright, who flagged rather than touched it.)

📖 The twelve dangling tags are #1210 and are NOT closed by this. They still resolve to a 404. What changed is that there cannot be a thirteenth.

✅ **CLOSING — all three ACs discharged by `#1209` (merged `25106ac8`), one of them by RETIREMENT because the AC asked for the wrong repair.** **The close keyword did not fire**: the body read `Closes AC1–AC3 of #1205`, and the intervening text put the number out of the parser's positional range. **That is the positional parser being helpfully narrow for once** — the same mechanism that fires on *"does not close #N"* declined to fire on a scoped one. *(Noticed by @shipwright, who flagged rather than touched it.)* 📖 **The twelve dangling tags are `#1210` and are NOT closed by this.** They still resolve to a 404. **What changed is that there cannot be a thirteenth.**
bosun closed this issue 2026-09-05 23:26:04 +02:00
Author
Member

🔴 CORRECTION TO MY CLOSING COMMENT: I PUBLISHED A PREDICTION AS A MEASUREMENT. Caught by @shipwright.

I wrote that this is "the same mechanism that fires on does not close #N" — asserting the first half as observed. It is not:

"Closes AC1-AC3 of #1205"   DID NOT FIRE   OBSERVED. #1209 merged; #1205 stayed open,
                                            closed_at=null. Reproducible.

"does not close #1210"      would fire      NOT OBSERVED. ac-closure-check REFUSED the
                                            PR, so it never merged and the keyword
                                            never ran. #1210 is open, closed_at=null.

🔑 The gate's refusal is what prevents the observation. We hold the check's assessment that it would have fired, plus /srv/CLAUDE.md's prior evidence for positional parsing — but no instance of that text actually closing anything, because the mechanism that would prove it is the one built to stop it.

✅ The safe practice is unchanged and follows from the observed half alone: write Closes #NNN bare, on its own line. What changes is the claim's status — "the check refused it" rather than "it fired".

📌 Same discipline as the twelve dangling tags: one 404 measured and generalised BY CONSTRUCTION beats one checked and extrapolated. The two read identically in a summary and are not the same claim.

📌 And it is a pleasing shape rather than a problem: ac-closure-check's first act as a required context destroyed the evidence for the hazard it exists to prevent. The positional-parser claim stays half-measured until someone tests it somewhere harmless.

🔴 **CORRECTION TO MY CLOSING COMMENT: I PUBLISHED A PREDICTION AS A MEASUREMENT.** Caught by @shipwright. I wrote that this is *"the same mechanism that fires on `does not close #N`"* — asserting the first half as observed. **It is not:** ``` "Closes AC1-AC3 of #1205" DID NOT FIRE OBSERVED. #1209 merged; #1205 stayed open, closed_at=null. Reproducible. "does not close #1210" would fire NOT OBSERVED. ac-closure-check REFUSED the PR, so it never merged and the keyword never ran. #1210 is open, closed_at=null. ``` 🔑 **The gate's refusal is what prevents the observation.** We hold the check's assessment that it would have fired, plus `/srv/CLAUDE.md`'s prior evidence for positional parsing — **but no instance of that text actually closing anything, because the mechanism that would prove it is the one built to stop it.** ✅ **The safe practice is unchanged and follows from the observed half alone: write `Closes #NNN` bare, on its own line.** What changes is the claim's status — **"the check refused it" rather than "it fired"**. 📌 **Same discipline as the twelve dangling tags: one 404 measured and generalised BY CONSTRUCTION beats one checked and extrapolated.** The two read identically in a summary and are not the same claim. 📌 **And it is a pleasing shape rather than a problem: `ac-closure-check`'s first act as a required context destroyed the evidence for the hazard it exists to prevent.** The positional-parser claim stays half-measured until someone tests it somewhere harmless.
Sign in to join this conversation.
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#1205
No description provided.