补齐 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:
@@ -71,7 +71,7 @@
|
||||
- 真实路径 canary 不再手工编辑 `/etc/nginx/conf.d/`。目标机从 current release 执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083`;脚本会渲染到 `/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf`,保持晚于主站 `log_format genarrative_upstream` 加载,执行 `nginx -t`、reload Nginx,并默认运行 realpath live smoke。`nginx -t`、reload 或 live smoke 任一失败都会恢复写入前配置。关闭时执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply`,脚本会删除该 `zz-...` 文件,执行 `nginx -t` 和 reload,失败时恢复删除前配置。
|
||||
- 真实路径 canary reload 后可再次运行 `node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>`,再运行 `node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/creation-entry/config --path /v1/identity --path /assets/app.js`。真实路径模式除 healthz 探针外要求 Nginx path 与 Pingora path 完全一致。
|
||||
- 正式切换前在源码 checkout / CI / 构建环境跑 `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 <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;启用真实路径 canary 时追加 `--require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`。
|
||||
- 目标机 `/opt/genarrative/current` 只能跑随包 runtime-only 门禁:`/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;验证真实路径 canary 或 Pingora 直连入口时分别追加 `--require-realpath-live ...` 或 `--require-direct ...` 参数。生成正式直连切换 runbook 时,`--direct-redirect-host` 和回退 smoke Host 必须与 `--direct-host` 同 hostname,只允许端口不同。
|
||||
- 目标机 `/opt/genarrative/current` 只能跑随包 runtime-only 门禁:`/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;验证真实路径 canary 或 Pingora 直连入口时分别追加 `--require-realpath-live ...` 或 `--require-direct ...` 参数。未带 `--require-direct` 的 runtime-only 基础门禁会自动执行直连彩排状态复核,确认公网 `80/443` 仍由 Nginx 接流、Pingora shadow `127.0.0.1:18081`、realpath canary `127.0.0.1:18083` 和 current release 自审均通过;启用后 `--require-direct` 复核不再要求 Nginx 接公网彩排状态。生成正式直连切换 runbook 时,`--direct-redirect-host` 和回退 smoke Host 必须与 `--direct-host` 同 hostname,只允许端口不同。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -2348,7 +2348,7 @@
|
||||
- 决策补充:Pingora Nginx canary snippet 变更必须运行 `npm run check:nginx-pingora-canary`;该脚本静态校验本机来源限制、handoff 响应头、probe token 占位、前缀 rewrite、低缓冲和 SpacetimeDB WebSocket Upgrade。目标 agent 或 CI 有 Nginx 时必须运行 `node scripts/check-nginx-pingora-canary.mjs --require-nginx`,把 snippet 包进临时 `server {}` 强制执行 `nginx -t`。
|
||||
- 决策补充:Pingora 与 Nginx 的核心路由 parity 以 `deploy/pingora/nginx-route-parity.matrix.json` 为共享检查输入;涉及 Nginx 模板、Pingora 路由、限流分组或路由文档时必须同步更新矩阵,并运行 `npm run check:pingora-route-parity` 与 `cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix`。Rust 单测读取同一份矩阵验证 `classify_path`、body limit 和接流保护分组,Node 检查同时覆盖生产 / 开发 Nginx 模板和试点文档片段。
|
||||
- 决策补充:Pingora 前缀 canary 在目标 Nginx 中人工 include 并 reload 后,必须运行 `npm run check:pingora-canary-live`;该脚本只读访问 `__genarrative_pingora_canary` 前缀下的 healthz、代表性 API、SpacetimeDB identity、静态资源和拒绝入口,并强制校验 `X-Genarrative-Nginx-Handoff: pingora-canary`,避免只通过 snippet 静态检查却没有证明真实 handoff 链路可用。
|
||||
- 决策补充:Pingora release readiness 分为源码全量门禁和 current release runtime-only 门禁。源码 checkout / CI / 构建环境继续运行默认 `check-pingora-release-readiness.mjs`,覆盖 Cargo、npm、Docker、Nginx 静态 / 真机校验和发布包构建烟测;目标机 `/opt/genarrative/current` 的启用前基础门禁和启用后 `--require-direct` 复核必须调用随包 `scripts/check-pingora-release-readiness.mjs --release-runtime-only`,只执行 current release 自审、live canary、access log 对账、direct preflight、health patrol env 复核和 direct live smoke。API release / current release 必须随包携带 `scripts/check-pingora-release-readiness.mjs`、`scripts/check-pingora-canary-live.mjs`、canary access log 对账脚本和 direct preflight / live 子脚本;runtime-only 模式不得依赖源码 checkout、npm project root、Docker 或目标机 Nginx 静态校验。
|
||||
- 决策补充:Pingora release readiness 分为源码全量门禁和 current release runtime-only 门禁。源码 checkout / CI / 构建环境继续运行默认 `check-pingora-release-readiness.mjs`,覆盖 Cargo、npm、Docker、Nginx 静态 / 真机校验和发布包构建烟测;目标机 `/opt/genarrative/current` 的启用前基础门禁和启用后 `--require-direct` 复核必须调用随包 `scripts/check-pingora-release-readiness.mjs --release-runtime-only`,只执行 current release 自审、启用前直连彩排状态、live canary、access log 对账、direct preflight、health patrol env 复核和 direct live smoke。未带 `--require-direct` 时 runtime-only 必须自动运行随包 `scripts/ops/pingora-direct-rehearsal-status.mjs --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical`,确认公网 `80/443` 仍由 Nginx 接流、Pingora shadow `127.0.0.1:18081`、realpath canary `127.0.0.1:18083` 和 current release 自审均通过;启用后 `--require-direct` 复核不再要求 Nginx 接公网彩排状态,改为检查 direct preflight、health patrol 直连模式和 direct live smoke。API release / current release 必须随包携带 `scripts/check-pingora-release-readiness.mjs`、`scripts/check-pingora-canary-live.mjs`、canary access log 对账脚本、直连彩排状态脚本和 direct preflight / live 子脚本;runtime-only 模式不得依赖源码 checkout、npm project root、Docker 或目标机 Nginx 静态校验。
|
||||
- 决策补充:Pingora 直连静态响应必须显式写入缓存头。HTML、目录 index 和 SPA fallback 默认 `Cache-Control: no-cache`;`/assets/*` 与 `/admin/assets/*` 中带 Vite 指纹文件名的资源默认 `Cache-Control: public, max-age=31536000, immutable`;非指纹静态和 ACME challenge 默认 `no-cache`。三档由 `GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL`、`GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL` 和 `GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL` 覆盖,配置值不能包含换行或 NUL;`npm run check:pingora-gateway-smoke` 必须覆盖这些缓存头,避免直连后入口 HTML 被长期缓存或指纹资源失去长期缓存收益。
|
||||
## 2026-06-11 资产计费边界改为 fail-closed 并补偿退款
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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