GitHub Actions: Pure Python wheels¶
We will cover binary wheels on the next page, but if you do not have a compiled extension, this is called a universal (pure Python) package, and the procedure to make a “built” wheel is simple. At the end of this page, there is a recipe that can often be used exactly for pure Python wheels (if the previous recommendations were followed).
Job setup¶
name: CD
on:
workflow_dispatch:
release:
types:
- published
jobs:This will run when you manually trigger a build (workflow_dispatch), or
when you publish a release. Later, we will make sure that the actual publish
step requires the event to be a publish event, so that manual triggers (and
branches/PRs, if those are enabled).
If you want tags instead of releases, you can add the on: push: tags: "v*" key
instead of the releases - however, please remember to make a GitHub release of
your tag! It shows up in the GUI and it notifies anyone watching
releases(-only). You will also need to change the event filter below.
You can merge the CI job and the CD job if you want. To do that, preferably with
the name “CI/CD”, you can just combine the two on dicts.
Distribution: Pure Python wheels¶
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build SDist and wheel
run: pipx run build
- uses: actions/upload-artifact@v7
with:
name: Packages
path: dist/*
- name: Check metadata
run: pipx run twine check dist/*We use PyPA-Build, a new build tool designed to make building wheels and SDists easy. It run a PEP 517 backend and can get PEP 518 requirements even for making SDists.
By default this will make an SDist and a wheel from the package in the current
directory, and they will be placed in ./dist. You can only build SDist (-s),
only build wheel (-w), change the output folder (-o <dir>) or give a
different input folder if you want.
You could use the setup-python action, install build and twine with pip,
and then use python -m build or pyproject-build, but it’s better to use
pipx to install and run python applications. Pipx is provided by default by
GitHub Actions (in fact, they use it to setup other applications).
We upload the artifact just to make it available via the GitHub PR/Checks API. You can download a file to test locally if you want without making a release.
We also add an optional check using twine for the metadata (it will be tested later in the upload action for the release job, as well).
And then, you need a release job. Trusted Publishing is more secure and recommended GH105:
publish:
needs: [dist]
environment: pypi
permissions:
id-token: write
attestations: write
contents: read
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
steps:
- uses: actions/download-artifact@v8
with:
name: Packages
path: dist
- name: Generate artifact attestation for sdist and wheel
uses: actions/attest-build-provenance@v4
with:
subject-path: "dist/*"
- uses: pypa/gh-action-pypi-publish@release/v1When you make a GitHub release in the web UI, we publish to PyPI. You’ll just
need to tell PyPI which org, repo, workflow, and set the pypi environment to
allow pushes from GitHub. If it’s the first time you’ve published a package, go
to the PyPI trusted publisher docs for instructions on preparing PyPI to
accept your initial package publish.
We are also generating artifact attestations, which can allow users to verify that the artifacts were built on your actions.
publish:
needs: [dist]
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
steps:
- uses: actions/download-artifact@v8
with:
name: Packages
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_password }}If you cannot use Trusted Publishing, this publishes to PyPI with a token.
You’ll need to go to PyPI, generate a token for your user, and put it into
pypi_password on your repo’s secrets page. Once you have a project, you should
delete your user-scoped token and generate a new project-scoped token.
Complete recipe
This can be used on almost any package with a standard
.github/workflows/cd.yml recipe. This works because pyproject.toml describes
exactly how to build your package, hence all packages build exactly via the same
interface:
name: CD
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
release:
types:
- published
jobs:
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: hynek/build-and-inspect-python-package@v2
publish:
needs: [dist]
environment: pypi
permissions:
id-token: write
attestations: write
contents: read
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
steps:
- uses: actions/download-artifact@v8
with:
name: Packages
path: dist
- name: Generate artifact attestation for sdist and wheel
uses: actions/attest-build-provenance@v4
with:
subject-path: "dist/*"
- uses: pypa/gh-action-pypi-publish@release/v1name: CD
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
release:
types:
- published
jobs:
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: hynek/build-and-inspect-python-package@v2
publish:
needs: [dist]
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
steps:
- uses: actions/download-artifact@v8
with:
name: Packages
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_password }}If you cannot use Trusted Publishing, this publishes to PyPI with a token.
You’ll need to go to PyPI, generate a token for your user, and put it into
pypi_password on your repo’s secrets page. Once you have a project, you should
delete your user-scoped token and generate a new project-scoped token.