How to Publish Python Packages to PyPI with GitHub Actions Trusted Publishers

Secure, automated PyPI publishing without managing API tokens

·Matija Žiberna·
How to Publish Python Packages to PyPI with GitHub Actions Trusted Publishers

I recently needed to publish my Python CLI tool pgdock to PyPI for the first time. After diving into the current Python packaging ecosystem, I discovered that the traditional approach of using API tokens has been superseded by something much better: trusted publishers with OpenID Connect authentication.

The old way required managing long-lived API tokens, which posed security risks and required manual credential management. The new trusted publisher approach eliminates these issues entirely by using GitHub's built-in authentication to securely publish packages without any secrets to manage.

This guide shows you the exact process I used to set up automated PyPI publishing that triggers on GitHub releases, using modern Python packaging standards and GitHub Actions.

Setting Up Modern Python Package Structure

The foundation of PyPI publishing starts with proper package structure. Modern Python packaging has moved away from setup.py files in favor of declarative configuration using pyproject.toml.

First, organize your package with this structure:

your-package/
├── .github/workflows/
├── your_package/
│   ├── __init__.py
│   └── cli.py
├── pyproject.toml
├── LICENSE
├── MANIFEST.in
└── README.md

The pyproject.toml file is where you define all your package metadata and build configuration. This replaces the old setup.py approach with a more standardized format that all modern Python tools understand.

Create your pyproject.toml file with the essential configuration:

# File: pyproject.toml
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "your-package-name"
version = "1.0.0"
authors = [
    {name = "Your Name", email = "your-email@example.com"},
]
description = "A concise description of what your package does"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers", 
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Topic :: Software Development :: Libraries :: Python Modules",
]
keywords = ["python", "cli", "tool"]
dependencies = [
    "typer>=0.12.0",
    "rich>=13.0.0",
]

[project.urls]
Homepage = "https://github.com/yourusername/your-package"
Repository = "https://github.com/yourusername/your-package"
Issues = "https://github.com/yourusername/your-package/issues"

[project.scripts]
your-command = "your_package.cli:main"

This configuration defines everything PyPI needs to know about your package. The build-system section tells tools like pip and build how to create your package distributions. The project section contains all the metadata that appears on your PyPI page.

If your package includes non-Python files like templates or configuration files, create a MANIFEST.in file to ensure they're included in the distribution:

# File: MANIFEST.in
include README.md
include LICENSE
recursive-include your_package/templates *.j2
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

Creating the GitHub Actions Workflow

GitHub Actions provides the automation layer that handles building and publishing your package. The key advantage of using GitHub Actions with trusted publishers is that you don't need to manage any secrets or API tokens.

Create the workflow file that will handle your package publishing:

# File: .github/workflows/pypi-publish.yml
name: Publish Python Package

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.x'
        
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build
        
    - name: Build package
      run: python -m build
      
    - name: Publish package
      uses: pypa/gh-action-pypi-publish@release/v1

The critical part here is the permissions section with id-token: write. This enables GitHub's OpenID Connect tokens, which the trusted publisher system uses for authentication. The workflow triggers only on published releases, ensuring you have control over when packages get published.

The python -m build command creates both a source distribution (.tar.gz) and a wheel (.whl) file. Modern pip installations prefer wheels for faster installation, but source distributions provide fallback compatibility.

Configuring PyPI Trusted Publisher

The trusted publisher setup on PyPI is what makes the magic happen. Instead of using API tokens, PyPI trusts GitHub to authenticate your publishing requests using OpenID Connect.

Navigate to your PyPI account's publishing settings at https://pypi.org/manage/account/publishing/ and add a new pending publisher with these details:

  • PyPI Project Name: your-package-name (exactly as in pyproject.toml)
  • Owner: your-github-username
  • Repository name: your-repository-name
  • Workflow name: pypi-publish.yml
  • Environment name: (leave empty or set to "Any")

The "pending publisher" approach is particularly useful because it allows you to configure the trusted relationship before your package exists on PyPI. Once you publish for the first time, the pending publisher automatically becomes an active publisher for that project.

This configuration creates a secure channel between your specific GitHub repository and your PyPI project. PyPI will only accept packages from GitHub Actions runs that match these exact parameters.

Testing the Package Build Process

Before triggering your first PyPI publish, verify that your package builds correctly locally. This catches configuration issues early and ensures your workflow will succeed.

Install the build tool and create your distributions:

pip install build
python -m build

This command creates a dist/ directory containing your package files:

dist/
├── your-package-1.0.0-py3-none-any.whl
└── your-package-1.0.0.tar.gz

You can test install your package locally to verify it works:

pip install dist/your-package-1.0.0-py3-none-any.whl
your-command --help

This local testing step is crucial because it catches issues like missing dependencies, incorrect entry points, or packaging problems before they reach PyPI.

Publishing Your First Release

With everything configured, publishing becomes as simple as creating a GitHub release. The workflow you created will automatically trigger and handle the entire publishing process.

Go to your GitHub repository's releases page and create a new release:

  • Tag version: v1.0.0 (following semantic versioning)
  • Release title: Package Name v1.0.0
  • Description: Describe what's new in this release

When you publish the release, GitHub triggers your workflow, which builds the package and publishes it to PyPI using the trusted publisher authentication you configured.

You can monitor the publishing process in your repository's Actions tab. The workflow will show each step, from building the package to the final PyPI upload. If successful, your package becomes immediately available for installation worldwide.

Monitoring and Maintaining Your Published Package

After your first successful publish, your package is live on PyPI and users can install it with pip install your-package-name. The trusted publisher relationship is now active, and future releases will follow the same automated process.

For subsequent releases, simply update the version number in your pyproject.toml file, commit your changes, and create a new GitHub release. The workflow handles everything else automatically.

You can add badges to your README to show the current PyPI version and build status:

[![PyPI version](https://badge.fury.io/py/your-package.svg)](https://badge.fury.io/py/your-package)
[![Publish to PyPI](https://github.com/username/repo/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/username/repo/actions/workflows/pypi-publish.yml)

The trusted publisher approach provides several advantages over traditional API tokens: it's more secure since there are no long-lived credentials to manage, it provides better audit trails through GitHub's logging, and it integrates seamlessly with your existing development workflow.

Conclusion

Setting up automated PyPI publishing with GitHub Actions trusted publishers eliminates the security risks and management overhead of API tokens while providing a streamlined publishing experience. You now have a modern, secure pipeline that publishes your Python packages automatically when you create GitHub releases.

The combination of declarative package configuration in pyproject.toml, automated building and publishing through GitHub Actions, and secure authentication via trusted publishers represents the current best practice for Python package distribution. This approach scales well as your project grows and provides the foundation for more advanced publishing workflows if needed.

Let me know in the comments if you have questions about implementing this setup, and subscribe for more practical development guides.

Thanks, Matija

2

Frequently Asked Questions

Comments

Enjoyed this article?
Subscribe to my newsletter for more insights and tutorials.
Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in