Files
Genarrative/scripts/check-pingora-release-readiness.mjs
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

2222 lines
80 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = process.cwd();
const scriptDir = path.dirname(resolveInvokedScriptPath());
const releaseRoot = path.resolve(scriptDir, '..');
const DEFAULT_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS = 24 * 60 * 60 * 1000;
const DEFAULT_CUTOVER_RUN_ID_PREFIX = 'pingora-direct-';
const SECRET_VALUE_FLAGS = new Set([
'--direct-probe-token',
'--probe-token',
'--pingora-shadow-probe-token',
'--rollback-pingora-shadow-probe-token',
]);
const config = parseArgs(process.argv.slice(2));
const failures = [];
if (config.dryRunPlan) {
printDryRunPlan(buildSteps(config));
process.exit(0);
}
if (config.dryRunCutover) {
printDryRunPlan(buildCutoverPlan(config));
process.exit(0);
}
for (const step of buildSteps(config)) {
const ok = await runStep(step);
if (!ok && !config.keepGoing) {
break;
}
}
if (failures.length > 0) {
console.error('\n[pingora-release-readiness] 未通过:');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('\n[pingora-release-readiness] 通过');
function resolveInvokedScriptPath() {
const invoked = process.argv[1];
if (invoked) {
return path.resolve(invoked);
}
return fileURLToPath(import.meta.url);
}
function parseArgs(argv) {
const result = {
requireDocker: false,
pullDocker: false,
requireNginx: false,
requireLive: false,
requireRealpathLive: false,
requireDirect: false,
liveBaseUrl: process.env.GENARRATIVE_PINGORA_CANARY_BASE_URL || '',
liveHost: process.env.GENARRATIVE_PINGORA_CANARY_HOST || '',
liveAccessLogNginx:
process.env.GENARRATIVE_PINGORA_CANARY_NGINX_ACCESS_LOG ||
'/var/log/nginx/genarrative.access.log',
liveAccessLogPingora:
process.env.GENARRATIVE_PINGORA_CANARY_PINGORA_ACCESS_LOG ||
'/var/log/genarrative/pingora-gateway.access.log',
liveAccessLogSinceLines:
process.env.GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES || '2000',
realpathLiveBaseUrl:
process.env.GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL || '',
realpathLiveHost:
process.env.GENARRATIVE_PINGORA_REALPATH_CANARY_HOST || '',
realpathLiveAccessLogNginx:
process.env.GENARRATIVE_PINGORA_REALPATH_CANARY_NGINX_ACCESS_LOG ||
'/var/log/nginx/genarrative-pingora-realpath-canary.access.log',
realpathLiveAccessLogPingora:
process.env.GENARRATIVE_PINGORA_REALPATH_CANARY_PINGORA_ACCESS_LOG ||
process.env.GENARRATIVE_PINGORA_CANARY_PINGORA_ACCESS_LOG ||
'/var/log/genarrative/pingora-gateway.access.log',
realpathLiveAccessLogSinceLines:
process.env.GENARRATIVE_PINGORA_REALPATH_CANARY_ACCESS_LOG_SINCE_LINES ||
process.env.GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES ||
'2000',
directHttpsBaseUrl:
process.env.GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL || '',
directHttpBaseUrl:
process.env.GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL || '',
directHost: process.env.GENARRATIVE_PINGORA_DIRECT_HOST || '',
directRedirectHost:
process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST || '',
directRedirectBaseUrl:
process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL || '',
directProbeToken: process.env.GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN || '',
directPingoraAccessLog:
process.env.GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG || '',
directAccessLogSinceLines:
process.env.GENARRATIVE_PINGORA_DIRECT_ACCESS_LOG_SINCE_LINES || '2000',
directPreflightEnvFile:
process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE || '',
directPreflightSystemd: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT',
),
directPreflightCheckCertReadable: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE',
),
directPreflightCheckServiceEnvFile: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE',
),
directPreflightCheckServiceUserCertReadable: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE',
),
directPreflightCheckServiceBinaryExecutable: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE',
),
directPreflightCheckPortsFree: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE',
),
directSpacetimeDatabase:
process.env.GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE || '',
directHealthPatrolEnvFile:
process.env.GENARRATIVE_PINGORA_DIRECT_HEALTH_PATROL_ENV_FILE ||
process.env.GENARRATIVE_HEALTH_PATROL_ENV_FILE ||
'',
directRequireWssUpgrade: readBoolEnv(
'GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE',
),
directSkipWss: readBoolEnv('GENARRATIVE_PINGORA_DIRECT_SKIP_WSS'),
directInsecureTls: readBoolEnv('GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS'),
cutoverReleaseRoot:
process.env.GENARRATIVE_PINGORA_CUTOVER_RELEASE_ROOT ||
'/opt/genarrative/current',
cutoverEvidenceOutputRoot:
process.env.GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_ROOT ||
'/var/log/genarrative/pingora-cutover-evidence',
cutoverEvidenceTimelineMaxSpanMs: parsePositiveIntEnv(
'GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS',
DEFAULT_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS,
),
cutoverRunId: process.env.GENARRATIVE_PINGORA_CUTOVER_RUN_ID || '',
rollbackNginxSmokeUrl:
process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_URL || '',
rollbackNginxSmokeHost:
process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_HOST || '',
rollbackNginxSmokeExpectBody:
process.env.GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY || '',
rollbackHealthPatrolPublicBaseUrl:
process.env.GENARRATIVE_PINGORA_ROLLBACK_HEALTH_PATROL_PUBLIC_BASE_URL ||
'',
rollbackHealthPatrolPublicHost:
process.env.GENARRATIVE_PINGORA_ROLLBACK_HEALTH_PATROL_PUBLIC_HOST || '',
rollbackPingoraShadowProbeUrl:
process.env.GENARRATIVE_PINGORA_ROLLBACK_SHADOW_PROBE_URL || '',
rollbackPingoraShadowProbeToken:
process.env.GENARRATIVE_PINGORA_ROLLBACK_SHADOW_PROBE_TOKEN || '',
dryRunPlan: false,
dryRunCutover: false,
releaseRuntimeOnly: readBoolEnv('GENARRATIVE_PINGORA_RELEASE_RUNTIME_ONLY'),
keepGoing: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case '-h':
case '--help':
printUsage();
process.exit(0);
break;
case '--require-docker':
result.requireDocker = true;
break;
case '--pull-docker':
result.pullDocker = true;
break;
case '--require-nginx':
result.requireNginx = true;
break;
case '--require-live':
result.requireLive = true;
break;
case '--require-realpath-live':
result.requireRealpathLive = true;
break;
case '--require-direct':
result.requireDirect = true;
break;
case '--live-base-url':
result.liveBaseUrl = requireValue(argv, ++index, arg);
break;
case '--live-host':
result.liveHost = requireValue(argv, ++index, arg);
break;
case '--live-nginx-access-log':
result.liveAccessLogNginx = requireValue(argv, ++index, arg);
break;
case '--live-pingora-access-log':
result.liveAccessLogPingora = requireValue(argv, ++index, arg);
break;
case '--live-access-log-since-lines':
result.liveAccessLogSinceLines = requireValue(argv, ++index, arg);
break;
case '--realpath-live-base-url':
result.realpathLiveBaseUrl = requireValue(argv, ++index, arg);
break;
case '--realpath-live-host':
result.realpathLiveHost = requireValue(argv, ++index, arg);
break;
case '--realpath-live-nginx-access-log':
result.realpathLiveAccessLogNginx = requireValue(argv, ++index, arg);
break;
case '--realpath-live-pingora-access-log':
result.realpathLiveAccessLogPingora = requireValue(argv, ++index, arg);
break;
case '--realpath-live-access-log-since-lines':
result.realpathLiveAccessLogSinceLines = requireValue(
argv,
++index,
arg,
);
break;
case '--direct-https-base-url':
result.directHttpsBaseUrl = requireValue(argv, ++index, arg);
break;
case '--direct-http-base-url':
result.directHttpBaseUrl = requireValue(argv, ++index, arg);
break;
case '--direct-host':
result.directHost = requireValue(argv, ++index, arg);
break;
case '--direct-redirect-host':
result.directRedirectHost = requireValue(argv, ++index, arg);
break;
case '--direct-redirect-base-url':
result.directRedirectBaseUrl = requireValue(argv, ++index, arg);
break;
case '--direct-probe-token':
result.directProbeToken = requireValue(argv, ++index, arg);
break;
case '--direct-pingora-access-log':
result.directPingoraAccessLog = requireValue(argv, ++index, arg);
break;
case '--direct-access-log-since-lines':
result.directAccessLogSinceLines = requireValue(argv, ++index, arg);
break;
case '--direct-preflight-env-file':
result.directPreflightEnvFile = requireValue(argv, ++index, arg);
break;
case '--direct-preflight-systemd':
result.directPreflightSystemd = true;
break;
case '--direct-preflight-check-cert-readable':
result.directPreflightCheckCertReadable = true;
break;
case '--direct-preflight-check-service-env-file':
result.directPreflightCheckServiceEnvFile = true;
break;
case '--direct-preflight-check-service-user-cert-readable':
result.directPreflightCheckServiceUserCertReadable = true;
break;
case '--direct-preflight-check-service-binary-executable':
result.directPreflightCheckServiceBinaryExecutable = true;
break;
case '--direct-preflight-check-ports-free':
result.directPreflightCheckPortsFree = true;
break;
case '--direct-spacetime-database':
result.directSpacetimeDatabase = requireValue(argv, ++index, arg);
break;
case '--direct-health-patrol-env-file':
result.directHealthPatrolEnvFile = requireValue(argv, ++index, arg);
break;
case '--direct-require-wss-upgrade':
result.directRequireWssUpgrade = true;
break;
case '--direct-skip-wss':
result.directSkipWss = true;
break;
case '--direct-insecure-tls':
result.directInsecureTls = true;
break;
case '--cutover-release-root':
result.cutoverReleaseRoot = requireValue(argv, ++index, arg);
break;
case '--cutover-evidence-output-root':
result.cutoverEvidenceOutputRoot = requireValue(argv, ++index, arg);
break;
case '--cutover-evidence-timeline-max-span-ms':
result.cutoverEvidenceTimelineMaxSpanMs = parsePositiveInt(
requireValue(argv, ++index, arg),
'--cutover-evidence-timeline-max-span-ms',
);
break;
case '--cutover-run-id':
result.cutoverRunId = requireValue(argv, ++index, arg);
break;
case '--rollback-nginx-smoke-url':
result.rollbackNginxSmokeUrl = requireValue(argv, ++index, arg);
break;
case '--rollback-nginx-smoke-host':
result.rollbackNginxSmokeHost = requireValue(argv, ++index, arg);
break;
case '--rollback-nginx-smoke-expect-body':
result.rollbackNginxSmokeExpectBody = requireValue(argv, ++index, arg);
break;
case '--rollback-health-patrol-public-base-url':
result.rollbackHealthPatrolPublicBaseUrl = requireValue(
argv,
++index,
arg,
);
break;
case '--rollback-health-patrol-public-host':
result.rollbackHealthPatrolPublicHost = requireValue(
argv,
++index,
arg,
);
break;
case '--rollback-pingora-shadow-probe-url':
result.rollbackPingoraShadowProbeUrl = requireValue(argv, ++index, arg);
break;
case '--rollback-pingora-shadow-probe-token':
result.rollbackPingoraShadowProbeToken = requireValue(
argv,
++index,
arg,
);
break;
case '--dry-run-plan':
result.dryRunPlan = true;
break;
case '--dry-run-cutover':
result.dryRunCutover = true;
break;
case '--release-runtime-only':
result.releaseRuntimeOnly = true;
break;
case '--keep-going':
result.keepGoing = true;
break;
default:
throw new Error(`未知参数: ${arg}`);
}
}
if (result.requireLive && !result.liveBaseUrl) {
throw new Error(
'启用 --require-live 时必须提供 --live-base-url 或 GENARRATIVE_PINGORA_CANARY_BASE_URL',
);
}
if (result.requireLive && !result.liveHost) {
throw new Error(
'启用 --require-live 时必须提供 --live-host 或 GENARRATIVE_PINGORA_CANARY_HOST',
);
}
if (result.liveHost) {
validateHostOption(result.liveHost, '--live-host');
}
if (result.requireLive) {
for (const [label, filePath] of [
['--live-nginx-access-log', result.liveAccessLogNginx],
['--live-pingora-access-log', result.liveAccessLogPingora],
]) {
validateSafeAbsoluteFilePath(filePath, label);
}
if (!isPositiveIntegerString(result.liveAccessLogSinceLines)) {
throw new Error('--live-access-log-since-lines 必须是正整数。');
}
}
if (result.requireRealpathLive && !result.realpathLiveBaseUrl) {
throw new Error(
'启用 --require-realpath-live 时必须提供 --realpath-live-base-url 或 GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL',
);
}
if (result.requireRealpathLive && !result.realpathLiveHost) {
throw new Error(
'启用 --require-realpath-live 时必须提供 --realpath-live-host 或 GENARRATIVE_PINGORA_REALPATH_CANARY_HOST',
);
}
if (result.realpathLiveHost) {
validateHostOption(result.realpathLiveHost, '--realpath-live-host');
}
if (result.requireRealpathLive || result.realpathLiveBaseUrl) {
for (const [label, filePath] of [
['--realpath-live-nginx-access-log', result.realpathLiveAccessLogNginx],
[
'--realpath-live-pingora-access-log',
result.realpathLiveAccessLogPingora,
],
]) {
validateSafeAbsoluteFilePath(filePath, label);
}
if (!isPositiveIntegerString(result.realpathLiveAccessLogSinceLines)) {
throw new Error('--realpath-live-access-log-since-lines 必须是正整数。');
}
}
if (result.requireDirect && !result.directHttpsBaseUrl) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-https-base-url 或 GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL',
);
}
if (result.requireDirect && !result.directHttpBaseUrl) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-http-base-url 或 GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL',
);
}
if (result.requireDirect && !result.directHost) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-host 或 GENARRATIVE_PINGORA_DIRECT_HOST',
);
}
if (result.directHost) {
validateHostOption(result.directHost, '--direct-host');
}
if (result.requireDirect && !result.directRedirectHost) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-redirect-host 或 GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST',
);
}
if (result.directRedirectHost) {
validateHostOption(result.directRedirectHost, '--direct-redirect-host');
}
if (result.directRedirectBaseUrl) {
validateHttpUrl(result.directRedirectBaseUrl, '--direct-redirect-base-url');
const parsed = new URL(result.directRedirectBaseUrl);
if (parsed.protocol !== 'https:') {
throw new Error('--direct-redirect-base-url 必须使用 https://。');
}
if (
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash ||
parsed.username ||
parsed.password
) {
throw new Error(
'--direct-redirect-base-url 只能是 HTTPS base URL,不能包含路径、查询、片段或认证信息。',
);
}
}
if (result.requireDirect && !result.directPingoraAccessLog) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-pingora-access-log 或 GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG',
);
}
if (result.directPingoraAccessLog) {
validateSafeAbsoluteFilePath(
result.directPingoraAccessLog,
'--direct-pingora-access-log',
);
}
if (
result.directPingoraAccessLog &&
!isPositiveIntegerString(result.directAccessLogSinceLines)
) {
throw new Error('--direct-access-log-since-lines 必须是正整数。');
}
if (result.requireDirect && !result.directPreflightEnvFile) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-preflight-env-file 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE',
);
}
if (result.directPreflightEnvFile) {
validateSafeAbsoluteFilePath(
result.directPreflightEnvFile,
'--direct-preflight-env-file',
);
}
if (result.requireDirect && !result.directPreflightSystemd) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-preflight-systemd 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT=true',
);
}
if (result.requireDirect && !result.directPreflightCheckCertReadable) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-preflight-check-cert-readable 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE=true',
);
}
if (result.requireDirect && !result.directPreflightCheckServiceEnvFile) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-preflight-check-service-env-file 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE=true',
);
}
if (
result.requireDirect &&
!result.directPreflightCheckServiceUserCertReadable
) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-preflight-check-service-user-cert-readable 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE=true',
);
}
if (
result.requireDirect &&
!result.directPreflightCheckServiceBinaryExecutable
) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-preflight-check-service-binary-executable 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE=true',
);
}
if (result.dryRunCutover && !result.directPreflightCheckPortsFree) {
throw new Error(
'启用 --dry-run-cutover 时必须提供 --direct-preflight-check-ports-free 或 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE=true,用于切换前证明 Nginx 和其它进程已释放 80/443。',
);
}
if (result.requireDirect && result.directSkipWss) {
throw new Error(
'启用 --require-direct 时不能使用 --direct-skip-wss 或 GENARRATIVE_PINGORA_DIRECT_SKIP_WSS=true',
);
}
if (result.requireDirect && !result.directSpacetimeDatabase) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-spacetime-database 或 GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE',
);
}
if (result.requireDirect && !result.directHealthPatrolEnvFile) {
throw new Error(
'启用 --require-direct 时必须提供 --direct-health-patrol-env-file 或 GENARRATIVE_PINGORA_DIRECT_HEALTH_PATROL_ENV_FILE',
);
}
if (result.directHealthPatrolEnvFile) {
validateSafeAbsoluteFilePath(
result.directHealthPatrolEnvFile,
'--direct-health-patrol-env-file',
);
}
if (result.requireDirect && result.directInsecureTls) {
throw new Error(
'启用 --require-direct 时不能使用 --direct-insecure-tls 或 GENARRATIVE_PINGORA_DIRECT_INSECURE_TLS=true',
);
}
if (result.requireDirect && !result.directSkipWss) {
result.directRequireWssUpgrade = true;
}
if (result.dryRunCutover && !result.requireDirect) {
throw new Error(
'启用 --dry-run-cutover 时必须同时提供 --require-direct,避免生成缺少直连硬门禁的切换计划。',
);
}
if (result.dryRunCutover && !path.isAbsolute(result.cutoverReleaseRoot)) {
throw new Error('--cutover-release-root 必须是绝对路径。');
}
if (result.dryRunCutover && isFilesystemRootPath(result.cutoverReleaseRoot)) {
throw new Error('--cutover-release-root 不能是文件系统根目录。');
}
if (
result.dryRunCutover &&
!path.isAbsolute(result.cutoverEvidenceOutputRoot)
) {
throw new Error('--cutover-evidence-output-root 必须是绝对路径。');
}
if (
result.dryRunCutover &&
isFilesystemRootPath(result.cutoverEvidenceOutputRoot)
) {
throw new Error('--cutover-evidence-output-root 不能是文件系统根目录。');
}
if (result.dryRunCutover && !result.rollbackNginxSmokeUrl) {
throw new Error(
'启用 --dry-run-cutover 时必须提供 --rollback-nginx-smoke-url 或 GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_URL。',
);
}
if (result.dryRunCutover && !result.rollbackNginxSmokeExpectBody) {
throw new Error(
'启用 --dry-run-cutover 时必须提供 --rollback-nginx-smoke-expect-body 或 GENARRATIVE_PINGORA_ROLLBACK_NGINX_SMOKE_EXPECT_BODY,且该片段必须来自切换前真实 Nginx 入口。',
);
}
if (result.dryRunCutover && !result.rollbackNginxSmokeHost) {
result.rollbackNginxSmokeHost = result.directHost || result.liveHost;
}
if (result.rollbackNginxSmokeHost) {
validateHostOption(
result.rollbackNginxSmokeHost,
'--rollback-nginx-smoke-host',
);
}
if (result.dryRunCutover && !result.rollbackHealthPatrolPublicBaseUrl) {
throw new Error(
'启用 --dry-run-cutover 时必须提供 --rollback-health-patrol-public-base-url 或 GENARRATIVE_PINGORA_ROLLBACK_HEALTH_PATROL_PUBLIC_BASE_URL,避免回退 runbook 覆盖现场原有 Nginx 巡检入口。',
);
}
if (result.rollbackHealthPatrolPublicBaseUrl) {
validateHttpUrl(
result.rollbackHealthPatrolPublicBaseUrl,
'--rollback-health-patrol-public-base-url',
);
}
if (result.rollbackHealthPatrolPublicHost) {
validateHostOption(
result.rollbackHealthPatrolPublicHost,
'--rollback-health-patrol-public-host',
);
}
if (
result.rollbackPingoraShadowProbeUrl &&
!result.rollbackPingoraShadowProbeToken
) {
throw new Error(
'--rollback-pingora-shadow-probe-url 必须同时提供 --rollback-pingora-shadow-probe-token。',
);
}
if (
result.rollbackPingoraShadowProbeToken &&
!result.rollbackPingoraShadowProbeUrl
) {
throw new Error(
'--rollback-pingora-shadow-probe-token 必须同时提供 --rollback-pingora-shadow-probe-url。',
);
}
if (result.rollbackPingoraShadowProbeUrl) {
validateHttpUrl(
result.rollbackPingoraShadowProbeUrl,
'--rollback-pingora-shadow-probe-url',
);
}
if (result.dryRunCutover) {
assertSameCutoverHostname(
result.directRedirectHost,
result.directHost,
'--direct-redirect-host',
'--direct-host',
);
assertSameCutoverHostname(
result.rollbackNginxSmokeHost,
result.directHost,
'--rollback-nginx-smoke-host',
'--direct-host',
);
if (result.liveHost) {
assertSameCutoverHostname(
result.liveHost,
result.directHost,
'--live-host',
'--direct-host',
);
}
}
if (result.dryRunCutover && !result.cutoverRunId) {
result.cutoverRunId = generateCutoverRunId();
}
if (result.cutoverRunId) {
validateSafeName(result.cutoverRunId, '--cutover-run-id');
}
if (
result.releaseRuntimeOnly &&
(result.requireDocker || result.pullDocker || result.requireNginx)
) {
throw new Error(
'--release-runtime-only 只执行 current release 包内运行时复核,不能同时使用 --require-docker / --pull-docker / --require-nginx;源码全量门禁请在构建环境运行默认模式。',
);
}
return result;
}
function printUsage() {
console.log(`Usage:
node scripts/check-pingora-release-readiness.mjs [options]
Options:
--require-docker Docker Nginx handoff smoke 必须执行通过,不允许跳过。
--pull-docker Docker 镜像缺失时允许拉取;通常和 --require-docker 一起用于 CI。
--require-nginx 本机 Nginx snippet 校验必须执行 nginx -t。
--require-live 目标 Nginx 已启用 canary 后,强制执行 live canary smoke。
--require-realpath-live
目标 Nginx 已启用真实路径 canary 后,强制执行 realpath live canary smoke。
--require-direct 目标 Pingora 已配置直连 HTTPS / HTTP redirect 后,强制执行 direct preflight 和 direct live smoke。
--live-base-url <url> live canary base URL;也可用 GENARRATIVE_PINGORA_CANARY_BASE_URL。
--live-host <host> live canary Host header;也可用 GENARRATIVE_PINGORA_CANARY_HOST。--require-live 时必须显式提供。
--live-nginx-access-log <path>
live canary 后 Nginx access log 对账路径,默认 /var/log/nginx/genarrative.access.log。
--live-pingora-access-log <path>
live canary 后 Pingora access log 对账路径,默认 /var/log/genarrative/pingora-gateway.access.log。
--live-access-log-since-lines <count>
live canary 后只读取日志尾部行数,默认 2000。
--realpath-live-base-url <url>
真实路径 canary base URL;也可用 GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL。
--realpath-live-host <host>
真实路径 canary Host header;也可用 GENARRATIVE_PINGORA_REALPATH_CANARY_HOST。--require-realpath-live 时必须显式提供。
--realpath-live-nginx-access-log <path>
真实路径 canary 独立 Nginx access log,默认 /var/log/nginx/genarrative-pingora-realpath-canary.access.log。
--realpath-live-pingora-access-log <path>
真实路径 canary 后 Pingora access log 对账路径,默认 /var/log/genarrative/pingora-gateway.access.log。
--realpath-live-access-log-since-lines <count>
真实路径 canary 后只读取日志尾部行数,默认 2000。
--direct-https-base-url <url> direct HTTPS base URL;也可用 GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL。
--direct-http-base-url <url> direct HTTP redirect base URL;也可用 GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL。
--direct-host <host> direct live Host header 和 TLS SNI;也可用 GENARRATIVE_PINGORA_DIRECT_HOST。--require-direct 时必须显式提供。
--direct-redirect-host <host> direct redirect Location host;也可用 GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST。--require-direct 时必须显式提供,cutover runbook 中必须和 --direct-host 使用同一 hostname。
--direct-redirect-base-url <url>
可选 direct redirect Location HTTPS base URL;用于高端口 rehearsal 访问 127.0.0.1:18443 但期望 Location 指向正式 HTTPS 入口。
--direct-probe-token <token> direct live shadow probe token;也可用 GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN。
--direct-pingora-access-log <path>
direct live 后 Pingora access log 落盘校验路径;--require-direct 时必须显式提供。
--direct-access-log-since-lines <count>
direct live 后只读取 Pingora access log 尾部行数,默认 2000。
--direct-preflight-env-file <path>
direct entry 预检使用的 pingora-gateway.env;也可用 GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE。
--direct-preflight-systemd direct entry 预检要求 systemctl cat 已包含低端口 capability drop-in。
--direct-preflight-check-cert-readable
direct entry 预检要求当前用户可读 cert/key。
--direct-preflight-check-service-env-file
direct entry 预检要求 service EnvironmentFile 包含本次 env 文件;--require-direct 时必须显式提供。
--direct-preflight-check-service-user-cert-readable
direct entry 预检要求 systemd 服务用户可读 cert/key--require-direct 时必须显式提供。
--direct-preflight-check-service-binary-executable
direct entry 预检要求 service ExecStart 指向的 pingora-gateway 已存在且可执行;--require-direct 时必须显式提供。
--direct-preflight-check-ports-free
direct entry 预检要求 TLS/HTTP redirect 端口当前可绑定;--dry-run-cutover / direct enable apply 前必须显式提供,启用后 --require-direct 复核不要求端口空闲。
--direct-spacetime-database <name>
direct live WSS subscribe 使用的数据库名;也可用 GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE。--require-direct 时必须显式提供。
--direct-health-patrol-env-file <path>
目标机 health-patrol env 文件;--require-direct 时必须显式提供,并复核 gateway mode / public base URL / Host。
--direct-require-wss-upgrade
要求 direct live WSS subscribe 必须返回 101--require-direct 时会自动启用。
--direct-skip-wss 跳过 direct live WSS subscribe 握手;只允许单独 direct live 临时排障,--require-direct 会拒绝。
--direct-insecure-tls direct live 允许自签证书;仅本机 smoke 使用。
--cutover-release-root <path>
生成切换 runbook 时使用的 current release 根目录,默认 /opt/genarrative/current。
--cutover-evidence-output-root <path>
生成切换 runbook 时传给证据包脚本的输出根目录,默认 /var/log/genarrative/pingora-cutover-evidence。
--cutover-evidence-timeline-max-span-ms <ms>
生成切换 runbook 时传给证据根目录总审计的标准五段时间线最大跨度,默认 ${DEFAULT_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS}ms。
--cutover-run-id <id> 生成切换 runbook 时写入所有证据 manifest 的本次切换批次 ID;不传则自动生成 ${DEFAULT_CUTOVER_RUN_ID_PREFIX}<UTC时间>。
--rollback-nginx-smoke-url <url>
生成切换 runbook 时传给回退脚本的 Nginx smoke URL;必须显式填写切换前真实 Nginx 入口。
--rollback-nginx-smoke-host <host>
生成切换 runbook 时传给回退脚本的 Host header;默认复用 --direct-host,显式传入时必须和 --direct-host 使用同一 hostname。
--rollback-nginx-smoke-expect-body <text>
生成切换 runbook 时传给回退脚本的 Nginx smoke 响应体预期片段;必须显式来自切换前真实响应,例如首页 <!doctype html>。
--rollback-health-patrol-public-base-url <url>
生成切换 runbook 时回退后恢复的 Nginx public base URL;应填切换前 health-patrol env 的原值。
--rollback-health-patrol-public-host <host>
生成切换 runbook 时回退后恢复的 public Host;不传则要求清空 Host 覆盖。
--rollback-pingora-shadow-probe-url <url>
生成切换 runbook 时传给回退脚本的可选 Pingora shadow 探针 URL;必须和 token 成对出现。
--rollback-pingora-shadow-probe-token <token>
生成切换 runbook 时传给回退脚本的可选 Pingora shadow 探针 tokenJSON 输出会隐藏 token 值。
--dry-run-plan 只打印将执行的检查计划,不启动检查命令。
--dry-run-cutover 只打印直连切换 runbook,不启动检查或修改系统。
--release-runtime-only
只执行 current release 包内运行时复核:current release 自审、live canary、access log 对账、direct preflight、health patrol env 和 direct live;不运行 npm/cargo 源码门禁。
--keep-going 单项失败后继续执行后续检查。
默认模式用于本机提交前检查;正式切换窗口建议:
node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名>
Pingora 直连入口切换窗口追加:
node scripts/check-pingora-release-readiness.mjs --require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/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
只生成直连切换 runbook
node scripts/check-pingora-release-readiness.mjs --dry-run-cutover --require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/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 --cutover-evidence-output-root /var/log/genarrative/pingora-cutover-evidence --rollback-nginx-smoke-url https://<域名>/ --rollback-nginx-smoke-expect-body '<!doctype html>' --rollback-health-patrol-public-base-url http://127.0.0.1
`);
}
function requireValue(argv, index, flag) {
const value = argv[index];
if (!value || value.startsWith('--')) {
throw new Error(`${flag} 缺少参数值`);
}
return value;
}
function readBoolEnv(name, fallback = false) {
const raw = process.env[name];
if (raw === undefined || raw === null || String(raw).trim() === '') {
return fallback;
}
const normalized = String(raw).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true;
}
if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false;
}
throw new Error(`${name} 必须是布尔值 true/false 或 1/0。`);
}
function parsePositiveIntEnv(name, fallback) {
const raw = process.env[name];
if (raw === undefined || raw === null || String(raw).trim() === '') {
return fallback;
}
return parsePositiveInt(raw, name);
}
function parsePositiveInt(value, label) {
const text = String(value || '').trim();
if (!/^[1-9]\d*$/u.test(text)) {
throw new Error(`${label} 必须是正整数。`);
}
const parsed = Number(text);
if (!Number.isSafeInteger(parsed)) {
throw new Error(`${label} 超出 JavaScript 安全整数范围。`);
}
return parsed;
}
function validateHostOption(value, flag) {
const raw = String(value);
if (raw !== raw.trim() || raw.includes('://') || /[\s/?#@]/.test(raw)) {
throw new Error(
`${flag} 只能是 host 或 host:port,不能包含 scheme、路径、查询、片段或空白字符`,
);
}
try {
const parsed = new URL(`https://${raw}`);
if (
!parsed.hostname ||
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash ||
parsed.username ||
parsed.password
) {
throw new Error('invalid host');
}
} catch {
throw new Error(`${flag} 不是合法的 host 或 host:port`);
}
}
function validateHttpUrl(value, flag) {
let parsed;
try {
parsed = new URL(value);
} catch {
throw new Error(`${flag} 必须是 http(s) URL: ${value}`);
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`${flag} 必须是 http(s) URL: ${value}`);
}
}
function isPositiveIntegerString(value) {
return /^[1-9]\d*$/u.test(String(value));
}
function validateSafeAbsoluteFilePath(value, flag) {
if (!path.isAbsolute(value)) {
throw new Error(`${flag} 必须是绝对路径。`);
}
if (isFilesystemRootPath(value)) {
throw new Error(`${flag} 不能是文件系统根目录。`);
}
}
function isFilesystemRootPath(value) {
const resolved = path.resolve(String(value));
return resolved === path.parse(resolved).root;
}
function hostnameForIdentity(value, flag) {
validateHostOption(value, flag);
const parsed = new URL(`https://${String(value).trim()}`);
return parsed.hostname.replace(/\.$/u, '').toLowerCase();
}
function assertSameCutoverHostname(actual, expected, actualFlag, expectedFlag) {
const actualHostname = hostnameForIdentity(actual, actualFlag);
const expectedHostname = hostnameForIdentity(expected, expectedFlag);
if (actualHostname !== expectedHostname) {
throw new Error(
`${actualFlag} 必须与 ${expectedFlag} 使用同一正式 hostname;当前 ${actualHostname} != ${expectedHostname}`,
);
}
}
function validateSafeName(value, flag) {
if (!/^[0-9A-Za-z._-]+$/u.test(String(value || ''))) {
throw new Error(`${flag} 只能包含 ASCII 字母、数字、点、下划线或短横线。`);
}
}
function generateCutoverRunId(now = new Date()) {
const timestamp = now
.toISOString()
.replace(/[-:.]/gu, '')
.replace(/\d{3}Z$/u, 'Z');
return `${DEFAULT_CUTOVER_RUN_ID_PREFIX}${timestamp}`;
}
function printDryRunPlan(steps) {
console.log(
JSON.stringify(
steps.map((step) => {
const output = {
name: step.name,
command: step.command,
args: redactSecretArgs(step.args),
};
if (step.phase) {
output.phase = step.phase;
}
if (step.when) {
output.when = step.when;
}
return output;
}),
null,
2,
),
);
}
function redactSecretArgs(args) {
return args.map((arg, index) =>
index > 0 && SECRET_VALUE_FLAGS.has(args[index - 1]) ? '<redacted>' : arg,
);
}
function buildCutoverPlan(config) {
const releaseRoot = config.cutoverReleaseRoot.replace(/\/+$/u, '');
const enableScript = path.join(
releaseRoot,
'scripts/deploy/pingora-direct-enable.sh',
);
const rollbackScript = path.join(
releaseRoot,
'scripts/deploy/pingora-direct-rollback.sh',
);
const gatewayEnvShadowSwitchScript = path.join(
releaseRoot,
'scripts/deploy/pingora-gateway-env-shadow-switch.mjs',
);
const healthPatrolEnvSwitchScript = path.join(
releaseRoot,
'scripts/deploy/pingora-health-patrol-env-switch.mjs',
);
const statusSnapshotScript = path.join(
releaseRoot,
'scripts/ops/pingora-cutover-status-snapshot.mjs',
);
const evidenceBundleScript = path.join(
releaseRoot,
'scripts/ops/pingora-cutover-evidence-bundle.mjs',
);
const commandEvidenceScript = path.join(
releaseRoot,
'scripts/ops/pingora-cutover-command-evidence.mjs',
);
const evidenceVerifyScript = path.join(
releaseRoot,
'scripts/ops/pingora-cutover-evidence-verify.mjs',
);
const evidenceAuditScript = path.join(
releaseRoot,
'scripts/ops/pingora-cutover-evidence-audit.mjs',
);
const currentReleaseAuditScript = path.join(
releaseRoot,
'scripts/ops/pingora-current-release-audit.mjs',
);
const releaseReadinessScript = path.join(
releaseRoot,
'scripts/check-pingora-release-readiness.mjs',
);
const releasePreflightScript = path.join(
releaseRoot,
'scripts/check-pingora-direct-preflight.mjs',
);
const releaseLiveScript = path.join(
releaseRoot,
'scripts/check-pingora-direct-live.mjs',
);
const preEnablePreflightArgs = [
releasePreflightScript,
'--env-file',
config.directPreflightEnvFile,
'--require-live-env',
...(config.directPreflightSystemd ? ['--systemd-cat'] : []),
'--check-cert-readable',
'--check-service-env-file',
'--check-service-user-cert-readable',
'--check-service-binary-executable',
'--check-ports-free',
];
const enableBaseArgs = [
'--current-release-audit-script',
currentReleaseAuditScript,
'--current-release-root',
releaseRoot,
'--preflight-script',
releasePreflightScript,
'--preflight-env-file',
config.directPreflightEnvFile,
'--preflight-check-cert-readable',
'--preflight-check-service-env-file',
'--preflight-check-service-user-cert-readable',
'--preflight-check-service-binary-executable',
'--preflight-check-ports-free',
'--direct-live-script',
releaseLiveScript,
'--direct-https-base-url',
config.directHttpsBaseUrl,
'--direct-http-base-url',
config.directHttpBaseUrl,
'--direct-host',
config.directHost,
'--direct-redirect-host',
config.directRedirectHost,
...(config.directRedirectBaseUrl
? ['--direct-redirect-base-url', config.directRedirectBaseUrl]
: []),
'--direct-spacetime-database',
config.directSpacetimeDatabase,
'--direct-pingora-access-log',
config.directPingoraAccessLog,
'--direct-access-log-since-lines',
config.directAccessLogSinceLines,
'--no-status',
];
if (config.directProbeToken) {
enableBaseArgs.push('--direct-probe-token', config.directProbeToken);
}
const rollbackBaseArgs = [
'--reload-nginx',
'--nginx-smoke-url',
config.rollbackNginxSmokeUrl,
'--nginx-smoke-expect-body',
config.rollbackNginxSmokeExpectBody,
'--health-patrol-env-file',
config.directHealthPatrolEnvFile,
'--health-patrol-expected-public-base-url',
config.rollbackHealthPatrolPublicBaseUrl,
];
if (config.rollbackNginxSmokeHost) {
rollbackBaseArgs.push('--nginx-smoke-host', config.rollbackNginxSmokeHost);
}
if (config.rollbackHealthPatrolPublicHost) {
rollbackBaseArgs.push(
'--health-patrol-expected-public-host',
config.rollbackHealthPatrolPublicHost,
);
} else {
rollbackBaseArgs.push('--health-patrol-require-empty-public-host');
}
if (config.rollbackPingoraShadowProbeUrl) {
rollbackBaseArgs.push(
'--pingora-shadow-probe-url',
config.rollbackPingoraShadowProbeUrl,
'--pingora-shadow-probe-token',
config.rollbackPingoraShadowProbeToken,
);
}
const healthPatrolDirectSwitchArgs = [
'--apply',
'--env-file',
config.directHealthPatrolEnvFile,
'--gateway-mode',
'pingora-direct',
'--public-base-url',
config.directHttpsBaseUrl,
'--public-host',
config.directHost,
];
const pingoraGatewayShadowSwitchArgs = [
'--apply',
'--env-file',
config.directPreflightEnvFile,
];
const healthPatrolNginxSwitchArgs = [
'--apply',
'--env-file',
config.directHealthPatrolEnvFile,
'--gateway-mode',
'nginx',
'--public-base-url',
config.rollbackHealthPatrolPublicBaseUrl,
...(config.rollbackHealthPatrolPublicHost
? ['--public-host', config.rollbackHealthPatrolPublicHost]
: ['--clear-public-host']),
];
return [
{
phase: 'preflight',
name: '切换 Host 一致性确认',
command: 'operator-action',
args: [
'confirm',
`cutover-run-id=${config.cutoverRunId}`,
`direct-host=${config.directHost}`,
`direct-redirect-host=${config.directRedirectHost}`,
`direct-redirect-base-url=${config.directRedirectBaseUrl || '-'}`,
`health-patrol-public-host=${config.directHost}`,
`rollback-nginx-smoke-host=${config.rollbackNginxSmokeHost}`,
`rollback-nginx-smoke-expect-body=${config.rollbackNginxSmokeExpectBody}`,
`rollback-health-patrol-public-base-url=${config.rollbackHealthPatrolPublicBaseUrl}`,
`rollback-health-patrol-public-host=${config.rollbackHealthPatrolPublicHost}`,
],
when: 'runbook 生成时已拒绝 hostname 漂移;切换窗口继续人工确认直连、回退 smoke 和回退巡检入口都指向预期目标。',
},
{
phase: 'preflight',
name: '切换前 current release 自包含自审',
command: 'node',
args: [
'--',
currentReleaseAuditScript,
'--release-root',
releaseRoot,
...(config.requireDirect ? ['--require-pingora-gateway'] : []),
'--systemd-show',
],
when: '正式启用前执行,只读确认 current release 已携带 Pingora 切换依赖;直连切换时要求 pingora-gateway 存在且 systemd ExecStart 指向 current release。',
},
{
phase: 'preflight',
name: '切换前状态快照证据包',
command: 'node',
args: [
'--',
evidenceBundleScript,
'--phase',
'pre-cutover',
'--cutover-run-id',
config.cutoverRunId,
'--release-root',
releaseRoot,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--snapshot-script',
statusSnapshotScript,
'--health-patrol-env-file',
config.directHealthPatrolEnvFile,
'--pingora-env-file',
config.directPreflightEnvFile,
'--expected-gateway-mode',
'nginx',
'--expected-public-base-url',
config.rollbackHealthPatrolPublicBaseUrl,
...(config.rollbackHealthPatrolPublicHost
? ['--expected-public-host', config.rollbackHealthPatrolPublicHost]
: ['--require-empty-public-host']),
...(config.requireDirect ? ['--require-pingora-gateway'] : []),
'--run-health-patrol',
'--fail-on-critical',
],
when: '正式启用前执行,只读采集 current release、health patrol env 和 systemd 最终配置,并把 JSON / stdout / stderr / manifest 归档为 Nginx 接流基线证据。',
},
{
phase: 'preflight',
name: '切换前证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<pre-cutover-bundle-dir>',
'--require-summary-ok',
],
when: '切换前状态快照证据包生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认 manifest.files 中的 sizeBytes / sha256 与实际归档文件一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'preflight',
name: '切换前 current release 直连 env / 证书 / 端口预检',
command: 'node',
args: ['--', ...preEnablePreflightArgs],
},
{
phase: 'preflight',
name: '切换前 release readiness 基础门禁',
command: 'node',
args: [
'--',
releaseReadinessScript,
...readinessArgs(config, { includeDirect: false }),
],
},
{
phase: 'enable-dry-run',
name: 'Pingora direct enable dry-run',
command: enableScript,
args: enableBaseArgs,
},
{
phase: 'enable-apply',
name: 'Pingora direct enable apply',
command: 'node',
args: [
'--',
commandEvidenceScript,
'--phase',
'enable-apply',
'--command-name',
'pingora-direct-enable-apply',
'--cutover-run-id',
config.cutoverRunId,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--expected-executable',
enableScript,
'--require-arg',
'--apply',
'--',
enableScript,
'--apply',
...enableBaseArgs,
],
when: '执行真实 direct enable apply,并把 stdout / stderr / 退出码和脱敏命令记录归档到切换证据目录。',
},
{
phase: 'enable-apply',
name: '启用命令证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<enable-apply-bundle-dir>',
'--require-summary-ok',
],
when: 'enable apply 命令证据生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认命令 stdout / stderr / command-record 与 manifest 元数据一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'post-enable',
name: '切换后 health patrol 切到 Pingora direct',
command: 'node',
args: [
'--',
commandEvidenceScript,
'--phase',
'post-enable',
'--command-name',
'pingora-health-patrol-direct-env-switch',
'--cutover-run-id',
config.cutoverRunId,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--expected-executable',
healthPatrolEnvSwitchScript,
'--require-arg',
'--apply',
'--require-arg',
'pingora-direct',
'--',
healthPatrolEnvSwitchScript,
...healthPatrolDirectSwitchArgs,
],
when: 'direct enable apply 成功后执行,并用命令证据归档 health patrol env 从 nginx 切到 pingora-direct 的真实 stdout / stderr / 退出码,避免巡检继续要求 nginx.service active。',
},
{
phase: 'post-enable',
name: '切换后 health patrol direct 命令证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<health-patrol-direct-env-switch-bundle-dir>',
'--require-summary-ok',
],
when: 'health patrol direct env switch 命令证据生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认 env 切换命令 stdout / stderr / command-record 与 manifest 元数据一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'post-enable',
name: '启用后 health patrol env 直连复核',
command: 'node',
args: [
'--',
path.join(
releaseRoot,
'scripts/check-production-health-patrol-env.mjs',
),
'--env-file',
config.directHealthPatrolEnvFile,
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
config.directHttpsBaseUrl,
'--expected-public-host',
config.directHost,
],
},
{
phase: 'post-enable',
name: '启用后状态快照证据包',
command: 'node',
args: [
'--',
evidenceBundleScript,
'--phase',
'post-enable',
'--cutover-run-id',
config.cutoverRunId,
'--release-root',
releaseRoot,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--snapshot-script',
statusSnapshotScript,
'--direct-live-script',
releaseLiveScript,
'--health-patrol-env-file',
config.directHealthPatrolEnvFile,
'--pingora-env-file',
config.directPreflightEnvFile,
'--expected-gateway-mode',
'pingora-direct',
'--expected-pingora-env-mode',
'direct',
'--expected-public-base-url',
config.directHttpsBaseUrl,
'--expected-public-host',
config.directHost,
...(config.requireDirect ? ['--require-pingora-gateway'] : []),
'--run-health-patrol',
'--run-direct-live',
'--direct-https-base-url',
config.directHttpsBaseUrl,
'--direct-http-base-url',
config.directHttpBaseUrl,
'--direct-host',
config.directHost,
'--direct-redirect-host',
config.directRedirectHost,
...(config.directRedirectBaseUrl
? ['--direct-redirect-base-url', config.directRedirectBaseUrl]
: []),
...(config.directProbeToken
? ['--direct-probe-token', config.directProbeToken]
: []),
'--direct-spacetime-database',
config.directSpacetimeDatabase,
'--direct-pingora-access-log',
config.directPingoraAccessLog,
'--direct-access-log-since-lines',
config.directAccessLogSinceLines,
'--fail-on-critical',
],
when: 'health patrol env 已切到 pingora-direct 且 env 复核通过后执行,归档 direct 接流状态、direct live 和 Pingora access log request_id 证据包。',
},
{
phase: 'post-enable',
name: '启用后证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<post-enable-bundle-dir>',
'--require-summary-ok',
],
when: '启用后状态快照证据包生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认 snapshot / direct live / stdout / stderr / 命令记录未在归档过程中漂移,且 manifest.summary.status 为 OK。',
},
{
phase: 'post-enable',
name: '启用后 release readiness 直连复核',
command: 'node',
args: [
'--',
releaseReadinessScript,
...readinessArgs(config, {
includeDirect: true,
includeDirectPortsFree: false,
}),
],
when: 'Pingora 已接管 80/443 后执行;此阶段不再检查端口空闲,因为端口应由 Pingora 直连入口占用。',
},
{
phase: 'rollback-dry-run',
name: 'Pingora direct rollback dry-run',
command: rollbackScript,
args: rollbackBaseArgs,
when: '启用失败、direct live smoke 失败或业务验证失败时先执行。',
},
{
phase: 'rollback-prep',
name: '回退前 Pingora env 预置回 shadow',
command: 'node',
args: [
'--',
commandEvidenceScript,
'--phase',
'rollback-prep',
'--command-name',
'pingora-gateway-shadow-env-switch',
'--cutover-run-id',
config.cutoverRunId,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--expected-executable',
gatewayEnvShadowSwitchScript,
'--require-arg',
'--apply',
'--',
gatewayEnvShadowSwitchScript,
...pingoraGatewayShadowSwitchArgs,
],
when: 'rollback apply 前执行,并用命令证据归档 Pingora gateway env 从 80/443 direct 配置恢复为 shadow 高端口配置;否则回退脚本移除低端口 capability 后重启 Pingora 可能仍按 80/443 配置启动失败。',
},
{
phase: 'rollback-prep',
name: '回退前 Pingora env shadow 命令证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<pingora-gateway-shadow-env-switch-bundle-dir>',
'--require-summary-ok',
],
when: 'Pingora gateway shadow env switch 命令证据生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认 env 切换命令 stdout / stderr / command-record 与 manifest 元数据一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'rollback-prep',
name: '回退前 health patrol 预置回 Nginx',
command: 'node',
args: [
'--',
commandEvidenceScript,
'--phase',
'rollback-prep',
'--command-name',
'pingora-health-patrol-nginx-env-switch',
'--cutover-run-id',
config.cutoverRunId,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--expected-executable',
healthPatrolEnvSwitchScript,
'--require-arg',
'--apply',
'--require-arg',
'nginx',
'--',
healthPatrolEnvSwitchScript,
...healthPatrolNginxSwitchArgs,
],
when: 'rollback apply 前执行,并用命令证据归档 health patrol env 从 pingora-direct 预置回 nginx,确保回退脚本内置 health patrol env 复核能校验切换前 public base URL / Host。',
},
{
phase: 'rollback-prep',
name: '回退前 health patrol nginx 命令证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<health-patrol-nginx-env-switch-bundle-dir>',
'--require-summary-ok',
],
when: 'health patrol nginx env switch 命令证据生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认 env 切换命令 stdout / stderr / command-record 与 manifest 元数据一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'rollback-apply',
name: 'Pingora direct rollback apply',
command: 'node',
args: [
'--',
commandEvidenceScript,
'--phase',
'rollback-apply',
'--command-name',
'pingora-direct-rollback-apply',
'--cutover-run-id',
config.cutoverRunId,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--expected-executable',
rollbackScript,
'--require-arg',
'--apply',
'--',
rollbackScript,
'--apply',
...rollbackBaseArgs,
],
when: 'health patrol env 预置回 Nginx 且 dry-run 确认后执行,用于回到 Nginx / shadow 入口,并把回退命令 stdout / stderr / 退出码归档。',
},
{
phase: 'rollback-apply',
name: '回退命令证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<rollback-apply-bundle-dir>',
'--require-summary-ok',
],
when: 'rollback apply 命令证据生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认回退命令 stdout / stderr / command-record 与 manifest 元数据一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'post-rollback',
name: '回退后 health patrol env Nginx 模式复核',
command: 'node',
args: [
'--',
path.join(
releaseRoot,
'scripts/check-production-health-patrol-env.mjs',
),
'--env-file',
config.directHealthPatrolEnvFile,
'--expected-gateway-mode',
'nginx',
...(config.rollbackHealthPatrolPublicBaseUrl
? [
'--expected-public-base-url',
config.rollbackHealthPatrolPublicBaseUrl,
]
: []),
...(config.rollbackHealthPatrolPublicHost
? ['--expected-public-host', config.rollbackHealthPatrolPublicHost]
: ['--require-empty-public-host']),
],
when: 'health patrol env 已切回 nginx 后执行,阻断 pingora-direct 模式或回退目标漂移。',
},
{
phase: 'post-rollback',
name: '回退后状态快照证据包',
command: 'node',
args: [
'--',
evidenceBundleScript,
'--phase',
'post-rollback',
'--cutover-run-id',
config.cutoverRunId,
'--release-root',
releaseRoot,
'--output-root',
config.cutoverEvidenceOutputRoot,
'--snapshot-script',
statusSnapshotScript,
'--health-patrol-env-file',
config.directHealthPatrolEnvFile,
'--pingora-env-file',
config.directPreflightEnvFile,
'--expected-gateway-mode',
'nginx',
'--expected-pingora-env-mode',
'shadow',
'--expected-public-base-url',
config.rollbackHealthPatrolPublicBaseUrl,
...(config.rollbackHealthPatrolPublicHost
? ['--expected-public-host', config.rollbackHealthPatrolPublicHost]
: ['--require-empty-public-host']),
...(config.requireDirect ? ['--require-pingora-gateway'] : []),
'--run-health-patrol',
'--fail-on-critical',
],
when: 'rollback apply 和回退后 env 复核通过后执行,归档 Nginx 接流状态证据包。',
},
{
phase: 'post-rollback',
name: '回退后证据 manifest 只读验真',
command: 'node',
args: [
'--',
evidenceVerifyScript,
'--bundle-dir',
'<post-rollback-bundle-dir>',
'--require-summary-ok',
],
when: '回退后状态快照证据包生成后,将上一步 stdout 中的 bundleDir 替换占位符执行,确认回退证据 manifest 和文件内容一致,且 manifest.summary.status 为 OK。',
},
{
phase: 'post-rollback',
name: '切换证据根目录三阶段总审计',
command: 'node',
args: [
'--',
evidenceAuditScript,
'--evidence-root',
config.cutoverEvidenceOutputRoot,
'--verify-script',
evidenceVerifyScript,
'--require-phase',
'pre-cutover',
'--require-phase',
'post-enable',
'--require-phase',
'post-rollback',
'--require-phase-direct-live-access-log',
'post-enable',
'--require-phase-direct-live-static-headers',
'post-enable',
'--require-phase-pingora-env-shadow',
'post-rollback',
'--require-command',
'enable-apply:pingora-direct-enable-apply',
'--require-command',
'post-enable:pingora-health-patrol-direct-env-switch',
'--require-command',
'rollback-prep:pingora-gateway-shadow-env-switch',
'--require-command',
'rollback-prep:pingora-health-patrol-nginx-env-switch',
'--require-command',
'rollback-apply:pingora-direct-rollback-apply',
'--require-command-executable',
`enable-apply:pingora-direct-enable-apply:${enableScript}`,
'--require-command-executable',
`post-enable:pingora-health-patrol-direct-env-switch:${healthPatrolEnvSwitchScript}`,
'--require-command-executable',
`rollback-prep:pingora-gateway-shadow-env-switch:${gatewayEnvShadowSwitchScript}`,
'--require-command-executable',
`rollback-prep:pingora-health-patrol-nginx-env-switch:${healthPatrolEnvSwitchScript}`,
'--require-command-executable',
`rollback-apply:pingora-direct-rollback-apply:${rollbackScript}`,
'--require-command-arg',
'enable-apply:pingora-direct-enable-apply:--apply',
'--require-command-arg',
'post-enable:pingora-health-patrol-direct-env-switch:--apply',
'--require-command-arg',
'post-enable:pingora-health-patrol-direct-env-switch:pingora-direct',
'--require-command-arg',
'rollback-prep:pingora-gateway-shadow-env-switch:--apply',
'--require-command-arg',
'rollback-prep:pingora-health-patrol-nginx-env-switch:--apply',
'--require-command-arg',
'rollback-prep:pingora-health-patrol-nginx-env-switch:nginx',
'--require-command-arg',
'rollback-apply:pingora-direct-rollback-apply:--apply',
'--require-cutover-run-id',
config.cutoverRunId,
'--timeline-max-span-ms',
String(config.cutoverEvidenceTimelineMaxSpanMs),
],
when: '三阶段证据包、enable / rollback apply 命令证据和三条 env 变更命令证据都生成并分别验真后执行,自动找每个 phase 与命令的最新 bundleDir 并再次做只读 manifest 验真,且要求 post-enable manifest.summary.directLiveAccessLog 和 directLiveStaticHeaders 可直接复盘 request_id 对账、静态缓存、校验器、Range 和 304 证据,同时要求 post-rollback manifest.summary.pingoraEnvShadow 证明 Pingora env 已恢复 shadow 高端口。',
},
];
}
function readinessArgs(
config,
{ includeDirect, includeDirectPortsFree = true },
) {
return [
'--release-runtime-only',
...(config.requireLive
? [
'--require-live',
'--live-base-url',
config.liveBaseUrl,
'--live-host',
config.liveHost,
]
: []),
...(config.requireRealpathLive
? [
'--require-realpath-live',
'--realpath-live-base-url',
config.realpathLiveBaseUrl,
'--realpath-live-host',
config.realpathLiveHost,
'--realpath-live-nginx-access-log',
config.realpathLiveAccessLogNginx,
'--realpath-live-pingora-access-log',
config.realpathLiveAccessLogPingora,
'--realpath-live-access-log-since-lines',
String(config.realpathLiveAccessLogSinceLines),
]
: []),
...(includeDirect
? [
'--require-direct',
'--direct-https-base-url',
config.directHttpsBaseUrl,
'--direct-http-base-url',
config.directHttpBaseUrl,
'--direct-host',
config.directHost,
'--direct-redirect-host',
config.directRedirectHost,
...(config.directRedirectBaseUrl
? ['--direct-redirect-base-url', config.directRedirectBaseUrl]
: []),
...(config.directProbeToken
? ['--direct-probe-token', config.directProbeToken]
: []),
'--direct-pingora-access-log',
config.directPingoraAccessLog,
'--direct-access-log-since-lines',
config.directAccessLogSinceLines,
'--direct-spacetime-database',
config.directSpacetimeDatabase,
'--direct-health-patrol-env-file',
config.directHealthPatrolEnvFile,
'--direct-preflight-env-file',
config.directPreflightEnvFile,
'--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',
...(includeDirectPortsFree
? ['--direct-preflight-check-ports-free']
: []),
]
: []),
];
}
function buildSteps(config) {
if (config.releaseRuntimeOnly) {
return buildReleaseRuntimeSteps(config);
}
const steps = [
{
name: 'Rust 路由与保护单测',
command: 'cargo',
args: [
'test',
'-p',
'pingora-gateway',
'--manifest-path',
'server-rs/Cargo.toml',
],
},
{
name: 'Pingora mock 上游 smoke',
command: 'npm',
args: ['run', 'check:pingora-gateway-smoke'],
},
{
name: 'Nginx/Pingora 路由矩阵 parity',
command: 'npm',
args: ['run', 'check:pingora-route-parity'],
},
{
name: 'Nginx canary snippet 校验',
command: 'node',
args: [
'scripts/check-nginx-pingora-canary.mjs',
...(config.requireNginx ? ['--require-nginx'] : []),
],
},
{
name: 'Docker Nginx handoff smoke',
command: 'node',
args: [
'scripts/check-pingora-canary-docker.mjs',
...(config.requireDocker ? ['--require-docker'] : []),
...(config.pullDocker ? ['--pull'] : []),
],
},
{
name: 'Pingora canary access log 对账烟测',
command: 'npm',
args: ['run', 'check:pingora-canary-access-log-parity'],
},
{
name: 'Pingora realpath canary 启停烟测',
command: 'npm',
args: ['run', 'check:pingora-realpath-canary-toggle'],
},
{
name: 'Pingora canary live 参数护栏',
command: 'node',
args: ['scripts/check-pingora-canary-live-guard.mjs'],
},
{
name: 'Pingora direct entry 静态预检',
command: 'npm',
args: ['run', 'check:pingora-direct-preflight'],
},
{
name: 'Pingora direct live 参数护栏',
command: 'node',
args: ['scripts/check-pingora-direct-live-guard.mjs'],
},
{
name: 'Pingora direct rollback dry-run',
command: 'npm',
args: ['run', 'check:pingora-direct-rollback'],
},
{
name: 'Pingora direct enable dry-run',
command: 'npm',
args: ['run', 'check:pingora-direct-enable'],
},
{
name: 'Pingora release readiness plan',
command: 'npm',
args: ['run', 'check:pingora-release-readiness-plan'],
},
{
name: '生产运维护栏',
command: 'npm',
args: ['run', 'check:production-ops'],
},
{
name: '生产健康巡检烟测',
command: 'npm',
args: ['run', 'check:production-health-patrol'],
},
{
name: '生产健康巡检 env 复核烟测',
command: 'npm',
args: ['run', 'check:production-health-patrol-env'],
},
{
name: 'Pingora health patrol env 切换烟测',
command: 'npm',
args: ['run', 'check:pingora-health-patrol-env-switch'],
},
{
name: 'Pingora gateway env shadow 切换烟测',
command: 'npm',
args: ['run', 'check:pingora-gateway-env-shadow-switch'],
},
{
name: 'Pingora current release 自审烟测',
command: 'npm',
args: ['run', 'check:pingora-current-release-audit'],
},
{
name: 'Pingora direct rehearsal 状态烟测',
command: 'npm',
args: ['run', 'check:pingora-direct-rehearsal-status'],
},
{
name: 'Pingora cutover 状态快照烟测',
command: 'npm',
args: ['run', 'check:pingora-cutover-status-snapshot'],
},
{
name: 'Pingora cutover 证据包烟测',
command: 'npm',
args: ['run', 'check:pingora-cutover-evidence-bundle'],
},
{
name: 'Pingora cutover 命令证据烟测',
command: 'npm',
args: ['run', 'check:pingora-cutover-command-evidence'],
},
{
name: 'Pingora cutover 证据 manifest 验真烟测',
command: 'npm',
args: ['run', 'check:pingora-cutover-evidence-verify'],
},
{
name: 'Pingora cutover 证据根目录审计烟测',
command: 'npm',
args: ['run', 'check:pingora-cutover-evidence-audit'],
},
{
name: 'API release build 烟测',
command: 'npm',
args: ['run', 'check:production-api-release'],
},
{
name: 'Pingora production release 真实构建烟测',
command: 'npm',
args: ['run', 'check:pingora-production-release-build'],
},
{
name: 'API deploy release 烟测',
command: 'npm',
args: ['run', 'check:production-api-deploy'],
},
];
if (config.requireLive || config.liveBaseUrl) {
appendTargetLiveSteps(steps, config, scriptPath);
}
appendTargetRealpathLiveSteps(steps, config, scriptPath);
if (config.requireDirect || config.directHttpsBaseUrl) {
steps.push({
name: '目标 Pingora direct entry preflight',
command: 'node',
args: [
'--',
'scripts/check-pingora-direct-preflight.mjs',
...(config.directPreflightEnvFile
? ['--env-file', config.directPreflightEnvFile, '--require-live-env']
: []),
...(config.directPreflightSystemd ? ['--systemd-cat'] : []),
...(config.directPreflightCheckCertReadable
? ['--check-cert-readable']
: []),
...(config.directPreflightCheckServiceEnvFile
? ['--check-service-env-file']
: []),
...(config.directPreflightCheckServiceUserCertReadable
? ['--check-service-user-cert-readable']
: []),
...(config.directPreflightCheckServiceBinaryExecutable
? ['--check-service-binary-executable']
: []),
...(config.directPreflightCheckPortsFree ? ['--check-ports-free'] : []),
],
});
if (config.requireDirect || config.directHealthPatrolEnvFile) {
steps.push({
name: '目标 health patrol env 直连模式复核',
command: 'node',
args: [
'--',
'scripts/check-production-health-patrol-env.mjs',
'--env-file',
config.directHealthPatrolEnvFile,
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
config.directHttpsBaseUrl,
'--expected-public-host',
config.directHost,
],
});
}
steps.push({
name: '目标 Pingora direct live smoke',
command: 'node',
args: [
'scripts/check-pingora-direct-live.mjs',
'--https-base-url',
config.directHttpsBaseUrl,
...(config.directHttpBaseUrl
? ['--http-base-url', config.directHttpBaseUrl]
: []),
...(config.directHost ? ['--host', config.directHost] : []),
...(config.directRedirectHost
? ['--redirect-host', config.directRedirectHost]
: []),
...(config.directRedirectBaseUrl
? ['--redirect-base-url', config.directRedirectBaseUrl]
: []),
...(config.directProbeToken
? ['--probe-token', config.directProbeToken]
: []),
...(config.directPingoraAccessLog
? [
'--pingora-access-log',
config.directPingoraAccessLog,
'--access-log-since-lines',
config.directAccessLogSinceLines,
]
: []),
...(config.directSpacetimeDatabase
? ['--spacetime-database', config.directSpacetimeDatabase]
: []),
...(config.directRequireWssUpgrade ? ['--require-wss-upgrade'] : []),
...(config.directSkipWss ? ['--skip-wss'] : []),
...(config.directInsecureTls ? ['--insecure-tls'] : []),
],
});
}
return steps;
}
function buildReleaseRuntimeSteps(config) {
const steps = [
{
name: 'current release 自包含自审',
command: 'node',
args: [
'--',
releaseScriptPath('scripts/ops/pingora-current-release-audit.mjs'),
'--release-root',
releaseRoot,
...(config.requireDirect ? ['--require-pingora-gateway'] : []),
...(config.requireDirect ? ['--systemd-show'] : []),
],
cwd: releaseRoot,
},
];
if (!config.requireDirect) {
steps.push({
name: '目标 Pingora direct rehearsal 状态复核',
command: 'node',
args: [
'--',
releaseScriptPath('scripts/ops/pingora-direct-rehearsal-status.mjs'),
'--release-root',
releaseRoot,
'--expect-public-gateway',
'nginx',
'--require-pingora-shadow',
'--require-realpath-canary',
'--require-current-release-gateway',
'--fail-on-critical',
],
cwd: releaseRoot,
});
}
appendTargetLiveSteps(steps, config, releaseScriptPath);
appendTargetRealpathLiveSteps(steps, config, releaseScriptPath);
appendTargetDirectSteps(steps, config, releaseScriptPath);
return steps;
}
function appendTargetLiveSteps(steps, config, scriptPath) {
if (!config.requireLive && !config.liveBaseUrl) {
return;
}
steps.push({
name: '目标 Nginx live canary smoke',
command: 'node',
args: [
'--',
scriptPath('scripts/check-pingora-canary-live.mjs'),
'--base-url',
config.liveBaseUrl,
...(config.liveHost ? ['--host', config.liveHost] : []),
],
cwd: releaseRoot,
});
steps.push({
name: '目标 Nginx live canary access log 对账',
command: 'node',
args: [
'--',
scriptPath('scripts/check-pingora-canary-access-log-parity.mjs'),
'--nginx-log-file',
config.liveAccessLogNginx,
'--pingora-log-file',
config.liveAccessLogPingora,
'--since-lines',
String(config.liveAccessLogSinceLines),
'--path',
'/__genarrative_pingora_canary/healthz',
'--path',
'/__genarrative_pingora_canary/api/assets/history',
],
cwd: releaseRoot,
});
}
function appendTargetRealpathLiveSteps(steps, config, scriptPath) {
if (!config.requireRealpathLive && !config.realpathLiveBaseUrl) {
return;
}
steps.push({
name: '目标 Nginx realpath canary smoke',
command: 'node',
args: [
'--',
scriptPath('scripts/check-pingora-canary-live.mjs'),
'--realpath',
'--base-url',
config.realpathLiveBaseUrl,
...(config.realpathLiveHost ? ['--host', config.realpathLiveHost] : []),
],
cwd: releaseRoot,
});
steps.push({
name: '目标 Nginx realpath canary access log 对账',
command: 'node',
args: [
'--',
scriptPath('scripts/check-pingora-canary-access-log-parity.mjs'),
'--realpath',
'--nginx-log-file',
config.realpathLiveAccessLogNginx,
'--pingora-log-file',
config.realpathLiveAccessLogPingora,
'--since-lines',
String(config.realpathLiveAccessLogSinceLines),
'--path',
'/__genarrative_pingora_realpath_canary/healthz',
'--path',
'/api/assets/history',
'--path',
'/v1/identity',
'--path',
'/assets/app.js',
],
cwd: releaseRoot,
});
}
function appendTargetDirectSteps(steps, config, scriptPath) {
if (!config.requireDirect && !config.directHttpsBaseUrl) {
return;
}
steps.push({
name: '目标 Pingora direct entry preflight',
command: 'node',
args: [
'--',
scriptPath('scripts/check-pingora-direct-preflight.mjs'),
...(config.directPreflightEnvFile
? ['--env-file', config.directPreflightEnvFile, '--require-live-env']
: []),
...(config.directPreflightSystemd ? ['--systemd-cat'] : []),
...(config.directPreflightCheckCertReadable
? ['--check-cert-readable']
: []),
...(config.directPreflightCheckServiceEnvFile
? ['--check-service-env-file']
: []),
...(config.directPreflightCheckServiceUserCertReadable
? ['--check-service-user-cert-readable']
: []),
...(config.directPreflightCheckServiceBinaryExecutable
? ['--check-service-binary-executable']
: []),
...(config.directPreflightCheckPortsFree ? ['--check-ports-free'] : []),
],
cwd: releaseRoot,
});
if (config.requireDirect || config.directHealthPatrolEnvFile) {
steps.push({
name: '目标 health patrol env 直连模式复核',
command: 'node',
args: [
'--',
scriptPath('scripts/check-production-health-patrol-env.mjs'),
'--env-file',
config.directHealthPatrolEnvFile,
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
config.directHttpsBaseUrl,
'--expected-public-host',
config.directHost,
],
cwd: releaseRoot,
});
}
steps.push({
name: '目标 Pingora direct live smoke',
command: 'node',
args: [
'--',
scriptPath('scripts/check-pingora-direct-live.mjs'),
'--https-base-url',
config.directHttpsBaseUrl,
...(config.directHttpBaseUrl
? ['--http-base-url', config.directHttpBaseUrl]
: []),
...(config.directHost ? ['--host', config.directHost] : []),
...(config.directRedirectHost
? ['--redirect-host', config.directRedirectHost]
: []),
...(config.directRedirectBaseUrl
? ['--redirect-base-url', config.directRedirectBaseUrl]
: []),
...(config.directProbeToken
? ['--probe-token', config.directProbeToken]
: []),
...(config.directPingoraAccessLog
? [
'--pingora-access-log',
config.directPingoraAccessLog,
'--access-log-since-lines',
config.directAccessLogSinceLines,
]
: []),
...(config.directSpacetimeDatabase
? ['--spacetime-database', config.directSpacetimeDatabase]
: []),
...(config.directRequireWssUpgrade ? ['--require-wss-upgrade'] : []),
...(config.directSkipWss ? ['--skip-wss'] : []),
...(config.directInsecureTls ? ['--insecure-tls'] : []),
],
cwd: releaseRoot,
});
}
function releaseScriptPath(relativePath) {
return path.join(releaseRoot, relativePath);
}
function scriptPath(relativePath) {
return relativePath;
}
function runStep(step) {
const args = commandArgsForSpawn(step);
console.log(`\n[pingora-release-readiness] ${step.name}`);
console.log(
`[pingora-release-readiness] ${step.command} ${redactSecretArgs(args).join(' ')}`,
);
return new Promise((resolve) => {
const child = spawn(step.command, args, {
cwd: step.cwd || repoRoot,
env: smokeEnv(),
shell: false,
stdio: 'inherit',
});
child.on('error', (error) => {
failures.push(`${step.name}: ${step.command} 启动失败:${error.message}`);
resolve(false);
});
child.on('exit', (status, signal) => {
if (signal) {
failures.push(`${step.name}: 被信号终止:${signal}`);
resolve(false);
return;
}
if ((status ?? 0) !== 0) {
failures.push(`${step.name}: 退出码 ${status}`);
resolve(false);
return;
}
resolve(true);
});
});
}
function commandArgsForSpawn(step) {
if (step.command === 'node' && step.args[0] !== '--') {
return ['--', ...step.args];
}
return step.args;
}
function smokeEnv() {
return {
...process.env,
PATH: `${path.join(os.homedir(), '.local', 'bin')}:${process.env.PATH || ''}`,
};
}