Skip to content

Commit 6411293

Browse files
authored
fix: Update Pypi publishing (#18)
1 parent a096346 commit 6411293

3 files changed

Lines changed: 152 additions & 54 deletions

File tree

docs/samples/release.pypi.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.

samples/publish.pypi.yml

Lines changed: 0 additions & 54 deletions
This file was deleted.

samples/release.pypi.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Sample workflow for releasing a Python package to PyPI using reusable workflows from health-informatics-uon/workflows
2+
# This demonstrates how to combine semantic-release with PyPI trusted publishing (OIDC)
3+
# Customize the placeholders and values below for your specific project
4+
5+
name: Release
6+
7+
permissions:
8+
contents: write
9+
pull-requests: write
10+
issues: write
11+
id-token: write # required for PyPI trusted publishing
12+
13+
on:
14+
push:
15+
branches:
16+
- main
17+
18+
jobs:
19+
release:
20+
uses: health-informatics-uon/workflows/.github/workflows/semantic-release.yml@1.4.0
21+
with:
22+
node-version: '24'
23+
secrets: inherit
24+
25+
# PyPI trusted publishing (OIDC) cannot be used from within a reusable workflow.
26+
# This job must stay as a direct job in this non-reusable workflow — do not extract it.
27+
# See: https://github.com/pypa/gh-action-pypi-publish/issues/166
28+
publish-pypi:
29+
needs: release
30+
if: needs.release.outputs.release-created == 'true'
31+
runs-on: ubuntu-latest
32+
environment:
33+
name: pypi
34+
# Set your PyPI package name
35+
url: https://pypi.org/p/<PACKAGE_NAME>
36+
permissions:
37+
id-token: write
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- uses: actions/setup-python@v5
42+
with:
43+
python-version: '3.x'
44+
45+
- name: Build package
46+
run: |
47+
pip install build
48+
python -m build
49+
50+
- name: Publish to PyPI
51+
uses: pypa/gh-action-pypi-publish@release/v1

0 commit comments

Comments
 (0)