feat(publish): un-draft the release as the final step #39

Merged
zach merged 1 commit from feat/publish-undraft into fix/workflow-injection-and-http-status 2026-07-25 21:46:19 +00:00
Owner

Stacked on #29 (fix/workflow-injection-and-http-status), which rewrote the resolve target step this reads steps.target.outputs.tag from.

build-kernel.yml creates the Forgejo release as a draft by design — that's the human review gate. publish.yml can read a draft's assets because it sends the token, so every step above succeeds while the release is still a draft. Which is precisely why the un-draft was easy to forget: nothing in any workflow did it, and it appeared in no runbook.

Until it happens, an anonymous GET of the release assets 404s, breaking:

  • secure-boot.md — users wget unredacted-hardened.der to enrol MOK. Without it they cannot boot the kernel they just installed on a Secure Boot machine.
  • verifying-packages.md — the same download, plus the fingerprint published in the release notes.
  • cve-policy.md — the releases RSS feed is the announced CVE-response channel, and drafts don't appear in it.

Being accurate about severity

This is not a live outage. I checked the server: every release shows status released and the .der returns HTTP 200 — the operator has been doing it by hand. The defect was a required manual step that existed in no runbook (documented as first-build.md step 10a in #34) and no automation. Worth stating plainly rather than letting the fix imply users were broken.

Design

  • Placed last, after the registry publish and the manifest commit, so a failure earlier leaves the release a draft rather than announcing a half-published one.
  • Idempotent — reads .draft first and exits early if already published, so re-running publish is safe.
  • Does not weaken the human gate. publish.yml is itself workflow_dispatch-only; a human still decides when this runs.
  • Cannot recurse into this workflow's own release: published trigger — Forgejo emits no event on the draft→published transition, already verified and documented at the top of this file.
  • Release id from the LIST endpoint, not /releases/tags/{tag}: some Forgejo versions filter drafts out of the by-tag lookup and return a null id. Same reason and same pattern as build-kernel.yml's release-creation step.
  • Verifies anonymous reachability afterwards and warns if it fails. A 200 from the PATCH is not the same property as a user being able to fetch the certificate — and the second is what the docs promise.

Verification

Simulated offline with a stubbed curl:

scenario behaviour
draft release, assets reachable published release v7.1.5-hardened1 (id 42) + anonymous asset fetch OK (200)
already published is already published; nothing to do — exits 0, no PATCH
draft, assets not anonymously reachable publishes, then ::warning:: naming the 404 and pointing at secure-boot.md

Passes all four workflow-expressions rules (including the new run:-body rule from #29) and the workflow-shell linter from #38 at --severity warning with no findings. yamllint -d relaxed clean.

Before trusting it in CI

The API shape isn't testable offline. Run the PATCH by hand once against the already-published v7.1.4-hardened1 to confirm it returns 200 and is a no-op — the step's own idempotency check means that's a safe experiment.

🤖 Generated with Claude Code

> **Stacked on #29** (`fix/workflow-injection-and-http-status`), which rewrote the `resolve target` step this reads `steps.target.outputs.tag` from. `build-kernel.yml` creates the Forgejo release as a **draft** by design — that's the human review gate. `publish.yml` can read a draft's assets because it sends the token, so every step above succeeds while the release is still a draft. Which is precisely why the un-draft was easy to forget: nothing in any workflow did it, and it appeared in no runbook. Until it happens, an anonymous GET of the release assets 404s, breaking: - [secure-boot.md](docs/users/secure-boot.md) — users `wget unredacted-hardened.der` to enrol MOK. Without it they **cannot boot the kernel they just installed** on a Secure Boot machine. - [verifying-packages.md](docs/users/verifying-packages.md) — the same download, plus the fingerprint published in the release notes. - [cve-policy.md](docs/users/cve-policy.md) — the releases RSS feed is the announced CVE-response channel, and drafts don't appear in it. ## Being accurate about severity **This is not a live outage.** I checked the server: every release shows status `released` and the `.der` returns HTTP 200 — the operator has been doing it by hand. The defect was a *required manual step that existed in no runbook* (documented as `first-build.md` step 10a in #34) and no automation. Worth stating plainly rather than letting the fix imply users were broken. ## Design - **Placed last**, after the registry publish and the manifest commit, so a failure earlier leaves the release a draft rather than announcing a half-published one. - **Idempotent** — reads `.draft` first and exits early if already published, so re-running publish is safe. - **Does not weaken the human gate.** `publish.yml` is itself `workflow_dispatch`-only; a human still decides when this runs. - **Cannot recurse** into this workflow's own `release: published` trigger — Forgejo emits no event on the draft→published transition, already verified and documented at the top of this file. - **Release id from the LIST endpoint**, not `/releases/tags/{tag}`: some Forgejo versions filter drafts out of the by-tag lookup and return a null id. Same reason and same pattern as `build-kernel.yml`'s release-creation step. - **Verifies anonymous reachability afterwards** and warns if it fails. A 200 from the `PATCH` is not the same property as a user being able to fetch the certificate — and the second is what the docs promise. ## Verification Simulated offline with a stubbed `curl`: | scenario | behaviour | |---|---| | draft release, assets reachable | `published release v7.1.5-hardened1 (id 42)` + `anonymous asset fetch OK (200)` | | already published | `is already published; nothing to do` — exits 0, no PATCH | | draft, assets **not** anonymously reachable | publishes, then `::warning::` naming the 404 and pointing at `secure-boot.md` | Passes all four `workflow-expressions` rules (including the new run:-body rule from #29) and the `workflow-shell` linter from #38 at `--severity warning` with no findings. `yamllint -d relaxed` clean. ## Before trusting it in CI The API shape isn't testable offline. Run the `PATCH` by hand once against the already-published `v7.1.4-hardened1` to confirm it returns 200 and is a no-op — the step's own idempotency check means that's a safe experiment. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(publish): un-draft the release as the final step
All checks were successful
validate / shellcheck (pull_request) Successful in 13s
validate / yamllint (pull_request) Successful in 12s
validate / workflow-expressions (pull_request) Successful in 5s
validate / pycompile (pull_request) Successful in 4s
validate / intent-matches-policy (pull_request) Successful in 5s
validate / dep-list-parity (pull_request) Successful in 4s
validate / no-placeholder-digests (pull_request) Has been skipped
e79b114a75
build-kernel.yml creates the Forgejo release as a DRAFT by design -- that is the
human review gate. publish.yml can read a draft's assets because it sends the
token, so every step above succeeds while the release is still a draft, which is
exactly why the un-draft was easy to forget. Nothing in any workflow did it, and
it appeared in no runbook.

Until it happens, an anonymous GET of the release assets 404s, which breaks:
docs/users/secure-boot.md (users wget unredacted-hardened.der to enrol MOK --
without it they cannot boot the kernel they just installed on a Secure Boot
machine), docs/users/verifying-packages.md (the same download plus the
fingerprint published in the release notes), and docs/users/cve-policy.md (the
releases RSS feed is the announced CVE-response channel, and drafts do not appear
in it).

To be accurate about severity: this is NOT a live outage. Checking the server,
every release shows status `released` and the .der returns HTTP 200 -- the
operator has been doing it by hand. The defect was a required manual step that
existed in no runbook (now documented as first-build.md step 10a, in a separate
change) and no automation.

Placed LAST, after the registry publish and the manifest commit, so a failure
earlier leaves the release a draft rather than announcing a half-published one.
Idempotent: it reads `.draft` first and exits early if already published, so
re-running publish is safe.

Two properties worth recording:
  - It does not weaken the human gate. publish.yml is itself
    workflow_dispatch-only, so a human still decides when this runs.
  - It cannot recurse into this workflow's own `release: published` trigger:
    Forgejo emits no event on the draft->published transition, verified and
    documented at the top of this file.

The release id comes from the LIST endpoint, not /releases/tags/{tag}: some
Forgejo versions filter drafts out of the by-tag lookup and return a null id.
Same reason and same pattern as build-kernel.yml's release-creation step.

Finally, it verifies the assets are reachable WITHOUT credentials and warns if
not. A 200 from the PATCH is not the same property as a user being able to fetch
the certificate, and the second one is what the docs promise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zach merged commit 928ae6219f into fix/workflow-injection-and-http-status 2026-07-25 21:46:19 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
unredacted/linux-hardened-unredacted!39
No description provided.