mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-10-31 08:15:07 +00:00
ci: split desktop feed publish into a manual deploy workflow
This commit is contained in:
@@ -10,6 +10,10 @@ name: Build HealthChat Desktop
|
||||
# (via `webapp-artifact`) and the per-platform build_desktop_{macos,windows}.yaml reusables.
|
||||
#
|
||||
# Notes:
|
||||
# - This workflow RELEASES but does not DEPLOY. It attaches the installers and a staged update-feed
|
||||
# tarball to the GitHub Release; the live auto-update feed at healthchat.ch/packages/desktop/ is
|
||||
# only touched by 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
|
||||
# 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
|
||||
@@ -269,9 +273,11 @@ jobs:
|
||||
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/).
|
||||
# Squirrel auto-updater. Those are served from healthchat.ch/packages/desktop/, which is
|
||||
# published by the SEPARATE, MANUALLY DISPATCHED "Deploy HealthChat Desktop" workflow
|
||||
# (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
|
||||
# deploy workflow can publish any past version without rebuilding or re-signing.
|
||||
- name: Stage update feed + public installers
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -328,31 +334,21 @@ jobs:
|
||||
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
|
||||
# Attach the staged feed to the Release instead of publishing it. "Deploy HealthChat
|
||||
# Desktop" (deploy-healthchat-desktop.yaml) downloads this tarball and mirrors it to
|
||||
# healthchat.ch when a human decides to ship. Release assets do not expire, so any past
|
||||
# version stays redeployable long after the workflow artifacts are gone.
|
||||
- name: Attach update feed to the GitHub Release
|
||||
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 }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
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
|
||||
VERSION="${GITHUB_REF_NAME#healthchat-v}"
|
||||
TARBALL="healthchat-desktop-feed-${VERSION}.tar.gz"
|
||||
# Store paths relative to feed/ so the deploy side extracts a bare feed/ tree.
|
||||
tar -czf "$TARBALL" feed
|
||||
ls -lh "$TARBALL"
|
||||
gh release upload "$TAG" "$TARBALL" --clobber
|
||||
echo "::notice::Update feed attached as ${TARBALL}. The live feed at healthchat.ch is UNCHANGED — run the 'Deploy HealthChat Desktop' workflow to publish it."
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
name: Deploy HealthChat Desktop
|
||||
|
||||
# Publishes an already-released desktop build to healthchat.ch/packages/desktop/ over FTPS.
|
||||
#
|
||||
# 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
|
||||
# installers plus a staged update-feed tarball to the GitHub Release, but never touches the live
|
||||
# feed. Nothing reaches installed apps until this workflow is dispatched by hand.
|
||||
#
|
||||
# Why that split matters: the feed under .../packages/desktop/update/ is what the Squirrel
|
||||
# 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
|
||||
# dispatch rather than a release trigger.
|
||||
#
|
||||
# The tarball is a GitHub Release asset (they do not expire, unlike workflow artifacts), so any
|
||||
# past version can be re-published without rebuilding or re-signing — that is the rollback path.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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."
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Dry run — list what would be uploaded without writing to the server."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read # download the Release asset
|
||||
|
||||
concurrency:
|
||||
# Never let two runs mirror onto the same FTP tree at once.
|
||||
group: deploy-healthchat-desktop
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Publish update feed to healthchat.ch
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Resolve version
|
||||
id: version
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="$INPUT_VERSION"
|
||||
# Same regex as the release workflows, so -rc1/-hotfix1 builds can be published
|
||||
# deliberately (e.g. to a test machine) but never by accident.
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "::error::Invalid version '$VERSION' — expected e.g. 1.5.0 or 1.5.0-rc1"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=healthchat-v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing HealthChat Desktop $VERSION (tag healthchat-v$VERSION)"
|
||||
|
||||
- name: Download update feed from the Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="healthchat-desktop-feed-${VERSION}.tar.gz"
|
||||
if ! gh release view "$TAG" >/dev/null 2>&1; then
|
||||
echo "::error::No GitHub Release found for tag $TAG"
|
||||
exit 1
|
||||
fi
|
||||
if ! gh release download "$TAG" --pattern "$TARBALL" --clobber; then
|
||||
echo "::error::Release $TAG has no asset '$TARBALL'. Releases cut before the deploy split do not carry one — re-run 'Build HealthChat Desktop' for this version to produce it."
|
||||
gh release view "$TAG" --json assets --jq '.assets[].name' || true
|
||||
exit 1
|
||||
fi
|
||||
tar -xzf "$TARBALL"
|
||||
test -d feed || { echo "::error::Tarball did not contain a feed/ directory"; exit 1; }
|
||||
echo "--- feed to publish ---"
|
||||
find feed -type f | sort
|
||||
|
||||
- name: Sanity-check the feed before publishing
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# A partial feed is worse than none: Squirrel clients poll these paths, and a
|
||||
# missing manifest breaks auto-update for everyone already installed.
|
||||
fail=0
|
||||
for f in feed/update/macos/releases.json \
|
||||
feed/update/win32/x64/RELEASES \
|
||||
feed/update/win32/arm64/RELEASES; do
|
||||
if [[ ! -s "$f" ]]; then echo "::error::missing or empty: $f"; fail=1; fi
|
||||
done
|
||||
# The macOS updater fetches the zip named in releases.json; make sure it is present.
|
||||
if [[ -s feed/update/macos/releases.json ]]; then
|
||||
if ! ls feed/update/macos/*-mac.zip >/dev/null 2>&1; then
|
||||
echo "::error::releases.json present but no *-mac.zip payload alongside it"; fail=1
|
||||
fi
|
||||
fi
|
||||
exit $fail
|
||||
|
||||
- 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 }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
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_OPTS="--parallel=4 --verbose"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "::notice::DRY RUN — no files will be written to the server."
|
||||
MIRROR_OPTS="$MIRROR_OPTS --dry-run"
|
||||
fi
|
||||
# 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 $MIRROR_OPTS feed/ /public_html/healthchat.ch/packages/desktop/
|
||||
bye
|
||||
LFTP
|
||||
|
||||
- name: Verify published feed
|
||||
if: ${{ !inputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base="https://healthchat.ch/packages/desktop"
|
||||
fail=0
|
||||
for url in "$base/update/macos/releases.json" \
|
||||
"$base/update/win32/x64/RELEASES" \
|
||||
"$base/update/win32/arm64/RELEASES" \
|
||||
"$base/install/macos/HealthChat.dmg" \
|
||||
"$base/install/win32/x64/HealthChat-Setup.exe"; do
|
||||
code=$(curl -fsS -o /dev/null -w '%{http_code}' -I "$url" 2>/dev/null || echo "000")
|
||||
if [[ "$code" == "200" ]]; then echo "ok $code $url"; else echo "FAIL $code $url"; fail=1; fi
|
||||
done
|
||||
if [[ $fail -ne 0 ]]; then
|
||||
echo "::error::One or more feed URLs are not reachable — auto-update may be broken."
|
||||
exit 1
|
||||
fi
|
||||
echo "::notice::Update feed for ${{ steps.version.outputs.version }} is live. Installed apps will pick it up on their next update check."
|
||||
Reference in New Issue
Block a user