06. Desktop Release Runbook
Scope: D126 tag artifacts for macOS arm64, Windows x64, and Linux x64; macOS signing/notarization remains the detailed qualification lane below. Cross-references: milestones · process model · security
1. Build lanes
| Lane | Command | Signing | Use |
|---|---|---|---|
| Dev | pnpm dev | none | daily development |
| Local package | pnpm --filter @pi-desktop/desktop pack | unsigned (identity: null) | packaging smoke (--dir output) |
| Local DMG | pnpm --filter @pi-desktop/desktop dist | unsigned | local install test |
| Release | scripts/release-macos.sh | Developer ID + optional notarization | distributable artifact |
The static electron-builder config stays unsigned-friendly (identity: null) so contributors without certificates can always package. The release script injects the real identity via -c.mac.identity at build time.
On macOS, pnpm dev creates and reuses a fingerprinted branded Electron host bundle under .cache/electron-dev/. Its bundle name, executable, identifier, and ICNS resource are development-only PI-Desktop values, so AppKit shows PI-Desktop in the application menu and uses the canonical icon in the native About panel. The runtime also applies build/icon_1024.png to the Dock. Stock files under node_modules are never modified. Windows/Linux development keeps the normal electron-vite executable. Windows Main nevertheless registers the same com.pi-desktop.app AppUserModelID used by the NSIS package before Electron readiness, preventing the stock host identity from owning native notifications or taskbar groups. The Windows package additionally pins the PI-Desktop executable and Start menu shortcut names. The launcher sets PI_DESKTOP_DEV=1 so runtime packaging checks keep update delivery disabled and preserve developer workspace defaults despite the branded executable name. Packaged lanes use build/icon.icns through electron-builder, and the renderer imports the same PNG through BrandLogo. The PNG is canonical; scripts/make-icon.py derives the 512px Windows/Linux package PNG on every platform and the iconset/ICNS when macOS iconutil is available, without overwriting the canonical source.
2. Prerequisites (release lane)
- Apple Developer account with a Developer ID Application certificate in the login keychain.
- Environment variables:
MAC_SIGNING_IDENTITY— e.g.Developer ID Application: <Name> (<TEAMID>)APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD,APPLE_TEAM_ID— required only for notarization; the script builds signed-but-unnotarized without them.
- Rust toolchain (arm64) and pnpm workspace installed.
3. What the build ships
- Electron app with hardened runtime + entitlements (
build/entitlements.mac.plist: JIT + unsigned-executable-memory + library-validation disable — the standard Electron set). Resources/bin/pi-desktop-host-core— Rust host binary (release build).Resources/agent-runtime/— bundled sidecar, executed withELECTRON_RUN_AS_NODE=1(no separate Node shipped).Resources/licenses/— notices that must remain distributable when the corresponding dependency's build-only source tree is pruned.Resources/app.asar— Electron Main, preload, renderer output, and only the runtime-resolved production modules. Renderer libraries are already present in Vite output and are not copied again as raw package trees.- Target-native
node-ptyassets underapp.asar.unpacked; other platform and architecture prebuilds are excluded when the package layout supports a reliable target filter. - Chromium locale packs for English and Simplified Chinese only. Product
en/zh-CNcatalogs remain bundled independently of Chromium locales. - App icon
build/icon.icns(derived from canonicalbuild/icon_1024.pngbyscripts/make-icon.py).
4. Release steps
4.1 Mandatory in-app changelog gate (D164)
Every product release that bumps a stable app version and cuts a tag MUST update the dual-locale in-app product changelog first. Tagging a stable version without matching EN + zh-CN entries in packages/shared/src/changelog.ts is a release process failure: packaged builds cannot show "what's new" without a network fetch, and GitHub auto-generated release bodies are not a substitute (extends D120 / ADR 0022).
Blocking steps:
- Edit
packages/shared/src/changelog.tsbeforenode scripts/release.mjs <version>/git tag:- Add a newest-first entry under both
enandzh-CN. - Same
versionstring (semver without a leadingv, matchingapps/desktop/APP_VERSION). - Optional ISO
date(YYYY-MM-DD). - Matching highlight counts; English is the source of truth (ADR 0009).
- Each bullet is one short user-facing idea (not raw PR titles).
- Add a newest-first entry under both
- Do not catalog pre-release-only versions (
x.y.z-rc.*) unless product explicitly ships in-app notes for that channel. - Run
pnpm --filter @pi-desktop/shared testand confirm catalog alignment (version sets + highlight counts) still passes. - Commit the catalog update so the tagged commit contains notes for that version (alone or adjacent to the version bump).
- GitHub Release bodies may still use
generate_release_notes: truefor the web page; they remain web-only and are not the in-app notes source.
Pre-tag checklist:
- [ ]
packages/shared/src/changelog.tshas EN + zh-CN entries for the version about to be tagged - [ ] Highlight counts match across locales
- [ ] Shared changelog tests pass
- [ ]
release.mjs/ tag runs only after the catalog commit is on the release branch
4.2 Build / package
export MAC_SIGNING_IDENTITY="Developer ID Application: ... (TEAMID)"
export APPLE_ID=...
export APPLE_APP_SPECIFIC_PASSWORD=...
export APPLE_TEAM_ID=...
scripts/release-macos.shArtifacts land in apps/desktop/release/ (DMG + blockmap).
4.3 GitHub tag workflow
The GitHub Release workflow starts all native platform runners without a separate validation-job barrier. Each runner validates that the pushed tag matches apps/desktop/package.json immediately after checkout, before package inputs are prepared.
On every platform, the release preparation step starts the locked Rust host build in parallel with pnpm installation and native dependency rebuilding. It then builds only the workspace dependencies selected by @pi-desktop/desktop^..., failing if that dependency selection is unexpectedly empty. The platform dist:* command remains responsible for bundling the agent runtime, verifying the host build, building the Desktop application once, and invoking electron-builder. This avoids a redundant Desktop build without changing the package scripts or release artifacts.
DMG, ZIP, NSIS, AppImage, deb, blockmap, and updater feed outputs are already compressed or compression-insensitive. The workflow therefore uploads their temporary Actions artifacts with compression level zero before the publish job assembles the GitHub Release.
5. Verification gates
Run after every release build:
APP="apps/desktop/release/mac-arm64/PI-Desktop.app"
codesign -dv --verbose=2 "$APP" # identity + hardened runtime flags
codesign --verify --deep --strict "$APP" # signature integrity
spctl -a -vv "$APP" # Gatekeeper assessment (notarized builds)
xcrun stapler validate "$APP" # notarization staple (if notarized)5.1 Package footprint gate
Inspect every native-runner package before publication and record all compressed artifact formats, unpacked application, ASAR, Electron framework/runtime, locale, and unpacked-native sizes. Compare them with the previous stable release; an unexplained increase above 15% blocks publication until reviewed.
The package inventory must confirm:
- exactly one
Resources/agent-runtime/sidecar.jsand one target-native Rust host binary - no raw renderer packages such as Mermaid, Shiki, React, KaTeX, or Lucide under packaged
node_modules - no dependency
*.map, test, example, declaration, or second agent-runtime tree in ASAR - required third-party license and notice files remain in ASAR or
Resources/licenseswhen their non-runtime package trees are pruned - only the configured English and Simplified Chinese Chromium locale packs
- a loadable target-native
node-ptybinary and no reliably excludable non-target prebuilds
The first audited optimized package establishes the platform baseline. Keep per-platform measurements rather than applying one budget to different Electron target layouts.
The first macOS arm64 baseline was captured on 2026-07-30 from an unsigned electron-builder --dir package. Sizes below sum regular-file bytes so they remain comparable across filesystems; the compressed artifact is not applicable to this directory-only validation build.
| Inventory | Bytes | MiB |
|---|---|---|
| Unpacked application | 251,724,810 | 240.1 |
Contents/Frameworks | 218,567,792 | 208.4 |
Contents/Resources | 33,102,807 | 31.6 |
Resources/app.asar | 20,944,962 | 20.0 |
Resources/app.asar.unpacked native payload | 137,336 | 0.1 |
| English and Simplified Chinese Chromium locale packs | 1,033,673 | 1.0 |
| Agent sidecar | 3,258,983 | 3.1 |
| Rust host | 7,160,000 | 6.8 |
The pre-optimization unpacked regular-file total was 559,355,716 bytes (533.4 MiB). The audited package is 307,630,906 bytes smaller, a 55.0% reduction. Its curated renderer output is 14.1 MiB, down from 20.5 MiB.
Manual smoke on a clean profile (PI_DESKTOP_DATA_DIR=$(mktemp -d)):
pnpm devlaunches withPI-Desktopin the macOS application menu and the canonical icon in both the Dock and native About panel; no Electron brand is visible.- App launches from DMG install, window appears, and the application-menu, About-panel, and Dock branding match the development lane.
- Empty home and expanded/collapsed sidebar show the canonical PI-Desktop logo; composer prompt rows have no leading brand icon; New task and project/Temporary create controls use the message-plus session icon.
- Onboarding checklist appears; configure provider; one streamed chat turn.
- One permissioned tool call (Write) allow + deny paths.
- Quit/relaunch → session history restored, window bounds restored.
~/.pi-desktop/logs/contains categorized NDJSON underapp/,host/, andagent/; timing records are inhost/timing.logandagent/timing.log.- With network access disabled, the shell still starts; English/Chinese switching, syntax highlighting, KaTeX, Mermaid fallback/rendering, terminal, host health, and sidecar health continue to use packaged local assets.
6. Windows/Linux release packages
The repository exposes dist:win and dist:linux for native-runner builds. Each packaging command first runs build:host-release, then bundles the agent runtime and Electron app. D126 tag workflows publish these outputs and their electron-updater manifests. Run a target command on that target OS:
Windows: pnpm --filter @pi-desktop/desktop dist:win
Linux: pnpm --filter @pi-desktop/desktop dist:linuxThe Windows package includes bin/pi-desktop-host-core.exe; Linux includes bin/pi-desktop-host-core. node-pty must also be rebuilt by electron-builder on the native runner. Signing, rollback, and installer upgrade qualification remain release hardening work; publication itself is active under D126.
Native-runner output matrix:
- Windows x64: NSIS installer
- Linux x64: AppImage and deb
Shell smoke on each native runner:
- Confirm no File/Edit/View/Window/Help menu appears inside the window.
- Verify F10 and Shift+F10 remain available to focused content.
- Execute application and editing shortcuts from a focused editor.
- Minimize, maximize, restore, and close from the custom controls.
- Relaunch with
PI_DESKTOP_START_MAXIMIZED=1; confirm the initial maximize/restore glyph matches the queried native state. - Verify unknown menu/window IPC actions fail with the window open and closed.
7. Known limitations
- macOS and Linux deb remain notify-and-link update modes.
- Signed in-app macOS delivery, rollback, staged rollout, and prerelease channel policy remain open release work.