diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7b47c006fb..ec70bd2714 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -63,28 +63,45 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ inputs.version }} - # Detect whether main already carries the release commit (we're - # resuming after a mid-flight failure) or whether this is a fresh - # release attempt. The downstream steps gate themselves on `resume`. + # Tag existence is the source of truth for "release in progress": + # main HEAD may have moved past the release commit (a follow-up fix + # merged on top), so the commit-message check on HEAD is too narrow. + # If v exists, resume from the commit it points at; + # otherwise it's a fresh attempt and tags must not exist. run: | set -euo pipefail - main_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/main" -q .object.sha) - first_line=$(gh api "repos/${GITHUB_REPOSITORY}/git/commits/${main_sha}" -q .message | head -n1) - expected="chore: prepare release ${VERSION} [skip ci]" - if [[ "${first_line}" == "${expected}" ]]; then - echo "Resuming: release commit already at ${main_sha}" + err=$(mktemp) + trap 'rm -f "${err}"' EXIT + # Capture stderr so we can distinguish a real 404 (tag absent → fresh + # attempt) from any other failure (rate limit, 5xx, auth) which must + # not be silently treated as "tag missing". + if ref=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/v${VERSION}" 2>"${err}"); then + sha=$(jq -r .object.sha <<<"${ref}") + type=$(jq -r .object.type <<<"${ref}") + if [[ "${type}" == "tag" ]]; then + sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${sha}" -q .object.sha) + fi + # Refuse to resume against a tag that isn't reachable from main: + # protects against an orphan tag created on a side branch. + if ! git merge-base --is-ancestor "${sha}" HEAD; then + echo "::error::Tag v${VERSION} (${sha}) is not reachable from main; refusing to resume." + exit 1 + fi + echo "Resuming: v${VERSION} exists at ${sha}" { echo "resume=true" - echo "release_commit=${main_sha}" + echo "release_commit=${sha}" } >> "${GITHUB_OUTPUT}" - else + elif grep -qF "(HTTP 404)" "${err}"; then echo "resume=false" >> "${GITHUB_OUTPUT}" - for tag in "v${VERSION}" "caddy/v${VERSION}"; do - if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${tag}" --silent 2>/dev/null; then - echo "::error::Tag ${tag} exists but main HEAD is not the release commit; refusing to release." - exit 1 - fi - done + if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/caddy/v${VERSION}" --silent 2>/dev/null; then + echo "::error::caddy/v${VERSION} exists but v${VERSION} does not; refusing to release into a split state." + exit 1 + fi + else + echo "::error::GitHub API call for tag v${VERSION} failed:" + cat "${err}" >&2 + exit 1 fi - if: steps.state.outputs.resume != 'true' uses: ./.github/actions/setup-go