Releases & deploys
This is the end-to-end story of how a change to a module, the dashboard,
or a customer (e.g. pisarna_ha) reaches a device. The engine is
@signapps/ha-tooling; the trigger is a git
tag; the destination is Cloudflare R2 + the API registry.
Tag conventions
Every release is identified by a git tag. The grammar (validated in
.github/workflows/build-release.yml) is:
| Kind | Stable | Pre-release |
|---|---|---|
| Module | module/<name>-v<semver> | module/<name>-v<semver>-beta (or -alpha) |
| Customer | customer/<key>-v<semver> | customer/<key>-v<semver>-beta |
| Dashboard | dashboard/v<semver> | dashboard/v<semver>-beta |
Examples: module/irrigation-v2.0.1, customer/pisarna_ha-v0.2.11,
dashboard/v0.0.44. Names match ^[a-z0-9][a-z0-9_-]*$; the channel is derived
from the suffix (stable when absent).
The two-stage CI pipeline (modules & customers)
Releases are split into a build stage (no secrets) and a publish stage (secrets), so code in a tagged commit can never read credentials.
- Build (
build-release.yml) — triggered by the tag push. It checks out the tagged commit, parses the tag, runsha-tooling build-module/build-customerto producemodule.tar.gz/customer.tar.gz+manifest.jsonrelease-info.json, and uploads them as a workflow artifact. No secrets are available to this job.
- Publish (
publish-release.yml) — triggered by the build's successfulworkflow_run. It loads the publish script from the default branch (not the tagged commit), downloads the artifact, uploads the tarball + manifest to R2 under the canonical key, and calls the API's publish endpoint with a scoped publisher token. The API re-hashes the tarball and re-validates the manifest before recording the version.
Where artifacts land
modules/<name>/<version[-channel]>/module.tar.gz
customers/<key>/<version[-channel]>/customer.tar.gz
dashboards/v<version[-channel]>/dashboard.tar.gz
Registry & devices
The publish endpoint upserts a ModuleVersion / CustomerVersion row
(publishStatus = published, with gitCommit, artifactPath, checksumSha256).
Devices query the API on check-in,
discover the new version, and pull the tarball from R2.
Dashboard releases
The dashboard uses the same shape with its own workflows:
build-dashboard.yml— triggered bydashboard/v*, validates the tag, builds the SPA withDASHBOARD_RELEASE_TAG, and uploads the artifact.publish-dashboard.yml— publishes to R2 (dashboards/v<version>/dashboard.tar.gz) and callsPOST /api/dashboards/versions/publish.
The local path (no CI)
For local development you can release straight to the local artifact store
(ARTIFACT_STORAGE_MODE=local) without tags or CI:
pnpm customer:release -- pisarna_ha --bump patch
pnpm module:release -- irrigation --bump minor
This bumps the version, builds the tarball into .local-artifacts/, and upserts
the registry. The registry helpers (pnpm registry:sync-local,
registry:publish-customer, …) reconcile local artifacts with the DB.
Releasing a customer today (step by step)
Using pisarna_ha as the example, the current manual flow:
- Edit the customer's authoring tree under
home-assistant/customers/pisarna_ha/and commit. - Push the commit to
main. - Decide the next version (look at existing
customer/pisarna_ha-v*tags) and create + push the tag:git tag customer/pisarna_ha-v0.2.11git push origin customer/pisarna_ha-v0.2.11 - GitHub Actions builds, publishes to R2, and records the version. The device picks it up on its next check-in.
The friction here is steps 3–4: you have to find the latest version yourself, hand-write the tag, and remember to commit/push first.
Guided deploys: pnpm deploy:*
Three interactive wrappers remove the manual bookkeeping in steps 3–4 above. They are a thin front-end over the exact same git-tag system — the only thing each one ultimately does is push a tag in the canonical grammar; GitHub Actions does the rest.
pnpm deploy:customer pisarna_ha # → customer/pisarna_ha-v<next>
pnpm deploy:module irrigation # → module/irrigation-v<next>
pnpm deploy:dashboard # → dashboard/v<next>
Source: scripts/deploy/
(lib.mjs + one entry script per kind). They require an interactive terminal.
What they do
Step by step:
-
Scoped change check — each command only looks at the paths that actually affect what's being deployed:
Command Watched paths deploy:customer <key>home-assistant/customers/<key>/deploy:module <name>home-assistant/modules/<name>/deploy:dashboardapps/dashboard/react/+packages/{ui,dashboard,lib,icons,types}/A change elsewhere in the repo won't prompt you — only relevant changes do.
-
Uncommitted changes — if any of those paths are dirty, it lists them and offers to commit (prompting for a message). You can also decline and continue.
-
Unpushed commits — if commits touching those paths aren't on the remote, it warns (a tag on an unpushed commit won't build in Actions) and offers: push now, I'll push myself — continue, or abort.
-
Preflight (dashboard only) — runs a local test build mirroring CI (
pnpm --filter=@signapps/dashboard-react... run build) before tagging, so a broken build is caught here instead of failing the Actions run. -
Version from git tags — reads existing tags for the kind/name, finds the highest semver, and offers
patch/minor/major(orcustom), showing the resulting version. It refuses a version that's already tagged. -
Confirm + tag — shows
current → proposed, then creates and pushes the tag. From there it's the same two-stage build/publish pipeline.
Transcript & output
The prompts keep a visible history of every selection (◆ Choose version bump › patch …), so the terminal reads like a record of the run. Heavy command
output (the test build, git push, etc.) is hidden — each collapses to a
single status line (✓ Test build passed, ✓ Committed "…", ✓ Tag pushed …).
On failure, the captured output is printed so you can debug.