Building and Automating a Python Package with Trusted Publishing
From “I Wrote a Script” to Shipping a Secure PyPI Package Automation

There’s a big difference between:
“I wrote a Python script.” and “I built, packaged, versioned, and automated a Python library for PyPI using Trusted Publishing.”
This project started small. I just wanted to understand how Python packages actually work.
Not just:
pip install -e .
But:
What makes a package installable?
What actually happens when you run
pip install?How does PyPI verify uploads?
How do you automate releases properly?
The Idea Behind the Project
In many projects, paths get messy.
Hardcoded directories creep in.
Relative paths become brittle.
Boilerplate repeats across repositories.
I was building multiple data scraping projects and found myself rewriting the same directory resolution logic again and again.
So I built a small utility package that dynamically resolves:
Project root
Absolute project root
Resource locations (like WebDriver paths)
Simple problem. Clean solution.
I just wanted a reusable library, so I didn’t have to repeat boilerplate code in every scraping project.
The Real Goal: Make It Real
I didn’t want this to be just a GitHub repository.
I wanted it to be installable via:
pip install directionfinder
That’s when things got interesting.
This article walks through that journey — step by step.
Step 1 — Modern Packaging with pyproject.toml
Instead of using the older setup.py, I followed PEP 621 and used pyproject.toml. I have both files in my git repo though.
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "directionfinder"
version = "0.6.1"
description = "Utility package to dynamically resolve project directory paths."
readme = "README.md"
requires-python = ">=3.7"
authors = [
{ name="Aayush Pokharel", email="official@aayushpokharel.com" }
]
license = { text = "MIT" }
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
Build Locally
pip install build
python -m build
Artifacts generated:
dist/
directionfinder-0.6.1.tar.gz
directionfinder-0.6.1-py3-none-any.whl
That felt satisfying.
But uploading manually using twine felt… outdated.
Step 2 — Trusted Publishing (No Tokens!)
Instead of storing an API token in GitHub Secrets, I used Trusted Publishing.
Trusted Publishing uses OIDC (OpenID Connect) to let GitHub prove its identity directly to PyPI.
No passwords.
No API tokens.
No secrets to rotate.
That’s beautiful.
Here’s how the trust chain works:

GitHub generates a short-lived identity token.
PyPI verifies that the repository and workflow match the registered trusted publisher.
If everything matches → upload allowed.
Step 3 — GitHub Actions Workflow
I created:
.github/workflows/publish.yml
name: Publish to TestPyPI
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- run: |
python -m pip install --upgrade pip
pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
Notice this part:
permissions:
id-token: write
This is required for OIDC authentication.
Without it, Trusted Publishing will fail.
Step 4 — Version-Based Release Flow
Publishing is triggered by tags:
git tag v0.5.0
git push origin v0.5.0
Behind the scenes:

When it worked for the first time, I just stared at the green checkmark.
It felt earned.
The Bugs Along the Way
It wasn’t smooth and I did make some mistakes.
Here’s what broke.
Workflow Didn’t Trigger
Because tags must be pushed.
Creating a tag locally is not enough:
git push origin v0.5.0
Otherwise, GitHub Actions never runs.
HTTP 400 Bad Request
That one was painful.
It turned out my Trusted Publisher was registered for:
direction_pkg
But my actual project name was:
directionfinder
Mismatch = rejection.
Lesson learned:
Project name must match exactly across:
pyproject.tomlPyPI project name
Trusted Publisher configuration
Even a small mismatch causes failure.
Version Conflicts
You cannot overwrite a version on PyPI.
If 0.6.0 already exists, you must bump:
version = "0.6.1"
And then:
git tag v0.6.1
git push origin v0.6.1
Version discipline matters.
Automatic Attestations (Unexpected Bonus)
Another surprise:
GitHub automatically generated digital attestations using Sigstore.
This means:
The package build is cryptographically verifiable
The provenance is recorded in a transparency log
The supply chain is traceable
Modern Python packaging is not just “uploading files.”
It’s about trust.
Here’s the bigger picture:

This is no longer “just a Python script.”
This is CI/CD + supply-chain-aware packaging.
Installing from TestPyPI
After successful publishing:
pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple \
directionfinder
And it works. That feeling is different from running a local script.
The extra flags in the install command tell
pipwhere to look for packages and how to resolve dependencies. The--index-url https://test.pypi.org/simple/flag instructspipto use TestPyPI’s Simple API index as the primary source for locating the package (directionfinderin this case). However, TestPyPI usually does not host common dependencies likesetuptools,requests, or other third-party libraries. That’s why we add--extra-index-url https://pypi.org/simple. This flag tellspipto fall back to the real PyPI index if a dependency is not found on TestPyPI. In short,--index-urldefines the main package source, and--extra-index-urlacts as a secondary lookup location to ensure dependency resolution works smoothly.
What This Project Taught Me
Packaging is more than code.
Versioning is a discipline.
Automation removes human error.
Trusted Publishing is the future.
Secure software supply chain matters.
Debugging CI is part of real engineering.
Most importantly:
Small projects become powerful when treated professionally.
Final Thoughts
This started as:
“I want to understand how Python packages work.”
It ended as:
“I built an automated, secure, versioned, supply-chain-aware release pipeline.”
And honestly, that shift in mindset is the real win.
If you're learning Python packaging:
Don’t stop at local builds.
Automate it.
Break it.
Fix it.
Secure it.
Understand it.
That’s where growth happens. You can find my git repo for this project at:
github.com/AayushPokharel/directions_pkg
