ci: link desktop installers instead of attaching them

This commit is contained in:
2026-07-29 23:19:35 +02:00
parent 8530b1f09a
commit 09e66dba47
3 changed files with 72 additions and 37 deletions
+20 -15
View File
@@ -19,16 +19,19 @@ Releases are driven by git tags of the form `healthchat-v<semver>` pushed to
`:<sha>`, so you can deploy them deliberately by overriding the image ref (see `:<sha>`, so you can deploy them deliberately by overriding the image ref (see
"Deploying the release"), but they can never become production by accident. "Deploying the release"), but they can never become production by accident.
- `.github/workflows/build-healthchat-desktop.yaml` (desktop) — builds the Electron - `.github/workflows/build-healthchat-desktop.yaml` (desktop) — builds the Electron
app for macOS (universal, signed+notarised) and Windows (x64/arm64, signed), and 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 publishes to the **same** GitHub Release as the web workflow (both create-if-missing,
workflows create-if-missing then upload, so they race safely. (Linux is intentionally so they race safely). (Linux is intentionally not built — the upstream Linux reusable
not built — the upstream Linux reusable pulls a build-env Docker image keyed by commit pulls a build-env Docker image keyed by commit SHA that only exists in the upstream
SHA that only exists in the upstream element-hq registry, so it cannot succeed in this element-hq registry, so it cannot succeed in this fork.) The `release` job **stages**
fork.) The `release` job also **stages** the Squirrel update feed and public installers and the Squirrel update feed and public installers and attaches them to the Release as
attaches them to the Release as `healthchat-desktop-feed-<version>.tar.gz` — but it split `healthchat-desktop-feed-<version>.tar.gz.part*` assets (the compressed feed
**does not publish them**. See "Deploying the desktop release" below. exceeds GitHub's 2 GiB per-asset cap) — but it **does not publish them**. Installer
binaries are not attached individually; the Release notes get a "## Downloads"
section linking their 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 - `.github/workflows/deploy-healthchat-desktop.yaml` (desktop deploy) — **manual
dispatch only.** Downloads that tarball and mirrors it to dispatch only.** Downloads and reassembles those feed parts and mirrors the feed to
`https://packages.healthchat.ch/desktop/` over FTPS (the private GitHub Release can `https://packages.healthchat.ch/desktop/` over FTPS (the private GitHub Release can
serve neither end users nor the auto-updater), plus the legacy serve neither end users nor the auto-updater), plus the legacy
`https://healthchat.ch/packages/desktop/` path while clients built <= 1.5.0 are still `https://healthchat.ch/packages/desktop/` path while clients built <= 1.5.0 are still
@@ -37,8 +40,8 @@ Releases are driven by git tags of the form `healthchat-v<semver>` pushed to
the two caveats (first install is manual; Windows auto-update needs the Setup.exe path, the two caveats (first install is manual; Windows auto-update needs the Setup.exe path,
not the .msi). not the .msi).
Publish target: **ghcr.io Docker image + GitHub Release (web bundle + desktop Publish target: **ghcr.io Docker image + GitHub Release (web bundle + desktop feed
installers).** No npm, no mobile fan-out. parts, with installer download links).** No npm, no mobile fan-out.
## Release ≠ deploy ## Release ≠ deploy
@@ -186,7 +189,8 @@ run or `:latest` had not moved yet.
## Deploying the desktop release ## Deploying the desktop release
The desktop build attaches `healthchat-desktop-feed-<version>.tar.gz` to the Release but The desktop build attaches the split `healthchat-desktop-feed-<version>.tar.gz.part*`
assets to the Release but
leaves `https://packages.healthchat.ch/desktop/` untouched. Until you run this, installed leaves `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 apps keep whatever version they have and the public download links still serve the old
installers. installers.
@@ -319,9 +323,10 @@ warning. Prefer fixing the changelog over relying on the fallback.
For an `-rc`/`-hotfix`/`-dryrun` tag the opposite is expected: `:latest` must For an `-rc`/`-hotfix`/`-dryrun` tag the opposite is expected: `:latest` must
be **unchanged**. be **unchanged**.
- [ ] The desktop workflow run ("Build HealthChat Desktop") is green and the - [ ] The desktop workflow run ("Build HealthChat Desktop") is green and the
GitHub Release has the desktop installers attached (.dmg, *-mac.zip, *.msi, GitHub Release carries the split feed assets
"* Setup*.exe") **plus** `healthchat-desktop-feed-<version>.tar.gz`. Without that `healthchat-desktop-feed-<version>.tar.gz.part*` plus a "## Downloads" section
tarball the desktop deploy has nothing to publish. in its notes linking the public installers. Without those parts the desktop
deploy has nothing to publish.
- [ ] Confirm the live feed is still on the **previous** version — releasing must not have - [ ] 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` 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>` - [ ] Smoke-check signing before shipping to users: macOS `spctl -a -vvv -t install <app>`
+49 -21
View File
@@ -10,10 +10,11 @@ name: Build HealthChat Desktop
# (via `webapp-artifact`) and the per-platform build_desktop_{macos,windows}.yaml reusables. # (via `webapp-artifact`) and the per-platform build_desktop_{macos,windows}.yaml reusables.
# #
# Notes: # Notes:
# - This workflow RELEASES but does not DEPLOY. It attaches the installers and a staged update-feed # - This workflow RELEASES but does not DEPLOY. It attaches a staged update-feed tarball (split
# tarball to the GitHub Release; the live auto-update feed at packages.healthchat.ch/desktop/ is # into <2 GiB parts) to the GitHub Release and links the public installer URLs in the Release
# only touched by the manually dispatched deploy-healthchat-desktop.yaml. So cutting a tag never # notes; the live auto-update feed at packages.healthchat.ch/desktop/ is only touched by the
# pushes an update to already-installed apps until a human decides to ship it. # manually dispatched deploy-healthchat-desktop.yaml. So cutting a tag never pushes an update
# to already-installed apps until a human decides to ship it.
# - The reusable build workflows are pointed at THIS fork (repository/ref inputs) so the Electron # - The reusable build workflows are pointed at THIS fork (repository/ref inputs) so the Electron
# shell, icons (apps/desktop/build/icon.*) and the healthchat:// deep-link handler # shell, icons (apps/desktop/build/icon.*) and the healthchat:// deep-link handler
# (apps/desktop/src/protocol.ts) are all fork-branded. Branding values come from # (apps/desktop/src/protocol.ts) are all fork-branded. Branding values come from
@@ -246,19 +247,12 @@ jobs:
with: with:
path: dist-artifacts path: dist-artifacts
- name: Collect installers # Installers are NOT attached to the Release: they ship inside the feed tarball and are
run: | # served from packages.healthchat.ch after the deploy — the Release body links to them
set -euo pipefail # there (see "Link public downloads in the Release notes" below). Attaching them here
mkdir -p release-files # would duplicate ~1 GiB per release and the x64/arm64 Setup.exe share a filename, so
# macOS: .dmg installer + .zip (Squirrel.Mac update payload) # they'd clobber each other as flat Release assets anyway.
find dist-artifacts -type f \( -iname '*.dmg' -o -iname '*-mac.zip' \) -exec cp -v {} release-files/ \; - name: Ensure GitHub Release exists
# Windows: per-machine MSI + Squirrel "Setup" installer
find dist-artifacts -type f \( -iname '*.msi' -o -iname '* Setup*.exe' \) -exec cp -v {} release-files/ \;
echo "--- release files ---"
ls -la release-files
test -n "$(ls -A release-files)" || { echo "::error::No installer files collected"; exit 1; }
- name: Upload to GitHub Release
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
@@ -266,11 +260,10 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
# The web workflow (build-healthchat.yaml) also creates this release for the same tag. # The web workflow (build-healthchat.yaml) also creates this release for the same tag.
# Create-if-missing (without touching existing notes) then upload --clobber so the two # Create-if-missing (without touching existing notes) so the two workflows race
# workflows race safely regardless of ordering. # safely regardless of ordering.
gh release view "$TAG" >/dev/null 2>&1 || \ gh release view "$TAG" >/dev/null 2>&1 || \
gh release create "$TAG" --title "$TAG" --notes "HealthChat desktop installers for $TAG" gh release create "$TAG" --title "$TAG" --notes "HealthChat $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 # The GitHub Release above is private (internal fork), so it can't serve end users or the
# Squirrel auto-updater. Those are served from packages.healthchat.ch/desktop/, which is # Squirrel auto-updater. Those are served from packages.healthchat.ch/desktop/, which is
@@ -356,3 +349,38 @@ jobs:
ls -lh "${TARBALL}".part* ls -lh "${TARBALL}".part*
gh release upload "$TAG" "${TARBALL}".part* --clobber gh release upload "$TAG" "${TARBALL}".part* --clobber
echo "::notice::Update feed attached as ${TARBALL}.part*. The live feed at packages.healthchat.ch is UNCHANGED — run the 'Deploy HealthChat Desktop' workflow to publish it." echo "::notice::Update feed attached as ${TARBALL}.part*. The live feed at packages.healthchat.ch is UNCHANGED — run the 'Deploy HealthChat Desktop' workflow to publish it."
# The Release carries no installer binaries (see "Ensure GitHub Release exists"), so
# point readers at the public download URLs instead. The links are derived from the
# staged feed/install tree, so they stay correct if electron-builder ever renames the
# artifacts. They resolve once "Deploy HealthChat Desktop" has published this version.
- name: Link public downloads in the Release notes
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
BASE="https://packages.healthchat.ch/desktop"
BODY=$(gh release view "$TAG" --json body --jq .body)
if [[ "$BODY" == *"## Downloads"* ]]; then
echo "Release notes already carry a Downloads section — leaving them untouched."
exit 0
fi
{
printf '%s\n\n' "$BODY"
echo "## Downloads"
echo ""
echo "Public installers, live once this version is deployed via \"Deploy HealthChat Desktop\":"
echo ""
for f in feed/install/macos/* feed/install/win32/x64/* feed/install/win32/arm64/*; do
name=$(basename "$f")
# Skip the stable "latest" aliases — they track whatever is deployed, not this version.
case "$name" in HealthChat.dmg|HealthChat-Setup.exe) continue ;; esac
dir=$(dirname "${f#feed/}")
enc=$(jq -rn --arg s "$name" '$s|@uri')
echo "- [$name]($BASE/$dir/$enc)"
done
} > release-body.md
gh release edit "$TAG" --notes-file release-body.md
echo "::notice::Release notes now link the public installers under $BASE/install/."
+3 -1
View File
@@ -49,7 +49,9 @@ feeds it into build_desktop_prepare.yaml (`config: element.io/healthchat`,
reusables. It runs on every `healthchat-v*` tag (alongside the web build) and via reusables. It runs on every `healthchat-v*` tag (alongside the web build) and via
workflow_dispatch. The reusables are pointed at THIS fork (`repository`/`ref` inputs) so the workflow_dispatch. The reusables are pointed at THIS fork (`repository`/`ref` inputs) so the
Electron shell, icons and the healthchat:// deep-link handler are fork-branded. On a tag push the Electron shell, icons and the healthchat:// deep-link handler are fork-branded. On a tag push the
`release` job attaches the .dmg / .zip / .exe / .msi to the GitHub Release. `release` job attaches the staged update feed as split healthchat-desktop-feed-<ver>.tar.gz.part*
assets (the compressed feed exceeds GitHub's 2 GiB per-asset cap) and links the public installer
URLs on packages.healthchat.ch in the Release notes — installer binaries are not attached.
(Linux is not built: the upstream build_desktop_linux.yaml needs a build-env Docker image (Linux is not built: the upstream build_desktop_linux.yaml needs a build-env Docker image
keyed by commit SHA that only exists in the upstream element-hq registry, so it can't run here.) keyed by commit SHA that only exists in the upstream element-hq registry, so it can't run here.)