Skip to content

Commit 1fdc036

Browse files
authored
Make release.yml idempotent and tighten permissions (#35)
* Make release.yml idempotent and tighten permissions The github-release step previously called `gh release create` unconditionally, which fails with HTTP 422 when a release already exists for the tag - e.g. when a maintainer publishes the release through the GitHub UI (which also creates the tag), as happened for v0.1.1. - Guard with `gh release view ... || gh release create ...` and upload `dist/*` to the existing release with `--clobber` when present. - `permissions: {}` at workflow level, escalated only where needed. - Drop unused `attestations: write`; PEP 740 attestations are produced by pypa/gh-action-pypi-publish v1.11+ from `id-token: write` alone. - Add `concurrency: cancel-in-progress: false` on the tag ref. - Verify the tagged commit is an ancestor of origin/main. - Attach built sdist + wheel to the GitHub Release. - Fold verify-version into build (no need for a separate runner). * Simplify release.yml Drop machinery that wasn't earning its lines: - The concurrency block - tag-push races are vanishingly rare for this repo. - UV_FROZEN env - irrelevant; uv build doesn't sync from the lockfile. - The tag-on-main ancestry check (and its fetch-depth: 0) - the tag-vs-pyproject match already catches the common error modes. - The if/else in github-release. Replace with the cleaner pattern of "create the release if missing, then always upload dist/* with --clobber." Idempotent on retry without branching. - --verify-tag and the post-publish info echo - noise. Use job-level env (GH_REPO, TAG) to drop --repo flags from each gh call. * Align release.yml with repo conventions - Add concurrency block matching ci.yml's group key. Use cancel-in-progress: false (unlike ci.yml which uses true) so an in-flight pypa/gh-action-pypi-publish never gets killed mid-upload. - Add env: UV_FROZEN: true to match the other uv-using workflows. - Name the final github-release step, matching the repo pattern of naming any step with a multi-line script.
1 parent f1485ac commit 1fdc036

1 file changed

Lines changed: 25 additions & 31 deletions

File tree

.github/workflows/release.yml

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,33 @@ on:
44
push:
55
tags: ["v*"]
66

7-
permissions:
8-
contents: read
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: false
10+
11+
permissions: {}
912

1013
env:
1114
UV_FROZEN: true
1215

1316
jobs:
14-
verify-version:
17+
build:
1518
runs-on: ubuntu-latest
16-
timeout-minutes: 5
19+
timeout-minutes: 10
1720
steps:
1821
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
1922
with:
2023
persist-credentials: false
2124
- name: Verify tag matches pyproject version
2225
run: |
2326
set -euo pipefail
24-
TAG_VERSION="${GITHUB_REF_NAME#v}"
25-
PYPROJECT_VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
26-
if [[ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]]; then
27-
echo "::error::Tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${PYPROJECT_VERSION}"
28-
exit 1
29-
fi
30-
if curl -sf "https://pypi.org/pypi/ionq-core/${PYPROJECT_VERSION}/json" > /dev/null 2>&1; then
31-
echo "::error::Version ${PYPROJECT_VERSION} already exists on PyPI"
27+
TAG="${GITHUB_REF_NAME#v}"
28+
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
29+
[[ "$TAG" == "$VERSION" ]] || { echo "::error::Tag $GITHUB_REF_NAME does not match pyproject version $VERSION"; exit 1; }
30+
if curl -sf "https://pypi.org/pypi/ionq-core/$VERSION/json" >/dev/null 2>&1; then
31+
echo "::error::Version $VERSION already exists on PyPI"
3232
exit 1
3333
fi
34-
echo "Releasing ionq-core ${PYPROJECT_VERSION}"
35-
36-
build:
37-
needs: verify-version
38-
runs-on: ubuntu-latest
39-
timeout-minutes: 10
40-
steps:
41-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
42-
with:
43-
persist-credentials: false
4434
- uses: ./.github/actions/setup-uv
4535
- run: uv build
4636
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
@@ -49,7 +39,7 @@ jobs:
4939
path: dist/
5040
if-no-files-found: error
5141

52-
publish:
42+
publish-pypi:
5343
needs: build
5444
runs-on: ubuntu-latest
5545
timeout-minutes: 5
@@ -58,7 +48,6 @@ jobs:
5848
url: https://pypi.org/p/ionq-core
5949
permissions:
6050
id-token: write
61-
attestations: write
6251
steps:
6352
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
6453
with:
@@ -67,16 +56,21 @@ jobs:
6756
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
6857

6958
github-release:
70-
needs: publish
59+
needs: publish-pypi
7160
runs-on: ubuntu-latest
7261
timeout-minutes: 5
7362
permissions:
7463
contents: write
64+
env:
65+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66+
GH_REPO: ${{ github.repository }}
67+
TAG: ${{ github.ref_name }}
7568
steps:
76-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
69+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
7770
with:
78-
persist-credentials: false
79-
- name: Create GitHub Release
80-
env:
81-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82-
run: gh release create "$GITHUB_REF_NAME" --generate-notes
71+
name: dist
72+
path: dist/
73+
- name: Create or update GitHub Release
74+
run: |
75+
gh release view "$TAG" >/dev/null 2>&1 || gh release create "$TAG" --generate-notes
76+
gh release upload "$TAG" --clobber dist/*

0 commit comments

Comments
 (0)