Skip to content

Publish the draft the release job created, not the tag - #24

Merged
chrisuthe merged 1 commit into
mainfrom
chrisuthe/task/publish-the-draft-the-release-job-created-not-the
Aug 24, 2026
Merged

chrisuthe merged 1 commit into
mainfrom
chrisuthe/task/publish-the-draft-the-release-job-created-not-the

Conversation

@chrisuthe

Copy link
Copy Markdown
Member

release.yml has not published a release since v0.1.0. Every v* tag since v0.1.1 failed in
the release job's last step, Assert every asset arrived, then publish, with the draft v0.1.3 is missing assets and will not be published and a diff reading @@ -1,5 +0,0 @@ --
five expected names against nothing read back.

The build was never the problem. Every leg was green, the pre-create assertion passed on a
complete dist/, and gh release create --draft uploaded all five files. The failure was
entirely in the read-back: gh release view "$TAG" resolves a tag through the
get-release-by-tag endpoint, and that endpoint does not return drafts. So the step read
whichever published release the tag pointed at -- on v0.1.1 and v0.1.3, an asset-less one --
and never looked at the draft it had just made.

What changed

  • Publish the release the create step made, by id. gh release create prints the new
    release's URL on stdout, unique to that draft. That URL is traded once for the numeric
    release id, and the read-back and the publish both go through the documented REST endpoints
    for a release by id.

    The draft's untagged-<hash> slug does resolve through gh release view -- I confirmed it
    against the live API -- but the behaviour is undocumented (gh release view [<tag>] promises
    a tag), and this job's whole shape is a bet against unpinned gh behaviour. The id route is
    the one that keeps the guarantee in this file.

  • The asset-set assertion is untouched. It was reading the wrong release, not asking the
    wrong question.

  • A tag that already carries a release is now refused in preflight, where it costs
    seconds instead of three runners. This is what should have stopped v0.1.3: a draft is not
    bound to a tag ref, so gh release create does not refuse a claimed tag -- it adds another
    draft beside it, which is how one tag came to name three releases.

  • Both comment blocks in the create/publish steps are rewritten. The --clobber block
    described the old model and was wrong; the still-accurate part of it -- re-run the failed
    jobs, because re-running all of them repeats the build and upload-artifact refuses an
    artifact name the run already has -- is kept.

How this was verified

No tag was pushed. Everything below is read-only against the live API:

check result
gh release view v0.1.3 (old code) returns the empty published release, 0 assets -- reproduces the reported failure exactly
gh api .../releases/375836659 (new code) returns all 5 assets, diff clean
id resolved from create's URL untagged-4d8f... -> 375836659, and each draft's URL resolves to its own distinct id
id guard on empty / multi-line / non-numeric all rejected
preflight guard fails on v0.1.3 and v0.1.0, passes on an unclaimed tag
actionlint 1.7.12, all four workflows clean
shellcheck 0.11.0, every run: block + scripts/*.sh clean

The C++ suite was not run locally -- cmake is not installed on that machine -- but no C++
file is touched here, and CI runs it on this PR.

Needs a hand, deliberately not done here

Four stale releases want deleting, and that is your call rather than this PR's:

id tag state assets
375835352 v0.1.3 draft 5
375836659 v0.1.3 draft 5
375834055 v0.1.3 published 0
373901783 v0.1.1 published 0

The v0.1.3 tag itself is fine and does not need re-cutting. Once the empty release is gone and
this is merged, re-running the failed jobs on run 32750006763 should publish it -- note
that re-running only the failed jobs does not re-run preflight, so the new guard will not
stand in the way of that recovery.


Open in Codrasil: codrasil://task/tsk_01M0T9QBPBKNVATD8J7FK2FHD0

`gh release view "$TAG"` resolves a tag through the get-release-by-tag
endpoint, and that endpoint does not return drafts. So the final step of the
release job never looked at the draft it had just created -- it read whatever
published release the tag pointed at, diffed five expected asset names against
nothing, and failed. Every tag since v0.1.1 died there with the build green and
a complete draft sitting beside it.

Capture what `gh release create` prints instead: its stdout is the new release's
URL, unique to that draft. Trade it once for the numeric release id, and read
the assets and publish by id through the documented REST endpoints. The draft's
`untagged-<hash>` slug does resolve through `gh release view`, but that fallback
is undocumented -- `gh release view [<tag>]` promises a tag -- and this job is
built as a bet against unpinned gh behaviour.

The asset-set assertion is unchanged. It was reading the wrong release, not
asking the wrong question.

Refuse a tag that already carries a release in the preflight, where it costs
seconds rather than three runners. A draft is not bound to a tag ref, so
`gh release create` will not refuse a claimed tag -- it adds a second draft
beside the first, which is how v0.1.3 came to name three releases.
@chrisuthe
chrisuthe marked this pull request as ready for review August 24, 2026 16:50
@chrisuthe
chrisuthe merged commit 71818c9 into main Aug 24, 2026
10 checks passed
@chrisuthe
chrisuthe deleted the chrisuthe/task/publish-the-draft-the-release-job-created-not-the branch August 24, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant