From 4fc9929c07424776f35858dbdcf655c3d4f08767 Mon Sep 17 00:00:00 2001 From: devrimcavusoglu Date: Wed, 13 May 2026 21:51:10 +0300 Subject: [PATCH] Make release workflow idempotent on duplicate tag-push deliveries Observed on v0.3.1: a single `git push origin v0.3.1` produced two parallel "Release" workflow runs (webhook re-delivery race). Both goreleaser jobs raced on asset upload; the loser failed with `422 already_exists` on every asset. The release itself was fine but the failed run is visible noise. Two changes, both required: 1. Concurrency group `release-` with `cancel-in-progress: false` so simultaneous runs for the same tag are serialized instead of racing. We do not cancel-in-progress because a real publish must not be interrupted mid-upload. 2. Pre-flight `gh release view` check before invoking goreleaser. If a release for the tag already exists (i.e. an earlier run for the same tag already published), skip goreleaser and let the job exit green. The concurrency lock guarantees the existence check happens AFTER the first run completes, so this is reliable. Either change alone is insufficient: serialization without idempotency still produces a `422` on the second run; idempotency without serialization races because the release is created ~25s into the first run, after the second run's pre-check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b950f68..2e4f211 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,15 @@ on: tags: - "v*" +# Serialize runs for the same tag. GitHub occasionally re-delivers tag-push +# webhooks, producing two parallel "Release" workflow runs (observed on v0.3.1). +# Without serialization both runs race on the GitHub release publish step and +# the loser fails with `422 already_exists` on every asset upload. We do NOT +# cancel-in-progress because a real release publish must not be interrupted. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + permissions: contents: write @@ -20,7 +29,23 @@ jobs: with: go-version: "1.25" + # Idempotency guard: if a release already exists for this tag (i.e. an + # earlier run for the same tag already published artifacts), skip + # goreleaser instead of trying to re-upload assets and failing with 422. + - name: Check whether release already exists + id: precheck + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "Release $GITHUB_REF_NAME already exists; skipping goreleaser." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - name: Run GoReleaser + if: steps.precheck.outputs.skip != 'true' uses: goreleaser/goreleaser-action@v6 with: version: latest