From 73be66b2fc688141182bb2955df7bb74e1573188 Mon Sep 17 00:00:00 2001 From: Dan Barrett Date: Tue, 18 Aug 2026 11:51:14 +1000 Subject: [PATCH 1/3] release: mark prerelease tags as prereleases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GoReleaser defaults release.prerelease to false, so v1.0.0-alpha.1 published as an ordinary release and took GitHub's "Latest" badge — an alpha presented to anyone landing on the repository as the current stable version. The release workflow reported success, because from its point of view nothing had gone wrong. That release has been corrected by hand; this stops the next -beta or -rc repeating it. "auto" reads the semver prerelease suffix. Verified only so far as 'goreleaser check' accepting the config — the behaviour itself is not observable until a prerelease tag is pushed, so watch the next one. The changelog is deliberately left as it is. The first release's changelog spanned the whole history only because there was no previous tag to diff against; subsequent tags diff from their predecessor. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0136bDtWBAdKtufTHcXoSwQK --- .goreleaser.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1377847..e64dc9b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -36,3 +36,12 @@ changelog: exclude: - "^docs:" - "^test:" + +release: + # GoReleaser defaults this to false, so without it a tag like v1.0.0-alpha.1 + # publishes as an ordinary release and takes GitHub's "Latest" badge — an + # alpha handed to everyone who lands on the repository as the current stable + # version. The workflow reports success either way, so nothing catches it. + # "auto" reads the semver prerelease suffix and marks -alpha, -beta and -rc + # accordingly. + prerelease: auto From bd1f00491ad0acd983a117ec352b9e42baa95c42 Mon Sep 17 00:00:00 2001 From: Dan Barrett Date: Tue, 18 Aug 2026 11:55:42 +1000 Subject: [PATCH 2/3] release: build the release as a draft, then publish it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub's immutable releases lock a release's assets and its git tag the moment it is published. The workflow published first and uploaded into the live release, which is the order that has to fight the lock. GoReleaser now creates the release as a draft with every archive attached, and a separate step flips it live. That is the order GitHub prescribes for immutability: "Create the release as a draft. Attach all associated assets to the draft release. Publish the draft release." It is the better failure mode regardless of whether immutability is on. A run that dies partway now leaves an unpublished draft holding whatever it managed to build, rather than a published release missing half its binaries — and a draft can be finished by hand, where a published immutable release cannot. The tag reaches the shell through env rather than inline expansion, matching the other workflows. Two things this cannot verify from config alone, both observable at the next tag: that `gh release edit --draft=false` leaves the prerelease flag GoReleaser set (it sends only the fields named, so it should), and that a draft release is addressable by tag name (the tag exists before the workflow runs, so it should). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0136bDtWBAdKtufTHcXoSwQK --- .github/workflows/release.yaml | 15 +++++++++++++++ .goreleaser.yaml | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 090d5a6..9cec328 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -62,3 +62,18 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # .goreleaser.yaml builds the release as a draft so every archive is + # attached before anyone can see it. Publishing is the separate step, + # because with release immutability enabled a published release has its + # assets and its git tag locked — an upload arriving afterwards has + # nothing to attach to. + # + # If this step fails the draft survives, holding every artefact, and can + # be published by hand. That is the outcome worth having: an unpublished + # complete release rather than a published incomplete one. + - name: Publish the draft release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: gh release edit "$TAG" --draft=false diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e64dc9b..8ab7191 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -45,3 +45,15 @@ release: # "auto" reads the semver prerelease suffix and marks -alpha, -beta and -rc # accordingly. prerelease: auto + + # Publish as a draft and let the workflow flip it live once every archive is + # attached. This is GitHub's prescribed order for immutable releases: assets + # and the git tag are locked the moment a release is published, so anything + # uploaded afterwards has to fight the lock. Creating the release complete + # and then publishing it means there is never an incomplete published + # release to repair. + # + # It is also the safer failure mode without immutability: a run that dies + # midway leaves an unpublished draft rather than a live release missing half + # its binaries. + draft: true From 6f8273a4a5f662e41562f81de6ac103cc5fe1402 Mon Sep 17 00:00:00 2001 From: Dan Barrett Date: Tue, 18 Aug 2026 12:01:11 +1000 Subject: [PATCH 3/3] docs(release): stop implying auto detects only three suffixes The comment said auto "marks -alpha, -beta and -rc accordingly", which reads as an exhaustive list and would leave someone tagging -preview thinking they had to add it. GoReleaser documents the rule only as "in case there is an indicator for this in the tag e.g. v1.0.0-rc1" -- vague, and notably not a list. The comment now says that, including that the precise rule is undocumented, so an unusual suffix gets checked rather than assumed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0136bDtWBAdKtufTHcXoSwQK --- .goreleaser.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8ab7191..ca54ae1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -42,8 +42,11 @@ release: # publishes as an ordinary release and takes GitHub's "Latest" badge — an # alpha handed to everyone who lands on the repository as the current stable # version. The workflow reports success either way, so nothing catches it. - # "auto" reads the semver prerelease suffix and marks -alpha, -beta and -rc - # accordingly. + # "auto" marks the release as a prerelease when the tag carries a prerelease + # indicator — v1.0.0-alpha.1, v1.0.0-rc1 and so on. GoReleaser documents the + # rule only as "in case there is an indicator for this in the tag", without + # saying what counts, so an unusual suffix is worth checking on the release + # itself rather than assuming. prerelease: auto # Publish as a draft and let the workflow flip it live once every archive is