Skip to content

add signing for macos #18

add signing for macos

add signing for macos #18

Workflow file for this run

name: Release
on:
push:
branches: [main]
workflow_dispatch:
# Changesets needs to push the version commit/tag and open the version PR;
# the binary job needs to upload an asset to the release.
permissions:
contents: write
pull-requests: write
# Don't run two releases at once, but let an in-progress one finish.
concurrency:
group: release
cancel-in-progress: false
jobs:
# 1. Changesets: open/update the "Version Packages" PR, or — once that PR is
# merged — tag the new version and create the GitHub Release.
release:
runs-on: ubuntu-latest
outputs:
published: ${{ steps.changesets.outputs.published }}
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install release tooling
# Only Changesets is needed here; skip zero-native's postinstall (the
# native CLI download) — the binary jobs do a full install for that.
run: npm install --ignore-scripts
- name: Create version PR or tag a release
id: changesets
uses: changesets/action@v1
with:
version: npm run version
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 2. Build the macOS app and attach it to the release Changesets just created.
binary-macos:
needs: release
if: needs.release.outputs.published == 'true'
runs-on: macos-latest
# Signing + notarization secrets. When unset, the signing steps below are
# skipped and the build ships unsigned (first launch needs right-click →
# Open). Set all of them to ship a signed, notarized .dmg with no Gatekeeper
# prompt. See "Code signing" in README.md for how to obtain each.
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
APPLE_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
APPLE_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Install dependencies (zero-native framework + CLI)
run: |
npm install
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
- name: Resolve the released version
id: ver
run: |
VER=$(echo '${{ needs.release.outputs.publishedPackages }}' | jq -r '.[0].version')
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "tag=keyparty@$VER" >> "$GITHUB_OUTPUT"
- name: Build & package the macOS app
run: |
zig build package \
-Doptimize=ReleaseFast \
-Dpackage-target=macos
- name: Import Developer ID certificate
if: ${{ env.MACOS_CERTIFICATE != '' }}
run: |
KEYCHAIN="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PWD="$(openssl rand -base64 24)"
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
security set-keychain-settings -lut 3600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" \
-P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
# Put the temp keychain on the search list so codesign finds the identity.
security list-keychains -d user -s "$KEYCHAIN" \
$(security list-keychains -d user | sed s/\"//g)
rm -f "$RUNNER_TEMP/cert.p12"
- name: Sign the app (Developer ID + Hardened Runtime)
if: ${{ env.MACOS_CERTIFICATE != '' }}
run: |
APP="$(ls -d zig-out/package/*.app | head -n1)"
codesign --force --deep --options runtime --timestamp \
--entitlements assets/keyparty.entitlements \
--sign "$MACOS_SIGN_IDENTITY" "$APP"
codesign --verify --strict --verbose=2 "$APP"
- name: Build a .dmg (drag-to-Applications)
run: |
APP="$(ls -d zig-out/package/*.app | head -n1)"
# Stage the bundle under its display name with an Applications shortcut,
# so the mounted disk image is a familiar drag-to-install window.
STAGE="dmg-stage"
rm -rf "$STAGE" && mkdir -p "$STAGE"
cp -R "$APP" "$STAGE/Key Party.app"
ln -s /Applications "$STAGE/Applications"
hdiutil create -volname "Key Party" -srcfolder "$STAGE" -ov -format UDZO "keyparty-macos.dmg"
- name: Sign & notarize the .dmg
if: ${{ env.MACOS_CERTIFICATE != '' }}
run: |
codesign --force --timestamp --sign "$MACOS_SIGN_IDENTITY" "keyparty-macos.dmg"
if [ -n "$APPLE_NOTARY_KEY" ]; then
echo "$APPLE_NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8"
# --wait blocks until Apple finishes; staple embeds the ticket so the
# .dmg launches offline with no Gatekeeper prompt.
xcrun notarytool submit "keyparty-macos.dmg" \
--key "$RUNNER_TEMP/notary.p8" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_ISSUER" \
--wait
xcrun stapler staple "keyparty-macos.dmg"
rm -f "$RUNNER_TEMP/notary.p8"
else
echo "::warning::APPLE_NOTARY_KEY unset — .dmg is signed but NOT notarized; Gatekeeper will still warn."
fi
- name: Attach the .dmg to the release
# Version-less name -> https://github.com/<owner>/<repo>/releases/latest/download/keyparty-macos.dmg
# is a stable link the web build can point at.
run: gh release upload "${{ steps.ver.outputs.tag }}" "keyparty-macos.dmg" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 3. Build the Windows app (full-screen kiosk WebView2 host) and attach it.
binary-windows:
needs: release
if: needs.release.outputs.published == 'true'
runs-on: windows-latest
env:
# Where the WebView2 SDK headers land (NuGet, version stripped).
NUGET_DIR: packages/Microsoft.Web.WebView2
# Code signing is scaffolded but inert. Set this secret AND drop a real
# signing command into the "Sign the Windows exe" step to enable it (see
# "Code signing" in README.md). Until then the exe ships unsigned.
WINDOWS_SIGN_CERT: ${{ secrets.WINDOWS_SIGN_CERT }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
# Sets the MSVC environment (INCLUDE, WindowsSdkDir, WindowsSdkVersion, …)
# so Zig's msvc target finds the toolchain and we can locate the winrt
# headers (wrl.h) the WebView2 host needs.
- uses: ilammy/msvc-dev-cmd@v1
- name: Install dependencies (zero-native framework + CLI)
shell: bash
run: |
npm install
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
- name: Fetch the WebView2 SDK headers + loader
shell: bash
run: nuget install Microsoft.Web.WebView2 -OutputDirectory packages -ExcludeVersion
- name: Resolve the released version
id: ver
shell: bash
run: |
VER=$(echo '${{ needs.release.outputs.publishedPackages }}' | jq -r '.[0].version')
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "tag=keyparty@$VER" >> "$GITHUB_OUTPUT"
- name: Build the single-file Windows exe
shell: bash
run: |
WV2_INC="$GITHUB_WORKSPACE/$NUGET_DIR/build/native/include"
WV2_LIB="$GITHUB_WORKSPACE/$NUGET_DIR/build/native/x64"
echo "WebView2 include: $WV2_INC"
echo "WebView2 lib: $WV2_LIB"
# Fail loudly if the SDK pieces are missing: without WebView2.h the host
# silently compiles to a blank-window stub; without the static loader lib
# it can't link a self-contained exe.
test -f "$WV2_INC/WebView2.h" || { echo "ERROR: WebView2.h not found at $WV2_INC"; ls -R "$GITHUB_WORKSPACE/$NUGET_DIR" 2>/dev/null | head -40; exit 1; }
test -f "$WV2_LIB/WebView2LoaderStatic.lib" || { echo "ERROR: WebView2LoaderStatic.lib not found at $WV2_LIB"; exit 1; }
# The frontend is embedded into the exe (served from memory) and the
# WebView2 loader is static-linked, so the release artifact is a single
# self-contained keyparty.exe. cl.exe finds the Windows SDK + winrt via
# %INCLUDE%.
zig build \
-Dtarget=x86_64-windows-msvc \
-Doptimize=ReleaseFast \
-Dwebview2-include="$WV2_INC" \
-Dwebview2-lib-dir="$WV2_LIB"
test -f zig-out/bin/keyparty.exe || { echo "ERROR: no exe produced"; ls -R zig-out; exit 1; }
# Version-less name -> .../releases/latest/download/keyparty-windows.exe is stable.
cp zig-out/bin/keyparty.exe "keyparty-windows.exe"
- name: Sign the Windows exe (scaffold — skipped until a cert is configured)
# Inert today (no WINDOWS_SIGN_CERT secret). Public-trust Windows certs now
# require hardware/cloud key storage, so plug your provider in here:
# • Microsoft Trusted Signing: azure/trusted-signing-action@v0
# • DigiCert KeyLocker / SSL.com eSigner: the provider's signtool dlib
# • signtool: signtool sign /fd SHA256 /tr <timestamp-url> /td SHA256 keyparty-windows.exe
# Replace the echo with the real command; the signed exe then flows through
# to the existing upload step unchanged.
if: ${{ env.WINDOWS_SIGN_CERT != '' }}
shell: bash
run: |
echo "TODO: sign keyparty-windows.exe (see step comment for provider options)."
- name: Attach the binary to the release
shell: bash
run: gh release upload "${{ steps.ver.outputs.tag }}" "keyparty-windows.exe" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}