From 13cd98b06b228ecebca64a75b54fc293e33b1f95 Mon Sep 17 00:00:00 2001 From: ethernet Date: Thu, 24 Sep 2026 19:39:19 -0400 Subject: [PATCH] ci: dispatch-only signed builds of the bootstrap installer 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. --- .../workflows/bootstrap-installer-build.yml | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 .github/workflows/bootstrap-installer-build.yml diff --git a/.github/workflows/bootstrap-installer-build.yml b/.github/workflows/bootstrap-installer-build.yml new file mode 100644 index 0000000000..e0b4e48e79 --- /dev/null +++ b/.github/workflows/bootstrap-installer-build.yml @@ -0,0 +1,257 @@ +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