Complete Python package release workflow with PyPI publishing and GitHub releases.
jobs:
release:
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}
pypi_token: ${{ secrets.PYPI_TOKEN }}Complete release pipeline:
- Dependencies: Install dependencies using your chosen dependency manager
- Build: Build Python packages (wheel + sdist)
- Publish PyPI: Optionally publish to PyPI (using
poetry publish,uv publish, ortwine) - Publish TestPyPI: Optionally publish to TestPyPI for testing
- GitHub Release: Create GitHub release with package artifacts
| Input | Type | Default | Description |
|---|---|---|---|
python-version |
string | '3.12' |
Python version to use |
working-directory |
string | '.' |
Directory containing pyproject.toml |
dependency-manager |
string | 'poetry' |
Dependency manager (poetry, uv, venv) |
poetry-version |
string | '2.2.0' |
Poetry version (when using poetry) |
publish-pypi |
boolean | false |
Publish to PyPI |
publish-testpypi |
boolean | false |
Publish to TestPyPI |
create-github-release |
boolean | true |
Create GitHub release |
snapshot |
boolean | false |
Build without publishing/releasing |
runs-on |
string | 'ubuntu-latest' |
Runner type |
| Secret | Required | Description |
|---|---|---|
release_token |
No* | GitHub token for creating releases |
pypi_token |
No** | PyPI API token for publishing |
testpypi_token |
No*** | TestPyPI API token for testing |
*Required if create-github-release: true
**Required if publish-pypi: true
***Required if publish-testpypi: true
| Output | Description |
|---|---|
version |
Package version extracted from build |
artifacts |
JSON array of built artifacts |
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}Creates GitHub release with wheel and sdist artifacts.
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
with:
publish-pypi: true
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}
pypi_token: ${{ secrets.PYPI_TOKEN }}Publishes to PyPI and creates GitHub release.
name: Release
on:
push:
tags:
- 'v*-rc*' # Release candidates
jobs:
release:
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
with:
publish-testpypi: true
publish-pypi: false
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}
testpypi_token: ${{ secrets.TEST_PYPI_TOKEN }}Test releases on TestPyPI before publishing to production PyPI.
name: Test Release Build
on:
pull_request:
paths:
- 'pyproject.toml'
- 'setup.py'
jobs:
snapshot:
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
with:
snapshot: trueBuild packages without publishing/releasing (for testing).
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
with:
dependency-manager: 'uv'
publish-pypi: true
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}
pypi_token: ${{ secrets.PYPI_TOKEN }}Uses uv build and uv publish instead of poetry.
name: Release
on:
push:
tags:
- 'api-v*'
- 'worker-v*'
jobs:
api:
if: startsWith(github.ref, 'refs/tags/api-v')
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
with:
working-directory: './packages/api'
publish-pypi: true
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}
pypi_token: ${{ secrets.PYPI_TOKEN }}
worker:
if: startsWith(github.ref, 'refs/tags/worker-v')
uses: whisller/forge-ci/.github/workflows/forge-ci-python-release.yml@v1
with:
working-directory: './packages/worker'
publish-pypi: true
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}
pypi_token: ${{ secrets.PYPI_TOKEN }}| Dependency Manager | Build Command | Publish Command |
|---|---|---|
| poetry | poetry build |
poetry publish |
| uv | uv build |
uv publish |
| venv/pip | python -m build |
twine upload |
- Go to https://pypi.org/manage/account/token/
- Create new API token
- Copy the token (starts with
pypi-)
- Go to repository Settings → Secrets → Actions
- Add new secret:
PYPI_TOKEN - Paste your PyPI token
- Go to https://test.pypi.org/manage/account/token/
- Create new API token
- Add to GitHub Secrets as
TEST_PYPI_TOKEN
The workflow automatically detects the package version from the built wheel filename. Ensure your package version matches the git tag:
Poetry:
[tool.poetry]
version = "1.2.3" # Should match git tag v1.2.3setuptools:
setup(
version="1.2.3", # Should match git tag v1.2.3
)uv/pyproject.toml:
[project]
version = "1.2.3" # Should match git tag v1.2.3The workflow automatically marks releases as pre-release if the version contains:
alpha(e.g.,1.0.0-alpha.1)beta(e.g.,1.0.0-beta.2)rc(e.g.,1.0.0-rc.1)
dependencies → build → publish (optional) → github release
- Dependencies: Reuses forge-ci-python-tools-dependency-manager.yml
- Build: Uses poetry/uv/build based on dependency manager
- Publish: Uses poetry publish/uv publish/twine based on dependency manager
- GitHub Release: Creates release with artifacts
Ensure your package builds correctly:
# Poetry
poetry build
# uv
uv build
# pip/venv
python -m buildCheck your PyPI token:
- Ensure token is valid and not expired
- Token should start with
pypi- - Check token has upload permissions
You can't overwrite existing versions on PyPI. Bump your version:
version = "1.2.4" # Increment versionEnsure:
release_tokensecret is providedcreate-github-release: true(default)- Not in snapshot mode
- Running on a git tag
- python-dependency-manager - Dependency installation
- go-release - Go releases with GoReleaser
- Poetry Publishing
- uv Publish