Two wheels, one version, published together — dynamic-config-py and
dynamic-config-py-remote. Nothing here goes to crates.io.
Two distributions, one version, always. dynamic-config-py[remote]
resolves to a pair: the remote wheel imports Format and RemoteSource
from the base one, so a gap between them is a combination nobody has
tested. The cost is stated rather than hidden — a fix to the etcd binding
bumps the base wheel too, and its changelog will carry a version whose
entry says nothing changed there.
scripts/release-python.sh moves all five files in one commit: both
manifests, both changelogs, and the dynamic-config-py>=… floor in the
remote wheel's pyproject.toml, which lags into a broken pair if it is
left behind.
Work lands on dev. main is production: it accepts no direct pushes —
not even from admins — only pull requests whose gates ("CI is green",
"Security is green") have passed, merged with a linear history.
Merging a version bump into main is the release. There is no tag to
push by hand: release.yml runs on every push to main, checks whether
the wheel version is new, and — only then — builds every wheel, uploads
them, and mints the tag and the GitHub release itself, at the merge commit.
-
Land the work on
devthrough pull requests, entries accumulating under## [Unreleased]in the changelog of whichever wheel changed. -
Pre-flight.
just checkondev, andjust python-free-threaded /path/to/venvwhen anything touched threading, the GIL, or a global. -
./scripts/release-python.sh --check minor— the target, not the version the tree is on. That one has already been released, so a bare--checkalways reports it as taken, which says nothing about the release being planned. It refuses a version that is already on PyPI — and that is not hypothetical: 0.1.0 shipped with one release, the next wave prepared 0.1.0 again, andmaturin upload --skip-existingmade the whole wave a silent no-op. It also checks that the two manifests agree, that the floor matches, and that there is something under## [Unreleased]to release.Optional, strictly: step 4 runs the same check on the same target before it changes a file, and stops there if anything fails.
-
./scripts/release-python.sh patch(orminor; pre-1.0 a breaking change isminor). Bumps both, rotates both changelogs, moves the floor, and makes one local commit — no push, no tag, no publish. -
cargo check -p dynamic-config-python, thengit add Cargo.lock && git commit --amend --no-edit: the lockfile follows the bump, and a release commit that leaves it behind makes the nextcargocommand dirty the tree. -
Read the commit.
git show --stat HEAD: two manifests, two changelogs, onepyproject.toml, one lockfile. Exactly one heading per version, entries under the new one,Unreleasedempty again. -
./scripts/promote.sh. Pushesdev, opens or updates the pull request, arms auto-merge and waits; when both gates pass, the squash-merge lands — that merge is the release. -
./scripts/watch-release.sh. Follows the run the merge set off: a wheel per platform, the free-threaded pair,maturin upload --skip-existing, then the tag and the GitHub release.
A Python-only release needs nothing else. ./scripts/release-python.sh --publish exists for the case where a wheel has to go out from a commit
that is already on main — it dispatches the same workflow by hand.
No token. The publish job authenticates through PyPI's Trusted
Publishing (OIDC): the one-time console entry per project — PyPI →
project → Settings → Publishing → Add a trusted publisher — names
owner dynamic-config-rs, repository dynamic-config-python,
workflow release.yml, for both dynamic-config-py and
dynamic-config-py-remote. The wheels are built by maturin-action
on GitHub's own runners; a leftover PYPI_TOKEN secret is inert and
should be revoked.
Check that both projects show the new version on PyPI, that the classifiers
list the interpreters this release actually tested, and that
pip install "dynamic-config-py[remote]" in a clean environment resolves
to the pair rather than to one new wheel and one old one.
- Pre-1.0, a breaking change bumps the minor version and everything else the patch.
- A change to the minimum supported Python version is breaking. So is dropping a platform wheel.
- MSRV changes are breaking for anybody building from source, and every floor has a CI row against a real toolchain.
- The engine is a dependency here, named with a caret. A breaking engine release is not automatically a breaking release of these wheels — what matters is whether the Python surface moved.