|
| 1 | +# Release PyPI Workflow Sample |
| 2 | + |
| 3 | +This sample workflow demonstrates a complete release pipeline for Python packages, combining semantic versioning with publishing to [PyPI](https://pypi.org) using trusted publishing (OIDC — no API tokens required). |
| 4 | + |
| 5 | +Like the container release sample, the goal is a drop-in model where developers communicate their intent through PR titles and the workflow takes care of the rest: generating release notes, creating GitHub releases, and publishing the package. |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The workflow performs the following steps: |
| 10 | + |
| 11 | +1. **Semantic Release**: Analyses commits since the last release to determine if a new version should be created, and if so, generates a new GitHub release with an automatically generated changelog. |
| 12 | +2. **Publish to PyPI**: If a release was created, builds the Python package and publishes it to PyPI using OIDC trusted publishing. |
| 13 | + |
| 14 | +## Why the publish step is inline |
| 15 | + |
| 16 | +PyPI's [trusted publishing](https://docs.pypi.org/trusted-publishers/) uses OIDC tokens, which GitHub only issues to jobs running directly in a non-reusable workflow. Reusable workflows (called via `uses:`) are not granted these tokens. |
| 17 | + |
| 18 | +This means the `publish-pypi` job **must** remain as a direct job in your non-reusable `release.yml` — it cannot be extracted into a reusable workflow. This is an intentional constraint from PyPI/GitHub, not a limitation of this library. See the [upstream issue](https://github.com/pypa/gh-action-pypi-publish/issues/166) for background. |
| 19 | + |
| 20 | +The `semantic-release` step can still be a reusable workflow call, and the `publish-pypi` job simply depends on its output. |
| 21 | + |
| 22 | +## Prerequisites |
| 23 | + |
| 24 | +- Your repository must follow [conventional commit](https://conventionalcommits.org/) format |
| 25 | +- We recommend `samples/check.pr-title.yaml` to enforce this on pull requests |
| 26 | +- Your repository settings should only allow squash merges, using the PR title as the commit message so semantic-release can parse it correctly |
| 27 | +- You need a `release.config.js` in your repository root — copy from `samples/release.config.js` and update `repositoryUrl` |
| 28 | +- Your package must be configured to derive its version from git tags (see [Python setup](#python-setup) below) |
| 29 | +- You must configure a PyPI trusted publisher for your package (see [PyPI setup](#pypi-trusted-publisher-setup) below) |
| 30 | + |
| 31 | +## PyPI Trusted Publisher Setup |
| 32 | + |
| 33 | +Before running this workflow, configure a trusted publisher on PyPI for your package: |
| 34 | + |
| 35 | +1. Go to your package on PyPI → **Manage** → **Publishing** |
| 36 | +2. Add a new trusted publisher with: |
| 37 | + - **Publisher:** GitHub Actions |
| 38 | + - **Owner:** your GitHub organisation or username |
| 39 | + - **Repository:** your repository name |
| 40 | + - **Workflow filename:** the filename you save this sample as (e.g. `release.yml`) |
| 41 | + - **Environment:** `pypi` (matches the `environment.name` in the workflow) |
| 42 | + |
| 43 | +This replaces API tokens entirely. The workflow authenticates to PyPI automatically via the OIDC token GitHub issues at runtime. |
| 44 | + |
| 45 | +## Python Setup |
| 46 | + |
| 47 | +Your `pyproject.toml` should derive its version dynamically from the git tag created by semantic-release. Follow `samples/pyproject.toml` using `hatch-vcs`: |
| 48 | + |
| 49 | +```toml |
| 50 | +dynamic = ["version"] |
| 51 | + |
| 52 | +[build-system] |
| 53 | +requires = ["hatchling", "hatch-vcs"] |
| 54 | +build-backend = "hatchling.build" |
| 55 | + |
| 56 | +[tool.hatch.version] |
| 57 | +source = "vcs" |
| 58 | +``` |
| 59 | + |
| 60 | +This means `python -m build` will automatically stamp the package with the correct version from the git tag — no manual version management needed. |
| 61 | + |
| 62 | +## Workflow Jobs |
| 63 | + |
| 64 | +### release |
| 65 | + |
| 66 | +Uses the [`semantic-release.yml`](../workflows/semantic-release.md) reusable workflow to: |
| 67 | + |
| 68 | +- Install and run semantic-release |
| 69 | +- Create a GitHub release with an automatically generated changelog |
| 70 | +- Output `release-created` (`'true'`/`'false'`) and `release-tag` (version string, e.g. `1.2.3`) |
| 71 | + |
| 72 | +### publish-pypi |
| 73 | + |
| 74 | +A direct (non-reusable) job that: |
| 75 | + |
| 76 | +- Only runs when `release-created == 'true'` |
| 77 | +- Builds the Python package with `python -m build` |
| 78 | +- Publishes to PyPI using `pypa/gh-action-pypi-publish` with OIDC trusted publishing |
| 79 | + |
| 80 | +**Parameters to customise:** |
| 81 | + |
| 82 | +- `environment.url`: Replace `<PACKAGE_NAME>` with your PyPI package name |
| 83 | + |
| 84 | +## Usage |
| 85 | + |
| 86 | +1. Copy `samples/release.pypi.yaml` to `.github/workflows/release.yml` in your repository |
| 87 | +2. Copy `samples/release.config.js` to your repository root and update `repositoryUrl` |
| 88 | +3. Replace `<PACKAGE_NAME>` with your PyPI package name |
| 89 | +4. Configure your `pyproject.toml` to use `hatch-vcs` (see [Python setup](#python-setup)) |
| 90 | +5. Configure a trusted publisher on PyPI (see [PyPI trusted publisher setup](#pypi-trusted-publisher-setup)) |
| 91 | +6. Ensure your repository follows the prerequisites (conventional commits, squash merges) |
| 92 | + |
| 93 | +### Version pinning |
| 94 | + |
| 95 | +The sample references `@1.4.0`. For production use, pin to a specific version and update deliberately: |
| 96 | + |
| 97 | +```yaml |
| 98 | +uses: health-informatics-uon/workflows/.github/workflows/semantic-release.yml@1.4.0 |
| 99 | +``` |
| 100 | +
|
| 101 | +Check [releases](https://github.com/health-informatics-uon/workflows/releases) for the latest version. |
0 commit comments