Publishing
Once your package is ready, you can publish it to PyPI (Python Package Index) so others can install it with pip.
There are two routes: manually from your machine with just publish, or
automatically via the generated release.yml workflow whenever you publish
a GitHub release.
Prerequisites
- Create a free account at pypi.org — and, for the automated route, a separate one at test.pypi.org
- For manual publishing: generate an API token in your PyPI account settings. The automated route needs no token (see Trusted publishing)
Manual Publishing
1. Update Version
Edit pyproject.toml:
[project]
name = "your-package"
version = "1.0.0"
description = "Your package description"
Follow Semantic Versioning: MAJOR.MINOR.PATCH - Major (1.0.0) - Breaking changes - Minor (0.1.0) - New features - Patch (0.0.1) - Bug fixes
2. Update Changelog
Document changes in CHANGELOG.md:
## Version 1.0.0 (2024-02-25)
### Added
- New feature X
### Fixed
- Bug in module Y
3. Build Package
just build
# Creates dist/your-package-*.whl and dist/your-package-*.tar.gz
4. Upload to PyPI
just publish
# Equivalent to: uv build && uv publish
When prompted for credentials, use:
- Username: __token__
- Password: Your PyPI API token
View on PyPI:
https://pypi.org/project/your-package-name/
Users can then install your package:
pip install your-package-name
Automated Publishing
The generated project ships .github/workflows/release.yml. It runs whenever you publish a
GitHub release and passes the package through three jobs:
build— runsuv buildonce and uploadsdist/as a workflow artifacttest-pypi— uploads that artifact to TestPyPI (GitHub environmenttestpypi)pypi— runs only if the TestPyPI upload succeeded, and uploads the very same files to PyPI (GitHub environmentpypi)
Both upload jobs authenticate via trusted publishing
(OpenID Connect, hence permissions: id-token: write): PyPI trusts this particular workflow in
this particular repository, so there is no API token to create, store as a secret, or leak.
1. Register the workflow as a trusted publisher
Do this once on both PyPI and TestPyPI. For a project that does not exist on the index yet, add a pending publisher with:
| Field | Value |
|---|---|
| PyPI Project Name | the name in your pyproject.toml (your project slug) |
| Owner | your GitHub user or organization |
| Repository name | your repository |
| Workflow name | release.yml |
| Environment name | pypi on PyPI, testpypi on TestPyPI |
The environment name must match exactly — it is part of what PyPI verifies.
2. Optional: approve each PyPI upload by hand
Uploads to PyPI are permanent: a version number can never be reused, even after deleting the
release on PyPI. To get a final confirmation step between TestPyPI and PyPI, make yourself a required
reviewer on the pypi environment:
just set-pypi-review
The recipe uses the GitHub CLI (gh, logged in with admin rights on the
repository) to create the pypi environment if needed and add you as its reviewer. Self-review is
explicitly allowed, so you can approve releases you started yourself. From then on, each release
run pauses before the pypi job until you click Review deployments in the run's page on the
Actions tab. Alternatively, configure the same under Settings → Environments → pypi →
Required reviewers.
Private repositories need GitHub Enterprise
On GitHub Free, Pro and Team plans, required reviewers are only available for public repositories.
3. Publish a release
Bump the version in pyproject.toml and update the changelog (steps 1 and 2 of
Manual Publishing), commit and push, then release:
just release # gh release create v1.0.0 --generate-notes, version taken from pyproject.toml
If the tag v1.0.0 does not exist yet, gh creates it on the tip of the default branch on GitHub —
so push first. To release from an annotated tag instead, run just tag beforehand; just release
then uses the existing tag. You can also create the release in the GitHub UI (Releases → Draft a
new release). Pushing a tag alone does not trigger the workflow — only publishing a release
does.
The workflow first checks that the release tag equals v + the version in pyproject.toml and
stops before uploading anything if they differ — for example when the version bump was not pushed.
It then uploads to TestPyPI, waits for your approval if you set up a reviewer, and uploads to PyPI.
Follow its progress on the Actions tab.
A failed release needs a new version number
If the PyPI job fails after the TestPyPI upload succeeded, re-running the whole workflow fails at TestPyPI, because that version already exists there. Re-run only the failed job, or bump the version and release again.
Note
Always test your package locally before publishing!
Configuration Reference
Key pyproject.toml fields:
[project]
name = "package-name" # Unique on PyPI
version = "1.0.0"
description = "Brief description"
readme = "README.md"
requires-python = ">= 3.10"
license = {file = "LICENSE"}
authors = [{name = "Your Name", email = "you@example.com"}]
[project.urls]
Homepage = "https://github.com/user/package"
Documentation = "https://user.github.io/package"
Repository = "https://github.com/user/package.git"
Issues = "https://github.com/user/package/issues"
[project.scripts]
my-cli = "my_package.cli:main" # CLI entry point
See Also
- GitHub Actions - CI/CD workflows
- Task Automation -
just release,just tag,just buildandjust set-pypi-review