build(desktop): isolate local release staging

* build(desktop): isolate local release staging

* Require Apple Silicon for local macOS smoke

---------

Co-authored-by: bradgroux <brad@digitalmeld.io>
This commit is contained in:
Brad Groux 2026-06-09 05:47:35 -05:00 committed by GitHub
parent ccdd55ce0b
commit 8b0ec6836b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 191 additions and 14 deletions

View file

@ -14,15 +14,15 @@
"dev:fresh": "VERITAS_DESKTOP_PROFILE=fresh electron-vite dev",
"build": "electron-vite build",
"package:prepare": "node ../scripts/prepare-desktop-release.mjs",
"package:mac:dir": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --mac dir --publish never --config.mac.identity=null --config.mac.notarize=false",
"package:mac:unsigned": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --mac dmg zip --publish never --config.mac.identity=null --config.mac.notarize=false",
"package:linux:dir": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --linux dir --x64 --publish never",
"package:linux:unsigned": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --linux AppImage deb rpm --x64 --publish never",
"package:win:dir": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --win dir --x64 --publish never",
"package:win:unsigned": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --win nsis zip --x64 --publish never",
"release:mac": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --mac dmg zip --publish always",
"release:linux": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --linux AppImage deb rpm --x64 --publish always",
"release:win": "node ../scripts/prepare-desktop-release.mjs && node ./node_modules/electron-builder/cli.js --win nsis zip --x64 --publish always",
"package:mac:dir": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --mac dir --publish never --config.mac.identity=null --config.mac.notarize=false",
"package:mac:unsigned": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --mac dmg zip --publish never --config.mac.identity=null --config.mac.notarize=false",
"package:linux:dir": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --linux dir --x64 --publish never",
"package:linux:unsigned": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --linux AppImage deb rpm --x64 --publish never",
"package:win:dir": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --win dir --x64 --publish never",
"package:win:unsigned": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --win nsis zip --x64 --publish never",
"release:mac": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --mac dmg zip --publish always",
"release:linux": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --linux AppImage deb rpm --x64 --publish always",
"release:win": "node ../scripts/prepare-desktop-release.mjs && node ../scripts/run-desktop-builder.mjs --win nsis zip --x64 --publish always",
"typecheck": "tsc --noEmit",
"lint": "eslint src electron.vite.config.ts --ext .ts",
"test": "vitest run --config vitest.config.ts",

View file

@ -20,6 +20,7 @@ pnpm desktop:package:windows:unsigned
pnpm desktop:release:mac
pnpm desktop:release:linux
pnpm desktop:release:windows
pnpm desktop:smoke:mac:local
```
`desktop:package:mac:dir` creates an unpacked local app for fast inspection.
@ -40,6 +41,13 @@ The package step builds the workspace, stages the production server runtime in
`desktop/.desktop-release/web`, and writes artifacts to `desktop/release/`.
Both staging and release directories are ignored by git.
`pnpm desktop:smoke:mac:local` runs the macOS directory packaging path and then
asserts that root dev tooling such as Prettier, ESLint, and the desktop
`electron-builder` dependency still exist. Use it after package-script changes
that touch production staging or pnpm deploy behavior. The staging deploy runs
from an isolated temporary workspace so `pnpm deploy --prod` cannot rewrite the
live repository install into a production-only dependency state.
## GitHub Workflows
`Desktop Artifacts` runs on desktop/server/web/shared changes and on manual
@ -161,6 +169,8 @@ policy is tracked in
- Update `CHANGELOG.md`.
- Run `pnpm typecheck`, `pnpm lint:budget`, `pnpm build`, and
`pnpm test:unit`.
- Run `pnpm desktop:smoke:mac:local` to verify local packaging does not prune
root dev tooling.
- Run `pnpm desktop:package:mac:unsigned` and inspect artifact names.
- Run `pnpm desktop:package:linux:unsigned` on Linux or the
`Desktop Artifacts` Linux job and inspect preview artifact names. This is not

View file

@ -50,7 +50,8 @@
"qmd:refresh": "bash scripts/qmd/refresh-veritas-qmd.sh",
"seed": "bash scripts/seed.sh",
"prepare": "node scripts/prepare-husky.mjs",
"demo": "docker compose -f demo/docker-compose.demo.yml up --build"
"demo": "docker compose -f demo/docker-compose.demo.yml up --build",
"desktop:smoke:mac:local": "node scripts/check-desktop-local-packaging-smoke.mjs"
},
"devDependencies": {
"@eslint/js": "9.38.0",

View file

@ -0,0 +1,94 @@
#!/usr/bin/env node
import { constants } from 'node:fs';
import { access, stat } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const desktopDir = path.join(rootDir, 'desktop');
const stagingDir = path.join(desktopDir, '.desktop-release');
async function assertExists(label, targetPath) {
try {
await access(targetPath, constants.F_OK);
} catch {
throw new Error(`${label} is missing: ${path.relative(rootDir, targetPath)}`);
}
}
async function snapshotPath(label, targetPath) {
await assertExists(label, targetPath);
const info = await stat(targetPath);
return { label, targetPath, mtimeMs: info.mtimeMs };
}
function run(command, args) {
const result = spawnSync(command, args, {
cwd: rootDir,
env: {
...process.env,
CI: 'true',
npm_config_confirm_modules_purge: 'false',
npm_config_confirmModulesPurge: 'false',
NPM_CONFIG_CONFIRM_MODULES_PURGE: 'false',
},
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`);
}
}
async function assertRootDevToolingStillPresent(beforeSnapshots) {
for (const snapshot of beforeSnapshots) {
await assertExists(snapshot.label, snapshot.targetPath);
}
}
async function main() {
if (process.platform !== 'darwin' || process.arch !== 'arm64') {
throw new Error(
'desktop:smoke:mac:local must run on Apple Silicon macOS because it exercises macOS arm64 packaging.'
);
}
const beforeSnapshots = await Promise.all([
snapshotPath('Root node_modules', path.join(rootDir, 'node_modules')),
snapshotPath('Root prettier binary', path.join(rootDir, 'node_modules/.bin/prettier')),
snapshotPath('Root eslint binary', path.join(rootDir, 'node_modules/.bin/eslint')),
snapshotPath(
'Desktop electron-builder package',
path.join(desktopDir, 'node_modules/electron-builder/package.json')
),
]);
run('pnpm', ['build']);
run('pnpm', ['--filter', '@veritas-kanban/desktop', 'package:mac:dir']);
await assertRootDevToolingStillPresent(beforeSnapshots);
await assertExists('Desktop staging server entry', path.join(stagingDir, 'server/dist/index.js'));
await assertExists(
'Desktop staging server dependencies',
path.join(stagingDir, 'server/node_modules')
);
await assertExists('Desktop staging web app', path.join(stagingDir, 'web/dist/index.html'));
await assertExists(
'Unpacked macOS app',
path.join(desktopDir, 'release/mac-arm64/Veritas Kanban.app')
);
console.log(
'Desktop local packaging smoke passed: root dev tooling survived isolated production staging.'
);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});

View file

@ -1,7 +1,8 @@
#!/usr/bin/env node
import { constants } from 'node:fs';
import { access, cp, mkdir, rm } from 'node:fs/promises';
import { access, cp, mkdir, mkdtemp, rm } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -34,9 +35,9 @@ function releaseChildEnv() {
};
}
function run(command, args) {
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: rootDir,
cwd: options.cwd ?? rootDir,
encoding: 'utf8',
env: releaseChildEnv(),
shell: process.platform === 'win32',
@ -83,6 +84,41 @@ async function materializeServerSelfReference() {
});
}
async function copyWorkspaceForDeploy(tempRoot) {
const entries = [
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'package.json',
'scripts/prepare-husky.mjs',
'server/package.json',
'server/dist',
'shared/package.json',
'shared/dist',
];
for (const entry of entries) {
const source = path.join(rootDir, entry);
const target = path.join(tempRoot, entry);
await mkdir(path.dirname(target), { recursive: true });
await cp(source, target, { recursive: true });
}
}
async function deployServerRuntime() {
const deployRoot = await mkdtemp(path.join(os.tmpdir(), 'veritas-desktop-deploy-'));
try {
await copyWorkspaceForDeploy(deployRoot);
run(
'pnpm',
['--filter', '@veritas-kanban/server', 'deploy', '--legacy', '--prod', serverStage],
{ cwd: deployRoot }
);
} finally {
await rm(deployRoot, { recursive: true, force: true });
}
}
async function main() {
await assertExists('Server build output', path.join(rootDir, 'server/dist/index.js'));
await assertExists('Web build output', path.join(rootDir, 'web/dist/index.html'));
@ -90,7 +126,7 @@ async function main() {
await rm(stagingDir, { recursive: true, force: true });
await mkdir(stagingDir, { recursive: true });
run('pnpm', ['--filter', '@veritas-kanban/server', 'deploy', '--legacy', '--prod', serverStage]);
await deployServerRuntime();
await pruneServerDeploy();
await materializeServerSelfReference();

36
scripts/run-desktop-builder.mjs Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const desktopDir = path.join(rootDir, 'desktop');
const requireFromDesktop = createRequire(path.join(desktopDir, 'package.json'));
function resolveElectronBuilderCli() {
try {
return requireFromDesktop.resolve('electron-builder/cli.js');
} catch (error) {
throw new Error(
[
'Unable to resolve electron-builder from the desktop package dependency graph.',
'Run `CI=true pnpm install --frozen-lockfile` from the repository root, then retry the desktop packaging command.',
error instanceof Error ? error.message : String(error),
].join('\n')
);
}
}
const cli = resolveElectronBuilderCli();
const result = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], {
cwd: desktopDir,
env: process.env,
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);