Skip to main content

CI/CD setup — module and customer releases

This guide configures GitHub Actions to publish Home Assistant modules and customer bundles when you push a version tag.

Architecture

Two workflows with separated credentials:

  1. Build release (.github/workflows/build-release.yml) — runs on the tagged commit, no production secrets. Produces a .tar.gz, manifest.json, and release-info.json as a GitHub Actions artifact.
  2. Publish release (.github/workflows/publish-release.yml) — triggered after a successful build. Downloads the build artifact, runs .github/scripts/publish-release.sh (bash + jq + AWS CLI + curl only — no pnpm install). Uploads to R2 and registers the version via scoped publisher API tokens. Workflow YAML and script are loaded from the default branch.

Prerequisites

  • GitHub repository with Actions enabled
  • API deployed and reachable from GitHub Actions (API_BASE_URL)
  • Cloudflare R2 bucket for production artifacts
  • API database migrated (includes publisher_tokens table)
  • Publisher tokens minted (see below)

GitHub Actions configuration

Repository variables

VariablePurpose
API_BASE_URLPublic API base URL (e.g. https://api.example.com)

Secrets (publish workflow only)

SecretPurpose
PUBLISHER_MODULE_TOKENBearer token with scope module:publish
PUBLISHER_CUSTOMER_TOKENBearer token with scope customer:publish
R2_ACCOUNT_IDCloudflare account ID
R2_ACCESS_KEY_IDR2 API token access key (scoped to modules/ and customers/ prefixes)
R2_SECRET_ACCESS_KEYR2 API token secret
R2_BUCKETBucket name

The build workflow does not use DATABASE_URL, R2 keys, or publisher tokens.

Environment

Create a GitHub environment release-publish (optional approval gate for production publishes).

Tag naming

KindChannelTag formatExample
modulestablemodule/<name>-v<semver>module/irrigation-v2.0.1
modulebetamodule/<name>-v<semver>-betamodule/irrigation-v2.0.1-beta
modulealphamodule/<name>-v<semver>-alphamodule/test-v0.1.0-alpha
customerstablecustomer/<key>-v<semver>customer/pisarna-v1.2.0
customerbetacustomer/<key>-v<semver>-betacustomer/pisarna-v1.2.0-beta
git tag module/test-v1.0.0-alpha
git push origin module/test-v1.0.0-alpha

Mint publisher tokens

Run once per scope (stores only the hash in Postgres):

cd apps/api
pnpm publisher:mint-token --name "github-actions-module" --scope module:publish
pnpm publisher:mint-token --name "github-actions-customer" --scope customer:publish

Copy each printed token into the matching GitHub secret. Tokens cannot be retrieved again.

Local development

Local releases still use ARTIFACT_STORAGE_MODE=local and direct DB scripts (no publisher API):

pnpm module:release -- test --bump patch
pnpm customer:release -- pisarna --bump patch
pnpm registry:sync-local

First-time checklist

  1. Apply DB migration (publisher_tokens table).
  2. Mint and store publisher tokens in GitHub secrets.
  3. Set API_BASE_URL repository variable.
  4. Push a test tag (e.g. module/test-v0.0.1-alpha) and confirm:
    • Build workflow succeeds and uploads artifact
    • Publish workflow uploads to R2 and creates a module_versions / customer_versions row with publishStatus = published
  5. Verify device check-in resolves versions from the DB catalog.

Rollback

Published artifacts are immutable. Roll back by tightening device versionConstraint / customerVersionConstraint to an older published semver.

Troubleshooting

IssueCheck
Build fails on tag parseTag must match module/<name>-vX.Y.Z or customer/<key>-vX.Y.Z
Publish 401Token revoked, wrong secret, or scope mismatch
Publish 400 checksumArtifact tampered or EXPECTED_HEAD_SHA mismatch
Artifact not in R2R2 credentials or prefix scope on publish job
R2 key mismatch.github/scripts/publish-release.sh out of sync with packages/artifact-storage/src/paths.ts