Skip to main content

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:

KindStablePre-release
Modulemodule/<name>-v<semver>module/<name>-v<semver>-beta (or -alpha)
Customercustomer/<key>-v<semver>customer/<key>-v<semver>-beta
Dashboarddashboard/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.

  1. Build (build-release.yml) — triggered by the tag push. It checks out the tagged commit, parses the tag, runs ha-tooling build-module / build-customer to produce module.tar.gz/customer.tar.gz + manifest.json
    • release-info.json, and uploads them as a workflow artifact. No secrets are available to this job.
  2. Publish (publish-release.yml) — triggered by the build's successful workflow_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 by dashboard/v*, validates the tag, builds the SPA with DASHBOARD_RELEASE_TAG, and uploads the artifact.
  • publish-dashboard.yml — publishes to R2 (dashboards/v<version>/dashboard.tar.gz) and calls POST /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:

  1. Edit the customer's authoring tree under home-assistant/customers/pisarna_ha/ and commit.
  2. Push the commit to main.
  3. Decide the next version (look at existing customer/pisarna_ha-v* tags) and create + push the tag:
    git tag customer/pisarna_ha-v0.2.11
    git push origin customer/pisarna_ha-v0.2.11
  4. 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:

  1. Scoped change check — each command only looks at the paths that actually affect what's being deployed:

    CommandWatched 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.

  2. 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.

  3. 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.

  4. 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.

  5. Version from git tags — reads existing tags for the kind/name, finds the highest semver, and offers patch / minor / major (or custom), showing the resulting version. It refuses a version that's already tagged.

  6. 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.