GitNexus/gitnexus-desktop/scripts/smoke-unpacked.mjs
Sparsh 610a102397 feat: add validation job to GitHub Actions for desktop packaging
- Introduced a new job `validate` in the GitHub Actions workflow to validate the desktop package.
- Added steps for checking out the repository, setting up Node.js, installing dependencies, type checking, and running unit tests.
- Updated the `package` job to depend on the `validate` job.

chore: update package-lock.json and package.json for vitest integration

- Added `vitest` as a development dependency in `package.json`.
- Updated `typecheck` script to include a new TypeScript configuration for vitest.
- Updated `package-lock.json` to reflect the new dependencies.

refactor: modularize runtime path handling in main process

- Created a new module `runtime-paths.ts` to handle static path normalization and request handling.
- Refactored `main.ts` to utilize the new `normalizeStaticPath`, `getRequestedPath`, and `getPackagedRendererEntry` functions.

test: add unit tests for runtime path utilities

- Implemented tests for `normalizeStaticPath`, `getRequestedPath`, and `getPackagedRendererEntry` using Vitest.
- Ensured coverage for directory traversal attempts and null-byte paths.

chore: configure Vitest for testing

- Added `vitest.config.ts` for Vitest configuration.
- Created `tsconfig.vitest.json` to include types for Vitest in TypeScript compilation.

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 13:45:23 +05:30

185 lines
5.2 KiB
JavaScript

import { spawn, spawnSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageRoot = path.resolve(__dirname, '..');
const releaseRoot = path.join(packageRoot, 'release');
const latestReleasePointerPath = path.join(releaseRoot, '.latest-unpacked-release');
const productName = 'GitNexus Desktop';
const smokeTimeoutMs = 120_000;
const releaseDirectoryPattern = /^\d{4}-\d{2}-\d{2}T.*Z$/;
const listTimestampedReleaseDirs = () => {
if (!existsSync(releaseRoot)) {
return [];
}
return readdirSync(releaseRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && releaseDirectoryPattern.test(entry.name))
.map((entry) => path.join(releaseRoot, entry.name))
.sort((leftPath, rightPath) => statSync(rightPath).mtimeMs - statSync(leftPath).mtimeMs);
};
const findFirstMatch = (rootDir, predicate) => {
for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
const entryPath = path.join(rootDir, entry.name);
if (predicate(entry, entryPath)) {
return entryPath;
}
if (entry.isDirectory()) {
const nestedMatch = findFirstMatch(entryPath, predicate);
if (nestedMatch) {
return nestedMatch;
}
}
}
return null;
};
const resolveExecutablePath = (releaseDir) => {
if (process.platform === 'win32') {
return path.join(releaseDir, 'win-unpacked', `${productName}.exe`);
}
if (process.platform === 'darwin') {
return findFirstMatch(releaseDir, (entry, entryPath) => {
return (
entry.isFile() &&
entry.name === productName &&
entryPath.includes(`${productName}.app${path.sep}Contents${path.sep}MacOS${path.sep}`)
);
});
}
const linuxUnpackedDir = path.join(releaseDir, 'linux-unpacked');
if (!existsSync(linuxUnpackedDir)) {
return null;
}
const candidateNames = [productName, 'gitnexus-desktop'];
for (const candidateName of candidateNames) {
const candidatePath = path.join(linuxUnpackedDir, candidateName);
if (existsSync(candidatePath) && statSync(candidatePath).isFile()) {
return candidatePath;
}
}
return findFirstMatch(linuxUnpackedDir, (entry, entryPath) => {
return entry.isFile() && path.extname(entryPath) === '';
});
};
const resolveLatestExecutable = () => {
if (existsSync(latestReleasePointerPath)) {
const releaseDir = readFileSync(latestReleasePointerPath, 'utf8').trim();
const executablePath = releaseDir ? resolveExecutablePath(releaseDir) : null;
return {
executablePath: executablePath && existsSync(executablePath) ? executablePath : null,
releaseDir: releaseDir || null,
};
}
const releaseDirs = listTimestampedReleaseDirs();
for (const releaseDir of releaseDirs) {
const executablePath = resolveExecutablePath(releaseDir);
if (executablePath && existsSync(executablePath)) {
return { executablePath, releaseDir };
}
}
return {
executablePath: null,
releaseDir: releaseDirs[0] ?? null,
};
};
const prepareExecutableForSmokeTest = (executablePath) => {
if (process.platform !== 'darwin') {
return;
}
const result = spawnSync('xattr', ['-cr', executablePath], { stdio: 'inherit' });
if (result.error) {
throw result.error;
}
if ((result.status ?? 0) !== 0) {
throw new Error(`Failed to clear macOS quarantine attributes for ${executablePath}.`);
}
};
const getSmokeTestArguments = () => {
return process.platform === 'linux' ? ['--no-sandbox'] : [];
};
const runSmokeTest = async () => {
const { executablePath, releaseDir: latestReleaseDir } = resolveLatestExecutable();
if (!latestReleaseDir) {
throw new Error(
'No timestamped desktop release directory was found. Run npm run build:dir first.',
);
}
if (!executablePath || !existsSync(executablePath)) {
throw new Error(`Unable to locate the unpacked desktop executable under ${latestReleaseDir}.`);
}
prepareExecutableForSmokeTest(executablePath);
console.info(`[gitnexus-desktop] Smoke testing unpacked app: ${executablePath}`);
await new Promise((resolve, reject) => {
const childProcess = spawn(executablePath, getSmokeTestArguments(), {
cwd: path.dirname(executablePath),
env: {
...process.env,
GITNEXUS_DESKTOP_SMOKE_TEST: '1',
},
stdio: 'inherit',
windowsHide: true,
});
const timeoutHandle = setTimeout(() => {
childProcess.kill(process.platform === 'win32' ? undefined : 'SIGTERM');
reject(
new Error(`Timed out waiting for unpacked desktop smoke test after ${smokeTimeoutMs}ms.`),
);
}, smokeTimeoutMs);
childProcess.once('error', (error) => {
clearTimeout(timeoutHandle);
reject(error);
});
childProcess.once('exit', (code, signal) => {
clearTimeout(timeoutHandle);
if (code === 0) {
resolve(undefined);
return;
}
reject(
new Error(
`Unpacked desktop smoke test exited with code ${code ?? 'null'}${signal ? ` (signal: ${signal})` : ''}.`,
),
);
});
});
};
await runSmokeTest();