补齐 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:
@@ -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([
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user