From 8aab4ce543a1350164d5e03caf3b8392e552b4d8 Mon Sep 17 00:00:00 2001 From: baht <44367278+soulbah@users.noreply.github.com> Date: Thu, 8 Oct 2026 02:45:06 +0200 Subject: [PATCH] ci(release): move the major tag only to an immutable release (#26) --- .github/workflows/release.yml | 10 +++++++++- CONTRIBUTING.md | 5 +++-- docs/adr/007-promoted-releases-and-major-tags.md | 3 +++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1de8759..b5436d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,8 +86,16 @@ jobs: fi jq -e '.draft == false' <<<"$release" >/dev/null || fail "$tag is still a draft." [[ "$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha)" == "$GITHUB_SHA" ]] || fail "$tag does not name $GITHUB_SHA." + # The repository enforces immutable releases. GitHub can report the lock a few seconds after publication. + for attempt in 1 2 3 4 5 6; do + jq -e '.immutable == true' <<<"$release" >/dev/null && break + [[ "$attempt" -lt 6 ]] || + fail "$tag is published but not immutable, so the major tag was not moved. Enable release immutability, delete this release (keep its tag) and rerun: the run republishes it immutable." + sleep $((attempt * 5)) + release="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$tag")" + done echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "Published $(jq -r .html_url <<<"$release") at \`$GITHUB_SHA\` (immutable: $(jq -r '.immutable // false' <<<"$release"))." >> "$GITHUB_STEP_SUMMARY" + echo "Published immutable $(jq -r .html_url <<<"$release") at \`$GITHUB_SHA\`." >> "$GITHUB_STEP_SUMMARY" - name: Point the major tag at this release if: steps.release.outputs.tag != '' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b63a410..f4d2f1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,8 +50,9 @@ A release is a separate maintainer dispatch on `main`: major version. A promotion carrying only `chore`, `ci`, `docs`, `style`, `test`, or `refactor` commits releases nothing. 3. The same run points the major tag (`v1` for `v1.x.y`) at the new release. - It only moves forward; rerunning resumes an interrupted release. See - `docs/adr/007-promoted-releases-and-major-tags.md`. + It only moves forward, and only once GitHub reports the release immutable + (the repository enforces immutable releases). Rerunning resumes an + interrupted release. See `docs/adr/007-promoted-releases-and-major-tags.md`. Never create, move, or delete a tag or release by hand. Published references are what other people's workflows execute; correct a mistake with a new release. diff --git a/docs/adr/007-promoted-releases-and-major-tags.md b/docs/adr/007-promoted-releases-and-major-tags.md index 9149dec..50290e3 100644 --- a/docs/adr/007-promoted-releases-and-major-tags.md +++ b/docs/adr/007-promoted-releases-and-major-tags.md @@ -52,6 +52,9 @@ under default rules is `v1.0.0`, which matches the public `@v1` contract. creates its own tag; `v0` is never maintained. Version tags never move. - A rerun resumes: if semantic-release pushed a tag without its release, the run publishes the release for that tag, then moves the major tag. +- The repository enforces immutable releases, so a published `vX.Y.Z` tag + and its release can no longer change. The run moves the major tag only + after GitHub reports the new release immutable. - `publish-v1.yml` is removed. The existing `v1` advances to `v1.0.0` through the first release run. - Running `release.yml` remains an owner decision.