Release Process¶
Commol follows Semantic Versioning (SemVer) for version numbering: MAJOR.MINOR.PATCH
Version Guidelines¶
MAJOR version (X.0.0)¶
Incremented for incompatible API changes:
- Breaking changes to public API
- Removal of deprecated features
- Major architectural changes affecting backwards compatibility
- Example: Changing method signatures, removing public methods
MINOR version (0.X.0)¶
Incremented for backwards-compatible new features:
- New functionality added
- New kinds of bins, stratifications or transitions
- Performance improvements without API changes
- Deprecation notices (without removal)
- Example: Adding new mathematical functions, new model types
PATCH version (0.0.X)¶
Incremented for backwards-compatible bug fixes:
- Bug fixes that don't change the API
- Documentation improvements
- Internal refactoring
- Security patches
- Example: Fixing calculation errors, typos in error messages
Versions before 1.0.0
While Commol is at 0.x, a minor release (0.X.0) ships one theme of new
features, the one that its milestone names, and it may break the API: the
changelog marks each break Breaking. A patch release (0.X.Y) ships
fixes only. So a breaking change waits for the next minor release, and a
new feature never goes into a patch release.
Where the Version Comes From¶
The version is not stored in the repository. [workspace.package] of the root
Cargo.toml keeps a placeholder (0.1.0), which every crate takes, and
pyproject.toml declares the version as dynamic. When a release is built, the
set-version action (.github/actions/set-version) reads the git tag, writes
it into [workspace.package], and maturin turns it into the Python package
version.
Do not commit version bumps. The tag is the version.
A build that is not a release, such as an install from a git commit or
maturin develop, keeps the placeholder. The build script of the extension
(py-commol/build.rs, with its logic in py-commol/src/build_info.rs) reads
the commit from the git metadata of the source, without the git program, and
commol.__version__ adds it to the placeholder as a PEP 440 local version:
0.1.0+g<first 7 characters of the commit>. A build of any other version is a
release, and __version__ gives that version alone, in PEP 440 (X.Y.Z-beta.N
gives X.Y.ZbN). commol.build_info() gives the version and the full commit,
or None when the source has no git metadata (a source distribution).
tests/suite/test_version.py checks the version and the commit of the build
that the tests run on, and the Rust tests of build_info.rs check the rule.
Release Workflow¶
A release is a tag on main. Only a fix of a version that main has left
behind is a tag on its branch release/X.Y
(Branching Strategy). Publishing a GitHub release starts the
Release to PyPI workflow (.github/workflows/release.yaml), which
publishes to PyPI immediately: the pypi environment has no approval step.
The release issue (label release) holds the checklist of this page, and the
milestone of the release lists the issues it ships. The release issue waits
for each open proposal and change of its milestone, through "blocked by" links
that next.sh and start.sh write.
The milestone has the version as its title (0.9.1): every issue of a later
milestone waits for this release issue, through a "blocked by" link that
next.sh and start.sh write (Milestones).
A published version is final
PyPI never accepts the same version number twice, even after you delete it. Check the tag and the commit before you publish.
Step 1: Choose the Version¶
An issue of the milestone that will not ship in this release moves to another milestone, with a comment that says why (Milestones). When every issue left in the milestone is closed, list the changes since the last release:
git fetch --tags origin
git log --oneline --first-parent "$(git describe --tags --abbrev=0 origin/main)"..origin/main
Use the Version Guidelines above. The [Unreleased] section of
docs/about/changelog.md lists the changes, and the breaking ones are marked
Breaking.
Step 2: Date the Changelog¶
On a branch from main, in docs/about/changelog.md:
- Rename
## [Unreleased]to## [X.Y.Z] - YYYY-MM-DD, the day you publish, and add an empty## [Unreleased]above it. - Start the section with a short paragraph that says what the release is about and what a user must read before upgrading. It opens the release notes.
- Add the version to the Version History list.
Check the notes the release will have (Step 4):
Open the pull request to main (docs(changelog): release X.Y.Z) and merge
it through the merge queue.
Step 3: Create the Tag¶
Tag the commit at the top of main with an annotated tag:
git checkout main
git pull --ff-only origin main
git tag -a vX.Y.Z -m "Release version X.Y.Z"
git push origin vX.Y.Z
The tag must start with v. Pushing a tag does not publish anything.
Step 4: Publish the GitHub Release¶
The notes come from the changelog of main, never by hand: its section
[X.Y.Z], with the links turned into links to the site, the install command
and the comparison with the previous release
(.github/scripts/release_notes.py). Then publish the release, from the
GitHub Releases page ("Draft a new
release", tag vX.Y.Z, title Version X.Y.Z) or with the GitHub CLI:
python .github/scripts/release_notes.py X.Y.Z > notes.md
gh release create vX.Y.Z --verify-tag --title "Version X.Y.Z" --notes-file notes.md --latest
Step 5: Watch the PyPI Publication¶
The Release to PyPI workflow:
- Builds the wheels for Linux and macOS (x86_64 and aarch64) and Windows (x86_64), for Python 3.12, 3.13 and 3.14
- Builds the source distribution (sdist); the wheels and the sdist build
with the maturin of
py-commol/uv.lock, never the latest one - Runs the coverage studies (
coverage.yaml), about 10 min - Publishes all of them to PyPI with trusted publishing, only when every step before passed
If a coverage study fails, nothing reaches PyPI: delete the GitHub release
and the tag, fix the method on main, and tag again. To learn the answer
before you tag, run the studies on main by hand:
Watch it in the Actions tab or with:
gh run watch "$(gh run list --workflow release.yaml --limit 1 --json databaseId --jq '.[0].databaseId')"
Step 6: Verify the Release¶
Check that the version appears at https://pypi.org/project/commol/. Then install it in a clean environment and run a model:
uv venv /tmp/commol-check
uv pip install --python /tmp/commol-check/bin/python --refresh commol==X.Y.Z
/tmp/commol-check/bin/python -I check.py
# check.py
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Release check")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k1", value=0.3)
.add_parameter(id="k2", value=0.1)
.add_transition(id="a_to_b", source=["A"], target=["B"], rate="k1 * A * B / total")
.add_transition(id="b_to_c", source=["B"], target=["C"], rate="k2")
.build(
Aggregate(
bin_amounts={"A": 990, "B": 10, "C": 0},
),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=100)
print(f"Final C: {results['C'][-1]:.0f}")
Then close the release issue and the milestone of the release.
Pre-release Versions¶
Use a beta tag in the PEP 440 form, as for v0.1.0b3:
The set-version action turns X.Y.ZbN into the Cargo version X.Y.Z-beta.N.
Mark the GitHub release as a pre-release. The workflow still publishes it
to PyPI, but pip installs a pre-release only when it is asked for by version
or with --pre.
Version Support Policy¶
- Latest version: Fully supported with new features and bug fixes
- Older versions: No support, users encouraged to upgrade
PyPI Trusted Publishing Setup¶
PyPI accepts the uploads of the release workflow without an API token. The
trusted publisher of the commol project on PyPI is:
- Owner: MUNQU
- Repository: commol
- Workflow:
release.yaml - Environment:
pypi
If you rename the workflow file or the environment, update the trusted publisher on PyPI first, or the upload fails.
Release Checklist¶
The release issue repeats this list.
- [ ] Every issue of the milestone closed, or moved to another milestone with a comment that says why
- [ ] Version chosen
- [ ] Changelog dated on
main, with its opening paragraph - [ ] Annotated tag
vX.Y.Zcreated onmainand pushed - [ ] GitHub release published with the notes of
release_notes.py - [ ] Release to PyPI workflow passed
- [ ] Version visible on PyPI and tested in a clean environment
- [ ] Release issue and milestone closed
Troubleshooting¶
PyPI Upload Fails¶
- Check the logs of the Release to PyPI workflow
- Check that the trusted publisher on PyPI matches the workflow file and environment names
- Check that the version is not already on PyPI. If it is, publish a new version; you cannot upload the same version again
Wheel Build Fails¶
Nothing reaches PyPI when a build job fails, because the upload job needs all of them.
- Fix the problem on
main(a pull request through the merge queue) -
Delete the GitHub release and the tag:
-
Tag the new
maincommit and publish the release again