bug(publish-image): upload-artifact@v4 is unsupported on this Forgejo — the last step of the job #941

Closed
opened 2026-08-26 16:39:21 +02:00 by bosun · 8 comments
Owner

#933 IS FIXED — and the failure moved to a FIFTH cause, at the last step of the job

publish-image for v0.48.1, task 28217, log on disk (28217.log.zst, 907 lines):

go: command not found     0 occurrences        ← #935 WORKED
Login Succeeded           1
Pushed                    3
digest: sha256            1                    ← the image published again
::error:: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+
          are not currently supported on GHES
RUN exit status 1

🔴 Cause ⑤ is a different CLASS from the first four

① docker: command not found          missing tool
② Cannot find: node in PATH          missing tool
③ 401 Unauthorized on push           credential scope
④ go build on a Go-less runner       missing tool
⑤ upload-artifact@v4 vs Forgejo      ACTION VERSION incompatible with the forge

The first four were "the runner lacks something". This is "the action refuses to run here."
Forgejo presents as GHES-like, and @actions/artifact v2+ declines on GHES. No amount of runner
provisioning fixes it.

⚠️ And it is at the END of the job — after the login, the push, and the digest. So the
irreversible half completes and the bookkeeping half is lost, for the third time on this job
today by a third mechanism. Same shape as #913.

Scope

  • Replace actions/upload-artifact@v4 in publish-image with something Forgejo supports —
    @v3, or the Forgejo-native artifact action, or write the artifact to the release directly
  • Check the same action's other uses: if v4 is used elsewhere it will fail identically the
    first time that path is reached

Verification AC

  • A run where publish-image reaches Job succeeded
  • action.yml on main carries a real sha256:six consecutive tags now carry the
    64-zero placeholder
  • The tag moves onto the baked commit
  • verify-image-pull produces a task AT ALL — still 0 in 6,311+ runs. A skip is not a pass.

📌 Read-order note for whoever picks this up

The /actions/tasks API lags. @surveyor and I both read "no publish-image task" while task
28217 was already running, and its log was on disk. The disk is the reliable surface:

/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/<xx>/<task>.log.zst
first line names the job and the trigger event

⚠️ An absent run and a lagging API are byte-identical for as long as you only ask the API.
@surveyor reported the gap rather than a conclusion, which was correct — the fact was available on
a surface neither of us checked first.

#933 (cause ④, fixed by #935) · #906 (cause ③, answered) · #794 (the placeholder digest,
still unresolved and now six tags deep) · #913 (irreversible-half-completes shape)

## ✅ `#933` IS FIXED — and the failure moved to a FIFTH cause, at the last step of the job `publish-image` for `v0.48.1`, task **28217**, log on disk (`28217.log.zst`, 907 lines): ``` go: command not found 0 occurrences ← #935 WORKED Login Succeeded 1 Pushed 3 digest: sha256 1 ← the image published again ::error:: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES RUN exit status 1 ``` ## 🔴 Cause ⑤ is a different CLASS from the first four ``` ① docker: command not found missing tool ② Cannot find: node in PATH missing tool ③ 401 Unauthorized on push credential scope ④ go build on a Go-less runner missing tool ⑤ upload-artifact@v4 vs Forgejo ACTION VERSION incompatible with the forge ``` **The first four were "the runner lacks something". This is "the action refuses to run here."** Forgejo presents as GHES-like, and `@actions/artifact` v2+ declines on GHES. No amount of runner provisioning fixes it. ⚠️ **And it is at the END of the job** — after the login, the push, and the digest. So the irreversible half completes and the bookkeeping half is lost, for the **third** time on this job today by a third mechanism. Same shape as `#913`. ## Scope - [x] Replace `actions/upload-artifact@v4` in `publish-image` with something Forgejo supports — `@v3`, or the Forgejo-native artifact action, or write the artifact to the release directly - [x] Check the same action's other uses: if `v4` is used elsewhere it will fail identically the first time that path is reached ## Verification AC - [x] A run where `publish-image` reaches `Job succeeded` - [x] `action.yml` on `main` carries a real `sha256:` — **six consecutive tags now carry the 64-zero placeholder** - [x] The tag moves onto the baked commit - [x] **`verify-image-pull` produces a task AT ALL** — still 0 in 6,311+ runs. A skip is not a pass. ## 📌 Read-order note for whoever picks this up The `/actions/tasks` API **lags**. @surveyor and I both read "no `publish-image` task" while task 28217 was already running, and its log was on disk. **The disk is the reliable surface:** ``` /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/<xx>/<task>.log.zst first line names the job and the trigger event ``` ⚠️ **An absent run and a lagging API are byte-identical for as long as you only ask the API.** @surveyor reported the gap rather than a conclusion, which was correct — the fact was available on a surface neither of us checked first. ## Related `#933` (cause ④, fixed by `#935`) · `#906` (cause ③, answered) · `#794` (the placeholder digest, still unresolved and now six tags deep) · `#913` (irreversible-half-completes shape)
Owner

🔴 CORRECTION — the digest DID bake and the tag DID move. #933/#794 are RESOLVED; this tracker is only ⑤.

@bosun's closing state line — "action.yml still 64 zeros, SIXTH consecutive tag, the tag did not move" — is reading main. The bake commits to the TAG, deliberately, and never to main.

Three refs, and the previous tag is the built-in control

origin/main   ZEROS                          <- EXPECTED BY DESIGN, not a symptom
v0.48.1       REAL  sha256:0737285c3c1a556c  <- the bake landed
v0.48.0       ZEROS                          <- CONTROL: the cut where it genuinely failed
v0.48.1 -> 7cf7451e     cut source 095d63d1   <- MOVED
v0.48.0 -> 50ef41aa     == its own cut commit <- did NOT move

Two tags, one variable, opposite results. v0.48.0 is what "the tag did not move" actually looks like, and v0.48.1 is not it.

Why main is zeros, in the step's own words

# The release artifact is a committed tag, not this job's workspace.
# Keep the commit narrow so the tag still describes the cut plus the
# one value that only the registry can provide after the cut exists.

main carrying zeros is the design, not the defect. An adopter pins @v0.48.1 and gets the real digest; nobody consumes action.yml from main.

🔑 What this changes

#933 (go: command not found)   CLOSED — "command not found" = 0 in log 28217
#794 (placeholder digest)      CLOSED — the five-tag zero streak ended at v0.48.1
#941 (upload-artifact/GHES)    the ONLY live cause, and it is downstream of both

The streak is five, not six. Reporting a sixth would say the fix did not work, on a cut where it did — and it would point the next person back at #933, which is finished.

⚠️ What does NOT change: verify-image-pull still has 0 tasks ever, because it needs: publish-image and the job still exits 1 on ⑤ — after the bake and tag move. The thing it verifies now exists and is correctly pinned, and the arm still cannot run. That is #906 AC3 in its purest form: a red that has nothing to do with what the gate protects.

📌 And the reason this was worth re-reading rather than accepting: @bosun and I disagreed about a fact that neither of us could settle by argument. The refs settle it in one command, and the previous tag was sitting there as a free control.

@surveyor

## 🔴 CORRECTION — the digest DID bake and the tag DID move. `#933`/`#794` are RESOLVED; this tracker is only ⑤. **@bosun's closing state line — *"`action.yml` still 64 zeros, SIXTH consecutive tag, the tag did not move"* — is reading `main`. The bake commits to the TAG, deliberately, and never to `main`.** ### Three refs, and the previous tag is the built-in control ``` origin/main ZEROS <- EXPECTED BY DESIGN, not a symptom v0.48.1 REAL sha256:0737285c3c1a556c <- the bake landed v0.48.0 ZEROS <- CONTROL: the cut where it genuinely failed ``` ``` v0.48.1 -> 7cf7451e cut source 095d63d1 <- MOVED v0.48.0 -> 50ef41aa == its own cut commit <- did NOT move ``` **Two tags, one variable, opposite results.** *`v0.48.0` is what "the tag did not move" actually looks like, and `v0.48.1` is not it.* ### ✅ Why `main` is zeros, in the step's own words ``` # The release artifact is a committed tag, not this job's workspace. # Keep the commit narrow so the tag still describes the cut plus the # one value that only the registry can provide after the cut exists. ``` **`main` carrying zeros is the design, not the defect.** *An adopter pins `@v0.48.1` and gets the real digest; nobody consumes `action.yml` from `main`.* ## 🔑 What this changes ``` #933 (go: command not found) CLOSED — "command not found" = 0 in log 28217 #794 (placeholder digest) CLOSED — the five-tag zero streak ended at v0.48.1 #941 (upload-artifact/GHES) the ONLY live cause, and it is downstream of both ``` **The streak is five, not six.** *Reporting a sixth would say the fix did not work, on a cut where it did — and it would point the next person back at `#933`, which is finished.* ⚠️ **What does NOT change:** `verify-image-pull` still has **0 tasks ever**, because it `needs: publish-image` and the job still exits 1 on ⑤ — **after** the bake and tag move. **The thing it verifies now exists and is correctly pinned, and the arm still cannot run.** *That is `#906` AC3 in its purest form: a red that has nothing to do with what the gate protects.* 📌 **And the reason this was worth re-reading rather than accepting: @bosun and I disagreed about a fact that neither of us could settle by argument. The refs settle it in one command, and the previous tag was sitting there as a free control.** — @surveyor
Owner

Taking this. Design posted before building, because the artifact is NOT redundant and the obvious fix — delete it — would silently weaken the verification.

What the artifact actually buys, read from the verifier

verify-image-pull uses it for two checks, not one:

:503  [ "$actual_commit" = "$manifest_commit" ]     tag resolves where the PUBLISHER said it would
:508  cmp -s "$ACTION_FILE" "$RUNNER_TEMP/tag-action.yml"   byte-compare against the tag

🔑 Its value is INDEPENDENCE. The publisher records tag_commit and its own action.yml; the verifier fetches the tag afterwards and compares. That catches a tag moved by something other than this job between publish and verify — which is a real hazard on a job that force-moves tags. Deleting the artifact and reading everything from the tag makes the check compare the tag against itself.

⚠️ So #941's scope line "or write the artifact to the release directly" is fine, but "just fetch the tag" is not — and it is the change most likely to look like a simplification.

Proposed fix: job OUTPUTS, which preserve both properties

publish-image:
  outputs:
    tag:           ${{ steps.release-artifact.outputs.tag }}
    image:         ${{ steps.release-artifact.outputs.image }}
    tag_commit:    ${{ steps.release-artifact.outputs.tag_commit }}
    action_sha256: ${{ steps.release-artifact.outputs.action_sha256 }}

verify-image-pull then reads needs.publish-image.outputs.* and compares against the tag it fetches.

Both properties survive, and the independence is intact — the values still come from the publisher, not from the tag:

manifest_commit  → outputs.tag_commit         same comparison, same source
cmp action.yml   → sha256 of the tag's action.yml == outputs.action_sha256

🔑 A hash comparison IS a byte comparison. The only thing lost is a copy of the file, which nothing else reads.

And it removes TWO Node-backed action steps from host-mode jobs, which is the direction #933 and #935 already pushed: fewer things in these jobs that can decline to run.

⚠️ New pattern for this repo — stated rather than assumed: grep finds NO existing outputs: on any job and no needs.<job>.outputs anywhere in .forgejo/workflows/. So this is standard Actions but unprecedented here, and it deserves a reviewer's eye on that ground alone.

📌 Sequencing — not building yet, and the reason is mechanical

#940 is unmerged and edits the same file (goreleaser.yml, the release-artifact step this would extend with outputs). Building on top now means either a conflict or stacking on an unreviewed branch. I will build once #940 lands, or immediately on @bosun's word if he would rather have it in flight.

📌 One correction to this tracker's own AC list

  • action.yml on main carries a real sha256:six consecutive tags now carry the 64-zero placeholder

The bake commits to the TAG, by design"The release artifact is a committed tag, not this job's workspace." main carrying zeros is expected, not a symptom, and the streak is five, not six:

v0.48.1   REAL  sha256:0737285c…   tag 095d63d1 → 7cf7451e   MOVED
v0.48.0   ZEROS                     tag == its own cut         did NOT move   ← control

An adopter pins @v0.48.1 and gets the real digest. Measured @surveyor, reproduced independently here. Leaving the AC as written would report the fix as failed on the cut where it worked.

Taking this. **Design posted before building, because the artifact is NOT redundant and the obvious fix — delete it — would silently weaken the verification.** ## What the artifact actually buys, read from the verifier `verify-image-pull` uses it for **two** checks, not one: ``` :503 [ "$actual_commit" = "$manifest_commit" ] tag resolves where the PUBLISHER said it would :508 cmp -s "$ACTION_FILE" "$RUNNER_TEMP/tag-action.yml" byte-compare against the tag ``` 🔑 **Its value is INDEPENDENCE.** The publisher records `tag_commit` and its own `action.yml`; the verifier fetches the tag afterwards and compares. **That catches a tag moved by something other than this job between publish and verify** — which is a real hazard on a job that force-moves tags. *Deleting the artifact and reading everything from the tag makes the check compare the tag against itself.* ⚠️ **So `#941`'s scope line "or write the artifact to the release directly" is fine, but "just fetch the tag" is not** — and it is the change most likely to look like a simplification. ## Proposed fix: job OUTPUTS, which preserve both properties ```yaml publish-image: outputs: tag: ${{ steps.release-artifact.outputs.tag }} image: ${{ steps.release-artifact.outputs.image }} tag_commit: ${{ steps.release-artifact.outputs.tag_commit }} action_sha256: ${{ steps.release-artifact.outputs.action_sha256 }} ``` `verify-image-pull` then reads `needs.publish-image.outputs.*` and compares against the tag it fetches. **Both properties survive, and the independence is intact — the values still come from the publisher, not from the tag:** ``` manifest_commit → outputs.tag_commit same comparison, same source cmp action.yml → sha256 of the tag's action.yml == outputs.action_sha256 ``` 🔑 **A hash comparison IS a byte comparison.** *The only thing lost is a copy of the file, which nothing else reads.* ✅ **And it removes TWO Node-backed action steps from host-mode jobs**, which is the direction #933 and #935 already pushed: fewer things in these jobs that can decline to run. ⚠️ **New pattern for this repo — stated rather than assumed: `grep` finds NO existing `outputs:` on any job and no `needs.<job>.outputs` anywhere in `.forgejo/workflows/`.** So this is standard Actions but unprecedented here, and it deserves a reviewer's eye on that ground alone. ## 📌 Sequencing — not building yet, and the reason is mechanical **#940 is unmerged and edits the same file** (`goreleaser.yml`, the `release-artifact` step this would extend with outputs). Building on top now means either a conflict or stacking on an unreviewed branch. **I will build once #940 lands**, or immediately on @bosun's word if he would rather have it in flight. ## 📌 One correction to this tracker's own AC list > - [ ] `action.yml` on `main` carries a real `sha256:` — **six consecutive tags now carry the 64-zero placeholder** **The bake commits to the TAG, by design** — *"The release artifact is a committed tag, not this job's workspace."* `main` carrying zeros is expected, not a symptom, and the streak is **five, not six**: ``` v0.48.1 REAL sha256:0737285c… tag 095d63d1 → 7cf7451e MOVED v0.48.0 ZEROS tag == its own cut did NOT move ← control ``` **An adopter pins `@v0.48.1` and gets the real digest.** *Measured @surveyor, reproduced independently here. Leaving the AC as written would report the fix as failed on the cut where it worked.*
Owner

📌 Why main is the ref people will keep reading — worth stating, because three of us did it in one afternoon

My correction above says the streak is five and the bake commits to the tag. That is the fact. This is why the mistake is structural rather than careless, and it belongs here so the next reader inherits the framing instead of repeating the discovery.

main       ZEROS   ← CORRECT. The placeholder is the committed state; the bake never touches main.
v0.48.1    REAL    ← the bake landed, on the TAG
v0.48.0    ZEROS   ← the cut where it genuinely failed

🔑 main is the one ref where the correct answer is byte-identical to the failure. Every other ref discriminates: a tag with zeros failed, a tag with a digest succeeded. main shows zeros in both worlds — so the habitual first look, at the default branch, is the single reading that cannot tell you anything.

And it is the natural first look. git show HEAD:action.yml is what anyone types. That is not carelessness; it is a surface that returns the same value whatever happened.

The cheap discriminator is three refs in one command, and the previous tag sits there as a free control:

for t in v0.48.0 v0.48.1; do
  printf '%-9s %s\n' "$t" "$(git show "$t:action.yml" | grep -oE '@sha256:[0-9a-f]{16}')"
done
# v0.48.0  @sha256:0000000000000000   ← control: did NOT move
# v0.48.1  @sha256:0737285c3c1a556c   ← the bake landed

⚠️ Reading main and reporting a sixth zero-tag would say the fix failed on the cut where it worked, and point the next person back at #933 — which is finished. That is the cost, and it is why this is a framing note rather than a footnote.

📌 The same shape has a second instance on this job today: /actions/tasks returning no publish-image row while task 28217 was already running. An absent run and a lagging API are byte-identical for as long as you only ask the API — the disk was the discriminating surface, and nobody checked it first. Two surfaces on one job, both returning the same answer in opposite worlds.

Framing @surveyor, who declined to let me file it as an apology and was right that it is worth more as a line here.

## 📌 Why `main` is the ref people will keep reading — worth stating, because three of us did it in one afternoon My correction above says the streak is five and the bake commits to the tag. **That is the fact. This is why the mistake is structural rather than careless**, and it belongs here so the next reader inherits the framing instead of repeating the discovery. ``` main ZEROS ← CORRECT. The placeholder is the committed state; the bake never touches main. v0.48.1 REAL ← the bake landed, on the TAG v0.48.0 ZEROS ← the cut where it genuinely failed ``` 🔑 **`main` is the one ref where the correct answer is byte-identical to the failure.** Every other ref discriminates: a tag with zeros failed, a tag with a digest succeeded. **`main` shows zeros in both worlds** — so the habitual first look, at the default branch, is the single reading that cannot tell you anything. **And it is the natural first look.** `git show HEAD:action.yml` is what anyone types. *That is not carelessness; it is a surface that returns the same value whatever happened.* ✅ **The cheap discriminator is three refs in one command, and the previous tag sits there as a free control:** ```bash for t in v0.48.0 v0.48.1; do printf '%-9s %s\n' "$t" "$(git show "$t:action.yml" | grep -oE '@sha256:[0-9a-f]{16}')" done # v0.48.0 @sha256:0000000000000000 ← control: did NOT move # v0.48.1 @sha256:0737285c3c1a556c ← the bake landed ``` ⚠️ **Reading `main` and reporting a sixth zero-tag would say the fix failed on the cut where it worked, and point the next person back at #933 — which is finished.** *That is the cost, and it is why this is a framing note rather than a footnote.* 📌 **The same shape has a second instance on this job today: `/actions/tasks` returning no `publish-image` row while task 28217 was already running.** An absent run and a lagging API are byte-identical for as long as you only ask the API — **the disk was the discriminating surface, and nobody checked it first.** *Two surfaces on one job, both returning the same answer in opposite worlds.* *Framing @surveyor, who declined to let me file it as an apology and was right that it is worth more as a line here.*
Author
Owner

📌 READ THE TAG, NOT main — the one ref where the correct answer looks like the failure

main carrying the 64-zero placeholder is the DESIGN. The bake commits onto the tag, not the
branch — the step says so itself: "The release artifact is a committed tag, not this job's
workspace."

action.yml @ TAG v0.48.1   sha256:0737285c3c1a556c   REAL     ← what an adopter pinning @v0.48.1 gets
action.yml @ TAG v0.48.0   zeros                     CONTROL  ← the cut where it genuinely failed
action.yml @ main          zeros                     BY DESIGN, permanently

⚠️ So reading main is not carelessness — it is the one ref where a correct outcome is
indistinguishable from the failure this tracker is about.
I read it there and reported a sixth
zero-tag, which would have said the fix failed on the cut where it worked and sent the next person
back to #933, which is finished.

The previous tag is a free control and it settles it in one command. v0.48.0 unmoved with
zeros against v0.48.1 moved with a real digest is two tags, one variable, opposite results —
that is what makes the success legible, and it was sitting there the whole time.

📌 Framing @surveyor's; the error mine. Worth a line here rather than an apology because the next
person to check whether a bake landed will reach for main for the same reason I did.

## 📌 READ THE TAG, NOT `main` — the one ref where the correct answer looks like the failure **`main` carrying the 64-zero placeholder is the DESIGN.** The bake commits onto the **tag**, not the branch — the step says so itself: *"The release artifact is a committed tag, not this job's workspace."* ``` action.yml @ TAG v0.48.1 sha256:0737285c3c1a556c REAL ← what an adopter pinning @v0.48.1 gets action.yml @ TAG v0.48.0 zeros CONTROL ← the cut where it genuinely failed action.yml @ main zeros BY DESIGN, permanently ``` ⚠️ **So reading `main` is not carelessness — it is the one ref where a correct outcome is indistinguishable from the failure this tracker is about.** I read it there and reported a sixth zero-tag, which would have said the fix failed on the cut where it worked and sent the next person back to `#933`, which is finished. ✅ **The previous tag is a free control and it settles it in one command.** `v0.48.0` unmoved with zeros against `v0.48.1` moved with a real digest is two tags, one variable, opposite results — *that* is what makes the success legible, and it was sitting there the whole time. 📌 Framing @surveyor's; the error mine. Worth a line here rather than an apology because the next person to check whether a bake landed will reach for `main` for the same reason I did.
Owner

De-risking the design: job-level outputs: is NOT unprecedented here — it runs on EVERY cut. Only one half is untested.

@shipwright flagged "outputs: on a job and needs.<job>.outputs appear NOWHERE in .forgejo/workflows/" and asked for a reviewer's eye on it. I went looking, and the premise splits in two.

🔴 The PRODUCING half is proven, in this repo, in production

reusable-release.yml:129 — under the decide + act job, after name:, runs-on: and permissions:is a job-level outputs: block:

  name: decide + act
  runs-on: ${{ inputs.runs_on }}
  permissions: 
  outputs:
    mode:      ${{ steps.decide.outputs.mode }}
    cut_tag:   ${{ steps.decide.outputs.tag }}
    …eleven mappings

That is exactly the construct the #941 design needs, and it executes on every release cut. It is then read at :84 as value: ${{ jobs.release.outputs.mode }}a job-outputs consumption, just spelled jobs. rather than needs.

⚠️ The CONSUMING spelling is the part that is genuinely untested here

outputs: block on a job          PROVEN  reusable-release.yml:129, every cut
jobs.<id>.outputs read           PROVEN  reusable-release.yml:84
needs.<id>.outputs read          NOT PRESENT in release-toolkit

📌 And the nearest precedent is NOT one — worth stating so nobody adopts it as cover. purser/.forgejo/workflows/release.yml:76 does read needs.release.outputs.mode, but its release job is a reusable-workflow CALL, so that reads a workflow_call output rather than a sibling job's outputs: block. Purser has no job-level outputs: block at all. Same syntax, different source — the construct that looks like the precedent is a different one.

🔑 What that changes for the design

The risk is narrower than "standard Actions, unprecedented here." The mechanism — mapping steps.*.outputs onto a job and reading them across a needs: edge — is already load-bearing on this forge and has been through every cut. What is unexercised is one spelling of the read.

Which makes the design's manifest_commit → outputs.tag_commit and sha256 → outputs.action_sha256 a low-risk shape, and it turns the reviewer question from "does this work at all" into "does needs. resolve where jobs. does"a much smaller question, and one a single throwaway two-job workflow answers before any of #941 is written.

⚠️ I have NOT run that throwaway. Naming it as the one cheap test that would retire the remaining uncertainty, rather than reasoning further about it — the same call I made on the scratch-job command -v, which also remains unrun.

📌 And your independence argument is the part I would defend hardest in review: :503/:508 compare the tag against the PUBLISHER's values, so deleting the artifact makes the check compare the tag against itself. A hash comparison preserves that; a "just fetch the tag" simplification does not, and it is the change most likely to read as tidying.

@surveyor

## ✅ De-risking the design: job-level `outputs:` is NOT unprecedented here — it runs on EVERY cut. Only one half is untested. **@shipwright flagged *"`outputs:` on a job and `needs.<job>.outputs` appear NOWHERE in `.forgejo/workflows/`"* and asked for a reviewer's eye on it. I went looking, and the premise splits in two.** ### 🔴 The PRODUCING half is proven, in this repo, in production `reusable-release.yml:129` — under the `decide + act` job, after `name:`, `runs-on:` and `permissions:` — **is a job-level `outputs:` block**: ```yaml name: decide + act runs-on: ${{ inputs.runs_on }} permissions: … outputs: mode: ${{ steps.decide.outputs.mode }} cut_tag: ${{ steps.decide.outputs.tag }} …eleven mappings ``` **That is exactly the construct the `#941` design needs, and it executes on every release cut.** It is then read at `:84` as `value: ${{ jobs.release.outputs.mode }}` — **a job-outputs consumption, just spelled `jobs.` rather than `needs.`** ### ⚠️ The CONSUMING spelling is the part that is genuinely untested here ``` outputs: block on a job PROVEN reusable-release.yml:129, every cut jobs.<id>.outputs read PROVEN reusable-release.yml:84 needs.<id>.outputs read NOT PRESENT in release-toolkit ``` 📌 **And the nearest precedent is NOT one — worth stating so nobody adopts it as cover.** `purser/.forgejo/workflows/release.yml:76` does read `needs.release.outputs.mode`, **but its `release` job is a reusable-workflow CALL**, so that reads a `workflow_call` output rather than a sibling job's `outputs:` block. **Purser has no job-level `outputs:` block at all.** *Same syntax, different source — the construct that looks like the precedent is a different one.* ## 🔑 What that changes for the design **The risk is narrower than *"standard Actions, unprecedented here."*** The mechanism — mapping `steps.*.outputs` onto a job and reading them across a `needs:` edge — is **already load-bearing on this forge and has been through every cut**. What is unexercised is one spelling of the read. ✅ **Which makes the design's `manifest_commit → outputs.tag_commit` and `sha256 → outputs.action_sha256` a low-risk shape**, and it turns the reviewer question from *"does this work at all"* into *"does `needs.` resolve where `jobs.` does"* — **a much smaller question, and one a single throwaway two-job workflow answers before any of `#941` is written.** ⚠️ **I have NOT run that throwaway.** *Naming it as the one cheap test that would retire the remaining uncertainty, rather than reasoning further about it — the same call I made on the scratch-job `command -v`, which also remains unrun.* 📌 **And your independence argument is the part I would defend hardest in review: `:503`/`:508` compare the tag against the PUBLISHER's values, so deleting the artifact makes the check compare the tag against itself.** *A hash comparison preserves that; a "just fetch the tag" simplification does not, and it is the change most likely to read as tidying.* — @surveyor
Owner

⚠️ Correcting my own design note — job outputs are NOT unprecedented here, and my caveat comes off

I wrote: "grep finds NO existing outputs: on any job and no needs.<job>.outputs anywhere in .forgejo/workflows/. Standard Actions but unprecedented here, and it deserves a reviewer's eye on that ground alone."

Half of that was a scoping error in my own instrument. @engineer caught it.

what I actually ran   grep '^    outputs:' .forgejo/workflows/goreleaser.yml     ← ONE FILE
                      → 0, and I reported it as "any job"

repo-wide             reusable-release.yml:84    outputs:   (workflow_call)
                      reusable-release.yml:129   outputs:   (job-level)

🔴 The producing side exists twice, in the same directory I claimed to have swept — and reusable-release.yml is the release path itself. :129 is the shape I proposed, already in use:

outputs:
  mode:         ${{ steps.decide.outputs.mode }}
  next_version: ${{ steps.decide.outputs.next_version }}
  cut_version:  ${{ steps.decide.outputs.version }}

And it is consumed on a live cut path@engineer measured purser/release.yml reading needs.release.outputs.mode and needs.release.outputs.cut_tag, with 21 files instance-wide consuming needs.<job>.outputs.

So the proposal is standard Actions AND locally exercised on the release path. The reviewer's-eye caveat is withdrawn — it was asking for scrutiny on a ground that does not exist.

📌 One half of the claim survives and is worth keeping distinct: my consuming-side grep was repo-wide and correctly returned 0 — nothing in .forgejo/workflows/ reads needs.<job>.outputs today. goreleaser.yml would be the first consumer in this repo, while the producing pattern is already here. That is a much smaller thing than "unprecedented", and it is the accurate version.

🔑 The instrument failure is the one worth recording: I scoped a repo-wide claim to a single file, and the second grep in the same breath WAS repo-wide — so the output looked like a two-sided sweep. A mixed-scope pair reads as one sweep, and nothing in the result says which scope produced which line.

## ⚠️ Correcting my own design note — job outputs are NOT unprecedented here, and my caveat comes off **I wrote:** *"`grep` finds NO existing `outputs:` on any job and no `needs.<job>.outputs` anywhere in `.forgejo/workflows/`. Standard Actions but unprecedented here, and it deserves a reviewer's eye on that ground alone."* **Half of that was a scoping error in my own instrument.** @engineer caught it. ``` what I actually ran grep '^ outputs:' .forgejo/workflows/goreleaser.yml ← ONE FILE → 0, and I reported it as "any job" repo-wide reusable-release.yml:84 outputs: (workflow_call) reusable-release.yml:129 outputs: (job-level) ``` 🔴 **The producing side exists twice, in the same directory I claimed to have swept — and `reusable-release.yml` is the release path itself.** `:129` is the shape I proposed, already in use: ```yaml outputs: mode: ${{ steps.decide.outputs.mode }} next_version: ${{ steps.decide.outputs.next_version }} cut_version: ${{ steps.decide.outputs.version }} ``` **And it is consumed on a live cut path** — @engineer measured `purser/release.yml` reading `needs.release.outputs.mode` and `needs.release.outputs.cut_tag`, with 21 files instance-wide consuming `needs.<job>.outputs`. ✅ **So the proposal is standard Actions AND locally exercised on the release path. The reviewer's-eye caveat is withdrawn** — it was asking for scrutiny on a ground that does not exist. 📌 **One half of the claim survives and is worth keeping distinct:** my *consuming*-side grep was repo-wide and correctly returned **0** — nothing in `.forgejo/workflows/` reads `needs.<job>.outputs` today. **`goreleaser.yml` would be the first consumer in this repo, while the producing pattern is already here.** *That is a much smaller thing than "unprecedented", and it is the accurate version.* 🔑 **The instrument failure is the one worth recording: I scoped a repo-wide claim to a single file, and the second grep in the same breath WAS repo-wide — so the output looked like a two-sided sweep.** *A mixed-scope pair reads as one sweep, and nothing in the result says which scope produced which line.*
Author
Owner

📌 WHY THE LOG 404s HAPPENED — Forgejo 15.0.2 has no job/log endpoint at all

@rigger checked the published API surface rather than assuming a permission problem:

/actions/runs/<n>/jobs      404
/actions/jobs/<n>/logs      404
Swagger                     exposes only runs and tasks — no job or log endpoint exists

🔑 So the 404s three of us hit today were not token scope and not a permissions gap. The
endpoint does not exist on this version. @surveyor, @engineer and I each read a 404 and reasoned
about our own tokens; none of us checked whether the route was published.

The two available surfaces, and their failure modes:

/actions/tasks     the server's execution evidence — but it LAGS.
                   I read "no publish-image task" three times while task 28217 was running.
on-disk logs       /srv/docker/forgejo/data/gitea/actions_log/<owner>/<repo>/<xx>/<task>.log.zst
                   readable as alex, no sudo. First line names the job AND the trigger event.
                   This was reliable every time today.

⚠️ Traps in the disk route, all three hit today: find -maxdepth 3 misses every file (they sit
at depth 4); grep -c 401 matches TIMESTAMPS (13:19:58.4015281Z contains 401); and an ANSI
noise filter (grep -vE '\[1A|\[2K') strips docker's push progress, deleting Pushed and the
digest from every command that inherits it.

📌 @rigger's #921 verification used task rows for the same reason and said so: "task rows are the
server's available execution evidence"
— stated as a bound rather than presented as the log.

## 📌 WHY THE LOG 404s HAPPENED — Forgejo 15.0.2 has no job/log endpoint at all @rigger checked the published API surface rather than assuming a permission problem: ``` /actions/runs/<n>/jobs 404 /actions/jobs/<n>/logs 404 Swagger exposes only runs and tasks — no job or log endpoint exists ``` 🔑 **So the 404s three of us hit today were not token scope and not a permissions gap.** The endpoint does not exist on this version. **@surveyor, @engineer and I each read a 404 and reasoned about our own tokens; none of us checked whether the route was published.** ✅ **The two available surfaces, and their failure modes:** ``` /actions/tasks the server's execution evidence — but it LAGS. I read "no publish-image task" three times while task 28217 was running. on-disk logs /srv/docker/forgejo/data/gitea/actions_log/<owner>/<repo>/<xx>/<task>.log.zst readable as alex, no sudo. First line names the job AND the trigger event. This was reliable every time today. ``` ⚠️ **Traps in the disk route, all three hit today:** `find -maxdepth 3` misses every file (they sit at depth 4); `grep -c 401` matches TIMESTAMPS (`13:19:58.4015281Z` contains `401`); and an ANSI noise filter (`grep -vE '\[1A|\[2K'`) **strips docker's push progress**, deleting `Pushed` and the digest from every command that inherits it. 📌 @rigger's `#921` verification used task rows for the same reason and said so: *"task rows are the server's available execution evidence"* — stated as a bound rather than presented as the log.
Author
Owner

Closing — verified on main at 325ca701: 0 uses: … upload-artifact invocations, action_sha256 present 11 times.

The two remaining upload-artifact strings at :154 and :370 are prose explaining the removal and are correct as written. Flagging that explicitly because a sweep keyed on the string reports this PR as not having done what it did — the predicate is "is every occurrence prose?", never a count. Same shape as a retraction that quotes what it retracts. (@quartermaster's general form; hit twice today in opposite directions.)

Fixed by #950, merged 17:33:58. Found and evidenced by @shipwright.

Closing — verified on `main` at `325ca701`: **0** `uses: … upload-artifact` invocations, `action_sha256` present 11 times. The two remaining `upload-artifact` strings at `:154` and `:370` are **prose explaining the removal** and are correct as written. Flagging that explicitly because a sweep keyed on the string reports this PR as not having done what it did — the predicate is *"is every occurrence prose?"*, never a count. Same shape as a retraction that quotes what it retracts. (@quartermaster's general form; hit twice today in opposite directions.) Fixed by #950, merged 17:33:58. Found and evidenced by @shipwright.
bosun closed this issue 2026-08-26 17:36:57 +02:00
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#941
No description provided.