Publishing a Poetry project with GitHub Actions and Trusted Publishing
Overview
I was experimenting with PyPI Trusted Publishing with GitHub Actions for my Poetry based project. The outcome is a fully automated, free CI/CD pipeline that builds and publishes on push without storing any PyPI tokens.
Why Trusted Publishing
Trusted Publishing removes long‑lived API tokens by letting GitHub Actions authenticate to PyPI using OpenID Connect (OIDC), which PyPI exchanges for a short‑lived upload token during the workflow run. This is more secure and recommended by PyPI for CI/CD releases.
Project Setup
A Poetry project with pyproject.toml configured for packaging and a build backend (poetry-core).
A GitHub repository with a workflow under .github/workflows.
A PyPI project where a Trusted Publisher is added for the repo/workflow and optional environment name (e.g., pypi).
Project configuration (pyproject.toml)
[project]
name = "filespawn"
version = "0.1.2"
description = ""
authors = [
{name = "XXX",email = "xxx@xxx.xxx"}
]
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
]
[tool.poetry]
packages = [{include = "filespawn", from = "src"}]
[tool.poetry.scripts]
filespawn = "filespawn.cli:main"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
Add a Trusted Publisher on PyPI
In the PyPI project settings, add a pending Trusted Publisher for Platform = GitHub, with exact Owner, Repository, Workflow file path (include .github/workflows/…), and optional Environment name; names must match case-sensitively with the workflow.
GitHub Actions workflow (working YAML)
Save as .github/workflows/workflow.yml and commit to the repository default branch; it builds with Poetry and publishes via PyPI Trusted Publishing.
name: Publish PyPI Repository
name: Publish PyPI Repository
on: push
jobs:
build:
environment: pypi
permissions:
id-token: write
contents: read
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
allow-prereleases: true
- name: Install Poetry
run: pipx install poetry==2.1.4 && poetry --version
- name: Configure Poetry (no venv)
run: poetry config virtualenvs.create false
- name: Install (if needed for build scripts)
run: poetry install --no-interaction --no-root
- name: Build wheel and sdist
run: poetry build
- name: Publish to PyPI via Trusted Publishing
uses: pypa/gh-action-pypi-publish@release/v1
Notes:
permissions: id-token: write is required so the action can request an OIDC token; no PYPI_TOKEN secret is needed.
environment: pypi must match the Environment name configured in PyPI if you set one; this enables optional approval gates and tight binding.
Trigger “on: push” publishes on every push; to avoid accidental releases, consider using tag triggers like v*.. per PyPA guidance.
Errors encountered and fixes
OIDC token failure: “missing or insufficient OIDC token permissions”
Cause: job lacked permissions: id-token: write, so ACTIONS_ID_TOKEN_REQUEST_TOKEN was unset. [Fix] Add job-level permissions: id-token: write; contents: read.
invalid-publisher: “valid token, but no corresponding publisher”
Cause: mismatch between the workflow’s claims and PyPI’s Trusted Publisher entry (commonly the workflow file path missing .github/workflows/ prefix, environment name mismatch, or wrong repo owner/name). [Fix] Update PyPI entry to use exact owner/repo and full workflow path (.github/workflows/workflow.yml), and match environment name. Re-run after pushing a new tag or commit.
Poetry “package mode” validation complaining about missing name/version/authors/description
Cause: required metadata split across [project] and [tool.poetry]; Poetry 2.x validates the active metadata table in package mode. [Fix] Github action was using Poetry version 1.8 instead of 2.1.3 that I was using in my development. I change the version to match my development version.
Verifying the setup
Logs: In a successful run, the publish step shows it is using OIDC; no PyPI token is displayed or required because the action handles the exchange internally.
Artifact check: dist/ must contain a .whl and a .tar.gz before publishing; the verify step lists them for sanity.
Installing the published package with pipx Once published, install the latest release isolated with pipx; pipx creates a dedicated venv and exposes the CLI entry point.
Install pipx if needed:
python -m pip install pipx
Install the package:
pipx install filespawn
Run the CLI:
filespawn