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:
macmini4in~/.ssh/config - Hostname:
hostname -sreportsAndrews-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:
- Export the required Homebrew PATH:
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
- 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
- Publish the self-contained .NET API backend:
dotnet publish src/WorkWingman.Api -c Release -r osx-arm64 --self-contained true -o electron/resources/api
- 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
- Generate the macOS
.icnsfile 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
- 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:
- 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
- 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
- Verify application icon:
file electron/build/icon.icns
Expected output:
electron/build/icon.icns: Mac OS X icon, 56895 bytes
- 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
identitytonullandhardenedRuntimetofalse, withCSC_IDENTITY_AUTO_DISCOVERY=falseset 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.