This project publishes to PyPI in two stages: a release candidate on TestPyPI first, then the same tagged version on production PyPI once it's been verified. The goal is to never publish a version to production PyPI that hasn't already been installed and exercised from TestPyPI.
- Cut a release candidate, publish it to TestPyPI, and tag it.
- Test the TestPyPI package.
- Optionally publish that same tag to production PyPI, to test against the real PyPI environment.
- Once happy, cut the final release (same version,
rc*suffix dropped), publish it to TestPyPI, and tag it. - Test again, then publish that tag to production PyPI.
Both the TestPyPI and production PyPI publish steps are only triggered
manually, from the Release workflow in GitHub Actions, and only run
from the main branch.
- Bump
__version__insrc/powersensor_local/__init__.pyand append anrc*suffix, e.g.2.2.1rc1. - Merge that change into
main.
- Trigger the Release workflow from the GitHub Actions web interface.
- Select
test.pypias the publish destination. - The workflow reads
__version__straight out of__init__.pyonmain, builds the distribution, and publishes it to TestPyPI. - On a successful publish, the workflow creates and pushes a git tag
matching the version string (e.g.
v2.2.1rc1).
- Install the package from TestPyPI and verify it behaves as expected.
- If you want to test against the real PyPI environment before doing a
final release, trigger the Release workflow again, this time
selecting
pypias the destination and entering the tag created in step 2 (e.g.v2.2.1rc1). - The workflow checks out that exact tag, builds from it, and publishes to PyPI. It does not create a tag in this path, since the tag already exists from step 2.
- Once you're happy with the release candidate, remove the
rc*suffix from__version__(e.g.2.2.1rc1→2.2.1) and merge intomain. - Repeat steps 2–3: publish to TestPyPI, which tags the final version
(e.g.
v2.2.1), and test that package. - Publish that tag to production PyPI the same way as step 4, using the final tag this time. This is the real release.
- TestPyPI before PyPI: PyPI (and TestPyPI) reject re-uploading a filename that's already been published — there's no way to overwrite or delete a bad upload. Requiring every version to land on TestPyPI first, and be tested there, catches packaging problems (missing files, bad metadata, broken entry points, etc.) before they can burn a version number on production PyPI.
- Publishing the exact same tag to both indexes: TestPyPI and PyPI are independent indexes, so the same version string can be published to both. Releasing to PyPI always checks out a tag that was already built and validated via the TestPyPI path, rather than rebuilding from a moving branch tip, so what you tested is what ships.
- Tagging only happens after a successful TestPyPI publish: this keeps a pushed tag meaningful — if the TestPyPI publish fails (for example, because the version wasn't bumped and the filename already exists), no tag is created and nothing needs to be cleaned up. The production PyPI publish step never creates a tag, since by construction the tag already exists from the TestPyPI step; this avoids ever trying to recreate/move an existing tag.
main-only restriction: publishing (to either index) is only allowed when the workflow is triggered frommain, so every published version corresponds to a real, reviewed commit on the trunk branch, never an untested feature branch.rc*suffix: using a PyPA pre-release suffix meanspip install powersensor-localnever resolves to a release candidate by accident — installers only pick up anrcversion if you explicitly ask for it (pip install --preor an exact version pin). It also keeps release candidates and final releases as distinct, individually addressable versions on both indexes.