Releasing to PyPI
PyPI is the index that pip and uv install from. radiowriter has been on PyPI since version 0.1.0 (3 September 2026). This page describes how to publish a new version.
A version is published by pushing a git tag that starts with v. Nothing else publishes: a push to main does not, and the workflow cannot be started by hand.
Requirements
- A PyPI account, from pypi.org/account/register, with two-factor authentication. PyPI requires it for publishing.
- A TestPyPI account, if you publish release candidates. TestPyPI is a separate site with separate accounts.
- Trusted Publishing set up on both sites, as described below.
- The
versioninpyproject.tomlequal to the tag. The workflow stops if they differ.
Trusted Publishing
With an API token, the token has to be stored on your computer or as a GitHub secret, and anyone who obtains it can publish under the project's name.
Trusted Publishing does not use a stored token. On PyPI you declare that the workflow release.yml in gmadevs/Radiowriter may publish radiowriter. GitHub signs each run of the workflow, and PyPI verifies the signature.
Set it up once on each site. The only difference between the two forms is the environment name:
| PyPI | TestPyPI | |
|---|---|---|
| Where | pypi.org | test.pypi.org |
| Project name | radiowriter | radiowriter |
| Owner | gmadevs | gmadevs |
| Repository | Radiowriter | Radiowriter |
| Workflow | release.yml | release.yml |
| Environment | pypi | testpypi |
The two environments also have to exist on GitHub. Create them under Settings → Environments → New environment, named pypi and testpypi.
For a project that does not exist on the site yet, use the "pending publisher" form. The first successful run creates the project.
If the workflow fails with invalid-publisher
The message is "valid token, but no corresponding publisher", followed by the claims the workflow sent: repository, workflow_ref and environment. The form on the site has to match them. The usual causes are a different environment name, or a publisher added on PyPI when the tag went to TestPyPI (or the reverse).
Where a tag is published
The form of the tag decides the destination:
| Tag | Published to |
|---|---|
a pre-release, such as v0.1.0rc1, v0.2.0a3, v1.0.0b2 | TestPyPI |
a final version, such as v0.1.0, v1.2.3 | PyPI |
test.pypi.org is a separate copy of PyPI for testing. Publish a release candidate there before a final version.
The workflow compares the tag with the version in pyproject.toml and stops if they differ. Otherwise the GitHub release page and the installed package would show different versions. The comparison uses packaging, because PEP 440 normalises versions: 0.1.0-rc1 and 0.1.0rc1 are the same version.
After pushing a release candidate, check that it installs:
uv tool install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ radiowriterBoth index URLs are needed. TestPyPI does not have Streamlit or pandas, so the package is installed from TestPyPI and its dependencies from PyPI.
Making a release
Set the version in
pyproject.toml, for exampleversion = "0.1.4".Run the tests:
bashpython3 check_rules.py && python3 check_structure.py && \ python3 check_search.py && python3 check_journals.py && \ python3 check_library.py && python3 check_bench.py && \ python3 check_app.pyCommit, tag and push:
bashgit commit -am "Version 0.1.4" git tag v0.1.4 git push && git push --tags
The tag starts release.yml. The workflow:
- checks that the tag and the version agree;
- runs the tests (all the scripts above except
check_library.py); - builds the wheel and the sdist and runs
twine check; - uploads them to PyPI, or to TestPyPI for a pre-release;
- creates a GitHub release with the files, marked as a pre-release when the tag is one.
A few minutes later the version can be installed:
uv tool install radiowriter # or: uv tool upgrade radiowriterChecking a build locally
pip install build twine
python -m build # writes dist/
twine check dist/* # checks that PyPI can render the README
pip install dist/radiowriter-*.whl # in a temporary venv
radiowriter --versionIf PyPI cannot render the README, the project page is empty. twine check reports this before the upload.
A published version cannot be replaced
A file uploaded as version 0.1.0 cannot be changed or uploaded again. A version can be yanked: it is then hidden from new installs and stays available to projects that pinned it. It cannot be overwritten. For this reason, publish a release candidate to TestPyPI before each final version.
Installing from git
The package can also be installed from the repository, without an index:
uv tool install git+https://github.com/gmadevs/RadiowriterPyPI gives the last released version. The repository gives the current state of main, which can include unreleased and unfinished changes.