diff --git a/.changeset/restore-desktop-build-resources.md b/.changeset/restore-desktop-build-resources.md new file mode 100644 index 0000000000..5b41e38ff0 --- /dev/null +++ b/.changeset/restore-desktop-build-resources.md @@ -0,0 +1,5 @@ +--- +"executor": patch +--- + +Desktop: restore the macOS signing entitlements and app icon that were accidentally removed from the build inputs, and fail fast at PR time and before publishing when any configured build resource is missing. The v1.6.1 desktop build could not be signed; this release supersedes it. diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 87705c9a1c..166fcbf559 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -106,6 +106,17 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + # apps/desktop/build/ holds committed static inputs (icon, entitlements) + # that no module imports, so a sweep can delete them without breaking a + # build or a test. v1.6.1 was tagged without them: both mac legs ran ~8 + # minutes, then died inside codesign with "build/entitlements.mac.plist: + # cannot read entitlement data" against an arbitrary locale.pak, while + # the missing icon only ever produced a "default Electron icon is used" + # notice. Check the tag's own tree here so the failure names the cause. + - name: Check desktop build resources + run: bun ./scripts/check-build-resources.ts + working-directory: apps/desktop + - name: Build web app run: bun run --filter @executor-js/local build diff --git a/apps/desktop/scripts/check-build-resources.ts b/apps/desktop/scripts/check-build-resources.ts new file mode 100644 index 0000000000..4f2d58aed2 --- /dev/null +++ b/apps/desktop/scripts/check-build-resources.ts @@ -0,0 +1,97 @@ +/** + * Preflight for the packaged app's static build inputs (`apps/desktop/build/`). + * + * electron-builder resolves `directories.buildResources` and `mac.entitlements` + * lazily, deep inside packaging, so a missing file there is silent until it is + * far too late. When `build/entitlements.mac.plist` is absent the mac legs run + * for ~8 minutes and then die inside codesign with + * `build/entitlements.mac.plist: cannot read entitlement data`, naming an + * arbitrary `*.lproj/locale.pak` — the first leaf codesign happened to reach, + * not the actual problem. A missing `build/icon.png` never fails at all: + * electron-builder logs `default Electron icon is used` and ships a generic + * Electron app. + * + * These files are load-bearing but unreferenced: nothing in the repo imports + * them, so deleting them breaks no build, type, or test. That is how the v1.6.1 + * tag was cut without them. This check gives them a reference. It runs in the + * desktop unit tests on every PR, and again in publish-desktop.yml before + * electron-builder starts, so the failure names the real cause in seconds. + */ +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import config from "../electron-builder.config"; + +// `import.meta.url` rather than Bun's `import.meta.dir`: this module is also +// imported by Vitest, which serves it through Vite's transform where the Bun +// property is undefined. +const ROOT = fileURLToPath(new URL("..", import.meta.url)); + +const buildResources = config.directories?.buildResources ?? "build"; + +const requiredPaths = (): readonly string[] => { + const paths = new Set(); + + // electron-builder finds the app icon by convention + // (`/icon.png`) rather than from a config field, so there is + // no value to read here — the convention has to be spelled out. + paths.add(`${buildResources}/icon.png`); + + const mac = config.mac; + for (const entitlements of [mac?.entitlements, mac?.entitlementsInherit]) { + if (typeof entitlements === "string") paths.add(entitlements); + } + + return [...paths].sort(); +}; + +const isPlistShaped = (text: string): boolean => + text.includes("") && text.includes(""); + +/** + * Returns one human-readable problem per broken build input, or an empty array + * when every input is present and usable. Never throws: callers decide whether + * a problem is a failed assertion or a non-zero exit. + */ +export const checkBuildResources = async (): Promise => { + const problems: string[] = []; + + for (const relativePath of requiredPaths()) { + const absolutePath = resolve(ROOT, relativePath); + const file = Bun.file(absolutePath); + + if (!(await file.exists())) { + problems.push(`missing: ${relativePath} (electron-builder.config.ts references it)`); + continue; + } + + if (file.size === 0) { + problems.push(`empty: ${relativePath}`); + continue; + } + + // codesign reads the entitlements file itself, so a truncated or + // non-plist file fails at signing time with the same opaque + // "cannot read entitlement data" as a missing one. + if (relativePath.endsWith(".plist") && !isPlistShaped(await file.text())) { + problems.push(`not a plist: ${relativePath}`); + } + } + + return problems; +}; + +if (import.meta.main) { + const problems = await checkBuildResources(); + + if (problems.length > 0) { + console.error("[check-build-resources] FAIL — apps/desktop/build/ is incomplete:"); + for (const problem of problems) console.error(` - ${problem}`); + console.error( + "\nThese files are committed static build inputs. Restore them from the last" + + "\ngood tag, e.g. `git checkout -- apps/desktop/build/`.", + ); + process.exit(1); + } + + console.log(`[check-build-resources] OK — ${requiredPaths().join(", ")}`); +} diff --git a/apps/desktop/src/build-resources.test.ts b/apps/desktop/src/build-resources.test.ts new file mode 100644 index 0000000000..f94527c9f2 --- /dev/null +++ b/apps/desktop/src/build-resources.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "@effect/vitest"; +import { checkBuildResources } from "../scripts/check-build-resources"; + +// The files under apps/desktop/build/ are committed inputs that no module +// imports, so nothing else in the repo notices when they disappear. This test +// is their only reference: it runs in `bun run test` on every PR, which is the +// gate that would have stopped them being deleted before the v1.6.1 tag. +describe("desktop build resources", () => { + it("keeps every static build input electron-builder packages and signs with", async () => { + expect(await checkBuildResources()).toEqual([]); + }); +});