72 lines
4 KiB
TypeScript
72 lines
4 KiB
TypeScript
// The pre-upload check of `apple upload` and `play upload`: a shell command the project owns (a store-readiness gate,
|
|
// a policy scan of the binary) runs before any byte is sent.
|
|
// --pre-check "<cmd with {file}>" (config: upload.preCheck) {file} is replaced by the quoted IPA / AAB path
|
|
// --evidence <path.json> (config: upload.evidence) what ran, written also when the check fails
|
|
// A nonzero exit refuses the upload: ShipStepError PRE_CHECK_FAILED (exit 1 under the ship-step contract), nothing
|
|
// is sent. The evidence ({file, sha256, size, command, exitCode, stdoutTail, timestamp, storeboundVersion}) goes to
|
|
// --evidence and into the --json / --result-file result as `preCheck`.
|
|
import { createHash } from 'node:crypto';
|
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
import { basename, dirname } from 'node:path';
|
|
import type { Args } from './args';
|
|
import type { ShipConfig } from './config';
|
|
import { log } from './log';
|
|
import { pump } from './runner';
|
|
import * as rt from './rt';
|
|
import { SHIP_EXIT, ShipStepError, shipExtra, shipPhase } from './ship-step';
|
|
|
|
export interface PreCheckEvidence {
|
|
file: string; sha256: string; size: number; command: string; exitCode: number;
|
|
stdoutTail: string[]; timestamp: string; storeboundVersion: string;
|
|
}
|
|
|
|
/** The command with {file} replaced by the POSIX-quoted path (sh -c); a command without {file} gets none. */
|
|
export function preCheckCommand(template: string, file: string): string {
|
|
const q = `'${file.replace(/\\/g, '/').replace(/'/g, `'\\''`)}'`;
|
|
return template.split('{file}').join(q);
|
|
}
|
|
|
|
function sha256File(path: string): string { return createHash('sha256').update(readFileSync(path)).digest('hex'); }
|
|
|
|
/**
|
|
* Runs the pre-check for `file` when --pre-check / upload.preCheck is set (else returns null). Throws PRE_CHECK_FAILED
|
|
* on a nonzero exit, after writing the evidence. `shown` is the file name for the evidence (not the absolute path).
|
|
*/
|
|
export async function runPreCheck(o: { args: Args; config: ShipConfig; root: string; resolve: (p: string) => string; file: string; shown: string; tag: string }): Promise<PreCheckEvidence | null> {
|
|
const flag = o.args.get('pre-check');
|
|
if (flag === 'true') throw new Error('--pre-check "<command with {file}>" needs a command');
|
|
const template = (flag ?? o.config.upload?.preCheck)?.trim();
|
|
const evFlag = o.args.get('evidence');
|
|
if (evFlag === 'true') throw new Error('--evidence <path.json> needs a path');
|
|
const evPath = evFlag ?? o.config.upload?.evidence;
|
|
if (!template) {
|
|
if (evFlag) throw new Error('--evidence needs --pre-check (or upload.preCheck in the config)');
|
|
return null;
|
|
}
|
|
const command = preCheckCommand(template, o.file);
|
|
shipPhase('pre-check', command);
|
|
log(o.tag, `pre-check: $ ${command}`);
|
|
const p = rt.spawn(['sh', '-c', command], { cwd: o.root, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', env: process.env });
|
|
const tail: string[] = [];
|
|
const each = (line: string, kind: 'stdout' | 'stderr'): void => {
|
|
if (kind === 'stdout') { tail.push(line); if (tail.length > 20) tail.shift(); }
|
|
console.error(` ${line}`);
|
|
};
|
|
await Promise.all([pump(p.stdout, 'stdout', each), pump(p.stderr, 'stderr', each)]);
|
|
const exitCode = Number(await p.exited);
|
|
const version = ((await import('../../package.json')) as { version: string }).version;
|
|
const ev: PreCheckEvidence = {
|
|
file: o.shown.split('\\').join('/'), sha256: sha256File(o.file), size: statSync(o.file).size, command, exitCode,
|
|
stdoutTail: tail, timestamp: new Date().toISOString(), storeboundVersion: version,
|
|
};
|
|
if (evPath) {
|
|
const abs = o.resolve(evPath);
|
|
mkdirSync(dirname(abs), { recursive: true });
|
|
writeFileSync(abs, JSON.stringify(ev, null, 2) + '\n');
|
|
log(o.tag, `pre-check evidence: ${basename(abs)}`);
|
|
}
|
|
shipExtra({ preCheck: ev });
|
|
if (exitCode !== 0) throw new ShipStepError(`pre-check failed (exit ${exitCode}), nothing was uploaded: ${command}${tail.length ? `\n${tail.slice(-5).join('\n')}` : ''}`, SHIP_EXIT.failure, 'PRE_CHECK_FAILED');
|
|
log(o.tag, 'pre-check: ok');
|
|
return ev;
|
|
}
|