Release process
LLenergyMeasure uses semantic versioning in the 0.x range while the project
is pre-1.0. This page documents the versioning scheme, the two files that must
stay in sync, and the step-by-step release procedure.
Versioning scheme
- Pre-1.0: minor version bumps (
0.1.0,0.2.0, ...,0.N.0) mark significant capability milestones. Patch versions (0.N.1) are reserved for hotfixes. 1.0.0is reserved for the production-ready release. It is not tied to a calendar date and will be cut when the project meets the production-readiness bar.- Version management is manual - no automated bump tooling is used.
Feature work lands on main at the current version. Version numbers do not
change on feature PRs - only on release commits.
Version sources
Two files hold the version and must stay in sync:
| File | Field | Example |
|---|---|---|
pyproject.toml | [project] version = "..." | version = "0.6.0" |
src/llenergymeasure/_version.py | __version__ = "..." | __version__ = "0.6.0" |
A mismatch between these two files is a bug. The build system reads
pyproject.toml; the runtime API (llem --version, llenergymeasure.__version__)
reads __version__, which is defined in src/llenergymeasure/_version.py and
re-exported from __init__.py.
Release steps
-
Bump the version in
pyproject.tomlUpdate the
versionfield under[project]:[project]name = "llenergymeasure"version = "0.6.0" -
Bump the version in
_version.pyUpdate
__version__insrc/llenergymeasure/_version.py:__version__ = "0.6.0"Both files must show the same version string.
-
Assemble
CHANGELOG.mdfrom fragmentsPer-PR entries accumulate as fragment files under
changelog.d/during the cycle (see development). Fold them intoCHANGELOG.mdwith towncrier, passing the tag as the version:uv run towncrier build --version v0.6.0This writes a new
## [v0.6.0] - <date>section at the changelog marker and deletes the consumed fragments. Review the result, and update the[Unreleased]format-break callout at the top if the break it describes has now shipped. -
Commit
Commit the changed files (the two version files, the assembled
CHANGELOG.md, and the removedchangelog.d/fragments) with the message:chore: release 0.6.0This commit goes directly to
mainvia the normal PR flow. -
Create and push the git tag
git tag v0.6.0git push origin v0.6.0The tag must use the
vprefix and match the version string exactly. Pushing the tag triggers the CI publication workflow. -
CI publishes to PyPI
The
release.ymlworkflow (.github/workflows/release.yml) triggers onpush: tags: ["v*"]. It runs lint, type-check, tests, and version validation, then thereleasejob builds the sdist and wheel withuv buildand thepublish-pypijob uploads them to PyPI. Publishing uses OIDC trusted publishing: the runner mints a short-lived identity token that PyPI verifies against a configured trusted publisher, so no API token or secret is stored in the repository. No manual upload is required.Trusted publishing requires a one-time setup on pypi.org: the project's trusted publisher must name this repository, the
release.ymlworkflow, and thepypienvironment. Until that publisher is configured, thepublish-pypijob fails at the publish step (the build and GitHub Release still succeed).
Verifying a release
After CI completes:
pip install --upgrade llenergymeasure
llem --version # should show the new version
Check the GitHub Releases page - the tag should appear there automatically once the workflow completes.
Pre-1.0 vs post-1.0 policy
The current 0.x scheme treats every release as potentially breaking. The
stability contract documented in src/llenergymeasure/__init__.py applies:
names in __all__ follow semver, internal names may change without notice.
When the project reaches 1.0.0, the guarantee tightens: breaking changes to
public API require a major version bump. Until then, 0.x minor bumps may
include breaking changes with a one-release deprecation window where practical.
See also
- Contributing: development - local dev setup
- Reference: CLI - verifying installed version with
llem --version