Skip to content

Release

Release #9

Workflow file for this run

name: Release
# Triggered by a successful "CI" run on `main`, via `workflow_run`. Guarantees
# the full CI gate passed before publishing. WORKFLOW-NAME COUPLING
# (LOAD-BEARING): the value below must match ci.yml's `name:` EXACTLY ("CI").
# Renaming either side silently breaks releases — `workflow_run` would never
# fire. ci.yml carries the mirror comment.
on:
workflow_run:
workflows: ['CI']
types: [completed]
branches: [main]
# Serialize releases. If two pushes land on main in quick succession, the second
# workflow_run waits for the first to finish — we must not have two publish runs
# racing to upload the same version or two @semantic-release/github instances
# racing to create the tag + Release.
# `cancel-in-progress: false` because cancelling a mid-publish workflow can
# leave a half-published state that needs manual cleanup.
concurrency:
group: release
cancel-in-progress: false
jobs:
prepare:
name: Compute version + build
runs-on: ubuntu-latest
# Twin guards (both required):
# (1) workflow_run.conclusion == 'success' → CI actually passed
# (workflow_run fires on `completed` regardless of outcome).
# (2) workflow_run.event == 'push' → the CI run that triggered us was on a
# push to main, not a pull_request. FORK PRs fire `pull_request` events
# and do NOT carry secret/OIDC access; triggering release on them would
# fail or risk leaking into PR logs. DO NOT REMOVE THIS GUARD.
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push'
permissions:
contents: write # semantic-release --dry-run verifies push permission (no actual push)
outputs:
released: ${{ steps.sr.outputs.released }}
version: ${{ steps.sr.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
# semantic-release analyzes all commits since the last tag; the
# default shallow clone (depth 1) would blind it.
fetch-depth: 0
# Required so semantic-release can verify push permission.
persist-credentials: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Enable corepack (Yarn from the lockfile dialect)
run: corepack enable
- name: Install Node release tooling
run: yarn install --immutable
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
version: "0.10.11"
enable-cache: true
- name: Set up Python
run: uv python install 3.13
# Dry-run: verifyReleaseCmd (release.config.mjs) exports version + writes
# notes file.
- name: semantic-release (dry-run → compute version + notes)
id: sr
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NOTES_FILE: ${{ github.workspace }}/release-notes.md
run: yarn release:dry-run
# Only when a release is due:
- name: Stamp version into version.py (UNCOMMITTED) + build
if: steps.sr.outputs.released == 'true'
run: |
python - <<'PY'
import pathlib, os, re
v = os.environ["VERSION"]
f = pathlib.Path("src/convert_sdk/version.py")
f.write_text(re.sub(r'__version__ = "[^"]*"', f'__version__ = "{v}"', f.read_text()))
PY
uv build
env:
VERSION: ${{ steps.sr.outputs.version }}
- name: Upload dist + notes
if: steps.sr.outputs.released == 'true'
uses: actions/upload-artifact@v4
with:
name: release-dist
path: |
dist/*
release-notes.md
if-no-files-found: error
publish-pypi:
name: Publish to PyPI (OIDC)
runs-on: ubuntu-latest
needs: prepare
if: needs.prepare.outputs.released == 'true'
# OIDC Trusted Publishing — the whole job runs in the `pypi` environment so
# the OIDC subject claim matches the registered Trusted Publisher
# (owner: convertcom/python-sdk, workflow: release.yml, env: pypi).
# This environment MUST have no required reviewers/wait timers, else the
# publish job blocks. See RELEASE.md One-Time Setup.
environment:
name: pypi
url: https://pypi.org/p/convert-python-sdk
permissions:
id-token: write # OIDC Trusted Publishing — NO PyPI tokens in secrets
steps:
- uses: actions/download-artifact@v4
with:
name: release-dist
# Trusted Publisher must be configured on pypi.org for this repo +
# workflow + environment (one-time manual step — see RELEASE.md).
# No password/token input: the action exchanges the OIDC token.
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist
release:
name: Tag + GitHub Release (semantic-release)
runs-on: ubuntu-latest
# publish-before-release: skipped if PyPI upload failed (needs both jobs).
needs: [prepare, publish-pypi]
if: needs.prepare.outputs.released == 'true'
permissions:
contents: write # push vX.Y.Z tag + create the GitHub Release
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Enable corepack
run: corepack enable
- name: Install Node release tooling
run: yarn install --immutable
# Real run: re-derives the SAME version (deterministic — no commit landed
# since prepare), prepareCmd re-stamps version.py (harmless),
# @semantic-release/github pushes the tag + creates the Release.
- name: semantic-release (tag + GitHub Release)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn release