diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index f019b3ae..4deffec7 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -7,7 +7,8 @@ on: - docker-bake.hcl concurrency: - group: auto-release-${{ github.workflow }}-${{ github.ref }} + # Shared with release.yml so a manual dispatch can't allocate the same tag. + group: release-tag-creation cancel-in-progress: false permissions: {} @@ -60,32 +61,9 @@ jobs: tag-and-release: needs: check-version-bump if: needs.check-version-bump.outputs.bumped == 'true' - runs-on: ubuntu-latest permissions: contents: write - steps: - - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # actions/checkout@v5 - with: - persist-credentials: false - - - name: Create release tag and GitHub release - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - base="release/$(date -u +%Y%m%d)" - tag="${base}" - suffix=1 - while gh api "repos/${{ github.repository }}/git/ref/tags/${tag}" >/dev/null 2>&1; do - tag="${base}-${suffix}" - suffix=$((suffix + 1)) - done - - gh release create "${tag}" \ - --target "${{ github.sha }}" \ - --title "${tag}" \ - --generate-notes + uses: ./.github/workflows/cut-release.yml publish: needs: tag-and-release diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml new file mode 100644 index 00000000..5ab8f97b --- /dev/null +++ b/.github/workflows/cut-release.yml @@ -0,0 +1,69 @@ +# Shared by auto-release.yml (version-bump trigger) and release.yml (manual +# dispatch): create a date-based (release/YYYYMMDD) GitHub release tag, with a +# numeric suffix on a same-day re-release, or an explicit override tag. +name: cut-release + +on: + workflow_call: + inputs: + tag: + description: "Override tag; empty uses release/YYYYMMDD with a numeric suffix on collision." + required: false + type: string + default: "" + +permissions: {} + +jobs: + cut: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # actions/checkout@v5 + with: + persist-credentials: false + + - name: Create release tag and GitHub release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + OVERRIDE: ${{ inputs.tag }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + + # 0 = the tag exists; 1 = confirmed absent (HTTP 404). Any other + # failure (network, rate limit, auth) aborts the job -- a lookup we + # can't complete must never be mistaken for "tag is free". + tag_taken() { + local out + if out="$(gh api "repos/${GH_REPO}/git/ref/tags/$1" 2>&1)"; then + return 0 + fi + if printf '%s\n' "$out" | grep -qiE 'HTTP 404|Not Found'; then + return 1 + fi + echo "::error::tag lookup for '$1' failed: ${out}" + exit 1 + } + + if [ -n "${OVERRIDE}" ]; then + tag="${OVERRIDE}" + if tag_taken "${tag}"; then + echo "::error::tag ${tag} already exists"; exit 1 + fi + else + base="release/$(date -u +%Y%m%d)" + tag="${base}"; n=1 + while tag_taken "${tag}"; do + tag="${base}-${n}"; n=$((n + 1)) + done + fi + + echo "Cutting release ${tag} at ${SHA}" + gh release create "${tag}" \ + --target "${SHA}" \ + --title "${tag}" \ + --generate-notes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b94a747d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +# Manual release, for improvements that don't bump a tracked upstream +# version and so wouldn't trigger auto-release.yml. +name: release + +on: + workflow_dispatch: + inputs: + tag: + description: "Override tag (default: release/YYYYMMDD). Must not already exist." + required: false + type: string + +permissions: {} + +concurrency: + # Shared with auto-release.yml so it can't allocate the same tag. + group: release-tag-creation + cancel-in-progress: false + +jobs: + tag-and-release: + permissions: + contents: write + uses: ./.github/workflows/cut-release.yml + with: + tag: ${{ inputs.tag }} + + publish: + needs: tag-and-release + permissions: + contents: read + packages: write + id-token: write + uses: ./.github/workflows/publish.yml + secrets: + dockerhub_user: ${{ secrets.dockerhub_user }} + dockerhub_token: ${{ secrets.dockerhub_token }}