Hermes-Setup has never had a CI build; every published copy was built by hand. This workflow_dispatch lane builds the Windows x64 exe (signed via the desktop MSIX's Azure Trusted Signing path, batch-sign-binaries.mjs) and the macOS arm64 dmg (Developer ID signed, app + dmg notarized and stapled) from any ref, and uploads them as run artifacts. Nothing is published. No caches: no actions/cache or setup-node cache, and fresh npm/cargo/electron-builder cache dirs.
258 lines
11 KiB
YAML
258 lines
11 KiB
YAML
name: Bootstrap installer build
|
|
|
|
# Hand-dispatched, signed builds of the Tauri bootstrap installer
|
|
# (apps/bootstrap-installer, "Hermes-Setup"): Windows x64 exe signed with
|
|
# Azure Trusted Signing, macOS arm64 dmg signed with the Developer ID cert and
|
|
# notarized. Uploaded as run artifacts only; nothing is published.
|
|
#
|
|
# No caches of any kind: no actions/cache, no setup-node cache, and fresh
|
|
# npm/cargo/electron-builder cache dirs under RUNNER_TEMP.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: Commit, branch or tag to build
|
|
required: true
|
|
default: main
|
|
pin_branch:
|
|
description: Branch the installer follows at install time (HERMES_BUILD_PIN_BRANCH)
|
|
required: true
|
|
default: main
|
|
pin_commit:
|
|
description: Optional commit pin baked in instead (HERMES_BUILD_PIN_COMMIT)
|
|
required: false
|
|
default: ''
|
|
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
|
|
concurrency:
|
|
group: bootstrap-installer-build-${{ github.run_id }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
HERMES_BUILD_PIN_BRANCH: ${{ inputs.pin_branch }}
|
|
HERMES_BUILD_PIN_COMMIT: ${{ inputs.pin_commit }}
|
|
CARGO_INCREMENTAL: '0'
|
|
SCCACHE_DISABLE: '1'
|
|
|
|
jobs:
|
|
windows-x64:
|
|
name: Hermes-Setup.exe (windows x64, signed)
|
|
runs-on: windows-latest
|
|
environment: release-signing
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 24.11.1
|
|
|
|
- name: Fresh cache dirs
|
|
shell: bash
|
|
run: |
|
|
{
|
|
echo "CARGO_HOME=$RUNNER_TEMP/cargo-home"
|
|
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/target"
|
|
echo "npm_config_cache=$RUNNER_TEMP/npm-cache"
|
|
echo "ELECTRON_BUILDER_CACHE=$RUNNER_TEMP/electron-builder-cache"
|
|
} >> "$GITHUB_ENV"
|
|
echo "$USERPROFILE/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
- name: Require Azure signing vars
|
|
shell: bash
|
|
env:
|
|
AZURE_SIGN_ENDPOINT: ${{ vars.AZURE_SIGN_ENDPOINT }}
|
|
AZURE_SIGN_ACCOUNT: ${{ vars.AZURE_SIGN_ACCOUNT }}
|
|
AZURE_SIGN_PROFILE: ${{ vars.AZURE_SIGN_PROFILE }}
|
|
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
|
|
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
|
|
run: |
|
|
missing=()
|
|
for name in AZURE_SIGN_ENDPOINT AZURE_SIGN_ACCOUNT AZURE_SIGN_PROFILE AZURE_CLIENT_ID AZURE_TENANT_ID; do
|
|
[ -z "${!name}" ] && missing+=("$name")
|
|
done
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
echo "::error::Azure Trusted Signing vars missing from release-signing: ${missing[*]}"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Build
|
|
shell: bash
|
|
run: |
|
|
rustup target add x86_64-pc-windows-msvc
|
|
npm ci --workspace apps/bootstrap-installer --include-workspace-root=false --prefer-online --no-audit --no-fund
|
|
cd apps/bootstrap-installer
|
|
npx tauri build --target x86_64-pc-windows-msvc --no-bundle
|
|
exe="$CARGO_TARGET_DIR/x86_64-pc-windows-msvc/release/Hermes-Setup.exe"
|
|
mkdir -p "$RUNNER_TEMP/out"
|
|
cp "$exe" "$RUNNER_TEMP/out/Hermes-Setup.exe"
|
|
|
|
- name: Azure login (OIDC)
|
|
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
|
with:
|
|
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
|
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
|
allow-no-subscriptions: true
|
|
|
|
- name: Mint federated token for the signing dlib
|
|
shell: bash
|
|
run: |
|
|
file="$RUNNER_TEMP/azure-federated-token"
|
|
mint() {
|
|
curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
|
|
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" \
|
|
| jq -r .value > "$file.tmp" && mv -f "$file.tmp" "$file"
|
|
}
|
|
mint
|
|
( while sleep 240; do mint || true; done ) &
|
|
echo "AZURE_FEDERATED_TOKEN_FILE=$file" >> "$GITHUB_ENV"
|
|
|
|
- name: Sign (same Azure Trusted Signing path as the desktop MSIX)
|
|
shell: bash
|
|
env:
|
|
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
|
AZURE_SIGN_ENDPOINT: ${{ vars.AZURE_SIGN_ENDPOINT }}
|
|
AZURE_SIGN_ACCOUNT: ${{ vars.AZURE_SIGN_ACCOUNT }}
|
|
AZURE_SIGN_PROFILE: ${{ vars.AZURE_SIGN_PROFILE }}
|
|
AZURE_SIGN_PUBLISHER: ${{ vars.AZURE_SIGN_PUBLISHER }}
|
|
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
|
|
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
|
|
AZURE_TOKEN_CREDENTIALS: prod
|
|
run: |
|
|
npm ci --workspace apps/desktop --include-workspace-root=false --prefer-online --no-audit --no-fund
|
|
# shellcheck disable=SC2016 # JS template literal, not shell
|
|
EXE="$(cygpath -w "$RUNNER_TEMP/out/Hermes-Setup.exe")" node --input-type=module -e '
|
|
import { pathToFileURL } from "node:url"
|
|
const m = await import(pathToFileURL("apps/desktop/scripts/batch-sign-binaries.mjs").href)
|
|
const r = await m.batchSignBinaries([process.env.EXE], { cache: null })
|
|
if (r.skipped || r.signed !== 1) throw new Error(`not signed: ${JSON.stringify(r)}`)
|
|
'
|
|
|
|
- name: Verify signature
|
|
shell: pwsh
|
|
run: |
|
|
$sig = Get-AuthenticodeSignature "$env:RUNNER_TEMP\out\Hermes-Setup.exe"
|
|
$sig | Format-List Status, StatusMessage, SignerCertificate, TimeStamperCertificate
|
|
if ($sig.Status -ne 'Valid') { throw "signature status: $($sig.Status)" }
|
|
if (-not $sig.TimeStamperCertificate) { throw "signature is not timestamped" }
|
|
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: Hermes-Setup-windows-x64
|
|
path: ${{ runner.temp }}/out/Hermes-Setup.exe
|
|
if-no-files-found: error
|
|
|
|
macos-arm64:
|
|
name: Hermes-Setup.dmg (macos arm64, signed + notarized)
|
|
runs-on: macos-latest
|
|
environment: release-signing
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 24.11.1
|
|
|
|
- name: Fresh cache dirs
|
|
run: |
|
|
{
|
|
echo "CARGO_HOME=$RUNNER_TEMP/cargo-home"
|
|
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/target"
|
|
echo "npm_config_cache=$RUNNER_TEMP/npm-cache"
|
|
} >> "$GITHUB_ENV"
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
- name: Import Developer ID certificate
|
|
env:
|
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
run: |
|
|
missing=()
|
|
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_API_KEY_P8 APPLE_API_KEY_ID APPLE_API_ISSUER; do
|
|
[ -z "${!name}" ] && missing+=("$name")
|
|
done
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
echo "::error::signing/notarization secrets missing from release-signing: ${missing[*]}"
|
|
exit 1
|
|
fi
|
|
p12="$RUNNER_TEMP/cert.p12"
|
|
printf '%s' "$CSC_LINK" | base64 --decode > "$p12"
|
|
kc="$RUNNER_TEMP/signing.keychain-db"
|
|
pw="$(uuidgen)"
|
|
security create-keychain -p "$pw" "$kc"
|
|
security set-keychain-settings -lut 21600 "$kc"
|
|
security unlock-keychain -p "$pw" "$kc"
|
|
security import "$p12" -k "$kc" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$kc" >/dev/null
|
|
# shellcheck disable=SC2046 # the existing search list must split
|
|
security list-keychains -d user -s "$kc" $(security list-keychains -d user | tr -d '"')
|
|
rm -f "$p12"
|
|
identity="$(security find-identity -v -p codesigning "$kc" | grep -o '"Developer ID Application: [^"]*"' | head -1 | tr -d '"')"
|
|
test -n "$identity" || { echo "::error::no Developer ID Application identity in CSC_LINK"; exit 1; }
|
|
printf '%s\n' "$APPLE_API_KEY_P8" > "$RUNNER_TEMP/apple-api-key.p8"
|
|
{
|
|
echo "APPLE_SIGNING_IDENTITY=$identity"
|
|
echo "APPLE_API_KEY_PATH=$RUNNER_TEMP/apple-api-key.p8"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Build, sign and notarize
|
|
env:
|
|
# Tauri notarizes the .app with an App Store Connect API key.
|
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
run: |
|
|
rustup target add aarch64-apple-darwin
|
|
npm ci --workspace apps/bootstrap-installer --include-workspace-root=false --prefer-online --no-audit --no-fund
|
|
cd apps/bootstrap-installer
|
|
npx tauri build --target aarch64-apple-darwin --bundles app,dmg
|
|
|
|
- name: Sign, notarize and staple the dmg
|
|
run: |
|
|
shopt -s nullglob
|
|
bundle="$CARGO_TARGET_DIR/aarch64-apple-darwin/release/bundle"
|
|
dmgs=("$bundle"/dmg/*.dmg)
|
|
test ${#dmgs[@]} -eq 1
|
|
mkdir -p "$RUNNER_TEMP/out"
|
|
dmg="$RUNNER_TEMP/out/Hermes-Setup.dmg"
|
|
cp "${dmgs[0]}" "$dmg"
|
|
codesign --force --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$dmg"
|
|
xcrun notarytool submit "$dmg" --wait \
|
|
--key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER"
|
|
xcrun stapler staple "$dmg"
|
|
env:
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
|
|
- name: Verify the app and dmg are signed and notarized
|
|
run: |
|
|
shopt -s nullglob
|
|
apps=("$CARGO_TARGET_DIR"/aarch64-apple-darwin/release/bundle/macos/*.app)
|
|
test ${#apps[@]} -eq 1
|
|
codesign --verify --strict --verbose=2 "${apps[0]}"
|
|
xcrun stapler validate "${apps[0]}"
|
|
spctl -a -vv -t exec "${apps[0]}"
|
|
dmg="$RUNNER_TEMP/out/Hermes-Setup.dmg"
|
|
codesign --verify --verbose=2 "$dmg"
|
|
xcrun stapler validate "$dmg"
|
|
spctl -a -vv -t open --context context:primary-signature "$dmg"
|
|
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: Hermes-Setup-macos-arm64
|
|
path: ${{ runner.temp }}/out/Hermes-Setup.dmg
|
|
if-no-files-found: error
|