177 lines
3.9 KiB
TypeScript
177 lines
3.9 KiB
TypeScript
import { execFile } from 'child_process';
|
|
import path from 'path';
|
|
import { pathToFileURL } from 'url';
|
|
export { computeVerificationSnapshotId } from '../../../shared/verification-snapshot.js';
|
|
|
|
export const VERIFICATION_PROFILES = [
|
|
'test',
|
|
'typecheck',
|
|
'build',
|
|
'lint',
|
|
] as const;
|
|
|
|
export type VerificationProfile = (typeof VERIFICATION_PROFILES)[number];
|
|
|
|
type VerificationHelperEnvKey =
|
|
| 'PATH'
|
|
| 'HOME'
|
|
| 'USER'
|
|
| 'LOGNAME'
|
|
| 'SHELL'
|
|
| 'LANG'
|
|
| 'LC_ALL'
|
|
| 'LC_CTYPE'
|
|
| 'NODE_PATH'
|
|
| 'NODE_OPTIONS'
|
|
| 'TERM'
|
|
| 'COLORTERM'
|
|
| 'FORCE_COLOR';
|
|
|
|
const VERIFICATION_HELPER_ENV_KEYS: readonly VerificationHelperEnvKey[] = [
|
|
'PATH',
|
|
'HOME',
|
|
'USER',
|
|
'LOGNAME',
|
|
'SHELL',
|
|
'LANG',
|
|
'LC_ALL',
|
|
'LC_CTYPE',
|
|
'NODE_PATH',
|
|
'NODE_OPTIONS',
|
|
'TERM',
|
|
'COLORTERM',
|
|
'FORCE_COLOR',
|
|
];
|
|
const HELPER_TIMEOUT_MS = 20 * 60 * 1000;
|
|
const HELPER_MAX_BUFFER = 20 * 1024 * 1024;
|
|
|
|
export interface VerificationRequest {
|
|
requestId: string;
|
|
profile: VerificationProfile;
|
|
expectedSnapshotId?: string;
|
|
}
|
|
|
|
export interface VerificationResponse {
|
|
requestId: string;
|
|
ok: boolean;
|
|
profile: VerificationProfile;
|
|
command: string;
|
|
stdout: string;
|
|
stderr: string;
|
|
exitCode: number;
|
|
snapshotId: string;
|
|
runtimeVersion: string;
|
|
workdir?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export function isVerificationProfile(
|
|
value: unknown,
|
|
): value is VerificationProfile {
|
|
return (
|
|
typeof value === 'string' &&
|
|
VERIFICATION_PROFILES.includes(value as VerificationProfile)
|
|
);
|
|
}
|
|
|
|
function buildVerificationHelperEnv(): NodeJS.ProcessEnv {
|
|
const env: NodeJS.ProcessEnv = {
|
|
TZ: 'Asia/Seoul',
|
|
CI: '1',
|
|
};
|
|
|
|
for (const key of VERIFICATION_HELPER_ENV_KEYS) {
|
|
const value = process.env[key];
|
|
if (value) {
|
|
env[key] = value;
|
|
}
|
|
}
|
|
|
|
return env;
|
|
}
|
|
|
|
function execFileCapture(
|
|
file: string,
|
|
args: string[],
|
|
options?: {
|
|
cwd?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
},
|
|
): Promise<{ stdout: string; stderr: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
execFile(
|
|
file,
|
|
args,
|
|
{
|
|
encoding: 'utf8',
|
|
timeout: HELPER_TIMEOUT_MS,
|
|
maxBuffer: HELPER_MAX_BUFFER,
|
|
cwd: options?.cwd,
|
|
env: options?.env,
|
|
},
|
|
(error, stdout, stderr) => {
|
|
if (error) {
|
|
reject(
|
|
Object.assign(error, {
|
|
stdout,
|
|
stderr,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
resolve({ stdout, stderr });
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
export async function runVerificationRequestDirect(
|
|
repoRoot: string,
|
|
request: VerificationRequest,
|
|
): Promise<VerificationResponse> {
|
|
const helperPath = path.join(
|
|
repoRoot,
|
|
'shared',
|
|
'verification-request-runner.js',
|
|
);
|
|
const helperEnv = buildVerificationHelperEnv();
|
|
const { stdout, stderr } = await execFileCapture(
|
|
'bun',
|
|
[helperPath, repoRoot, JSON.stringify(request)],
|
|
{
|
|
cwd: repoRoot,
|
|
env: helperEnv,
|
|
},
|
|
);
|
|
|
|
try {
|
|
return JSON.parse(stdout) as VerificationResponse;
|
|
} catch (error) {
|
|
const detail = stderr.trim() || stdout.trim();
|
|
throw new Error(
|
|
`Failed to parse verification response from ${pathToFileURL(helperPath).href}: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}${detail ? `\n${detail}` : ''}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function formatVerificationResponse(
|
|
response: VerificationResponse,
|
|
): string {
|
|
const parts = [
|
|
`Verification profile: ${response.profile}`,
|
|
`Snapshot: ${response.snapshotId}`,
|
|
`Runtime: ${response.runtimeVersion}`,
|
|
`Exit code: ${response.exitCode}`,
|
|
response.workdir ? `Workdir: ${response.workdir}` : null,
|
|
response.command ? `$ ${response.command}` : null,
|
|
response.stdout ? response.stdout.trimEnd() : null,
|
|
response.stderr ? `[stderr]\n${response.stderr.trimEnd()}` : null,
|
|
response.error ? `[error] ${response.error}` : null,
|
|
].filter((value): value is string => Boolean(value));
|
|
|
|
return parts.join('\n');
|
|
}
|