e3258f6f1d
新增 Pingora shadow env 回切脚本与对应检查。 补齐直连证据包时间线和 cutoverRunId 审计门禁。 支持 Gitea Host 透传并更新直连多域名文档。 修复百分号编码静态图标路径并补 smoke 覆盖。 更新生产发布与运维护栏对 Pingora 发布包的校验。
2223 lines
80 KiB
JavaScript
2223 lines
80 KiB
JavaScript
#!/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 探针 token;JSON 输出会隐藏 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/creation-entry/config',
|
||
],
|
||
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/creation-entry/config',
|
||
'--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 || ''}`,
|
||
};
|
||
}
|