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:
- Build release (
.github/workflows/build-release.yml) — runs on the tagged commit, no production secrets. Produces a.tar.gz,manifest.json, andrelease-info.jsonas a GitHub Actions artifact. - 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 +curlonly — nopnpm 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_tokenstable) - Publisher tokens minted (see below)
GitHub Actions configuration
Repository variables
| Variable | Purpose |
|---|---|
API_BASE_URL | Public API base URL (e.g. https://api.example.com) |
Secrets (publish workflow only)
| Secret | Purpose |
|---|---|
PUBLISHER_MODULE_TOKEN | Bearer token with scope module:publish |
PUBLISHER_CUSTOMER_TOKEN | Bearer token with scope customer:publish |
R2_ACCOUNT_ID | Cloudflare account ID |
R2_ACCESS_KEY_ID | R2 API token access key (scoped to modules/ and customers/ prefixes) |
R2_SECRET_ACCESS_KEY | R2 API token secret |
R2_BUCKET | Bucket 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
| Kind | Channel | Tag format | Example |
|---|---|---|---|
| module | stable | module/<name>-v<semver> | module/irrigation-v2.0.1 |
| module | beta | module/<name>-v<semver>-beta | module/irrigation-v2.0.1-beta |
| module | alpha | module/<name>-v<semver>-alpha | module/test-v0.1.0-alpha |
| customer | stable | customer/<key>-v<semver> | customer/pisarna-v1.2.0 |
| customer | beta | customer/<key>-v<semver>-beta | customer/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
- Apply DB migration (
publisher_tokenstable). - Mint and store publisher tokens in GitHub secrets.
- Set
API_BASE_URLrepository variable. - 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_versionsrow withpublishStatus = published
- 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
| Issue | Check |
|---|---|
| Build fails on tag parse | Tag must match module/<name>-vX.Y.Z or customer/<key>-vX.Y.Z |
| Publish 401 | Token revoked, wrong secret, or scope mismatch |
| Publish 400 checksum | Artifact tampered or EXPECTED_HEAD_SHA mismatch |
| Artifact not in R2 | R2 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 |