统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
This commit is contained in:
@@ -22,10 +22,22 @@ const REQUIRED_ARTIFACTS = [
|
||||
{ path: 'scripts/check-pingora-canary-access-log-parity.mjs' },
|
||||
{ path: 'scripts/deploy/pingora-direct-enable.sh', executable: true },
|
||||
{ path: 'scripts/deploy/pingora-direct-rollback.sh', executable: true },
|
||||
{ path: 'scripts/deploy/pingora-realpath-canary-enable.sh', executable: true },
|
||||
{ path: 'scripts/deploy/pingora-realpath-canary-disable.sh', executable: true },
|
||||
{ path: 'scripts/deploy/pingora-health-patrol-env-switch.mjs', executable: true },
|
||||
{ path: 'scripts/deploy/pingora-gateway-env-shadow-switch.mjs', executable: true },
|
||||
{
|
||||
path: 'scripts/deploy/pingora-realpath-canary-enable.sh',
|
||||
executable: true,
|
||||
},
|
||||
{
|
||||
path: 'scripts/deploy/pingora-realpath-canary-disable.sh',
|
||||
executable: true,
|
||||
},
|
||||
{
|
||||
path: 'scripts/deploy/pingora-health-patrol-env-switch.mjs',
|
||||
executable: true,
|
||||
},
|
||||
{
|
||||
path: 'scripts/deploy/pingora-gateway-env-shadow-switch.mjs',
|
||||
executable: true,
|
||||
},
|
||||
{ path: 'scripts/deploy/pingora-tls-cert-sync.mjs', executable: true },
|
||||
{ path: 'deploy/systemd/genarrative-pingora-gateway.service' },
|
||||
{ path: 'deploy/systemd/genarrative-pingora-gateway-direct-entry.conf' },
|
||||
@@ -478,7 +490,10 @@ async function inspectReleaseManifest(input, pingoraGateway) {
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const content = await readFile(path.join(input.releaseRoot, candidate), 'utf8');
|
||||
const content = await readFile(
|
||||
path.join(input.releaseRoot, candidate),
|
||||
'utf8',
|
||||
);
|
||||
manifest = JSON.parse(content);
|
||||
pathUsed = candidate;
|
||||
break;
|
||||
@@ -501,7 +516,9 @@ async function inspectReleaseManifest(input, pingoraGateway) {
|
||||
|
||||
diagnostics.length = 0;
|
||||
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
||||
const apiArtifact = artifacts.find((artifact) => artifact?.path === 'api-server');
|
||||
const apiArtifact = artifacts.find(
|
||||
(artifact) => artifact?.path === 'api-server',
|
||||
);
|
||||
const pingoraArtifact = artifacts.find(
|
||||
(artifact) => artifact?.path === 'pingora-gateway',
|
||||
);
|
||||
@@ -695,10 +712,13 @@ function runCommand(command, args, input) {
|
||||
}
|
||||
|
||||
function summarize(statuses) {
|
||||
const criticalCount = statuses.filter((status) => status === 'CRITICAL').length;
|
||||
const criticalCount = statuses.filter(
|
||||
(status) => status === 'CRITICAL',
|
||||
).length;
|
||||
const warningCount = statuses.filter((status) => status === 'WARNING').length;
|
||||
return {
|
||||
status: criticalCount > 0 ? 'CRITICAL' : warningCount > 0 ? 'WARNING' : 'OK',
|
||||
status:
|
||||
criticalCount > 0 ? 'CRITICAL' : warningCount > 0 ? 'WARNING' : 'OK',
|
||||
criticalCount,
|
||||
warningCount,
|
||||
};
|
||||
|
||||
@@ -45,10 +45,9 @@ const stdoutPath = path.join(bundleDir, 'command.stdout.txt');
|
||||
const stderrPath = path.join(bundleDir, 'command.stderr.txt');
|
||||
const recordPath = path.join(bundleDir, 'command-record.json');
|
||||
const manifestPath = path.join(bundleDir, 'manifest.json');
|
||||
const exitCode =
|
||||
run.signal
|
||||
? run.exitCode ?? 1
|
||||
: run.exitCode === null || run.exitCode === undefined
|
||||
const exitCode = run.signal
|
||||
? (run.exitCode ?? 1)
|
||||
: run.exitCode === null || run.exitCode === undefined
|
||||
? run.error
|
||||
? 1
|
||||
: 0
|
||||
@@ -140,8 +139,7 @@ if (exitCode !== 0) {
|
||||
|
||||
function parseArgs(argv) {
|
||||
const separatorIndex = argv.indexOf('--');
|
||||
const optionArgs =
|
||||
separatorIndex < 0 ? argv : argv.slice(0, separatorIndex);
|
||||
const optionArgs = separatorIndex < 0 ? argv : argv.slice(0, separatorIndex);
|
||||
if (optionArgs.includes('-h') || optionArgs.includes('--help')) {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
@@ -153,8 +151,7 @@ function parseArgs(argv) {
|
||||
const command = argv[separatorIndex + 1] || '';
|
||||
const commandArgs = argv.slice(separatorIndex + 2);
|
||||
const result = {
|
||||
phase:
|
||||
process.env.GENARRATIVE_PINGORA_CUTOVER_COMMAND_PHASE || 'manual',
|
||||
phase: process.env.GENARRATIVE_PINGORA_CUTOVER_COMMAND_PHASE || 'manual',
|
||||
commandName:
|
||||
process.env.GENARRATIVE_PINGORA_CUTOVER_COMMAND_NAME || 'command',
|
||||
cutoverRunId: process.env.GENARRATIVE_PINGORA_CUTOVER_RUN_ID || '',
|
||||
@@ -229,12 +226,18 @@ function validateConfig(config) {
|
||||
validateSafeName(config.cutoverRunId, '--cutover-run-id');
|
||||
}
|
||||
if (config.expectedExecutable) {
|
||||
validateSafeExecutablePath(config.expectedExecutable, '--expected-executable');
|
||||
validateSafeExecutablePath(
|
||||
config.expectedExecutable,
|
||||
'--expected-executable',
|
||||
);
|
||||
}
|
||||
if (!path.isAbsolute(config.outputRoot)) {
|
||||
throw new Error('--output-root 必须是绝对路径。');
|
||||
}
|
||||
if (path.resolve(config.outputRoot) === path.parse(path.resolve(config.outputRoot)).root) {
|
||||
if (
|
||||
path.resolve(config.outputRoot) ===
|
||||
path.parse(path.resolve(config.outputRoot)).root
|
||||
) {
|
||||
throw new Error('--output-root 不能是文件系统根目录。');
|
||||
}
|
||||
validateNoControlCharacters(config.outputRoot, '--output-root');
|
||||
@@ -310,9 +313,7 @@ function validateRequiredArg(value, label) {
|
||||
function validateSafeName(value, label) {
|
||||
const text = String(value || '');
|
||||
if (!/^[0-9A-Za-z._-]+$/u.test(text)) {
|
||||
throw new Error(
|
||||
`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`,
|
||||
);
|
||||
throw new Error(`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,10 +359,7 @@ async function validateOutputRootForWriting(outputRoot) {
|
||||
const target = path.resolve(outputRoot);
|
||||
const root = path.parse(target).root;
|
||||
let current = root;
|
||||
const segments = path
|
||||
.relative(root, target)
|
||||
.split(path.sep)
|
||||
.filter(Boolean);
|
||||
const segments = path.relative(root, target).split(path.sep).filter(Boolean);
|
||||
|
||||
for (const segment of segments) {
|
||||
current = path.join(current, segment);
|
||||
@@ -401,7 +399,10 @@ async function writeEvidenceFile(filePath, content) {
|
||||
}
|
||||
|
||||
async function buildEvidenceFileMetadata(filePath) {
|
||||
const [stats, content] = await Promise.all([stat(filePath), readFile(filePath)]);
|
||||
const [stats, content] = await Promise.all([
|
||||
stat(filePath),
|
||||
readFile(filePath),
|
||||
]);
|
||||
return {
|
||||
path: path.basename(filePath),
|
||||
sizeBytes: stats.size,
|
||||
@@ -476,15 +477,18 @@ function redactSecrets(text, secrets) {
|
||||
}
|
||||
|
||||
function formatCommand(command, args, secrets) {
|
||||
return [redactSecrets(command, secrets), ...redactSecretArgs(args, secrets)].join(' ');
|
||||
return [
|
||||
redactSecrets(command, secrets),
|
||||
...redactSecretArgs(args, secrets),
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function redactSecretArgs(args, secrets = []) {
|
||||
return args.map((arg, index) => {
|
||||
const redacted =
|
||||
index > 0 && SECRET_VALUE_FLAGS.has(args[index - 1])
|
||||
? '<redacted>'
|
||||
: redactInlineSecretArg(arg);
|
||||
? '<redacted>'
|
||||
: redactInlineSecretArg(arg);
|
||||
return redactSecrets(redacted, secrets);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ function parseArgs(argv) {
|
||||
DEFAULT_CUTOVER_TIMELINE_MAX_SPAN_MS,
|
||||
'GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS',
|
||||
),
|
||||
requiredCutoverRunId:
|
||||
process.env.GENARRATIVE_PINGORA_CUTOVER_RUN_ID || '',
|
||||
requiredCutoverRunId: process.env.GENARRATIVE_PINGORA_CUTOVER_RUN_ID || '',
|
||||
allowExtraRootEntries: false,
|
||||
requiredPhases: [],
|
||||
requiredPhaseDirectLiveAccessLog: [],
|
||||
@@ -117,7 +116,9 @@ function parseArgs(argv) {
|
||||
);
|
||||
break;
|
||||
case '--require-command-executable':
|
||||
result.requiredCommandExecutables.push(requireValue(argv, ++index, arg));
|
||||
result.requiredCommandExecutables.push(
|
||||
requireValue(argv, ++index, arg),
|
||||
);
|
||||
break;
|
||||
case '--require-command-arg':
|
||||
result.requiredCommandArgs.push(requireValue(argv, ++index, arg));
|
||||
@@ -277,7 +278,9 @@ function normalizeRequiredCommandExecutables(commandExecutables) {
|
||||
'--require-command-executable commandName',
|
||||
);
|
||||
if (!path.isAbsolute(executable)) {
|
||||
throw new Error('--require-command-executable executable 必须是绝对路径。');
|
||||
throw new Error(
|
||||
'--require-command-executable executable 必须是绝对路径。',
|
||||
);
|
||||
}
|
||||
if (isFilesystemRootPath(executable)) {
|
||||
throw new Error(
|
||||
@@ -357,7 +360,8 @@ function findRequiredCommandExecutable(requiredCommandExecutables, command) {
|
||||
return (
|
||||
requiredCommandExecutables.find(
|
||||
(item) =>
|
||||
item.phase === command.phase && item.commandName === command.commandName,
|
||||
item.phase === command.phase &&
|
||||
item.commandName === command.commandName,
|
||||
)?.executable || null
|
||||
);
|
||||
}
|
||||
@@ -366,32 +370,27 @@ function findRequiredCommandArgs(requiredCommandArgs, command) {
|
||||
return requiredCommandArgs
|
||||
.filter(
|
||||
(item) =>
|
||||
item.phase === command.phase && item.commandName === command.commandName,
|
||||
item.phase === command.phase &&
|
||||
item.commandName === command.commandName,
|
||||
)
|
||||
.map((item) => item.arg);
|
||||
}
|
||||
|
||||
function validateSafePhase(value, label) {
|
||||
if (!isSafePhase(value)) {
|
||||
throw new Error(
|
||||
`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`,
|
||||
);
|
||||
throw new Error(`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateSafeCommandName(value, label) {
|
||||
if (!isSafePhase(value)) {
|
||||
throw new Error(
|
||||
`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`,
|
||||
);
|
||||
throw new Error(`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateSafeCutoverRunId(value, label) {
|
||||
if (!isSafePhase(value)) {
|
||||
throw new Error(
|
||||
`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`,
|
||||
);
|
||||
throw new Error(`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +515,11 @@ async function discoverEvidence(evidenceRoot, options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const manifest = await readJsonManifest(manifestPath, entry.name, diagnostics);
|
||||
const manifest = await readJsonManifest(
|
||||
manifestPath,
|
||||
entry.name,
|
||||
diagnostics,
|
||||
);
|
||||
if (!manifest) {
|
||||
continue;
|
||||
}
|
||||
@@ -742,32 +745,29 @@ async function buildAudit(config, discovery) {
|
||||
config.requiredPhases.length > 0
|
||||
? config.requiredPhases
|
||||
: config.requiredCommands.length > 0
|
||||
? []
|
||||
: sortedUnique(
|
||||
discovery.candidates
|
||||
.filter((candidate) => candidate.commandName === null)
|
||||
.map((candidate) => candidate.phase),
|
||||
);
|
||||
? []
|
||||
: sortedUnique(
|
||||
discovery.candidates
|
||||
.filter((candidate) => candidate.commandName === null)
|
||||
.map((candidate) => candidate.phase),
|
||||
);
|
||||
const phases = [];
|
||||
|
||||
for (const phase of phasesToCheck) {
|
||||
const requiredAccessLog = config.requiredPhaseDirectLiveAccessLog.includes(
|
||||
phase,
|
||||
);
|
||||
const requiredStaticHeaders = config.requiredPhaseDirectLiveStaticHeaders.includes(
|
||||
phase,
|
||||
);
|
||||
const requiredPingoraEnvShadow = config.requiredPhasePingoraEnvShadow.includes(
|
||||
phase,
|
||||
);
|
||||
const requiredAccessLog =
|
||||
config.requiredPhaseDirectLiveAccessLog.includes(phase);
|
||||
const requiredStaticHeaders =
|
||||
config.requiredPhaseDirectLiveStaticHeaders.includes(phase);
|
||||
const requiredPingoraEnvShadow =
|
||||
config.requiredPhasePingoraEnvShadow.includes(phase);
|
||||
const allPhaseCandidates = discovery.candidates.filter(
|
||||
(candidate) => candidate.phase === phase && candidate.commandName === null,
|
||||
(candidate) =>
|
||||
candidate.phase === phase && candidate.commandName === null,
|
||||
);
|
||||
const candidates = filterCandidatesByCutoverRunId(
|
||||
allPhaseCandidates,
|
||||
config.requiredCutoverRunId,
|
||||
)
|
||||
.sort(compareEvidenceCandidates);
|
||||
).sort(compareEvidenceCandidates);
|
||||
if (candidates.length === 0) {
|
||||
phases.push({
|
||||
phase,
|
||||
@@ -856,8 +856,7 @@ async function buildAudit(config, discovery) {
|
||||
const candidates = filterCandidatesByCutoverRunId(
|
||||
allCommandCandidates,
|
||||
config.requiredCutoverRunId,
|
||||
)
|
||||
.sort(compareEvidenceCandidates);
|
||||
).sort(compareEvidenceCandidates);
|
||||
if (candidates.length === 0) {
|
||||
commands.push({
|
||||
phase: requiredCommand.phase,
|
||||
@@ -1048,8 +1047,8 @@ function buildOperatorSummary(input) {
|
||||
? phase.manifestCheck?.directLiveAccessLogCheck?.ok === true
|
||||
: null,
|
||||
reason: accessLogChecked
|
||||
? phase.manifestCheck?.directLiveAccessLogCheck?.reason ??
|
||||
firstDiagnostic
|
||||
? (phase.manifestCheck?.directLiveAccessLogCheck?.reason ??
|
||||
firstDiagnostic)
|
||||
: null,
|
||||
summary: accessLogSummary,
|
||||
},
|
||||
@@ -1059,8 +1058,8 @@ function buildOperatorSummary(input) {
|
||||
? phase.manifestCheck?.directLiveStaticHeadersCheck?.ok === true
|
||||
: null,
|
||||
reason: staticHeadersChecked
|
||||
? phase.manifestCheck?.directLiveStaticHeadersCheck?.reason ??
|
||||
firstDiagnostic
|
||||
? (phase.manifestCheck?.directLiveStaticHeadersCheck?.reason ??
|
||||
firstDiagnostic)
|
||||
: null,
|
||||
summary: staticHeadersSummary,
|
||||
},
|
||||
@@ -1140,7 +1139,11 @@ function filterCandidatesByCutoverRunId(candidates, requiredCutoverRunId) {
|
||||
);
|
||||
}
|
||||
|
||||
function missingEvidenceDiagnostic(label, requiredCutoverRunId, allCandidateCount) {
|
||||
function missingEvidenceDiagnostic(
|
||||
label,
|
||||
requiredCutoverRunId,
|
||||
allCandidateCount,
|
||||
) {
|
||||
if (!requiredCutoverRunId) {
|
||||
return `没有找到${label}的证据 manifest。`;
|
||||
}
|
||||
@@ -1252,7 +1255,9 @@ function checkPingoraEnvShadowSummary(summary) {
|
||||
);
|
||||
}
|
||||
if (summary.tlsListen !== '') {
|
||||
failures.push(`tlsListen 必须为空,实际为 ${formatNullable(summary.tlsListen)}`);
|
||||
failures.push(
|
||||
`tlsListen 必须为空,实际为 ${formatNullable(summary.tlsListen)}`,
|
||||
);
|
||||
}
|
||||
if (summary.httpRedirectListen !== '') {
|
||||
failures.push(
|
||||
@@ -1448,8 +1453,12 @@ async function checkCommandManifest(
|
||||
commandRecordExpectedExecutable:
|
||||
commandRecordCheck.record?.expectedExecutable ?? null,
|
||||
commandRecordExecutable: commandRecordCheck.record?.executable ?? null,
|
||||
commandRecordStartedAt: normalizeIsoTime(commandRecordCheck.record?.startedAt),
|
||||
commandRecordFinishedAt: normalizeIsoTime(commandRecordCheck.record?.finishedAt),
|
||||
commandRecordStartedAt: normalizeIsoTime(
|
||||
commandRecordCheck.record?.startedAt,
|
||||
),
|
||||
commandRecordFinishedAt: normalizeIsoTime(
|
||||
commandRecordCheck.record?.finishedAt,
|
||||
),
|
||||
commandRecordDurationMs: commandRecordCheck.record?.durationMs ?? null,
|
||||
requiredArgs,
|
||||
diagnostics,
|
||||
@@ -1463,9 +1472,7 @@ function checkCommandOutputEvidenceFileNames(candidate) {
|
||||
['stderr', candidate.stderrFileName],
|
||||
]) {
|
||||
if (!fileName) {
|
||||
diagnostics.push(
|
||||
`最新命令证据 manifest.files.${field}.path 必须存在。`,
|
||||
);
|
||||
diagnostics.push(`最新命令证据 manifest.files.${field}.path 必须存在。`);
|
||||
continue;
|
||||
}
|
||||
if (!isSafeEvidenceFileName(fileName)) {
|
||||
@@ -1484,7 +1491,9 @@ async function readCommandRecord(candidate) {
|
||||
ok: false,
|
||||
path: null,
|
||||
record: null,
|
||||
diagnostics: ['最新命令证据 manifest.files.commandRecord.path 必须存在。'],
|
||||
diagnostics: [
|
||||
'最新命令证据 manifest.files.commandRecord.path 必须存在。',
|
||||
],
|
||||
};
|
||||
}
|
||||
if (!isSafeEvidenceFileName(fileName)) {
|
||||
@@ -1524,7 +1533,9 @@ async function readCommandRecord(candidate) {
|
||||
ok: false,
|
||||
path: recordPath,
|
||||
record: null,
|
||||
diagnostics: ['最新命令证据 command-record.json 顶层必须是 JSON object。'],
|
||||
diagnostics: [
|
||||
'最新命令证据 command-record.json 顶层必须是 JSON object。',
|
||||
],
|
||||
};
|
||||
}
|
||||
return { ok: true, path: recordPath, record, diagnostics: [] };
|
||||
@@ -1645,9 +1656,7 @@ function checkCommandRecordInvocation(record, label) {
|
||||
} else if (record.args.some((item) => typeof item !== 'string')) {
|
||||
diagnostics.push(`最新命令证据 ${label}.args 必须只包含字符串。`);
|
||||
} else if (record.args.some((item) => /[\0\r\n]/u.test(item))) {
|
||||
diagnostics.push(
|
||||
`最新命令证据 ${label}.args 不能包含换行或 NUL 字符。`,
|
||||
);
|
||||
diagnostics.push(`最新命令证据 ${label}.args 不能包含换行或 NUL 字符。`);
|
||||
}
|
||||
if (typeof record.command !== 'string' || record.command.length === 0) {
|
||||
diagnostics.push(`最新命令证据 ${label}.command 必须是非空字符串。`);
|
||||
@@ -1675,9 +1684,7 @@ function checkCommandRecordTimes(record, label, candidate) {
|
||||
const startedMs = Date.parse(startedAt);
|
||||
const finishedMs = Date.parse(finishedAt);
|
||||
if (finishedMs < startedMs) {
|
||||
diagnostics.push(
|
||||
`最新命令证据 ${label}.finishedAt 不能早于 startedAt。`,
|
||||
);
|
||||
diagnostics.push(`最新命令证据 ${label}.finishedAt 不能早于 startedAt。`);
|
||||
}
|
||||
if (candidate.generatedAt) {
|
||||
const generatedMs = Date.parse(candidate.generatedAt);
|
||||
@@ -1689,9 +1696,7 @@ function checkCommandRecordTimes(record, label, candidate) {
|
||||
}
|
||||
const durationMs = record.durationMs;
|
||||
if (!Number.isSafeInteger(durationMs) || durationMs < 0) {
|
||||
diagnostics.push(
|
||||
`最新命令证据 ${label}.durationMs 必须是非负安全整数。`,
|
||||
);
|
||||
diagnostics.push(`最新命令证据 ${label}.durationMs 必须是非负安全整数。`);
|
||||
} else {
|
||||
const actualDurationMs = finishedMs - startedMs;
|
||||
if (durationMs !== actualDurationMs) {
|
||||
@@ -1767,7 +1772,9 @@ function commandStatus(verify, manifestCheck) {
|
||||
}
|
||||
|
||||
function formatNullable(value) {
|
||||
return value === null || value === undefined ? '<missing>' : JSON.stringify(value);
|
||||
return value === null || value === undefined
|
||||
? '<missing>'
|
||||
: JSON.stringify(value);
|
||||
}
|
||||
|
||||
function isSafeEvidenceFileName(value) {
|
||||
|
||||
@@ -219,9 +219,7 @@ function summarizePingoraEnvShadow(snapshotRun) {
|
||||
);
|
||||
}
|
||||
if (summary.tlsListen !== '') {
|
||||
summary.diagnostics.push(
|
||||
`tlsListen 应为空,实际 ${summary.tlsListen}`,
|
||||
);
|
||||
summary.diagnostics.push(`tlsListen 应为空,实际 ${summary.tlsListen}`);
|
||||
}
|
||||
if (summary.httpRedirectListen !== '') {
|
||||
summary.diagnostics.push(
|
||||
@@ -229,14 +227,10 @@ function summarizePingoraEnvShadow(snapshotRun) {
|
||||
);
|
||||
}
|
||||
if (summary.tlsCertFile !== '') {
|
||||
summary.diagnostics.push(
|
||||
`tlsCertFile 应为空,实际 ${summary.tlsCertFile}`,
|
||||
);
|
||||
summary.diagnostics.push(`tlsCertFile 应为空,实际 ${summary.tlsCertFile}`);
|
||||
}
|
||||
if (summary.tlsKeyFile !== '') {
|
||||
summary.diagnostics.push(
|
||||
`tlsKeyFile 应为空,实际 ${summary.tlsKeyFile}`,
|
||||
);
|
||||
summary.diagnostics.push(`tlsKeyFile 应为空,实际 ${summary.tlsKeyFile}`);
|
||||
}
|
||||
if (summary.mode && summary.mode !== 'shadow') {
|
||||
summary.diagnostics.push(`mode 应为 shadow,实际 ${summary.mode}`);
|
||||
@@ -278,9 +272,7 @@ function summarizeDirectLiveAccessLog(directLiveRun) {
|
||||
present: true,
|
||||
logFile: accessLogCheck.logFile || '',
|
||||
sinceLines: coerceNonNegativeInteger(accessLogCheck.sinceLines),
|
||||
scannedLineCount: coerceNonNegativeInteger(
|
||||
accessLogCheck.scannedLineCount,
|
||||
),
|
||||
scannedLineCount: coerceNonNegativeInteger(accessLogCheck.scannedLineCount),
|
||||
checked: coerceNonNegativeInteger(accessLogCheck.checked),
|
||||
matchedCount: coerceNonNegativeInteger(accessLogCheck.matchedCount),
|
||||
missingCount: coerceNonNegativeInteger(accessLogCheck.missingCount),
|
||||
@@ -398,7 +390,9 @@ function summarizeStaticHeaderEntry(entry, options = {}) {
|
||||
};
|
||||
const diagnostics = [];
|
||||
if (summary.statusCode !== 200) {
|
||||
diagnostics.push(`GET statusCode 应为 200,实际 ${formatValue(summary.statusCode)}`);
|
||||
diagnostics.push(
|
||||
`GET statusCode 应为 200,实际 ${formatValue(summary.statusCode)}`,
|
||||
);
|
||||
}
|
||||
if (!summary.cacheControl) {
|
||||
diagnostics.push('缺少 Cache-Control');
|
||||
@@ -763,7 +757,10 @@ function validateConfig(config) {
|
||||
}
|
||||
validateEvidenceCommandArgs('状态快照命令参数', buildSnapshotArgs(config));
|
||||
if (config.runDirectLive) {
|
||||
validateEvidenceCommandArgs('direct live 命令参数', buildDirectLiveArgs(config));
|
||||
validateEvidenceCommandArgs(
|
||||
'direct live 命令参数',
|
||||
buildDirectLiveArgs(config),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,7 +782,10 @@ function validateDirectLiveConfig(config) {
|
||||
}
|
||||
validateHostOption(config.directRedirectHost, '--direct-redirect-host');
|
||||
if (config.directRedirectBaseUrl) {
|
||||
validateHttpsBaseUrl(config.directRedirectBaseUrl, '--direct-redirect-base-url');
|
||||
validateHttpsBaseUrl(
|
||||
config.directRedirectBaseUrl,
|
||||
'--direct-redirect-base-url',
|
||||
);
|
||||
}
|
||||
if (!config.directSpacetimeDatabase) {
|
||||
throw new Error('--run-direct-live 必须提供 --direct-spacetime-database。');
|
||||
@@ -800,7 +800,10 @@ function validateDirectLiveConfig(config) {
|
||||
);
|
||||
}
|
||||
if (config.directProbeToken) {
|
||||
validateNoControlCharacters(config.directProbeToken, '--direct-probe-token');
|
||||
validateNoControlCharacters(
|
||||
config.directProbeToken,
|
||||
'--direct-probe-token',
|
||||
);
|
||||
}
|
||||
if (!config.directPingoraAccessLog) {
|
||||
throw new Error('--run-direct-live 必须提供 --direct-pingora-access-log。');
|
||||
@@ -825,9 +828,7 @@ function validatePhase(value) {
|
||||
function validateSafeName(value, label) {
|
||||
const text = String(value || '');
|
||||
if (!/^[0-9A-Za-z._-]+$/u.test(text)) {
|
||||
throw new Error(
|
||||
`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`,
|
||||
);
|
||||
throw new Error(`${label} 只能包含 ASCII 字母、数字、点、下划线或短横线。`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,10 +988,7 @@ async function validateOutputRootForWriting(outputRoot) {
|
||||
const target = path.resolve(outputRoot);
|
||||
const root = path.parse(target).root;
|
||||
let current = root;
|
||||
const segments = path
|
||||
.relative(root, target)
|
||||
.split(path.sep)
|
||||
.filter(Boolean);
|
||||
const segments = path.relative(root, target).split(path.sep).filter(Boolean);
|
||||
|
||||
for (const segment of segments) {
|
||||
current = path.join(current, segment);
|
||||
@@ -1030,7 +1028,10 @@ async function writeEvidenceFile(filePath, content) {
|
||||
}
|
||||
|
||||
async function buildEvidenceFileMetadata(filePath) {
|
||||
const [stats, content] = await Promise.all([stat(filePath), readFile(filePath)]);
|
||||
const [stats, content] = await Promise.all([
|
||||
stat(filePath),
|
||||
readFile(filePath),
|
||||
]);
|
||||
return {
|
||||
path: path.basename(filePath),
|
||||
sizeBytes: stats.size,
|
||||
@@ -1130,7 +1131,10 @@ async function runEvidenceCommand({
|
||||
let jsonMetadata = null;
|
||||
let parseErrorMetadata = null;
|
||||
if (parsed.ok) {
|
||||
await writeEvidenceFile(jsonPath, `${JSON.stringify(parsed.value, null, 2)}\n`);
|
||||
await writeEvidenceFile(
|
||||
jsonPath,
|
||||
`${JSON.stringify(parsed.value, null, 2)}\n`,
|
||||
);
|
||||
jsonMetadata = await buildEvidenceFileMetadata(jsonPath);
|
||||
} else {
|
||||
await writeEvidenceFile(parseErrorPath, `${parsed.error}\n`);
|
||||
|
||||
@@ -12,8 +12,10 @@ await validateBundleDir(bundleDir);
|
||||
|
||||
const manifest = await readManifest(manifestPath);
|
||||
validateManifestSchemaVersion(manifest);
|
||||
const { verifications, registeredFileNames } =
|
||||
await verifyManifestFiles(bundleDir, manifest);
|
||||
const { verifications, registeredFileNames } = await verifyManifestFiles(
|
||||
bundleDir,
|
||||
manifest,
|
||||
);
|
||||
const extraFiles = config.allowExtraFiles
|
||||
? []
|
||||
: await findExtraEvidenceEntries(bundleDir, registeredFileNames);
|
||||
@@ -237,8 +239,8 @@ async function findExtraEvidenceEntries(bundleDir, registeredFileNames) {
|
||||
reason: entry.isSymbolicLink()
|
||||
? 'extra symlink not registered in manifest.files'
|
||||
: entry.isDirectory()
|
||||
? 'extra directory not registered in manifest.files'
|
||||
: 'extra file not registered in manifest.files',
|
||||
? 'extra directory not registered in manifest.files'
|
||||
: 'extra file not registered in manifest.files',
|
||||
});
|
||||
}
|
||||
return extras.sort((left, right) => left.path.localeCompare(right.path));
|
||||
@@ -267,7 +269,10 @@ function validateMetadata(key, value) {
|
||||
if (!Number.isSafeInteger(value.sizeBytes) || value.sizeBytes < 0) {
|
||||
throw new Error(`manifest.files.${key}.sizeBytes 必须是非负安全整数。`);
|
||||
}
|
||||
if (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/u.test(value.sha256)) {
|
||||
if (
|
||||
typeof value.sha256 !== 'string' ||
|
||||
!/^[0-9a-f]{64}$/u.test(value.sha256)
|
||||
) {
|
||||
throw new Error(`manifest.files.${key}.sha256 必须是 64 位小写十六进制。`);
|
||||
}
|
||||
}
|
||||
@@ -276,7 +281,9 @@ async function verifyMetadata(bundleDir, key, metadata) {
|
||||
const filePath = path.join(bundleDir, metadata.path);
|
||||
const expectedPath = path.resolve(bundleDir, metadata.path);
|
||||
if (path.dirname(expectedPath) !== path.resolve(bundleDir)) {
|
||||
throw new Error(`manifest.files.${key}.path 超出证据目录: ${metadata.path}`);
|
||||
throw new Error(
|
||||
`manifest.files.${key}.path 超出证据目录: ${metadata.path}`,
|
||||
);
|
||||
}
|
||||
let stats;
|
||||
let content;
|
||||
|
||||
@@ -357,8 +357,7 @@ async function inspectHealthPatrolEnv(input) {
|
||||
|
||||
const values = parsed.values;
|
||||
const publicHost = values.GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST || '';
|
||||
const publicBaseUrl =
|
||||
values.GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL || '';
|
||||
const publicBaseUrl = values.GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL || '';
|
||||
const gatewayMode = values.GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE || '';
|
||||
const diagnostics = [];
|
||||
let status = 'OK';
|
||||
@@ -371,14 +370,18 @@ async function inspectHealthPatrolEnv(input) {
|
||||
}
|
||||
if (
|
||||
input.expectedPublicBaseUrl &&
|
||||
normalizeBaseUrl(publicBaseUrl) !== normalizeBaseUrl(input.expectedPublicBaseUrl)
|
||||
normalizeBaseUrl(publicBaseUrl) !==
|
||||
normalizeBaseUrl(input.expectedPublicBaseUrl)
|
||||
) {
|
||||
diagnostics.push(
|
||||
`GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL 应为 ${normalizeBaseUrl(input.expectedPublicBaseUrl)},实际 ${publicBaseUrl || '(空)'}`,
|
||||
);
|
||||
status = maxStatus(status, 'CRITICAL');
|
||||
}
|
||||
if (input.expectedPublicHost !== null && publicHost !== input.expectedPublicHost) {
|
||||
if (
|
||||
input.expectedPublicHost !== null &&
|
||||
publicHost !== input.expectedPublicHost
|
||||
) {
|
||||
diagnostics.push(
|
||||
`GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 应为 ${input.expectedPublicHost},实际 ${publicHost || '(空)'}`,
|
||||
);
|
||||
@@ -402,8 +405,7 @@ async function inspectHealthPatrolEnv(input) {
|
||||
apiBaseUrl: values.GENARRATIVE_HEALTH_PATROL_API_BASE_URL || '',
|
||||
spacetimeBaseUrl:
|
||||
values.GENARRATIVE_HEALTH_PATROL_SPACETIME_BASE_URL || '',
|
||||
pingoraBaseUrl:
|
||||
values.GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL || '',
|
||||
pingoraBaseUrl: values.GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL || '',
|
||||
hasPingoraProbeToken: Boolean(
|
||||
values.GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN ||
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN,
|
||||
@@ -436,21 +438,18 @@ async function inspectPingoraEnv(input) {
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN || '',
|
||||
tlsCertFile: values.GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE || '',
|
||||
tlsKeyFile: values.GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE || '',
|
||||
forwardedProto:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO || '',
|
||||
forwardedProto: values.GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO || '',
|
||||
trustXForwardedFor:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR || '',
|
||||
trustedFrontProxyConfirmed:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED || '',
|
||||
protectionEnabled:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED || '',
|
||||
instanceCount:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT || '',
|
||||
instanceCount: values.GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT || '',
|
||||
sharedProtectionConfirmed:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED || '',
|
||||
hasProbeToken: Boolean(values.GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN),
|
||||
accessLogFile:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE || '',
|
||||
accessLogFile: values.GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE || '',
|
||||
};
|
||||
const posture = summarizePingoraEnvPosture(
|
||||
envValues,
|
||||
@@ -753,7 +752,9 @@ async function inspectPingoraUnit(input) {
|
||||
let status = 'OK';
|
||||
|
||||
if (catResult.code !== 0) {
|
||||
diagnostics.push(`systemctl cat 失败: ${catResult.stderr || catResult.error}`);
|
||||
diagnostics.push(
|
||||
`systemctl cat 失败: ${catResult.stderr || catResult.error}`,
|
||||
);
|
||||
status = 'CRITICAL';
|
||||
}
|
||||
if (showResult.code !== 0) {
|
||||
@@ -848,7 +849,9 @@ async function runChecks(input, healthPatrolEnv, secrets) {
|
||||
checks.push(await runHealthPatrolEnvCheck(input, secrets));
|
||||
}
|
||||
if (input.runHealthPatrol) {
|
||||
checks.push(await runProductionHealthPatrol(input, healthPatrolEnv, secrets));
|
||||
checks.push(
|
||||
await runProductionHealthPatrol(input, healthPatrolEnv, secrets),
|
||||
);
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
@@ -930,14 +933,11 @@ async function runProductionHealthPatrol(input, healthPatrolEnv, secrets) {
|
||||
const rawEnv = await readEnvFile(input.healthPatrolEnvFile);
|
||||
const env = {
|
||||
...process.env,
|
||||
...readEnvValuesForChild(rawEnv.status === 'CRITICAL' ? healthPatrolEnv : rawEnv),
|
||||
...readEnvValuesForChild(
|
||||
rawEnv.status === 'CRITICAL' ? healthPatrolEnv : rawEnv,
|
||||
),
|
||||
};
|
||||
const result = await runCommand(
|
||||
'node',
|
||||
['--', script, '--json'],
|
||||
input,
|
||||
env,
|
||||
);
|
||||
const result = await runCommand('node', ['--', script, '--json'], input, env);
|
||||
return commandCheck('production-health-patrol', result, secrets);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,7 @@ function parseArgs(argv) {
|
||||
process.env.GENARRATIVE_PINGORA_REALPATH_CANARY_CONFIG_FILE ||
|
||||
'/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf',
|
||||
expectedPublicGateway:
|
||||
process.env.GENARRATIVE_PINGORA_REHEARSAL_EXPECT_PUBLIC_GATEWAY ||
|
||||
'none',
|
||||
process.env.GENARRATIVE_PINGORA_REHEARSAL_EXPECT_PUBLIC_GATEWAY || 'none',
|
||||
expectedHealthPatrolGatewayMode:
|
||||
process.env.GENARRATIVE_HEALTH_PATROL_EXPECTED_GATEWAY_MODE || '',
|
||||
requireRealpathCanary: readBoolEnv(
|
||||
@@ -298,12 +297,10 @@ async function inspectHealthPatrolEnv(input) {
|
||||
secretValues: collectSecretValuesFromEnv(values),
|
||||
values: {
|
||||
gatewayMode,
|
||||
publicBaseUrl:
|
||||
values.GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL || '',
|
||||
publicBaseUrl: values.GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL || '',
|
||||
publicHost: values.GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST || '',
|
||||
apiBaseUrl: values.GENARRATIVE_HEALTH_PATROL_API_BASE_URL || '',
|
||||
pingoraBaseUrl:
|
||||
values.GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL || '',
|
||||
pingoraBaseUrl: values.GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL || '',
|
||||
hasPingoraProbeToken: Boolean(
|
||||
values.GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN ||
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN,
|
||||
@@ -345,12 +342,9 @@ async function inspectPingoraEnv(input) {
|
||||
httpRedirectListen:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN || '',
|
||||
tlsCertFile: values.GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE || '',
|
||||
hasTlsKeyFile: Boolean(
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE,
|
||||
),
|
||||
hasTlsKeyFile: Boolean(values.GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE),
|
||||
hasProbeToken: Boolean(values.GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN),
|
||||
accessLogFile:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE || '',
|
||||
accessLogFile: values.GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE || '',
|
||||
compressionAlgorithms:
|
||||
values.GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS || '',
|
||||
trustXForwardedFor:
|
||||
@@ -519,7 +513,9 @@ async function inspectPingoraSystemd(input) {
|
||||
let status = 'OK';
|
||||
|
||||
if (catResult.code !== 0) {
|
||||
diagnostics.push(`systemctl cat 失败: ${catResult.stderr || catResult.error}`);
|
||||
diagnostics.push(
|
||||
`systemctl cat 失败: ${catResult.stderr || catResult.error}`,
|
||||
);
|
||||
status = 'CRITICAL';
|
||||
}
|
||||
if (showResult.code !== 0) {
|
||||
@@ -646,7 +642,9 @@ function parseSsOutput(stdout) {
|
||||
}
|
||||
const processText = parts.slice(5).join(' ');
|
||||
const processNames = [
|
||||
...new Set([...processText.matchAll(/"([^"]+)"/gu)].map((match) => match[1])),
|
||||
...new Set(
|
||||
[...processText.matchAll(/"([^"]+)"/gu)].map((match) => match[1]),
|
||||
),
|
||||
];
|
||||
listeners.push({
|
||||
localAddress: local,
|
||||
@@ -681,7 +679,9 @@ function inspectPort(port, listeners, input) {
|
||||
listener.processNames.some((name) => name.toLowerCase().includes('nginx')),
|
||||
);
|
||||
const hasPingora = listeners.some((listener) =>
|
||||
listener.processNames.some((name) => name.toLowerCase().includes('pingora')),
|
||||
listener.processNames.some((name) =>
|
||||
name.toLowerCase().includes('pingora'),
|
||||
),
|
||||
);
|
||||
const hasUnknownProcess =
|
||||
listeners.length > 0 &&
|
||||
@@ -702,7 +702,9 @@ function inspectPort(port, listeners, input) {
|
||||
status = 'CRITICAL';
|
||||
}
|
||||
if (hasPingora) {
|
||||
diagnostics.push(`${port} 已被 Pingora 监听,不能作为未切公网彩排状态。`);
|
||||
diagnostics.push(
|
||||
`${port} 已被 Pingora 监听,不能作为未切公网彩排状态。`,
|
||||
);
|
||||
status = 'CRITICAL';
|
||||
}
|
||||
}
|
||||
@@ -785,10 +787,14 @@ async function inspectRealpathCanary(input, ports) {
|
||||
'genarrative-pingora-realpath-canary.access.log',
|
||||
'X-Genarrative-Nginx-Handoff pingora-realpath-canary',
|
||||
];
|
||||
const missing = requiredSnippets.filter((snippet) => !content.includes(snippet));
|
||||
const missing = requiredSnippets.filter(
|
||||
(snippet) => !content.includes(snippet),
|
||||
);
|
||||
templateLooksValid = missing.length === 0;
|
||||
if (missing.length > 0) {
|
||||
diagnostics.push(`realpath canary 配置缺少关键片段: ${missing.join(', ')}`);
|
||||
diagnostics.push(
|
||||
`realpath canary 配置缺少关键片段: ${missing.join(', ')}`,
|
||||
);
|
||||
status = 'CRITICAL';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,7 @@ const STATUS_RANK = {
|
||||
CRITICAL: 2,
|
||||
};
|
||||
|
||||
const DEFAULT_PUBLIC_PATHS = [
|
||||
'/',
|
||||
];
|
||||
const DEFAULT_PUBLIC_PATHS = ['/'];
|
||||
|
||||
const DEFAULT_SERVICES = [
|
||||
'genarrative-api.service',
|
||||
@@ -30,7 +28,8 @@ const PINGORA_DIRECT_SERVICES = [
|
||||
'spacetimedb.service',
|
||||
'genarrative-pingora-gateway.service',
|
||||
];
|
||||
const WORKER_SERVICE_PATTERN = 'genarrative-external-generation-worker@*.service';
|
||||
const WORKER_SERVICE_PATTERN =
|
||||
'genarrative-external-generation-worker@*.service';
|
||||
const GATEWAY_MODES = new Set(['nginx', 'pingora-direct']);
|
||||
|
||||
function usage() {
|
||||
|
||||
Reference in New Issue
Block a user