Releases¶
Versioning is automated with python-semantic-release (PSR) driven by
Conventional Commits. The git tag is the source of truth; the only stamped
file is custom_components/climate_orchestrator/manifest.json
(version_variables) — what HA and HACS actually read. pyproject.toml
keeps a frozen 0.0.0 placeholder on purpose: uv.lock records the (virtual)
project's version, so stamping pyproject on every release would invalidate
the lockfile and fail CI's uv lock --check until someone re-ran uv lock.
flowchart LR
FB["push to feat/* or fix/*"] --> CI1["CI green"]
CI1 --> RC["prerelease vX.Y.Z-rc.N<br>(HACS: enable beta versions)"]
MM["merge to main"] --> CI2["CI green"]
CI2 --> REL["release workflow<br>(same-repo guard)"]
REL --> PSR["semantic-release<br>version bump · changelog · tag"]
PSR --> ZIP["zip + release asset<br>+ provenance attestation"]
ZIP --> HACS["HACS serves the release"]
Conventional commits drive the version¶
fix:→ patchfeat:→ minorfeat!:/BREAKING CHANGE:→ major- Non-conventional commits (e.g.
chore:,docs:) don't trigger a release.
Branch-based prereleases¶
Prereleases are branch-based (PSR's native model — it releases from the
branch it runs on). [tool.semantic_release.branches] marks main as stable
and any feat/* or fix/* branch as prerelease (prerelease_token = "rc"):
- Push to a
feat/*/fix/*branch (after CI is green) → anX.Y.Z-rc.Nprerelease. Testers enable Show beta versions on the integration in HACS to install it. - Merge that branch into
main→ the stableX.Y.Zrelease HACS serves by default.
Two consequences of feature-branch prereleases: PSR commits the version bump +
tag onto the feature branch (the [skip ci] in the commit message stops a
re-trigger loop), and the version is PEP 440 (rcN), so the branch name
can't appear in it. One-time bootstrap: the then-current commit was tagged
v0.11.0 so PSR continues from that baseline rather than recomputing from
zero.
The release workflow¶
A single workflow, release.yml, handles both stable and prerelease. It is
triggered by a successful CI run of a push event on main, feat/**,
or fix/** (workflow_run), so a red commit is never tagged — and a
same-repo pull-request CI run (whose jobs are skipped as duplicates of the
push run) can never trigger a release before the push run's tests finish. It
checks out whichever branch CI ran on and runs PSR, which:
- reads the branch config to decide stable vs prerelease,
- bumps
manifest.json, - updates
CHANGELOG.md(changelog update mode — appends new entries rather than regenerating the file), - runs
build_commandto producedist/climate_orchestrator.zip(the integration directory's contents, manifest at the zip root), - commits (
chore(release): … [skip ci]), tags, and - publishes the GitHub Release with the zip attached
(
[tool.semantic_release.publish]).
The workflow then re-downloads the published asset and verifies the two
things HACS depends on — manifest.json at the zip root, with a version
matching the tag — so a packaging regression fails the release loudly instead
of surfacing as a broken install.
Provenance and HACS consumption¶
actions/attest-build-provenancerecords verifiable build provenance for the zip; verify withgh attestation verify climate_orchestrator.zip -R <repo>. The Sigstore bundle is also attached to the release asclimate_orchestrator.zip.intoto.jsonl, so the provenance is visible to scanners (OpenSSF Scorecard's Signed-Releases check) and verifiable offline (cosign verify-blob --bundle).- HACS installs that exact asset:
hacs.jsonsetszip_releaseplusfilename, so HACS downloads the publishedclimate_orchestrator.zipfrom the GitHub Release rather than cloning the repository tree.
Supply-chain hardening around the release job (SHA-pinned actions, zizmor, the
head_repository == github.repository guard) is covered in
Tooling.
Next: Contributing — dev setup and project conventions.