Skip to content

feat: simpler icon-forward quit hint, drop grown-ups wording #47

feat: simpler icon-forward quit hint, drop grown-ups wording

feat: simpler icon-forward quit hint, drop grown-ups wording #47

Workflow file for this run

name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to (re)build & attach binaries to, e.g. v1.0.0. Leave blank for the normal Changesets release flow."
required: false
type: string
# 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
# Normal flow: only when Changesets published a new version. Manual flow:
# a workflow_dispatch with a `tag` input rebuilds/attaches binaries to that
# existing release without needing a publish.
if: ${{ needs.release.outputs.published == 'true' || inputs.tag != '' }}
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: |
# Manual dispatch with a tag attaches to that existing release;
# otherwise use the version Changesets just published. Changesets tags
# a single-package repo as v<version> (only monorepos use
# <name>@<version>), so the normal release tag is "v$VER".
if [ -n "${{ github.event.inputs.tag }}" ]; then
TAG="${{ github.event.inputs.tag }}"
VER="${TAG#v}"
else
VER=$(echo '${{ needs.release.outputs.publishedPackages }}' | jq -r '.[0].version')
TAG="v$VER"
fi
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Build & package the macOS app
env:
# Embedded into the frontend by Next during `zig build` (which runs
# npm run build); tags desktop telemetry as platform=macos.
NEXT_PUBLIC_GA_ID: ${{ secrets.GA_MEASUREMENT_ID }}
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"
# Diagnostics: list every imported identity (any policy) with its name —
# the name reveals the cert TYPE, which must be "Developer ID Application"
# (an Installer/Development cert won't satisfy the codesigning policy).
echo "=== All identities in the temp keychain ==="
security find-identity "$KEYCHAIN" || true
echo "=== Imported certificate (subject + validity window) ==="
security find-certificate -a -p "$KEYCHAIN" \
| openssl x509 -noout -subject -dates 2>/dev/null || true
# Gate on what codesign actually sees: a VALID codesigning identity on the
# full search list (temp keychain for the leaf+key, system keychain for the
# Apple intermediate/root the chain needs). Checking the temp keychain alone
# reports a false "0 valid" because the chain can't be built there.
echo "=== Valid code-signing identities on the search list (codesign's view) ==="
security find-identity -v -p codesigning
if [ "$(security find-identity -v -p codesigning | grep -c '"')" -eq 0 ]; then
echo "::error::No VALID code-signing identity. From the dump above, confirm the cert is a 'Developer ID Application' cert (not Installer/Development), is unexpired, and chains to Apple's intermediate. If the identity is listed but invalid, the Developer ID intermediate may be missing from the runner."
exit 1
fi
# Hand the keychain path to the signing steps so they target it explicitly.
echo "KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV"
- 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 \
--keychain "$KEYCHAIN" \
--entitlements assets/keyparty.entitlements \
--sign "$MACOS_SIGN_IDENTITY" "$APP"
codesign --verify --strict --verbose=2 "$APP"
- name: Build a .dmg (branded, drag-to-Applications)
# Same script used for local testing (scripts/make-dmg.sh), so the
# install window here is identical to what you preview on a Mac. It
# picks up the already-signed .app from zig-out/package, lays it out
# over the branded backdrop next to an Applications drop-link, and
# sets the volume icon. Signing/notarization happens in the next step.
run: scripts/make-dmg.sh
- name: Sign & notarize the .dmg
if: ${{ env.MACOS_CERTIFICATE != '' }}
run: |
codesign --force --timestamp --keychain "$KEYCHAIN" \
--sign "$MACOS_SIGN_IDENTITY" "KeyParty.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.dmg" \
--key "$RUNNER_TEMP/notary.p8" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_ISSUER" \
--wait
xcrun stapler staple "KeyParty.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.dmg
# is a stable link the web build can point at.
run: gh release upload "${{ steps.ver.outputs.tag }}" "KeyParty.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
# Normal flow: only when Changesets published a new version. Manual flow:
# a workflow_dispatch with a `tag` input rebuilds/attaches binaries to that
# existing release without needing a publish.
if: ${{ needs.release.outputs.published == 'true' || inputs.tag != '' }}
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: |
# Manual dispatch with a tag attaches to that existing release;
# otherwise use the version Changesets just published. Changesets tags
# a single-package repo as v<version> (only monorepos use
# <name>@<version>), so the normal release tag is "v$VER".
if [ -n "${{ github.event.inputs.tag }}" ]; then
TAG="${{ github.event.inputs.tag }}"
VER="${TAG#v}"
else
VER=$(echo '${{ needs.release.outputs.publishedPackages }}' | jq -r '.[0].version')
TAG="v$VER"
fi
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Build the single-file Windows exe
shell: bash
env:
# Embedded into the frontend by Next during `zig build` (which runs
# npm run build); tags desktop telemetry as platform=windows.
NEXT_PUBLIC_GA_ID: ${{ secrets.GA_MEASUREMENT_ID }}
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.exe is stable.
cp zig-out/bin/keyparty.exe "KeyParty.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.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.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.exe" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}