Releasing to PyPI¶
The six publishable workspace members ship to PyPI in lockstep — every release tags every package at the same version. The internal dhis2w-codegen package is workspace-only and does not ship.
| Package | PyPI |
|---|---|
dhis2w-client |
https://pypi.org/project/dhis2w-client/ |
dhis2w-core |
https://pypi.org/project/dhis2w-core/ |
dhis2w-cli |
https://pypi.org/project/dhis2w-cli/ |
dhis2w-browser |
https://pypi.org/project/dhis2w-browser/ |
dhis2w-mcp |
https://pypi.org/project/dhis2w-mcp/ |
dhis2w-mcp-bridge |
https://pypi.org/project/dhis2w-mcp-bridge/ |
Versioning policy¶
- Lockstep. All six publishable packages share the same
version =value in theirpyproject.toml. Bump them together, never one at a time. - SemVer.
MAJOR.MINOR.PATCHfor stable releases; pre-releases use SemVer suffixes (0.6.0a1,0.6.0rc1). Pre-1.0 means breaking changes can land on minor bumps. - Inter-package deps are pinned to
>=<current>,<<next-major>(e.g.dhis2w-client>=0.5.0,<0.6). When the next minor lands, every consumer's pin needs the same shift.
How to cut a release¶
-
Decide the version: pick a SemVer next from the current
version =in anypackages/*/pyproject.toml. For 0.5.0 → 0.5.1 (patch), 0.5.0 → 0.6.0 (minor with possibly-breaking changes), 0.5.0 → 1.0.0 (committed stable surface). -
Bump every
packages/*/pyproject.tomlin lockstep. Update both: - The package's own
version = "X.Y.Z". -
Every workspace dep pin like
"dhis2w-core>=0.5.0,<0.6". The lower bound should match the new release; the upper bound shifts to the next major (<0.6→<0.7only on minor bumps, never on patch). -
Refresh the lockfile:
-
Commit the bump with a short conventional-commit message —
chore(release): v0.6.0. -
Tag the commit and push (annotated — the repo's git config requires a tag message):
-
Watch the workflow. The tag triggers
.github/workflows/pypi-publish.yml. Sixbuildjobs produce wheels in parallel; onepublishjob uploads them all via PyPI Trusted Publishing (OIDC, no API token), withskip-existingso a re-run after a partial publish is safe. -
Create the GitHub release (the tag alone does not — the Releases page stays on the previous version otherwise):
gh release create v0.6.0 --verify-tag --title v0.6.0 --notes-file <(sed -n '/^## 0.6.0/,/^## /p' CHANGELOG.md | sed '1d;$d') --latest
- Verify:
- https://github.com/winterop-com/dhis2w-utils/actions — all green.
uvx --refresh --from 'dhis2w-client==0.6.0' python -c 'import dhis2w_client; print(dhis2w_client.__file__)'pulls and imports the new wheel.uv tool list(oruv tool upgrade dhis2w-cli) shows the right version.
First release of a new package¶
A brand-new dhis2w-* project does not exist on PyPI yet, and OIDC cannot create it from a
non-user identity. Before its first release, add a pending publisher on PyPI (one-time, web UI
only): https://pypi.org/manage/account/publishing/ → "Add a new pending publisher":
- PyPI Project Name:
dhis2w-<name> - Owner:
winterop-com· Repository:dhis2w-utils - Workflow filename:
pypi-publish.yml· Environment:pypi
Without it, the publish job 400s on that wheel (Non-user identities cannot create new projects).
The siblings that sort earlier still upload, so the publish step is skip-existing: once the
pending publisher exists, re-run with gh workflow run pypi-publish.yml -f version=<X.Y.Z> and only
the missing package uploads.
Pre-release flow¶
For dry runs without committing to a SemVer slot:
The workflow accepts the pre-release pattern and uploads as a pre-release to PyPI. Consumers get it only with uv tool install dhis2w-cli --prerelease=allow (or uv add dhis2w-client --prerelease=allow inside a project).
Yanking a release¶
Don't delete published wheels — yank them instead. Yanking keeps the file available so existing pins still resolve, but new resolves skip it:
(Or do it through PyPI's web UI under each project's Manage page.)
When to bump major (1.0.0)¶
The pre-1.0 marker says "API is still moving". Move to 1.0.0 when:
- The dhis2w-client public surface (the imported names from dhis2w_client) is committed for at least 6 months across two minor releases.
- The CLI command names + flags are stable.
- The MCP tool catalogue is committed.
After 1.0.0, breaking changes require a major bump.