Files
learnharness/docs/desktop-release.md
T
amosblomqvist cf042b25e7 icons: per-platform icon set — real Liquid Glass on macOS, flat design on Windows/Linux
The hand-packed icns was pixel-clean (verified by unpacking and scanning
every size), but macOS 26 composites legacy .icns icons into the Liquid
Glass system at render time; stacked on Icon Composer's baked PNG-export
lighting, that clips the rim highlights into the near-corner specks and
abrupt highlight ends (owner report 2026-10-13). The fix is the real
layered icon: mac.icon now points at the Icon Composer document
(build/icon.icon, copied from ~/Documents/Affinity/lh-logo) and
electron-builder compiles it via actool into Assets.car — macOS 26+
renders the true Liquid Glass design (dark/tinted variants included).

actool's backwards-compat icns caps at 256px, so scripts/after-pack.cjs
swaps in the existing full-res build/icon.icns before signing: pre-Tahoe
macOS keeps today's quality, Tahoe gets the layered icon.

Windows/Linux get the owner's flat design (rounded squircle with baked
transparency): scripts/make-icons.mjs renders build/icon-flat.svg via
resvg into build/icon.ico (16→256 PNG entries) + build/icon.png (512),
and electron-builder.yml pins win.icon/linux.icon.

desktop:dist injects DEVELOPER_DIR (actool needs Xcode; the machine
selects CommandLineTools by default, CI's setup-xcode takes precedence
via the :- default). The release workflow's mac job selects Xcode 26+
explicitly. Verified locally: mac --dir build (Assets.car + checksummed
full-res fallback + CFBundleIconName/IconFile) and the win-unpacked exe
carries our ico bytes; NSIS/AppImage assembly is CI's (cross-build
tooling can't run on arm64 macOS). Runbook updated. 1610/1610.
2026-10-13 10:51:22 +03:00

6.7 KiB
Raw Blame History

Desktop builds and releases

Platforms

Platform Build runner Packages In-app update
macOS, Apple Silicon macos-15 (arm64) signed/notarized DMG + ZIP ZIP via Squirrel.Mac
Windows, x64 windows-2025 unsigned per-user NSIS installer NSIS
Linux, x64 ubuntu-24.04 AppImage + .deb AppImage replacement; .deb through dpkg/apt with elevation if needed

Windows signing is deliberately disabled for this first release. SmartScreen may warn about an unknown publisher on initial installation. AppImage is the primary Linux download because it is self-contained and updates without administrator permission. The installed electron-updater 6.8.9 also supports .deb updates; an update may request an administrator password. Both use the same download/restart banner.

The custom app bar stays on all three platforms. macOS keeps its left-side traffic lights; Windows/Linux use Electron's native right-side Window Controls Overlay, with CSS space reserved from the overlay's actual geometry. Windows/Linux hide the native menu until Alt is pressed. macOS keeps its system menu.

Icons are per-platform (2026-10-13): macOS compiles the Icon Composer document build/icon.icon via actool into Assets.car (macOS 26 Liquid Glass) plus a fallback icns that scripts/after-pack.cjs replaces with the hand-packed full-res build/icon.icns (actool's caps at 256px); Windows embeds build/icon.ico and Linux uses build/icon.png, both generated from the flat design (build/icon-flat.svg) by node scripts/make-icons.mjs. Building the mac icon needs Xcode 26+ (actool) — desktop:dist injects DEVELOPER_DIR locally and the release workflow selects Xcode via setup-xcode. Linux's desktop identity is learnharness.desktop. Public package-maintainer metadata uses the owner's GitHub no-reply address; support goes through GitHub Issues.

Build pipeline

Each OS installs its own dependencies and builds its own Next standalone tree:

  1. npm ci
  2. Native PDF/SVG renderer tests for that OS, running in Electron's bundled Node (no display needed).
  3. npm run build (output: "standalone").
  4. node scripts/build-server.mjs — copy static assets, refuse leaked data/keys, remove only standalone-root dist/ and node_modules/electron/ (old installers and the development runtime), check prompt assets. Runtime dependencies' own dist/ directories and native renderer binaries stay intact.
  5. node scripts/smoke-server.mjs --electron — boot the standalone server in Electron's bundled Node, using empty temporary data/auth, then check the home route and GET-only APIs.
  6. npx electron-builder <platform flags> --publish <policy>.

The shell/updater live in app.asar; the standalone server and native renderer binaries ship unpacked as extraResources. Do not cross-build the server payload. Next traces the build machine's native binaries, so a macOS-built standalone tree is not a Windows/Linux payload.

npm run desktop:dist remains the local macOS signing path, using the LH_NOTARY keychain profile. For a local Windows/Linux build, run steps 1–5 above, then:

# On Windows:
npx electron-builder --win --x64 --publish never

# On Linux:
npx electron-builder --linux --x64 --publish never

CI vs release

.github/workflows/ci.yml runs on main pushes and pull requests:

  • Typecheck, full deterministic test suite, and lint on Ubuntu.
  • Native renderer tests plus standalone build/boot-smoke on macOS, Windows, and Linux.

.github/workflows/release.yml has two deliberately separate modes:

  • Manual / Run workflow: unsigned rehearsal installers saved as workflow artifacts for 7 days. No signing secrets needed, no release created, and nothing published. macOS notarization is skipped. Use the Windows/Linux artifacts for the first human smoke pass. These are not production macOS installers.
  • Push v<package.json version>: gates first, then one job creates or reuses a single draft GitHub Release before the three OS jobs package/upload their artifacts and update metadata in parallel. This avoids competing draft creation. A published or ambiguous existing release fails the preparation step instead of being modified. Separate runs for the same ref are serialized, never cancelled mid-upload. The tag must match the package version. macOS must sign and notarize successfully; missing secrets fail loudly. The workflow never publishes the draft.

The three jobs upload distinct updater feeds: latest-mac.yml, latest.yml, latest-linux.yml, with the matching ZIP/NSIS/AppImage/deb assets and blockmaps. Do not publish a partially populated draft. The app uses the baked GitHub feed without a token: the repository must be public before end users can download updates. Making the repository public and publishing the release remain the owner's actions.

macOS CI secrets

Add these under repository Settings → Secrets and variables → Actions:

Secret Value
MAC_CSC_LINK Base64-encoded Developer ID Application .p12 backup, including its private key
MAC_CSC_KEY_PASSWORD Password protecting that .p12
APPLE_ID Apple ID used for notarization
APPLE_APP_SPECIFIC_PASSWORD App-specific password for that Apple ID
APPLE_TEAM_ID JS6XN22HY6

The workflow maps the certificate secrets to electron-builder's CSC_LINK / CSC_KEY_PASSWORD. GitHub's built-in GITHUB_TOKEN supplies GH_TOKEN with job-scoped contents: write; no separate GitHub token secret is required. Signing credentials are present only for the packaging step, not the Next build. Never commit certificates, passwords, or .env.local.

First release runbook

  1. Push the implementation; run the manual Desktop release workflow.
  2. Human smoke on Windows/Linux: install/open, window buttons and dragging, sidebar at narrow widths, zoom, setup/keys, session/resume, native PDF/SVG verification and demo helper. macOS remains signed/notarized through the production path.
  3. Add macOS signing/notarization secrets.
  4. Bump package.json and the lockfile to the agreed release version; run local gates and push a matching v... tag.
  5. Wait for all three packaging jobs. Verify all installer assets and updater feeds are in the draft.
  6. Owner makes the repository public, publishes the complete draft, then checks the real GitHub update path using a prior installed version.

A full human pass is required before the first three-platform release. Subsequent content-only updates rely on the deterministic suite plus the per-OS build/boot-smoke matrix. Changes to electron/, native modules, packaging/updater configuration, or Electron itself call for another platform smoke pass.