From ade1dff201e97c8049e358d286deadb0f378e664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl?= Date: Sun, 28 Jun 2026 09:54:08 +0200 Subject: [PATCH 1/2] 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. --- .claude/skills/releasing-healthchat/SKILL.md | 13 +++- .../workflows/build-healthchat-desktop.yaml | 67 +++++++++++++++++++ apps/desktop/element.io/healthchat/README | 34 +++++++++- .../desktop/element.io/healthchat/config.json | 1 + 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/.claude/skills/releasing-healthchat/SKILL.md b/.claude/skills/releasing-healthchat/SKILL.md index abca00dfdb..bf77a9b669 100644 --- a/.claude/skills/releasing-healthchat/SKILL.md +++ b/.claude/skills/releasing-healthchat/SKILL.md @@ -18,7 +18,12 @@ Releases are driven by git tags of the form `healthchat-v` pushed to 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.) + 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. @@ -165,6 +170,11 @@ warning. Prefer fixing the changelog over relying on the fallback. - [ ] 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 ` is accepted; Windows installer shows a valid publisher (no SmartScreen block). @@ -176,6 +186,7 @@ Configured in repo Settings → Secrets and variables → Actions: |---|---| | `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 diff --git a/.github/workflows/build-healthchat-desktop.yaml b/.github/workflows/build-healthchat-desktop.yaml index 2455b75221..7810dd36eb 100644 --- a/.github/workflows/build-healthchat-desktop.yaml +++ b/.github/workflows/build-healthchat-desktop.yaml @@ -189,6 +189,12 @@ jobs: sign: true test: false targets: "dmg zip" + # Generates dist/releases.json (the Squirrel.Mac feed manifest). The reusable appends + # `/update/macos/` to this, so the in-manifest URL becomes + # https://healthchat.ch/packages/desktop/update/macos/, matching where the + # `release` job publishes the feed. Keep in sync with update_base_url in + # element.io/healthchat/config.json (.../packages/desktop/update/). + base-url: https://healthchat.ch/packages/desktop windows: name: Windows ${{ matrix.arch }} @@ -255,3 +261,64 @@ jobs: gh release view "$TAG" >/dev/null 2>&1 || \ gh release create "$TAG" --title "$TAG" --notes "HealthChat desktop installers for $TAG" gh release upload "$TAG" release-files/* --clobber + + # The GitHub Release above is private (internal fork), so it can't serve end users or the + # Squirrel auto-updater. Mirror the update feed + public installers to healthchat.ch over + # FTPS so installed apps self-update (update_base_url -> .../packages/desktop/update/) and + # users can download from a public URL (.../packages/desktop/install/). + - name: Stage update feed + public installers + run: | + set -euo pipefail + # --- macOS update feed: releases.json (Squirrel.Mac manifest) + signed *-mac.zip payload --- + mkdir -p feed/update/macos feed/install/macos + test -f dist-artifacts/macos/releases.json || { + echo "::error::dist-artifacts/macos/releases.json missing (did the macos job get base-url?)"; exit 1; } + cp -v dist-artifacts/macos/releases.json feed/update/macos/ + find dist-artifacts/macos -type f -iname '*-mac.zip' -exec cp -v {} feed/update/macos/ \; + # --- macOS installer: versioned .dmg + a stable "latest" copy --- + find dist-artifacts/macos -type f -iname '*.dmg' -exec cp -v {} feed/install/macos/ \; + dmg=$(find feed/install/macos -type f -iname '*.dmg' | head -n1) + test -n "$dmg" && cp -v "$dmg" feed/install/macos/HealthChat.dmg + # --- Windows: per-arch Squirrel feed (RELEASES + full nupkg) + installers (Setup.exe, msi) --- + for arch in x64 arm64; do + src="dist-artifacts/win-$arch" + [ -d "$src" ] || { echo "::error::$src missing"; exit 1; } + mkdir -p "feed/update/win32/$arch" "feed/install/win32/$arch" + find "$src" -type f -iname 'RELEASES' -exec cp -v {} "feed/update/win32/$arch/" \; + find "$src" -type f -iname '*-full.nupkg' -exec cp -v {} "feed/update/win32/$arch/" \; + find "$src" -type f -iname '* Setup*.exe' -exec cp -v {} "feed/install/win32/$arch/" \; + find "$src" -type f -iname '*.msi' -exec cp -v {} "feed/install/win32/$arch/" \; + setup=$(find "feed/install/win32/$arch" -type f -iname '* Setup*.exe' | head -n1) + test -n "$setup" && cp -v "$setup" "feed/install/win32/$arch/HealthChat-Setup.exe" + done + echo "--- staged feed tree ---" + find feed -type f | sort + + - name: Install lftp + run: sudo apt-get update && sudo apt-get install -y lftp + + - name: Publish to healthchat.ch over FTPS + env: + FTP_SERVER: ${{ secrets.FTP_SERVER }} + FTP_USERNAME: ${{ secrets.FTP_USERNAME }} + # Passed to lftp via LFTP_PASSWORD (--env-password) so it never appears on the + # command line / process list. + LFTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} + run: | + set -euo pipefail + : "${FTP_SERVER:?FTP_SERVER secret not set}" + : "${FTP_USERNAME:?FTP_USERNAME secret not set}" + : "${LFTP_PASSWORD:?FTP_PASSWORD secret not set}" + # mirror -R (reverse) uploads the local tree; default is no-delete, so prior + # *.nupkg are preserved (Squirrel.Windows delta history) and only the staged files + # are written/overwritten. If the host's FTPS cert is untrusted, set + # ssl:verify-certificate to no. + lftp -u "$FTP_USERNAME" --env-password "$FTP_SERVER" <<'LFTP' + set cmd:fail-exit true + set ftp:ssl-force true + set ftp:ssl-protect-data true + set ssl:verify-certificate true + mkdir -pf /public_html/healthchat.ch/packages/desktop + mirror -R --parallel=4 --verbose feed/ /public_html/healthchat.ch/packages/desktop/ + bye + LFTP diff --git a/apps/desktop/element.io/healthchat/README b/apps/desktop/element.io/healthchat/README index 4a02c4bb1e..95c5405ed1 100644 --- a/apps/desktop/element.io/healthchat/README +++ b/apps/desktop/element.io/healthchat/README @@ -90,6 +90,38 @@ Without these the jobs still build but produce unsigned/untrusted output. Becaus a public-history fork, add environment/branch protection so only release/healthchat + healthchat-v* runs can reach the signing credentials. +Auto-update + public installer hosting +-------------------------------------- +The apps self-update over a feed hosted on healthchat.ch (the GitHub Release is private to this +internal fork, so it can serve neither end users nor the Squirrel auto-updater). Both the update +feed and the public installers live under https://healthchat.ch/packages/desktop/ (FTP web root +/public_html/healthchat.ch maps to https://healthchat.ch/; no packages. subdomain, so it's a path): + + update/ the Squirrel feed the installed apps poll hourly. config.json sets + "update_base_url": "https://healthchat.ch/packages/desktop/update/" + and the macos job passes base-url: https://healthchat.ch/packages/desktop so the + generated releases.json points the *-mac.zip at this origin. + update/macos/releases.json + HealthChat--mac.zip (Squirrel.Mac) + update/win32//RELEASES + HealthChat--full.nupkg (Squirrel.Windows; x64 + arm64) + install/ public downloads for the FIRST install (users get these URLs, not the GitHub Release): + install/macos/HealthChat-.dmg + HealthChat.dmg (stable "latest") + install/win32//HealthChat-Setup-.exe + HealthChat-Setup.exe (stable) + *.msi + +The `release` job in build-healthchat-desktop.yaml stages these trees and uploads them via lftp/FTPS +(mirror -R, no-delete so prior nupkgs survive for delta history). Add these repo secrets: + FTP_SERVER hostname only (explicit FTPS / AUTH TLS on port 21; if the host cert is untrusted, + flip ssl:verify-certificate to no in the workflow) + FTP_USERNAME + FTP_PASSWORD + +Caveats: + - First install is manual. Apps already installed without update_base_url (<= 1.1.2) cannot + self-update to the first build that carries it; users must download once from the install/ URL. + Every release after that updates automatically. + - Windows auto-update only works for the Squirrel Setup.exe install path, NOT the per-machine + .msi (the updater needs Squirrel's Update.exe, which the MSI doesn't install). Hand users the + Setup.exe as the primary Windows installer. macOS: install the .dmg into /Applications. + Open items ---------- - api_base_url: confirm the production API origin in config.json (currently assumed @@ -99,5 +131,3 @@ Open items rewrites any `version` override into a nightly string), NOT the healthchat-v tag. Bump apps/desktop/package.json in the release commit if you want the About dialog / installer filenames to match the tag. - - Auto-update: not wired up. The apps install from the GitHub Release but do not self-update. - Adding it needs a hosted update feed (releases.json / RELEASES) on a stable URL. diff --git a/apps/desktop/element.io/healthchat/config.json b/apps/desktop/element.io/healthchat/config.json index 8484257b34..1c202650b1 100644 --- a/apps/desktop/element.io/healthchat/config.json +++ b/apps/desktop/element.io/healthchat/config.json @@ -96,6 +96,7 @@ ] }, "show_labs_settings": false, + "update_base_url": "https://healthchat.ch/packages/desktop/update/", "modules": ["/modules/healthchat-module.js"], "ch.healthchat": { "api_base_url": "https://healthchat.ch", From 15161b6ddf237b671ec57dda69935e521169cd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl?= Date: Sun, 28 Jun 2026 09:58:07 +0200 Subject: [PATCH 2/2] ci: gate storybook build workflow on fork Add the same repo guard applied to sibling upstream workflows in bd6241ad9d. On the private fork this job failed at checkout because permissions: {} leaves GITHUB_TOKEN without contents:read, so actions/checkout 404s on the private repo. --- .github/workflows/shared-component-storybook-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/shared-component-storybook-build.yml b/.github/workflows/shared-component-storybook-build.yml index ff3383f430..c04115c553 100644 --- a/.github/workflows/shared-component-storybook-build.yml +++ b/.github/workflows/shared-component-storybook-build.yml @@ -8,6 +8,7 @@ permissions: {} jobs: doc: + if: ${{ github.repository == 'element-hq/element-web' }} name: Build storybook runs-on: ubuntu-latest steps: