21 KiB
name, description
| name | description |
|---|---|
| releasing-healthchat | Cut a versioned HealthChat release of element-web by tagging release/healthchat. Use when the user asks to release, publish, tag, or cut a new HealthChat version, or asks about the release/deployment process for HealthChat element-web. |
Releasing HealthChat (element-web)
Releases are driven by git tags of the form healthchat-v<semver> pushed to
origin. The tag triggers two workflows in parallel:
-
.github/workflows/build-healthchat.yaml(web) — builds the HealthChat Docker image, publishes it to ghcr.io underhealthchat-web:<version>,:latest, and:<sha>, and creates a GitHub Release whose body is the matching section ofHEALTHCHAT_CHANGES.md.:latestis the tag production deploys from. It only moves for a cleanX.Y.Ztag push — never for-rc1/-hotfix1/-dryrun1tags, and never for a manualworkflow_dispatchrun. Those builds still publish:<version>and:<sha>, so you can deploy them deliberately by overriding the image ref (see "Deploying the release"), but they can never become production by accident. -
.github/workflows/build-healthchat-desktop.yaml(desktop) — builds the Electron app for macOS (universal, signed+notarised) and Windows (x64/arm64, signed) and publishes to the same GitHub Release as the web workflow (both create-if-missing, so they race safely). (Linux is intentionally not built — the upstream Linux reusable pulls a build-env Docker image keyed by commit SHA that only exists in the upstream element-hq registry, so it cannot succeed in this fork.) Thereleasejob stages the Squirrel update feed and public installers and uploads them to the versioned archive athttps://packages.healthchat.ch/desktop-releases/<version>/— but it does not publish them to the live feed. Nothing binary is attached to the GitHub Release (the compressed feed outgrew GitHub's 2 GiB per-asset cap at 1.6.0); the Release notes get a "## Downloads" section linking the installers' public URLs on packages.healthchat.ch, which resolve once the version is deployed. See "Deploying the desktop release" below. -
.github/workflows/deploy-healthchat-desktop.yaml(desktop deploy) — manual dispatch only. Fetches the staged archive for the requested version and mirrors it to the live feed athttps://packages.healthchat.ch/desktop/over FTPS (the private GitHub Release can serve neither end users nor the auto-updater). The pre-1.6.0 feed athttps://healthchat.ch/packages/desktop/is retired and frozen at 1.5.0 — installs that old must reinstall once from the new download URL. This is what makes installed apps self-update; seeapps/desktop/element.io/healthchat/READMEfor the feed layout and the two caveats (first install is manual; Windows auto-update needs the Setup.exe path, not the .msi).
Publish target: ghcr.io Docker image + versioned feed archive on packages.healthchat.ch + GitHub Release (web bundle + installer download links). No npm, no mobile fan-out.
Release ≠ deploy
Cutting a tag never changes what any user is running. Both halves of the deployment are separate, manual, and need no code change:
| Component | Released by | Deployed by |
|---|---|---|
| Web (ghcr.io image) | healthchat-v* tag push → :latest moves |
gh workflow run deploy.yml on the ansible repo |
| Desktop (update feed) | healthchat-v* tag push → feed tarball on the Release |
gh workflow run deploy-healthchat-desktop.yaml |
Writing the desktop feed pushes an update to every installed client, so it is a deployment, not a publication step — hence the same manual gate as the server side. Deploy the two together unless you have a reason not to: a desktop client talks to the web deployment, so leaving one behind means users run a client built against a different release.
The desktop app version is taken from apps/desktop/package.json (the per-job
version input cannot set it — Windows squirrel mangles it into a nightly string,
and on macOS it only feeds the update manifest). So apps/desktop/package.json
must be bumped to the release version in the release commit to keep the About
dialog and installer filenames matching the tag. This is no longer optional: the
desktop workflow's "Assert desktop app version matches release" step fails the build
if apps/desktop/package.json drifts from the tag's MAJOR.MINOR.PATCH (any
-rc1/-hotfix1 suffix on the tag is ignored — squirrel needs a clean numeric
version, so set 1.4.0 even when tagging healthchat-v1.4.0-rc1).
Preflight checklist
Verify each item before tagging. If any fail, fix and commit first.
- On branch
release/healthchatwith a clean working tree (git status). HEALTHCHAT_CHANGES.mdhas a section## Changes in <new-version> (<date>)at the top of the file. The pipeline's awk extractor matches this exact heading format.apps/desktop/package.jsonversionis set to the release'sMAJOR.MINOR.PATCH(no suffix). Required — the desktop build asserts this and fails otherwise. This is what the desktop apps report as their version..healthchat-workspace-versionpoints at the healthchat-workspace tag or ref that should ship with this release. Ask the user whether to bump it to the latest plugin release or keep the currently pinned ref (see "Choosing the plugin version" below). Commit any bump in the same commit as the changelog entry.- Root
package.jsonversion can optionally be bumped to match (cosmetic — the web bundle version is injected from the tag at build time, and the git tag is the source of truth for the build). - No uncommitted changes to
.github/workflows/build-healthchat.yaml,Dockerfile, ordocker/that haven't been manually sanity-checked.
Choosing the plugin version
The HealthChat plugin (healthchat-workspace) ref that ships with the release is
pinned in .healthchat-workspace-version (a single line, e.g. v0.5.0-rc.4).
Before tagging, always ask the user which they want:
-
Update to the latest plugin release — check the available releases:
gh release list --repo Novaloop-AG/healthchat-workspace --limit 10If they choose a newer ref, write it to
.healthchat-workspace-versionand commit it alongside the changelog entry. -
Keep the currently pinned ref — leave
.healthchat-workspace-versionuntouched (show them the current value so the choice is informed).
Do not silently change the plugin ref; the user decides per release. The
workspace_ref workflow_dispatch input can override it for a one-off build
without committing, but the committed file is the source of truth for tag builds.
Cutting the release (normal path)
# from release/healthchat, fully committed:
git tag healthchat-v<MAJOR>.<MINOR>.<PATCH>
git push origin healthchat-v<MAJOR>.<MINOR>.<PATCH>
Examples:
git tag healthchat-v1.0.0git tag healthchat-v1.4.0-rc1git tag healthchat-v1.4.0-hotfix1
The workflow triggers automatically. Watch it via gh run watch or GitHub
Actions UI.
Deploying the release
Publishing the image does not deploy it. Deployment is a separate manual
dispatch against the ansible repo — but it needs no code change anywhere: the
inventory pins matrix_client_element_container_image to
ghcr.io/novaloop-ag/element-web/healthchat-web:latest, and the ansible role pulls
with pull: always, so it picks up whatever :latest now points at and restarts
the container only if the digest actually moved.
Wait for the release workflow to go green first (the deploy pulls whatever is in the registry at that moment), then:
gh workflow run deploy.yml \
--repo Novaloop-AG/matrix-docker-ansible-deploy \
--ref release/healthchat \
-f command="install-service client-element"
Watch it: gh run watch --repo Novaloop-AG/matrix-docker-ansible-deploy.
Or via the UI: GitHub → Novaloop-AG/matrix-docker-ansible-deploy → Actions →
"Deploy HealthChat (Synapse, Web app etc.)" → Run workflow → command:
install-service client-element.
Add -f check_mode=true for a dry run (--check --diff).
Deploying a specific version instead of :latest
RCs, hotfix tags and dispatch builds do not move :latest. To ship one — or to roll
back — pass an explicit image ref through extra_args, which the workflow forwards
verbatim to ansible-playbook. No inventory edit, no commit:
gh workflow run deploy.yml \
--repo Novaloop-AG/matrix-docker-ansible-deploy \
--ref release/healthchat \
-f command="install-service client-element" \
-f extra_args="-e matrix_client_element_container_image=ghcr.io/novaloop-ag/element-web/healthchat-web:1.5.0"
Note this is a one-shot override: the next plain deploy returns to :latest. For a
sustained hold, pin the version in the inventory instead.
Confirming what actually shipped
Because the inventory tracks :latest, git no longer records the deployed version —
check the running app, not the repo:
curl -fsS https://app.healthchat.ch/version
That serves the build version baked in by scripts/docker-package.sh (e.g.
healthchat-v1.5.0). If it still reports the old version, the deploy either did not
run or :latest had not moved yet.
Deploying the desktop release
The desktop build stages the feed at
https://packages.healthchat.ch/desktop-releases/<version>/ but
leaves the live feed at https://packages.healthchat.ch/desktop/ untouched. Until you run this, installed
apps keep whatever version they have and the public download links still serve the old
installers.
Wait for "Build HealthChat Desktop" to go green, then:
gh workflow run deploy-healthchat-desktop.yaml \
--ref release/healthchat \
-f version=1.5.0
Or: GitHub → Actions → "Deploy HealthChat Desktop" → Run workflow → version: 1.5.0.
Dry run first if you want to see what would be written without touching the server:
gh workflow run deploy-healthchat-desktop.yaml \
--ref release/healthchat \
-f version=1.5.0 -f dry_run=true
The workflow refuses to publish a partial feed — it fails before contacting the server if
releases.json, either arch's RELEASES, or the macOS *-mac.zip payload is missing. A
half-written feed breaks auto-update for everyone already installed, which is much worse
than not deploying.
On success it curls the five load-bearing URLs and fails if any is not 200, so a green run means the feed is genuinely live. Installed apps pick it up on their next update check.
Deploying an RC or rolling back
version accepts any released version, including 1.5.0-rc1. Because staged feeds
persist in the versioned archive (desktop-releases/<version>/), re-running this
workflow with an older version republishes that feed — no rebuild, no re-signing.
That is the desktop rollback path:
gh workflow run deploy-healthchat-desktop.yaml --ref release/healthchat -f version=1.4.1
Caveat: mirror -R does not delete, so rolling back rewrites the manifests but leaves the
newer *.nupkg files in place. That is intentional — Squirrel.Windows needs the delta
history — and harmless, since clients follow RELEASES/releases.json, not directory
listings.
Versions with no staged archive (cut before the archive design) fail the deploy with a
pointer; dispatch "Build HealthChat Desktop" with version=<version> to rebuild and
stage that version, then re-run the deploy.
Manual / retry path (workflow_dispatch)
Use when retrying a failed run, testing the pipeline, or rebuilding an image without moving tags.
- GitHub → Actions → "Release HealthChat" → Run workflow.
- Fill in:
- version (required): e.g.
1.4.0. Same regex validation as tag path. - workspace_ref (optional): overrides
.healthchat-workspace-version. - release_notes_override (optional): replaces the
HEALTHCHAT_CHANGES.mdlookup.
- version (required): e.g.
- Run. Docker image is pushed. No GitHub Release is created on the dispatch
path — only the tag-push path creates Releases (keeps manual retries from
clobbering the release history).
:latestis not moved either, for the same reason: a retry or pipeline test must not become production. The build still publishes:<version>and:<sha>; deploy it with the explicit-version override in "Deploying the release" if that is what you want.
The desktop workflow ("Build HealthChat Desktop") has its own workflow_dispatch
with the same inputs. Its release job also runs on dispatch to (re)stage the
feed archive at desktop-releases/<version>/ — that is the recovery path when a
version's archive is missing — but the GitHub Release steps (create + Downloads
links) stay tag-only, so a dispatch can never touch release history.
Tag format
healthchat-v<MAJOR>.<MINOR>.<PATCH>[-<suffix>]
Suffix conventions (informational; regex accepts any
[0-9A-Za-z.-]+):
-rc1,-rc2— release candidates-hotfix1— post-release fixes on an existing minor-dryrun1,-renew-test— one-off pipeline tests
The healthchat- prefix keeps these tags distinct from upstream
element-hq v<semver> tags if they are ever fetched into the repo.
Regex used for validation (must match):
^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$
Release notes
HEALTHCHAT_CHANGES.md is the single source. The workflow extracts the
section matching the version being released. Format:
## Changes in 1.4.0 (2026-04-21)
Short summary line (optional).
🙌 Improvements
- Bullet one.
- Bullet two.
If no matching section exists, the pipeline falls back to
HealthChat <version> — automated build from <ref>. and emits a workflow
warning. Prefer fixing the changelog over relying on the fallback.
After the release
-
Workflow run is green in GitHub Actions.
-
docker pull ghcr.io/novaloop-ag/element-web/healthchat-web:<version>succeeds. (Note the image path contains the repo segment — the workflow builds it asformat('{0}/healthchat-web', github.repository).) -
GitHub Release exists at
https://github.com/<owner>/<repo>/releases/tag/healthchat-v<version>with the correct body. -
The
:latesttag on ghcr.io now points at the new digest, and matches:<version>:```bash docker buildx imagetools inspect ghcr.io/novaloop-ag/element-web/healthchat-web:latest docker buildx imagetools inspect ghcr.io/novaloop-ag/element-web/healthchat-web:<version> ``` For an `-rc`/`-hotfix`/`-dryrun` tag the opposite is expected: `:latest` must be **unchanged**. -
The desktop workflow run ("Build HealthChat Desktop") is green, the GitHub Release notes carry the "## Downloads" section, and the staged archive exists:
curl -fsS https://packages.healthchat.ch/desktop-releases/<version>/update/macos/releases.json. Without that archive the desktop deploy has nothing to publish. -
Confirm the live feed is still on the previous version — releasing must not have moved it.
curl -fsS https://packages.healthchat.ch/desktop/update/macos/releases.json -
Smoke-check signing before shipping to users: macOS
spctl -a -vvv -t install <app>is accepted; Windows installer shows a valid publisher (no SmartScreen block).
After the deployment
Two independent deploys — do both (see "Deploying the release" and "Deploying the desktop release"):
- Web deployed and
curl -fsS https://app.healthchat.ch/versionreports the new version. - Desktop feed deployed: the "Deploy HealthChat Desktop" run is green. Its own verify step already asserts the feed URLs return 200, so a green run is the check.
releases.jsonnow advertises the new version:curl -fsS https://packages.healthchat.ch/desktop/update/macos/releases.json | jq -r '.releases[0].updateTo.version'- Spot-check auto-update on a real installed client (or at least confirm the app's update check reaches the feed) before announcing.
Required secrets
Configured in repo Settings → Secrets and variables → Actions:
| Secret | Purpose |
|---|---|
HEALTHCHAT_WORKSPACE_TOKEN |
PAT with read access to Novaloop-AG/healthchat-workspace. Needed by actions/checkout for the private plugin repo. |
GITHUB_TOKEN |
Auto-provided. Used for ghcr.io login and GitHub Release creation. No manual setup required. |
FTP_SERVER / FTP_USERNAME / FTP_PASSWORD |
Credentials for the healthchat.ch FTP(S) account (docroot of packages.healthchat.ch is /public_html/packages). Used by the release job of build-healthchat-desktop.yaml to stage the feed archive under /public_html/packages/desktop-releases/<version>/, and by deploy-healthchat-desktop.yaml to publish the live feed under /public_html/packages/desktop/. FTP_SERVER is the hostname only (explicit FTPS / AUTH TLS). |
Desktop signing additionally requires two GitHub environments (Settings → Environments). Without them the desktop builds still run but produce unsigned/untrusted output:
| Environment | Secrets / variables |
|---|---|
Desktop Apple |
secrets APPLE_ID, APPLE_ID_PASSWORD (app-specific), APPLE_CSC_LINK (base64 .p12), APPLE_CSC_KEY_PASSWORD; variable APPLE_TEAM_ID |
Windows code-signing reuses the org-level SSL.com eSigner secrets shared with the
other Novaloop apps (ESIGNER_USERNAME, ESIGNER_PASSWORD, ESIGNER_TOTP_SECRET);
the windows job in build-healthchat-desktop.yaml maps them to the CKA names the
reusable expects (no credential_id needed). Set variable ESIGNER_MODE=prod for a
trusted signature. These org secrets are scoped to private/internal repos, so the
fork must stay internal (not public). Add branch/environment protection so only
release/healthchat + healthchat-v* runs can reach the signing credentials.
Rollback
- Bad image already deployed → redeploy the previous version immediately using the
explicit-version override in "Deploying the release" (no commit, no inventory edit).
That restores service; then fix forward with a hotfix tag. Remember the override is
one-shot — the next plain deploy goes back to
:latest, so land the real fix before anyone runssetup-all. - Bad image, notes fine → fix the cause, commit, tag
healthchat-v<ver>-hotfix1. Note a-hotfix1tag does not move:latest; once you have verified it, either deploy it via the explicit-version override or cut a cleanX.Y.Ztag to promote it. - Bad desktop build already deployed to the feed → re-run "Deploy HealthChat Desktop" with the previous version (see "Deploying an RC or rolling back"). Clients that already auto-updated stay on the bad version until the next release — the feed only controls what is offered next, it cannot recall an installed build. That asymmetry is exactly why the desktop deploy is gated behind a manual dispatch.
- Bad desktop build caught before deploying → nothing to undo. Do not run the deploy workflow; fix forward and cut a new tag.
- Bad release notes → edit
HEALTHCHAT_CHANGES.mdand the GitHub Release body manually (GitHub UI). The ghcr.io tag is immutable, don't try to move it. Note the desktop feed bakes the notes in at build time (intoreleases.jsonandnotes.txt), so a post-hoc edit toHEALTHCHAT_CHANGES.mddoes not reach the update dialog — rebuild that version if the notes matter to desktop users. - Completely wrong tag → delete the GitHub Release (UI). Do not delete the ghcr.io image tag — treat it as burned. Re-tag with a new version.
Known branch divergence on rebase
These files are HealthChat-only and will conflict cleanly when rebasing
release/healthchat onto upstream. Always keep the HealthChat side (--ours
during rebase):
.github/workflows/build-healthchat.yaml.github/workflows/build-healthchat-desktop.yaml.github/workflows/deploy-healthchat-desktop.yamlHEALTHCHAT_CHANGES.md.healthchat-workspace-version.claude/skills/releasing-healthchat/SKILL.mdapps/desktop/build/icon.png,apps/desktop/build/icon.ico,apps/desktop/build/icon.icon/(HealthChat icons)
The four shared desktop reusables carry small HealthChat additions (a repository
input + product-name-agnostic asserts). These should merge cleanly, but if they
conflict on rebase, re-apply the HealthChat changes rather than blindly taking
either side:
.github/workflows/build_desktop_prepare.yaml.github/workflows/build_desktop_linux.yaml(still in the tree, but no longer invoked bybuild-healthchat-desktop.yaml— the fork doesn't build Linux).github/workflows/build_desktop_macos.yaml.github/workflows/build_desktop_windows.yaml