diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a52e2f7..9bbf783 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,7 +92,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-15 + - os: macos-26 platform: mac-arm64 builder_args: --mac --arm64 - os: windows-2025 @@ -114,9 +114,9 @@ jobs: node-version: "26" cache: npm # The mac icon is the Icon Composer document (build/icon.icon); - # compiling it needs actool ≥ 26 (Xcode 26+). macos-15 images carry - # several Xcodes with an older default — select the latest stable - # explicitly. No-op for the windows/linux matrix legs. + # compiling it needs both macOS 26+ and actool ≥ 26 (Xcode 26+). + # Xcode 26's asset compiler crashes on macOS 15's older frameworks. + # Select the latest stable Xcode; no-op for Windows/Linux. - name: Select Xcode 26+ for actool (mac icon compile) if: runner.os == 'macOS' uses: maxim-lobanov/setup-xcode@v1 diff --git a/docs/desktop-release.md b/docs/desktop-release.md index 59554ad..b758acb 100644 --- a/docs/desktop-release.md +++ b/docs/desktop-release.md @@ -4,7 +4,7 @@ | Platform | Build runner | Packages | In-app update | |---|---|---|---| -| macOS, Apple Silicon | `macos-15` (arm64) | signed/notarized DMG + ZIP | ZIP via Squirrel.Mac | +| macOS, Apple Silicon | `macos-26` (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 | @@ -12,7 +12,7 @@ Windows signing is deliberately disabled for this first release. SmartScreen may 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](https://github.com/amosblomqvist/learnharness/issues). +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 **macOS 26+ and Xcode 26+** (`actool`): Xcode 26's asset compiler crashes on macOS 15 because its host frameworks lack required symbols. `desktop:dist` injects `DEVELOPER_DIR` locally; release packaging uses `macos-26` and selects Xcode via setup-xcode. The older-OS fallback icon remains included. Linux's desktop identity is `learnharness.desktop`. Public package-maintainer metadata uses the owner's GitHub no-reply address; support goes through [GitHub Issues](https://github.com/amosblomqvist/learnharness/issues). ## Build pipeline @@ -42,7 +42,7 @@ npx electron-builder --linux --x64 --publish never `.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. +- Native renderer tests plus standalone build/boot-smoke on macOS (`macos-15`), Windows, and Linux. This macOS job does not compile the Icon Composer asset; only release packaging needs the newer runner. `.github/workflows/release.yml` has two deliberately separate modes: diff --git a/tests/desktop-platform.test.ts b/tests/desktop-platform.test.ts index 3b0268f..28e6887 100644 --- a/tests/desktop-platform.test.ts +++ b/tests/desktop-platform.test.ts @@ -115,10 +115,16 @@ describe("desktop packaging policy", () => { }); describe("desktop release workflow", () => { - type Step = { if?: string; run?: string; env?: Record; with?: { script: string } }; + type Step = { + if?: string; run?: string; uses?: string; env?: Record; + with?: { script?: string; "xcode-version"?: string }; + }; type Workflow = { concurrency: { group: string; "cancel-in-progress": boolean }; - jobs: Record; + jobs: Record; }; const yaml = require("js-yaml") as { load(source: string): Workflow }; const workflow = yaml.load(readFileSync(new URL("../.github/workflows/release.yml", import.meta.url), "utf8")); @@ -128,6 +134,7 @@ describe("desktop release workflow", () => { const prepare = draft.steps[0]; assert.ok(prepare?.with); const script = prepare.with.script; + assert.ok(script); type Release = { id: number; tag_name: string; draft: boolean }; /** Execute the REAL action script with an in-memory GitHub API. */ @@ -176,6 +183,24 @@ describe("desktop release workflow", () => { } }); + it("pairs Icon Composer packaging with macOS 26 and Xcode selection, retaining macOS 15 CI", () => { + const mac = packaging.strategy?.matrix.include.find(entry => entry.platform === "mac-arm64"); + assert.equal(mac?.os, "macos-26", "actool's .icon compiler needs macOS 26 host frameworks, not just newer Xcode"); + assert.equal(mac?.builder_args, "--mac --arm64"); + assert.equal(packaging["runs-on"], "${{ matrix.os }}"); + const xcodeIndex = packaging.steps.findIndex(step => step.uses === "maxim-lobanov/setup-xcode@v1"); + assert.ok(xcodeIndex >= 0); + const xcode = packaging.steps[xcodeIndex]; + assert.equal(xcode?.if, "runner.os == 'macOS'"); + assert.equal(xcode?.with?.["xcode-version"], "latest-stable"); + const buildIndex = packaging.steps.findIndex(step => step.run?.includes("electron-builder")); + assert.ok(buildIndex > xcodeIndex, "select Xcode before packaging compiles the icon"); + + const ci = yaml.load(readFileSync(new URL("../.github/workflows/ci.yml", import.meta.url), "utf8")); + const ciMac = ci.jobs.standalone?.strategy?.matrix.include.find(entry => entry.platform === "mac-arm64"); + assert.equal(ciMac?.os, "macos-15", "ordinary CI keeps older macOS build/boot coverage"); + }); + it("creates exactly one unpublished draft when none matches", async () => { assert.deepEqual(await prepareDraft([{ id: 9, tag_name: "v0.2.0", draft: false }]), [{ owner: "test-owner", repo: "test-repo", tag_name: "v0.3.0", name: "0.3.0", draft: true,