Skip to content

macOS Desktop Build Runbook

1. What this produces

This runbook builds an unsigned macOS .dmg installer for Apple Silicon (arm64) containing the WorkWingman Electron desktop application, the self-contained .NET 10 backend API, and bundled Playwright Chromium browser binaries.

2. Prerequisites

Build operations execute on the dedicated macOS build host:

  • Host alias: macmini4 in ~/.ssh/config
  • Hostname: hostname -s reports Andrews-Mac-mini-4
  • Operating system: macOS 15.5 (arm64)
  • Installed toolchain:
  • .NET SDK: 10.0.302 (via Homebrew)
  • Node.js: v26.7.0 (via Homebrew)
  • Package manager: npm (via Homebrew)
  • Shell: PowerShell (pwsh, via Homebrew)
  • Git: git (via Homebrew)
  • GitHub CLI: gh (via Homebrew)
  • Xcode Command Line Tools: /Library/Developer/CommandLineTools

3. The five traps

Trap 1: PATH missing in non-interactive SSH sessions

which dotnet returns nothing, and installed tools appear missing.

Non-interactive SSH commands do not inherit /opt/homebrew/bin on PATH. Every remote command string must prepend Homebrew search paths.

Fix:

export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

Trap 2: GitHub authentication failure in non-interactive environments

The token in default is invalid from gh auth status or could not read Username for 'https://github.com': Device not configured from git fetch origin.

Remote git authentication currently fails on the Mac hosts because GH_TOKEN is unset, credential.helper points to osxkeychain (inaccessible non-interactively), and no SSH keypair exists in ~/.ssh/id_*.

Fix:

Transfer source files with scp and create a detached worktree from the local master branch on the Mac:

git worktree add --detach ~/source/repos/<name> master

Trap 3: Overwriting read-only automatic variables in PowerShell 7

Cannot overwrite variable IsLinux because it is read-only or constant.

In PowerShell 7, $IsWindows, $IsMacOS, and $IsLinux are built-in, read-only automatic variables. Variable names are case-insensitive, so assigning to a local variable named $isLinux attempts to overwrite $IsLinux and terminates the script immediately when $ErrorActionPreference = 'Stop' is active.

Fix:

Use $hostIsWindows, $hostIsMac, and $hostIsLinux instead of the reserved variable names.

Trap 4: Masked exit codes when piping to output filters

EXIT=0 reported by tail despite a build failure.

Piping directly from pwsh into tail -40 causes the shell to capture the exit status of tail rather than the exit status of pwsh. A catastrophic build failure will appear successful.

Fix:

Redirect output to a dedicated log file and check the process exit status explicitly before reading the log:

pwsh -NoProfile -File electron/build-installer.ps1 -Rid osx-arm64 -Target dmg > /tmp/wwmac-build.log 2>&1; echo "EXIT=$?"; tail -40 /tmp/wwmac-build.log

Trap 5: Missing Playwright shell script

playwright.sh: No such file or directory

Microsoft.Playwright emits playwright.ps1 on all supported operating systems including macOS. No playwright.sh script exists in the package. The platform-specific components reside in the underlying driver path at .playwright/node/darwin-arm64.

Fix:

Execute playwright.ps1 via pwsh on macOS.

4. Build it

Run the following commands in sequence on macmini4:

  1. Export the required Homebrew PATH:
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
  1. Create a clean build workspace from the local master branch:
cd ~/source/repos/WorkWingman
git worktree add --detach /tmp/ww-build master
cd /tmp/ww-build
  1. Publish the self-contained .NET API backend:
dotnet publish src/WorkWingman.Api -c Release -r osx-arm64 --self-contained true -o electron/resources/api
  1. Install the bundled Playwright Chromium binaries:
export PLAYWRIGHT_BROWSERS_PATH="$(pwd)/electron/resources/playwright-browsers"
pwsh -NoProfile -File electron/resources/api/playwright.ps1 install chromium
  1. Generate the macOS .icns file from the vector source:
TMP_ICON_DIR="$(mktemp -d)"
qlmanage -t -s 1024 -o "$TMP_ICON_DIR" frontend/src/assets/brand/ww-mark.svg
mkdir -p "$TMP_ICON_DIR/icon.iconset"
sips -z 16 16 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_16x16.png"
sips -z 32 32 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_16x16@2x.png"
sips -z 32 32 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_32x32.png"
sips -z 64 64 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_32x32@2x.png"
sips -z 128 128 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_128x128.png"
sips -z 256 256 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_128x128@2x.png"
sips -z 256 256 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_256x256.png"
sips -z 512 512 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_256x256@2x.png"
sips -z 512 512 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_512x512.png"
sips -z 1024 1024 "$TMP_ICON_DIR/ww-mark.svg.png" --out "$TMP_ICON_DIR/icon.iconset/icon_512x512@2x.png"
mkdir -p electron/build
iconutil -c icns "$TMP_ICON_DIR/icon.iconset" -o electron/build/icon.icns
  1. Build the installer package:
export CSC_IDENTITY_AUTO_DISCOVERY=false
pwsh -NoProfile -File electron/build-installer.ps1 -Rid osx-arm64 -Target dmg > /tmp/wwmac-build.log 2>&1; echo "EXIT=$?"; tail -40 /tmp/wwmac-build.log

5. Verify it

Run the following checks to confirm build artifact validity:

  1. Verify .NET API publication:
file electron/resources/api/WorkWingman.Api

Expected output:

electron/resources/api/WorkWingman.Api: Mach-O 64-bit executable arm64

Check bundled runtime files:

ls electron/resources/api/libcoreclr.dylib electron/resources/api/System.Private.CoreLib.dll

Count total published files:

find electron/resources/api -type f | wc -l

Expected output: 390

  1. Verify Playwright browser payload:
ls -d electron/resources/playwright-browsers/*

Expected output directories: * chromium-1228 * chromium_headless_shell-1228 * ffmpeg-1011

Verify directory size:

du -sh electron/resources/playwright-browsers

Expected output: 539M

  1. Verify application icon:
file electron/build/icon.icns

Expected output:

electron/build/icon.icns: Mac OS X icon, 56895 bytes
  1. Verify installer output:
ls -lh electron/dist/*.dmg

Expected output: .dmg installer file present in electron/dist/.

6. Known limitations

  • Unsigned package: There is no Apple Developer Program account configured (decision date 2026-08-14). Electron builder configuration sets identity to null and hardenedRuntime to false, with CSC_IDENTITY_AUTO_DISCOVERY=false set in the environment. macOS Gatekeeper will present a security warning on execution. The package is suitable for internal fleet use only.
  • Architecture constraint: arm64 only. Universal binaries are not produced because bundling x64 and arm64 versions of the .NET runtime and Playwright browser payload would double binary footprint.
  • CI automation: No automated CI build leg exists for macOS targets.
  • Backend portability: Backend runtime paths across non-Windows operating systems are not yet verified.