The desktop DMG's volume icon defaulted to the packager's icns, which is actool's 256px fallback whenever mac.icon is the Icon Composer package, and dmgbuild's badge option can only paste onto the stock removable-drive icon. Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png (hand-made, one artwork since volume icons have no appearance variants) is emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background so the `files` whitelist keeps it out of the app bundle, and dmg.icon points at it. Tauri always uses the app icns as the volume icon and offers no override, so the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the built DMG before signing (the same .VolumeIcon.icns + custom-icon flag dmgbuild sets), and the verify step checks the icon is on the mounted image. Verified on macOS 26.6.2: both DMGs render the identical volume icon.
287 lines
12 KiB
YAML
287 lines
12 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@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # 7.0.0
|
|
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@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # 7.0.0
|
|
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: Select Xcode 26 for the macOS 26 layered icon
|
|
# Tauri's bundler compiles src-tauri/icons/icon.icon to Assets.car with
|
|
# actool, which only Xcode >= 26 ships; on an older Xcode it logs an
|
|
# error and ships the .icns alone. The runner keeps an older Xcode
|
|
# selected, so pick 26 and assert it.
|
|
run: |
|
|
xcode="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -1)"
|
|
if [ -z "$xcode" ]; then
|
|
echo "::error::no Xcode 26 on this runner; the macOS 26 layered icon cannot be compiled"
|
|
exit 1
|
|
fi
|
|
echo "DEVELOPER_DIR=$xcode/Contents/Developer" >> "$GITHUB_ENV"
|
|
export DEVELOPER_DIR="$xcode/Contents/Developer"
|
|
version="$(actool --version | python3 -c 'import plistlib, sys; print(plistlib.loads(sys.stdin.buffer.read())["com.apple.actool.version"]["short-bundle-version"])')"
|
|
echo "actool $version from $xcode"
|
|
case "$version" in
|
|
2[6-9].*|[3-9][0-9].*) ;;
|
|
*) echo "::error::actool $version cannot compile .icon packages (need >= 26)"; exit 1 ;;
|
|
esac
|
|
|
|
- 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"
|
|
# Tauri always uses the app icns as the volume icon; give the DMG the
|
|
# same drive artwork as the desktop installer before signing.
|
|
scripts/bundles/set_dmg_volume_icon.sh "${dmgs[0]}" apps/desktop/packaging/dmg-volume.icns "$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]}"
|
|
# The macOS 26 layered icon: Tauri only logs when actool is missing,
|
|
# so prove the catalog and its plist key made it into the bundle.
|
|
test -f "${apps[0]}/Contents/Resources/Assets.car"
|
|
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "${apps[0]}/Contents/Info.plist")" = "Icon"
|
|
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"
|
|
mount=$(hdiutil attach -nobrowse -readonly "$dmg" | awk -F'\t' '/\/Volumes\//{print $NF; exit}')
|
|
test -f "$mount/.VolumeIcon.icns"
|
|
hdiutil detach "$mount"
|
|
|
|
- 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
|