e9c3dc1120
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码 切换前端 active 入口并解除旧创作页面和路由编译链 移除旧后端路由、worker 与纯业务 crate 依赖 收敛 SpacetimeDB 模块为历史数据壳 同步 Nginx、Pingora、验证门禁与架构文档
4337 lines
144 KiB
JavaScript
4337 lines
144 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { spawnSync } from 'node:child_process';
|
||
import {
|
||
mkdirSync,
|
||
mkdtempSync,
|
||
readFileSync,
|
||
rmSync,
|
||
symlinkSync,
|
||
writeFileSync,
|
||
} from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import path from 'node:path';
|
||
|
||
const failures = [];
|
||
|
||
const requireDirectBaseArgs = [
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-health-patrol-env-file',
|
||
'/etc/genarrative/health-patrol.env',
|
||
];
|
||
|
||
const cutoverRequiredArgs = [
|
||
'--direct-preflight-check-ports-free',
|
||
'--rollback-nginx-smoke-url',
|
||
'https://example.com/',
|
||
'--rollback-nginx-smoke-expect-body',
|
||
'<!doctype html>',
|
||
'--rollback-health-patrol-public-base-url',
|
||
'http://127.0.0.1',
|
||
];
|
||
|
||
function cutoverPlanArgs(...extraArgs) {
|
||
return [
|
||
...requireDirectBaseArgs,
|
||
'--dry-run-cutover',
|
||
...cutoverRequiredArgs,
|
||
...extraArgs,
|
||
];
|
||
}
|
||
|
||
const requireLiveBaseArgs = [
|
||
'--require-live',
|
||
'--live-base-url',
|
||
'http://127.0.0.1',
|
||
'--live-host',
|
||
'example.com',
|
||
'--live-nginx-access-log',
|
||
'/var/log/nginx/genarrative.access.log',
|
||
'--live-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--live-access-log-since-lines',
|
||
'3000',
|
||
];
|
||
|
||
const requireRealpathLiveBaseArgs = [
|
||
'--require-realpath-live',
|
||
'--realpath-live-base-url',
|
||
'http://127.0.0.1:18083',
|
||
'--realpath-live-host',
|
||
'example.com',
|
||
'--realpath-live-nginx-access-log',
|
||
'/var/log/nginx/genarrative-pingora-realpath-canary.access.log',
|
||
'--realpath-live-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--realpath-live-access-log-since-lines',
|
||
'3000',
|
||
];
|
||
|
||
main();
|
||
|
||
if (failures.length > 0) {
|
||
console.error('[check:pingora-release-readiness-plan] FAILED');
|
||
for (const failure of failures) {
|
||
console.error(`- ${failure}`);
|
||
}
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('[check:pingora-release-readiness-plan] OK');
|
||
|
||
function main() {
|
||
assertDefaultPlanIncludesStaticDirectPreflight();
|
||
assertDefaultPlanIncludesProductionApiReleaseSmoke();
|
||
assertDefaultPlanIncludesProductionApiDeploySmoke();
|
||
assertDefaultPlanIncludesHealthPatrolEnvSmoke();
|
||
assertDefaultPlanIncludesPingoraHealthPatrolEnvSwitchSmoke();
|
||
assertDefaultPlanIncludesPingoraGatewayEnvShadowSwitchSmoke();
|
||
assertDefaultPlanIncludesPingoraCurrentReleaseAuditSmoke();
|
||
assertDefaultPlanIncludesPingoraDirectRehearsalStatusSmoke();
|
||
assertDefaultPlanIncludesPingoraCutoverStatusSnapshotSmoke();
|
||
assertDefaultPlanIncludesPingoraCutoverEvidenceBundleSmoke();
|
||
assertDefaultPlanIncludesPingoraCutoverCommandEvidenceSmoke();
|
||
assertDefaultPlanIncludesPingoraCutoverEvidenceVerifySmoke();
|
||
assertDefaultPlanIncludesPingoraCutoverEvidenceAuditSmoke();
|
||
assertDefaultPlanIncludesCanaryAccessLogParitySmoke();
|
||
assertDefaultPlanIncludesRealpathCanaryToggleSmoke();
|
||
assertDefaultPlanIncludesPingoraProductionReleaseBuildSmoke();
|
||
assertReleaseRuntimeOnlyPlanUsesCurrentReleaseScripts();
|
||
assertReleaseRuntimeOnlyPlanIncludesDirectRehearsalStatus();
|
||
assertReleaseRuntimeOnlyAllowsRealpathOnlyCanary();
|
||
assertReleaseRuntimeOnlyKeepsInvokedCurrentSymlinkPath();
|
||
assertReleaseRuntimeOnlyRejectsSourceOnlyFlags();
|
||
assertDryRunCutoverPlanIncludesDirectEnableAndRollbackRunbook();
|
||
assertDryRunCutoverExplicitRollbackBodyIgnoresProcessEnv();
|
||
assertDryRunCutoverPostEnableReadinessRedactsDirectProbeToken();
|
||
assertDryRunCutoverPlanIncludesHostConsistencyConfirmation();
|
||
assertDryRunCutoverAllowsSameHostnameWithDifferentPorts();
|
||
assertDryRunCutoverCanRestoreRollbackHealthPatrolPublicHost();
|
||
assertDryRunCutoverPlanCanIncludeRollbackShadowProbe();
|
||
assertDryRunCutoverPlanCanOverrideEvidenceTimelineMaxSpan();
|
||
assertDryRunCutoverPlanAvoidsManualEvidenceEscapeHatches();
|
||
assertTechnicalDocCutoverAuditExamplesRequireCommandExecutables();
|
||
assertDryRunCutoverPlanCarriesCutoverRunId();
|
||
assertDryRunCutoverPlanGeneratesDefaultCutoverRunId();
|
||
assertRequireLiveForcesHost();
|
||
assertRequireRealpathLiveForcesHostAndRealpathParity();
|
||
assertRequireLiveRejectsRelativeAccessLog();
|
||
assertRequireLiveRejectsFilesystemRootAccessLog();
|
||
assertRequireLiveRejectsInvalidAccessLogSinceLines();
|
||
assertRequireRealpathLiveRejectsMissingHost();
|
||
assertRequireRealpathLiveRejectsRelativeAccessLog();
|
||
assertLiveSmokeScriptsRejectInvalidTimeout();
|
||
assertDirectLiveRejectsInvalidBoolEnv();
|
||
assertDirectLiveRejectsFilesystemRootAccessLog();
|
||
assertReleaseReadinessRejectsInvalidDirectBoolEnv();
|
||
assertDirectPreflightRejectsInvalidBoolEnv();
|
||
assertDirectPreflightRejectsUnconfirmedMultiInstanceProtection();
|
||
assertCutoverEvidenceScriptsRejectInvalidBoolEnv();
|
||
assertRequireDirectForcesWssUpgrade();
|
||
assertRequireDirectForcesHttpBaseUrl();
|
||
assertRequireDirectForcesHostSni();
|
||
assertRequireDirectForcesRedirectHost();
|
||
assertRequireDirectForcesPingoraAccessLog();
|
||
assertRequireDirectForcesPreflightSystemdCertReadable();
|
||
assertDryRunCutoverForcesPortsFreePreflight();
|
||
assertRequireDirectForcesExplicitSpacetimeDatabase();
|
||
assertRequireDirectForcesHealthPatrolEnvCheck();
|
||
assertDryRunPlanRedactsDirectProbeToken();
|
||
assertRunStepLogRedactsDirectProbeToken();
|
||
assertRequireLiveRejectsMissingHost();
|
||
assertRequireLiveRejectsInvalidHost();
|
||
assertRequireDirectRejectsMissingHttpBaseUrl();
|
||
assertRequireDirectRejectsMissingHost();
|
||
assertRequireDirectRejectsInvalidHost();
|
||
assertRequireDirectRejectsMissingRedirectHost();
|
||
assertRequireDirectRejectsInvalidRedirectHost();
|
||
assertRequireDirectRejectsMissingPingoraAccessLog();
|
||
assertRequireDirectRejectsFilesystemRootPingoraAccessLog();
|
||
assertRequireDirectRejectsMissingSystemdPreflight();
|
||
assertRequireDirectRejectsMissingCertReadablePreflight();
|
||
assertRequireDirectRejectsMissingServiceEnvFilePreflight();
|
||
assertRequireDirectRejectsMissingServiceUserCertReadablePreflight();
|
||
assertRequireDirectRejectsMissingServiceBinaryExecutablePreflight();
|
||
assertRequireDirectAllowsPostEnablePortsOwnedByPingora();
|
||
assertDryRunCutoverRejectsMissingPortsFreePreflight();
|
||
assertRequireDirectRejectsMissingSpacetimeDatabase();
|
||
assertRequireDirectRejectsFilesystemRootPreflightEnvFile();
|
||
assertRequireDirectRejectsMissingHealthPatrolEnvFile();
|
||
assertRequireDirectRejectsRelativeHealthPatrolEnvFile();
|
||
assertRequireDirectRejectsFilesystemRootHealthPatrolEnvFile();
|
||
assertRequireDirectRejectsSkipWss();
|
||
assertRequireDirectRejectsInsecureTls();
|
||
assertDryRunCutoverRejectsMissingRequireDirect();
|
||
assertDryRunCutoverRejectsRelativeEvidenceOutputRoot();
|
||
assertDryRunCutoverRejectsFilesystemRootReleaseRoot();
|
||
assertDryRunCutoverRejectsFilesystemRootEvidenceOutputRoot();
|
||
assertUsageCutoverExampleIncludesExplicitEvidenceOutputRoot();
|
||
assertUsageExamplesIncludeDirectPingoraAccessLog();
|
||
assertDryRunCutoverRejectsMissingRollbackSmokeBody();
|
||
assertDryRunCutoverRejectsMissingRollbackHealthPatrolPublicBaseUrl();
|
||
assertDryRunCutoverRejectsInvalidRollbackHealthPatrolPublicBaseUrl();
|
||
assertDryRunCutoverRejectsInvalidRollbackHealthPatrolPublicHost();
|
||
assertDryRunCutoverRejectsRedirectHostMismatch();
|
||
assertDryRunCutoverRejectsRollbackHostMismatch();
|
||
assertDryRunCutoverRejectsIncompleteRollbackShadowProbe();
|
||
assertDryRunCutoverRejectsInvalidRollbackShadowProbeUrl();
|
||
assertDryRunCutoverRejectsInvalidEvidenceTimelineMaxSpan();
|
||
assertDryRunCutoverRejectsInvalidCutoverRunId();
|
||
}
|
||
|
||
function assertDefaultPlanIncludesStaticDirectPreflight() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora direct entry 静态预检');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 direct-entry 静态预检。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-direct-preflight',
|
||
'direct-entry 静态预检必须调用 check:pingora-direct-preflight。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesProductionApiDeploySmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'API deploy release 烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 API deploy release 烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:production-api-deploy',
|
||
'API deploy release 烟测必须调用 check:production-api-deploy。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesHealthPatrolEnvSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, '生产健康巡检 env 复核烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含生产健康巡检 env 复核烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:production-health-patrol-env',
|
||
'生产健康巡检 env 复核烟测必须调用 check:production-health-patrol-env。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraHealthPatrolEnvSwitchSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora health patrol env 切换烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora health patrol env 切换烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-health-patrol-env-switch',
|
||
'Pingora health patrol env 切换烟测必须调用 check:pingora-health-patrol-env-switch。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraGatewayEnvShadowSwitchSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora gateway env shadow 切换烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora gateway env shadow 切换烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-gateway-env-shadow-switch',
|
||
'Pingora gateway env shadow 切换烟测必须调用 check:pingora-gateway-env-shadow-switch。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraCutoverStatusSnapshotSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora cutover 状态快照烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora cutover 状态快照烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-cutover-status-snapshot',
|
||
'Pingora cutover 状态快照烟测必须调用 check:pingora-cutover-status-snapshot。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraCurrentReleaseAuditSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora current release 自审烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora current release 自审烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-current-release-audit',
|
||
'Pingora current release 自审烟测必须调用 check:pingora-current-release-audit。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraDirectRehearsalStatusSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora direct rehearsal 状态烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora direct rehearsal 状态烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-direct-rehearsal-status',
|
||
'Pingora direct rehearsal 状态烟测必须调用 check:pingora-direct-rehearsal-status。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraCutoverEvidenceBundleSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora cutover 证据包烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora cutover 证据包烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-cutover-evidence-bundle',
|
||
'Pingora cutover 证据包烟测必须调用 check:pingora-cutover-evidence-bundle。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraCutoverCommandEvidenceSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora cutover 命令证据烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora cutover 命令证据烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-cutover-command-evidence',
|
||
'Pingora cutover 命令证据烟测必须调用 check:pingora-cutover-command-evidence。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraCutoverEvidenceVerifySmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora cutover 证据 manifest 验真烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora cutover 证据 manifest 验真烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-cutover-evidence-verify',
|
||
'Pingora cutover 证据 manifest 验真烟测必须调用 check:pingora-cutover-evidence-verify。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraCutoverEvidenceAuditSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora cutover 证据根目录审计烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora cutover 证据根目录审计烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-cutover-evidence-audit',
|
||
'Pingora cutover 证据根目录审计烟测必须调用 check:pingora-cutover-evidence-audit。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesCanaryAccessLogParitySmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora canary access log 对账烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora canary access log 对账烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-canary-access-log-parity',
|
||
'Pingora canary access log 对账烟测必须调用 check:pingora-canary-access-log-parity。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesRealpathCanaryToggleSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora realpath canary 启停烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora realpath canary 启停烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-realpath-canary-toggle',
|
||
'Pingora realpath canary 启停烟测必须调用 check:pingora-realpath-canary-toggle。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesProductionApiReleaseSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'API release build 烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 API release build 烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:production-api-release',
|
||
'API release build 烟测必须调用 check:production-api-release。',
|
||
);
|
||
}
|
||
|
||
function assertDefaultPlanIncludesPingoraProductionReleaseBuildSmoke() {
|
||
const plan = readPlan(['--dry-run-plan']);
|
||
const step = findStep(plan, 'Pingora production release 真实构建烟测');
|
||
if (!step) {
|
||
failures.push(
|
||
'默认 release readiness 计划必须包含 Pingora production release 真实构建烟测。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'check:pingora-production-release-build',
|
||
'Pingora production release 真实构建烟测必须调用 check:pingora-production-release-build。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverPlanIncludesDirectEnableAndRollbackRunbook() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
));
|
||
const preflight = findStep(
|
||
plan,
|
||
'切换前 current release 直连 env / 证书 / 端口预检',
|
||
);
|
||
const releaseAudit = findStep(plan, '切换前 current release 自包含自审');
|
||
if (!releaseAudit) {
|
||
failures.push('dry-run cutover 必须包含切换前 current release 自包含自审。');
|
||
} else {
|
||
assertIncludes(
|
||
releaseAudit.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs',
|
||
'切换前 current release 自审必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
releaseAudit.args,
|
||
'--require-pingora-gateway',
|
||
'正式直连 runbook 的 current release 自审必须要求 pingora-gateway 已随包存在。',
|
||
);
|
||
assertIncludes(
|
||
releaseAudit.args,
|
||
'--systemd-show',
|
||
'正式直连 runbook 的 current release 自审必须核验 systemd ExecStart。',
|
||
);
|
||
}
|
||
const preSnapshot = findStep(plan, '切换前状态快照证据包');
|
||
const preVerify = findStep(plan, '切换前证据 manifest 只读验真');
|
||
if (!preSnapshot) {
|
||
failures.push('dry-run cutover 必须包含切换前状态快照证据包。');
|
||
} else {
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-bundle.mjs',
|
||
'切换前状态快照证据包必须使用 current release 随包证据包脚本。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'--snapshot-script',
|
||
'切换前状态快照证据包必须显式透传 snapshot 脚本。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'--output-root',
|
||
'切换前状态快照证据包必须显式传证据输出根目录。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'/var/log/genarrative/pingora-cutover-evidence',
|
||
'切换前状态快照证据包必须默认写入固定证据根目录。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-status-snapshot.mjs',
|
||
'切换前状态快照证据包必须使用 current release 随包状态快照脚本。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'--expected-gateway-mode',
|
||
'切换前状态快照证据包必须显式传 expected gateway mode。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'nginx',
|
||
'切换前状态快照证据包必须确认仍是 Nginx 巡检口径。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'--run-health-patrol',
|
||
'切换前状态快照证据包必须执行生产健康巡检。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'--require-pingora-gateway',
|
||
'切换前状态快照证据包必须收录 Pingora 发布物 checksum / manifest 自审。',
|
||
);
|
||
assertIncludes(
|
||
preSnapshot.args,
|
||
'--fail-on-critical',
|
||
'切换前状态快照证据包出现 CRITICAL 必须阻断切换。',
|
||
);
|
||
}
|
||
if (!preVerify) {
|
||
failures.push('dry-run cutover 必须包含切换前证据 manifest 只读验真。');
|
||
} else {
|
||
assertIncludes(
|
||
preVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'切换前证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
preVerify.args,
|
||
'<pre-cutover-bundle-dir>',
|
||
'切换前证据验真必须提示替换刚生成的 pre-cutover bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
preVerify.args,
|
||
'--require-summary-ok',
|
||
'切换前证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
if (!preflight) {
|
||
failures.push('dry-run cutover 必须包含启用前 current release 预检。');
|
||
} else {
|
||
assertIncludes(
|
||
preflight.args,
|
||
'/opt/genarrative/current/scripts/check-pingora-direct-preflight.mjs',
|
||
'dry-run cutover 启用前预检必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
preflight.args,
|
||
'--check-service-user-cert-readable',
|
||
'dry-run cutover 启用前预检必须检查 systemd 服务用户可读证书和私钥。',
|
||
);
|
||
assertIncludes(
|
||
preflight.args,
|
||
'--check-service-env-file',
|
||
'dry-run cutover 启用前预检必须检查 service EnvironmentFile 包含本次 env。',
|
||
);
|
||
assertIncludes(
|
||
preflight.args,
|
||
'--check-service-binary-executable',
|
||
'dry-run cutover 启用前预检必须检查 current release Pingora 二进制可执行。',
|
||
);
|
||
assertIncludes(
|
||
preflight.args,
|
||
'--check-ports-free',
|
||
'dry-run cutover 启用前预检必须检查 80/443 端口释放。',
|
||
);
|
||
assertIncludes(
|
||
preflight.args,
|
||
'--systemd-cat',
|
||
'dry-run cutover 启用前预检必须检查当前 systemd direct-entry 状态。',
|
||
);
|
||
}
|
||
|
||
const baseReadiness = findStep(plan, '切换前 release readiness 基础门禁');
|
||
if (!baseReadiness) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含启用前 release readiness 基础门禁。',
|
||
);
|
||
} else if (baseReadiness.args.includes('--require-direct')) {
|
||
failures.push(
|
||
'dry-run cutover 启用前基础门禁不能带 --require-direct;systemd drop-in 尚未生效。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
baseReadiness.args,
|
||
'/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs',
|
||
'dry-run cutover 启用前基础门禁必须使用 current release 随包 release readiness 聚合门禁脚本。',
|
||
);
|
||
assertIncludes(
|
||
baseReadiness.args,
|
||
'--release-runtime-only',
|
||
'dry-run cutover 启用前基础门禁必须使用 current release 包内运行时复核模式。',
|
||
);
|
||
}
|
||
|
||
const enableDryRun = findStep(plan, 'Pingora direct enable dry-run');
|
||
if (!enableDryRun) {
|
||
failures.push('dry-run cutover 必须包含 direct enable dry-run。');
|
||
} else {
|
||
assertIncludes(
|
||
enableDryRun.command,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh',
|
||
'dry-run cutover enable dry-run 必须使用 current release 随包启用脚本。',
|
||
);
|
||
if (enableDryRun.args.includes('--apply')) {
|
||
failures.push('dry-run cutover enable dry-run 不能带 --apply。');
|
||
}
|
||
assertIncludes(
|
||
enableDryRun.args,
|
||
'--current-release-audit-script',
|
||
'dry-run cutover enable dry-run 必须显式固定 current release 自审脚本。',
|
||
);
|
||
assertIncludes(
|
||
enableDryRun.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs',
|
||
'dry-run cutover enable dry-run 必须使用 current release 随包自审脚本。',
|
||
);
|
||
assertIncludes(
|
||
enableDryRun.args,
|
||
'--current-release-root',
|
||
'dry-run cutover enable dry-run 必须显式固定 current release 根目录。',
|
||
);
|
||
assertIncludes(
|
||
enableDryRun.args,
|
||
'/opt/genarrative/current',
|
||
'dry-run cutover enable dry-run 必须把 current release 根目录传给启用脚本。',
|
||
);
|
||
assertIncludes(
|
||
enableDryRun.args,
|
||
'/opt/genarrative/current/scripts/check-pingora-direct-live.mjs',
|
||
'dry-run cutover enable dry-run 必须指定 current release 随包 direct live smoke。',
|
||
);
|
||
}
|
||
|
||
const enableApply = findStep(plan, 'Pingora direct enable apply');
|
||
const enableApplyVerify = findStep(plan, '启用命令证据 manifest 只读验真');
|
||
if (!enableApply) {
|
||
failures.push('dry-run cutover 必须包含 direct enable apply。');
|
||
} else {
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs',
|
||
'dry-run cutover enable apply 必须通过 current release 随包命令证据脚本归档执行输出。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--command-name',
|
||
'dry-run cutover enable apply 必须显式记录命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'pingora-direct-enable-apply',
|
||
'dry-run cutover enable apply 必须记录固定命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--output-root',
|
||
'dry-run cutover enable apply 必须显式传证据输出根目录。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'/var/log/genarrative/pingora-cutover-evidence',
|
||
'dry-run cutover enable apply 必须使用固定证据根目录。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh',
|
||
'dry-run cutover enable apply 必须在命令证据脚本后执行 current release 随包启用脚本。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--expected-executable',
|
||
'dry-run cutover enable apply 必须把预期真实命令绑定到命令证据。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--require-arg',
|
||
'dry-run cutover enable apply 必须在命令证据脚本中要求真实命令参数。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--apply',
|
||
'dry-run cutover enable apply 必须显式带 --apply。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--current-release-audit-script',
|
||
'dry-run cutover enable apply 必须显式固定 current release 自审脚本。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs',
|
||
'dry-run cutover enable apply 必须使用 current release 随包自审脚本。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--current-release-root',
|
||
'dry-run cutover enable apply 必须显式固定 current release 根目录。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--preflight-check-service-user-cert-readable',
|
||
'dry-run cutover enable apply 必须验证 systemd 服务用户可读证书和私钥。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--preflight-check-service-env-file',
|
||
'dry-run cutover enable apply 必须验证 service EnvironmentFile 包含本次 env。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--preflight-check-service-binary-executable',
|
||
'dry-run cutover enable apply 必须验证 current release Pingora 二进制可执行。',
|
||
);
|
||
assertIncludes(
|
||
enableApply.args,
|
||
'--direct-spacetime-database',
|
||
'dry-run cutover enable apply 必须验证显式 SpacetimeDB 数据库名。',
|
||
);
|
||
}
|
||
if (!enableApplyVerify) {
|
||
failures.push('dry-run cutover 必须包含启用命令证据 manifest 只读验真。');
|
||
} else {
|
||
assertIncludes(
|
||
enableApplyVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'启用命令证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
enableApplyVerify.args,
|
||
'<enable-apply-bundle-dir>',
|
||
'启用命令证据验真必须提示替换刚生成的 enable apply bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
enableApplyVerify.args,
|
||
'--require-summary-ok',
|
||
'启用命令证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
|
||
const postReadiness = findStep(plan, '启用后 release readiness 直连复核');
|
||
const postEnableSnapshot = findStep(plan, '启用后状态快照证据包');
|
||
const postEnableVerify = findStep(plan, '启用后证据 manifest 只读验真');
|
||
if (!postEnableSnapshot) {
|
||
failures.push('dry-run cutover 必须包含启用后状态快照证据包。');
|
||
} else {
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-bundle.mjs',
|
||
'启用后状态快照证据包必须使用 current release 随包证据包脚本。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--snapshot-script',
|
||
'启用后状态快照证据包必须显式透传 snapshot 脚本。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--output-root',
|
||
'启用后状态快照证据包必须显式传证据输出根目录。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-status-snapshot.mjs',
|
||
'启用后状态快照证据包必须使用 current release 随包状态快照脚本。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'pingora-direct',
|
||
'启用后状态快照证据包必须确认 pingora-direct 巡检口径。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--expected-pingora-env-mode',
|
||
'启用后状态快照证据包必须显式校验 active Pingora env 姿态。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'direct',
|
||
'启用后状态快照证据包必须要求 active Pingora env 是 direct 姿态。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--expected-public-host',
|
||
'启用后状态快照证据包必须确认正式 Host 覆盖。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--run-health-patrol',
|
||
'启用后状态快照证据包必须执行生产健康巡检。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--direct-live-script',
|
||
'启用后状态快照证据包必须显式透传 direct live smoke 脚本。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'/opt/genarrative/current/scripts/check-pingora-direct-live.mjs',
|
||
'启用后状态快照证据包必须使用 current release 随包 direct live smoke。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--run-direct-live',
|
||
'启用后状态快照证据包必须归档 direct live smoke 证据。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--direct-pingora-access-log',
|
||
'启用后状态快照证据包必须归档 direct access log request_id 证据。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'启用后状态快照证据包必须使用显式 Pingora access log 路径。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--direct-spacetime-database',
|
||
'启用后状态快照证据包必须传递显式 SpacetimeDB 数据库名。',
|
||
);
|
||
assertIncludes(
|
||
postEnableSnapshot.args,
|
||
'--require-pingora-gateway',
|
||
'启用后状态快照证据包必须收录 Pingora 发布物 checksum / manifest 自审。',
|
||
);
|
||
}
|
||
if (!postEnableVerify) {
|
||
failures.push('dry-run cutover 必须包含启用后证据 manifest 只读验真。');
|
||
} else {
|
||
assertIncludes(
|
||
postEnableVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'启用后证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
postEnableVerify.args,
|
||
'<post-enable-bundle-dir>',
|
||
'启用后证据验真必须提示替换刚生成的 post-enable bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
postEnableVerify.args,
|
||
'--require-summary-ok',
|
||
'启用后证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
if (!postReadiness) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含启用后 release readiness 直连复核。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs',
|
||
'启用后 release readiness 直连复核必须使用 current release 随包 release readiness 聚合门禁脚本。',
|
||
);
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'--release-runtime-only',
|
||
'启用后 release readiness 直连复核必须使用 current release 包内运行时复核模式。',
|
||
);
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'--require-direct',
|
||
'dry-run cutover 启用后复核必须带 --require-direct。',
|
||
);
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'dry-run cutover 启用后复核必须检查 systemd 服务用户可读证书和私钥。',
|
||
);
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'--direct-preflight-check-service-env-file',
|
||
'dry-run cutover 启用后复核必须检查 service EnvironmentFile 包含本次 env。',
|
||
);
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'dry-run cutover 启用后复核必须检查 Pingora service 二进制可执行。',
|
||
);
|
||
assertIncludes(
|
||
postReadiness.args,
|
||
'--direct-preflight-systemd',
|
||
'dry-run cutover 启用后复核必须检查 systemd drop-in 生效。',
|
||
);
|
||
if (postReadiness.args.includes('--direct-preflight-check-ports-free')) {
|
||
failures.push(
|
||
'dry-run cutover 启用后 release readiness 复核不能继续要求 80/443 空闲;此时端口应由 Pingora 直连入口占用。',
|
||
);
|
||
}
|
||
}
|
||
|
||
const healthPatrolEnvCheck = findStep(
|
||
plan,
|
||
'启用后 health patrol env 直连复核',
|
||
);
|
||
if (!healthPatrolEnvCheck) {
|
||
failures.push('dry-run cutover 必须包含启用后 health patrol env 复核。');
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolEnvCheck.args,
|
||
'/opt/genarrative/current/scripts/check-production-health-patrol-env.mjs',
|
||
'dry-run cutover health patrol env 复核必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolEnvCheck.args,
|
||
'/etc/genarrative/health-patrol.env',
|
||
'dry-run cutover health patrol env 复核必须检查目标 env 文件。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolEnvCheck.args,
|
||
'--expected-gateway-mode',
|
||
'dry-run cutover health patrol env 复核必须显式传 gateway mode。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolEnvCheck.args,
|
||
'pingora-direct',
|
||
'dry-run cutover health patrol env 复核必须要求 pingora-direct。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolEnvCheck.args,
|
||
'--expected-public-base-url',
|
||
'dry-run cutover health patrol env 复核必须校验 public base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolEnvCheck.args,
|
||
'--expected-public-host',
|
||
'dry-run cutover health patrol env 复核必须校验正式 Host。',
|
||
);
|
||
}
|
||
|
||
const healthPatrolDirect = findStep(
|
||
plan,
|
||
'切换后 health patrol 切到 Pingora direct',
|
||
);
|
||
const healthPatrolDirectVerify = findStep(
|
||
plan,
|
||
'切换后 health patrol direct 命令证据 manifest 只读验真',
|
||
);
|
||
if (!healthPatrolDirect) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含切换后 health patrol direct 模式脚本。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs',
|
||
'dry-run cutover 切换后 health patrol env 切换必须通过命令证据脚本归档。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--command-name',
|
||
'dry-run cutover 切换后 health patrol env 切换必须显式记录命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'pingora-health-patrol-direct-env-switch',
|
||
'dry-run cutover 切换后 health patrol env 切换必须记录固定命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--expected-executable',
|
||
'dry-run cutover 切换后 health patrol env 切换必须绑定真实脚本身份。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs',
|
||
'dry-run cutover 切换后必须使用 current release 随包 health patrol env 切换脚本。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--apply',
|
||
'dry-run cutover 切换后 health patrol env 切换必须显式带 --apply。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--env-file',
|
||
'dry-run cutover 切换后 health patrol env 切换必须显式传 env 文件。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'/etc/genarrative/health-patrol.env',
|
||
'dry-run cutover 切换后 health patrol env 切换必须使用显式 env 文件。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--gateway-mode',
|
||
'dry-run cutover 切换后 health patrol env 切换必须显式传 gateway mode。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'pingora-direct',
|
||
'dry-run cutover 切换后 health patrol env 切换必须改为 pingora-direct。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--public-base-url',
|
||
'dry-run cutover 切换后 health patrol env 切换必须写 direct public base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'https://127.0.0.1',
|
||
'dry-run cutover 切换后 health patrol env 切换必须写 direct HTTPS base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'--public-host',
|
||
'dry-run cutover 切换后 health patrol env 切换必须写正式 Host。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirect.args,
|
||
'example.com',
|
||
'dry-run cutover 切换后 health patrol env 切换必须使用正式 Host。',
|
||
);
|
||
}
|
||
if (!healthPatrolDirectVerify) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含切换后 health patrol direct 命令证据 manifest 只读验真。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolDirectVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'切换后 health patrol direct 命令证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirectVerify.args,
|
||
'<health-patrol-direct-env-switch-bundle-dir>',
|
||
'切换后 health patrol direct 命令证据验真必须提示替换刚生成的 bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolDirectVerify.args,
|
||
'--require-summary-ok',
|
||
'切换后 health patrol direct 命令证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
assertStepBefore(
|
||
plan,
|
||
'Pingora direct enable apply',
|
||
'启用命令证据 manifest 只读验真',
|
||
'dry-run cutover 必须先生成 enable apply 命令证据,再验真该命令证据。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'启用命令证据 manifest 只读验真',
|
||
'切换后 health patrol 切到 Pingora direct',
|
||
'dry-run cutover 必须先验真 enable apply 命令证据,再切换 health patrol 到 pingora-direct。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'切换后 health patrol 切到 Pingora direct',
|
||
'切换后 health patrol direct 命令证据 manifest 只读验真',
|
||
'dry-run cutover 必须先生成 health patrol direct env 命令证据,再验真该命令证据。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'切换后 health patrol direct 命令证据 manifest 只读验真',
|
||
'启用后 health patrol env 直连复核',
|
||
'dry-run cutover 必须先验真 health patrol direct env 命令证据,再执行直连 env 复核。',
|
||
);
|
||
|
||
const rollbackDryRun = findStep(plan, 'Pingora direct rollback dry-run');
|
||
if (!rollbackDryRun) {
|
||
failures.push('dry-run cutover 必须包含 rollback dry-run。');
|
||
} else {
|
||
assertIncludes(
|
||
rollbackDryRun.command,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh',
|
||
'dry-run cutover rollback dry-run 必须使用 current release 随包回退脚本。',
|
||
);
|
||
if (rollbackDryRun.args.includes('--apply')) {
|
||
failures.push('dry-run cutover rollback dry-run 不能带 --apply。');
|
||
}
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'--reload-nginx',
|
||
'dry-run cutover rollback dry-run 必须包含 Nginx reload。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'--nginx-smoke-host',
|
||
'dry-run cutover rollback dry-run 使用 loopback smoke URL 时必须带正式 Host。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'--nginx-smoke-expect-body',
|
||
'dry-run cutover rollback dry-run 必须校验 Nginx smoke 响应体片段。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'<!doctype html>',
|
||
'dry-run cutover rollback dry-run 必须使用显式传入的 Nginx smoke body 片段。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'--health-patrol-env-file',
|
||
'dry-run cutover rollback dry-run 必须把 health patrol env 复核交给回退脚本。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'/etc/genarrative/health-patrol.env',
|
||
'dry-run cutover rollback dry-run 必须使用显式 health patrol env 文件。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'--health-patrol-expected-public-base-url',
|
||
'dry-run cutover rollback dry-run 必须校验回退 health patrol public base URL。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'http://127.0.0.1',
|
||
'dry-run cutover rollback dry-run 必须传递显式回退 public base URL。',
|
||
);
|
||
assertIncludes(
|
||
rollbackDryRun.args,
|
||
'--health-patrol-require-empty-public-host',
|
||
'dry-run cutover rollback dry-run 默认必须要求回退 public Host 为空。',
|
||
);
|
||
}
|
||
|
||
const rollbackApply = findStep(plan, 'Pingora direct rollback apply');
|
||
const rollbackApplyVerify = findStep(plan, '回退命令证据 manifest 只读验真');
|
||
if (!rollbackApply) {
|
||
failures.push('dry-run cutover 必须包含 rollback apply。');
|
||
} else {
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs',
|
||
'dry-run cutover rollback apply 必须通过 current release 随包命令证据脚本归档执行输出。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--command-name',
|
||
'dry-run cutover rollback apply 必须显式记录命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'pingora-direct-rollback-apply',
|
||
'dry-run cutover rollback apply 必须记录固定命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh',
|
||
'dry-run cutover rollback apply 必须在命令证据脚本后执行 current release 随包回退脚本。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--expected-executable',
|
||
'dry-run cutover rollback apply 必须把预期真实命令绑定到命令证据。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--require-arg',
|
||
'dry-run cutover rollback apply 必须在命令证据脚本中要求真实命令参数。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--apply',
|
||
'dry-run cutover rollback apply 必须显式带 --apply。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'https://example.com/',
|
||
'dry-run cutover rollback apply 必须包含 Nginx smoke URL。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'example.com',
|
||
'dry-run cutover rollback apply 默认必须复用 direct Host 作为 Nginx smoke Host。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--nginx-smoke-host',
|
||
'dry-run cutover rollback apply 使用 loopback smoke URL 时必须带正式 Host。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--nginx-smoke-expect-body',
|
||
'dry-run cutover rollback apply 必须校验 Nginx smoke 响应体片段。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'<!doctype html>',
|
||
'dry-run cutover rollback apply 必须使用显式传入的 Nginx smoke body 片段。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--health-patrol-env-file',
|
||
'dry-run cutover rollback apply 必须把 health patrol env 复核交给回退脚本。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--health-patrol-expected-public-base-url',
|
||
'dry-run cutover rollback apply 必须校验回退 health patrol public base URL。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'http://127.0.0.1',
|
||
'dry-run cutover rollback apply 必须传递显式回退 public base URL。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'--health-patrol-require-empty-public-host',
|
||
'dry-run cutover rollback apply 默认必须要求回退 public Host 为空。',
|
||
);
|
||
}
|
||
if (!rollbackApplyVerify) {
|
||
failures.push('dry-run cutover 必须包含回退命令证据 manifest 只读验真。');
|
||
} else {
|
||
assertIncludes(
|
||
rollbackApplyVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'回退命令证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApplyVerify.args,
|
||
'<rollback-apply-bundle-dir>',
|
||
'回退命令证据验真必须提示替换刚生成的 rollback apply bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
rollbackApplyVerify.args,
|
||
'--require-summary-ok',
|
||
'回退命令证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
|
||
const healthPatrolNginx = findStep(plan, '回退前 health patrol 预置回 Nginx');
|
||
const pingoraEnvShadow = findStep(plan, '回退前 Pingora env 预置回 shadow');
|
||
const pingoraEnvShadowVerify = findStep(
|
||
plan,
|
||
'回退前 Pingora env shadow 命令证据 manifest 只读验真',
|
||
);
|
||
const healthPatrolNginxVerify = findStep(
|
||
plan,
|
||
'回退前 health patrol nginx 命令证据 manifest 只读验真',
|
||
);
|
||
if (!pingoraEnvShadow) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退前 Pingora env shadow 配置预置确认。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs',
|
||
'dry-run cutover 回退前 Pingora env shadow 预置必须通过命令证据脚本归档。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'--command-name',
|
||
'dry-run cutover 回退前 Pingora env shadow 预置必须显式记录命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'pingora-gateway-shadow-env-switch',
|
||
'dry-run cutover 回退前 Pingora env shadow 预置必须记录固定命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'--expected-executable',
|
||
'dry-run cutover 回退前 Pingora env shadow 预置必须绑定真实脚本身份。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs',
|
||
'dry-run cutover 回退前必须使用 current release 随包 Pingora gateway env shadow 切换脚本。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'--apply',
|
||
'dry-run cutover 回退前 Pingora gateway env shadow 切换必须显式带 --apply。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'--env-file',
|
||
'dry-run cutover 回退前 Pingora gateway env shadow 切换必须显式传 env 文件。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadow.args,
|
||
'/tmp/pingora-gateway.env',
|
||
'dry-run cutover 回退前 Pingora gateway env shadow 切换必须使用显式 env 文件。',
|
||
);
|
||
if (!pingoraEnvShadow.when.includes('80/443 direct')) {
|
||
failures.push(
|
||
'dry-run cutover 回退前 Pingora env shadow 预置说明必须解释从 80/443 direct 配置恢复。',
|
||
);
|
||
}
|
||
}
|
||
if (!pingoraEnvShadowVerify) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退前 Pingora env shadow 命令证据 manifest 只读验真。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
pingoraEnvShadowVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'回退前 Pingora env shadow 命令证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadowVerify.args,
|
||
'<pingora-gateway-shadow-env-switch-bundle-dir>',
|
||
'回退前 Pingora env shadow 命令证据验真必须提示替换刚生成的 bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
pingoraEnvShadowVerify.args,
|
||
'--require-summary-ok',
|
||
'回退前 Pingora env shadow 命令证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
if (!healthPatrolNginx) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退前 health patrol Nginx 模式预置脚本。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs',
|
||
'dry-run cutover 回退前 health patrol env 切回 nginx 必须通过命令证据脚本归档。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--command-name',
|
||
'dry-run cutover 回退前 health patrol env 切回 nginx 必须显式记录命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'pingora-health-patrol-nginx-env-switch',
|
||
'dry-run cutover 回退前 health patrol env 切回 nginx 必须记录固定命令证据名称。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--expected-executable',
|
||
'dry-run cutover 回退前 health patrol env 切回 nginx 必须绑定真实脚本身份。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs',
|
||
'dry-run cutover 回退前必须使用 current release 随包 health patrol env 切换脚本。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--apply',
|
||
'dry-run cutover 回退前 health patrol env 切换必须显式带 --apply。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--gateway-mode',
|
||
'dry-run cutover 回退前 health patrol env 切换必须显式传 gateway mode。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'nginx',
|
||
'dry-run cutover 回退前 health patrol env 切换必须改回 nginx。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--public-base-url',
|
||
'dry-run cutover 回退前 health patrol env 切换必须恢复 Nginx public base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'http://127.0.0.1',
|
||
'dry-run cutover 回退前 health patrol env 切换必须恢复切换前 Nginx public base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--clear-public-host',
|
||
'dry-run cutover 回退前 health patrol env 切换必须清理 public Host 覆盖。',
|
||
);
|
||
}
|
||
if (!healthPatrolNginxVerify) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退前 health patrol nginx 命令证据 manifest 只读验真。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolNginxVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'回退前 health patrol nginx 命令证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxVerify.args,
|
||
'<health-patrol-nginx-env-switch-bundle-dir>',
|
||
'回退前 health patrol nginx 命令证据验真必须提示替换刚生成的 bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxVerify.args,
|
||
'--require-summary-ok',
|
||
'回退前 health patrol nginx 命令证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
|
||
const healthPatrolIndex = plan.findIndex(
|
||
(step) => step.name === '回退前 health patrol 预置回 Nginx',
|
||
);
|
||
const rollbackApplyIndex = plan.findIndex(
|
||
(step) => step.name === 'Pingora direct rollback apply',
|
||
);
|
||
const pingoraEnvShadowIndex = plan.findIndex(
|
||
(step) => step.name === '回退前 Pingora env 预置回 shadow',
|
||
);
|
||
if (
|
||
pingoraEnvShadowIndex >= 0 &&
|
||
rollbackApplyIndex >= 0 &&
|
||
pingoraEnvShadowIndex > rollbackApplyIndex
|
||
) {
|
||
failures.push(
|
||
'dry-run cutover 必须先预置 Pingora shadow env,再执行 rollback apply。',
|
||
);
|
||
}
|
||
if (
|
||
healthPatrolIndex >= 0 &&
|
||
rollbackApplyIndex >= 0 &&
|
||
healthPatrolIndex > rollbackApplyIndex
|
||
) {
|
||
failures.push(
|
||
'dry-run cutover 必须先预置 health patrol env,再执行 rollback apply。',
|
||
);
|
||
}
|
||
assertStepBefore(
|
||
plan,
|
||
'回退前 Pingora env 预置回 shadow',
|
||
'回退前 Pingora env shadow 命令证据 manifest 只读验真',
|
||
'dry-run cutover 必须先生成 Pingora shadow env 命令证据,再验真该命令证据。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'回退前 Pingora env shadow 命令证据 manifest 只读验真',
|
||
'回退前 health patrol 预置回 Nginx',
|
||
'dry-run cutover 必须先验真 Pingora shadow env 命令证据,再预置 health patrol nginx env。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'回退前 health patrol 预置回 Nginx',
|
||
'回退前 health patrol nginx 命令证据 manifest 只读验真',
|
||
'dry-run cutover 必须先生成 health patrol nginx env 命令证据,再验真该命令证据。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'回退前 health patrol nginx 命令证据 manifest 只读验真',
|
||
'Pingora direct rollback apply',
|
||
'dry-run cutover 必须先验真 health patrol nginx env 命令证据,再执行 rollback apply。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'Pingora direct rollback apply',
|
||
'回退命令证据 manifest 只读验真',
|
||
'dry-run cutover 必须先生成 rollback apply 命令证据,再验真该命令证据。',
|
||
);
|
||
assertStepBefore(
|
||
plan,
|
||
'回退命令证据 manifest 只读验真',
|
||
'回退后 health patrol env Nginx 模式复核',
|
||
'dry-run cutover 必须先验真 rollback apply 命令证据,再执行回退后 env 复核。',
|
||
);
|
||
|
||
const healthPatrolNginxCheck = findStep(
|
||
plan,
|
||
'回退后 health patrol env Nginx 模式复核',
|
||
);
|
||
if (!healthPatrolNginxCheck) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退后 health patrol env Nginx 模式复核。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'/opt/genarrative/current/scripts/check-production-health-patrol-env.mjs',
|
||
'dry-run cutover 回退后 health patrol env 复核必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'--expected-gateway-mode',
|
||
'dry-run cutover 回退后 health patrol env 复核必须显式传 gateway mode。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'nginx',
|
||
'dry-run cutover 回退后 health patrol env 复核必须要求 nginx。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'--expected-public-base-url',
|
||
'dry-run cutover 回退后 health patrol env 复核必须校验 Nginx public base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'http://127.0.0.1',
|
||
'dry-run cutover 回退后 health patrol env 复核必须使用显式回退 public base URL。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'--require-empty-public-host',
|
||
'dry-run cutover 回退后 health patrol env 复核必须要求清空 public Host。',
|
||
);
|
||
}
|
||
|
||
const postRollbackSnapshot = findStep(plan, '回退后状态快照证据包');
|
||
const postRollbackVerify = findStep(plan, '回退后证据 manifest 只读验真');
|
||
const evidenceRootAudit = findStep(plan, '切换证据根目录三阶段总审计');
|
||
if (!postRollbackSnapshot) {
|
||
failures.push('dry-run cutover 必须包含回退后状态快照证据包。');
|
||
} else {
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-bundle.mjs',
|
||
'回退后状态快照证据包必须使用 current release 随包证据包脚本。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'--snapshot-script',
|
||
'回退后状态快照证据包必须显式透传 snapshot 脚本。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'--output-root',
|
||
'回退后状态快照证据包必须显式传证据输出根目录。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-status-snapshot.mjs',
|
||
'回退后状态快照证据包必须使用 current release 随包状态快照脚本。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'nginx',
|
||
'回退后状态快照证据包必须确认 nginx 巡检口径。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'--expected-pingora-env-mode',
|
||
'回退后状态快照证据包必须显式校验 active Pingora env 姿态。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'shadow',
|
||
'回退后状态快照证据包必须要求 active Pingora env 已恢复 shadow 姿态。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'--require-empty-public-host',
|
||
'默认回退后状态快照证据包必须要求 public Host 为空。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'--run-health-patrol',
|
||
'回退后状态快照证据包必须执行生产健康巡检。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackSnapshot.args,
|
||
'--require-pingora-gateway',
|
||
'回退后状态快照证据包必须收录 Pingora 发布物 checksum / manifest 自审。',
|
||
);
|
||
}
|
||
if (!postRollbackVerify) {
|
||
failures.push('dry-run cutover 必须包含回退后证据 manifest 只读验真。');
|
||
} else {
|
||
assertIncludes(
|
||
postRollbackVerify.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'回退后证据验真必须使用 current release 随包 verifier。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackVerify.args,
|
||
'<post-rollback-bundle-dir>',
|
||
'回退后证据验真必须提示替换刚生成的 post-rollback bundleDir。',
|
||
);
|
||
assertIncludes(
|
||
postRollbackVerify.args,
|
||
'--require-summary-ok',
|
||
'回退后证据验真必须要求 manifest.summary.status=OK。',
|
||
);
|
||
}
|
||
if (!evidenceRootAudit) {
|
||
failures.push('dry-run cutover 必须包含切换证据根目录三阶段总审计。');
|
||
} else {
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-audit.mjs',
|
||
'三阶段总审计必须使用 current release 随包 evidence audit 脚本。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--evidence-root',
|
||
'三阶段总审计必须显式传证据根目录。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'/var/log/genarrative/pingora-cutover-evidence',
|
||
'三阶段总审计必须使用固定证据根目录。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--verify-script',
|
||
'三阶段总审计必须显式固定 verifier 脚本。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs',
|
||
'三阶段总审计必须复用 current release 随包 verifier。',
|
||
);
|
||
for (const phase of ['pre-cutover', 'post-enable', 'post-rollback']) {
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
phase,
|
||
`三阶段总审计必须要求 ${phase} 证据。`,
|
||
);
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-phase-direct-live-access-log',
|
||
'三阶段总审计必须要求 post-enable 阶段带 direct live access log 对账摘要。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'post-enable',
|
||
'三阶段总审计必须把 direct live access log 摘要要求绑定到 post-enable 阶段。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-phase-direct-live-static-headers',
|
||
'三阶段总审计必须要求 post-enable 阶段带 direct live 静态响应头摘要。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'post-enable',
|
||
'三阶段总审计必须把 direct live 静态响应头摘要要求绑定到 post-enable 阶段。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-phase-pingora-env-shadow',
|
||
'三阶段总审计必须要求 post-rollback 阶段带 Pingora env shadow 摘要。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'post-rollback',
|
||
'三阶段总审计必须把 Pingora env shadow 摘要要求绑定到 post-rollback 阶段。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-command',
|
||
'三阶段总审计必须显式要求真实切换命令证据。',
|
||
);
|
||
for (const command of [
|
||
'enable-apply:pingora-direct-enable-apply',
|
||
'post-enable:pingora-health-patrol-direct-env-switch',
|
||
'rollback-prep:pingora-gateway-shadow-env-switch',
|
||
'rollback-prep:pingora-health-patrol-nginx-env-switch',
|
||
'rollback-apply:pingora-direct-rollback-apply',
|
||
]) {
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
command,
|
||
`三阶段总审计必须要求 ${command} 命令证据。`,
|
||
);
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-command-executable',
|
||
'三阶段总审计必须显式要求真实切换脚本身份。',
|
||
);
|
||
for (const commandExecutable of [
|
||
'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh',
|
||
'post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs',
|
||
'rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs',
|
||
'rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs',
|
||
'rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh',
|
||
]) {
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
commandExecutable,
|
||
`三阶段总审计必须要求 ${commandExecutable} 命令脚本身份。`,
|
||
);
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-command-arg',
|
||
'三阶段总审计必须显式要求真实切换命令必需参数。',
|
||
);
|
||
for (const commandArg of [
|
||
'enable-apply:pingora-direct-enable-apply:--apply',
|
||
'post-enable:pingora-health-patrol-direct-env-switch:--apply',
|
||
'post-enable:pingora-health-patrol-direct-env-switch:pingora-direct',
|
||
'rollback-prep:pingora-gateway-shadow-env-switch:--apply',
|
||
'rollback-prep:pingora-health-patrol-nginx-env-switch:--apply',
|
||
'rollback-prep:pingora-health-patrol-nginx-env-switch:nginx',
|
||
'rollback-apply:pingora-direct-rollback-apply:--apply',
|
||
]) {
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
commandArg,
|
||
`三阶段总审计必须要求 ${commandArg} 命令参数。`,
|
||
);
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--timeline-max-span-ms',
|
||
'三阶段总审计必须显式传标准切换时间线最大跨度。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'86400000',
|
||
'三阶段总审计默认必须把标准切换时间线最大跨度固定为 24 小时。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-cutover-run-id',
|
||
'三阶段总审计必须要求同一 cutoverRunId。',
|
||
);
|
||
}
|
||
|
||
}
|
||
|
||
function assertDryRunCutoverPlanCanOverrideEvidenceTimelineMaxSpan() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-evidence-timeline-max-span-ms',
|
||
'172800000',
|
||
));
|
||
const evidenceRootAudit = findStep(plan, '切换证据根目录三阶段总审计');
|
||
if (!evidenceRootAudit) {
|
||
failures.push('dry-run cutover 必须包含切换证据根目录三阶段总审计。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--timeline-max-span-ms',
|
||
'三阶段总审计必须传递自定义标准切换时间线最大跨度参数。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'172800000',
|
||
'三阶段总审计必须传递自定义标准切换时间线最大跨度值。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverPlanAvoidsManualEvidenceEscapeHatches() {
|
||
const plan = readPlan(cutoverPlanArgs());
|
||
const forbiddenArgs = [
|
||
'--warn-only',
|
||
'--allow-extra-files',
|
||
'--allow-extra-root-entries',
|
||
];
|
||
|
||
for (const step of plan) {
|
||
for (const forbiddenArg of forbiddenArgs) {
|
||
assertNotIncludes(
|
||
step.args || [],
|
||
forbiddenArg,
|
||
`正式 dry-run cutover 计划的 ${step.name} 不能携带人工排障放行参数 ${forbiddenArg}。`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
function assertTechnicalDocCutoverAuditExamplesRequireCommandExecutables() {
|
||
const docPath =
|
||
'docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md';
|
||
const content = readFileSync(docPath, 'utf8');
|
||
const executableArgs = [
|
||
'--require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh',
|
||
'--require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh',
|
||
];
|
||
const auditExamples = [
|
||
...content.matchAll(
|
||
/\/opt\/genarrative\/current\/scripts\/ops\/pingora-cutover-evidence-audit\.mjs[\s\S]*?(?=。|` 生成总审计 JSON|`,由脚本|`。)/gu,
|
||
),
|
||
]
|
||
.map((match) => match[0])
|
||
.filter(
|
||
(example) =>
|
||
example.includes(
|
||
'--require-command enable-apply:pingora-direct-enable-apply',
|
||
) ||
|
||
example.includes(
|
||
'--require-command rollback-apply:pingora-direct-rollback-apply',
|
||
),
|
||
);
|
||
|
||
if (auditExamples.length === 0) {
|
||
failures.push('Pingora 技术文档必须包含最终证据根目录总审计命令示例。');
|
||
return;
|
||
}
|
||
for (const example of auditExamples) {
|
||
for (const executableArg of executableArgs) {
|
||
assertIncludes(
|
||
example,
|
||
executableArg,
|
||
'Pingora 技术文档里的最终证据根目录总审计命令示例必须要求真实脚本身份。',
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
function assertDryRunCutoverPlanIncludesHostConsistencyConfirmation() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
));
|
||
const step = findStep(plan, '切换 Host 一致性确认');
|
||
if (!step) {
|
||
failures.push('dry-run cutover 必须包含 Host 一致性确认步骤。');
|
||
return;
|
||
}
|
||
if (
|
||
!step.args.some((arg) => String(arg).startsWith('cutover-run-id='))
|
||
) {
|
||
failures.push('dry-run cutover Host 一致性确认必须展示 cutoverRunId。');
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'direct-host=example.com',
|
||
'dry-run cutover Host 一致性确认必须展示 direct Host。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'direct-redirect-host=example.com',
|
||
'dry-run cutover Host 一致性确认必须展示 redirect Host。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'health-patrol-public-host=example.com',
|
||
'dry-run cutover Host 一致性确认必须展示 health patrol Host。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'rollback-nginx-smoke-host=example.com',
|
||
'dry-run cutover Host 一致性确认必须展示 rollback smoke Host。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'rollback-nginx-smoke-expect-body=<!doctype html>',
|
||
'dry-run cutover Host 一致性确认必须展示 rollback smoke body 证据片段。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'rollback-health-patrol-public-base-url=http://127.0.0.1',
|
||
'dry-run cutover Host 一致性确认必须展示回退 health patrol public base URL。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'rollback-health-patrol-public-host=',
|
||
'dry-run cutover Host 一致性确认必须展示回退 health patrol public Host 为空。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverPlanCarriesCutoverRunId() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-run-id',
|
||
'cutover-20260617T010000Z',
|
||
));
|
||
const hostConsistency = findStep(plan, '切换 Host 一致性确认');
|
||
if (!hostConsistency) {
|
||
failures.push('dry-run cutover 必须包含 Host 一致性确认步骤。');
|
||
} else {
|
||
assertIncludes(
|
||
hostConsistency.args,
|
||
'cutover-run-id=cutover-20260617T010000Z',
|
||
'Host 一致性确认必须展示显式 cutoverRunId。',
|
||
);
|
||
}
|
||
|
||
for (const stepName of [
|
||
'切换前状态快照证据包',
|
||
'Pingora direct enable apply',
|
||
'启用后状态快照证据包',
|
||
'Pingora direct rollback apply',
|
||
'回退后状态快照证据包',
|
||
]) {
|
||
const step = findStep(plan, stepName);
|
||
if (!step) {
|
||
failures.push(`dry-run cutover 必须包含 ${stepName}。`);
|
||
continue;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--cutover-run-id',
|
||
`${stepName} 必须传递 cutoverRunId。`,
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'cutover-20260617T010000Z',
|
||
`${stepName} 必须使用同一个 cutoverRunId。`,
|
||
);
|
||
}
|
||
|
||
const evidenceRootAudit = findStep(plan, '切换证据根目录三阶段总审计');
|
||
if (!evidenceRootAudit) {
|
||
failures.push('dry-run cutover 必须包含切换证据根目录三阶段总审计。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'--require-cutover-run-id',
|
||
'三阶段总审计必须要求同一 cutoverRunId。',
|
||
);
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
'cutover-20260617T010000Z',
|
||
'三阶段总审计必须使用同一个 cutoverRunId。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverPlanGeneratesDefaultCutoverRunId() {
|
||
const plan = readPlan(cutoverPlanArgs());
|
||
const hostConsistency = findStep(plan, '切换 Host 一致性确认');
|
||
if (!hostConsistency) {
|
||
failures.push('dry-run cutover 必须包含 Host 一致性确认步骤。');
|
||
return;
|
||
}
|
||
const runArg = hostConsistency.args.find((arg) =>
|
||
String(arg).startsWith('cutover-run-id='),
|
||
);
|
||
if (!runArg) {
|
||
failures.push('dry-run cutover 未显式提供 run id 时必须自动生成 cutoverRunId。');
|
||
return;
|
||
}
|
||
const runId = runArg.slice('cutover-run-id='.length);
|
||
if (!/^pingora-direct-\d{8}T\d{6}Z$/u.test(runId)) {
|
||
failures.push(`自动生成的 cutoverRunId 格式不正确: ${runId}`);
|
||
}
|
||
|
||
const evidenceRootAudit = findStep(plan, '切换证据根目录三阶段总审计');
|
||
if (!evidenceRootAudit) {
|
||
failures.push('dry-run cutover 必须包含切换证据根目录三阶段总审计。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
evidenceRootAudit.args,
|
||
runId,
|
||
'自动生成的 cutoverRunId 必须传给三阶段总审计。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverExplicitRollbackBodyIgnoresProcessEnv() {
|
||
const previous =
|
||
process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY;
|
||
process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY =
|
||
'polluted-body';
|
||
try {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
));
|
||
const rollbackApply = findStep(plan, 'Pingora direct rollback apply');
|
||
if (!rollbackApply) {
|
||
failures.push('dry-run cutover 必须包含 rollback apply。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
rollbackApply.args,
|
||
'<!doctype html>',
|
||
'dry-run cutover 必须使用显式 rollback body 断言,且不能被本机 env 污染。',
|
||
);
|
||
if (rollbackApply.args.includes('polluted-body')) {
|
||
failures.push(
|
||
'readinessPlanEnv 必须清理 GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY,避免 plan 自测受本机 env 污染。',
|
||
);
|
||
}
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY;
|
||
} else {
|
||
process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY =
|
||
previous;
|
||
}
|
||
}
|
||
}
|
||
|
||
function assertDryRunCutoverPostEnableReadinessRedactsDirectProbeToken() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--direct-probe-token',
|
||
'direct-secret-token',
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
));
|
||
const step = findStep(plan, '启用后 release readiness 直连复核');
|
||
if (!step) {
|
||
failures.push('带 direct probe token 的 cutover runbook 必须包含启用后 readiness 复核。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--direct-probe-token',
|
||
'启用后 release readiness 直连复核必须继续传递 direct probe token 参数。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'<redacted>',
|
||
'启用后 release readiness 直连复核必须隐藏 direct probe token 值。',
|
||
);
|
||
if (JSON.stringify(plan).includes('direct-secret-token')) {
|
||
failures.push('dry-run cutover JSON 不能泄露 direct probe token 原文。');
|
||
}
|
||
}
|
||
|
||
function assertDryRunCutoverAllowsSameHostnameWithDifferentPorts() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--direct-redirect-host',
|
||
'example.com:443',
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
'--rollback-nginx-smoke-host',
|
||
'example.com:80',
|
||
));
|
||
const step = findStep(plan, '切换 Host 一致性确认');
|
||
if (!step) {
|
||
failures.push('dry-run cutover 同 hostname 不同端口时仍必须能生成计划。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'direct-redirect-host=example.com:443',
|
||
'dry-run cutover 应允许 redirect Host 使用同 hostname 的显式端口。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'rollback-nginx-smoke-host=example.com:80',
|
||
'dry-run cutover 应允许 rollback smoke Host 使用同 hostname 的显式端口。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'rollback-health-patrol-public-base-url=http://127.0.0.1',
|
||
'dry-run cutover 同 hostname 不同端口时仍必须展示回退 health patrol public base URL。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverCanRestoreRollbackHealthPatrolPublicHost() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
'--rollback-health-patrol-public-host',
|
||
'nginx.example.com',
|
||
));
|
||
const healthPatrolNginx = findStep(plan, '回退前 health patrol 预置回 Nginx');
|
||
const hostConsistency = findStep(plan, '切换 Host 一致性确认');
|
||
if (!hostConsistency) {
|
||
failures.push('dry-run cutover 必须包含 Host 一致性确认步骤。');
|
||
} else {
|
||
assertIncludes(
|
||
hostConsistency.args,
|
||
'rollback-health-patrol-public-base-url=http://127.0.0.1',
|
||
'dry-run cutover Host 一致性确认必须展示回退 health patrol public base URL。',
|
||
);
|
||
assertIncludes(
|
||
hostConsistency.args,
|
||
'rollback-health-patrol-public-host=nginx.example.com',
|
||
'dry-run cutover Host 一致性确认必须展示显式回退 health patrol public Host。',
|
||
);
|
||
}
|
||
|
||
if (!healthPatrolNginx) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退前 health patrol Nginx 模式预置脚本。',
|
||
);
|
||
} else {
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'--public-host',
|
||
'dry-run cutover 回退前必须能恢复切换前 Nginx public Host。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginx.args,
|
||
'nginx.example.com',
|
||
'dry-run cutover 回退前必须使用显式回退 public Host。',
|
||
);
|
||
}
|
||
|
||
const healthPatrolNginxCheck = findStep(
|
||
plan,
|
||
'回退后 health patrol env Nginx 模式复核',
|
||
);
|
||
if (!healthPatrolNginxCheck) {
|
||
failures.push(
|
||
'dry-run cutover 必须包含回退后 health patrol env Nginx 模式复核。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'--expected-public-host',
|
||
'dry-run cutover 回退后 health patrol env 复核必须能校验 Nginx public Host。',
|
||
);
|
||
assertIncludes(
|
||
healthPatrolNginxCheck.args,
|
||
'nginx.example.com',
|
||
'dry-run cutover 回退后 health patrol env 复核必须使用显式回退 public Host。',
|
||
);
|
||
if (healthPatrolNginxCheck.args.includes('--require-empty-public-host')) {
|
||
failures.push(
|
||
'显式提供回退 public Host 时,回退后 health patrol env 复核不能同时要求 Host 为空。',
|
||
);
|
||
}
|
||
|
||
for (const stepName of [
|
||
'Pingora direct rollback dry-run',
|
||
'Pingora direct rollback apply',
|
||
]) {
|
||
const step = findStep(plan, stepName);
|
||
if (!step) {
|
||
failures.push(`dry-run cutover 必须包含 ${stepName}。`);
|
||
continue;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--health-patrol-expected-public-host',
|
||
`dry-run cutover ${stepName} 必须把显式回退 public Host 传给回退脚本。`,
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'nginx.example.com',
|
||
`dry-run cutover ${stepName} 必须使用显式回退 public Host。`,
|
||
);
|
||
if (step.args.includes('--health-patrol-require-empty-public-host')) {
|
||
failures.push(
|
||
`显式提供回退 public Host 时,${stepName} 不能同时要求 Host 为空。`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
function assertDryRunCutoverPlanCanIncludeRollbackShadowProbe() {
|
||
const plan = readPlan(cutoverPlanArgs(
|
||
'--cutover-release-root',
|
||
'/opt/genarrative/current',
|
||
'--rollback-pingora-shadow-probe-url',
|
||
'http://127.0.0.1:18081/__genarrative_pingora/healthz',
|
||
'--rollback-pingora-shadow-probe-token',
|
||
'probe-secret-token',
|
||
));
|
||
const rollbackDryRun = findStep(plan, 'Pingora direct rollback dry-run');
|
||
const rollbackApply = findStep(plan, 'Pingora direct rollback apply');
|
||
for (const step of [rollbackDryRun, rollbackApply]) {
|
||
if (!step) {
|
||
failures.push('带 shadow probe 的 dry-run cutover 必须包含 rollback 步骤。');
|
||
continue;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--pingora-shadow-probe-url',
|
||
'dry-run cutover rollback 步骤必须传递 shadow probe URL。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'http://127.0.0.1:18081/__genarrative_pingora/healthz',
|
||
'dry-run cutover rollback 步骤必须保留 shadow probe URL。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'--pingora-shadow-probe-token',
|
||
'dry-run cutover rollback 步骤必须传递 shadow probe token 参数。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'<redacted>',
|
||
'dry-run cutover rollback 步骤必须隐藏 shadow probe token 值。',
|
||
);
|
||
if (step.args.includes('probe-secret-token')) {
|
||
failures.push('dry-run cutover JSON 不能泄露 shadow probe token 原文。');
|
||
}
|
||
}
|
||
}
|
||
|
||
function assertRequireLiveForcesHost() {
|
||
const plan = readPlan([...requireLiveBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Nginx live canary smoke');
|
||
if (!step) {
|
||
failures.push('--require-live 计划必须包含 live canary smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--host',
|
||
'--require-live 必须把正式域名 Host 传给 live canary smoke。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'example.com',
|
||
'--require-live live canary smoke 必须使用显式传入的正式域名 Host。',
|
||
);
|
||
|
||
const parity = findStep(plan, '目标 Nginx live canary access log 对账');
|
||
if (!parity) {
|
||
failures.push('--require-live 计划必须在 live smoke 后包含 access log 对账。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
parity.args,
|
||
'scripts/check-pingora-canary-access-log-parity.mjs',
|
||
'--require-live 必须调用 canary access log 对账脚本。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'--nginx-log-file',
|
||
'--require-live access log 对账必须显式传 Nginx access log。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/var/log/nginx/genarrative.access.log',
|
||
'--require-live access log 对账必须使用显式 Nginx access log 路径。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'--pingora-log-file',
|
||
'--require-live access log 对账必须显式传 Pingora access log。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--require-live access log 对账必须使用显式 Pingora access log 路径。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'--since-lines',
|
||
'--require-live access log 对账必须显式传 tail 行数。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'3000',
|
||
'--require-live access log 对账必须使用显式 tail 行数。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/__genarrative_pingora_canary/healthz',
|
||
'--require-live access log 对账必须覆盖 canary healthz。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/__genarrative_pingora_canary/api/assets/history',
|
||
'--require-live access log 对账必须覆盖代表性 API canary 路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireRealpathLiveForcesHostAndRealpathParity() {
|
||
const plan = readPlan([...requireRealpathLiveBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Nginx realpath canary smoke');
|
||
if (!step) {
|
||
failures.push('--require-realpath-live 计划必须包含 realpath canary smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--realpath',
|
||
'--require-realpath-live 必须把 live smoke 切到真实路径 canary 模式。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'--host',
|
||
'--require-realpath-live 必须把正式域名 Host 传给 realpath live canary smoke。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'example.com',
|
||
'--require-realpath-live realpath smoke 必须使用显式传入的正式域名 Host。',
|
||
);
|
||
|
||
const parity = findStep(plan, '目标 Nginx realpath canary access log 对账');
|
||
if (!parity) {
|
||
failures.push(
|
||
'--require-realpath-live 计划必须在 realpath smoke 后包含 access log 对账。',
|
||
);
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
parity.args,
|
||
'--realpath',
|
||
'--require-realpath-live access log 对账必须使用真实路径模式。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/var/log/nginx/genarrative-pingora-realpath-canary.access.log',
|
||
'--require-realpath-live access log 对账必须使用独立 Nginx realpath canary access log。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/__genarrative_pingora_realpath_canary/healthz',
|
||
'--require-realpath-live access log 对账必须覆盖 realpath healthz。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/api/assets/history',
|
||
'--require-realpath-live access log 对账必须覆盖真实 API 路径。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/v1/identity',
|
||
'--require-realpath-live access log 对账必须覆盖真实 SpacetimeDB identity 路径。',
|
||
);
|
||
assertIncludes(
|
||
parity.args,
|
||
'/assets/app.js',
|
||
'--require-realpath-live access log 对账必须覆盖真实静态资源路径。',
|
||
);
|
||
}
|
||
|
||
function assertReleaseRuntimeOnlyPlanUsesCurrentReleaseScripts() {
|
||
const plan = readPlan([
|
||
...requireLiveBaseArgs,
|
||
...requireRealpathLiveBaseArgs,
|
||
...requireDirectBaseArgs,
|
||
'--release-runtime-only',
|
||
'--dry-run-plan',
|
||
]);
|
||
const sourceOnlyNames = new Set([
|
||
'Rust 路由与保护单测',
|
||
'Pingora mock 上游 smoke',
|
||
'Nginx/Pingora 路由矩阵 parity',
|
||
'Pingora production release 真实构建烟测',
|
||
'API deploy release 烟测',
|
||
]);
|
||
for (const name of sourceOnlyNames) {
|
||
if (findStep(plan, name)) {
|
||
failures.push(`release runtime-only 计划不能包含源码全量门禁: ${name}。`);
|
||
}
|
||
}
|
||
|
||
const audit = findStep(plan, 'current release 自包含自审');
|
||
if (!audit) {
|
||
failures.push('release runtime-only 计划必须先包含 current release 自包含自审。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
audit.args,
|
||
'scripts/ops/pingora-current-release-audit.mjs',
|
||
'release runtime-only 自审必须使用随当前脚本定位的包内自审脚本。',
|
||
);
|
||
assertIncludes(
|
||
audit.args,
|
||
'--release-root',
|
||
'release runtime-only 自审必须显式传 current release 根目录。',
|
||
);
|
||
assertIncludes(
|
||
audit.args,
|
||
'--require-pingora-gateway',
|
||
'release runtime-only direct 复核必须要求 Pingora 发布物存在。',
|
||
);
|
||
assertIncludes(
|
||
audit.args,
|
||
'--systemd-show',
|
||
'release runtime-only direct 复核必须核验 systemd ExecStart。',
|
||
);
|
||
}
|
||
|
||
const live = findStep(plan, '目标 Nginx live canary smoke');
|
||
if (!live) {
|
||
failures.push('release runtime-only 计划必须包含 live canary smoke。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
live.args,
|
||
'scripts/check-pingora-canary-live.mjs',
|
||
'release runtime-only live canary smoke 必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
}
|
||
|
||
const liveParity = findStep(plan, '目标 Nginx live canary access log 对账');
|
||
if (!liveParity) {
|
||
failures.push('release runtime-only 计划必须包含 live access log 对账。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
liveParity.args,
|
||
'scripts/check-pingora-canary-access-log-parity.mjs',
|
||
'release runtime-only live access log 对账必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
}
|
||
|
||
const realpathLive = findStep(plan, '目标 Nginx realpath canary smoke');
|
||
if (!realpathLive) {
|
||
failures.push('release runtime-only 计划必须包含 realpath live canary smoke。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
realpathLive.args,
|
||
'scripts/check-pingora-canary-live.mjs',
|
||
'release runtime-only realpath live canary smoke 必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
assertIncludes(
|
||
realpathLive.args,
|
||
'--realpath',
|
||
'release runtime-only realpath live canary smoke 必须传 --realpath。',
|
||
);
|
||
}
|
||
|
||
const realpathParity = findStep(
|
||
plan,
|
||
'目标 Nginx realpath canary access log 对账',
|
||
);
|
||
if (!realpathParity) {
|
||
failures.push('release runtime-only 计划必须包含 realpath live access log 对账。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
realpathParity.args,
|
||
'scripts/check-pingora-canary-access-log-parity.mjs',
|
||
'release runtime-only realpath access log 对账必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
assertIncludes(
|
||
realpathParity.args,
|
||
'--realpath',
|
||
'release runtime-only realpath access log 对账必须传 --realpath。',
|
||
);
|
||
}
|
||
|
||
const preflight = findStep(plan, '目标 Pingora direct entry preflight');
|
||
if (!preflight) {
|
||
failures.push('release runtime-only 计划必须包含 direct preflight。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
preflight.args,
|
||
'scripts/check-pingora-direct-preflight.mjs',
|
||
'release runtime-only direct preflight 必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
}
|
||
|
||
const healthPatrol = findStep(plan, '目标 health patrol env 直连模式复核');
|
||
if (!healthPatrol) {
|
||
failures.push('release runtime-only 计划必须包含 health patrol env 复核。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
healthPatrol.args,
|
||
'scripts/check-production-health-patrol-env.mjs',
|
||
'release runtime-only health patrol env 复核必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
}
|
||
|
||
const direct = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!direct) {
|
||
failures.push('release runtime-only 计划必须包含 direct live smoke。');
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
direct.args,
|
||
'scripts/check-pingora-direct-live.mjs',
|
||
'release runtime-only direct live smoke 必须使用随当前脚本定位的包内脚本。',
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertReleaseRuntimeOnlyPlanIncludesDirectRehearsalStatus() {
|
||
const basePlan = readPlan(['--release-runtime-only', '--dry-run-plan']);
|
||
const baseStep = findStep(
|
||
basePlan,
|
||
'目标 Pingora direct rehearsal 状态复核',
|
||
);
|
||
if (!baseStep) {
|
||
failures.push(
|
||
'release runtime-only 基础计划必须包含直连彩排状态复核。',
|
||
);
|
||
return;
|
||
}
|
||
assertAbsoluteScriptArg(
|
||
baseStep.args,
|
||
'scripts/ops/pingora-direct-rehearsal-status.mjs',
|
||
'release runtime-only 直连彩排状态复核必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'--release-root',
|
||
'release runtime-only 直连彩排状态复核必须显式传 current release 根目录。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'--expect-public-gateway',
|
||
'release runtime-only 直连彩排状态复核必须声明公网入口期望。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'nginx',
|
||
'release runtime-only 直连彩排状态复核必须确认 Nginx 仍接公网。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'--require-pingora-shadow',
|
||
'release runtime-only 直连彩排状态复核必须要求 Pingora shadow 高端口在线。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'--require-realpath-canary',
|
||
'release runtime-only 直连彩排状态复核必须要求 realpath canary 高端口在线。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'--require-current-release-gateway',
|
||
'release runtime-only 直连彩排状态复核必须复用 current release Pingora 自审。',
|
||
);
|
||
assertIncludes(
|
||
baseStep.args,
|
||
'--fail-on-critical',
|
||
'release runtime-only 直连彩排状态复核出现 CRITICAL 必须阻断门禁。',
|
||
);
|
||
|
||
const directPlan = readPlan([
|
||
...requireDirectBaseArgs,
|
||
'--release-runtime-only',
|
||
'--dry-run-plan',
|
||
]);
|
||
if (findStep(directPlan, '目标 Pingora direct rehearsal 状态复核')) {
|
||
failures.push(
|
||
'release runtime-only --require-direct 阶段不能继续要求 Nginx 接公网的彩排状态。',
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertReleaseRuntimeOnlyAllowsRealpathOnlyCanary() {
|
||
const plan = readPlan([
|
||
...requireRealpathLiveBaseArgs,
|
||
'--release-runtime-only',
|
||
'--dry-run-plan',
|
||
]);
|
||
const prefixLive = findStep(plan, '目标 Nginx live canary smoke');
|
||
if (prefixLive) {
|
||
failures.push(
|
||
'release runtime-only 只要求 realpath canary 时不能强制执行前缀 canary smoke。',
|
||
);
|
||
}
|
||
const prefixParity = findStep(plan, '目标 Nginx live canary access log 对账');
|
||
if (prefixParity) {
|
||
failures.push(
|
||
'release runtime-only 只要求 realpath canary 时不能强制执行前缀 canary access log 对账。',
|
||
);
|
||
}
|
||
|
||
const realpathLive = findStep(plan, '目标 Nginx realpath canary smoke');
|
||
if (!realpathLive) {
|
||
failures.push(
|
||
'release runtime-only 只要求 realpath canary 时必须包含 realpath smoke。',
|
||
);
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
realpathLive.args,
|
||
'scripts/check-pingora-canary-live.mjs',
|
||
'release runtime-only realpath-only smoke 必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
realpathLive.args,
|
||
'--realpath',
|
||
'release runtime-only realpath-only smoke 必须传 --realpath。',
|
||
);
|
||
}
|
||
|
||
const realpathParity = findStep(
|
||
plan,
|
||
'目标 Nginx realpath canary access log 对账',
|
||
);
|
||
if (!realpathParity) {
|
||
failures.push(
|
||
'release runtime-only 只要求 realpath canary 时必须包含 realpath access log 对账。',
|
||
);
|
||
} else {
|
||
assertAbsoluteScriptArg(
|
||
realpathParity.args,
|
||
'scripts/check-pingora-canary-access-log-parity.mjs',
|
||
'release runtime-only realpath-only 对账必须使用 current release 随包脚本。',
|
||
);
|
||
assertIncludes(
|
||
realpathParity.args,
|
||
'--realpath',
|
||
'release runtime-only realpath-only 对账必须传 --realpath。',
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertReleaseRuntimeOnlyKeepsInvokedCurrentSymlinkPath() {
|
||
const tmpRoot = mkdtempSync(
|
||
path.join(tmpdir(), 'genarrative-release-readiness-current-'),
|
||
);
|
||
try {
|
||
const releaseDir = path.join(tmpRoot, 'releases', 'test-release');
|
||
const currentLink = path.join(tmpRoot, 'current');
|
||
const scriptDir = path.join(releaseDir, 'scripts');
|
||
mkdirSync(scriptDir, { recursive: true });
|
||
symlinkSync(releaseDir, currentLink, 'dir');
|
||
const invokedScript = path.join(
|
||
currentLink,
|
||
'scripts',
|
||
'check-pingora-release-readiness.mjs',
|
||
);
|
||
const realScript = path.join(
|
||
scriptDir,
|
||
'check-pingora-release-readiness.mjs',
|
||
);
|
||
symlinkSync(
|
||
path.join(process.cwd(), 'scripts/check-pingora-release-readiness.mjs'),
|
||
realScript,
|
||
);
|
||
|
||
const result = spawnSync(
|
||
'node',
|
||
[invokedScript, '--release-runtime-only', '--dry-run-plan'],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) !== 0) {
|
||
failures.push(
|
||
`通过 current symlink 读取 runtime-only plan 失败,退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||
);
|
||
return;
|
||
}
|
||
const plan = JSON.parse(result.stdout);
|
||
const audit = findStep(plan, 'current release 自包含自审');
|
||
const rehearsal = findStep(plan, '目标 Pingora direct rehearsal 状态复核');
|
||
for (const [label, step] of [
|
||
['current release 自审', audit],
|
||
['直连彩排状态复核', rehearsal],
|
||
]) {
|
||
if (!step) {
|
||
failures.push(
|
||
`通过 current symlink 生成的 runtime-only plan 缺少${label}步骤。`,
|
||
);
|
||
continue;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
currentLink,
|
||
`${label}必须保留调用方 current symlink 作为 release root,而不是展开到真实 release 目录。`,
|
||
);
|
||
if (step.args.includes(releaseDir)) {
|
||
failures.push(
|
||
`${label}不应把 current symlink 展开成真实 release 目录: ${releaseDir}`,
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
failures.push(`current symlink runtime-only plan 自测异常: ${error.message}`);
|
||
} finally {
|
||
rmSync(tmpRoot, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
function assertReleaseRuntimeOnlyRejectsSourceOnlyFlags() {
|
||
for (const flag of ['--require-docker', '--pull-docker', '--require-nginx']) {
|
||
const result = runReadinessExpectFailure([
|
||
'--release-runtime-only',
|
||
flag,
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--release-runtime-only 只执行 current release 包内运行时复核',
|
||
`release runtime-only 不能同时接受源码或本机依赖参数 ${flag}。`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertRequireDirectForcesWssUpgrade() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--require-wss-upgrade',
|
||
'--require-direct 必须自动强制 WSS subscribe 返回 101。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesHttpBaseUrl() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--http-base-url',
|
||
'--require-direct 必须把 HTTP redirect / ACME 入口纳入 direct live smoke。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesHostSni() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--host',
|
||
'--require-direct 必须把正式域名 Host/SNI 传给 direct live smoke。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'example.com',
|
||
'--require-direct direct live smoke 必须使用显式传入的正式域名 Host/SNI。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesRedirectHost() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--redirect-host',
|
||
'--require-direct 必须把显式 redirect Location host 传给 direct live smoke。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'example.com',
|
||
'--require-direct direct live smoke 必须使用显式传入的 redirect Location host。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesPingoraAccessLog() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--pingora-access-log',
|
||
'--require-direct 必须把 Pingora access log 落盘校验传给 direct live smoke。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--require-direct direct live smoke 必须使用显式 Pingora access log 路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesPreflightSystemdCertReadable() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct entry preflight');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct entry preflight。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--systemd-cat',
|
||
'--require-direct 必须检查 systemd drop-in 最终生效配置。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'--check-cert-readable',
|
||
'--require-direct 必须检查当前用户可读 TLS 证书和私钥。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'--check-service-env-file',
|
||
'--require-direct 必须检查 service EnvironmentFile 包含本次 env。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'--check-service-user-cert-readable',
|
||
'--require-direct 必须检查 systemd 服务用户可读 TLS 证书和私钥。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'--check-service-binary-executable',
|
||
'--require-direct 必须检查 Pingora service ExecStart 二进制可执行。',
|
||
);
|
||
assertNotIncludes(
|
||
step.args,
|
||
'--check-ports-free',
|
||
'--require-direct 启用后复核不能继续要求 TLS/HTTP redirect 端口空闲;此时端口应由 Pingora 占用。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverForcesPortsFreePreflight() {
|
||
const plan = readPlan(cutoverPlanArgs());
|
||
const step = findStep(plan, '切换前 current release 直连 env / 证书 / 端口预检');
|
||
if (!step) {
|
||
failures.push('dry-run cutover 计划必须包含切换前 direct entry preflight。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--check-ports-free',
|
||
'dry-run cutover 启用前 preflight 必须检查 TLS/HTTP redirect 端口已释放。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesExplicitSpacetimeDatabase() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--spacetime-database',
|
||
'--require-direct 必须把显式 SpacetimeDB 数据库名传给 direct live smoke。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'genarrative-prod',
|
||
'--require-direct direct live smoke 必须使用显式传入的 SpacetimeDB 数据库名。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectForcesHealthPatrolEnvCheck() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 health patrol env 直连模式复核');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 health patrol env 复核。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'scripts/check-production-health-patrol-env.mjs',
|
||
'--require-direct 必须复核 health patrol env。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'/etc/genarrative/health-patrol.env',
|
||
'--require-direct health patrol env 复核必须使用显式 env 文件。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'pingora-direct',
|
||
'--require-direct health patrol env 复核必须要求 pingora-direct。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'https://127.0.0.1',
|
||
'--require-direct health patrol env 复核必须校验 direct HTTPS base URL。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'example.com',
|
||
'--require-direct health patrol env 复核必须校验正式 Host。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunPlanRedactsDirectProbeToken() {
|
||
const plan = readPlan([
|
||
...requireDirectBaseArgs,
|
||
'--direct-probe-token',
|
||
'direct-secret-token',
|
||
'--dry-run-plan',
|
||
]);
|
||
const step = findStep(plan, '目标 Pingora direct live smoke');
|
||
if (!step) {
|
||
failures.push('带 direct probe token 的计划必须包含 direct live smoke。');
|
||
return;
|
||
}
|
||
assertIncludes(
|
||
step.args,
|
||
'--probe-token',
|
||
'dry-run plan direct live smoke 必须传递 probe token 参数。',
|
||
);
|
||
assertIncludes(
|
||
step.args,
|
||
'<redacted>',
|
||
'dry-run plan direct live smoke 必须隐藏 probe token 值。',
|
||
);
|
||
if (step.args.includes('direct-secret-token')) {
|
||
failures.push('dry-run plan JSON 不能泄露 direct probe token 原文。');
|
||
}
|
||
}
|
||
|
||
function assertRunStepLogRedactsDirectProbeToken() {
|
||
const output = readFileSync(
|
||
'scripts/check-pingora-release-readiness.mjs',
|
||
'utf8',
|
||
);
|
||
assertIncludes(
|
||
output,
|
||
'redactSecretArgs(args).join',
|
||
'真实执行日志里的命令展示必须使用脱敏参数。',
|
||
);
|
||
}
|
||
|
||
function assertRequireLiveRejectsMissingHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-live',
|
||
'--live-base-url',
|
||
'http://127.0.0.1',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-live 时必须提供 --live-host 或 GENARRATIVE_PINGORA_CANARY_HOST',
|
||
'--require-live 缺少 live Host 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireLiveRejectsInvalidHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-live',
|
||
'--live-base-url',
|
||
'http://127.0.0.1',
|
||
'--live-host',
|
||
'https://example.com',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--live-host 只能是 host 或 host:port',
|
||
'--require-live 的 live Host 不能接受 URL 或路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireLiveRejectsRelativeAccessLog() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-live',
|
||
'--live-base-url',
|
||
'http://127.0.0.1',
|
||
'--live-host',
|
||
'example.com',
|
||
'--live-nginx-access-log',
|
||
'nginx.access.log',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--live-nginx-access-log 必须是绝对路径',
|
||
'--require-live 的 Nginx access log 不能接受相对路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireLiveRejectsFilesystemRootAccessLog() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-live',
|
||
'--live-base-url',
|
||
'http://127.0.0.1',
|
||
'--live-host',
|
||
'example.com',
|
||
'--live-nginx-access-log',
|
||
'/',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--live-nginx-access-log 不能是文件系统根目录',
|
||
'--require-live 的 Nginx access log 不能指向文件系统根目录。',
|
||
);
|
||
}
|
||
|
||
function assertRequireLiveRejectsInvalidAccessLogSinceLines() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-live',
|
||
'--live-base-url',
|
||
'http://127.0.0.1',
|
||
'--live-host',
|
||
'example.com',
|
||
'--live-access-log-since-lines',
|
||
'0',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--live-access-log-since-lines 必须是正整数',
|
||
'--require-live 的 access log tail 行数必须是正整数。',
|
||
);
|
||
}
|
||
|
||
function assertRequireRealpathLiveRejectsMissingHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-realpath-live',
|
||
'--realpath-live-base-url',
|
||
'http://127.0.0.1:18083',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-realpath-live 时必须提供 --realpath-live-host 或 GENARRATIVE_PINGORA_REALPATH_CANARY_HOST',
|
||
'--require-realpath-live 缺少 realpath live Host 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireRealpathLiveRejectsRelativeAccessLog() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-realpath-live',
|
||
'--realpath-live-base-url',
|
||
'http://127.0.0.1:18083',
|
||
'--realpath-live-host',
|
||
'example.com',
|
||
'--realpath-live-nginx-access-log',
|
||
'nginx.realpath.access.log',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--realpath-live-nginx-access-log 必须是绝对路径',
|
||
'--require-realpath-live 的 Nginx access log 不能接受相对路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingHttpBaseUrl() {
|
||
const result = spawnSync(
|
||
'node',
|
||
[
|
||
'scripts/check-pingora-release-readiness.mjs',
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push('--require-direct 缺少 direct HTTP base URL 时必须失败。');
|
||
return;
|
||
}
|
||
const output = `${result.stdout}\n${result.stderr}`;
|
||
if (
|
||
!output.includes(
|
||
'启用 --require-direct 时必须提供 --direct-http-base-url 或 GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL',
|
||
)
|
||
) {
|
||
failures.push(
|
||
'--require-direct 缺少 direct HTTP base URL 时必须给出明确错误。',
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-host 或 GENARRATIVE_PINGORA_DIRECT_HOST',
|
||
'--require-direct 缺少 direct Host/SNI 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsInvalidHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com/path',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-host 只能是 host 或 host:port',
|
||
'--require-direct 的 direct Host/SNI 不能接受 URL 或路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingRedirectHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-redirect-host 或 GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST',
|
||
'--require-direct 缺少 direct redirect Location host 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsInvalidRedirectHost() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'https://example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-redirect-host 只能是 host 或 host:port',
|
||
'--require-direct 的 redirect Location host 不能接受 URL 或路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingPingoraAccessLog() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-pingora-access-log 或 GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG',
|
||
'--require-direct 缺少 direct Pingora access log 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsFilesystemRootPingoraAccessLog() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--direct-health-patrol-env-file',
|
||
'/etc/genarrative/health-patrol.env',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-pingora-access-log 不能是文件系统根目录',
|
||
'--require-direct 的 Pingora access log 不能指向文件系统根目录。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingSystemdPreflight() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-preflight-systemd 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT=true',
|
||
'--require-direct 缺少 systemd preflight 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingCertReadablePreflight() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-preflight-check-cert-readable 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE=true',
|
||
'--require-direct 缺少证书可读 preflight 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingServiceEnvFilePreflight() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-preflight-check-service-env-file 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE=true',
|
||
'--require-direct 缺少 service EnvironmentFile preflight 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingServiceUserCertReadablePreflight() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-preflight-check-service-user-cert-readable 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE=true',
|
||
'--require-direct 缺少服务用户证书可读 preflight 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingServiceBinaryExecutablePreflight() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-preflight-check-service-binary-executable 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE=true',
|
||
'--require-direct 缺少 service 二进制可执行 preflight 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectAllowsPostEnablePortsOwnedByPingora() {
|
||
const plan = readPlan([...requireDirectBaseArgs, '--dry-run-plan']);
|
||
const step = findStep(plan, '目标 Pingora direct entry preflight');
|
||
if (!step) {
|
||
failures.push('--require-direct 计划必须包含 direct entry preflight。');
|
||
return;
|
||
}
|
||
assertNotIncludes(
|
||
step.args,
|
||
'--check-ports-free',
|
||
'--require-direct post-enable 复核不能要求 80/443 空闲。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsMissingPortsFreePreflight() {
|
||
const result = runReadinessExpectFailure([
|
||
...requireDirectBaseArgs,
|
||
'--dry-run-cutover',
|
||
'--rollback-nginx-smoke-url',
|
||
'https://example.com/',
|
||
'--rollback-nginx-smoke-expect-body',
|
||
'<!doctype html>',
|
||
'--rollback-health-patrol-public-base-url',
|
||
'http://127.0.0.1',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --dry-run-cutover 时必须提供 --direct-preflight-check-ports-free',
|
||
'dry-run cutover 缺少端口释放 preflight 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingSpacetimeDatabase() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-spacetime-database 或 GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE',
|
||
'--require-direct 缺少 SpacetimeDB 数据库名时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsFilesystemRootPreflightEnvFile() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--direct-health-patrol-env-file',
|
||
'/etc/genarrative/health-patrol.env',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-preflight-env-file 不能是文件系统根目录',
|
||
'--require-direct 的 Pingora preflight env 文件不能指向文件系统根目录。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsMissingHealthPatrolEnvFile() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时必须提供 --direct-health-patrol-env-file 或 GENARRATIVE_PINGORA_DIRECT_HEALTH_PATROL_ENV_FILE',
|
||
'--require-direct 缺少 health patrol env 文件时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsRelativeHealthPatrolEnvFile() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--direct-health-patrol-env-file',
|
||
'health-patrol.env',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-health-patrol-env-file 必须是绝对路径',
|
||
'--require-direct 的 health patrol env 文件不能接受相对路径。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsFilesystemRootHealthPatrolEnvFile() {
|
||
const result = runReadinessExpectFailure([
|
||
'--require-direct',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
'--direct-health-patrol-env-file',
|
||
'/',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-health-patrol-env-file 不能是文件系统根目录',
|
||
'--require-direct 的 health patrol env 文件不能指向文件系统根目录。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsSkipWss() {
|
||
const result = runReadinessExpectFailure([
|
||
...requireDirectBaseArgs,
|
||
'--direct-skip-wss',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时不能使用 --direct-skip-wss 或 GENARRATIVE_PINGORA_DIRECT_SKIP_WSS=true',
|
||
'--require-direct 不能允许跳过 WSS subscribe。',
|
||
);
|
||
}
|
||
|
||
function assertRequireDirectRejectsInsecureTls() {
|
||
const result = runReadinessExpectFailure([
|
||
...requireDirectBaseArgs,
|
||
'--direct-insecure-tls',
|
||
'--dry-run-plan',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --require-direct 时不能使用 --direct-insecure-tls 或 GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS=true',
|
||
'--require-direct 不能允许 insecure TLS。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsMissingRequireDirect() {
|
||
const result = runReadinessExpectFailure([
|
||
'--dry-run-cutover',
|
||
'--direct-https-base-url',
|
||
'https://127.0.0.1',
|
||
'--direct-http-base-url',
|
||
'http://127.0.0.1',
|
||
'--direct-host',
|
||
'example.com',
|
||
'--direct-redirect-host',
|
||
'example.com',
|
||
'--direct-pingora-access-log',
|
||
'/var/log/genarrative/pingora-gateway.access.log',
|
||
'--direct-preflight-env-file',
|
||
'/tmp/pingora-gateway.env',
|
||
'--direct-preflight-systemd',
|
||
'--direct-preflight-check-cert-readable',
|
||
'--direct-preflight-check-service-env-file',
|
||
'--direct-preflight-check-service-user-cert-readable',
|
||
'--direct-preflight-check-service-binary-executable',
|
||
'--direct-preflight-check-ports-free',
|
||
'--direct-spacetime-database',
|
||
'genarrative-prod',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --dry-run-cutover 时必须同时提供 --require-direct',
|
||
'dry-run cutover 缺少 --require-direct 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsRelativeEvidenceOutputRoot() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--cutover-evidence-output-root',
|
||
'relative-evidence',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--cutover-evidence-output-root 必须是绝对路径',
|
||
'dry-run cutover 证据输出根目录使用相对路径时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsFilesystemRootReleaseRoot() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--cutover-release-root',
|
||
'/',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--cutover-release-root 不能是文件系统根目录',
|
||
'dry-run cutover current release 根目录使用 / 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsFilesystemRootEvidenceOutputRoot() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--cutover-evidence-output-root',
|
||
'/',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--cutover-evidence-output-root 不能是文件系统根目录',
|
||
'dry-run cutover 证据输出根目录使用 / 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertUsageCutoverExampleIncludesExplicitEvidenceOutputRoot() {
|
||
const usage = readUsage();
|
||
assertIncludes(
|
||
usage,
|
||
'--cutover-evidence-output-root /var/log/genarrative/pingora-cutover-evidence',
|
||
'只生成直连切换 runbook 的 usage 示例必须显式传证据输出根目录。',
|
||
);
|
||
}
|
||
|
||
function assertUsageExamplesIncludeDirectPingoraAccessLog() {
|
||
const usage = readUsage();
|
||
assertIncludes(
|
||
usage,
|
||
'Pingora 直连入口切换窗口追加:',
|
||
'usage 必须包含正式直连入口示例。',
|
||
);
|
||
assertIncludes(
|
||
usage,
|
||
'只生成直连切换 runbook:',
|
||
'usage 必须包含只生成直连切换 runbook 示例。',
|
||
);
|
||
const directExample = usage.slice(
|
||
usage.indexOf('Pingora 直连入口切换窗口追加:'),
|
||
usage.indexOf('只生成直连切换 runbook:'),
|
||
);
|
||
const cutoverExample = usage.slice(
|
||
usage.indexOf('只生成直连切换 runbook:'),
|
||
);
|
||
assertIncludes(
|
||
directExample,
|
||
'--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log',
|
||
'正式直连入口 usage 示例必须显式传 Pingora access log 路径。',
|
||
);
|
||
assertIncludes(
|
||
cutoverExample,
|
||
'--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log',
|
||
'只生成直连切换 runbook 的 usage 示例必须显式传 Pingora access log 路径。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsMissingRollbackHealthPatrolPublicBaseUrl() {
|
||
const result = runReadinessExpectFailure([
|
||
...requireDirectBaseArgs,
|
||
'--dry-run-cutover',
|
||
'--direct-preflight-check-ports-free',
|
||
'--rollback-nginx-smoke-url',
|
||
'https://example.com/',
|
||
'--rollback-nginx-smoke-expect-body',
|
||
'<!doctype html>',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --dry-run-cutover 时必须提供 --rollback-health-patrol-public-base-url',
|
||
'dry-run cutover 缺少回退后 health patrol public base URL 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsMissingRollbackSmokeBody() {
|
||
const result = runReadinessExpectFailure([
|
||
...requireDirectBaseArgs,
|
||
'--dry-run-cutover',
|
||
'--direct-preflight-check-ports-free',
|
||
'--rollback-nginx-smoke-url',
|
||
'https://example.com/',
|
||
'--rollback-health-patrol-public-base-url',
|
||
'http://127.0.0.1',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'启用 --dry-run-cutover 时必须提供 --rollback-nginx-smoke-expect-body',
|
||
'dry-run cutover 缺少回退 Nginx smoke body 证据片段时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsInvalidRollbackHealthPatrolPublicBaseUrl() {
|
||
const result = runReadinessExpectFailure([
|
||
...requireDirectBaseArgs,
|
||
'--dry-run-cutover',
|
||
'--direct-preflight-check-ports-free',
|
||
'--rollback-nginx-smoke-url',
|
||
'https://example.com/',
|
||
'--rollback-nginx-smoke-expect-body',
|
||
'<!doctype html>',
|
||
'--rollback-health-patrol-public-base-url',
|
||
'ftp://127.0.0.1',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--rollback-health-patrol-public-base-url 必须是 http(s) URL',
|
||
'dry-run cutover 回退 health patrol public base URL 非 http(s) 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsInvalidRollbackHealthPatrolPublicHost() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--rollback-health-patrol-public-host',
|
||
'https://example.com',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--rollback-health-patrol-public-host 只能是 host 或 host:port',
|
||
'dry-run cutover 回退 health patrol public Host 非 host 格式时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsRedirectHostMismatch() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(
|
||
'--direct-redirect-host',
|
||
'redirect.example.com',
|
||
),
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--direct-redirect-host 必须与 --direct-host 使用同一正式 hostname',
|
||
'dry-run cutover redirect Host 与 direct Host 不一致时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsRollbackHostMismatch() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--rollback-nginx-smoke-host',
|
||
'nginx.example.com',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--rollback-nginx-smoke-host 必须与 --direct-host 使用同一正式 hostname',
|
||
'dry-run cutover rollback smoke Host 与 direct Host 不一致时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsIncompleteRollbackShadowProbe() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--rollback-pingora-shadow-probe-url',
|
||
'http://127.0.0.1:18081/__genarrative_pingora/healthz',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--rollback-pingora-shadow-probe-url 必须同时提供 --rollback-pingora-shadow-probe-token',
|
||
'dry-run cutover rollback shadow probe URL 缺少 token 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsInvalidRollbackShadowProbeUrl() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--rollback-pingora-shadow-probe-url',
|
||
'ftp://127.0.0.1/probe',
|
||
'--rollback-pingora-shadow-probe-token',
|
||
'probe-secret-token',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--rollback-pingora-shadow-probe-url 必须是 http(s) URL',
|
||
'dry-run cutover rollback shadow probe URL 非 http(s) 时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsInvalidEvidenceTimelineMaxSpan() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(),
|
||
'--cutover-evidence-timeline-max-span-ms',
|
||
'0',
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--cutover-evidence-timeline-max-span-ms 必须是正整数',
|
||
'dry-run cutover 标准切换时间线最大跨度非正整数时必须失败。',
|
||
);
|
||
}
|
||
|
||
function assertDryRunCutoverRejectsInvalidCutoverRunId() {
|
||
const result = runReadinessExpectFailure([
|
||
...cutoverPlanArgs(
|
||
'--cutover-run-id',
|
||
'cutover 1',
|
||
),
|
||
]);
|
||
assertFailureIncludes(
|
||
result,
|
||
'--cutover-run-id 只能包含 ASCII 字母、数字、点、下划线或短横线',
|
||
'dry-run cutover 必须拒绝不安全 cutoverRunId。',
|
||
);
|
||
}
|
||
|
||
function runReadinessExpectFailure(args) {
|
||
const result = spawnSync(
|
||
'node',
|
||
['scripts/check-pingora-release-readiness.mjs', ...args],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push('release readiness 缺少硬门禁参数时必须失败。');
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function assertLiveSmokeScriptsRejectInvalidTimeout() {
|
||
const cases = [
|
||
{
|
||
script: 'scripts/check-pingora-canary-live.mjs',
|
||
args: ['--base-url', 'http://127.0.0.1', '--timeout-ms', '0'],
|
||
expected: '--timeout-ms 必须是正整数',
|
||
reason: 'canary live smoke 必须拒绝非正数 --timeout-ms。',
|
||
},
|
||
{
|
||
script: 'scripts/check-pingora-canary-live.mjs',
|
||
args: ['--base-url', 'http://127.0.0.1'],
|
||
env: {
|
||
GENARRATIVE_PINGORA_CANARY_TIMEOUT_MS: 'abc',
|
||
},
|
||
expected: 'GENARRATIVE_PINGORA_CANARY_TIMEOUT_MS 必须是正整数',
|
||
reason: 'canary live smoke 必须拒绝非法 env timeout。',
|
||
},
|
||
{
|
||
script: 'scripts/check-pingora-direct-live.mjs',
|
||
args: ['--https-base-url', 'https://127.0.0.1', '--timeout-ms', '0'],
|
||
expected: '--timeout-ms 必须是正整数',
|
||
reason: 'direct live smoke 必须拒绝非正数 --timeout-ms。',
|
||
},
|
||
{
|
||
script: 'scripts/check-pingora-direct-live.mjs',
|
||
args: ['--https-base-url', 'https://127.0.0.1'],
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_TIMEOUT_MS: 'abc',
|
||
},
|
||
expected: 'GENARRATIVE_PINGORA_DIRECT_TIMEOUT_MS 必须是正整数',
|
||
reason: 'direct live smoke 必须拒绝非法 env timeout。',
|
||
},
|
||
];
|
||
|
||
for (const testCase of cases) {
|
||
const result = spawnSync('node', [testCase.script, ...testCase.args], {
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: {
|
||
...readinessPlanEnv(),
|
||
...(testCase.env || {}),
|
||
},
|
||
});
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push(testCase.reason);
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
testCase.expected,
|
||
`${testCase.reason} 必须给出明确错误。`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertDirectLiveRejectsInvalidBoolEnv() {
|
||
const cases = [
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE: 'ture',
|
||
},
|
||
expected: 'GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE 必须是布尔值',
|
||
reason: 'direct live smoke 必须拒绝拼写错误的 require WSS env。',
|
||
},
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_SKIP_WSS: 'maybe',
|
||
},
|
||
expected: 'GENARRATIVE_PINGORA_DIRECT_SKIP_WSS 必须是布尔值',
|
||
reason: 'direct live smoke 必须拒绝非法 skip WSS env。',
|
||
},
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS: 'enabled',
|
||
},
|
||
expected: 'GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS 必须是布尔值',
|
||
reason: 'direct live smoke 必须拒绝非法 insecure TLS env。',
|
||
},
|
||
];
|
||
|
||
for (const testCase of cases) {
|
||
const result = spawnSync(
|
||
'node',
|
||
['scripts/check-pingora-direct-live.mjs', '--https-base-url', 'https://127.0.0.1'],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: {
|
||
...readinessPlanEnv(),
|
||
...testCase.env,
|
||
},
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push(testCase.reason);
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
testCase.expected,
|
||
`${testCase.reason} 必须给出明确错误。`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertDirectLiveRejectsFilesystemRootAccessLog() {
|
||
const result = spawnSync(
|
||
'node',
|
||
[
|
||
'scripts/check-pingora-direct-live.mjs',
|
||
'--https-base-url',
|
||
'https://127.0.0.1',
|
||
'--pingora-access-log',
|
||
'/',
|
||
],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push('direct live smoke 必须拒绝 Pingora access log 指向文件系统根目录。');
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
'--pingora-access-log 不能是文件系统根目录',
|
||
'direct live smoke 的 Pingora access log 不能指向文件系统根目录。',
|
||
);
|
||
}
|
||
|
||
function assertReleaseReadinessRejectsInvalidDirectBoolEnv() {
|
||
const cases = [
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT: 'ture',
|
||
},
|
||
expected:
|
||
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT 必须是布尔值',
|
||
reason: 'release readiness 必须拒绝拼写错误的 preflight systemd env。',
|
||
},
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE: 'definitely',
|
||
},
|
||
expected: 'GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE 必须是布尔值',
|
||
reason: 'release readiness 必须拒绝非法 require WSS env。',
|
||
},
|
||
];
|
||
|
||
for (const testCase of cases) {
|
||
const result = spawnSync(
|
||
'node',
|
||
['scripts/check-pingora-release-readiness.mjs', '--dry-run-plan'],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: {
|
||
...readinessPlanEnv(),
|
||
...testCase.env,
|
||
},
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push(testCase.reason);
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
testCase.expected,
|
||
`${testCase.reason} 必须给出明确错误。`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertDirectPreflightRejectsInvalidBoolEnv() {
|
||
const cases = [
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_REQUIRE_LIVE_ENV: 'ture',
|
||
},
|
||
expected:
|
||
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_REQUIRE_LIVE_ENV 必须是布尔值',
|
||
reason: 'direct preflight 必须拒绝拼写错误的 require live env。',
|
||
},
|
||
{
|
||
env: {
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE: 'maybe',
|
||
},
|
||
expected:
|
||
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE 必须是布尔值',
|
||
reason: 'direct preflight 必须拒绝非法 check ports free env。',
|
||
},
|
||
];
|
||
|
||
for (const testCase of cases) {
|
||
const result = spawnSync(
|
||
'node',
|
||
['scripts/check-pingora-direct-preflight.mjs'],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: {
|
||
...readinessPlanEnv(),
|
||
...testCase.env,
|
||
},
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push(testCase.reason);
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
testCase.expected,
|
||
`${testCase.reason} 必须给出明确错误。`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertDirectPreflightRejectsUnconfirmedMultiInstanceProtection() {
|
||
const root = mkdtempSync(path.join(tmpdir(), 'genarrative-direct-preflight-'));
|
||
try {
|
||
const envFile = path.join(root, 'pingora-gateway.env');
|
||
writeFileSync(
|
||
envFile,
|
||
[
|
||
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=0.0.0.0:18443',
|
||
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=0.0.0.0:18080',
|
||
'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=/tmp/cert.pem',
|
||
'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=/tmp/key.pem',
|
||
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
||
'GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true',
|
||
'GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT=2',
|
||
'GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=false',
|
||
'',
|
||
].join('\n'),
|
||
);
|
||
|
||
const result = spawnSync(
|
||
'node',
|
||
[
|
||
'scripts/check-pingora-direct-preflight.mjs',
|
||
'--env-file',
|
||
envFile,
|
||
'--require-live-env',
|
||
],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push(
|
||
'direct preflight 必须拒绝未确认共享保护的多实例 Pingora 配置。',
|
||
);
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
'GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1',
|
||
'direct preflight 多实例保护失败必须给出明确错误。',
|
||
);
|
||
} finally {
|
||
rmSync(root, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
function assertCutoverEvidenceScriptsRejectInvalidBoolEnv() {
|
||
const cases = [
|
||
{
|
||
script: 'scripts/ops/pingora-current-release-audit.mjs',
|
||
env: {
|
||
GENARRATIVE_PINGORA_CURRENT_RELEASE_REQUIRE_GATEWAY: 'ture',
|
||
},
|
||
expected:
|
||
'GENARRATIVE_PINGORA_CURRENT_RELEASE_REQUIRE_GATEWAY 必须是布尔值',
|
||
reason:
|
||
'current release 自审 smoke 必须覆盖非法 require gateway 布尔 env。',
|
||
},
|
||
{
|
||
script: 'scripts/ops/pingora-cutover-status-snapshot.mjs',
|
||
env: {
|
||
GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_RUN_HEALTH_PATROL: 'maybe',
|
||
},
|
||
expected:
|
||
'GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_RUN_HEALTH_PATROL 必须是布尔值',
|
||
reason:
|
||
'cutover 状态快照 smoke 必须覆盖非法 run health patrol 布尔 env。',
|
||
},
|
||
{
|
||
script: 'scripts/ops/pingora-cutover-evidence-bundle.mjs',
|
||
env: {
|
||
GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_FAIL_ON_CRITICAL: 'enabled',
|
||
},
|
||
expected:
|
||
'GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_FAIL_ON_CRITICAL 必须是布尔值',
|
||
reason:
|
||
'cutover 证据包 smoke 必须覆盖非法 fail on critical 布尔 env。',
|
||
},
|
||
];
|
||
|
||
for (const testCase of cases) {
|
||
const result = spawnSync('node', [testCase.script], {
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: {
|
||
...readinessPlanEnv(),
|
||
...testCase.env,
|
||
},
|
||
});
|
||
if ((result.status ?? 0) === 0) {
|
||
failures.push(
|
||
`${testCase.reason} 脚本本体必须失败。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||
);
|
||
}
|
||
assertFailureIncludes(
|
||
result,
|
||
testCase.expected,
|
||
`${testCase.reason} 必须能在输出中看到明确错误。`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertFailureIncludes(result, expected, reason) {
|
||
const output = `${result.stdout}\n${result.stderr}`;
|
||
if (!output.includes(expected)) {
|
||
failures.push(reason);
|
||
}
|
||
}
|
||
|
||
function readPlan(args) {
|
||
const result = spawnSync(
|
||
'node',
|
||
['scripts/check-pingora-release-readiness.mjs', ...args],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) !== 0) {
|
||
failures.push(
|
||
`读取 release readiness dry-run plan 失败,退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||
);
|
||
return [];
|
||
}
|
||
try {
|
||
return JSON.parse(result.stdout);
|
||
} catch (error) {
|
||
failures.push(
|
||
`release readiness dry-run plan 不是合法 JSON: ${error.message}`,
|
||
);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function readUsage() {
|
||
const result = spawnSync(
|
||
'node',
|
||
['scripts/check-pingora-release-readiness.mjs', '--help'],
|
||
{
|
||
cwd: process.cwd(),
|
||
encoding: 'utf8',
|
||
env: readinessPlanEnv(),
|
||
},
|
||
);
|
||
if ((result.status ?? 0) !== 0) {
|
||
failures.push(
|
||
`读取 release readiness usage 失败,退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||
);
|
||
return '';
|
||
}
|
||
return result.stdout;
|
||
}
|
||
|
||
function readinessPlanEnv() {
|
||
return {
|
||
...process.env,
|
||
GENARRATIVE_PINGORA_CANARY_HOST: '',
|
||
GENARRATIVE_PINGORA_CANARY_BASE_URL: '',
|
||
GENARRATIVE_PINGORA_REALPATH_CANARY_HOST: '',
|
||
GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL: '',
|
||
GENARRATIVE_PINGORA_REALPATH_CANARY_NGINX_ACCESS_LOG: '',
|
||
GENARRATIVE_PINGORA_REALPATH_CANARY_PINGORA_ACCESS_LOG: '',
|
||
GENARRATIVE_PINGORA_REALPATH_CANARY_ACCESS_LOG_SINCE_LINES: '',
|
||
GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL: '',
|
||
GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL: '',
|
||
GENARRATIVE_PINGORA_DIRECT_HOST: '',
|
||
GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_HEALTH_PATROL_ENV_FILE: '',
|
||
GENARRATIVE_HEALTH_PATROL_ENV_FILE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE: '',
|
||
GENARRATIVE_PINGORA_DIRECT_SKIP_WSS: '',
|
||
GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_URL: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_HOST: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_HEALTH_PATROL_PUBLIC_BASE_URL: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_HEALTH_PATROL_PUBLIC_HOST: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_SHADOW_PROBE_URL: '',
|
||
GENARRATIVE_PINGORA_ROLLBACK_SHADOW_PROBE_TOKEN: '',
|
||
GENARRATIVE_PINGORA_CUTOVER_RUN_ID: '',
|
||
};
|
||
}
|
||
|
||
function findStep(plan, name) {
|
||
return plan.find((step) => step.name === name);
|
||
}
|
||
|
||
function assertIncludes(values, expected, reason) {
|
||
if (!values.includes(expected)) {
|
||
failures.push(`${reason} 缺少: ${expected}`);
|
||
}
|
||
}
|
||
|
||
function assertAbsoluteScriptArg(values, expectedSuffix, reason) {
|
||
const normalizedSuffix = expectedSuffix.replace(/\\/g, '/');
|
||
const matched = values.some((value) => {
|
||
const text = String(value).replace(/\\/g, '/');
|
||
return text.startsWith('/') && text.endsWith(normalizedSuffix);
|
||
});
|
||
if (!matched) {
|
||
failures.push(`${reason} 缺少绝对脚本路径后缀: ${expectedSuffix}`);
|
||
}
|
||
}
|
||
|
||
function assertNotIncludes(values, forbidden, reason) {
|
||
if (values.includes(forbidden)) {
|
||
failures.push(reason);
|
||
}
|
||
}
|
||
|
||
function assertStepBefore(plan, firstName, secondName, reason) {
|
||
const firstIndex = plan.findIndex((step) => step.name === firstName);
|
||
const secondIndex = plan.findIndex((step) => step.name === secondName);
|
||
if (firstIndex < 0 || secondIndex < 0) {
|
||
return;
|
||
}
|
||
if (firstIndex >= secondIndex) {
|
||
failures.push(reason);
|
||
}
|
||
}
|