a7711d2dc3
新增 pingora-gateway 独立二进制 crate,覆盖路由、静态资源、压缩、接流保护、TLS 直连和访问日志能力。 新增 Nginx canary、realpath canary、direct preflight、direct live、direct enable 和 rollback 脚本。 新增 Pingora 切流证据包、命令证据、manifest 验真、根目录总审计和 release readiness 聚合门禁。 完善 API release、Jenkins、systemd、health patrol、生产部署和发布包自包含校验。 更新 Pingora 试点文档、Nginx README 与 Hermes 共享记忆。
909 lines
26 KiB
JavaScript
909 lines
26 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const COMMAND_EVIDENCE_SCRIPT =
|
|
'scripts/ops/pingora-cutover-command-evidence.mjs';
|
|
const failures = [];
|
|
const tmpRoot = mkdtempSync(
|
|
path.join(tmpdir(), 'genarrative-pingora-cutover-command-evidence-'),
|
|
);
|
|
|
|
try {
|
|
main();
|
|
} finally {
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error('[check:pingora-cutover-command-evidence] FAILED');
|
|
for (const failure of failures) {
|
|
console.error(`- ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('[check:pingora-cutover-command-evidence] OK');
|
|
|
|
function main() {
|
|
assertScriptShape();
|
|
assertCommandEvidenceSucceedsAndWritesArtifacts();
|
|
assertCommandEvidenceWritesCutoverRunId();
|
|
assertCommandFailureStillWritesEvidenceAndFails();
|
|
assertCommandEvidenceRedactsProbeTokensFromArtifacts();
|
|
assertRejectsUnsafeNames();
|
|
assertRejectsRelativeOutputRoot();
|
|
assertRejectsFilesystemRootOutputRoot();
|
|
assertRejectsOutputRootWithControlCharacters();
|
|
assertRejectsSymlinkOutputRootBeforeCommand();
|
|
assertRejectsFileOutputRootBeforeCommand();
|
|
assertRejectsRelativeCommandPath();
|
|
assertRejectsBareCommandName();
|
|
assertRejectsFilesystemRootCommand();
|
|
assertRejectsCommandWithControlCharacters();
|
|
assertRejectsCommandArgWithControlCharacters();
|
|
assertRejectsRelativeExpectedExecutable();
|
|
assertRejectsFilesystemRootExpectedExecutable();
|
|
assertRejectsExpectedExecutableWithControlCharacters();
|
|
assertRejectsMismatchedExpectedExecutableBeforeCommand();
|
|
assertRejectsMissingRequiredArgBeforeCommand();
|
|
}
|
|
|
|
function assertScriptShape() {
|
|
const content = readFileSync(COMMAND_EVIDENCE_SCRIPT, 'utf8');
|
|
assertIncludes(
|
|
content,
|
|
'command.stdout.txt',
|
|
'命令证据脚本必须保存真实命令 stdout。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'command.stderr.txt',
|
|
'命令证据脚本必须保存真实命令 stderr。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'command-record.json',
|
|
'命令证据脚本必须保存结构化命令记录。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'manifest.json',
|
|
'命令证据脚本必须写 manifest。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'cutoverRunId',
|
|
'命令证据脚本必须支持写入切换批次 ID。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--cutover-run-id',
|
|
'命令证据脚本 usage 必须公开切换批次 ID 参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--expected-executable',
|
|
'命令证据脚本 usage 必须公开预期真实命令参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--require-arg',
|
|
'命令证据脚本 usage 必须公开真实命令必需参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'SECRET_VALUE_FLAGS',
|
|
'命令证据脚本必须集中维护敏感参数脱敏列表。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'redactSecrets(text',
|
|
'命令证据脚本必须脱敏 stdout/stderr 中的敏感值。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
"flag: 'wx'",
|
|
'命令证据脚本写证据文件时必须避免覆盖既有文件。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'BUNDLE_DIR_MODE = 0o750',
|
|
'命令证据目录必须显式固定权限。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'EVIDENCE_FILE_MODE = 0o640',
|
|
'命令证据文件必须显式固定权限。',
|
|
);
|
|
}
|
|
|
|
function assertCommandEvidenceSucceedsAndWritesArtifacts() {
|
|
const fixture = prepareFixture('success');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--phase',
|
|
'enable-apply',
|
|
'--command-name',
|
|
'pingora-direct-enable-apply',
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--expected-executable',
|
|
fixture.commandScript,
|
|
'--',
|
|
fixture.commandScript,
|
|
'--message',
|
|
'hello-command',
|
|
]);
|
|
|
|
assertStatus(result, 0, '真实命令成功时命令证据脚本应成功退出。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'hello-command',
|
|
'命令证据脚本必须把真实命令 stdout 透传到终端。',
|
|
);
|
|
const output = parseLastJsonObject(result.stdout, '成功命令证据 stdout');
|
|
assertEqual(output.status, 'OK', 'stdout JSON 必须记录 OK 状态。');
|
|
assertFileExists(output.bundleDir, '命令证据目录必须存在。');
|
|
|
|
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
|
|
assertEqual(manifest.summary.status, 'OK', 'manifest 必须记录 OK 状态。');
|
|
assertEqual(
|
|
manifest.command?.name,
|
|
'pingora-direct-enable-apply',
|
|
'manifest 必须记录命令名。',
|
|
);
|
|
assertEqual(
|
|
manifest.command?.exitCode,
|
|
0,
|
|
'manifest 必须记录真实命令退出码。',
|
|
);
|
|
assertEqual(
|
|
manifest.expectedExecutable,
|
|
fixture.commandScript,
|
|
'manifest 必须记录预期真实命令路径。',
|
|
);
|
|
assertEqual(
|
|
manifest.command?.expectedExecutable,
|
|
fixture.commandScript,
|
|
'command-record 必须记录预期真实命令路径。',
|
|
);
|
|
assertEqual(
|
|
manifest.command?.executable,
|
|
fixture.commandScript,
|
|
'manifest.command 必须记录真实命令绝对路径。',
|
|
);
|
|
const commandRecord = readJson(path.join(output.bundleDir, 'command-record.json'));
|
|
assertEqual(
|
|
commandRecord.executable,
|
|
fixture.commandScript,
|
|
'command-record.json 必须记录真实命令绝对路径。',
|
|
);
|
|
assertEqual(
|
|
output.expectedExecutable,
|
|
fixture.commandScript,
|
|
'stdout JSON 必须返回预期真实命令路径。',
|
|
);
|
|
assertIncludes(
|
|
manifest.command?.args || [],
|
|
'--message',
|
|
'manifest 必须记录结构化命令参数。',
|
|
);
|
|
assertManifestFileMetadata(
|
|
output.bundleDir,
|
|
manifest.files?.stdout,
|
|
'command.stdout.txt',
|
|
);
|
|
assertManifestFileMetadata(
|
|
output.bundleDir,
|
|
manifest.files?.stderr,
|
|
'command.stderr.txt',
|
|
);
|
|
assertManifestFileMetadata(
|
|
output.bundleDir,
|
|
manifest.files?.commandRecord,
|
|
'command-record.json',
|
|
);
|
|
assertFileExists(
|
|
path.join(output.bundleDir, 'command.stdout.txt'),
|
|
'命令证据必须保存 stdout。',
|
|
);
|
|
assertFileExists(
|
|
path.join(output.bundleDir, 'command.stderr.txt'),
|
|
'命令证据必须保存 stderr。',
|
|
);
|
|
assertFileExists(
|
|
path.join(output.bundleDir, 'command-record.json'),
|
|
'命令证据必须保存 command-record.json。',
|
|
);
|
|
assertMode(output.bundleDir, 0o750, '命令证据目录权限必须是 0750。');
|
|
for (const fileName of [
|
|
'manifest.json',
|
|
'command.stdout.txt',
|
|
'command.stderr.txt',
|
|
'command-record.json',
|
|
]) {
|
|
assertMode(
|
|
path.join(output.bundleDir, fileName),
|
|
0o640,
|
|
`${fileName} 权限必须是 0640。`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertCommandEvidenceWritesCutoverRunId() {
|
|
const fixture = prepareFixture('cutover-run-id');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--phase',
|
|
'enable-apply',
|
|
'--command-name',
|
|
'pingora-direct-enable-apply',
|
|
'--cutover-run-id',
|
|
'cutover-20260617T020000Z',
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
'--message',
|
|
'hello-cutover-run',
|
|
]);
|
|
|
|
assertStatus(result, 0, '提供 cutover run id 时命令证据脚本应成功退出。');
|
|
if (result.status !== 0) {
|
|
return;
|
|
}
|
|
const output = parseLastJsonObject(result.stdout, 'cutover run id 命令证据 stdout');
|
|
assertEqual(
|
|
output.cutoverRunId,
|
|
'cutover-20260617T020000Z',
|
|
'命令证据 stdout 必须返回 cutoverRunId。',
|
|
);
|
|
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
|
|
assertEqual(
|
|
manifest.cutoverRunId,
|
|
'cutover-20260617T020000Z',
|
|
'命令证据 manifest 必须记录 cutoverRunId。',
|
|
);
|
|
assertEqual(
|
|
manifest.command?.cutoverRunId,
|
|
'cutover-20260617T020000Z',
|
|
'command-record 必须同步记录 cutoverRunId。',
|
|
);
|
|
}
|
|
|
|
function assertCommandFailureStillWritesEvidenceAndFails() {
|
|
const fixture = prepareFixture('failure');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--phase',
|
|
'rollback-apply',
|
|
'--command-name',
|
|
'pingora-direct-rollback-apply',
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
'--message',
|
|
'rollback-output',
|
|
'--fail',
|
|
]);
|
|
|
|
assertStatus(result, 37, '真实命令失败时命令证据脚本应返回真实退出码。');
|
|
const output = parseLastJsonObject(result.stdout, '失败命令证据 stdout');
|
|
assertEqual(output.status, 'FAILED', '失败 stdout JSON 必须记录 FAILED。');
|
|
assertFileExists(output.bundleDir, '失败时仍必须保留命令证据目录。');
|
|
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
|
|
assertEqual(
|
|
manifest.summary.exitCode,
|
|
37,
|
|
'失败 manifest 必须记录真实退出码。',
|
|
);
|
|
assertFileExists(
|
|
path.join(output.bundleDir, 'command.stdout.txt'),
|
|
'失败时仍必须保存 stdout。',
|
|
);
|
|
assertFileExists(
|
|
path.join(output.bundleDir, 'command.stderr.txt'),
|
|
'失败时仍必须保存 stderr。',
|
|
);
|
|
}
|
|
|
|
function assertCommandEvidenceRedactsProbeTokensFromArtifacts() {
|
|
const fixture = prepareFixture('redact-token');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--phase',
|
|
'rollback-apply',
|
|
'--command-name',
|
|
'pingora-direct-rollback-apply',
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
'--message',
|
|
'probe-token=secret-shadow-token',
|
|
'--pingora-shadow-probe-token',
|
|
'secret-shadow-token',
|
|
'--direct-probe-token=secret-inline-token',
|
|
'--echo-env-token',
|
|
]);
|
|
|
|
assertStatus(result, 0, '带 token 的命令证据脚本应成功退出。');
|
|
for (const secret of [
|
|
'secret-shadow-token',
|
|
'secret-inline-token',
|
|
'secret-env-token',
|
|
]) {
|
|
assertNotIncludes(
|
|
result.stdout,
|
|
secret,
|
|
'命令证据脚本透传 stdout 时不能泄露 token。',
|
|
);
|
|
}
|
|
const output = parseLastJsonObject(result.stdout, '脱敏命令证据 stdout');
|
|
for (const fileName of [
|
|
'manifest.json',
|
|
'command.stdout.txt',
|
|
'command.stderr.txt',
|
|
'command-record.json',
|
|
]) {
|
|
const content = readFileSync(path.join(output.bundleDir, fileName), 'utf8');
|
|
for (const secret of [
|
|
'secret-shadow-token',
|
|
'secret-inline-token',
|
|
'secret-env-token',
|
|
]) {
|
|
assertNotIncludes(
|
|
content,
|
|
secret,
|
|
`${fileName} 不能泄露 token 原文。`,
|
|
);
|
|
}
|
|
assertIncludes(content, '<redacted>', `${fileName} 必须保留脱敏占位符。`);
|
|
}
|
|
}
|
|
|
|
function assertRejectsUnsafeNames() {
|
|
const fixture = prepareFixture('unsafe-name');
|
|
for (const [flag, value, expected] of [
|
|
['--phase', '../rollback', '--phase 只能包含 ASCII'],
|
|
['--command-name', '回退', '--command-name 只能包含 ASCII'],
|
|
['--cutover-run-id', 'cutover 1', '--cutover-run-id 只能包含 ASCII'],
|
|
]) {
|
|
const result = runCommandEvidence(fixture, [
|
|
flag,
|
|
value,
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push(`${flag} 使用不安全值时必须失败。`);
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
expected,
|
|
`${flag} 不安全值必须给出明确错误。`,
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
`${flag} 不安全值被拒绝时不应执行真实命令。`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertRejectsRelativeOutputRoot() {
|
|
const fixture = prepareFixture('relative-output-root');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
'pingora-evidence',
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝相对 --output-root。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--output-root 必须是绝对路径',
|
|
'相对 output-root 必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsFilesystemRootOutputRoot() {
|
|
const fixture = prepareFixture('root-output-root');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
'/',
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝文件系统根目录 output-root。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--output-root 不能是文件系统根目录',
|
|
'根目录 output-root 必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsOutputRootWithControlCharacters() {
|
|
const fixture = prepareFixture('output-root-control-characters');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
`${fixture.outputRoot}\n--phase rollback-apply`,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝带换行的 --output-root。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--output-root 不能包含换行或 NUL 字符',
|
|
'带换行 output-root 必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'带换行 output-root 被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsSymlinkOutputRootBeforeCommand() {
|
|
const fixture = prepareFixture('symlink-output-root');
|
|
const realTarget = path.join(tmpRoot, 'symlink-output-root-real-target');
|
|
rmSync(fixture.outputRoot, { recursive: true, force: true });
|
|
mkdirSync(realTarget, { recursive: true });
|
|
symlinkSync(realTarget, fixture.outputRoot, 'dir');
|
|
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝符号链接 output-root。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--output-root 不能是符号链接',
|
|
'符号链接 output-root 必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'符号链接 output-root 被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsFileOutputRootBeforeCommand() {
|
|
const fixture = prepareFixture('file-output-root');
|
|
rmSync(fixture.outputRoot, { recursive: true, force: true });
|
|
writeFileSync(fixture.outputRoot, 'not a directory\n', 'utf8');
|
|
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝非目录 output-root。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--output-root 已存在但不是目录',
|
|
'非目录 output-root 必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'非目录 output-root 被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsRelativeCommandPath() {
|
|
const fixture = prepareFixture('relative-command-path');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
'scripts/deploy/pingora-direct-rollback.sh',
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝带路径的相对命令。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令必须是绝对路径',
|
|
'相对命令路径必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsBareCommandName() {
|
|
const fixture = prepareFixture('bare-command-name');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
'echo',
|
|
'SHOULD_NOT_RUN',
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝 PATH 裸命令名。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令必须是绝对路径',
|
|
'PATH 裸命令名必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'SHOULD_NOT_RUN',
|
|
'PATH 裸命令名被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsFilesystemRootCommand() {
|
|
const fixture = prepareFixture('filesystem-root-command');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
path.parse(process.cwd()).root,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝文件系统根目录真实命令。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令不能是文件系统根目录',
|
|
'文件系统根目录真实命令必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsCommandWithControlCharacters() {
|
|
const fixture = prepareFixture('command-control-characters');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
`${fixture.commandScript}\n`,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝带换行的真实命令。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令不能包含换行或 NUL 字符',
|
|
'带换行的真实命令必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'带换行的真实命令被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsCommandArgWithControlCharacters() {
|
|
const fixture = prepareFixture('command-arg-control-characters');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--',
|
|
fixture.commandScript,
|
|
'--message',
|
|
'line1\nline2',
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝带换行的真实命令参数。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令参数不能包含换行或 NUL 字符',
|
|
'带换行的真实命令参数必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'带换行的真实命令参数被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsRelativeExpectedExecutable() {
|
|
const fixture = prepareFixture('relative-expected-executable');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--expected-executable',
|
|
'scripts/deploy/pingora-direct-enable.sh',
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝相对 --expected-executable。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--expected-executable 必须是绝对路径',
|
|
'相对 expected executable 必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'相对 expected executable 被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsFilesystemRootExpectedExecutable() {
|
|
const fixture = prepareFixture('filesystem-root-expected-executable');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--expected-executable',
|
|
path.parse(process.cwd()).root,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝文件系统根目录 --expected-executable。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--expected-executable 不能是文件系统根目录',
|
|
'文件系统根目录 expected executable 必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'文件系统根目录 expected executable 被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsExpectedExecutableWithControlCharacters() {
|
|
const fixture = prepareFixture('expected-executable-control-characters');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--expected-executable',
|
|
`${fixture.commandScript}\n`,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝带换行的 --expected-executable。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--expected-executable 不能包含换行或 NUL 字符',
|
|
'带换行的 expected executable 必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'带换行 expected executable 被拒绝时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsMismatchedExpectedExecutableBeforeCommand() {
|
|
const fixture = prepareFixture('mismatched-expected-executable');
|
|
const expected = path.join(fixture.root, 'expected-enable.sh');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--expected-executable',
|
|
expected,
|
|
'--',
|
|
fixture.commandScript,
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝与 --expected-executable 不一致的真实命令。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令与 --expected-executable 不一致',
|
|
'真实命令与 expected executable 不一致时必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'真实命令与 expected executable 不一致时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsMissingRequiredArgBeforeCommand() {
|
|
const fixture = prepareFixture('missing-required-arg');
|
|
const result = runCommandEvidence(fixture, [
|
|
'--output-root',
|
|
fixture.outputRoot,
|
|
'--require-arg',
|
|
'--apply',
|
|
'--',
|
|
fixture.commandScript,
|
|
'--message',
|
|
'dry-run-only',
|
|
]);
|
|
if ((result.status ?? 0) === 0) {
|
|
failures.push('命令证据脚本必须拒绝缺少 --require-arg 的真实命令。');
|
|
}
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'真实命令参数缺少 --require-arg 要求的 --apply',
|
|
'真实命令缺少 required arg 时必须给出明确错误。',
|
|
);
|
|
assertNotIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'[fake-command]',
|
|
'真实命令缺少 required arg 时不应执行真实命令。',
|
|
);
|
|
}
|
|
|
|
function prepareFixture(name) {
|
|
const root = path.join(tmpRoot, name);
|
|
const outputRoot = path.join(root, 'evidence');
|
|
const commandScript = path.join(root, 'fake-command.sh');
|
|
mkdirSync(outputRoot, { recursive: true });
|
|
writeFileSync(
|
|
commandScript,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'set -euo pipefail',
|
|
'message="ok"',
|
|
'exit_code=0',
|
|
'while [[ $# -gt 0 ]]; do',
|
|
' case "$1" in',
|
|
' --message)',
|
|
' message="$2"',
|
|
' shift 2',
|
|
' ;;',
|
|
' --fail)',
|
|
' exit_code=37',
|
|
' shift',
|
|
' ;;',
|
|
' --echo-env-token)',
|
|
' echo "[fake-command] env=${GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN:-}"',
|
|
' shift',
|
|
' ;;',
|
|
' --pingora-shadow-probe-token)',
|
|
' echo "[fake-command] token=$2" >&2',
|
|
' shift 2',
|
|
' ;;',
|
|
' *)',
|
|
' shift',
|
|
' ;;',
|
|
' esac',
|
|
'done',
|
|
'echo "[fake-command] stdout ${message}"',
|
|
'echo "[fake-command] stderr ${message}" >&2',
|
|
'exit "${exit_code}"',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
spawnSync('chmod', ['0755', commandScript], { encoding: 'utf8' });
|
|
return { root, outputRoot, commandScript };
|
|
}
|
|
|
|
function runCommandEvidence(fixture, args) {
|
|
return spawnSync('node', [COMMAND_EVIDENCE_SCRIPT, ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
GENARRATIVE_PINGORA_CUTOVER_RUN_ID: '',
|
|
GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_ROOT: fixture.outputRoot,
|
|
GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN: 'secret-env-token',
|
|
},
|
|
});
|
|
}
|
|
|
|
function parseLastJsonObject(text, label) {
|
|
const end = text.lastIndexOf('}');
|
|
if (end < 0) {
|
|
failures.push(`${label} 未包含 JSON 结果。`);
|
|
return {};
|
|
}
|
|
for (let start = text.lastIndexOf('{', end); start >= 0; start = text.lastIndexOf('{', start - 1)) {
|
|
try {
|
|
return JSON.parse(text.slice(start, end + 1));
|
|
} catch {
|
|
// 继续向前找外层 JSON 起点。
|
|
}
|
|
}
|
|
failures.push(`${label} 未包含合法 JSON 结果。`);
|
|
return {};
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
try {
|
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
failures.push(`${filePath} 不是合法 JSON: ${error.message}`);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function assertManifestFileMetadata(bundleDir, metadata, expectedPath) {
|
|
if (!metadata || typeof metadata !== 'object') {
|
|
failures.push(`${expectedPath} 必须在 manifest.files 中记录结构化元数据。`);
|
|
return;
|
|
}
|
|
assertEqual(
|
|
metadata.path,
|
|
expectedPath,
|
|
`${expectedPath} manifest 元数据必须记录文件名。`,
|
|
);
|
|
const filePath = path.join(bundleDir, expectedPath);
|
|
const content = readFileSync(filePath);
|
|
assertEqual(
|
|
metadata.sizeBytes,
|
|
content.length,
|
|
`${expectedPath} manifest 元数据必须记录文件大小。`,
|
|
);
|
|
assertEqual(
|
|
metadata.sha256,
|
|
createHash('sha256').update(content).digest('hex'),
|
|
`${expectedPath} manifest 元数据必须记录 sha256。`,
|
|
);
|
|
}
|
|
|
|
function assertFileExists(filePath, reason) {
|
|
if (!existsSync(filePath)) {
|
|
failures.push(`${reason} 缺少: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
function assertMode(filePath, expected, reason) {
|
|
if (!existsSync(filePath)) {
|
|
return;
|
|
}
|
|
const actual = statSync(filePath).mode & 0o777;
|
|
if (actual !== expected) {
|
|
failures.push(
|
|
`${reason} 实际 ${actual.toString(8)},预期 ${expected.toString(8)}。`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertStatus(result, expected, reason) {
|
|
const actual = result.status ?? 0;
|
|
if (actual !== expected) {
|
|
failures.push(
|
|
`${reason} 预期退出码 ${expected},实际 ${actual}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, reason) {
|
|
if (actual !== expected) {
|
|
failures.push(`${reason} 实际 ${JSON.stringify(actual)},预期 ${JSON.stringify(expected)}。`);
|
|
}
|
|
}
|
|
|
|
function assertIncludes(value, expected, reason) {
|
|
if (!String(value).includes(expected)) {
|
|
failures.push(`${reason} 缺少: ${expected}`);
|
|
}
|
|
}
|
|
|
|
function assertNotIncludes(value, unexpected, reason) {
|
|
if (String(value).includes(unexpected)) {
|
|
failures.push(`${reason} 不应包含: ${unexpected}`);
|
|
}
|
|
}
|