flopscope.
Development

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 bump updates 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 type0.x bump≥1.0 bump
fix:patch (0.2.00.2.1)patch
feat:minor (0.2.00.3.0)minor
feat!: / BREAKING CHANGE:minor (0.2.00.3.0)major (1.42.0)
Other types (chore, docs, …)no releaseno 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-tags

Pre-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].version fields
  • All three __version__ strings in package __init__.py files
  • The cross-package pin (flopscope==X.Y.Z in flopscope-server's dependencies)

Before tagging, check-sync-versions asserts that all seven version locations agree. Run locally:

make check-sync-versions

CI 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:

  1. Builds wheels in parallel via a matrix job — one per published package.
  2. Pauses for approval in the pypi GitHub environment (manual gate). A single approval click covers all matrix-publish jobs.
  3. Publishes the packages to PyPI in parallel via Trusted Publishing (OIDC; no API token stored in repo secrets).
  4. 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):

  1. Go to https://pypi.org/manage/account/publishing/
  2. Click "Add a new pending publisher"
  3. Fill in (repeat with each project name in turn):
    • PyPI Project Name: flopscope, then flopscope-server, then flopscope-client
    • Owner: AIcrowd
    • Repository name: flopscope
    • Workflow name: pypi-publish.yml
    • Environment name: pypi
  4. Save each one.

On GitHub — create the pypi environment with required reviewers (do this once; it is shared across all matrix-publish jobs):

  1. Go to https://github.com/AIcrowd/flopscope/settings/environments
  2. Click "New environment", name it pypi
  3. Under "Deployment protection rules" → "Required reviewers", add the release maintainers
  4. 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

  1. Install twine as a uv-managed tool (keeps it out of project dependencies):

    uv tool install twine
  2. 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
  3. 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-client
    chmod 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).

  1. 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)
  2. Verify each wheel's metadata is what you expect — particularly the Requires-Dist cross-pin in flopscope-server and the Provides-Extra: server in 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):"
  3. 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-existing to make the command idempotent.

  4. 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 upload with --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.

On this page