From 10e559701f601c78601f25778d3022a2b2bff35b Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 14:33:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20Pingora=20runtime-only=20?= =?UTF-8?q?=E5=BD=A9=E6=8E=92=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 current release runtime-only 基础门禁接入直连彩排状态复核 保留 /opt/genarrative/current 调用路径,避免 symlink 展开导致 systemd ExecStart 自审误判 补充 release readiness plan 自测和 Pingora 运维文档 在 dev 服务器真实验证 runtime-only 门禁、Nginx 公网边界、Pingora shadow 和 realpath canary --- deploy/nginx/README.md | 2 +- .../shared-memory/decision-log.md | 2 +- ...开发运维】Pingora独立网关试点-2026-06-11.md | 4 +- .../check-pingora-release-readiness-plan.mjs | 146 +++++++++++++++++- scripts/check-pingora-release-readiness.mjs | 30 +++- 5 files changed, 178 insertions(+), 6 deletions(-) diff --git a/deploy/nginx/README.md b/deploy/nginx/README.md index 2e400add7..55b572897 100644 --- a/deploy/nginx/README.md +++ b/deploy/nginx/README.md @@ -71,7 +71,7 @@ - 真实路径 canary 不再手工编辑 `/etc/nginx/conf.d/`。目标机从 current release 执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-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,只允许端口不同。 ## 验证 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0a6cf7756..d85d12761 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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 并补偿退款 diff --git a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md index 001662fe1..debc09d7f 100644 --- a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md +++ b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md @@ -119,7 +119,7 @@ node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs `check:pingora-release-readiness` 是正式切换前的聚合门禁,默认串行执行 `cargo test -p pingora-gateway`、mock 上游 smoke、路由矩阵 parity、Nginx canary snippet 校验、Docker handoff smoke、canary access log 对账烟测、直连入口静态预检、直连启用 / 回退 dry-run 行为检查、current release 自审烟测、直连彩排状态烟测、release readiness 计划自检、生产运维护栏、Pingora cutover 状态快照烟测、Pingora cutover 证据包烟测、Pingora cutover 命令证据烟测、Pingora cutover 证据 manifest 验真烟测、Pingora cutover 证据根目录审计烟测、API release build 烟测、Pingora production release 真实构建烟测和 API deploy release 烟测。普通本机执行时 Docker / Nginx 能力仍按子脚本默认口径跳过;切换窗口或 CI 必须执行: -目标机 current release 上的启用前基础门禁和启用后 `--require-direct` 复核使用同一个聚合脚本,但必须追加 `--release-runtime-only`。该模式只执行发布包内可自包含的运行时复核:current release 自审、live canary、真实 access log 对账、direct preflight、health patrol env 复核和 direct live smoke;不会运行 Cargo、npm、Docker 或 Nginx 源码 / 构建环境门禁,并会拒绝 `--require-docker`、`--pull-docker` 与 `--require-nginx`。 +目标机 current release 上的启用前基础门禁和启用后 `--require-direct` 复核使用同一个聚合脚本,但必须追加 `--release-runtime-only`。该模式只执行发布包内可自包含的运行时复核:current release 自审、启用前直连彩排状态复核、live canary、真实 access log 对账、direct preflight、health patrol env 复核和 direct live smoke;不会运行 Cargo、npm、Docker 或 Nginx 源码 / 构建环境门禁,并会拒绝 `--require-docker`、`--pull-docker` 与 `--require-nginx`。未带 `--require-direct` 的启用前基础门禁会自动执行 `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 与 realpath canary 高端口在线;启用后 `--require-direct` 复核不再要求 Nginx 接公网的彩排状态,改为检查 direct preflight、health patrol 直连模式和 direct live smoke。 正式 runbook 的 direct enable apply / rollback apply 命令证据必须额外传 `--expected-executable ` 和 `--require-arg --apply`;命令证据脚本会在执行前拒绝与预期脚本不一致、缺少必需 `--apply` 参数,或任一真实命令参数包含换行 / NUL 字符的真实命令,并把 `expectedExecutable` 写入 manifest / command-record。命令名只用于审计分类,不能替代真实脚本身份和真实 apply 参数校验。 @@ -196,7 +196,7 @@ npm run plan:pingora-direct-cutover -- \ runbook 会列出 Host 与回退巡检入口确认、切换前 current release 自包含自审、切换前状态快照证据包、切换前证据 manifest 只读验真、current release 随包 preflight、启用前基础门禁、direct enable dry-run、direct enable apply、启用命令证据 manifest 只读验真、用 current release 随包 `scripts/deploy/pingora-health-patrol-env-switch.mjs --apply` 把 health patrol 切到 `pingora-direct`、启用后 health patrol env 直连复核、启用后状态快照证据包、启用后证据 manifest 只读验真、启用后 `--require-direct` 复核、rollback dry-run、用同一脚本在 rollback apply 前把 health patrol 预置回 `nginx`、rollback apply、回退命令证据 manifest 只读验真、回退后 health patrol env Nginx 模式复核、回退后状态快照证据包、回退后证据 manifest 只读验真和切换证据根目录三阶段总审计。rollback dry-run / apply 默认会传 `--nginx-smoke-expect-body '"ok":true'`,因此回退 smoke 不只接受 HTTP 200,还要求 `/healthz` 响应体包含预期片段;若现场回退 smoke URL 换成其它端点,必须显式调整 `--rollback-nginx-smoke-expect-body`。启用前 current release 自审使用 `--require-pingora-gateway --systemd-show`,先确认发布包自包含、`api-server.sha256` / `pingora-gateway.sha256` 匹配、`release-manifest.api-server.json` 已登记 `pingora-gateway`、`pingora-gateway` 可执行且 systemd `ExecStart` 指向 current release;启用前基础门禁不带 `--require-direct`,因为此时 systemd direct-entry drop-in 尚未生效;启用后复核必须带 `--require-direct`,若切换参数提供 `--direct-probe-token`,启用后复核也会继续透传该 token 检查内部探针,JSON runbook 只显示 ``。三个状态快照证据包都使用 current release 随包脚本,显式传 `--output-root <证据根目录>` 和同一个 `--cutover-run-id`,并带 `--require-pingora-gateway --run-health-patrol --fail-on-critical`,用于把切换前基线、启用后 direct 状态、回退后 Nginx 状态和当前发布物自审结果留成可归档证据;其中启用后证据包还会额外传 `--run-direct-live`,把 direct live smoke 的 `direct-live.json`、stdout / stderr、命令记录和 Pingora access log `request_id` 反查结果一起写入同一证据目录,且 `direct-access-log` JSON 要保留匹配数量、缺失明细和 method/path/status 漂移明细,避免只在终端输出里保留直连证据;证据包 manifest 会给已生成的 snapshot、direct live、stdout / stderr、命令记录和 parse-error 文件记录 `path`、`sizeBytes` 与 `sha256`,direct enable apply / rollback apply 通过命令证据脚本归档时,命令证据 manifest 也会给 `command.stdout.txt`、`command.stderr.txt` 和 `command-record.json` 同步记录 `sizeBytes` 与 `sha256`。每次复制或归档证据目录后,都要把对应阶段 stdout 中的 `bundleDir` 填入 `/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir ` 做只读验真,确认文件未在归档过程中损坏或被替换;enable / rollback apply 命令证据生成后也要分别把 stdout 中的 `bundleDir` 填入 verifier 的 `` / `` 占位符,先验真命令证据,再继续 health patrol 切换或回退后复核;三阶段证据和 enable / rollback apply 命令证据都完成并即时验真后,再运行 `/opt/genarrative/current/scripts/ops/pingora-cutover-evidence-audit.mjs --evidence-root <证据根目录> --require-phase pre-cutover --require-phase post-enable --require-phase post-rollback --require-command enable-apply:pingora-direct-enable-apply --require-command rollback-apply:pingora-direct-rollback-apply --require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --require-command-arg enable-apply:pingora-direct-enable-apply:--apply --require-command-arg rollback-apply:pingora-direct-rollback-apply:--apply --require-cutover-run-id <本次cutoverRunId> --timeline-max-span-ms 86400000` 生成总审计 JSON,并在 JSON 中记录 `requiredCutoverRunId`、`requiredCommandExecutables`、`requiredCommandArgs`、`timeline.maxSpanMs` 与 `timeline.spanMs`。若 snapshot 或 direct live stdout 解析失败,证据包会保留对应 `*-parse-error.txt` 并写入 manifest 索引。`--cutover-release-root` 与 `--cutover-evidence-output-root` 必须是绝对路径,且不能是文件系统根目录,证据根目录默认 `/var/log/genarrative/pingora-cutover-evidence`;`--cutover-evidence-timeline-max-span-ms` 默认 `86400000`,仅用于明确批准超过 24 小时的维护窗口。 -runbook 中“启用前基础门禁”和“启用后 `--require-direct` 复核”都调用 current release 随包 `scripts/check-pingora-release-readiness.mjs --release-runtime-only`。前者不带 `--require-direct`,因为 direct-entry drop-in 尚未生效;后者必须带 `--require-direct`,并复核 direct preflight、health patrol env 直连模式和 direct live smoke。 +runbook 中“启用前基础门禁”和“启用后 `--require-direct` 复核”都调用 current release 随包 `scripts/check-pingora-release-readiness.mjs --release-runtime-only`。前者不带 `--require-direct`,因为 direct-entry drop-in 尚未生效,并会额外跑直连彩排状态复核,确认 Nginx 仍接公网、Pingora shadow / realpath canary 高端口和 current release 自审均可用;后者必须带 `--require-direct`,不再要求 Nginx 接公网彩排状态,并复核 direct preflight、health patrol env 直连模式和 direct live smoke。 证据 verifier / 总审计的入口路径、verifier 脚本路径和 manifest 登记文件名都不能包含换行或 NUL 字符,避免污染 JSON 证据、终端输出或归档复盘;遇到此类失败应重新生成证据或修正 runbook 参数,不要手工改 manifest 兜底。 diff --git a/scripts/check-pingora-release-readiness-plan.mjs b/scripts/check-pingora-release-readiness-plan.mjs index 89b9c20bd..b7e434af2 100644 --- a/scripts/check-pingora-release-readiness-plan.mjs +++ b/scripts/check-pingora-release-readiness-plan.mjs @@ -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([ diff --git a/scripts/check-pingora-release-readiness.mjs b/scripts/check-pingora-release-readiness.mjs index 61491e811..126e95a18 100644 --- a/scripts/check-pingora-release-readiness.mjs +++ b/scripts/check-pingora-release-readiness.mjs @@ -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);