From d5fb2eca232a0e71a46af18ec9a5a3a6cad368ed Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 8 Aug 2026 22:08:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BD=95(08-08-production-deployment-?= =?UTF-8?q?execution):=20=E6=94=B6=E5=8F=A3=E7=94=9F=E4=BA=A7=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=AF=81=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trellis/spec/hyb-oa/deployment-trialrun.md | 23 +++++++- .../implement.md | 56 ++++++++++++------- .../prd.md | 18 +++--- .../task.json | 2 +- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/.trellis/spec/hyb-oa/deployment-trialrun.md b/.trellis/spec/hyb-oa/deployment-trialrun.md index 77add8b4..44b0b66a 100644 --- a/.trellis/spec/hyb-oa/deployment-trialrun.md +++ b/.trellis/spec/hyb-oa/deployment-trialrun.md @@ -1496,6 +1496,13 @@ bash scripts/qa/run-automatic-backup-contract.sh old runtime artifacts, so retrieve a failed stage log before any approved retry. Keeping an interactive execution handle is not a substitute for this second copy. +- An SSH transport loss after a stage starts makes the remote deployment state + `UNKNOWN`; it does not prove that the remote shell or later stages stopped. + Before choosing retry or rollback, inspect every candidate-stage log in + order, then run independent read-only migration reconciliation, active + runtime SHA and public readiness checks. If migration/application switching + completed but contract refresh did not, classify partial success and repair + only the refresh contract; never replay the deployment. ### 4. Validation & Error Matrix @@ -1515,7 +1522,8 @@ bash scripts/qa/run-automatic-backup-contract.sh - Long stage exits non-zero -> preserve its original status and relative log reference, stop under `set -e`, and do not enter the next stage. - SSH stream disappears -> use the already-written remote log for diagnosis; - do not repeat the deployment merely to recreate console output. + treat remote completion as unknown, inspect all stage logs plus post-status, + and do not repeat the deployment merely to recreate console output. - Runtime log directory/file mode is broader than `0700`/`0600`, stage output has no log reference, or a bare build/init/server-switch call bypasses the wrapper -> deployment evidence contract fails locally. @@ -1526,6 +1534,9 @@ bash scripts/qa/run-automatic-backup-contract.sh - Good: a long stage writes its complete remote log, returns a stable relative reference and preserves the exact non-zero child status even if the interactive stream is incomplete. +- Good: SSH closes after the first stage marker; all three remote logs and + post-status prove migration/application completion, so operations run only + the exact contract refresh and finish with aggregate read-only preflight. - Good: ordinary and forced-command identities pass separately, all aggregate items are reported, deployment passes, root helper updates only fixed build drop-ins, and four timers remain enabled/active. - Good: a broken-root recovery creates a clean published-commit source tree, proves one environment candidate against running containers without exposing values, copies it with mode `0600`, deploys with `SYNC_ENV=0`, then updates the backup contracts and completes a real off-host backup. - Base: first initialization uses reviewed explicit parameters and `DEPLOY_DATA_MODE=initialize`; it does not pretend an existing unit is available. @@ -1534,6 +1545,9 @@ bash scripts/qa/run-automatic-backup-contract.sh - Bad: grant NOPASSWD to a repository installer, point forced-command at a deploy-user-writable script, reuse an unrestricted root key, stop aggregation after its first failure, or downgrade refresh failure to a warning. - Bad: run `docker compose build/run/up` as bare SSH-stream-only calls, lose the terminal tail, then retry production solely to recover diagnostics. +- Bad: interpret a local SSH 255/disconnect as proof that the remote command + stopped, then replay migration/application deployment without checking later + stage logs or the active runtime. ### 6. Tests Required @@ -1545,6 +1559,9 @@ bash scripts/qa/run-automatic-backup-contract.sh - Static deployment tests must prove all three long stages use the persistent wrapper, output is redirected to the mode-`0600` file, the error branch captures `$?` immediately, and the former bare calls are absent. +- A disconnect recovery fixture must model missing local PASS output while + later remote stage logs exist; the expected decision is read-only + reconciliation and partial-success refresh, with no second deploy command. - A controlled recovery must additionally record: exact published source commit, per-key PASS set/date without values, byte-copy/owner/mode PASS, new-SHA service/timer contract PASS, a successful complete backup, and a successful off-host freshness check. Old candidates remain untouched until a separately approved cleanup. ### 7. Wrong vs Correct @@ -1576,6 +1593,10 @@ DEPLOY_SERVER= \ # Correct: the remote file is authoritative and the stream carries a safe ref. run_logged_deploy_stage "build-init" "${compose_cmd[@]}" build init + +# A disconnect means UNKNOWN, not failed-before-write: +# inspect build-init/run-init/build-switch-server logs and post-status first. +# If app/schema are current and only backup_build is stale, run refresh only. ``` ## Scenario: Production Migration-First Target Gate diff --git a/.trellis/tasks/08-08-production-deployment-execution/implement.md b/.trellis/tasks/08-08-production-deployment-execution/implement.md index 83b1cf6c..0f5fb253 100644 --- a/.trellis/tasks/08-08-production-deployment-execution/implement.md +++ b/.trellis/tasks/08-08-production-deployment-execution/implement.md @@ -35,34 +35,34 @@ - [x] 在同一进程内加载现行 OPS7 origin、历史身份恢复结果、exact refresh identity、只读 DB bootstrap 结果和 `EXPECTED_PENDING_MIGRATIONS`;不把任何值写入仓库或对话。 - [x] 先运行 `check-existing-deployment.sh`,记录 13 项真实 PASS/FAIL 数字;任何 FAIL 停止。 -- [ ] 运行 `deploy-existing.sh` 内置的 source/target/preflight,或按同一 wrapper 的只读阶段执行;人工阅读 target gate 原始输出和 Prisma status marker 原文。 -- [ ] 核对 candidate migration manifest、`_prisma_migrations` applied/异常/pending 集合和批准 allowlist;未知 pending、failed、rolled-back、unfinished、diverged 或目标身份不确定均 `PAUSED`。 -- [ ] 同一只读窗口查询 `terminated` 行数,记录 `rollbackWindow=OPEN|CLOSED`、实际 count 和读取时间;确认这是部署前快照。 -- [ ] 确认 preflight 到 deploy 之间有配置/数据库变更冻结;无法保证则停止。 +- [x] 运行 `deploy-existing.sh` 内置的 source/target/preflight,或按同一 wrapper 的只读阶段执行;人工阅读 target gate 原始输出和 Prisma status marker 原文。 +- [x] 核对 candidate migration manifest、`_prisma_migrations` applied/异常/pending 集合和批准 allowlist;未知 pending、failed、rolled-back、unfinished、diverged 或目标身份不确定均 `PAUSED`。 +- [x] 同一只读窗口查询 `terminated` 行数,记录 `rollbackWindow=OPEN|CLOSED`、实际 count 和读取时间;确认这是部署前快照。 +- [x] 确认 preflight 到 deploy 之间有配置/数据库变更冻结;无法保证则停止。 ## 3. 获批发布(唯一写入阶段) -- [ ] 再次确认 preflight 所有门禁仍为 PASS,且没有新的远端配置/候选变化。 -- [ ] 仅调用 `deploy/trialrun/deploy-existing.sh`,固定 `SYNC_ENV=0`、`DEPLOY_DATA_MODE=existing`、`DEPLOY_STATUS_OUTPUT_MODE=quiet`。 -- [ ] 允许的 schema 写入仅为候选 migration;不得执行 `migrate dev`、`reset`、`db push`、`resolve`、手工 DDL、seed 或业务 API 写入。 -- [ ] 若 deploy/migrate/init 中途失败,保留远端现场并停止;不自行重试、回滚或清理。 +- [x] 再次确认 preflight 所有门禁仍为 PASS,且没有新的远端配置/候选变化。 +- [x] 仅调用 `deploy/trialrun/deploy-existing.sh`,固定 `SYNC_ENV=0`、`DEPLOY_DATA_MODE=existing`、`DEPLOY_STATUS_OUTPUT_MODE=quiet`。 +- [x] 允许的 schema 写入仅为候选 migration;未执行 `migrate dev`、`reset`、`db push`、`resolve`、手工 DDL、额外 seed 或业务 API 写入。既有 wrapper 的 init 阶段按固定合同执行幂等 config/trialrun seed,不属于额外现场操作,真实结果单独记入证据。 +- [x] SSH 通道中断后没有再次部署、回滚或清理;先读取三个远端 `0600` 阶段日志,再按已落地状态走只读 post-status 和合同 refresh 恢复。 ## 4. 发布后只读验收 -- [ ] 保存 post-status 原始输出(仅受控现场)及脱敏副本;确认全部候选 migration applied / schema up to date。 -- [ ] 核对 `/ready`、完整 build SHA、公开短 SHA 与候选 commit。 -- [ ] 运行基础 smoke、external deps smoke、runtime smoke 和公开 bundle 扫描,记录真实数字和输出摘要。 -- [ ] 在任何真实终止写入前完成未登录、无权限、详情不存在/无权等权限负向路径及合同详情只读检查;缺凭据项标 `PAUSED/SKIP`。 -- [ ] 检查备份合同 refresh;失败记 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED`,不重复发布应用。 +- [x] 保存 post-status 原始输出(仅受控现场)及脱敏副本;确认全部候选 migration applied / schema up to date。 +- [x] 核对 `/ready`、完整 build SHA、公开短 SHA 与候选 commit。 +- [x] 运行基础 smoke、external deps smoke、runtime smoke 和公开 bundle 扫描,记录真实数字和输出摘要。 +- [x] 在任何真实终止写入前完成匿名合同列表/详情/终止端点 3/3 鉴权负向检查;登录态无权限与合同详情只读检查因仓库外管理员、员工凭据均返回 401 而标记 `PAUSED`,未修改生产账号或凭据。 +- [x] 初次只读复核发现 `backup_build FAIL` 后按 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED` 分流;没有重复发布应用,只执行 exact forced-command refresh,随后聚合体检恢复 `PASS=13 FAIL=0`。 ## 5. 失败分流、review 与收口 -- [ ] 将每个失败归入 target/migration、应用健康、refresh、public/live/permission 之一;不得把迁移失败与产品回归混为一类。 -- [ ] 记录真实业务写入计数为 0(本 task 不主动写入 `terminated` 或其他业务数据)。 -- [ ] 对照部署前窗口快照决定兼容回退或 roll-forward;`CLOSED` 时不得回退到不识别 `terminated` 的旧应用,不 drop 三列。 -- [ ] 启动只读 reviewer,确认门禁顺序、原始证据可追溯、脱敏副本无敏感信息、失败后没有继续推进。 -- [ ] 运行最终 Trellis/process/docs/governance/sensitive/diff 门禁;通过后提交中文 commit 并引用 `08-08-production-deployment-execution`。 -- [ ] 只有生产、公网和必要真机全部获批并通过时才可告知用户可以测试;否则保留 `PAUSED`。 +- [x] 将每个失败归入 target/migration、应用健康、refresh、public/live/permission 之一;不得把迁移失败与产品回归混为一类。 +- [x] 记录本 task 主动发起的真实业务记录写入为 0,`terminated` 行数为 0;schema 写入仅有候选 migration,固定 init 阶段执行了幂等配置 seed。 +- [x] 对照部署前/后窗口快照确认均为 `OPEN`;没有执行应用回退或 drop 三列。 +- [x] 启动只读 reviewer,确认门禁顺序、原始证据可追溯、脱敏副本无敏感信息、失败后没有继续推进。 +- [x] 运行最终 Trellis/process/docs/governance/sensitive/diff 门禁;task/process/governance/sensitive/diff 均 PASS,docs audit 为 0 errors 并保留仓库既有 22 个未注册文件 warning。 +- [x] 只有生产、公网和必要真机全部获批并通过时才可告知用户可以测试;本轮登录态与企业微信真机仍 `PAUSED`,最终答复不得告知“可以测试”。 - [ ] 归档 task 并记录会话日志,报告中保留 pre/post status、target gate、窗口判定和所有 `PAUSED/SKIP` 原因。 ## Rollback Points @@ -78,7 +78,7 @@ - 任务 artifacts 只保存脱敏副本、commit、状态码、migration ID、计数、窗口状态和证据引用。 - 不保存生产 origin、数据库名、SSH target、用户名、绝对远端路径、连接串、凭据、业务正文或原始 payload。 -## Execution Evidence (2026-08-08) +## Initial Input-gate Pause Snapshot (2026-08-08) - 规划提交:`4596ed0`;暂停证据提交:`73f1467`;当前候选已推送到配置的 `origin/main`,`check-source-provenance.mjs` 返回 `published=true`、`remoteVerified=true`。 - 本地门禁:env contract、shell/Node syntax、task validate、process contract、docs audit(0 errors,保留仓库既有 warning)、Trellis governance(0 errors/0 warnings)、sensitive scan(0 findings)均通过。 @@ -179,3 +179,19 @@ - 普通输出仅记录 stage、真实 child exit code 和相对 `logRef`。失败状态在 Bash `else` 分支中立即捕获并返回,随后由 `set -e` 停止,不会因 `fi` 后读取 `$?` 而丢失原码。 - 原始 build/init 日志不写入 Git、task 或普通对话;若受控重试失败,先只读取对应远端日志尾部并脱敏定性,再决定后续动作。任何下一次 rsync 可能清除旧 runtime artifacts,因此读取必须发生在重试之前。 - 聚焦静态回归证明三个裸调用均已替换、重定向和权限合同存在、失败分支立即捕获退出码;shell syntax、env contract 与部署安全测试 PASS。 + +## Controlled Retry, Partial-success Recovery and Post-status Evidence (2026-08-08) + +- 日志修复提交 `0224ad8` 已进入 `origin/main`;执行前本地与上游一致、工作区 clean,provenance 为 `published=true`、`remoteVerified=true`。唯一受控重试的生产 preflight 再次通过:聚合体检 `PASS=13 FAIL=0`、唯一 pending 与批准 allowlist 精确一致、`rollbackWindow=OPEN terminatedRows=0 basis=migration_not_applied`。 +- SSH 流在 `build-init` 期间被远端关闭,外层稳定停止码为 `DEPLOY_EXISTING_FAILED`。没有发起第二次部署;在任何再次同步之前,只读取得三个候选阶段日志,三者均存在且 mode `0600`。日志证明 `build-init` 完成、`run-init` 成功应用 `20260807193000_contract_registry_termination_workspace`、`build-switch-server` 完成 server 重建,故本次属于远端继续完成后的部分成功态,不是构建失败。 +- init 固定阶段显示 migration 成功,并执行既有幂等 config/trialrun seed:银行、审批规则等既有记录保持,试运行管理员未新建;没有调用合同终止、付款、审批或其他业务 API。原始阶段日志只留远端,task 仅保存本段脱敏结论。 +- 首次断线后聚合体检为 `PASS=12 FAIL=1`,唯一失败为 `backup_build`,与应用已切换但外层尚未执行合同 refresh 一致。独立 post-status 在不受该 refresh 前置项遮蔽的只读路径完成:数据库三方身份 PASS、`pending=none`、65 条 migration 的脱敏 status 为 `Database schema is up to date!`,窗口为 `OPEN terminatedRows=0 basis=terminated_status_count`。 +- 运行容器完整 SHA 与候选一致,公网 `/ready` 为 200 且短 SHA 一致;external deps smoke 四个域 PASS,runtime smoke 通过 Prisma、12 个环境开关、13 项工作区显隐、12 个 feature flag 与导航检查;db/server 两个服务运行。公网 bundle 扫描 `scanned=101 focused=2 buildMarker=PASS findings=0`。 +- 按部分成功恢复合同,只调用 exact forced-command `refresh`,没有重复发布应用。refresh 返回精确 PASS;随后完整只读复核恢复 `PASS=13 FAIL=0`,`pending=none`,窗口仍为 `OPEN terminatedRows=0 basis=terminated_status_count`。 +- Playwright 初次因本机缺少 lockfile 对应 Chromium 未触达公网;补齐用户级浏览器缓存后,公网基础探活与企微配置 1/1 PASS。管理员和员工只读 smoke 均在登录处稳定返回 401,说明仓库外验收凭据已失效;登录态权限负向和合同详情只读检查保持 `PAUSED`,未据此宣称产品失败或修改生产账号。 +- 匿名合同守卫使用不可命中占位 ID 验证列表 GET、详情 GET、终止 POST 均在鉴权层返回 401,`passed=3/3`;POST 未取得身份,未进入业务处理。企业微信真机仍为 `PAUSED`,本 task 不宣称全部用户侧验收完成。 + +## Read-only Reviewer Evidence (2026-08-08) + +- reviewer 结论为 `PASS_WITH_FINDINGS`,无阻塞性缺陷:写入前门禁、64→65 migration、唯一 pending→none、窗口依据切换、运行/公网 SHA、12/1→refresh→13/13 和匿名鉴权 3/3 均自洽;未发现第二次部署迹象或敏感值泄漏。 +- reviewer 指出的两处过期未来时已改为实际部署状态,早期未注入输入的段落已明确标记为初次输入门禁暂停快照;登录态与真机 `PAUSED` 继续保留。 diff --git a/.trellis/tasks/08-08-production-deployment-execution/prd.md b/.trellis/tasks/08-08-production-deployment-execution/prd.md index 5c0d2a27..ad0b8849 100644 --- a/.trellis/tasks/08-08-production-deployment-execution/prd.md +++ b/.trellis/tasks/08-08-production-deployment-execution/prd.md @@ -64,14 +64,14 @@ ## Acceptance Criteria -- [ ] AC1:候选 commit 已提交、工作区 clean、已进入上游;来源校验和既有 13 项体检均 PASS。若候选未发布,任务在此处 PAUSED,不使用 override。 -- [ ] AC2:只读身份引导证明历史部署目标、Compose project、受控 systemd contract 与公网 origin 属于同一唯一运行环境;远端 `.env` 与运行容器 origin 双取样均匹配后,contract `REMOTE_DIR` 已冻结为进程内精确基线,数据库名只在该前提下读取。target gate 的原始输出在受控现场可供人工阅读,脱敏副本只证明 PASS/FAIL 和原因,未泄露任何身份值。 -- [ ] AC3:任何 schema 写入前完成 pre-status;原始 status 输出与脱敏副本可对账,异常集合为空,pending 与 allowlist 精确一致,`terminated` 行数和 `rollbackWindow=OPEN|CLOSED` 判定已记录。 -- [ ] AC4:仅在 AC1–AC3 全部 PASS 后调用 `deploy-existing.sh`;未执行 reset、db push、resolve、手工 DDL、seed 之外的业务写入或真实 `terminated` 写入。 -- [ ] AC5:部署后 status 显示全部候选迁移 applied/schema up to date,`/ready`、build SHA、smoke、external/runtime smoke、bundle scan 结果与候选 commit 一致。 -- [ ] AC6:权限负向路径和合同详情只读检查排在任何真实终止写入之前;缺少凭据项明确标记 `PAUSED/SKIP`。 -- [ ] AC7:结果证据包含候选 commit、target gate、pre/post status 引用、窗口状态、各检查真实数字/输出摘要、失败分流和业务写入计数;不含凭据、原始环境或合同正文。 -- [ ] AC8:Trellis task validate、process contract、docs audit、governance、sensitive scan 和 diff check 通过;生产、公网或真机未全通过时,最终答复不得告知“可以测试”。 +- [x] AC1:候选 commit 已提交、工作区 clean、已进入上游;来源校验和既有 13 项体检均 PASS。若候选未发布,任务在此处 PAUSED,不使用 override。 +- [x] AC2:只读身份引导证明历史部署目标、Compose project、受控 systemd contract 与公网 origin 属于同一唯一运行环境;远端 `.env` 与运行容器 origin 双取样均匹配后,contract `REMOTE_DIR` 已冻结为进程内精确基线,数据库名只在该前提下读取。target gate 的原始输出在受控现场可供人工阅读,脱敏副本只证明 PASS/FAIL 和原因,未泄露任何身份值。 +- [x] AC3:任何 schema 写入前完成 pre-status;原始 status 输出与脱敏副本可对账,异常集合为空,pending 与 allowlist 精确一致,`terminated` 行数和 `rollbackWindow=OPEN|CLOSED` 判定已记录。 +- [x] AC4:仅在 AC1–AC3 全部 PASS 后调用 `deploy-existing.sh`;未执行 reset、db push、resolve、手工 DDL、seed 之外的业务写入或真实 `terminated` 写入。 +- [x] AC5:部署后 status 显示全部候选迁移 applied/schema up to date,`/ready`、build SHA、smoke、external/runtime smoke、bundle scan 结果与候选 commit 一致。 +- [x] AC6:权限负向路径和合同详情只读检查排在任何真实终止写入之前;匿名鉴权负向 3/3 PASS,登录态与合同详情因仓库外验收凭据失效明确标记 `PAUSED`,企业微信真机仍为 `PAUSED`。 +- [x] AC7:结果证据包含候选 commit、target gate、pre/post status 引用、窗口状态、各检查真实数字/输出摘要、失败分流和业务写入计数;不含凭据、原始环境或合同正文。 +- [x] AC8:Trellis task validate、process contract、docs audit、governance、sensitive scan 和 diff check 通过;登录态与企业微信真机仍 `PAUSED`,最终答复不得告知“可以测试”。 ## Out of Scope @@ -99,7 +99,7 @@ ## Open Questions -- 无。唯一公网环境拓扑、精确身份匹配和数据库名只读引导方式均已由用户确认;修订规划仍需复核批准后才实施 tooling 改动或连接生产。 +- 无。唯一公网环境拓扑、精确身份匹配和数据库名只读引导方式均已确认并按批准执行;最终登录态与真机边界见 AC6 和实施证据中的 `PAUSED` 记录。 ## Test Plan diff --git a/.trellis/tasks/08-08-production-deployment-execution/task.json b/.trellis/tasks/08-08-production-deployment-execution/task.json index b01bae26..6c7bc0db 100644 --- a/.trellis/tasks/08-08-production-deployment-execution/task.json +++ b/.trellis/tasks/08-08-production-deployment-execution/task.json @@ -31,6 +31,6 @@ ".trellis/spec/hyb-oa/deployment-trialrun.md", "server/prisma/migrations/20260807193000_contract_registry_termination_workspace" ], - "notes": "本 task 只执行获批生产发布与只读验收,不执行真实 terminated 或其他业务写入。target/pre-status/terminated 窗口检查必须先于任何写入;首条已提交 terminated 记录按事件关闭回滚窗口。规划提交 4596ed0、暂停证据提交 73f1467、证据校准提交 e2ad1e2 均已发布且 provenance PASS。已确认不存在独立预发环境,历史 hyb-oa-trialrun 身份承载唯一公网 OA;规划修订要求用精确身份组替代 trialrun 名称排除,并在身份匹配后只读引导 POSTGRES_DB。当前未连接生产、未写库;修订 tooling 需用户复核批准后再实施。原始 status 仅留受控现场,任务证据脱敏。", + "notes": "获批生产发布已完成:候选 termination migration 已应用,post-status 为 65 条 migration 全部 applied,运行/公网 build 与候选一致,备份合同 refresh 后聚合体检恢复 13/13。部署前后 rollback window 均为 OPEN,terminatedRows=0;未执行真实 terminated 或其他业务 API 写入。登录态只读 smoke 因仓库外管理员、员工凭据均返回 401 而 PAUSED,企业微信真机仍 PAUSED;不得据此宣称全部用户侧验收完成。原始 status 与阶段日志仅留受控现场,任务证据脱敏。", "meta": {} }