Files
healthchat-web/.github/workflows/build_desktop_macos.yaml
T
joel.poyet f6925dbd77 feat(desktop): build signed Windows + macOS apps in healthchat pipeline
Extend the HealthChat desktop pipeline beyond Linux to produce fully branded,
signed Windows (.exe/.msi) and macOS (.dmg/.zip) installers, published to the
GitHub Release on the healthchat-v* tag alongside the web build.

- Add HealthChat desktop icons (icon.png/ico + Apple Icon Composer bundle),
  derived from apps/web/res/vector-icons/1024.png
- Point the reusable build workflows at this fork (new repository/ref inputs)
  so the Electron shell, icons and healthchat:// handler are fork-branded
- Relax hardcoded Element*/element-desktop* filename asserts to be
  product-name-agnostic
- Wire signed macOS + Windows jobs into build-healthchat-desktop.yaml, trigger
  it on healthchat-v* tags, and add a release job that uploads installers
- Map the org-level eSigner secrets to the CKA names the reusable expects
- Document signing setup (Apple env, eSigner mapping, ESIGNER_MODE=prod) in the
  desktop README and releasing-healthchat skill
2026-06-26 15:10:38 +02:00

239 lines
10 KiB
YAML

# This workflow relies on actions/cache to store the hak dependency artifacts as they take a long time to build
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
on:
workflow_call:
secrets:
APPLE_ID:
required: false
APPLE_ID_PASSWORD:
required: false
APPLE_CSC_KEY_PASSWORD:
required: false
APPLE_CSC_LINK:
required: false
inputs:
repository:
type: string
required: false
description: "The repository to checkout the desktop source from, defaults to element-hq/element-web"
ref:
type: string
required: false
description: "The git ref to checkout, defaults to the default branch"
version:
type: string
required: false
description: "Version string to override the one in package.json, used for non-release builds"
sign:
type: string
required: false
description: "Whether to sign & notarise the build, requires 'Desktop Apple' environment"
base-url:
type: string
required: false
description: "The URL to which the output will be deployed."
blob_report:
type: boolean
required: false
description: "Whether to run the blob report"
prepare-artifact-name:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
+ hakHash - the hash of the .hak directory to use for cache keying
+ variant.json - the variant configuration to use for the build
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "desktop-prepare"
test:
type: boolean
required: false
default: true
description: "Whether to run the test stage after building"
test-args:
type: string
required: false
description: "Additional arguments to pass to playwright"
artifact-prefix:
type: string
required: false
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
default: ""
targets:
type: string
required: false
description: "List of targets to build"
default: "dmg zip"
permissions: {} # No permissions required
jobs:
build:
name: Build macOS Universal
runs-on: macos-26 # M1
environment: ${{ inputs.sign && 'Desktop Apple' || '' }}
steps:
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
xcode-version: latest-stable
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ inputs.repository || 'element-hq/element-web' }}
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.prepare-artifact-name }}
path: apps/desktop
- name: Cache .hak
id: cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
key: ${{ runner.os }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }}
path: |
apps/desktop/.hak
- name: Install Rust
if: steps.cache.outputs.cache-hit != 'true'
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
# M1 macos-14 comes without Python preinstalled
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
# Install Quartz for DMG badges
# https://github.com/electron-userland/electron-builder/issues/9511#issuecomment-3774092888
- run: sudo pip3 install pyobjc-framework-Quartz
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Build Natives
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop
run: pnpm run build:native:universal
# We split these because electron-builder gets upset if we set CSC_LINK even to an empty string
- name: "[Signed] Build App"
if: inputs.sign != ''
working-directory: apps/desktop
run: |
pnpm run build:universal --publish never -m ${TARGETS}
env:
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
VARIANT_PATH: variant.json
# Only set for Nightly builds
VERSION: ${{ inputs.version }}
TARGETS: ${{ inputs.targets }}
- name: Check app was signed & notarised successfully
if: inputs.sign != ''
working-directory: apps/desktop
run: |
hdiutil attach dist/*.dmg -mountpoint /Volumes/Element
codesign -dv --verbose=4 /Volumes/Element/*.app
spctl -a -vvv -t install /Volumes/Element/*.app
hdiutil detach /Volumes/Element
- name: "[Unsigned] Build App"
if: inputs.sign == ''
working-directory: apps/desktop
run: |
pnpm run build:universal --publish never -m ${TARGETS}
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
VARIANT_PATH: variant.json
TARGETS: ${{ inputs.targets }}
- name: Generate releases.json
if: inputs.base-url
working-directory: apps/desktop
run: |
PKG_JSON_VERSION=$(cat package.json | jq -r .version)
LATEST=$(find dist -type f -iname "*-mac.zip" | xargs -0 -n1 -- basename)
# Encode spaces in the URL as Squirrel.Mac complains about bad JSON otherwise
URL="${BASE_URL}/update/macos/${LATEST// /%20}"
jq -n --arg version "${VERSION:-$PKG_JSON_VERSION}" --arg url "$URL" '
{
currentRelease: $version,
releases: [{
version: $version,
updateTo: {
version: $version,
url: $url,
},
}],
}
' > dist/releases.json
jq -n --arg url "$URL" '
{ url: $url }
' > dist/releases-legacy.json
env:
VERSION: ${{ inputs.version }}
BASE_URL: ${{ inputs.base-url }}
# We exclude mac-universal as the unpacked app takes forever to upload and zip and dmg already contains it
- name: Upload Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.artifact-prefix }}macos
path: |
apps/desktop/dist
!apps/desktop/dist/mac-universal/**
retention-days: 1
- name: Assert zip is present
if: contains(inputs.targets, 'zip')
working-directory: apps/desktop
run: |
test -f ./dist/*-mac.zip
- name: Assert dmg is present
if: contains(inputs.targets, 'dmg')
working-directory: apps/desktop
run: |
test -f ./dist/*.dmg
test:
name: Test macOS Universal
needs: build
if: inputs.test && contains(inputs.targets, 'dmg')
uses: ./.github/workflows/build_desktop_test.yaml
with:
project: macos
artifact: ${{ inputs.artifact-prefix }}macos
runs-on: macos-14
executable: /Users/runner/Applications/*.app/Contents/MacOS/*
# We need to mount the DMG and copy the app to the Applications folder as a mounted DMG is
# read-only and thus would not allow us to override the fuses as is required for Playwright.
prepare_cmd: |
hdiutil attach ./dist/*.dmg -mountpoint /Volumes/Element &&
rsync -a /Volumes/Element/*.app ~/Applications/ &&
hdiutil detach /Volumes/Element
blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}