ci: stage desktop feed on packages.healthchat.ch archive

This commit is contained in:
2026-07-29 23:28:39 +02:00
parent 09e66dba47
commit 30bb2bfca3
4 changed files with 143 additions and 103 deletions
+32 -30
View File
@@ -24,24 +24,26 @@ Releases are driven by git tags of the form `healthchat-v<semver>` pushed to
so they race safely). (Linux is intentionally not built — the upstream Linux reusable 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 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 **stages** element-hq registry, so it cannot succeed in this fork.) The `release` job **stages**
the Squirrel update feed and public installers and attaches them to the Release as the Squirrel update feed and public installers and uploads them to the versioned
split `healthchat-desktop-feed-<version>.tar.gz.part*` assets (the compressed feed archive at `https://packages.healthchat.ch/desktop-releases/<version>/` — but it
exceeds GitHub's 2 GiB per-asset cap) — but it **does not publish them**. Installer **does not publish them** to the live feed. Nothing binary is attached to the GitHub
binaries are not attached individually; the Release notes get a "## Downloads" Release (the compressed feed outgrew GitHub's 2 GiB per-asset cap at 1.6.0); the
section linking their public URLs on packages.healthchat.ch, which resolve once the Release notes get a "## Downloads" section linking the installers' public URLs on
version is deployed. See "Deploying the desktop release" below. 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 and reassembles those feed parts and mirrors the feed to dispatch only.** Fetches the staged archive for the requested version and mirrors it
`https://packages.healthchat.ch/desktop/` over FTPS (the private GitHub Release can to the live feed at `https://packages.healthchat.ch/desktop/` over FTPS (the private
serve neither end users nor the auto-updater), plus the legacy GitHub Release can 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
in the field. This is what makes installed apps in the field. This is what makes installed apps
self-update; see `apps/desktop/element.io/healthchat/README` for the feed layout and 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, 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 feed Publish target: **ghcr.io Docker image + versioned feed archive on
parts, with installer download links).** No npm, no mobile fan-out. packages.healthchat.ch + GitHub Release (web bundle + installer download links).**
No npm, no mobile fan-out.
## Release ≠ deploy ## Release ≠ deploy
@@ -189,9 +191,9 @@ run or `:latest` had not moved yet.
## Deploying the desktop release ## Deploying the desktop release
The desktop build attaches the split `healthchat-desktop-feed-<version>.tar.gz.part*` The desktop build stages the feed at
assets to the Release but `https://packages.healthchat.ch/desktop-releases/<version>/` but
leaves `https://packages.healthchat.ch/desktop/` untouched. Until you run this, installed 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 apps keep whatever version they have and the public download links still serve the old
installers. installers.
@@ -223,10 +225,10 @@ means the feed is genuinely live. Installed apps pick it up on their next update
### Deploying an RC or rolling back ### Deploying an RC or rolling back
`version` accepts any released version, including `1.5.0-rc1`. Because the feed lives on `version` accepts any released version, including `1.5.0-rc1`. Because staged feeds
the Release (assets never expire, unlike workflow artifacts), **re-running this workflow persist in the versioned archive (`desktop-releases/<version>/`), **re-running this
with an older version republishes that feed** — no rebuild, no re-signing. That is the workflow with an older version republishes that feed** — no rebuild, no re-signing.
desktop rollback path: That is the desktop rollback path:
```bash ```bash
gh workflow run deploy-healthchat-desktop.yaml --ref release/healthchat -f version=1.4.1 gh workflow run deploy-healthchat-desktop.yaml --ref release/healthchat -f version=1.4.1
@@ -237,9 +239,9 @@ newer `*.nupkg` files in place. That is intentional — Squirrel.Windows needs t
history — and harmless, since clients follow `RELEASES`/`releases.json`, not directory history — and harmless, since clients follow `RELEASES`/`releases.json`, not directory
listings. listings.
Releases cut **before** this release/deploy split carry no feed tarball. The workflow says Versions with no staged archive (cut before the archive design) fail the deploy with a
so explicitly and lists the assets it did find; re-run "Build HealthChat Desktop" for that pointer; dispatch "Build HealthChat Desktop" with `version=<version>` to rebuild and
version to produce one. stage that version, then re-run the deploy.
## Manual / retry path (workflow_dispatch) ## Manual / retry path (workflow_dispatch)
@@ -260,9 +262,10 @@ without moving tags.
override in "Deploying the release" if that is what you want. override in "Deploying the release" if that is what you want.
The desktop workflow ("Build HealthChat Desktop") has its own workflow_dispatch 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 with the same inputs. Its `release` job **also runs on dispatch** to (re)stage the
real tag push, so a manual dispatch builds and uploads run artifacts but does not feed archive at `desktop-releases/<version>/` — that is the recovery path when a
attach anything to a Release. 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 ## Tag format
@@ -322,11 +325,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, the GitHub
GitHub Release carries the split feed assets Release notes carry the "## Downloads" section, and the staged archive exists:
`healthchat-desktop-feed-<version>.tar.gz.part*` plus a "## Downloads" section `curl -fsS https://packages.healthchat.ch/desktop-releases/<version>/update/macos/releases.json`.
in its notes linking the public installers. Without those parts the desktop Without that archive the desktop deploy has nothing to publish.
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>`
@@ -354,7 +356,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. | | `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. | | `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. Used **only** by `deploy-healthchat-desktop.yaml` to publish the update feed + public installers under `/public_html/packages/desktop/` (docroot of packages.healthchat.ch) plus, during the URL migration, the legacy `/public_html/healthchat.ch/packages/desktop/`. `FTP_SERVER` is the hostname only (explicit FTPS / AUTH TLS). The release workflows never touch FTP, so a missing/expired credential breaks the deploy but never the release. | | `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/` plus, during the URL migration, the legacy `/public_html/healthchat.ch/packages/desktop/`. `FTP_SERVER` is the hostname only (explicit FTPS / AUTH TLS). |
Desktop signing additionally requires two GitHub **environments** (Settings → Desktop signing additionally requires two GitHub **environments** (Settings →
Environments). Without them the desktop builds still run but produce Environments). Without them the desktop builds still run but produce
+64 -32
View File
@@ -10,11 +10,12 @@ 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 a staged update-feed tarball (split # - This workflow RELEASES but does not DEPLOY. It stages the update feed + public installers
# into <2 GiB parts) to the GitHub Release and links the public installer URLs in the Release # to the versioned archive at packages.healthchat.ch/desktop-releases/<version>/ and links
# notes; the live auto-update feed at packages.healthchat.ch/desktop/ is only touched by the # the public installer URLs in the Release notes (nothing binary is attached to the GitHub
# manually dispatched deploy-healthchat-desktop.yaml. So cutting a tag never pushes an update # Release); the live auto-update feed at packages.healthchat.ch/desktop/ is only touched by
# to already-installed apps until a human decides to ship it. # the 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
@@ -228,10 +229,13 @@ jobs:
targets: "squirrel msi" targets: "squirrel msi"
release: release:
name: Publish to GitHub Release name: Stage feed archive + Release notes
needs: [macos, windows] needs: [macos, windows]
# Only publish on a real tag push; manual workflow_dispatch runs build but do not publish. # Runs on tag pushes AND manual dispatches: the archive upload is idempotent per version
if: startsWith(github.ref, 'refs/tags/') # (mirror --delete into desktop-releases/<version>/), and re-running it via dispatch is
# the recovery path for a version whose archive is missing. The GitHub Release steps
# below stay tag-only, so a dispatch can never touch release history.
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -242,17 +246,31 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Resolve version
id: relversion
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
VERSION="${GITHUB_REF_NAME#healthchat-v}"
else
VERSION="${INPUT_VERSION:?No version input on workflow_dispatch}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download all build artifacts - name: Download all build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: with:
path: dist-artifacts path: dist-artifacts
# Installers are NOT attached to the Release: they ship inside the feed tarball and are # Installers are NOT attached to the Release: they ship inside the staged feed and are
# served from packages.healthchat.ch after the deploy — the Release body links to them # served from packages.healthchat.ch after the deploy — the Release body links to them
# there (see "Link public downloads in the Release notes" below). Attaching them here # there (see "Link public downloads in the Release notes" below). Attaching them here
# would duplicate ~1 GiB per release and the x64/arm64 Setup.exe share a filename, so # would duplicate ~1 GiB per release and the x64/arm64 Setup.exe share a filename, so
# they'd clobber each other as flat Release assets anyway. # they'd clobber each other as flat Release assets anyway.
- name: Ensure GitHub Release exists - name: Ensure GitHub Release exists
if: startsWith(github.ref, 'refs/tags/')
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
@@ -265,13 +283,16 @@ jobs:
gh release view "$TAG" >/dev/null 2>&1 || \ gh release view "$TAG" >/dev/null 2>&1 || \
gh release create "$TAG" --title "$TAG" --notes "HealthChat $TAG" gh release create "$TAG" --title "$TAG" --notes "HealthChat $TAG"
# The GitHub Release above is private (internal fork), so it can't serve end users or the # The GitHub Release 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
# published by the SEPARATE, MANUALLY DISPATCHED "Deploy HealthChat Desktop" workflow # published by the SEPARATE, MANUALLY DISPATCHED "Deploy HealthChat Desktop" workflow
# (deploy-healthchat-desktop.yaml) — releasing never touches the live update feed. # (deploy-healthchat-desktop.yaml) — releasing never touches the live update feed.
# This job only *stages* that feed and attaches it to the Release as a tarball, so the # This job only *stages* that feed and uploads it to the versioned archive at
# deploy workflow can publish any past version without rebuilding or re-signing. # packages.healthchat.ch/desktop-releases/<version>/ (the archive persists, so the
# deploy workflow can publish any staged version without rebuilding or re-signing).
- name: Stage update feed + public installers - name: Stage update feed + public installers
env:
VERSION: ${{ steps.relversion.outputs.version }}
run: | run: |
set -euo pipefail set -euo pipefail
# --- Release notes: extract this version's section from HEALTHCHAT_CHANGES.md --- # --- Release notes: extract this version's section from HEALTHCHAT_CHANGES.md ---
@@ -279,7 +300,6 @@ jobs:
# auto-update "See changes" dialog: macOS reads it from releases.json's `notes` # auto-update "See changes" dialog: macOS reads it from releases.json's `notes`
# field natively, Windows fetches notes.txt (Squirrel.Windows can't carry notes # field natively, Windows fetches notes.txt (Squirrel.Windows can't carry notes
# through the update-downloaded event — only releaseName is available). # through the update-downloaded event — only releaseName is available).
VERSION="${GITHUB_REF_NAME#healthchat-v}"
NOTES=$(awk -v ver="$VERSION" ' NOTES=$(awk -v ver="$VERSION" '
/^## Changes in / { /^## Changes in / {
if (found) { exit } if (found) { exit }
@@ -327,34 +347,46 @@ jobs:
echo "--- staged feed tree ---" echo "--- staged feed tree ---"
find feed -type f | sort find feed -type f | sort
# Attach the staged feed to the Release instead of publishing it. "Deploy HealthChat # Upload the staged feed to the versioned archive instead of publishing it. "Deploy
# Desktop" (deploy-healthchat-desktop.yaml) downloads this tarball and mirrors it to # HealthChat Desktop" (deploy-healthchat-desktop.yaml) mirrors this archive to the
# healthchat.ch when a human decides to ship. Release assets do not expire, so any past # live feed when a human decides to ship. The archive directories persist on the
# version stays redeployable long after the workflow artifacts are gone. # server, so any staged version stays redeployable without rebuilding or re-signing.
- name: Attach update feed to the GitHub Release # (The feed used to be attached to the GitHub Release as a tarball, but the
# compressed feed outgrew GitHub's 2 GiB per-asset cap at 1.6.0.)
- name: Publish staged feed to the versioned archive
env: env:
GH_TOKEN: ${{ github.token }} FTP_SERVER: ${{ secrets.FTP_SERVER }}
GH_REPO: ${{ github.repository }} FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
TAG: ${{ github.ref_name }} # Passed to lftp via LFTP_PASSWORD (--env-password) so it never appears on the
# command line / process list.
LFTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
VERSION: ${{ steps.relversion.outputs.version }}
run: | run: |
set -euo pipefail set -euo pipefail
VERSION="${GITHUB_REF_NAME#healthchat-v}" : "${FTP_SERVER:?FTP_SERVER secret not set}"
TARBALL="healthchat-desktop-feed-${VERSION}.tar.gz" : "${FTP_USERNAME:?FTP_USERNAME secret not set}"
# Store paths relative to feed/ so the deploy side extracts a bare feed/ tree. : "${LFTP_PASSWORD:?FTP_PASSWORD secret not set}"
# GitHub caps Release assets at 2 GiB and the compressed feed exceeds that sudo apt-get update && sudo apt-get install -y lftp
# (first hit by 1.6.0), so stream the tarball through split and attach the # The archive dir is owned by this version's build: --delete makes re-runs
# parts; the deploy workflow concatenates ${TARBALL}.part* in glob order # idempotent (a rebuild fully replaces the staged tree). The live feed under
# before extracting. # /public_html/packages/desktop is never touched here.
tar -czf - feed | split -b 1536m -d - "${TARBALL}.part" lftp -u "$FTP_USERNAME" --env-password "$FTP_SERVER" <<LFTP
ls -lh "${TARBALL}".part* set cmd:fail-exit true
gh release upload "$TAG" "${TARBALL}".part* --clobber set ftp:ssl-force true
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." set ftp:ssl-protect-data true
set ssl:verify-certificate true
mkdir -pf /public_html/packages/desktop-releases/$VERSION
mirror -R --delete --parallel=4 --verbose feed/ /public_html/packages/desktop-releases/$VERSION/
bye
LFTP
echo "::notice::Feed staged at https://packages.healthchat.ch/desktop-releases/$VERSION/ — the live feed is UNCHANGED. Run 'Deploy HealthChat Desktop' to publish it."
# The Release carries no installer binaries (see "Ensure GitHub Release exists"), so # 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 # 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 # 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. # artifacts. They resolve once "Deploy HealthChat Desktop" has published this version.
- name: Link public downloads in the Release notes - name: Link public downloads in the Release notes
if: startsWith(github.ref, 'refs/tags/')
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
@@ -3,23 +3,24 @@ name: Deploy HealthChat Desktop
# Publishes an already-released desktop build to packages.healthchat.ch/desktop/ over FTPS. # Publishes an already-released desktop build to packages.healthchat.ch/desktop/ over FTPS.
# #
# This is the DEPLOY half of the desktop pipeline, deliberately split from the release half # This is the DEPLOY half of the desktop pipeline, deliberately split from the release half
# (build-healthchat-desktop.yaml). Cutting a `healthchat-v*` tag builds, signs and attaches the # (build-healthchat-desktop.yaml). Cutting a `healthchat-v*` tag builds, signs and stages the
# installers plus a staged update-feed tarball to the GitHub Release, but never touches the live # update feed + public installers at packages.healthchat.ch/desktop-releases/<version>/, but
# feed. Nothing reaches installed apps until this workflow is dispatched by hand. # never touches the live feed. Nothing reaches installed apps until this workflow is dispatched
# by hand.
# #
# Why that split matters: the feed under .../desktop/update/ is what the Squirrel # Why that split matters: the feed under .../desktop/update/ is what the Squirrel
# auto-updater polls. Writing to it pushes an update to every installed client, so it is a # auto-updater polls. Writing to it pushes an update to every installed client, so it is a
# deployment, not a publication step — the same reason the ansible playbook's deploy is a manual # deployment, not a publication step — the same reason the ansible playbook's deploy is a manual
# dispatch rather than a release trigger. # dispatch rather than a release trigger.
# #
# The tarball is a GitHub Release asset (they do not expire, unlike workflow artifacts), so any # The versioned archive directories persist on the server, so any staged version can be
# past version can be re-published without rebuilding or re-signing — that is the rollback path. # re-published without rebuilding or re-signing — that is the rollback path.
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
description: "Version to publish (e.g. 1.5.0). Must have a healthchat-v<version> Release carrying a healthchat-desktop-feed-<version>.tar.gz asset." description: "Version to publish (e.g. 1.5.0). Must have a staged feed at packages.healthchat.ch/desktop-releases/<version>/ (produced by Build HealthChat Desktop)."
required: true required: true
type: string type: string
dry_run: dry_run:
@@ -28,8 +29,7 @@ on:
type: boolean type: boolean
default: false default: false
permissions: permissions: {} # the feed comes from the packages.healthchat.ch archive, not from GitHub
contents: read # download the Release asset
concurrency: concurrency:
# Never let two runs mirror onto the same FTP tree at once. # Never let two runs mirror onto the same FTP tree at once.
@@ -40,7 +40,8 @@ jobs:
deploy: deploy:
name: Publish update feed to packages.healthchat.ch name: Publish update feed to packages.healthchat.ch
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 # Fetches ~2 GiB from the archive and uploads it twice (canonical + legacy path).
timeout-minutes: 60
steps: steps:
- name: Resolve version - name: Resolve version
id: version id: version
@@ -59,33 +60,37 @@ jobs:
echo "tag=healthchat-v$VERSION" >> "$GITHUB_OUTPUT" echo "tag=healthchat-v$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing HealthChat Desktop $VERSION (tag healthchat-v$VERSION)" echo "Publishing HealthChat Desktop $VERSION (tag healthchat-v$VERSION)"
- name: Download update feed from the Release - name: Install lftp
run: sudo apt-get update && sudo apt-get install -y lftp
- name: Fetch staged feed from the archive
env: env:
GH_TOKEN: ${{ github.token }} FTP_SERVER: ${{ secrets.FTP_SERVER }}
GH_REPO: ${{ github.repository }} FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
TAG: ${{ steps.version.outputs.tag }} # Passed to lftp via LFTP_PASSWORD (--env-password) so it never appears on the
# command line / process list.
LFTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
VERSION: ${{ steps.version.outputs.version }} VERSION: ${{ steps.version.outputs.version }}
run: | run: |
set -euo pipefail set -euo pipefail
TARBALL="healthchat-desktop-feed-${VERSION}.tar.gz" : "${FTP_SERVER:?FTP_SERVER secret not set}"
if ! gh release view "$TAG" >/dev/null 2>&1; then : "${FTP_USERNAME:?FTP_USERNAME secret not set}"
echo "::error::No GitHub Release found for tag $TAG" : "${LFTP_PASSWORD:?FTP_PASSWORD secret not set}"
# The archive is public — probe it over HTTPS first for a clear error before
# opening the FTP session.
if ! curl -fsSI "https://packages.healthchat.ch/desktop-releases/$VERSION/update/macos/releases.json" >/dev/null 2>&1; then
echo "::error::No staged feed at https://packages.healthchat.ch/desktop-releases/$VERSION/ — run 'Build HealthChat Desktop' (workflow_dispatch, version=$VERSION) to build and stage this version, then re-run the deploy."
exit 1 exit 1
fi fi
# The feed is attached as split ${TARBALL}.part* files (the compressed feed lftp -u "$FTP_USERNAME" --env-password "$FTP_SERVER" <<LFTP
# outgrew GitHub's 2 GiB per-asset cap at 1.6.0); older releases may carry a set cmd:fail-exit true
# single-file tarball instead. Handle both. set ftp:ssl-force true
if ! gh release download "$TAG" --pattern "${TARBALL}*" --clobber; then set ftp:ssl-protect-data true
echo "::error::Release $TAG has no asset matching '${TARBALL}*'. Releases cut before the deploy split do not carry one — re-run 'Build HealthChat Desktop' for this version to produce it." set ssl:verify-certificate true
gh release view "$TAG" --json assets --jq '.assets[].name' || true mirror --parallel=4 --verbose /public_html/packages/desktop-releases/$VERSION/ feed/
exit 1 bye
fi LFTP
if compgen -G "${TARBALL}.part*" > /dev/null; then test -d feed || { echo "::error::Archive fetch produced no feed/ directory"; exit 1; }
cat "${TARBALL}".part* | tar -xzf -
else
tar -xzf "$TARBALL"
fi
test -d feed || { echo "::error::Tarball did not contain a feed/ directory"; exit 1; }
echo "--- feed to publish ---" echo "--- feed to publish ---"
find feed -type f | sort find feed -type f | sort
@@ -108,10 +113,7 @@ jobs:
fi fi
exit $fail exit $fail
- name: Install lftp - name: Publish to packages.healthchat.ch over FTPS
run: sudo apt-get update && sudo apt-get install -y lftp
- name: Publish to healthchat.ch over FTPS
env: env:
FTP_SERVER: ${{ secrets.FTP_SERVER }} FTP_SERVER: ${{ secrets.FTP_SERVER }}
FTP_USERNAME: ${{ secrets.FTP_USERNAME }} FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
+11 -7
View File
@@ -48,10 +48,12 @@ feeds it into build_desktop_prepare.yaml (`config: element.io/healthchat`,
`webapp-artifact: healthchat-webapp`) and the per-platform build_desktop_{macos,windows}.yaml `webapp-artifact: healthchat-webapp`) and the per-platform build_desktop_{macos,windows}.yaml
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. The `release`
`release` job attaches the staged update feed as split healthchat-desktop-feed-<ver>.tar.gz.part* job uploads the staged update feed + installers to the versioned archive at
assets (the compressed feed exceeds GitHub's 2 GiB per-asset cap) and links the public installer packages.healthchat.ch/desktop-releases/<ver>/ (also on workflow_dispatch — the recovery path
URLs on packages.healthchat.ch in the Release notes — installer binaries are not attached. for a missing archive) and, on tag pushes only, links the public installer URLs in the Release
notes. Nothing binary is attached to the GitHub Release (the compressed feed outgrew GitHub's
2 GiB per-asset cap at 1.6.0).
(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.)
@@ -115,9 +117,11 @@ subdomain's docroot on the healthchat.ch FTP account is /public_html/packages):
install/macos/HealthChat-<ver>.dmg + HealthChat.dmg (stable "latest") install/macos/HealthChat-<ver>.dmg + HealthChat.dmg (stable "latest")
install/win32/<arch>/HealthChat-Setup-<ver>.exe + HealthChat-Setup.exe (stable) + *.msi install/win32/<arch>/HealthChat-Setup-<ver>.exe + HealthChat-Setup.exe (stable) + *.msi
The `release` job in build-healthchat-desktop.yaml stages these trees; the manually dispatched The `release` job in build-healthchat-desktop.yaml stages these trees and uploads them to the
deploy-healthchat-desktop.yaml uploads them via lftp/FTPS (mirror -R, no-delete so prior nupkgs versioned archive desktop-releases/<ver>/ (sibling of desktop/ under /public_html/packages);
survive for delta history). Add these repo secrets: the manually dispatched deploy-healthchat-desktop.yaml mirrors that archive onto the live
desktop/ tree 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, 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) flip ssl:verify-certificate to no in the workflow)
FTP_USERNAME FTP_USERNAME