Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/restore-desktop-build-resources.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions .github/workflows/publish-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
97 changes: 97 additions & 0 deletions apps/desktop/scripts/check-build-resources.ts
Original file line number Diff line number Diff line change
@@ -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<string>();

// electron-builder finds the app icon by convention
// (`<buildResources>/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("<plist") && text.includes("</plist>") && text.includes("<dict>");

/**
* 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<readonly string[]> => {
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 <tag> -- apps/desktop/build/`.",
);
process.exit(1);
}

console.log(`[check-build-resources] OK — ${requiredPaths().join(", ")}`);
}
12 changes: 12 additions & 0 deletions apps/desktop/src/build-resources.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Loading