Skip to main content

Command Palette

Search for a command to run...

Building and Automating a Python Package with Trusted Publishing

From “I Wrote a Script” to Shipping a Secure PyPI Package Automation

Published
6 min read
Building and Automating a Python Package with Trusted Publishing

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:

![https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/f2ae8dcc-9a8a-4619-9c92-38a6c4d9cdcf.png](https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/f2ae8dcc-9a8a-4619-9c92-38a6c4d9cdcf.png align="middle")

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:

![](https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/21f71f86-a2cf-42af-a299-1d531b7042be.png align="middle")

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.toml

  • PyPI 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:

![](https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/462511fe-4edb-4423-aa4d-0fee7397d979.png align="middle")

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 pip where to look for packages and how to resolve dependencies. The --index-url https://test.pypi.org/simple/ flag instructs pip to use TestPyPI’s Simple API index as the primary source for locating the package (directionfinder in this case). However, TestPyPI usually does not host common dependencies like setuptools, requests, or other third-party libraries. That’s why we add --extra-index-url https://pypi.org/simple. This flag tells pip to fall back to the real PyPI index if a dependency is not found on TestPyPI. In short, --index-url defines the main package source, and --extra-index-url acts 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