fix(desktop): harden embedded Postgres packaging

This commit is contained in:
gsxdsm
2026-07-14 09:47:41 -07:00
parent 12a4fbe9bb
commit 1b9dce22c0
12 changed files with 254 additions and 15 deletions

View File

@@ -71,6 +71,13 @@ jobs:
if: steps.changes.outputs.relevant == 'true'
run: pnpm --filter @fusion/desktop build
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# The advisory Linux packaging lane also executes the real bundled
# lifecycle, catching native payload regressions before a release run.
- name: Smoke embedded Postgres lifecycle
if: steps.changes.outputs.relevant == 'true'
run: pnpm --filter @fusion/core test:embedded-postgres
# Authoritative check: electron-builder's production-dependency walk over the staged closure.
# --dir skips installer/signing but still FAILS if any production dependency's declared version
# range is unsatisfied in the closure — which is exactly the aws-sdk skew that broke the release.

View File

@@ -22,6 +22,12 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# The manual Windows installer path must boot the same embedded database
# payload used by Local mode before it can publish an installer artifact.
- name: Smoke embedded Postgres on Windows
run: pnpm --filter @fusion/core test:embedded-postgres
# FNXC:WindowsDesktopPackaging 2026-07-01-19:45:
# Mirror release.yml: build every workspace package's tsc dist (incl.
# @fusion/core and @fusion/engine, which are gitignored) before packaging.
@@ -38,7 +44,7 @@ jobs:
# Code-signing hardening is intentionally deferred to FN-5592.
- name: Package signed Windows EXE
if: ${{ env.WINDOWS_CERTIFICATE_BASE64 != '' }}
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --arm64 --publish never
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --publish never
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
@@ -48,7 +54,7 @@ jobs:
- name: Package unsigned Windows EXE
if: ${{ env.WINDOWS_CERTIFICATE_BASE64 == '' }}
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --arm64 --publish never
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --publish never
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CSC_IDENTITY_AUTO_DISCOVERY: "false"

View File

@@ -121,7 +121,8 @@ jobs:
# ── Build Windows desktop EXE artifacts ──────────────────────────────
# Code-signing with WINDOWS_CERTIFICATE_BASE64 / WINDOWS_CERTIFICATE_PASSWORD
# is intentionally deferred to FN-5592; ARM64 support is tracked in FN-5594.
# is intentionally deferred to FN-5592. Desktop Windows remains x64 because
# embedded-postgres does not publish a native Windows ARM64 payload.
build-desktop-windows:
name: Build Desktop Windows EXE
runs-on: windows-latest
@@ -133,6 +134,12 @@ jobs:
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# Every Windows desktop release must prove its bundled default database can
# initialize, serve SQL, persist across restart, and stop without orphans.
- name: Smoke embedded Postgres on Windows
run: pnpm --filter @fusion/core test:embedded-postgres
- name: Build
run: pnpm build
@@ -188,6 +195,11 @@ jobs:
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# Exercise the native payload on the release host before signing artifacts.
- name: Smoke embedded Postgres on macOS
run: pnpm --filter @fusion/core test:embedded-postgres
- name: Build
run: pnpm build
@@ -287,6 +299,11 @@ jobs:
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# Exercise the native payload on the release host before packaging artifacts.
- name: Smoke embedded Postgres on Linux
run: pnpm --filter @fusion/core test:embedded-postgres
- name: Build
run: pnpm build

View File

@@ -113,7 +113,8 @@ jobs:
# ── Build Windows desktop EXE artifacts ──────────────────────────────
# Code-signing with WINDOWS_CERTIFICATE_BASE64 / WINDOWS_CERTIFICATE_PASSWORD
# is intentionally deferred to FN-5592; ARM64 support is tracked in FN-5594.
# is intentionally deferred to FN-5592. Desktop Windows remains x64 because
# embedded-postgres does not publish a native Windows ARM64 payload.
build-desktop-windows:
name: Build Desktop Windows EXE
runs-on: windows-latest
@@ -125,6 +126,11 @@ jobs:
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# Test releases use the same native database proof as production releases.
- name: Smoke embedded Postgres on Windows
run: pnpm --filter @fusion/core test:embedded-postgres
- name: Build
run: pnpm build
@@ -177,6 +183,9 @@ jobs:
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
- name: Smoke embedded Postgres on macOS
run: pnpm --filter @fusion/core test:embedded-postgres
- name: Build
run: pnpm build
@@ -276,6 +285,9 @@ jobs:
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
- name: Smoke embedded Postgres on Linux
run: pnpm --filter @fusion/core test:embedded-postgres
- name: Build
run: pnpm build

View File

@@ -37,6 +37,7 @@
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "vitest run --silent=passed-only --reporter=dot",
"test:embedded-postgres": "vitest run src/__tests__/postgres/embedded-lifecycle.test.ts --silent=passed-only --reporter=dot",
"test:pg-gate": "vitest run src/__tests__/postgres/handoff-to-review-atomicity.pg.test.ts src/__tests__/postgres/store-list.pg.test.ts src/__tests__/postgres/task-lifecycle-e2e.pg.test.ts src/__tests__/postgres/soft-delete-resurrection-FN-5233.pg.test.ts src/__tests__/postgres/agent-logs-and-monitor.pg.test.ts src/__tests__/postgres/todo-store.pg.test.ts src/__tests__/postgres/workflow-definitions.pg.test.ts src/__tests__/postgres/message-store.pg.test.ts src/__tests__/postgres/insight-store.pg.test.ts src/__tests__/postgres/insight-run-execution.pg.test.ts src/__tests__/postgres/research-store.pg.test.ts src/__tests__/postgres/mission-store.pg.test.ts src/__tests__/postgres/goal-store.pg.test.ts src/__tests__/postgres/artifacts-documents-evals.pg.test.ts src/__tests__/postgres/command-center-analytics.pg.test.ts src/__tests__/postgres/command-center-remaining-analytics.pg.test.ts src/__tests__/postgres/research-execution.pg.test.ts src/__tests__/postgres/async-store-events.pg.test.ts src/__tests__/postgres/signal-ingestion.pg.test.ts src/__tests__/postgres/mission-autopilot.pg.test.ts src/__tests__/postgres/workflow-create.pg.test.ts src/__tests__/postgres/monitor-trait-storm-guard.pg.test.ts src/__tests__/postgres/agent-wake-getagent.pg.test.ts --silent=passed-only --reporter=dot"
},
"devDependencies": {

View File

@@ -362,8 +362,8 @@ Desktop packaging is configured in `electron-builder.yml`.
- Output directory: `packages/desktop/dist-electron`
- Targets: macOS (`dmg`, `zip`), Windows (`nsis`, `portable`), Linux (`AppImage`, `deb`, `tar.gz`)
- Windows NSIS installer artifacts: `Fusion-<version>-win-x64.exe` and `Fusion-<version>-win-arm64.exe` in `packages/desktop/dist-electron/`
- Windows portable artifacts: `Fusion-<version>-win-x64-portable.exe` and `Fusion-<version>-win-arm64-portable.exe` in `packages/desktop/dist-electron/`
- Windows NSIS installer artifact: `Fusion-<version>-win-x64.exe` in `packages/desktop/dist-electron/`
- Windows portable artifact: `Fusion-<version>-win-x64-portable.exe` in `packages/desktop/dist-electron/`
- Silent NSIS installs support a custom destination with `/S /D=<absolute path>`; keep `/D=...` as the final installer argument (for example, `Fusion-<version>-win-x64.exe /S /D=C:\\Users\\me\\Tools\\fusion`).
- The Windows packaging workflow verifies `win*-unpacked` contains Electron root runtime resources (`chrome_100_percent.pak`, `chrome_200_percent.pak`, and `resources.pak`) before uploading artifacts.
- Binary GitHub Release workflow (`.github/workflows/release.yml`) now attaches desktop artifacts for all supported platforms:
@@ -405,8 +405,8 @@ gpg --import KEYS && gpg --verify Fusion-<version>-linux-x64.AppImage.asc Fusion
```
The public key (`KEYS`) must be distributed out-of-band. See `docs/CODE_SIGNING.md` for canonical setup, key publication, and troubleshooting guidance.
- Isolated manual Windows build path: `.github/workflows/desktop-windows.yml` (`workflow_dispatch` on `windows-latest`) runs `electron-builder --win --x64 --arm64 --publish never`.
- ARM64 artifacts are cross-built on the `windows-latest` x64 runner; execution/validation still requires a Windows ARM64 device or emulator.
- Isolated manual Windows build path: `.github/workflows/desktop-windows.yml` (`workflow_dispatch` on `windows-latest`) runs `electron-builder --win --x64 --publish never`.
- Windows desktop artifacts are x64. Windows ARM64 users run the x64 build through Windows compatibility because upstream `embedded-postgres` does not publish a native Windows ARM64 payload.
- Deep link protocol: `fusion://`
- Publish provider: GitHub (`gsxdsm/fusion`)

View File

@@ -83,7 +83,12 @@ files:
- node_modules/tar-fs/**/*
- node_modules/uuid/**/*
asarUnpack: []
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:30:
# Embedded Postgres launches native initdb/pg_ctl/postgres child processes.
# Executables and their shared libraries must live on the real filesystem;
# processes cannot execute payloads directly from Electron's virtual app.asar.
asarUnpack:
- "node_modules/@embedded-postgres/*/native/**/*"
extraMetadata:
main: dist/main.js
@@ -116,7 +121,13 @@ mac:
minimumSystemVersion: "10.15"
target:
- target: dmg
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
@@ -124,15 +135,18 @@ mac:
notarize: true
win:
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:30:
# embedded-postgres 15.18 publishes only @embedded-postgres/windows-x64 and
# throws on win32/arm64. Do not publish an ARM64 installer whose default local
# database cannot start; Windows ARM64 can use the supported x64 installer via
# the OS compatibility layer until upstream ships a native ARM64 payload.
target:
- target: nsis
arch:
- x64
- arm64
- target: portable
arch:
- x64
- arm64
signtoolOptions:
signingHashAlgorithms:
- sha256

View File

@@ -1,7 +1,7 @@
import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { existsSync } from "node:fs";
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -143,6 +143,72 @@ export const desktopDeployDir = process.env.FUSION_DESKTOP_DEPLOY_DIR
? resolve(process.env.FUSION_DESKTOP_DEPLOY_DIR)
: resolve(packageRoot, "deploy");
export type EmbeddedPostgresDesktopPlatform = "darwin" | "linux" | "win32";
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-09:30:
* These are the native Postgres payloads shipped by each desktop release job.
* macOS and Linux jobs cross-build x64 and ARM64 from one staged closure;
* Windows is x64-only because embedded-postgres has no Windows ARM64 binary.
*/
export function requiredEmbeddedPostgresPackages(
platform: EmbeddedPostgresDesktopPlatform,
): readonly string[] {
switch (platform) {
case "darwin":
return ["@embedded-postgres/darwin-x64", "@embedded-postgres/darwin-arm64"];
case "linux":
return ["@embedded-postgres/linux-x64", "@embedded-postgres/linux-arm64"];
case "win32":
return ["@embedded-postgres/windows-x64"];
}
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-09:30:
* A desktop build is incomplete unless the staged production closure contains
* embedded-postgres plus every native executable needed by the artifacts that
* job emits. Fail before electron-builder so a host-only optional-dependency
* install cannot silently produce an app that crashes on another architecture.
*/
export async function verifyEmbeddedPostgresPayloads(
deployDir = desktopDeployDir,
platform: EmbeddedPostgresDesktopPlatform = process.platform as EmbeddedPostgresDesktopPlatform,
): Promise<void> {
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
throw new Error(`Unsupported desktop platform for embedded Postgres: ${platform}`);
}
const executableSuffix = platform === "win32" ? ".exe" : "";
const requiredPaths = [
resolve(deployDir, "node_modules", "embedded-postgres", "package.json"),
...requiredEmbeddedPostgresPackages(platform).flatMap((packageName) => {
const packageRoot = resolve(deployDir, "node_modules", ...packageName.split("/"));
return [
resolve(packageRoot, "package.json"),
resolve(packageRoot, "native", "bin", `initdb${executableSuffix}`),
resolve(packageRoot, "native", "bin", `pg_ctl${executableSuffix}`),
resolve(packageRoot, "native", "bin", `postgres${executableSuffix}`),
];
}),
];
const missing: string[] = [];
for (const requiredPath of requiredPaths) {
try {
await stat(requiredPath);
} catch {
missing.push(requiredPath);
}
}
if (missing.length > 0) {
throw new Error(
`Desktop embedded Postgres payload is incomplete for ${platform}: ${missing.join(", ")}. ` +
"Run pnpm install with pnpm-workspace.yaml supportedArchitectures before packaging.",
);
}
}
export async function stageDesktopDeploy(): Promise<void> {
console.log("[desktop:build] Staging complete production closure via pnpm deploy...");
await rm(desktopDeployDir, { recursive: true, force: true });
@@ -164,6 +230,7 @@ export async function stageDesktopDeploy(): Promise<void> {
? config.replace(/output:\s*dist-electron/, "output: ../dist-electron")
: `directories:\n output: ../dist-electron\n${config}`;
await writeFile(stagedConfig, patched);
await verifyEmbeddedPostgresPayloads();
console.log(`[desktop:build] Deploy staged at ${desktopDeployDir}`);
}

View File

@@ -1,7 +1,12 @@
import { readFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
requiredEmbeddedPostgresPackages,
verifyEmbeddedPostgresPayloads,
} from "../../scripts/workspace-tools";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -59,3 +64,55 @@ describe("desktop Electron main bundling", () => {
}
});
});
describe("desktop embedded Postgres payload", () => {
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-09:31:
* Desktop packaging must prove the zero-config database payload for every
* emitted architecture, not merely that Electron can assemble an installer.
*/
it("maps release hosts to supported native database packages", () => {
expect(requiredEmbeddedPostgresPackages("darwin")).toEqual([
"@embedded-postgres/darwin-x64",
"@embedded-postgres/darwin-arm64",
]);
expect(requiredEmbeddedPostgresPackages("linux")).toEqual([
"@embedded-postgres/linux-x64",
"@embedded-postgres/linux-arm64",
]);
expect(requiredEmbeddedPostgresPackages("win32")).toEqual([
"@embedded-postgres/windows-x64",
]);
});
it("rejects a staged desktop closure missing a native executable", async () => {
const deployDir = await mkdtemp(path.join(tmpdir(), "fusion-desktop-pg-payload-"));
try {
await mkdir(path.join(deployDir, "node_modules", "embedded-postgres"), { recursive: true });
await writeFile(
path.join(deployDir, "node_modules", "embedded-postgres", "package.json"),
"{}",
);
const packageRoot = path.join(
deployDir,
"node_modules",
"@embedded-postgres",
"windows-x64",
);
await mkdir(path.join(packageRoot, "native", "bin"), { recursive: true });
await writeFile(path.join(packageRoot, "package.json"), "{}");
await writeFile(path.join(packageRoot, "native", "bin", "initdb.exe"), "");
await writeFile(path.join(packageRoot, "native", "bin", "pg_ctl.exe"), "");
await expect(verifyEmbeddedPostgresPayloads(deployDir, "win32")).rejects.toThrow(
/postgres\.exe/,
);
await writeFile(path.join(packageRoot, "native", "bin", "postgres.exe"), "");
await expect(verifyEmbeddedPostgresPayloads(deployDir, "win32")).resolves.toBeUndefined();
} finally {
await rm(deployDir, { recursive: true, force: true });
}
});
});

View File

@@ -32,8 +32,8 @@ describe("electron-builder desktop config", () => {
const extractArchValues = (archBlock: string) =>
Array.from(archBlock.matchAll(/-\s*(x64|arm64)/g), (match) => match[1]).sort();
expect(extractArchValues(nsisArchMatch![1])).toEqual(["arm64", "x64"]);
expect(extractArchValues(portableArchMatch![1])).toEqual(["arm64", "x64"]);
expect(extractArchValues(nsisArchMatch![1])).toEqual(["x64"]);
expect(extractArchValues(portableArchMatch![1])).toEqual(["x64"]);
expect(builderConfig).toMatch(/nsis:\s*[\s\S]*?artifactName:\s*"\$\{productName\}-\$\{version\}-\$\{os\}-\$\{arch\}\.\$\{ext\}"/m);
expect(builderConfig).toMatch(/nsis:\s*[\s\S]*?oneClick:\s*false/m);
@@ -76,6 +76,18 @@ describe("electron-builder desktop config", () => {
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?target:\s*[\s\S]*?-\s*target:\s*dmg/m);
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?target:\s*[\s\S]*?-\s*target:\s*zip/m);
const macSection = builderConfig.match(/mac:\s*([\s\S]*?)(?=\nwin:)/m)?.[1] ?? "";
const macArchBlocks = Array.from(
macSection.matchAll(/-\s*target:\s*(dmg|zip)\s*arch:\s*([\s\S]*?)(?=\n\s*-\s*target:|\n\w|$)/g),
);
expect(macArchBlocks).toHaveLength(2);
for (const [, , archBlock] of macArchBlocks) {
expect(Array.from(archBlock.matchAll(/-\s*(x64|arm64)/g), (entry) => entry[1]).sort()).toEqual([
"arm64",
"x64",
]);
}
const linuxSectionMatch = builderConfig.match(/linux:\s*[\s\S]*$/m);
expect(linuxSectionMatch?.[0]).toBeDefined();
@@ -120,6 +132,14 @@ describe("electron-builder desktop config", () => {
}
});
it("unpacks native embedded Postgres processes from app.asar", async () => {
const builderConfig = await readDesktopFile("electron-builder.yml");
expect(builderConfig).toMatch(
/asarUnpack:\s*[\s\S]*?-\s*"node_modules\/@embedded-postgres\/\*\/native\/\*\*\/\*"/m,
);
});
it("packages @fusion/core runtime dependencies used during desktop startup", async () => {
const builderConfig = await readDesktopFile("electron-builder.yml");
const requiredRuntimeDependencyGlobs = [

View File

@@ -41,6 +41,28 @@ describe("desktop release workflow wiring", () => {
}
});
it("boots embedded Postgres on every native desktop release host", async () => {
const release = await readRepoFile(".github/workflows/release.yml");
const testRelease = await readRepoFile(".github/workflows/test-release.yml");
const manualWindows = await readRepoFile(".github/workflows/desktop-windows.yml");
const advisoryPackaging = await readRepoFile(".github/workflows/desktop-packaging.yml");
// FNXC:DesktopEmbeddedPostgres 2026-07-14-09:40:
// Installer assembly alone cannot prove native Postgres starts. Keep a real
// lifecycle smoke on Windows, macOS, and Linux release hosts, plus the
// isolated Windows and advisory packaging paths.
for (const workflow of [release, testRelease]) {
for (const platform of ["Windows", "macOS", "Linux"]) {
expect(workflow).toContain(`Smoke embedded Postgres on ${platform}`);
}
expect(workflow.match(/pnpm --filter @fusion\/core test:embedded-postgres/g)).toHaveLength(3);
}
expect(manualWindows).toContain("Smoke embedded Postgres on Windows");
expect(manualWindows).toContain("pnpm --filter @fusion/core test:embedded-postgres");
expect(advisoryPackaging).toContain("Smoke embedded Postgres lifecycle");
expect(advisoryPackaging).toContain("pnpm --filter @fusion/core test:embedded-postgres");
});
it("wires release aggregation to include desktop assets across platforms", async () => {
const release = await readRepoFile(".github/workflows/release.yml");

View File

@@ -6,10 +6,26 @@ ignoredBuiltDependencies:
- sharp
- ssh2
onlyBuiltDependencies:
- '@embedded-postgres/darwin-arm64'
- '@embedded-postgres/darwin-x64'
- '@embedded-postgres/linux-arm64'
- '@embedded-postgres/linux-x64'
- '@embedded-postgres/windows-x64'
- '@homebridge/node-pty-prebuilt-multiarch'
- esbuild
- koffi
- protobufjs
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:30:
# Desktop release jobs cross-build Intel and ARM64 artifacts on one host. Install
# optional native payloads for both CPU families on the current OS so each
# packaged macOS/Linux app contains the Postgres binaries selected at runtime.
# Upstream publishes Windows x64 only, so unsupported Windows ARM64 is not listed.
supportedArchitectures:
os:
- current
cpu:
- x64
- arm64
packages:
- "packages/*"
- "plugins/examples/*"