Release process & strategy¶
Audience: maintainers. This page documents how a release is cut today, what CI enforces and the known risks of the setup. It is the single place to start when preparing a release; the design rationale behind individual decisions stays in Development, and planned improvements are tracked in the Roadmap.
What a release ships¶
| Artifact | Source | Consumer |
|---|---|---|
| Integration (HACS/zip) | custom_components/nspanel_haui/ at the tag |
Home Assistant users |
nspanel_haui.bin |
factory flash image compiled by CI from esphome/nspanel_haui_universal.yaml |
manual/serial flashing; same binary for EU and US panels |
nspanel_haui_ota.bin |
OTA app image compiled by the same CI build | installed by the HAUI Firmware update entity |
firmware-manifest.json |
generated by CI for v* tag pushes |
ESPHome web-tools update schema; the firmware polls it for OTA metadata |
nspanel_haui.tft |
nextion/nspanel_haui.HMI, compiled in Nextion Editor |
landscape panels |
nspanel_haui_portrait.tft |
nextion/nspanel_haui_portrait.HMI, generated from the landscape HMI by scripts/gen_portrait |
display_model: us panels |
The five attached asset names — nspanel_haui.bin, nspanel_haui_ota.bin,
firmware-manifest.json, nspanel_haui.tft and nspanel_haui_portrait.tft — are
a frozen contract. haui/device_config.py maps display_model to the TFT
filenames; haui/controller/update.py resolves the matching TFT from the GitHub
release whose tag matches the firmware's yaml_version (the v prefix is ignored
by _try_parse_version). The firmware polls firmware-manifest.json for the OTA
image and its checksum. Renaming an asset breaks automatic updates for
already-flashed panels, so a rename needs a migration story first. Adding a
variant (for example the high-speed TFT planned for v0.5) is safe as long as the
existing names stay unchanged; the update controller must map each device to the
right variant.
Versioning and tag policy¶
Semantic Versioning. custom_components/nspanel_haui/manifest.json is the
source of truth; haui/version.py reads it at runtime. Four places must agree,
and the combined bump script updates all of them:
custom_components/nspanel_haui/manifest.json— source of truth.pyproject.toml— packaging mirror.esphome/nspanel_haui/version.yaml— firmware compatibility version.- The Nextion HMI files (
nextion/nspanel_haui.HMIandnextion/nspanel_haui_portrait.HMI) — globalsystem.tftVersion, checked by the release gate againstmanifest.json.
Also update the CHANGELOG.md heading and release date by hand.
scripts/bump_version.py --check is the dry run CI uses to fail on drift, and
tests/test_bump_version.py covers the script.
Shipping unchanged TFTs: a release may keep the previous TFT binaries (a
hotfix whose display file did not change). Bump with --text-only so the HMI
system.tftVersion — and with it the compiled TFTs — stays at the previous
version, and set the repository variable TFT_VERSION_PIN to that version
(e.g. 0.4.3). CI (tests.yml) and the release gate then tolerate the
HMI/integration version mismatch and report it as a warning
(verify_nextion.py --allow-tft-version-mismatch) instead of failing. Delete
the variable again when a release bumps the TFTs. Panels keep the pinned TFT:
the display reports the old version and the update controller does not
auto-reinstall it; an automatic firmware update to such a release runs one TFT
install that cannot confirm and leaves the HAUI Display entity asking for a
manual retry until a release ships a matching newer TFT.Tags: use vX.Y.Z. A v* tag push starts release.yml. Manual dispatch
re-runs the release workflow for the selected ref and generates
firmware-manifest.json only when that ref is a v* tag. Older history is tagged without the prefix (0.3.2, 0.3.1);
those tags never ran the automated workflow. Do not "fix" old tags — the
device-side lookup tolerates both forms.
Never push a tag to test firmware. Every v* tag becomes a regular
release, GitHub marks it latest, and every panel's HAUI Firmware entity
reads releases/latest/download/firmware-manifest.json — so all panels with
firmware_auto_update on install it. Use a
local test release instead.
Test firmware updates with a local release¶
scripts/dev_release.py turns a local build into the two files a release
carries for over-the-air updates, written to a folder Home Assistant serves
(/config/www/… is served as /local/…): nspanel_haui_ota.bin and
firmware-manifest.json (the schema the release workflow writes, with the
image URL and md5).
-
Build the test panel's firmware with two substitutions, either in its device YAML or on the command line:
esphome -s yaml_version 0.4.3-dev1 \ -s firmware_manifest_url http://homeassistant.local:8123/local/nspanel/firmware-manifest.json \ compile esphome/nspanel_haui_universal.yamlyaml_versionmust equal the manifest version (ESPHome offers an update whenever they differ, so a mismatch reinstalls forever), and each test release needs a newer one (-dev1,-dev2, …) because Home Assistant only turns the update entity on for a newer version. The manifest URL keeps the installed test build reading the local manifest; one that reads the GitHub manifest sees the release as an update, and the hub reinstalls the release over it. -
Write the test release:
python scripts/dev_release.py \ --image esphome/.esphome/build/nspanel-haui/build/firmware.ota.bin \ --version 0.4.3-dev1 \ --base-url http://homeassistant.local:8123/local/nspanel \ --out /path/to/homeassistant/config/www/nspanelThe script refuses a factory image, an image larger than the app partition, and an image whose embedded
yaml_versionor manifest URL does not match. -
Flash the first test build once from the ESPHome dashboard (or with
nspanel_haui.update_firmwareand the imageurl). From then on, each newer test release shows up on the panel's HAUI Firmware entity, and the hub installs it automatically or with Device Manager → Install firmware.
The hub treats yaml_version as the TFT version the firmware needs, so a test
version newer than the installed TFT makes it look for a GitHub release of that
version and report "No GitHub release". On the test panel, turn off
auto_install, or install a TFT once with nspanel_haui.update_tft and url
(it then stays until Home Assistant restarts). To leave the test, flash a build
without the two substitutions.
Branches and cadence¶
master is release-only. Normal work never lands there directly: it arrives
through a merge from dev. A release hotfix may branch from master when a
patch cannot wait for the next dev cycle. Every tag is cut from a commit whose
CI was green on its source branch.
master— release branch. Receives merges fromdev; release hotfixes branch frommasteronly when a patch cannot wait. Every tagvX.Y.Zis cut from a green commit here. GitHub Pages deploys from this branch and HACS users install from its releases. Protected by the "Protect master (release-only)" ruleset: no direct pushes, no force-pushes, no deletion, and a merge needs a PR with the always-running CI checks green.dev— integration branch, the target for all day-to-day work, and the repository's default branch, so new PRs base on it automatically. CI runs here on every push and PR.- Feature branches — branch off
dev, open the PR againstdev. - Hotfix branches — branch off
masteronly when a released version needs a patch that cannot wait for the nextdevcycle; PR intomaster, tag, then mergemasterback intodev.
The cycle in one line: feature → PR into dev → release prep on dev (version
bump, CHANGELOG, release notes, TFTs) → PR dev → master → tag on master →
release.yml publishes → merge master back into dev → open the next
CHANGELOG section.
Enforcement: the master ruleset blocks direct pushes, force-pushes and branch
deletion, and requires a pull request whose always-running checks are green
before merge. It has no bypass actors, so the release merge from dev goes
through a PR like everything else. Tags are unaffected — release.yml still
fires on a v* tag pushed to master. To add an escape hatch, add a bypass
actor to the ruleset rather than weakening the rules.
dev itself is not protected: it takes direct pushes and PRs alike. Add a
ruleset there if the branch starts collecting unreviewed changes.
No fixed calendar. Ship when user-visible changes accumulate or a fix cannot
wait. The 0.4.x line is a hard gate: it finishes the binary and TFT install
and proves the base and core functions before any v0.5 work starts. Patch
releases in that line close regressions and gate failures; minor releases
(0.5.0) ship features and breaking changes. Any breaking change must ship
with a docs/upgrading.md entry and a CHANGELOG "Breaking changes" section —
no alias is kept as a fallback, so the upgrade guide is the only migration path.
CI gates and local checks¶
| Workflow | Triggers | What it enforces |
|---|---|---|
tests.yml |
pushes/PRs to master, dev |
mkdocs build --strict; icon cheatsheet, translation and version-source freshness; Nextion asset gate; pytest tests/; ruff check custom_components/ scripts/; mypy; node --test 'tests/frontend/*.test.mjs'; esphome config esphome/nspanel_haui_universal.yaml |
docs.yml |
docs/mkdocs changes on pushes/PRs to master, dev; manual dispatch |
mkdocs build --strict; deploys Pages from master only |
validate.yml |
pushes/PRs to master, dev; nightly; manual dispatch |
hassfest and HACS validation |
release.yml |
v* tags; manual dispatch |
tag vs manifest.json version; Nextion release gate; ESPHome config + compile; firmware manifest (tag refs only); prepared release notes; asset upload |
Gap to keep in mind: a tag push does not re-run tests.yml — only
release.yml runs. The tag build trusts that the tagged commit was already green
on a branch. Cut tags only from a passing master commit, and re-check
git status for uncommitted work first.
Run the same gates locally¶
Every command below is what CI runs. Run them before opening a PR or cutting a tag; release prep is not finished until they pass on the final commit.
# Documentation and generated assets
mkdocs build --strict
python3 scripts/gen_fonts/gen_cheatsheet.py && git diff --exit-code -- docs/cheatsheet.html
python3 scripts/translations/extract_translations.py && git diff --exit-code -- custom_components/nspanel_haui/haui/locale scripts/translations/translate.json
# Version and Nextion gates
VERSION=$(python3 -c "import json; print(json.load(open('custom_components/nspanel_haui/manifest.json'))['version'])")
python3 scripts/bump_version.py "$VERSION" --check
python3 scripts/verify_nextion/verify_nextion.py --require-portrait
python3 scripts/gen_portrait/gen_portrait.py --check
# Backend, lint and type checks
pytest tests/
ruff check custom_components/ scripts/
mypy
# Frontend tests
node --test 'tests/frontend/*.test.mjs'
# Firmware config (install requirements-esphome.txt first)
esphome config esphome/nspanel_haui_universal.yaml
python3 may be python in your environment; on this repo's dev venv use
.venv/bin/python (or activate .venv first). The ESPHome check needs the
pinned toolchain from requirements-esphome.txt.
Release runbook¶
1. Pre-flight (a few days before)¶
- Confirm
devandmasterare green (tests, frontend, docs, hassfest/HACS). - Confirm the device passlist in the Roadmap's v0.4.x section passed on real EU and US hardware. The 0.4.x line is a hard gate: v0.5 work does not start until every row is green.
- Freeze features; only fixes land from here.
- Decide the version number and check for pending breaking changes.
2. Version bump¶
Run the bump script — it writes the manifest, pyproject.toml, ESPHome
version.yaml and both HMI system.tftVersion values in one pass:
python3 scripts/bump_version.py 0.5.0 # bump every source
python3 scripts/bump_version.py 0.5.0 --check # dry run, writes nothing
The script edits the HMI in place; that is only legal for a same-length version
(0.4.2 → 0.4.3). A shorter or longer version resizes the attribute record and
the script refuses it — use the Nextion Editor for that bump. The script does not
touch CHANGELOG.md, so add or update its section and date by hand. Re-save the
HMI and recompile both TFTs (see next step) — the gate fails on a stale
tftVersion or a stale TFT.
scripts/bump_tft_version.py bumps only the HMI tftVersion text; the combined
script above calls it. Both are importable and covered by
tests/test_bump_version.py.
3. Nextion assets (human, GUI-only)¶
- Open
nextion/nspanel_haui.HMIin Nextion Editor; setsystem.tftVersion. - Make or verify the release's display changes; compile
File → TFT file output→nextion/nspanel_haui.tft. - Re-run
python3 scripts/gen_portrait/gen_portrait.pyso the portrait HMI picks up landscape edits, then compilenextion/nspanel_haui_portrait.tft. The recompile is mandatory after anyPORTRAIT_ATTRS/HMI change — the committed HMI alone does not reach devices.gen_portrait.py --checkverifies attribute values as well as widget coverage, so an attribute-only change fails the gate until the HMI is regenerated. - The Nextion gates are part of Run the same gates locally.
Also run
python3 scripts/gen_preview/gen_preview.py --check-missingto confirm every panel type has a preview renderer.
4. Docs and firmware¶
- Update user-facing docs for behaviour changes (install, services, display models).
- Breaking changes: write the
docs/upgrading.mdentry and the CHANGELOG "Breaking changes" section during release prep ondev. - Verify the universal firmware still validates:
esphome config esphome/nspanel_haui_universal.yaml. - The universal binary serves any number of panels: the MAC suffix gives each device a unique name and access point, so no per-device build is needed. The baked API/OTA/web credentials remain shared — keep that caveat documented.
5. Release notes and tagging¶
Prepare the GitHub release body in .github/release-notes/<tag>.md (for example
.github/release-notes/v0.4.2.md) during release prep on dev, before the merge
to master. Because master takes no direct commits, the merge carries the file
to the tagged commit. release.yml publishes it as the release body and falls
back to the changelog pointer when no file exists. Keep the changelog as the
canonical notes and summarise them here: highlights, anything to do before
updating, links and the issues addressed with their contributors. Credit
external contributors by their GitHub handle and keep individual maintainer
names out of the release body.
After the merge lands on master, tag and publish:
The tag push starts release.yml, which checks that the tag matches
manifest.json, runs the Nextion gate, validates and compiles the universal
firmware, and attaches the two firmware images, both TFTs and
firmware-manifest.json to the GitHub release. Watch the run; do not announce
before it is green.
6. Post-release¶
- Verify the release page carries the two firmware images (
nspanel_haui.bin,nspanel_haui_ota.bin), both TFTs andfirmware-manifest.jsonwith the expected names, and that the body shows the prepared release notes. - Flash one panel from the published
.binand confirm TFT auto-update, as a smoke test of the exact published artifacts. - Announce; link the changelog and the update warning.
- Merge
masterback intodev, then open the nextCHANGELOG.mdsection.
7. Rollback¶
If the published assets are broken: mark the GitHub release as a draft (or
delete it) and publish a patch release. The update controller only installs the
TFT for the firmware's yaml_version, so a corrected tag is enough; never
re-point an existing tag at different bytes.
Guardrails and known gaps¶
These are the places the process can still go wrong; check them during release prep.
Version and release notes
- Four version places — manifest,
pyproject.toml, ESPHomeversion.yamland both HMI files. Drift is the most likely release failure;scripts/bump_version.pyupdates them in one pass and--checkcatches drift in CI. - Tag pushes skip
tests.yml— onlyrelease.ymlruns on a tag. Tag only a commit that was green on its source branch. - Release notes are a separate file from the changelog —
.github/release-notes/<tag>.mdbecomes the release body when present;release.ymlfalls back to the changelog pointer. Keep the changelog canonical and the prepared notes in sync.
Nextion (GUI-only)
- The Nextion Editor is GUI-only — TFT compilation cannot be scripted. The version bump is scripted only for same-length versions, and the release gate can only verify the result.
- The portrait TFT derives from the landscape HMI — regenerate it after every landscape edit. Forgetting the step fails the gate or ships a stale portrait.
- Asset names are frozen — renaming one silently breaks auto-update for flashed panels. Add variants without renaming existing assets.
Distribution
- ~120 MB of Nextion binaries live in git — HACS downloads include them; repo and zip size grow with every TFT rebuild. Git LFS or release-only storage is planned.
- The universal binary bakes shared credentials — MAC suffixes give each device a unique name and access point, but the API/OTA/web credentials are shared. Credential isolation is not solved.
- ESPHome is pinned to a minimum version —
base.yamldefines the floor andrequirements-esphome.txtpins the build toolchain. Bump both together.
Planned improvements to this setup are tracked in the Roadmap.