open-design/e2e/scripts/release-smoke.ts
PerishFire 976edaf38e
test: harden e2e smoke and release reports (#1140)
* test: harden e2e inspect specs

* test: wire e2e release reports

* chore: bump packaged beta base to 0.6.1

* test: run release smoke vitest directly

* test: add suite-owned tools-dev lifecycle

* ci: harden stable release packaging

* fix(release,e2e): gate stable signing on verify and harden suite cleanup

- restore `needs: [metadata, verify]` on the stable release `build_mac`,
  `build_mac_intel`, `build_win`, and `build_linux` jobs so Apple
  signing/notarization and Windows release builds cannot run before
  pnpm guard, typecheck, and layout checks complete on the metadata commit.
- in `runToolsDevSuite`, drop the `started` flag and always attempt
  `stopToolsDevWeb` in `finally`; record stop errors in diagnostics, and
  when the test body succeeded, escalate the stop failure to the suite
  result and rethrow — so orphan daemon/web processes from an interrupted
  `startToolsDevWeb` or a broken shutdown can no longer pass silently.

Addresses PR #1140 review feedback from lefarcen and mrcfps.
2026-05-11 13:11:16 +08:00

143 lines
4.7 KiB
TypeScript

import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createReport } from '../lib/vitest/report.ts';
type Platform = 'mac' | 'win';
const e2eRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const workspaceRoot = dirname(e2eRoot);
async function main(): Promise<void> {
const platform = parsePlatform(process.argv[2]);
const spec = process.argv[3] ?? defaultSpec(platform);
const namespace = process.env.OD_PACKAGED_E2E_NAMESPACE ?? defaultNamespace(platform);
const reportRoot = resolveFromWorkspace(
process.env.OD_PACKAGED_E2E_REPORT_DIR ?? join('.tmp', 'release-report', platform),
);
const report = await createReport(reportRoot);
process.env.OD_PACKAGED_E2E_REPORT_DIR = report.root;
await report.json('manifest.json', {
...(process.env.OD_PACKAGED_E2E_RELEASE_CHANNEL == null
? {}
: { channel: process.env.OD_PACKAGED_E2E_RELEASE_CHANNEL }),
...(process.env.OD_PACKAGED_E2E_RELEASE_VERSION == null
? {}
: { releaseVersion: process.env.OD_PACKAGED_E2E_RELEASE_VERSION }),
commit: process.env.GITHUB_SHA ?? null,
generatedAt: new Date().toISOString(),
githubRunAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null,
githubRunId: process.env.GITHUB_RUN_ID ?? null,
namespace,
platform,
reportPath: report.root,
screenshot: `screenshots/open-design-${platform}-smoke.png`,
spec,
});
await saveRequiredSource(report, 'tools-pack.json', process.env.OD_PACKAGED_E2E_BUILD_JSON_PATH);
await saveOptionalSource(report, 'tools-pack.log', process.env.OD_PACKAGED_E2E_BUILD_LOG_PATH);
const startedAt = Date.now();
const result = await runVitest(spec).catch((error: unknown) => ({
exitCode: 1,
log: formatUnknown(error),
}));
await report.save('vitest.log', result.log);
await report.json('suite-result.json', {
durationMs: Date.now() - startedAt,
exitCode: result.exitCode,
namespace,
platform,
reportPath: report.root,
spec,
status: result.exitCode === 0 ? 'success' : 'failed',
timestamp: new Date().toISOString(),
});
if (result.exitCode !== 0) {
process.exitCode = result.exitCode;
}
}
async function saveRequiredSource(
report: Awaited<ReturnType<typeof createReport>>,
relpath: string,
sourcePath: string | undefined,
): Promise<void> {
if (sourcePath == null || sourcePath === '') {
throw new Error(`missing source path for ${relpath}`);
}
const resolved = resolveFromWorkspace(sourcePath);
if (!existsSync(resolved)) {
throw new Error(`source file for ${relpath} does not exist: ${resolved}`);
}
await report.save(relpath, await readFile(resolved));
}
async function saveOptionalSource(
report: Awaited<ReturnType<typeof createReport>>,
relpath: string,
sourcePath: string | undefined,
): Promise<void> {
if (sourcePath == null || sourcePath === '') return;
const resolved = resolveFromWorkspace(sourcePath);
if (!existsSync(resolved)) return;
await report.save(relpath, await readFile(resolved));
}
async function runVitest(spec: string): Promise<{ exitCode: number; log: string }> {
const chunks: string[] = [];
const child = spawn(process.execPath, [join(e2eRoot, 'node_modules', 'vitest', 'vitest.mjs'), 'run', '-c', 'vitest.config.ts', spec], {
cwd: e2eRoot,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.on('data', (chunk: Buffer) => {
chunks.push(chunk.toString('utf8'));
process.stdout.write(chunk);
});
child.stderr.on('data', (chunk: Buffer) => {
chunks.push(chunk.toString('utf8'));
process.stderr.write(chunk);
});
const exitCode = await new Promise<number>((resolveExit, rejectExit) => {
child.once('error', rejectExit);
child.once('exit', (code) => resolveExit(code ?? 1));
});
return { exitCode, log: chunks.join('') };
}
function parsePlatform(value: string | undefined): Platform {
if (value === 'mac' || value === 'win') return value;
throw new Error('usage: tsx scripts/release-smoke.ts <mac|win> [spec]');
}
function defaultSpec(platform: Platform): string {
return platform === 'mac' ? 'specs/mac.spec.ts' : 'specs/win.spec.ts';
}
function defaultNamespace(platform: Platform): string {
return platform === 'mac' ? 'release-beta' : 'release-beta-win';
}
function resolveFromWorkspace(path: string): string {
return isAbsolute(path) ? path : resolve(workspaceRoot, path);
}
function formatUnknown(value: unknown): string {
if (value instanceof Error) return value.stack ?? `${value.name}: ${value.message}`;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
await main();