This document describes how to create a new release of Cable Modem Monitor.
Releases follow a PR-based workflow:
- Develop on feature branch → Run release script (prepares version bump)
- Create/update PR → CI validates, review changes
- Merge to main → Creates clean history
- Tag main → Triggers GitHub release workflow
- CI runs on push. Publishing runs on tag push. Don't skip the tag.
- Never manually edit version numbers. Use
scripts/release.py. - Never run
gh release createmanually. Let CI handle it. - Dogfood on local HA before stable releases (maintainer launches, not automated).
Run scripts/check_owned_deps.py and batch-update anything shown —
releases shouldn't ship with stale declared deps.
# Ensure you're on your feature branch with all changes committed
git checkout feature/your-branch
git status # Should be clean
# Run the release script
.venv/bin/python scripts/release.py 3.14.0The release script will:
- Validate version format and clean working directory
- Run tests and code quality checks (pytest, ruff, black, mypy)
- Verify translations are in sync
- Update version in manifest.json, const.py, pyproject.toml files, and test assertions
- Move
[Unreleased]to[X.Y.Z] - YYYY-MM-DDin CHANGELOG.md - Print changed files and suggested next steps
The script does not stage, commit, tag, push, or create releases — the developer handles all git operations.
# Review changed files, stage, and commit
git diff
git add <changed-files>
git commit -m "chore: bump version to 3.14.0"
# Push your feature branch
git push origin feature/your-branch
# Create PR if it doesn't exist, or it will update the existing one
gh pr create --title "feat: v3.14.0 - Your Release Title" --body "..."
# Or update existing PR via GitHub UI- Wait for CI to pass
- Review the changes
- Merge the PR (squash or merge commit)
After the PR is merged:
# Switch to main and pull the merged changes
git checkout main
git pull origin main
# Create the release tag on main
git tag -a v3.14.0 -m "Cable Modem Monitor v3.14.0
Key features:
- Feature 1
- Feature 2
- See CHANGELOG.md for details"
# Push the tag (triggers release workflow)
git push origin v3.14.0The tag push triggers .github/workflows/release.yml which:
- Creates a GitHub Release
- Attaches release notes from the tag message
Verify at: https://github.com/solentlabs/cable_modem_monitor/releases
Two tiers ship: beta and stable. Both publish to PyPI and create
GitHub Releases with a zip asset. Beta tags carry the GitHub
prerelease flag; stable tags don't.
| Tier | Tag pattern | PyPI | GitHub Release | HACS visibility |
|---|---|---|---|---|
| Beta | v*.*.*-beta.* |
Published | Pre-release + zip asset | Manual install via version picker |
| Stable | v*.*.* (no -) |
Published | Release + zip asset | Default (auto-update offer) |
Betas install manually via HACS → integration → Redownload → "Need a different version?" → Release. There is no auto-update path on betas — by design, each beta is a deliberate per-version install.
Where tags live:
All tags live on main, beta and stable alike. A release is cut on a
short-lived branch off current main (feature/vX.Y.0-beta.N for a
beta), opened as a PR, gated by required-status-checks + commitlint,
then merged and tagged on the merge commit. Delete the branch after.
require-pr-approvalcan't be self-satisfied (GitHub never counts the author's own approval), so the maintainer merges with admin bypass.- A branch off current
maincan't fall behind. If one drifts, rebase it ontomain, don't cherry-pick.
This replaced a long-lived beta-line branch that fell behind main and
let commits dodge the PR gates until they surfaced all at once (the
cause of the beta.11 commitlint rewrite and force-push).
{
"name": "Cable Modem Monitor",
"render_readme": true,
"homeassistant": "2024.12.0",
"hacs": "2.0.0",
"zip_release": true,
"filename": "cable_modem_monitor.zip",
"hide_default_branch": true
}zip_release: true and hide_default_branch: true are paired. With
both set, HACS distributes only the zip assets attached to GitHub
Releases and hides the default-branch option from the version
selector entirely. Users can't accidentally try to install from a
branch ref, which would 404 (see "Why branch tracking is unsupported"
below).
HACS reads hacs.json from the default branch, not from the
branch a user is tracking. When zip_release: true is set on the
default branch, HACS expects a zip asset on every install path
including branch refs. Branch refs have no GitHub Release object, so
HACS constructs a zip URL that returns 404 — by design, not a bug.
Confirmed by HACS maintainer in hacs/integration#3513 (closed 2024-02-16):
"This is correct. It should not be possible. When a repository uses release assets (
zip_release), only those assets are valid." — ludeeus, HACS maintainer"Ask the author if you need a development version. If that was possible before, that was a bug."
hide_default_branch: true is the maintainer-recommended companion:
HACS hides the branch dropdown from the version selector entirely so
users don't see the broken path.
For developers who do need to run from a branch (rare — typically
only the maintainer testing an unreleased commit), install outside
HACS: clone the repo, symlink custom_components/cable_modem_monitor
into the HA config dir, and pip install -e the Core and Catalog
packages.
References:
- HACS docs — Publish: Integration — overall integration publishing requirements
- HACS docs — Publish: General —
zip_releasefield documented as "only supported for integrations" - hacs/integration#3513 — definitive answer on
zip_release+ branch tracking incompatibility
zip_release: true means HACS expects a cable_modem_monitor.zip
asset on every release a user might roll back to. Older stable
releases (v3.13.x and earlier) lack this asset because the zip
wasn't built before the beta cut. Without a backfill of those older
releases, rollback to a pre-beta release fails with a HACS
asset-not-found error. Forward-only updates work fine.
We follow Semantic Versioning:
- MAJOR (X.0.0): Breaking changes
- MINOR (0.X.0): New features, backward compatible
- PATCH (0.0.X): Bug fixes, backward compatible
manifest.json pins both PyPI packages with exact == specifiers.
This is deliberate — HA only checks whether the installed version
satisfies the specifier; it never queries PyPI for newer versions
within a range. A >= pin would leave users permanently on the
version installed at first setup. The == pin forces HA to upgrade
when the manifest changes.
All three packages are released in lock-step. scripts/release.py
bumps all version files atomically. Independent versioning is not
supported — even a catalog-only change (new modem config) triggers a
full release, because the user needs a HACS update (new manifest pin)
to receive the new catalog version.
| Package | Delivery | Contents |
|---|---|---|
HACS zip (cable_modem_monitor.zip) |
GitHub release asset | HA adapter: config flow, coordinator, sensors, buttons, services, translations, icons |
Core (solentlabs-cable-modem-monitor-core) |
PyPI wheel | Auth, parsers, orchestration, loaders, protocol, MCP tools |
Catalog (solentlabs-cable-modem-monitor-catalog) |
PyPI wheel | modem.yaml, parser.yaml for each supported modem |
The HACS zip contains only runtime files — docs/ is excluded from
the zip build. Spec files stay in the repo for contributors but don't
ship to users.
- HACS downloads
cable_modem_monitor.zipfrom the GitHub release - HACS extracts the zip into
custom_components/cable_modem_monitor/ - HA reads
manifest.json→ seesrequirementspins - HA pip-installs Core and Catalog from PyPI (exact
==pins) - Integration loads
manifest.json declares loggers for both PyPI packages:
"loggers": [
"solentlabs.cable_modem_monitor_core",
"solentlabs.cable_modem_monitor_catalog"
]This tells HA to include Core and Catalog log output when a user
enables debug logging for the integration. Without this field, only
the custom_components.cable_modem_monitor logger is captured.
When the same fix needs to land on multiple in-flight feature branches (e.g., v3.11.0 and v3.12.0):
- Commit the fix to the base branch first (e.g., v3.11.0).
- Rebase the child branch onto the updated parent:
git checkout feature/v3.12.0 && git rebase feature/v3.11.0 - Force push the rebased branch:
git push --force-with-lease
Cherry-picking creates duplicate commits with different SHAs; when the branches eventually merge to main, you get duplicate history or merge conflicts. The exception is backporting to unrelated branches (e.g., a hotfix into an older release line) — cherry-pick is fine there.
- Preserves release branch history.
- Child branches rebase cleanly without "skipped commit" noise.
- Shows clear release boundaries in
git log.
Whether to open a beta-to-main merge PR after each beta tag turns
on contributor-onboarding pressure, not a blanket "always
merge" rule. The default GitHub view of the repo shows main;
contributors landing on the catalog README, contributor docs, or
intake tooling see whatever is on main. When the canonical view
is stale relative to current pipeline reality, contributors start
on outdated docs and friction accumulates.
Merge a given beta to main when: the beta contains contributor-facing doc or tool changes that should be visible at the canonical GitHub URL. Examples:
- Catalog README updates (new confirmed modems, chipset metadata, badge changes)
cable_modem_monitor_catalog_tools/workflow changes (intake/confirmation tooling, MCP tool additions)- Contributor-onboarding docs (CONTRIBUTING, INTAKE_PIPELINE, MODEM_INTAKE_WORKFLOW, AUTHORING guides)
- Dev tooling that contributors use (
maketargets, pre-commit hooks, local CI mirror commands)
Defer the merge when: the beta is mostly runtime-code changes, internal refactors, or test/spec updates with no user- or contributor-facing surface change. The merge can wait until the next beta with contributor-facing changes, or until stable cut.
HACS note: main being on a beta version string is acceptable
here. HACS users who haven't opted into the beta channel install
the latest stable tag, not the default branch — so a beta version
string on main doesn't break stable installs. Beta-channel users
opt in separately. Don't argue against the merge on HACS grounds.
Original driver: the v3.14 architecture moved to main early
because contributors were starting on the wrong branch — a major
operational headache. With v3.14 on main, that acute pressure is
reduced; later beta merges become more situational.
Creates one clean commit on main.
For urgent fixes to a released version:
# Create hotfix branch from the release tag
git checkout -b hotfix/3.14.1 v3.14.0
# Make fixes, commit, then follow normal release process
.venv/bin/python scripts/release.py 3.14.1
# Stage and commit version bump, then push
git push origin hotfix/3.14.1
# Create PR, merge, then tag main- All tests passing (
pytest tests/ -v) - Code quality checks passing (
ruff check,black --check,mypy) - CHANGELOG.md
[Unreleased]reconciled againstgit log <last-tag>..HEAD --oneline— every user-facing commit has an entry (release.pypromotes[Unreleased]verbatim; it does not generate entries) - Version numbers consistent (manifest.json, const.py)
- PR reviewed and merged to main
- Tag created on main (not feature branch)
- GitHub Release created automatically
- Verify HACS picks up new version
If you accidentally pushed a tag to the wrong branch:
# Delete remote tag
git push origin --delete v3.14.0
# Delete local tag
git tag -d v3.14.0
# Start fresh from the tagging stepThe require-status-checks repository ruleset matches check names as
plain strings. If a workflow job is renamed, the ruleset doesn't know —
it just never sees the old name reported and the check hangs as
"Expected" forever.
Fix: update the ruleset to use the new job name. The current required
checks live in ruleset ID 10547747. Update via:
gh api repos/solentlabs/cable_modem_monitor/rulesets/10547747 --method PUT --input <payload>where <payload> is the full ruleset JSON with the corrected context
strings. See CLAUDE.md § "Adding a new CI job" for the rule that
prevents this drift.
Ensure the tag follows the pattern v* (e.g., v3.14.0). Check .github/workflows/release.yml for the trigger configuration.
Last updated: 2026-04-02