Release Process
Use this page when cutting a new release of flopscope.
You will learn:
- How commit types map to version bumps under semver
- How
cz bumpupdates versions across the three packages and tags - What happens after
git push --follow-tags - The one-time PyPI Trusted Publisher setup per project
Version bumps
Versions follow Semantic Versioning. While at 0.x, the project is
API-unstable — breaking changes bump the minor digit, not the major.
Commit types map to bumps as follows:
| Commit type | 0.x bump | ≥1.0 bump |
|---|---|---|
fix: | patch (0.2.0 → 0.2.1) | patch |
feat: | minor (0.2.0 → 0.3.0) | minor |
feat!: / BREAKING CHANGE: | minor (0.2.0 → 0.3.0) | major (1.4 → 2.0) |
Other types (chore, docs, …) | no release | no release |
Commitizen handles this automatically via major_version_zero = true in
the [tool.commitizen] block of pyproject.toml. Flip that to false
when we cut v1.0.0.
End-to-end flow
We release with commitizen. End-to-end flow:
# 1. Make sure local main is at origin/main
git checkout main && git pull origin main
# 2. Preview the next version + changelog (no writes)
uv run cz bump --dry-run
# 3. Cut the release: bumps pyproject.toml versions across all three
# packages, prepends to CHANGELOG.md, creates the v<x.y.z> git tag.
uv run cz bump
# 4. Push the commit + tag. Tag push triggers the PyPI publish workflow.
git push --follow-tagsPre-releases: uv run cz bump --prerelease alpha → produces tags like
v0.3.0a0.
Multi-package lockstep
flopscope, flopscope-server, and flopscope-client are released in
lockstep at the same version. cz bump updates all version
locations in one invocation via the version_files config in
[tool.commitizen]:
- All three
pyproject.toml[project].versionfields - All three
__version__strings in package__init__.pyfiles - The cross-package pin (
flopscope==X.Y.Zin flopscope-server'sdependencies)
Before tagging, check-sync-versions asserts that all seven version
locations agree. Run locally:
make check-sync-versionsCI runs this in the client-server-sync job on every PR, so drift
introduced by hand-edits is caught before merge.
What happens after git push --follow-tags
The tag push triggers
.github/workflows/pypi-publish.yml,
which:
- Builds wheels in parallel via a matrix job — one per published package.
- Pauses for approval in the
pypiGitHub environment (manual gate). A single approval click covers all matrix-publish jobs. - Publishes the packages to PyPI in parallel via Trusted Publishing (OIDC; no API token stored in repo secrets).
- Creates one GitHub Release whose body is the matching CHANGELOG section for that tag.
End result: pip install <package>==X.Y.Z works ~2 minutes after a
maintainer clicks "approve" on the pypi environment gate.
First-time PyPI setup (one-time per project)
Before the first release will succeed, configure two things outside the repo. The Trusted Publisher steps must be repeated for each PyPI project name being published.
On PyPI — add a Trusted Publisher (do this for each project name):
- Go to https://pypi.org/manage/account/publishing/
- Click "Add a new pending publisher"
- Fill in (repeat with each project name in turn):
- PyPI Project Name:
flopscope, thenflopscope-server, thenflopscope-client - Owner:
AIcrowd - Repository name:
flopscope - Workflow name:
pypi-publish.yml - Environment name:
pypi
- PyPI Project Name:
- Save each one.
On GitHub — create the pypi environment with required reviewers
(do this once; it is shared across all matrix-publish jobs):
- Go to https://github.com/AIcrowd/flopscope/settings/environments
- Click "New environment", name it
pypi - Under "Deployment protection rules" → "Required reviewers", add the release maintainers
- Save
After all configured Trusted Publishers and the shared environment are
set up, the next v* tag push will trigger the publish flow
end-to-end — and a single approval click covers all matrix publishes.
Manual publish from a maintainer's machine (fallback)
The normal path is the matrix workflow described above. Use this section only
when GitHub Actions is unavailable or won't fire (Actions outage, repo policy
glitch, etc.) and you need to ship a release right now. This path bypasses
Trusted Publishing, the pypi environment approval gate, and the auto-created
GitHub Release.
One-time setup
-
Install
twineas a uv-managed tool (keeps it out of project dependencies):uv tool install twine -
Generate three project-scoped PyPI API tokens, one per project. For each of
flopscope,flopscope-server,flopscope-client:- Go to https://pypi.org/manage/account/token/
- Click "Add API token"
- Token name: something self-descriptive (e.g.
flopscope-manual-2026) - Scope: "Project: flopscope" (scope to a single project — not "Entire account")
- Copy the
pypi-...token immediately; PyPI will not show it again
-
Add all three tokens to
~/.pypirc(chmod 600 the file). Per-project sections let twine pick the right one based on the wheel's distribution name:[distutils] index-servers = flopscope flopscope-server flopscope-client [flopscope] repository = https://upload.pypi.org/legacy/ username = __token__ password = pypi-AgEIcHlwaS5vcmcCJ... # token scoped to flopscope [flopscope-server] repository = https://upload.pypi.org/legacy/ username = __token__ password = pypi-AgEIcHlwaS5vcmcCJ... # token scoped to flopscope-server [flopscope-client] repository = https://upload.pypi.org/legacy/ username = __token__ password = pypi-AgEIcHlwaS5vcmcCJ... # token scoped to flopscope-clientchmod 600 ~/.pypirc
Publishing v<X.Y.Z> manually
Assumes the bump commit + vX.Y.Z tag already exist on main (use
cz bump --increment <PATCH|MINOR> as usual to produce them; the
difference is only that the GH workflow won't run).
-
Build all three packages from the tagged commit:
git checkout vX.Y.Z rm -rf dist flopscope-server/dist flopscope-client/dist uv build (cd flopscope-server && uv build) (cd flopscope-client && uv build) -
Verify each wheel's metadata is what you expect — particularly the
Requires-Distcross-pin in flopscope-server and theProvides-Extra: serverin flopscope:unzip -p dist/flopscope-X.Y.Z-py3-none-any.whl flopscope-X.Y.Z.dist-info/METADATA \ | grep -E "^(Name|Version|Requires-Dist|Provides-Extra):" unzip -p flopscope-server/dist/flopscope_server-X.Y.Z-py3-none-any.whl \ flopscope_server-X.Y.Z.dist-info/METADATA \ | grep -E "^(Name|Version|Requires-Dist):" unzip -p flopscope-client/dist/flopscope_client-X.Y.Z-py3-none-any.whl \ flopscope_client-X.Y.Z.dist-info/METADATA \ | grep -E "^(Name|Version|Requires-Dist):" -
Upload each wheel + sdist with
twine, pointing at the per-project section of~/.pypirc:uv tool run twine upload --repository flopscope dist/* uv tool run twine upload --repository flopscope-server flopscope-server/dist/* uv tool run twine upload --repository flopscope-client flopscope-client/dist/*Twine prints a https://pypi.org/project/.../ URL after each successful upload. If a previous attempt already uploaded any of the files, add
--skip-existingto make the command idempotent. -
Create the GitHub Release manually (the workflow would normally do this):
version=X.Y.Z awk -v ver="v$version" ' $1 == "##" && $2 == ver { capture = 1; next } $1 == "##" && capture { exit } capture { print } ' CHANGELOG.md > /tmp/release-notes.md gh release create "v$version" --title "v$version" --notes-file /tmp/release-notes.md
Trade-offs vs the workflow
The manual path skips four safety nets the workflow gives you:
- Environment approval gate. Anyone with the API tokens can publish; there is no second-pair-of-eyes click. Scope tokens narrowly and rotate them after use if leaked or no longer needed.
- Trusted Publishing audit trail. PyPI records "uploaded by API token X" instead of "uploaded by GH workflow run #..." — harder to trace.
- Auto-generated GitHub Release with notes from CHANGELOG. Step 4 above is the manual replacement.
- Matrix isolation. Network/IO failures during upload could leave a
partial publish (e.g., flopscope uploaded but flopscope-server failed).
In that case re-run only the failed
twine uploadwith--skip-existing.
Once Actions is healthy again, switch back to the workflow path — delete
the API tokens you created for the manual publish, and the next vX.Y.Z tag
push will go through Trusted Publishing as normal.
Related references
[tool.commitizen]in pyproject.toml — release tooling config.github/workflows/pypi-publish.yml— the publish workflowscripts/check_version_sync.py— version-sync checker- Contributor Guide — repo layout, dev setup, commit conventions,
commit-msghook