Files
healthchat-web/.claude/skills/releasing-healthchat/SKILL.md
T
joel.poyet ade1dff201 feat(desktop): wire up HealthChat auto-update + public installer hosting
The Electron auto-updater (Squirrel.Mac / Squirrel.Windows) was already
implemented but switched off for HealthChat because the variant config
omitted update_base_url, and CI published only installers (no update feed),
to a private GitHub Release that can't serve end users or the updater.

- config.json: set update_base_url to https://healthchat.ch/packages/desktop/update/
  so electron-main starts the updater.
- build-healthchat-desktop.yaml: pass base-url to the macOS job so releases.json
  is generated; extend the release job to stage the update feed + public
  installers and mirror them to healthchat.ch over FTPS (lftp, no-delete to
  preserve prior nupkgs; password via LFTP_PASSWORD).
- Document the feed/install layout, FTP secrets, and the two caveats (first
  install is manual; Windows auto-update needs the Setup.exe path, not .msi)
  in the healthchat README and the releasing-healthchat skill.

Requires repo secrets FTP_SERVER / FTP_USERNAME / FTP_PASSWORD.
2026-06-28 09:54:08 +02:00

11 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 under healthchat-web:<version>, :stable, and :<sha>, and creates a GitHub Release whose body is the matching section of HEALTHCHAT_CHANGES.md.
  • .github/workflows/build-healthchat-desktop.yaml (desktop) — builds the Electron app for macOS (universal, signed+notarised) and Windows (x64/arm64, signed), and attaches the installers (.dmg/.zip/.exe/.msi) to the same GitHub Release. The two workflows create-if-missing then upload, 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.) The release job additionally mirrors the Squirrel update feed and the public installers to https://healthchat.ch/packages/desktop/ over FTPS (the private GitHub Release can serve neither end users nor the auto-updater). This is what makes installed apps self-update; see apps/desktop/element.io/healthchat/README for 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 + GitHub Release (web bundle + desktop installers). No npm, no mobile fan-out.

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/healthchat with a clean working tree (git status).
  • HEALTHCHAT_CHANGES.md has 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.json version is set to the release's MAJOR.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-version points 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.json version 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, or docker/ 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:

  1. Update to the latest plugin release — check the available releases:

    gh release list --repo Novaloop-AG/healthchat-workspace --limit 10
    

    If they choose a newer ref, write it to .healthchat-workspace-version and commit it alongside the changelog entry.

  2. Keep the currently pinned ref — leave .healthchat-workspace-version untouched (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.0
  • git tag healthchat-v1.4.0-rc1
  • git tag healthchat-v1.4.0-hotfix1

The workflow triggers automatically. Watch it via gh run watch or GitHub Actions UI.

Manual / retry path (workflow_dispatch)

Use when retrying a failed run, testing the pipeline, or rebuilding an image without moving tags.

  1. GitHub → Actions → "Release HealthChat" → Run workflow.
  2. 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.md lookup.
  3. 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).

The desktop workflow ("Build HealthChat Desktop") has its own workflow_dispatch with the same inputs; like the web path, its release job only publishes on a real tag push, so a manual dispatch builds and uploads run artifacts but does not attach anything to a Release.

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/<owner>/healthchat-web:<version> succeeds.
  • GitHub Release exists at https://github.com/<owner>/<repo>/releases/tag/healthchat-v<version> with the correct body.
  • The :stable tag on ghcr.io now points at the new digest (docker buildx imagetools inspect ghcr.io/<owner>/healthchat-web:stable).
  • The desktop workflow run ("Build HealthChat Desktop") is green and the GitHub Release has the desktop installers attached (.dmg, -mac.zip, .msi, " Setup.exe").
  • The update feed + installers published to healthchat.ch: curl -fsI https://healthchat.ch/packages/desktop/update/macos/releases.json, .../update/win32/x64/RELEASES, and the public installer URLs .../install/macos/HealthChat.dmg / .../install/win32/x64/HealthChat-Setup.exe all return 200.
  • Smoke-check signing: macOS spctl -a -vvv -t install <app> is accepted; Windows installer shows a valid publisher (no SmartScreen block).

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. The desktop release job uses them to publish the update feed + public installers under /public_html/healthchat.ch/packages/desktop/. FTP_SERVER is the hostname only (explicit FTPS / AUTH TLS). Without them the FTP publish step fails (but the GitHub Release still gets the installers).

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, notes fine → fix the cause, commit, tag healthchat-v<ver>-hotfix1.
  • Bad release notes → edit HEALTHCHAT_CHANGES.md and the GitHub Release body manually (GitHub UI). The ghcr.io tag is immutable, don't try to move it.
  • 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
  • HEALTHCHAT_CHANGES.md
  • .healthchat-workspace-version
  • .claude/skills/releasing-healthchat/SKILL.md
  • apps/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 by build-healthchat-desktop.yaml — the fork doesn't build Linux)
  • .github/workflows/build_desktop_macos.yaml
  • .github/workflows/build_desktop_windows.yaml