docs(08-08-production-deployment-validation): 补齐部署验证规范与证据边界

这个提交包含在:
2026-08-08 11:11:31 +08:00
父节点 5e2d038938
当前提交 65e22c60c7
共修改 9 个文件,包含 445 行新增和 6 行删除
+81 -1
查看文件
@@ -1442,7 +1442,7 @@ bash scripts/qa/run-automatic-backup-contract.sh
### 6. Tests Required
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/deploy-existing.sh deploy/trialrun/preflight-remote.sh`.
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/deploy-existing.sh deploy/trialrun/preflight-remote.sh deploy/trialrun/production-preflight-remote.sh`.
- `bash scripts/qa/run-deployment-contract-refresh-contract.sh` must prove multi-failure aggregation reaches the final item, output redaction, forced-command exact matching, four failure categories, four build drop-ins and four timer checks.
- `node deploy/trialrun/validate-env-contract.js`.
- `cd server && npm test -- tests/unit/scripts/deployment-safety-contract.test.js`.
@@ -1472,6 +1472,86 @@ DEPLOY_SERVER=<approved-ssh-target> \
# recovery runbook to prove one complete backup and off-host freshness.
```
## Scenario: Production Migration-First Target Gate
### 1. Scope / Trigger
- Trigger: an approved existing-host release whose candidate commit contains a
Prisma migration, especially the contract `terminated` workspace migration.
- Purpose: prove the remote target identity and migration history before any
schema write, then preserve the state-based rollback boundary.
### 2. Signatures
- Wrapper: `deploy/trialrun/deploy-existing.sh`.
- Local orchestrator: `deploy/trialrun/production-preflight.mjs`.
- Remote read-only helper: `deploy/trialrun/production-preflight-remote.sh`.
- Required process inputs: `EXPECTED_PRODUCTION_ORIGIN`,
`EXPECTED_PRODUCTION_DB_NAME`, and `EXPECTED_PENDING_MIGRATIONS`.
### 3. Contracts
- Production R2 is the inverse of the local migration task: the gate must prove
the target is the expected production HTTPS origin and database identity, and
reject loopback, local regression, staging and trialrun identities. It must
never infer production from an SSH alias, Compose label or directory name.
- The strict production-validation wrapper rejects unpublished-commit overrides;
disaster-recovery exceptions use a separately approved recovery flow.
- Expected target values remain process-only. The local orchestrator sends only
one-way digests to the remote helper and records stable codes, not values,
credentials, paths or raw environment content.
- The candidate manifest is generated from the verified commit. Before
`deploy.sh`, the remote helper confirms the runtime `DATABASE_URL` host and
database identity, then runs `prisma migrate status`. Raw stdout/stderr stays
in the controlled execution environment; the local report contains only a
redacted status copy. The helper compares `_prisma_migrations` with the
candidate manifest. Pending must exactly equal the explicit allowlist;
pending IDs reported by status must belong to the candidate manifest;
failed, rolled-back, unfinished, diverged or unknown history fails closed.
- The preflight helper is read-only: it does not run `migrate dev`, reset,
`db push`, `migrate resolve`, seed, DDL or business writes. `deploy.sh` is
reached only after target and pre-status gates pass.
- The post-deploy status is a separate evidence item and must be manually read;
the wrapper re-runs the read-only helper with an empty pending allowlist
before backup-contract refresh, and a successful exit code alone is
insufficient.
- Each preflight run removes only its two known report files before SSH; a
reused report directory must never leave an old PASS status artifact in place.
- When the strict wrapper invokes `deploy.sh`, it sets
`DEPLOY_STATUS_OUTPUT_MODE=quiet`; the legacy status check must not echo raw
datasource output. Detailed evidence comes from the controlled pre/post
helper artifacts.
- `terminated` count defines a state window: zero rows means `OPEN`; the first
committed row closes it. Once closed, an old application that does not know
`terminated` cannot be restored and the new columns are never auto-dropped;
recovery is roll-forward or a compatible hotfix.
- The target identity must remain unchanged between the read-only preflight and
the schema write; if a controlled change freeze cannot be established, stop
before `deploy.sh` rather than relying on post-status to repair a wrong-target
write.
### 4. Validation & Error Matrix
- Missing or invalid expected target input -> stop before SSH mutation.
- Target hash, runtime mode, cookie security or DB identity mismatch -> stop
before `deploy.sh`.
- Status output missing/unreadable, failed/rolled-back/diverged history or an
unknown pending migration -> `PAUSED`; do not reset or resolve in place.
- Pending set differs from the approved allowlist -> `PAUSED` and retain the
status artifact for human review.
- `terminated` count greater than zero -> record `rollbackWindow=CLOSED` and
disallow compatibility rollback; this task never creates such a row.
### 5. Tests Required
- `node --check deploy/trialrun/production-preflight.mjs`.
- `bash -n deploy/trialrun/production-preflight-remote.sh`.
- `node deploy/trialrun/validate-env-contract.js`.
- `cd server && npm test -- tests/unit/scripts/deployment-safety-contract.test.js`.
- Static ordering must prove production preflight precedes `deploy.sh`, and the
remote status marker precedes migration-history reconciliation. Fixtures must
reject loopback/staging target identities without invoking SSH.
## Scenario: Environment Recovery Classification And Secret Custody
### 1. Scope / Trigger
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,106 @@
# 生产部署验证与迁移门禁:设计说明
## 1. 边界与目标
本任务是一次受控生产发布与验证合同,不改变合同业务实现。部署 tooling 只在必要时增加生产 target/migration preflight 及静态测试;应用代码、生产 `.env`、真实业务数据和企业微信不在本任务改动范围。
核心状态序列:
```text
candidate/source
-> production target identity (read-only)
-> existing deployment aggregate preflight (read-only)
-> candidate migration manifest + pre-status (read-only)
-> terminated row count / rollback window snapshot (read-only)
-> approved deploy-existing (schema/app write)
-> post-status applied (read-only)
-> /ready + build + smoke + permission-negative (read-only)
-> closeout / PAUSED classification
```
任何箭头左侧门禁失败都不得进入右侧动作。尤其 `migrate deploy` 之前不得 seed、切换应用或执行业务写入。
## 2. 生产目标识别
### 2.1 输入
- `EXPECTED_PRODUCTION_ORIGIN`:执行时提供的完整 HTTPS origin。
- `EXPECTED_PRODUCTION_DB_NAME`:执行时提供的预期生产数据库名;仅用于比较,不输出。
- Existing deployment contract:由 `deployment-contract-lib.sh` 从受控 systemd unit 读取六个 allowlist 参数。
- 本任务 wrapper 不接受未发布提交 override;灾备/远端故障恢复保持为另一条需单独批准的流程。
### 2.2 只读判据
远端 helper 读取 `.env` 的键名和值只在内存中比较:
- `NODE_ENV=production`、`COOKIE_SECURE=true`。
- `APP_ORIGIN` 与 `EXPECTED_PRODUCTION_ORIGIN` 完全一致,scheme 为 HTTPS,host 不是 `localhost`、`127.0.0.1`、`::1`,也不是示例/测试/暂存 origin。
- `POSTGRES_DB` 与 `EXPECTED_PRODUCTION_DB_NAME` 一致;数据库 URL 的 database 部分一致,host 不得是回环地址或本地回归库标识。
- 生产目标的 compose/path 名称不能单独作为身份依据;`trialrun` 只描述部署脚本族,不能被误当作生产身份或排除条件。生产/非生产判定以显式 origin + DB identity 为准。
失败只输出稳定 code,例如 `PROD_TARGET_ORIGIN_MISMATCH`、`PROD_TARGET_DB_MISMATCH`,不输出实际值。
## 3. 迁移 preflight 设计
### 3.1 Candidate manifest
从候选 commit 的 `server/prisma/migrations/*` 生成排序后的 migration ID 清单。ID 只允许 Prisma migration 目录安全字符。`EXPECTED_PENDING_MIGRATIONS` 是执行时的显式 allowlist;本轮预期是合同 termination migration,若已提前应用则 pending 为空。
### 3.2 Production read-only comparison
在当前生产 Compose 数据库容器上只读查询 `_prisma_migrations`:
- 已应用集合:`finished_at IS NOT NULL AND rolled_back_at IS NULL`。
- 异常集合:`rolled_back_at IS NOT NULL`、失败日志/未完成行或 applied ID 不在 candidate manifest。
- pending 集合:candidate manifest − applied 集合。
- 输出仅为计数、migration ID 和状态,不输出 `DATABASE_URL`、密码或原始日志。
同时运行 `npx prisma migrate status`。原始 stdout/stderr 只保留在受控执行现场;任务 artifacts 写入去除 datasource、数据库 URL、目标 origin/DB 值的脱敏副本,人工核对副本是否与集合比较一致。退出码不能替代原始输出。
### 3.3 Gate rules
- 异常集合非空 -> `PAUSED`。
- pending 集合不是 `EXPECTED_PENDING_MIGRATIONS` 的精确集合 -> `PAUSED`。
- candidate manifest 无法读取、status 输出无法人工解释或数据库连接目标不确定 -> `PAUSED`。
- 只读查询不得调用 `migrate deploy`、seed、reset、resolve、db push 或手工 SQL DDL。
## 4. 回滚窗口
预部署只读查询:
```sql
SELECT count(*) FROM contract_registry WHERE status = 'terminated';
```
- count = 0:`rollbackWindow=OPEN`;部署失败可在不 drop schema 的前提下回退到兼容应用。
- count > 0:`rollbackWindow=CLOSED`;旧应用回退被禁止,只允许 roll-forward/兼容 hotfix。
- 窗口关闭事件是第一条已提交 `terminated` 记录产生的瞬间,而非发布时间、迁移时间或人工计时。
- 本任务不执行终止写入,因此不会主动把窗口从 OPEN 变为 CLOSED。
## 5. 发布与只读验收
- 只用 `deploy-existing.sh`,固定 `SYNC_ENV=0` 与 `DEPLOY_DATA_MODE=existing`。
- wrapper 调用既有 `deploy.sh` 时设置 `DEPLOY_STATUS_OUTPUT_MODE=quiet`,避免其历史 status 检查把 datasource 原文打到部署终端;详细 pre/post 证据由受控 helper 生成。
- 部署脚本内部 `migrate deploy` 是唯一允许的 schema 写入;其后必须在受控现场保留 post-status 原始输出,并生成脱敏证据副本,确认全部 applied。
- `/ready` build SHA、容器 build、公开短 SHA和候选 commit 一致。
- 基础 smoke、external deps、runtime smoke、公开资源扫描及权限负向路径均只读;登录态/企微/真实 OCR 缺少批准凭据时记录 `PAUSED/SKIP`。
- 权限负向路径(未登录、不具备合同管理权限、详情不存在/无权)必须排在任何未来真实终止写入之前;当前任务没有该写入。
## 6. 失败分流与证据
| 阶段 | 失败处理 | 是否可重复部署 |
| --- | --- | --- |
| target/preflight/status | 停止,不同步/不写 DB | 修正输入后重新预检 |
| migrate deploy/init | 保留现场,禁止 reset/resolve/drop;按批准决定兼容回退或 roll-forward | 未确认现场前不可重复 |
| `/ready`/runtime smoke | 记录应用失败;窗口 OPEN 时按既有发布合同回退,否则 roll-forward | 需复核窗口状态 |
| backup refresh | 标记 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED`,不重复发布应用 | 只修复合同 |
| public/live/permission negative | 不宣称可测,记录 PAUSED/FAIL | 不自动重跑业务写入 |
Preflight 与 `deploy.sh` 之间要求远端配置/数据库身份变更冻结;若无法保证该窗口不可变,停止在 preflight 阶段。post-status 只能确认结果,不能补救并发期间写入错误目标的风险。
## 7. 证据与隐私
- 任务只保存 commit、状态码、计数、migration ID、报告引用和脱敏原因。
- 每次 pre/post 运行开始先清理报告目录中两个已知 evidence 文件,避免重试复用目录时旧 PASS 证据残留;不做目录级递归删除。
- 不保存 SSH target、用户名、绝对远端路径、数据库 URL、凭据、业务合同正文、终止原因或真实响应 payload。
- 生产 target gate 和 pre/post status 的原始输出若包含环境敏感内容,先做脱敏副本;原始日志留在受控执行环境,不进 Git。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,61 @@
# 生产部署验证与迁移门禁:实施计划
## 1. 规划与激活
- [x] 用户已批准本最终规划摘要,已执行 `task.py start`;未连接生产或执行生产 migration。
- [ ] 确认候选 commit、上游包含关系和工作区 clean;记录本次发布候选。
- [ ] 确认执行时的 `EXPECTED_PRODUCTION_ORIGIN`、`EXPECTED_PRODUCTION_DB_NAME`、`EXPECTED_PENDING_MIGRATIONS` 来自受控渠道,值不写入任务文件。
## 2. 部署 tooling 加固(仅必要时)
- [x] 新增只读 production target/migration preflight,由 `deploy-existing.sh` 在 `deploy.sh` 前调用。
- [x] preflight 验证生产 origin/DB identity(含运行时 `DATABASE_URL` host/DB)、candidate migration manifest、`_prisma_migrations` 集合和可解释的 `prisma migrate status` 输出;13 项既有部署体检仍由 wrapper 先执行。
- [x] 明确保证 pre-status 在 `migrate deploy`、seed、应用切换和业务写入之前;未知 pending、failed、rolled-back、diverged 或目标不明均停止。
- [x] 增加部署安全静态/fixture 测试,证明 target gate 不接受回环/本地目标,status 空/未知文案、日志失败和未知 pending fail-closed,报告不落原始 datasource,且 preflight 顺序早于 `deploy.sh`。
- [x] 更新 deployment spec/runbook 记录生产目标判据与首条 `terminated` 回滚窗口语义;不记录真实环境值。
- [x] 在应用部署成功、备份合同 refresh 之前,以空 pending allowlist 复用只读 preflight,生成独立 post-status evidence。
- [x] 严格 wrapper 调用既有 `deploy.sh` 时启用 `DEPLOY_STATUS_OUTPUT_MODE=quiet`,避免历史 status 检查把 datasource 原文回显;详情由受控 pre/post helper evidence 提供。
## 3. 本地门禁
- [x] `node deploy/trialrun/validate-env-contract.js`
- [x] `bash -n` 检查新增/修改的部署 shell(含 `production-preflight-remote.sh`)
- [x] 聚焦 deployment safety tests
- [x] `python3 ./.trellis/scripts/task.py validate 08-08-production-deployment-validation`
- [x] `node scripts/qa/check-process-contract.mjs --baseline --json`
- [x] `node scripts/qa/audit-docs.mjs --check-registry`(0 errors;保留仓库既有 1 条 unregistered docs warning)
- [x] `node scripts/qa/check-trellis-governance.mjs --json`
- [x] `node scripts/qa/check-sensitive-content.mjs`
- [x] `git diff --check`
## 4. 生产执行(需明确批准)
- [ ] 先运行 target gate 和 `check-existing-deployment.sh`;只保存脱敏 PASS/FAIL。(待单独生产批准)
- [ ] 先运行 candidate migration pre-status,确认运行时 `DATABASE_URL` 身份后在受控现场人工阅读原始输出,并核对脱敏副本、pending allowlist、异常集合和 `terminated` count;任一不满足即 `PAUSED`。
- [ ] 只有第 4.2 全部 PASS 后,才调用 `DEPLOY_SERVER=<...> DEPLOY_REFRESH_SERVER=<...> bash deploy/trialrun/deploy-existing.sh`,保持 `SYNC_ENV=0`/existing 数据合同。(待单独生产批准)
- [ ] 在受控现场保留 post-status 原始输出并保存脱敏副本,确认全部 migration applied/schema up to date;不能只看退出码。
- [ ] 验证 `/ready`、build SHA、公开 bundle、基础 smoke、external/runtime smoke 和权限负向路径;这些全部排在任何真实终止写入之前。
- [ ] 本任务不执行真实 `terminated` 写入、合同编辑/归档/恢复或任何付款/审批/企微写入。
## 5. 回滚与收口
- [ ] 记录 `rollbackWindow=OPEN|CLOSED` 及判据;第一条已提交 `terminated` 产生即关闭窗口。
- [ ] 应用/迁移失败按 design 分流,禁止 reset、resolve、drop;窗口 CLOSED 时只 roll-forward。
- [ ] 记录发布 commit、pre/post status、target gate、`/ready`、smoke、权限负向、PAUSED/SKIP 和业务写入计数。
- [ ] 只有部署、公网和获批真机检查全部通过时,才允许在最终答复中告知用户可以测试;否则明确 PAUSED。
- [ ] 归档任务并记录会话日志;生产部署不与本任务之外的功能开发混做。
## Rollback Points
- target/preflight/status 失败:尚未同步/写库,修正输入后重新预检。
- `migrate deploy`/init 中途失败:保留现场,禁止就地 reset/resolve/drop,等待确认。
- `/ready` 或 smoke 失败:窗口 OPEN 时按既有发布合同回退兼容应用;窗口 CLOSED 时 roll-forward。
- backup refresh 失败:标记已部署但合同刷新失败,不重复发布应用。
## Evidence Notes
- 规划阶段未执行生产 SSH、migration、部署、真实业务写入或 live smoke。
- 本地验证:`node --check deploy/trialrun/production-preflight.mjs`、相关部署 shell `bash -n`、`node deploy/trialrun/validate-env-contract.js` 均 PASS。
- 聚焦 Vitest:`server/tests/unit/scripts/deployment-safety-contract.test.js` 13 tests passed;包含受控 SSH 脱敏证据 fixture 和 synthetic Docker remote gate happy path。
- 任务/process/docs/governance/sensitive/diff 门禁均已执行并通过;`audit-docs` 仅报告仓库既有 unregistered docs warning,不属于本任务新增。
- 生产执行段全部保持未勾选:没有生产 SSH、`migrate deploy`、真实 `terminated` 写入、业务数据写入、公网 smoke 或真机验证;因此不得据此告知用户“可以测试”。
@@ -0,0 +1,102 @@
# 生产部署验证与迁移门禁
## Goal
在不写入真实业务数据的前提下,将包含合同 `terminated` 三列迁移的当前发布提交安全部署到预期生产环境,并用可追溯、脱敏的证据证明生产迁移状态、运行版本、只读权限边界和回滚窗口边界。
## Background and Confirmed Facts
- 当前既有生产发布入口是 `deploy/trialrun/deploy-existing.sh`;它会先调用 `check-existing-deployment.sh` 的 13 项聚合体检,再调用 `deploy.sh`,默认 `SYNC_ENV=0`。
- 当前 `deploy.sh` 在远端先执行 `prisma migrate deploy`(4.4),之后才执行 `prisma migrate status`(4.6)。这只能证明写入后的状态,不能作为本轮要求的部署前迁移门禁。
- 当前发布提交包含合同迁移 `20260807193000_contract_registry_termination_workspace`:三个可空列、`ON DELETE RESTRICT` 外键和索引;本轮不应执行任何真实 `terminated` 业务写入。
- 既有部署合同从受控 systemd unit 读取六个 allowlist 参数,13 项聚合体检覆盖部署身份、仓库、环境文件、数据目录、运行实例、挂载、build 和 refresh 身份;不得从 Compose 标签猜路径或选择 SSH 身份。
- 合同设计规定:生产出现第一条已提交 `terminated` 记录的瞬间关闭应用回滚窗口;此后不得回退到不识别该状态的旧应用,也不自动 drop 三列,只能 roll-forward。
- 本任务当前尚未执行生产部署、生产 migration、真实合同写入、企业微信写入或付款/审批操作。
## Scope
- 为既有生产部署入口增加/启用生产目标识别与迁移 preflight,使 `migrate status` 成为任何 schema 写入前的硬门禁。
- 在获批窗口内部署包含合同 `terminated` 三列迁移的明确候选 commit,并完成迁移、运行版本、只读权限和公网 readiness 验证。
- 记录首条 `terminated` 记录前后的回滚窗口判据、脱敏证据和失败分流;不主动制造任何真实终止记录。
## Requirements
### R1 任务来源与部署身份
- 发布候选必须是干净、已提交且已进入上游的明确 commit;来源校验和 13 项既有部署体检通过后才可继续。
- 本任务的 `deploy-existing.sh` 拒绝 `DEPLOY_ALLOW_UNPUBLISHED_COMMIT`/`DEPLOY_UNPUBLISHED_REASON` override;离线灾备或远端故障的未发布提交必须走独立、另行批准的恢复流程。
- 生产目标必须通过独立 target gate:执行时显式提供 `EXPECTED_PRODUCTION_ORIGIN` 与 `EXPECTED_PRODUCTION_DB_NAME`(仅进程环境,不写入仓库、日志或报告),与远端 `.env` 的 `APP_ORIGIN`、数据库身份逐项比对。
- target gate 必须确认 `NODE_ENV=production`、`COOKIE_SECURE=true`、公网 origin 为 HTTPS 且与预期值一致;拒绝回环地址、本地数据库(含 `hyb_oa_regression`)、占位符或 staging/trialrun 身份标识。不要把本地任务中“必须是回环地址”的 R2 复用于生产。
- 目标、账号、路径、数据库名和环境值不得回显;只记录 PASS/FAIL 及脱敏原因。
### R2 部署前迁移只读门禁
- 在任何 `migrate deploy`、seed、应用切换或真实业务写入之前,针对已确认身份的预期生产数据库执行只读 migration status 检查;原始 stdout/stderr 只留在受控执行现场,任务 artifacts 仅保存脱敏副本供人工核对。
- 预检必须明确区分 `applied`、pending、failed、rolled back、diverged;失败/回滚/分叉或无法人工解释的输出立即 `PAUSED`。
- 候选迁移目录与生产 `_prisma_migrations` 的已应用集合必须可对账;pending 集合只能是执行时显式批准的 allowlist(本轮预期最多为合同迁移),出现未知 pending 必须停止。
- 禁止 `prisma migrate dev`、`migrate reset`、`db push`、`migrate resolve`、手工 DDL、修改 `_prisma_migrations` 或以退出码代替原始输出核对。
### R3 受控发布
- 仅使用既有 `deploy-existing.sh`,保持 `SYNC_ENV=0`、`DEPLOY_DATA_MODE=existing` 和既有 PGDATA/附件目录;不得覆盖生产 `.env`。
- 预检通过后才允许部署脚本执行已批准的 `migrate deploy`、构建、服务切换、运行时 smoke 和备份合同 refresh。
- 本任务不创建、编辑、终止、归档或恢复真实合同,不写报销、付款、审批、银行、ERP 或企业微信数据。
### R4 发布后迁移与运行验证
- 部署后再次保存并人工核对 `migrate status` 的脱敏证据副本,必须显示所有候选迁移已 `applied`/数据库 schema up to date;原始 stdout/stderr 仍只留在受控执行现场,不能只看命令退出码。
- `/ready`、运行时完整 build SHA、公开短 SHA、基础 smoke、external deps、runtime smoke 和公开 bundle 扫描必须与发布 commit 一致。
- 所有权限负向路径、详情只读检查和迁移状态复核必须排在任何真实 `terminated` 写入之前;本任务不执行真实终止写入。缺少专用凭据时记录 `PAUSED/SKIP`,不伪装成通过。
### R5 回滚窗口与结果证据
- 部署前只读查询 `terminated` 行数量并记录窗口状态:数量为 0 记为 `OPEN`,大于 0 记为 `CLOSED`;窗口按首条已提交记录的产生时刻关闭,不按时间计算。
- 在窗口 `OPEN` 时,应用健康失败可按既有发布合同回退到兼容版本,但不 drop 新列;窗口一旦 `CLOSED`,任何失败只允许 roll-forward 或兼容 hotfix,不回退到旧状态机。
- 结果记录发布 commit、target gate、pre/post migration 原始证据引用、`/ready`/smoke、权限负向路径、窗口状态、可解释 SKIP/PAUSED 和是否发生真实写入;不记录凭据、原始环境或业务正文。
## Acceptance Criteria
- [ ] AC1:候选 commit 干净、已进入上游,来源校验与 13 项部署体检通过。
- [ ] AC2:生产 target gate 证明目标是预期生产 origin/数据库,明确排除本地、回环和 staging/trialrun 身份,且没有泄露环境值。
- [ ] AC3:任何 schema 写入前完成 pre-migration status;受控现场原始输出与 artifacts 脱敏副本均可核对,failed/rolled-back/diverged 为 0,`_prisma_migrations` 的 pending 集合与批准 allowlist 完全一致,status 文案中出现的 pending ID 不得超出候选 manifest。
- [ ] AC4:`deploy-existing.sh` 仅在 AC1–AC3 通过后执行,保持 `SYNC_ENV=0`/existing 数据合同,未执行 reset、db push、resolve、手工 DDL 或真实业务写入。
- [ ] AC5:部署后 migration status 脱敏证据副本显示全部候选迁移 applied/schema up to date;`/ready`、build SHA、运行时 smoke、external deps 和公开 bundle 与候选 commit 一致。
- [ ] AC6:权限负向路径和只读合同详情检查在任何真实终止写入前完成;本任务无真实 `terminated` 写入,缺凭据项明确记录 PAUSED/SKIP。
- [ ] AC7:以首条已提交 `terminated` 记录定义回滚窗口 OPEN/CLOSED,并记录兼容回滚或 roll-forward 判据;不自动 drop 三列。
- [ ] AC8:任务证据、Trellis/process/docs/sensitive/diff 门禁通过;只有部署、公网和必要真机检查全部获批并通过后,才可告知用户可以测试。
## Out of Scope
- 真实 `terminated` 写入、合同业务编辑/归档/恢复、报销/付款/审批/银行/ERP/企业微信写入。
- 生产数据库 reset、db push、resolve、手工 DDL、删除或修改 `_prisma_migrations`。
- 修改合同业务代码、权限模型、状态机或生产环境变量;如部署 preflight 需要工具加固,只改部署/QA 脚本与其测试。
- 以本地或试运行数据库代替预期生产目标;以临时 runner、删除断言或未授权凭据冒充公网通过。
## Accepted Risk and Deferred Items
- 登录态 smoke、企业微信真机和真实 OCR 样本可能因专用凭据/样本缺失而 `PAUSED/SKIP`;这不等同于生产部署或业务功能通过。
- 首条 `terminated` 写入后的旧版本回退风险由产品与合同 task 已接受;本任务只验证窗口判据,不主动关闭窗口。
- preflight 到 `deploy.sh` 的受控窗口内,远端 `.env`、Compose project 和数据库连接目标必须保持不变;并发改动无法由 post-status 追溯性补救。若无法取得该变更冻结,任务应在写入前 `PAUSED`,不得把 post gate 当作事后纠错。
## Operational Inputs (not committed)
- `EXPECTED_PRODUCTION_ORIGIN`
- `EXPECTED_PRODUCTION_DB_NAME`
- `EXPECTED_PENDING_MIGRATIONS`(本轮候选迁移 allowlist)
- 受控 `DEPLOY_SERVER`、`DEPLOY_REFRESH_SERVER` 与 live smoke 凭据(如获批提供)
## Open Questions
- 无;目标识别、迁移门禁顺序、回滚窗口定义和真实写入边界均已确认。
## Test Plan
- 本地静态:`node deploy/trialrun/validate-env-contract.js`、shell syntax、相关部署安全测试、task validate、process contract、docs registry、Trellis governance、sensitive scan、`git diff --check`。
- 部署前:生产 target gate、`check-existing-deployment.sh` 13 项体检、candidate migration manifest 对账、pre-status 原始输出(仅受控现场)及其脱敏副本和 `terminated` 计数。
- 受控发布:`DEPLOY_SERVER=<...> DEPLOY_REFRESH_SERVER=<...> bash deploy/trialrun/deploy-existing.sh`;仅在前置门禁全 PASS 后执行。
- 部署后:post-status 原始输出(仅受控现场)及脱敏副本、`/ready`/build SHA、`scripts/qa/run-live-smoke-readiness.sh`、external/runtime smoke、公开 bundle 扫描和权限负向路径。
- 失败分流:target/migration/preflight 失败不触碰远端应用;应用发布后 refresh 失败不重复发布;首条 `terminated` 出现后只允许 roll-forward。
## Evidence Notes
- 规划阶段未执行生产 SSH、migration、部署、真实数据写入或 live smoke。
@@ -0,0 +1,36 @@
{
"id": "production-deployment-validation",
"name": "production-deployment-validation",
"title": "生产部署验证与迁移门禁",
"description": "以迁移状态为第一道生产部署门禁,验证预期生产目标、合同 terminated 三列迁移、运行版本、只读权限边界与首条 terminated 记录后的回滚窗口;不执行真实终止或其他业务写入。",
"status": "in_progress",
"dev_type": null,
"scope": "deployment-production-migration-gate-and-readonly-verification",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-08-08",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"deploy/trialrun/deploy-existing.sh",
"deploy/trialrun/deploy.sh",
"deploy/trialrun/check-existing-deployment.sh",
"deploy/trialrun/deployment-contract-lib.sh",
"deploy/trialrun/validate-env-contract.js",
"deploy/trialrun/production-preflight.mjs",
"deploy/trialrun/production-preflight-remote.sh",
".trellis/spec/hyb-oa/deployment-trialrun.md",
"server/prisma/migrations/20260807193000_contract_registry_termination_workspace"
],
"notes": "部署前必须先确认预期生产目标与运行时 DATABASE_URL 身份,并在受控现场读取 migration status 原始输出;任务证据只保存脱敏副本。首条已提交 terminated 记录产生时关闭回滚窗口。生产部署、真实终止写入和企业微信/付款/审批写入均需分别受控;本 wrapper 不接受未发布提交 override。",
"meta": {}
}
+8 -1
查看文件
@@ -69,9 +69,13 @@
## B. 发布(Deploy)
- [ ] 本地工作树 clean,目标 commit 已进入 upstream;部署日志中的 commit 与 `/ready.data.buildSha` 一致
- [ ] **生产迁移先行门禁**:仅通过进程环境显式提供 `EXPECTED_PRODUCTION_ORIGIN`、`EXPECTED_PRODUCTION_DB_NAME`、`EXPECTED_PENDING_MIGRATIONS`;未把目标值写入仓库、日志或报告
- [ ] **生产 target gate**:已确认 HTTPS origin 与数据库身份均为预期生产目标,明确排除回环、本地回归库、staging/trialrun;不得套用本地任务“必须是回环地址”的判据
- [ ] **pre-status 在写入前**:运行时 `DATABASE_URL` host/DB 身份先通过;`prisma migrate status` 原始输出仅留受控现场、脱敏副本已保存并人工阅读;候选 manifest 与 `_prisma_migrations` 对账通过,pending 与批准 allowlist 精确一致,status 报出的 pending ID 均属于候选 manifest;failed/rolled-back/diverged/未知项为 0
- [ ] pre-status、target gate 或对账失败时保持 PAUSED,未进入 `deploy.sh`;未执行 `migrate dev`、reset、db push、resolve、手工 DDL、seed 或业务写入
- [ ] **EXTERNAL_GATED**:操作人已在边缘暂停 `/api/auth/forgot-password`、`/api/auth/reset-password`、`/api/auth/email-login/code`、`/api/auth/email-login/verify`;部署脚本不会自动修改边缘配置,未人工确认四端点关闭不得发布,配置预检、迁移、新应用和 smoke 全部通过后才人工恢复
- [ ] 标准路径已成功刷新 upstream;若因受控离线恢复使用 unpublished override,日志包含批准原因码和 `UNPUBLISHED-OVERRIDE` 标识
- [ ] 已有部署通过 `DEPLOY_SERVER=<deploy-ssh-target> DEPLOY_REFRESH_SERVER=<forced-command-ssh-target> bash deploy/trialrun/deploy-existing.sh` 执行;首次初始化才使用完整显式参数的 `deploy.sh`
- [ ] 已有部署通过 `deploy/trialrun/deploy-existing.sh` 执行;生产迁移已按 §2.0 提供三个 `EXPECTED_*` 进程变量;首次初始化才使用完整显式参数的 `deploy.sh`
- [ ] forced-command 使用独立专用 key,同一 key 不存在不受限 root 授权;root-owned target 只接受精确 `probe/refresh`,不调用部署用户可写仓库脚本
- [ ] 13 项聚合体检在任何 mkdir/rsync 前全部执行并汇总;报告只有固定检查名与 PASS/FAIL,任一失败均阻断同步
- [ ] 未从 Compose `working_dir` 猜仓库根目录;同步前 preflight 已确认仓库根标识、普通远端 `.env`、PG_VERSION 与附件目录,且该检查发生在 rsync 之前
@@ -97,6 +101,9 @@
- [ ] 登录验证已由 live smoke 与 release gate 覆盖;仅首次接入或第三方后台配置变更时补做人工确认
## C. 发布后(Post-check)
- [ ] wrapper 已在合同 refresh 前以空 pending allowlist 复跑只读 preflight,并保存/人工阅读部署后的第二份 `prisma migrate status` 脱敏副本(原始输出仅留受控现场);候选 migration 显示 applied/schema up to date,未只看退出码
- [ ] 权限负向路径、合同详情只读检查和 `/ready`/smoke 均在任何真实 `terminated` 写入前完成;本次没有主动终止写入
- [ ] 已记录 `rollbackWindow=OPEN|CLOSED`;首条已提交 `terminated` 记录产生时关闭,关闭后只允许 roll-forward/兼容 hotfix,不自动 drop 三列
- [ ] 公网 live smoke(只读,不做写操作):`bash scripts/qa/run-web-gates.sh smoke-live`
- [ ] 若本地已配置 `LIVE_SMOKE_*`,确认 `bash deploy/trialrun/deploy.sh` 输出中包含 live smoke 结果
- [ ] 已确认 release gate / live smoke 结果为通过;关键链路不再作为每次发布的手工必做项
+47 -2
查看文件
@@ -71,7 +71,7 @@ curl -fsS <shadow-ready-url>
- 反向代理:`caddy` 正在运行,`oa.hyinda.com` 应反代到 `.env` 中 `APP_HOST_PORT` 对应的宿主回环端口。
- 部署目录通过 `REMOTE_DIR` 显式传入。
- 远端目录当前不是 git 工作树;发布应优先从本地执行 `bash deploy/trialrun/deploy.sh`,由脚本 `rsync` 到服务器
- 已有部署优先执行 `DEPLOY_SERVER=<deploy-ssh-target> DEPLOY_REFRESH_SERVER=<forced-command-ssh-target> bash deploy/trialrun/deploy-existing.sh`;包装脚本只从受控 automatic-backup systemd unit 读取 allowlist 参数且不回显路径。两个 SSH target 是独立身份:前者按部署用户执行同步/Compose,后者只能执行 root-owned forced-command 的 `probe/refresh`。不得把 Compose `com.docker.compose.project.working_dir` 当仓库根目录,该标签可能指向 `deploy/trialrun` 配置目录。
- 已有部署优先执行 `deploy/trialrun/deploy-existing.sh`;涉及生产迁移时按 §2.0 额外提供三个 `EXPECTED_*` 进程变量。包装脚本只从受控 automatic-backup systemd unit 读取 allowlist 参数且不回显路径。两个 SSH target 是独立身份:前者按部署用户执行同步/Compose,后者只能执行 root-owned forced-command 的 `probe/refresh`。不得把 Compose `com.docker.compose.project.working_dir` 当仓库根目录,该标签可能指向 `deploy/trialrun` 配置目录。
- 远端容器编排当前实际为 `docker-compose 1.29.2`;本 runbook 命令以 `docker-compose` 为准
- 所有 Compose 命令必须显式使用同一个 `COMPOSE_PROJECT_NAME` 和 `-p`;不得依赖目录名推导项目身份,也不得写死容器名。
- 当前公网 `.env` 已确认:`APP_ORIGIN=https://oa.hyinda.com`、`COOKIE_SECURE=true`、`PUBLIC_SECURITY_HARDENING_ENABLED=true`
@@ -411,6 +411,51 @@ systemctl status hyb-oa-public-app-health.service --no-pager
建议版本化方式:
- 每次发布打 tag,例如 `trialrun-2026-02-08-01`
### 2.0 生产迁移先行门禁(08-08)
涉及生产 schema 迁移的既有部署,必须先使用 `deploy-existing.sh` 的只读
production preflight,再进入 `deploy.sh`。执行时由受控进程环境显式提供:
```bash
EXPECTED_PRODUCTION_ORIGIN=<approved-https-origin> \
EXPECTED_PRODUCTION_DB_NAME=<approved-production-db-name> \
EXPECTED_PENDING_MIGRATIONS=<approved-comma-separated-migration-ids-or-empty> \
DEPLOY_SERVER=<deploy-ssh-target> \
DEPLOY_REFRESH_SERVER=<forced-command-ssh-target> \
bash deploy/trialrun/deploy-existing.sh
```
该生产验证 wrapper 明确拒绝 `DEPLOY_ALLOW_UNPUBLISHED_COMMIT=1` 及其原因码;离线
灾备或远端故障恢复若需未发布提交,必须退出本门禁并走另行批准的恢复流程。
这里的生产目标判据与本地迁移检查相反:必须确认目标是预期生产 origin 和
数据库身份,且不是回环、本地回归库、staging 或 trialrun 身份。目标值只留在
进程环境和受控执行现场,不写入仓库、任务记录或输出;脚本只记录稳定的
PASS/FAIL code。
门禁顺序固定为:候选 commit/13 项既有部署体检 → target identity(含运行时
`DATABASE_URL` host/DB 校验)→
`prisma migrate status` 原始输出(只在受控现场保留、人工阅读其脱敏副本)→ `_prisma_migrations`
与候选 manifest 对账 → `terminated` 行计数 → `deploy.sh`。pending 集合必须
与 `EXPECTED_PENDING_MIGRATIONS` 精确一致;failed、rolled-back、diverged、
未知 migration 或无法解释的 status 立即 PAUSED。preflight 不执行
`migrate dev`、reset、db push、resolve、seed、手工 DDL 或业务写入。
从 preflight 到 `deploy.sh` 的执行窗口必须有远端 `.env`、Compose project 和数据库
身份变更冻结;无法保证冻结时应在写入前 PAUSED,不能把部署后的 status 当作并发
改目标的补救。
部署过程中 wrapper 以 `DEPLOY_STATUS_OUTPUT_MODE=quiet` 调用既有 `deploy.sh`,不把
历史 status 命令的 datasource 原文打到终端;部署完成后 wrapper 会在合同 refresh 前复用只读 preflight,并以空 pending allowlist
生成独立的 post artifact;仍须人工核对第二份 `prisma migrate status` 脱敏证据副本,确认
候选 migration 已 applied/schema up to date;原始输出只留在受控现场,退出码不能替代原始输出。权限负向
路径、合同详情只读检查和 `/ready`/smoke 必须排在任何真实终止写入之前。
回滚窗口按状态而非时间定义:部署前 `terminated` 行数为 0 时记为 `OPEN`;
第一条已提交 `terminated` 记录产生的瞬间关闭窗口。窗口关闭后不得回退到不认识
该状态的旧应用,也不自动 drop 三列,只能 roll-forward 或兼容 hotfix。本任务
不主动制造终止记录。
### 2.1 网页自动化门禁(R28)
发布前后统一使用三层网页自动化:
- `bash scripts/qa/run-web-gates.sh smoke-local`:本地/PR 快速门禁,只覆盖登录、Portal、导航、企微入口 UI。
@@ -424,7 +469,7 @@ systemctl status hyb-oa-public-app-health.service --no-pager
步骤:
1) 先做备份(见 E14 脚本)
2) 已有部署从本地仓库执行 `DEPLOY_SERVER=<deploy-ssh-target> DEPLOY_REFRESH_SERVER=<forced-command-ssh-target> bash deploy/trialrun/deploy-existing.sh`(推荐);它从受控 systemd unit 读取既有 `REMOTE_DIR`、env、Compose project 与数据目录合同,不从 Docker 标签猜路径,也不回显参数值。首次初始化或 unit 不存在时,才使用完整显式参数执行 `deploy.sh`。
2) 已有部署从本地仓库执行 `deploy/trialrun/deploy-existing.sh`(推荐;生产迁移按 §2.0 提供 target/pending 进程变量);它从受控 systemd unit 读取既有 `REMOTE_DIR`、env、Compose project 与数据目录合同,不从 Docker 标签猜路径,也不回显参数值。首次初始化或 unit 不存在时,才使用完整显式参数执行 `deploy.sh`。
- 正式使用新版入口前,root 在单独获批窗口执行 `DEPLOY_REFRESH_PUBLIC_KEY_FILE=<dedicated-public-key-file> bash deploy/trialrun/install-deployment-contract-refresh.sh install`。专用 key 不得同时存在不受限 root 授权;installer 会安装 root-owned 自包含 target 和 forced-command 行,但 installer 本身不进入免密授权。
- 新版入口先运行 `check-existing-deployment.sh` 的 13 项只读聚合体检。报告只含固定项名与 PASS/FAIL;任何失败都在 mkdir/rsync 前阻断,且前项失败不会跳过后续独立检查。
- `DEPLOY_REFRESH_SERVER` 必须是只携带专用 forced-command key 的 SSH alias。不得用 `sudo -n true` 代替该身份检查,也不得复用仍具有不受限 root 登录能力的同一 key。