Skip to content

Release

Release #60

Workflow file for this run

name: Release
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
# Least privilege by default; elevated per-job.
permissions:
contents: read
# Prevent overlapping releases if two merges land in quick succession.
concurrency:
group: release-main
cancel-in-progress: false
env:
POETRY_VERSION: "2.3.2"
jobs:
# ── Job 1: Semantic Release — bump, changelog, tag ─────────────
release:
name: Semantic Release + Build
runs-on: ubuntu-latest
timeout-minutes: 30
# Gate: CI passed, on main, not a PSR release commit (defense-in-depth).
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
!startsWith(github.event.workflow_run.head_commit.message, 'chore(release):')
permissions:
contents: write
outputs:
released: ${{ steps.psr.outputs.released }}
tag: ${{ steps.psr.outputs.tag }}
version: ${{ steps.psr.outputs.version }}
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Checkout main at CI-tested commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- run: pipx install "poetry==${POETRY_VERSION}"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
cache: "poetry"
- name: Install dependencies
run: poetry install --no-interaction
- name: Python Semantic Release
id: psr
uses: python-semantic-release/python-semantic-release@350c48fcb3ffcdfd2e0a235206bc2ecea6b69df0 # v10.5.3
with:
github_token: ${{ steps.app-token.outputs.token }}
vcs_release: "true"
changelog: "true"
build: "false"
- name: Build package
if: steps.psr.outputs.released == 'true'
run: poetry build
- name: Upload dist artifacts
if: steps.psr.outputs.released == 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: dist
path: dist/*
if-no-files-found: error
# ── Job 2: Publish to TestPyPI + smoke test ────────────────────
publish-testpypi:
name: TestPyPI + Smoke Test
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
environment: testpypi
permissions:
id-token: write # OIDC + PEP 740 attestations
steps:
- name: Checkout (for smoke-test script)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
sparse-checkout: scripts
sparse-checkout-cone-mode: true
- name: Download dist
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: dist
path: dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
with:
repository-url: https://test.pypi.org/legacy/
attestations: true
- name: Smoke test — install and verify from TestPyPI
env:
PKG_VERSION: ${{ needs.release.outputs.version }}
run: ./scripts/smoke-test.sh
# ── Job 3: Publish to PyPI ─────────────────────────────────────
publish-pypi:
name: Publish to PyPI
needs: [release, publish-testpypi]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
environment: pypi
permissions:
id-token: write # OIDC + PEP 740 attestations
steps:
- name: Download dist
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
with:
attestations: true
# ── Job 4: GitHub Release ──────────────────────────────────────
github-release:
name: GitHub Release
needs: [release, publish-pypi]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Download dist
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: dist
path: dist/
- name: Create GitHub Release + upload assets
uses: python-semantic-release/publish-action@310a9983a0ae878b29f3aac778d7c77c1db27378 # v10.5.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ needs.release.outputs.tag }}