Skip to content

ci: add PyPI release workflow via Trusted Publishing - #243

Open
fe-neu wants to merge 1 commit into
danieldeer:mainfrom
fe-neu:ci/automated_release_workflow
Open

fe-neu wants to merge 1 commit into
danieldeer:mainfrom
fe-neu:ci/automated_release_workflow

Conversation

@fe-neu

@fe-neu fe-neu commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a GitHub Actions workflow that publishes seriousdb to PyPI and TestPyPI using Trusted Publishing (OIDC), so no API tokens are needed.

  • TestPyPI: started by hand (workflow_dispatch) as a dry run before a real release.
  • PyPI: runs when a GitHub Release tagged v* is published. Tag pushes alone don't start it, and releases without a version tag are skipped.

The workflow won't work until the maintainer setup in #228 is done. The values needed for that are listed below.

Closes #229

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality) — CI only, no changes to the package
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Key Changes

  • New .github/workflows/publish.yml:
    • build: only runs for manual runs or releases tagged v*; other releases are skipped, not failed. Runs uv build (sdist + wheel), then twine check --strict, and saves the files for the publish jobs. On a release, it fails if the tag isn't exactly v<version> from pyproject.toml.
    • publish-testpypi / publish-pypi: download the files and publish them with pypa/gh-action-pypi-publish.
  • The workflow is read-only by default. Only the two publish jobs get id-token: write, which Trusted Publishing needs.
  • PyPI attestations are created by the publish action by default and only need id-token: write. attestations: write is not needed.
  • Uses uv build instead of python -m build to match the other workflows. The output is the same.

Setup needed for #228

Trusted Publisher settings (PyPI and TestPyPI):

Field PyPI TestPyPI
Project name seriousdb seriousdb
Owner danieldeer danieldeer
Repository seriousdb seriousdb
Workflow filename publish.yml publish.yml
Environment pypi testpypi

Notes:

  • The workflow has to reach main before it works:
    • The "Run workflow" button for TestPyPI only appears once publish.yml is on the default branch.
    • Release runs use the workflow file from the tagged commit, so that commit must contain publish.yml.
  • TestPyPI: the project doesn't exist there yet. You can add it as a "pending publisher", which creates the project on the first upload.
  • GitHub environments: create pypi and testpypi in repo settings. The names must match exactly.
  • Deployment rules:
    • Limit pypi to tags matching v*. Release runs deploy from the tag, not a branch, and GitHub enforces this rule even if the workflow is changed later.
    • testpypi needs to allow whichever branch the manual run is started from (e.g. main).
  • Release tags: releases must be tagged v<version> matching pyproject.toml (e.g. v0.1.0), or the build job fails. Releases without a v* tag are skipped.
  • TestPyPI versions: TestPyPI won't accept the same version twice. To repeat a dry run, bump the version first.

Validation

  • uv build: sdist and wheel built successfully.
  • uvx twine check --strict dist/*: both passed.
  • Tag check: v0.1.0 passes; v0.2.0 and 0.1.0 fail against version 0.1.0.
  • The workflow file is valid YAML.
  • Not run on GitHub Actions: publishing depends on the Maintainer setup required for automated releases #228 setup and on the workflow being on main. The first real test would be a manual TestPyPI run once both are in place.

Checklist

  • I reviewed the diff and kept it focused on the described change.
  • Python changes pass uv run ruff check . and uv run ruff format --check ., or this PR does not change Python.
  • Changed behavior is covered by appropriate tests, including a regression test for bug fixes; any limitations are explained under Validation.
  • Validation lists the checks or manual flows I ran and their results.
  • Relevant documentation is updated, or no documentation change is needed.

Publishes to TestPyPI on manual trigger and to PyPI on GitHub
Release. Maintainer setup is tracked in danieldeer#228.

Refs danieldeer#229
@AnnoDomine

Copy link
Copy Markdown
Contributor

@fe-neu
As there is a lot happens in this workflow, clould you please provide here as a comment, what is going on in the different steps?
I say it how it is, i have a lot of questionmarks, when i go over the file.
A description from you would help me understanding it.

@AnnoDomine

AnnoDomine commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

In additional the question:
What we collaborators and Daniel have to take care about, when this face the main?
Do we manuell need to create a tag from the main?
Do we need to trigger the action?

I just know it from azure from the last company i worked for. They had the policy the build pipelines only run on manuel trigger.

@fe-neu

fe-neu commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

@AnnoDomine Sorry, I could have explained better what #228 and #229 are trying to do.

The goal

The goal is to make releasing a new version to PyPI simple and repeatable. Once everything is set up, a release works like this:

  1. Bump the version in pyproject.toml and merge that to main.
  2. Create a new GitHub Release with a tag like v0.2.0 (matching the version from step 1).
  3. The workflow runs and publishes to PyPI. Nothing else needs to be done.

This is a fairly standard setup for Python packages.

Your questions

Do we need to create a tag manually?
No separate step is needed. In the "Draft a new release" form, you type the new tag (e.g. v0.2.0) and GitHub creates it from the branch you pick (main) when you publish the release.

Do we need to trigger the action?

  • PyPI: publishing the release is the trigger. It's still a deliberate, manual step, similar to your manual-trigger policy in Azure: nothing is published unless someone publishes a release. Pushing a tag on its own does nothing. The workflow only reacts to published releases, and releases without a v* tag are skipped. This also keeps GitHub Releases and PyPI versions in sync.
  • TestPyPI: runs only when triggered manually from the Actions tab ("Run workflow"), for anyone who wants to test a release first.

Recommended: set up the pypi environment (and optionally testpypi) to require an approval before anything is uploaded:

  • Required reviewers: add a small group of maintainers (e.g. Daniel plus one or two others). Only the people on that list can approve a run, even if others can publish releases. GitHub allows up to 6 reviewers or teams.
  • Prevent self-review: turn this on so the person who published the release can't approve their own run. Every PyPI release then needs a second person.

The run waits with "Waiting for review" until one of the reviewers approves it in the Actions tab. This is set under Settings → Environments → pypi → Deployment protection rules, alongside the other steps in #228.

What the workflow does

build, which runs first:

  1. Skip check: runs only for manual runs or releases whose tag starts with v. Other releases are skipped, not failed.
  2. Checkout + uv: gets the code and sets up uv, like our other workflows.
  3. Tag check: for releases, makes sure the tag is exactly v + the version in pyproject.toml. If someone forgot to bump the version, the run stops here and nothing is published.
  4. Build: uv build creates the two files that get uploaded to PyPI, the source archive (.tar.gz) and the wheel (.whl).
  5. Metadata check: twine check makes sure PyPI will accept the package metadata, e.g. that the README renders.
  6. Store files: saves the built files so the publish job can use them.

publish-testpypi: manual runs only.
publish-pypi: releases only.
Both do the same thing:

  1. Download the files from build.
  2. Upload them with the official PyPA publish action.

Why are build and publish separate jobs?
Only the publish jobs get id-token: write. That permission lets the job prove to PyPI that it's this workflow in this repo (Trusted Publishing), so we don't need to store any PyPI password or token in GitHub. Keeping those jobs small makes them easy to review.

What's still needed

#228 lists the remaining steps, which need owner/admin access: the GitHub environments (including the approval rules above), the Trusted Publisher registration on PyPI/TestPyPI, and so on. The exact values to enter are in this PR's description.

If anything is still unclear, I'm happy to explain more or jump on a call.

@AnnoDomine

Copy link
Copy Markdown
Contributor

@AnnoDomine Sorry, I could have explained better what #228 and #229 are trying to do.

The goal

The goal is to make releasing a new version to PyPI simple and repeatable. Once everything is set up, a release works like this:

  1. Bump the version in pyproject.toml and merge that to main.
  2. Create a new GitHub Release with a tag like v0.2.0 (matching the version from step 1).
  3. The workflow runs and publishes to PyPI. Nothing else needs to be done.

This is a fairly standard setup for Python packages.

Your questions

Do we need to create a tag manually? No separate step is needed. In the "Draft a new release" form, you type the new tag (e.g. v0.2.0) and GitHub creates it from the branch you pick (main) when you publish the release.

Do we need to trigger the action?

  • PyPI: publishing the release is the trigger. It's still a deliberate, manual step, similar to your manual-trigger policy in Azure: nothing is published unless someone publishes a release. Pushing a tag on its own does nothing. The workflow only reacts to published releases, and releases without a v* tag are skipped. This also keeps GitHub Releases and PyPI versions in sync.
  • TestPyPI: runs only when triggered manually from the Actions tab ("Run workflow"), for anyone who wants to test a release first.

Recommended: set up the pypi environment (and optionally testpypi) to require an approval before anything is uploaded:

  • Required reviewers: add a small group of maintainers (e.g. Daniel plus one or two others). Only the people on that list can approve a run, even if others can publish releases. GitHub allows up to 6 reviewers or teams.
  • Prevent self-review: turn this on so the person who published the release can't approve their own run. Every PyPI release then needs a second person.

The run waits with "Waiting for review" until one of the reviewers approves it in the Actions tab. This is set under Settings → Environments → pypi → Deployment protection rules, alongside the other steps in #228.

What the workflow does

build, which runs first:

  1. Skip check: runs only for manual runs or releases whose tag starts with v. Other releases are skipped, not failed.
  2. Checkout + uv: gets the code and sets up uv, like our other workflows.
  3. Tag check: for releases, makes sure the tag is exactly v + the version in pyproject.toml. If someone forgot to bump the version, the run stops here and nothing is published.
  4. Build: uv build creates the two files that get uploaded to PyPI, the source archive (.tar.gz) and the wheel (.whl).
  5. Metadata check: twine check makes sure PyPI will accept the package metadata, e.g. that the README renders.
  6. Store files: saves the built files so the publish job can use them.

publish-testpypi: manual runs only. publish-pypi: releases only. Both do the same thing:

  1. Download the files from build.
  2. Upload them with the official PyPA publish action.

Why are build and publish separate jobs? Only the publish jobs get id-token: write. That permission lets the job prove to PyPI that it's this workflow in this repo (Trusted Publishing), so we don't need to store any PyPI password or token in GitHub. Keeping those jobs small makes them easy to review.

What's still needed

#228 lists the remaining steps, which need owner/admin access: the GitHub environments (including the approval rules above), the Trusted Publisher registration on PyPI/TestPyPI, and so on. The exact values to enter are in this PR's description.

If anything is still unclear, I'm happy to explain more or jump on a call.

Thanks a lot for the detailed overview.
@danieldeer I attached you as a reviewer too because i think these are highly important information for you.

Comment thread .github/workflows/publish.yml
@VihaanReddyM
VihaanReddyM removed the request for review from AnnoDomine September 20, 2026 17:39

@MarkVDD MarkVDD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Please target main, since chore/contributor-workflow-and-docs has been merged already ;)

@fe-neu
fe-neu changed the base branch from chore/contributor-workflow-and-docs to main September 20, 2026 18:22
@fe-neu

fe-neu commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Looks good to me. Please target main, since chore/contributor-workflow-and-docs has been merged already ;)

Done

@danieldeer

Copy link
Copy Markdown
Owner

Does this work without me setting up anything on pypi @fe-neu ?

@fe-neu

fe-neu commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Does this work without me setting up anything on pypi @fe-neu ?

No this will not work just like this. See Issue #228

In short you have to setup this on pypi:

image

In here : https://pypi.org/manage/account/publishing/

I know you have more Experience with GitLab, so If you would like any help with this let me know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add automated release workflow (TestPyPI + PyPI via Trusted Publishing)

5 participants