Merge pull request #5 from Novaloop-AG/feat/desktop-auto-update

feat(desktop): wire up HealthChat auto-update + public installer hosting
This commit is contained in:
2026-06-28 10:02:55 +02:00
committed by GitHub
5 changed files with 113 additions and 3 deletions
+12 -1
View File
@@ -18,7 +18,12 @@ Releases are driven by git tags of the form `healthchat-v<semver>` 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 <app>` 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
@@ -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/<zip>` to this, so the in-manifest URL becomes
# https://healthchat.ch/packages/desktop/update/macos/<zip>, 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
@@ -8,6 +8,7 @@ permissions: {}
jobs:
doc:
if: ${{ github.repository == 'element-hq/element-web' }}
name: Build storybook
runs-on: ubuntu-latest
steps:
+32 -2
View File
@@ -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-<ver>-mac.zip (Squirrel.Mac)
update/win32/<arch>/RELEASES + HealthChat-<ver>-full.nupkg (Squirrel.Windows; x64 + arm64)
install/ public downloads for the FIRST install (users get these URLs, not the GitHub Release):
install/macos/HealthChat-<ver>.dmg + HealthChat.dmg (stable "latest")
install/win32/<arch>/HealthChat-Setup-<ver>.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.
@@ -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",