From b9f9580482404781bd75b7d3b8d58e7fe6810b90 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 5 Oct 2026 10:13:24 +0300 Subject: [PATCH] ci: keep release recovery from rolling back the Homebrew formula A dispatched release_tag older than the newest stable release used to pass, publish nothing new, and rewrite the tap formula to the old version. The release job now checks the requested tag against main and the newest stable release before it mints the release bot token. A newer draft still resumes. --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++++++----- docs/DISTRIBUTION.md | 6 ++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3c6c9c..8cd95e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,37 @@ jobs: release_version: ${{ steps.release-target.outputs.version }} steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Validate requested release tag + if: github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ github.token }} + REQUESTED_RELEASE_TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + tag="$REQUESTED_RELEASE_TAG" + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "unsupported release tag: $tag" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "refs/tags/$tag" HEAD; then + echo "release tag is not on main: $tag" >&2 + exit 1 + fi + + # The Homebrew job writes this tag to the tap, so only the newest + # stable release or a newer draft may pass. + newest="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases -L1 --json tagName --jq '.[0].tagName // empty')" + if [[ -n "$newest" && "$tag" != "$newest" && "$(printf '%s\n' "$newest" "$tag" | sort -V | tail -n1)" != "$tag" ]]; then + echo "release tag $tag is older than the newest stable release $newest" >&2 + exit 1 + fi + - name: Create release bot token id: release-bot uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 @@ -107,11 +138,6 @@ jobs: exit 1 fi echo "user_id=${user_id}" >> "$GITHUB_OUTPUT" - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - name: Set up Vite+ uses: ./.github/actions/setup-vp diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md index 46569c5..83d6067 100644 --- a/docs/DISTRIBUTION.md +++ b/docs/DISTRIBUTION.md @@ -44,8 +44,10 @@ The release-bot remote is configured only after dependencies are installed and t ## Recover After a partial release failure, dispatch `CI` from current `main` with the -exact existing tag. The release job validates that tag against `main` and reads -its exact GitHub Release state: +exact existing tag. Before it mints the release bot token, the release job +checks that the tag is on `main` and not older than the newest stable release, +so recovery cannot roll the Homebrew formula back. It then reads the tag's exact +GitHub Release state: - a draft rebuilds and replaces its binary assets, verifies all six names, and publishes once