补齐 Pingora runtime-only 彩排门禁

将 current release runtime-only 基础门禁接入直连彩排状态复核

保留 /opt/genarrative/current 调用路径,避免 symlink 展开导致 systemd ExecStart 自审误判

补充 release readiness plan 自测和 Pingora 运维文档

在 dev 服务器真实验证 runtime-only 门禁、Nginx 公网边界、Pingora shadow 和 realpath canary
This commit is contained in:
2026-06-18 14:33:54 +08:00
parent 4bca480f3c
commit 10e559701f
5 changed files with 178 additions and 6 deletions
@@ -1,7 +1,14 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
@@ -89,6 +96,8 @@ function main() {
assertDefaultPlanIncludesCanaryAccessLogParitySmoke();
assertDefaultPlanIncludesPingoraProductionReleaseBuildSmoke();
assertReleaseRuntimeOnlyPlanUsesCurrentReleaseScripts();
assertReleaseRuntimeOnlyPlanIncludesDirectRehearsalStatus();
assertReleaseRuntimeOnlyKeepsInvokedCurrentSymlinkPath();
assertReleaseRuntimeOnlyRejectsSourceOnlyFlags();
assertDryRunCutoverPlanIncludesDirectEnableAndRollbackRunbook();
assertDryRunCutoverDefaultRollbackBodyIgnoresProcessEnv();
@@ -2194,6 +2203,141 @@ function assertReleaseRuntimeOnlyPlanUsesCurrentReleaseScripts() {
}
}
function assertReleaseRuntimeOnlyPlanIncludesDirectRehearsalStatus() {
const basePlan = readPlan(['--release-runtime-only', '--dry-run-plan']);
const baseStep = findStep(
basePlan,
'目标 Pingora direct rehearsal 状态复核',
);
if (!baseStep) {
failures.push(
'release runtime-only 基础计划必须包含直连彩排状态复核。',
);
return;
}
assertAbsoluteScriptArg(
baseStep.args,
'scripts/ops/pingora-direct-rehearsal-status.mjs',
'release runtime-only 直连彩排状态复核必须使用 current release 随包脚本。',
);
assertIncludes(
baseStep.args,
'--release-root',
'release runtime-only 直连彩排状态复核必须显式传 current release 根目录。',
);
assertIncludes(
baseStep.args,
'--expect-public-gateway',
'release runtime-only 直连彩排状态复核必须声明公网入口期望。',
);
assertIncludes(
baseStep.args,
'nginx',
'release runtime-only 直连彩排状态复核必须确认 Nginx 仍接公网。',
);
assertIncludes(
baseStep.args,
'--require-pingora-shadow',
'release runtime-only 直连彩排状态复核必须要求 Pingora shadow 高端口在线。',
);
assertIncludes(
baseStep.args,
'--require-realpath-canary',
'release runtime-only 直连彩排状态复核必须要求 realpath canary 高端口在线。',
);
assertIncludes(
baseStep.args,
'--require-current-release-gateway',
'release runtime-only 直连彩排状态复核必须复用 current release Pingora 自审。',
);
assertIncludes(
baseStep.args,
'--fail-on-critical',
'release runtime-only 直连彩排状态复核出现 CRITICAL 必须阻断门禁。',
);
const directPlan = readPlan([
...requireDirectBaseArgs,
'--release-runtime-only',
'--dry-run-plan',
]);
if (findStep(directPlan, '目标 Pingora direct rehearsal 状态复核')) {
failures.push(
'release runtime-only --require-direct 阶段不能继续要求 Nginx 接公网的彩排状态。',
);
}
}
function assertReleaseRuntimeOnlyKeepsInvokedCurrentSymlinkPath() {
const tmpRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-release-readiness-current-'),
);
try {
const releaseDir = path.join(tmpRoot, 'releases', 'test-release');
const currentLink = path.join(tmpRoot, 'current');
const scriptDir = path.join(releaseDir, 'scripts');
mkdirSync(scriptDir, { recursive: true });
symlinkSync(releaseDir, currentLink, 'dir');
const invokedScript = path.join(
currentLink,
'scripts',
'check-pingora-release-readiness.mjs',
);
const realScript = path.join(
scriptDir,
'check-pingora-release-readiness.mjs',
);
symlinkSync(
path.join(process.cwd(), 'scripts/check-pingora-release-readiness.mjs'),
realScript,
);
const result = spawnSync(
'node',
[invokedScript, '--release-runtime-only', '--dry-run-plan'],
{
cwd: process.cwd(),
encoding: 'utf8',
env: readinessPlanEnv(),
},
);
if ((result.status ?? 0) !== 0) {
failures.push(
`通过 current symlink 读取 runtime-only plan 失败,退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
return;
}
const plan = JSON.parse(result.stdout);
const audit = findStep(plan, 'current release 自包含自审');
const rehearsal = findStep(plan, '目标 Pingora direct rehearsal 状态复核');
for (const [label, step] of [
['current release 自审', audit],
['直连彩排状态复核', rehearsal],
]) {
if (!step) {
failures.push(
`通过 current symlink 生成的 runtime-only plan 缺少${label}步骤。`,
);
continue;
}
assertIncludes(
step.args,
currentLink,
`${label}必须保留调用方 current symlink 作为 release root,而不是展开到真实 release 目录。`,
);
if (step.args.includes(releaseDir)) {
failures.push(
`${label}不应把 current symlink 展开成真实 release 目录: ${releaseDir}`,
);
}
}
} catch (error) {
failures.push(`current symlink runtime-only plan 自测异常: ${error.message}`);
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
}
function assertReleaseRuntimeOnlyRejectsSourceOnlyFlags() {
for (const flag of ['--require-docker', '--pull-docker', '--require-nginx']) {
const result = runReadinessExpectFailure([
+29 -1
View File
@@ -6,7 +6,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = process.cwd();
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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-';
@@ -46,6 +46,14 @@ if (failures.length > 0) {
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,
@@ -1802,6 +1810,26 @@ function buildReleaseRuntimeSteps(config) {
},
];
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);