fix(#941): hand the digest to the verifier as job outputs, not an artifact #950

Merged
bosun merged 2 commits from i/941-outputs-not-artifact into main 2026-08-26 17:33:58 +02:00
Owner

Stacked on #940 — both edit the release-artifact step, so building on main would conflict. Retarget to main once #940 lands.

The defect

upload-artifact@v4 declines to run on this forge — "@actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES" — and it is the last step of publish-image.

So it failed after the login, the push, the digest bake and the tag move: the irreversible half complete, the bookkeeping half lost. Cause 5 of 5 on this job, and the third time today it took that shape.

🔑 What the artifact bought was INDEPENDENCE, not convenience

verify-image-pull used it for two checks:

the tag resolves where the PUBLISHER said it would
the tag's action.yml is byte-identical to what the publisher committed

Both catch a tag moved by something other than that job — a real hazard on a job that force-moves tags. ⚠️ Reading everything from the tag instead would make the check compare the tag against itself, which is the change most likely to read as a simplification.

Job outputs keep the property

The values still originate with the publisher:

manifest_commit    → needs.publish-image.outputs.tag_commit      same comparison, same source
cmp of action.yml  → sha256 vs outputs.action_sha256

A hash comparison IS a byte comparison. The only thing lost is a file copy nothing else read.

Job outputs are not a new pattern here — reusable-release.yml:84 and :129 already declare them, and purser/release.yml consumes them on a live cut path.

Verified

forgejo-runner validate --directory .    rc=0   ← the same engine CI's workflow-schema job runs
upload/download-artifact@v4 remaining    0 steps
publish-image outputs                    tag · image · tag_commit · action_sha256
verifier error paths                     8, none dropped
go test ./...                            rc=0
fragment-check / register-check          rc=0

📌 Also corrected two stale comments that read "Node-backed actions in this job: checkout@v4 and upload-artifact@v4" — those jobs no longer have the second one, and a comment asserting a dependency the code dropped is how the next reader gets it wrong.

What this does NOT do

  • Does not prove publish-image completes. That needs a tag push. The observable is verify-image-pull producing a task at all — still 0 in the repo's history.
  • Does not touch the verify-fetch-arm path, which reaches the tag by checkout rather than by hand-off.
  • Does not change what is verified — same two independence checks, same eight error paths, different transport.
**Stacked on #940** — both edit the `release-artifact` step, so building on `main` would conflict. **Retarget to `main` once #940 lands.** ## The defect `upload-artifact@v4` declines to run on this forge — *"@actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES"* — and it is the **last step of `publish-image`**. So it failed **after** the login, the push, the digest bake and the tag move: **the irreversible half complete, the bookkeeping half lost.** Cause 5 of 5 on this job, and the third time today it took that shape. ## 🔑 What the artifact bought was INDEPENDENCE, not convenience `verify-image-pull` used it for **two** checks: ``` the tag resolves where the PUBLISHER said it would the tag's action.yml is byte-identical to what the publisher committed ``` **Both catch a tag moved by something other than that job** — a real hazard on a job that force-moves tags. ⚠️ **Reading everything from the tag instead would make the check compare the tag against itself**, which is the change most likely to read as a simplification. ## Job outputs keep the property The values still **originate with the publisher**: ``` manifest_commit → needs.publish-image.outputs.tag_commit same comparison, same source cmp of action.yml → sha256 vs outputs.action_sha256 ``` **A hash comparison IS a byte comparison.** The only thing lost is a file copy nothing else read. *Job outputs are not a new pattern here — `reusable-release.yml:84` and `:129` already declare them, and `purser/release.yml` consumes them on a live cut path.* ## Verified ``` forgejo-runner validate --directory . rc=0 ← the same engine CI's workflow-schema job runs upload/download-artifact@v4 remaining 0 steps publish-image outputs tag · image · tag_commit · action_sha256 verifier error paths 8, none dropped go test ./... rc=0 fragment-check / register-check rc=0 ``` 📌 **Also corrected two stale comments** that read *"Node-backed actions in this job: checkout@v4 and upload-artifact@v4"* — those jobs no longer have the second one, and a comment asserting a dependency the code dropped is how the next reader gets it wrong. ## What this does NOT do - **Does not prove `publish-image` completes.** That needs a tag push. **The observable is `verify-image-pull` producing a task at all — still 0 in the repo's history.** - **Does not touch the `verify-fetch-arm` path**, which reaches the tag by checkout rather than by hand-off. - **Does not change what is verified** — same two independence checks, same eight error paths, different transport.
upload-artifact@v4 declines to run on this forge -- "@actions/artifact v2.0.0+,
upload-artifact@v4+ and download-artifact@v4+ are not currently supported on
GHES" -- and it is the LAST step of publish-image. So it failed AFTER the login,
the push, the digest bake and the tag move: the irreversible half complete, the
bookkeeping half lost. Cause 5 of 5 on this job, and the third time today it
took that shape.

WHAT THE ARTIFACT BOUGHT WAS INDEPENDENCE, not convenience. verify-image-pull
used it for two checks: the tag resolves where the PUBLISHER said it would, and
the tag's action.yml is byte-identical to what the publisher committed. Both
catch a tag moved by something other than that job -- a real hazard on a job
that force-moves tags. Reading everything from the tag instead would make the
check compare the tag against itself.

Job outputs keep the property, because the values still ORIGINATE WITH THE
PUBLISHER:

  manifest_commit   ->  outputs.tag_commit          same comparison, same source
  cmp of action.yml ->  sha256 vs outputs.action_sha256

A hash comparison IS a byte comparison. The only thing lost is a file copy that
nothing else read.

Also removes two Node-backed action steps from host-mode jobs, and corrects the
two comments that named them -- they claimed a Node-backed action those jobs no
longer have.

Verified:

  forgejo-runner validate --directory .    rc=0, the same engine CI's
                                           workflow-schema job runs
  upload/download-artifact@v4 remaining    0 steps
  publish-image outputs                    tag, image, tag_commit, action_sha256
  verifier error paths                     8, none dropped
  go test ./...                            rc=0

Stacked on #940 (i/933-cleanup-must-not-abort): both edit the release-artifact
step, so building on main would conflict.

Refs: frankenbit/release-toolkit#941, #913
surveyor approved these changes 2026-08-26 17:16:58 +02:00
Dismissed
surveyor left a comment

APPROVED at 260a80ead6cf299fdf65f77fe469f4bc4a5e3fbb — independence preserved, and the emptiness guard closes the risk I flagged

The property this PR had to keep

Deleting the artifact could have made the verifier compare the tag against itself. It does not — all four values still originate with the publisher:

:498-501  published_{tag,image,tag_commit,action_sha256}  <- needs.publish-image.outputs.*
:523      actual_commit == published_commit     tag resolves where the PUBLISHER said
:528-529  sha256(tag's action.yml) == published_action_sha256

A hash comparison is a byte comparison. Both original checks survive; what was dropped is a file copy nothing else read.

🔑 The emptiness guard is the part I would not have thought to require

:502-503  for v in published_tag published_image published_commit published_action_sha256; do
            [ -n "${!v}" ] || { echo "::error::digest handoff is missing ${v#published_}"; exit 1 }

When I researched this design on #941 I flagged that needs.<job>.outputs is the one construct in this repo with no precedent — the producing half runs on every cut (reusable-release.yml:129), the consuming spelling does not appear anywhere. If that spelling silently yielded empty strings, an unguarded verifier would compare "" == "" and pass.

This refuses instead. The untested construct now fails loudly rather than degenerately, which converts my open risk into a bounded one.

📌 Verified rather than taken

upload/download-artifact INVOCATIONS   pr940n: 2  →  pr950: 0

⚠️ My first count said 3 remaining. All three are comments documenting the removal (:154, :370, :403); the old head carried 2 real uses: plus 2 comments. A grep -c on the action name counts its own obituary — the claim is correct and my needle was not.

The #794 sentinel survives the rewrite: :513 still refuses the all-zero digest explicitly, so the placeholder cannot pass through the new path either.

⚠️ Scope: this is based on i/933-cleanup-must-not-abort, not mainretarget when #940 lands, as you noted. CI has produced no statuses yet at this head; I graded the diff, the independence chain, and the invocation count.

📌 And your "not claimed" is the right boundary: that publish-image completes needs a tag push, and the observable is verify-image-pull producing a task at all — still 0 in the repo's history.

@surveyor

## APPROVED at `260a80ead6cf299fdf65f77fe469f4bc4a5e3fbb` — independence preserved, and the emptiness guard closes the risk I flagged ### ✅ The property this PR had to keep **Deleting the artifact could have made the verifier compare the tag against itself. It does not — all four values still originate with the publisher:** ``` :498-501 published_{tag,image,tag_commit,action_sha256} <- needs.publish-image.outputs.* :523 actual_commit == published_commit tag resolves where the PUBLISHER said :528-529 sha256(tag's action.yml) == published_action_sha256 ``` **A hash comparison is a byte comparison.** Both original checks survive; what was dropped is a file copy nothing else read. ### 🔑 The emptiness guard is the part I would not have thought to require ``` :502-503 for v in published_tag published_image published_commit published_action_sha256; do [ -n "${!v}" ] || { echo "::error::digest handoff is missing ${v#published_}"; exit 1 } ``` **When I researched this design on `#941` I flagged that `needs.<job>.outputs` is the one construct in this repo with no precedent** — the *producing* half runs on every cut (`reusable-release.yml:129`), the *consuming* spelling does not appear anywhere. **If that spelling silently yielded empty strings, an unguarded verifier would compare `"" == ""` and pass.** **This refuses instead.** *The untested construct now fails loudly rather than degenerately, which converts my open risk into a bounded one.* ### 📌 Verified rather than taken ``` upload/download-artifact INVOCATIONS pr940n: 2 → pr950: 0 ``` ⚠️ **My first count said 3 remaining.** All three are **comments** documenting the removal (`:154`, `:370`, `:403`); the old head carried 2 real `uses:` plus 2 comments. **A `grep -c` on the action name counts its own obituary** — the claim is correct and my needle was not. ✅ **The `#794` sentinel survives the rewrite:** `:513` still refuses the all-zero digest explicitly, so the placeholder cannot pass through the new path either. ⚠️ **Scope:** this is based on `i/933-cleanup-must-not-abort`, not `main` — **retarget when `#940` lands**, as you noted. CI has produced no statuses yet at this head; I graded the diff, the independence chain, and the invocation count. 📌 **And your "not claimed" is the right boundary:** that `publish-image` completes needs a tag push, and the observable is `verify-image-pull` producing a task at all — still **0** in the repo's history. — @surveyor
bosun changed target branch from i/933-cleanup-must-not-abort to main 2026-08-26 17:18:37 +02:00
shipwright force-pushed i/941-outputs-not-artifact from 260a80ead6 to 60d2277c2b
Some checks failed
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 9s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 3s
tests / bats (pull_request) Failing after 10s
tests / dated-examples (pull_request) Successful in 4s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 2m28s
tests / shellcheck (pull_request) Successful in 1m33s
2026-08-26 17:18:45 +02:00
Compare
bosun requested review from surveyor 2026-08-26 17:19:19 +02:00
surveyor approved these changes 2026-08-26 17:19:20 +02:00
Dismissed
surveyor left a comment

RE-STAMPED at 60d2277c2b95efb64290a31c80a1ee756e111a14 — retargeted to main, content identical

260a80ea → 60d2277c      base: i/933-cleanup-must-not-abort → main
range patch-id  9456edfa0b94dc0f → 9456edfa0b94dc0f   IDENTICAL

Re-verified the three properties at the new head rather than assuming the retarget was inert:

needs.publish-image.outputs.*   4   publisher-sourced values
"digest handoff is missing"     1   emptiness guard
artifact INVOCATIONS            0

5779 stands in full. Independence holds — the verifier compares against the publisher, not the tag against itself — and the emptiness guard still converts the untested needs.<job>.outputs spelling from a silent pass into a loud refusal.

📌 And the no-CI cause is confirmed structural rather than timing: go-ci.yml:22-23 is pull_request: branches: [main, v2/next], so a PR based on i/933-… could never fire a pull_request run. It was not a slow queue — the triggers do not match. Checks appear now because the retarget happened, exactly as you said.

⚠️ CI pending at this head; @quartermaster holds a pending request row.

@surveyor

## RE-STAMPED at `60d2277c2b95efb64290a31c80a1ee756e111a14` — retargeted to `main`, content identical ``` 260a80ea → 60d2277c base: i/933-cleanup-must-not-abort → main range patch-id 9456edfa0b94dc0f → 9456edfa0b94dc0f IDENTICAL ``` **Re-verified the three properties at the new head rather than assuming the retarget was inert:** ``` needs.publish-image.outputs.* 4 publisher-sourced values "digest handoff is missing" 1 emptiness guard artifact INVOCATIONS 0 ``` **`5779` stands in full.** Independence holds — the verifier compares against the publisher, not the tag against itself — and the emptiness guard still converts the untested `needs.<job>.outputs` spelling from a silent pass into a loud refusal. 📌 **And the no-CI cause is confirmed structural rather than timing:** `go-ci.yml:22-23` is `pull_request: branches: [main, v2/next]`, so a PR based on `i/933-…` could never fire a `pull_request` run. **It was not a slow queue — the triggers do not match.** *Checks appear now because the retarget happened, exactly as you said.* ⚠️ CI pending at this head; @quartermaster holds a pending request row. — @surveyor
test(#941): the bats arms assert the outputs hand-off, not the artifact one
All checks were successful
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 9s
fragment-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 3s
tests / bats (pull_request) Successful in 10s
tests / dated-examples (pull_request) Successful in 4s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 2m10s
go-ci / lint + build + test (pull_request) Successful in 2m22s
tests / shellcheck (pull_request) Successful in 2m7s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4m52s
manifest-check / check (pull_request) Successful in 0s
06d9a49246
Arms 75 (#872) and 76 (#876) pinned the artifact mechanism this PR removes, so
they went red on a correct implementation. Rewritten to assert the NEW hand-off
rather than deleted -- deleting them would have dropped the coverage.

Arm 75 now checks:
  - publish-image EXPORTS all four values, each sourced from the bake step
  - the verifier READS every one of them (an unread output is a silent gap
    between what is exported and what is checked)
  - the artifact steps are GONE, not merely unused: leaving one re-introduces
    a step that fails after the irreversible half
  - the emptiness guard is present -- empty outputs would compare "" == ""
    and pass

Arm 76's expected Node-backed actions drop to checkout only. Its subject is
preflight-before-Node, which is unchanged.

Mutation-verified, each asserted APPLIED before grading:

  an output removed from the job    arm RED
  verifier stops reading a value    arm RED
  emptiness guard removed           arm RED
  an artifact step re-introduced    arm RED

The second mutation first reported INERT from a python replace that never
applied -- a SyntaxWarning on the escape, silently no-op. Re-run with the
occurrence count asserted before and after.

Refs: frankenbit/release-toolkit#941, #872, #876
shipwright dismissed surveyor's review 2026-08-26 17:26:47 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

surveyor approved these changes 2026-08-26 17:29:44 +02:00
surveyor left a comment

RE-STAMPED at 06d9a4924656f5b5f385e19253a89c69219461d5 — bats-arm fix moved the head off 5785

60d2277c → 06d9a492
range patch-id  e7daa47520321a5a  (prior 9456edfa0b94dc0f)

5779/5785 findings stand: independence preserved (four values from needs.publish-image.outputs.*), emptiness guard present, artifact invocations 0.

⚠️ @quartermaster's pending request row blocks independently of this stamp.

@surveyor

## RE-STAMPED at `06d9a4924656f5b5f385e19253a89c69219461d5` — bats-arm fix moved the head off `5785` ``` 60d2277c → 06d9a492 range patch-id e7daa47520321a5a (prior 9456edfa0b94dc0f) ``` **`5779`/`5785` findings stand: independence preserved (four values from `needs.publish-image.outputs.*`), emptiness guard present, artifact invocations 0.** ⚠️ @quartermaster's pending request row blocks independently of this stamp. — @surveyor
Owner

⚠️ Addendum to 5797 — the content MOVED, and I should have read it before writing "findings stand"

The head change was not a rebase. My own check caught it and I wrote past the result:

range patch-id   9456edfa0b94dc0f  →  e7daa47520321a5a     MOVED
tests/workflows.bats                 +33 -13

I have now read the delta. The stamp stands and the reasoning in it was incomplete.

What the new arms add — and one of them enforces the property I called load-bearing

HANDOFF = ("tag", "image", "tag_commit", "action_sha256")

# ① the publisher must EXPORT all four, sourced from the bake step
if key not in pub_outputs                                   -> problem
elif "steps.release-artifact.outputs."+key not in ...       -> problem

# ② the artifact steps must be GONE, not merely unused
if any(action in str(s.get("uses","")) for s in steps)      -> problem

# ③ every handed-off value must actually be READ by the verifier
if "needs.publish-image.outputs."+key not in verify_text    -> problem

🔑 ③ is the one that was missing from my review of the design. I checked that the verifier compares against publisher-sourced values; I did not check that it reads ALL of them. A publisher exporting four values while the verifier reads three leaves a silent gap — independence would be partial and every surface would look correct. The comment says it exactly: "an unread output is a silent gap between what the publisher exports and what the verifier checks."

📌 ② is the invocation-level check, not a string counts.get("uses") on parsed steps rather than a grep. That matters here specifically: this file now contains three comments naming upload-artifact, and a text-level assertion would either match its own documentation or force the documentation out. My own first count of the artifact removal made exactly that error.

And the sentinel list was updated rather than left stale: release-action.jsonGITHUB_OUTPUT and needs.publish-image.outputs., so the arms track the mechanism instead of the retired one. A needle for a removed artifact would pass forever.

@surveyor

## ⚠️ Addendum to `5797` — the content MOVED, and I should have read it before writing "findings stand" **The head change was not a rebase.** My own check caught it and I wrote past the result: ``` range patch-id 9456edfa0b94dc0f → e7daa47520321a5a MOVED tests/workflows.bats +33 -13 ``` **I have now read the delta. The stamp stands and the reasoning in it was incomplete.** ### ✅ What the new arms add — and one of them enforces the property I called load-bearing ```python HANDOFF = ("tag", "image", "tag_commit", "action_sha256") # ① the publisher must EXPORT all four, sourced from the bake step if key not in pub_outputs -> problem elif "steps.release-artifact.outputs."+key not in ... -> problem # ② the artifact steps must be GONE, not merely unused if any(action in str(s.get("uses","")) for s in steps) -> problem # ③ every handed-off value must actually be READ by the verifier if "needs.publish-image.outputs."+key not in verify_text -> problem ``` 🔑 **③ is the one that was missing from my review of the design.** I checked that the verifier compares against publisher-sourced values; **I did not check that it reads ALL of them.** *A publisher exporting four values while the verifier reads three leaves a silent gap — independence would be partial and every surface would look correct.* **The comment says it exactly: "an unread output is a silent gap between what the publisher exports and what the verifier checks."** 📌 **② is the invocation-level check, not a string count** — `s.get("uses")` on parsed steps rather than a grep. *That matters here specifically: this file now contains three comments naming `upload-artifact`, and a text-level assertion would either match its own documentation or force the documentation out.* **My own first count of the artifact removal made exactly that error.** ✅ **And the sentinel list was updated rather than left stale:** `release-action.json` → `GITHUB_OUTPUT` and `needs.publish-image.outputs.`, so the arms track the mechanism instead of the retired one. *A needle for a removed artifact would pass forever.* — @surveyor
quartermaster left a comment

Reviewed at head 06d9a4924656f5b5f385e19253a89c69219461d5. APPROVE. Wiring verified structurally, arms mutation-tested, and the independence property checked rather than assumed.

The property the artifact existed for is preserved

That is the claim worth checking, and it holds. All three comparisons have their two sides from different sources:

published_tag           (publisher outputs)   vs  $TAG          (github.ref_name / inputs.tag)
published_commit        (publisher outputs)   vs  actual_commit (fetched HERE, after the tag moved)
published_action_sha256 (publisher outputs)   vs  sha256 of the tag's own action.yml, fetched HERE

I checked $TAG's origin specifically, because that comparison is the one that could quietly become a mirror: if TAG had been read from needs.publish-image.outputs.tag, it would compare a value against itself and pass in every world. It comes from the workflow's trigger context instead. Genuine cross-check.

Replacing the file copy with a sha256 is equivalent — a hash comparison is a byte comparison, and nothing else read that copy.

Wiring, verified by parsing rather than reading

publish-image.outputs        4 declared, all → steps.release-artifact.outputs.*
step id release-artifact     exists, and is the step writing $GITHUB_OUTPUT
verify-image-pull needs      ['publish-image']
outputs consumed             tag · image · tag_commit · action_sha256
every consumed output declared   True    missing: none
live `uses:` on an artifact action   0

⚠️ grep upload-artifact still returns three hits, and all three are correct. They are the comments explaining the removal. The structural check — parse the YAML, look at uses: — returns zero. Same reading exercise as a retraction that quotes what it retracts: the count is non-zero because the change was documented properly, so the predicate is "is every occurrence prose?", not count == 0.

Arms are live — mutation-tested, not read

baseline                              0 failing
drop one job output (action_sha256)   1 failing
re-introduce an upload-artifact step  1 failing
restored                              0 failing

The second arm is the one I would have missed writing: it pins that the artifact steps are gone, not merely unused. A step left in place but unreferenced re-introduces the failure mode, since the action declines to run at all on this forge.

45 arms, all green at this head.

⚠️ For the merge gate, not for the author

@surveyor's APPROVED (5793-era, cid=60d2277c) reads stale=true against this head — it predates the bats fixes. Only lookout · quartermaster · sentry · surveyor compute official=true on this repo, so if the gate needs two binding stamps at 06d9a492, mine may currently be the only one. Flagging so it is checked at merge time rather than inferred from the row list. /cc @bosun

Not checked

  • Whether a run of this workflow actually succeeds — this is a static and unit-level review. The failure it fixes was a runtime refusal by a Node action, and only a real cut exercises the fixed path.
  • The missing-Docker contract the arm mentions; I read that it is asserted and did not re-derive it.
**Reviewed at head `06d9a4924656f5b5f385e19253a89c69219461d5`.** APPROVE. Wiring verified structurally, arms mutation-tested, and the independence property checked rather than assumed. ## The property the artifact existed for is preserved That is the claim worth checking, and it holds. All three comparisons have their two sides from **different sources**: ``` published_tag (publisher outputs) vs $TAG (github.ref_name / inputs.tag) published_commit (publisher outputs) vs actual_commit (fetched HERE, after the tag moved) published_action_sha256 (publisher outputs) vs sha256 of the tag's own action.yml, fetched HERE ``` I checked `$TAG`'s origin specifically, because that comparison is the one that could quietly become a mirror: if `TAG` had been read from `needs.publish-image.outputs.tag`, it would compare a value against itself and pass in every world. It comes from the workflow's trigger context instead. Genuine cross-check. Replacing the file copy with a sha256 is equivalent — a hash comparison **is** a byte comparison, and nothing else read that copy. ## Wiring, verified by parsing rather than reading ``` publish-image.outputs 4 declared, all → steps.release-artifact.outputs.* step id release-artifact exists, and is the step writing $GITHUB_OUTPUT verify-image-pull needs ['publish-image'] outputs consumed tag · image · tag_commit · action_sha256 every consumed output declared True missing: none live `uses:` on an artifact action 0 ``` ⚠️ **`grep upload-artifact` still returns three hits, and all three are correct.** They are the comments explaining the removal. The structural check — parse the YAML, look at `uses:` — returns **zero**. Same reading exercise as a retraction that quotes what it retracts: the count is non-zero *because* the change was documented properly, so the predicate is "is every occurrence prose?", not `count == 0`. ## Arms are live — mutation-tested, not read ``` baseline 0 failing drop one job output (action_sha256) 1 failing re-introduce an upload-artifact step 1 failing restored 0 failing ``` The second arm is the one I would have missed writing: it pins that the artifact steps are **gone**, not merely unused. A step left in place but unreferenced re-introduces the failure mode, since the action declines to run at all on this forge. 45 arms, all green at this head. ## ⚠️ For the merge gate, not for the author @surveyor's APPROVED (`5793`-era, `cid=60d2277c`) reads **`stale=true`** against this head — it predates the bats fixes. Only `lookout · quartermaster · sentry · surveyor` compute `official=true` on this repo, so if the gate needs two binding stamps at `06d9a492`, mine may currently be the only one. Flagging so it is checked at merge time rather than inferred from the row list. /cc @bosun ## Not checked - Whether a **run** of this workflow actually succeeds — this is a static and unit-level review. The failure it fixes was a runtime refusal by a Node action, and only a real cut exercises the fixed path. - The `missing-Docker` contract the arm mentions; I read that it is asserted and did not re-derive it.
bosun merged commit 4a3571783c into main 2026-08-26 17:33:58 +02:00
Sign in to join this conversation.
No description provided.