修正(08-08-production-deployment-execution): 先验生产公网身份再冻结目录
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-08 19:36:32 +08:00
父节点 eb76de7c98
当前提交 bf2631c0d4
共修改 8 个文件,包含 151 行新增和 48 行删除
+12 -9
查看文件
@@ -29,30 +29,32 @@ operations require explicit approval and evidence.
### 2. Signatures ### 2. Signatures
- Bootstrap entry: `node deploy/trialrun/production-identity-bootstrap.mjs`. - Bootstrap entry: `node deploy/trialrun/production-identity-bootstrap.mjs`.
- Identity-only stop: `PRODUCTION_IDENTITY_ONLY=1 node deploy/trialrun/production-identity-bootstrap.mjs`.
- Read-only stop: `PRODUCTION_READ_ONLY_PREFLIGHT=1 node deploy/trialrun/production-identity-bootstrap.mjs`. - Read-only stop: `PRODUCTION_READ_ONLY_PREFLIGHT=1 node deploy/trialrun/production-identity-bootstrap.mjs`.
- Exact local inputs passed to the existing wrapper: `EXPECTED_DEPLOY_SERVER`, `EXPECTED_REMOTE_DIR`, `EXPECTED_COMPOSE_PROJECT_NAME`, `EXPECTED_PRODUCTION_ORIGIN` and `EXPECTED_PRODUCTION_DB_NAME_SHA256`. - Exact local inputs passed to the existing wrapper: `EXPECTED_DEPLOY_SERVER`, `EXPECTED_REMOTE_DIR`, `EXPECTED_COMPOSE_PROJECT_NAME`, `EXPECTED_PRODUCTION_ORIGIN` and `EXPECTED_PRODUCTION_DB_NAME_SHA256`.
### 3. Contracts ### 3. Contracts
- Recover deployment target, repository root and Compose project from reviewed Git history; when values come from different commits, require their shared identity fields to agree. - Recover deployment target and Compose project from reviewed Git history. A shell `${REMOTE_DIR:-...}` default that operations routinely override is a fallback, not proof of the production repository root.
- Select the refresh identity only when exactly one concrete SSH candidate returns the exact forced-command probe token. Do not print candidate aliases. - Select the refresh identity only when exactly one concrete SSH candidate returns the exact forced-command probe token. Do not print candidate aliases.
- Compare target, directory and project independently. Missing or empty expected values are errors, not reasons to skip a comparison. - Compare target and project against their independent history. Use a structurally validated systemd contract only to locate the remote env/runtime, verify both origin samples, then freeze its directory as the exact expected value for later preflight. Missing or empty expected values are errors, not reasons to skip a comparison.
- A legacy token such as `trialrun` is neutral. Continue to reject loopback/zero addresses, local regression, staging, shadow and placeholder identities. - A legacy token such as `trialrun` is neutral. Continue to reject loopback/zero addresses, local regression, staging, shadow and placeholder identities.
- Read `POSTGRES_DB` only after history, systemd contract and public origin agree. Keep the database name remote; pass only a hash comparison baseline to the preflight. - Identity-only mode must stop after env/runtime origin agreement and contract-directory freeze; it must not read `POSTGRES_DB`, run aggregate checks or enter Prisma status.
- Read `POSTGRES_DB` only after history target/project, systemd contract structure and both public-origin samples agree. Keep the database name remote; pass only a hash comparison baseline to the preflight.
- Read-only mode must exit after target/pre-status and rollback-window checks and before `deploy.sh`, rsync, migration or application switching. - Read-only mode must exit after target/pre-status and rollback-window checks and before `deploy.sh`, rsync, migration or application switching.
### 4. Validation & Error Matrix ### 4. Validation & Error Matrix
- Zero refresh probe matches -> `REFRESH_IDENTITY_NOT_FOUND`; stop before target gate. - Zero refresh probe matches -> `REFRESH_IDENTITY_NOT_FOUND`; stop before target gate.
- Multiple exact probe matches -> `REFRESH_IDENTITY_NOT_UNIQUE`; do not select the first candidate. - Multiple exact probe matches -> `REFRESH_IDENTITY_NOT_UNIQUE`; do not select the first candidate.
- Historical target/directory/project disagreement -> an explicit `HISTORICAL_*` or `SYSTEMD_*_MISMATCH`; stop before database identity read. - Historical target/project or contract-relative-path disagreement -> an explicit `HISTORICAL_*` or `SYSTEMD_*_MISMATCH`; stop before database identity read.
- Any expected exact value missing/empty -> `EXPECTED_*_MISSING`; no SSH preflight fallback. - Any expected exact value missing/empty -> `EXPECTED_*_MISSING`; no SSH preflight fallback.
- Env origin or running-container origin differs from the approved origin -> `PROD_TARGET_*_ORIGIN_MISMATCH`; do not read migration status. - Env origin or running-container origin differs from the approved origin -> `PROD_TARGET_*_ORIGIN_MISMATCH`; do not read migration status.
- Read-only mode reaches `deploy.sh` -> contract failure; tests and env validation must reject the ordering. - Read-only mode reaches `deploy.sh` -> contract failure; tests and env validation must reject the ordering.
### 5. Good/Base/Bad Cases ### 5. Good/Base/Bad Cases
- Good: a legacy-named deployment passes because all independent identity values match exactly, then read-only mode records status/window and exits before writes. - Good: a legacy-named deployment passes target/project checks, both origin samples match, the contract directory becomes the exact in-process baseline, then identity-only mode exits before DB/status reads.
- Base: an explicitly supplied, unambiguous production identity passes the same exact comparisons. - Base: an explicitly supplied, unambiguous production identity passes the same exact comparisons.
- Bad: reject or accept solely because the alias/project contains or omits `trialrun`. - Bad: reject or accept solely because the alias/project contains or omits `trialrun`.
- Bad: database name lookup fails and the wrapper continues without comparing database identity. - Bad: database name lookup fails and the wrapper continues without comparing database identity.
@@ -60,10 +62,10 @@ operations require explicit approval and evidence.
### 6. Tests Required ### 6. Tests Required
- Positive fixture for the approved legacy naming pattern. - Positive fixture for the approved legacy naming pattern.
- Separate negative assertions and stable error codes for deploy target, remote directory and Compose project mismatches. - Separate negative assertions and stable error codes for deploy target, Compose project, contract-relative env path and later preflight directory mismatches.
- Missing-value tests proving exact comparison cannot be skipped. - Missing-value tests proving exact comparison cannot be skipped.
- Zero/multiple refresh-probe fixtures. - Zero/multiple refresh-probe fixtures.
- Ordering assertion: identity/DB bootstrap -> target/pre-status/window -> read-only exit -> `deploy.sh` only when read-only mode is off. - Ordering assertion: contract structure -> env/runtime origin -> contract-directory freeze -> identity-only exit; only a later approved run may read DB -> target/pre-status/window -> read-only exit -> `deploy.sh` when read-only mode is off.
- Evidence assertions proving target/path/database values are absent from stdout and committed reports. - Evidence assertions proving target/path/database values are absent from stdout and committed reports.
### 7. Wrong vs Correct ### 7. Wrong vs Correct
@@ -77,8 +79,9 @@ name does not contain staging -> assume production -> run migrate deploy
#### Correct #### Correct
```text ```text
reviewed history + unique refresh probe + exact systemd contract + exact origin reviewed target/project + unique refresh probe + contract structure
-> remote-only DB identity hash -> target/pre-status/window -> env/runtime origin agreement -> freeze contract directory -> identity-only stop
-> separately approved remote-only DB identity hash -> target/pre-status/window
-> explicit read-only stop or separately approved deploy -> explicit read-only stop or separately approved deploy
``` ```
@@ -13,7 +13,8 @@
```text ```text
候选发布来源 候选发布来源
-> 历史身份恢复 + refresh exact probe(只读) -> 历史身份恢复 + refresh exact probe(只读)
-> systemd contract / 公网 origin 精确核对(只读) -> systemd contract 结构读取 + 公网 origin 双取样精确核对(只读)
-> origin PASS 后冻结 contract REMOTE_DIR 为进程基线(只读)
-> 远端 POSTGRES_DB 只读引导(同一进程、不回显) -> 远端 POSTGRES_DB 只读引导(同一进程、不回显)
-> 上游/工作区/13 项部署体检 -> 上游/工作区/13 项部署体检
-> 生产 target identity(只读) -> 生产 target identity(只读)
@@ -41,13 +42,13 @@
### 4.1 只读身份引导 ### 4.1 只读身份引导
1. 从已审查历史提交 `34181e3` 恢复既有 `DEPLOY_SERVER`、`REMOTE_DIR`,从首次显式冻结 Compose project 的提交 `5e2d03893826` 恢复 `COMPOSE_PROJECT_NAME`;两份历史记录的 `REMOTE_DIR` 必须一致,且不输出任何字面值。 1. 从已审查历史提交 `34181e3` 恢复既有 `DEPLOY_SERVER`,从首次显式冻结 Compose project 的提交 `5e2d03893826` 恢复 `COMPOSE_PROJECT_NAME`;不输出任何字面值。两份脚本里的 `REMOTE_DIR` 只是可被显式输入覆盖的 fallback,不作为生产目录基线。
2. 从本机受控 SSH 配置无回显探测 refresh 候选,只有 exact forced-command `probe` 唯一 PASS 时才采用;零个或多个候选均停止。 2. 从本机受控 SSH 配置无回显探测 refresh 候选,只有 exact forced-command `probe` 唯一 PASS 时才采用;零个或多个候选均停止。
3. 使用恢复的部署目标只读读取受控 systemd contract,并要求 contract 的远端目录、Compose project 及其余路径合同通过 13 项既有校验;历史身份与当前 contract 不一致即停止。 3. 使用恢复的部署目标只读读取受控 systemd contract,先校验六项存在、路径/Compose 格式、`REMOTE_ENV_FILE` 必须位于 contract `REMOTE_DIR` 的固定相对位置、Compose file 固定且 project 与历史基线一致;此时不把历史目录 fallback 与 contract 比较。
4. 从 OPS7 当前公网检查基准取得预期 HTTPS origin;只读比较远端 `.env` 和运行容器的 `APP_ORIGIN`,一致后才认为目标已被独立识别。 4. 从 OPS7 当前公网检查基准取得预期 HTTPS origin;以 contract 仅作为定位信息,只读比较远端 `.env` 和运行容器的 `APP_ORIGIN`。两个独立取样均一致后才确认目标主机,并把 contract `REMOTE_DIR` 冻结为当前进程 `EXPECTED_REMOTE_DIR`;origin 不一致时不得采信或输出该目录。
5. 仅在前四步通过后,从同一远端 `.env` 读取 `POSTGRES_DB` 并在远端转换为 SHA-256 比较基线;数据库名明文不离开远端、不打印、不写文件。该基线用于后续 env、`current_database()` 和运行时 `DATABASE_URL` 三方一致性核对。 5. 身份确认复核模式在第 4 步成功后立即停止,不读取数据库名、不进入 13 项体检或 Prisma。获得继续批准后,重新执行第 4 步,并从同一远端 `.env` 读取 `POSTGRES_DB`、在远端转换为 SHA-256 比较基线;数据库名明文不离开远端、不打印、不写文件。该基线用于后续 env、`current_database()` 和运行时 `DATABASE_URL` 三方一致性核对。
数据库名读取不是“连接任意目标后相信其自述”:它必须排在历史目标、远端 contract 和公网 origin 三层独立匹配之后。任一前置失败时不得读取或进入 Prisma status。 数据库名读取不是“连接任意目标后相信其自述”:它必须排在历史 target/project、contract 结构约束和公网 origin 双取样之后。任一前置失败时不得读取或进入 Prisma status。
### 4.2 完整 target/pre-status ### 4.2 完整 target/pre-status
@@ -21,12 +21,13 @@
## 1.1 拓扑校准与 tooling 修订(生产前) ## 1.1 拓扑校准与 tooling 修订(生产前)
- [ ] **刻意次序调整:**本轮 target gate 不再是第一条生产命令;历史身份恢复、refresh exact probe、systemd contract/公网 origin 核对、远端 `POSTGRES_DB` 只读引导四步探测排在其之前。四步全程只读、逐步 fail-closed,任一失败均不得进入 target gate 或 Prisma status。 - [ ] **刻意次序调整:**本轮 target gate 不再是第一条生产命令;历史身份恢复、refresh exact probe、systemd contract/公网 origin 核对、远端 `POSTGRES_DB` 只读引导四步探测排在其之前。四步全程只读、逐步 fail-closed,任一失败均不得进入 target gate 或 Prisma status。
- [x] 从 `34181e3` 只读恢复历史 `DEPLOY_SERVER`、`REMOTE_DIR`,从 `5e2d03893826` 恢复首次显式冻结的 `COMPOSE_PROJECT_NAME`,并要求两份历史记录的 `REMOTE_DIR` 一致;仅保存在当前进程,不把值写入 task、日志或对话。 - [x] 从 `34181e3` 只读恢复历史 `DEPLOY_SERVER`,从 `5e2d03893826` 恢复首次显式冻结的 `COMPOSE_PROJECT_NAME`;确认历史 `REMOTE_DIR` 只是未验证 fallback,不作为身份基线。值仅保存在当前进程,不写入 task、日志或对话。
- [x] 无回显探测 `DEPLOY_REFRESH_SERVER`,要求 exact forced-command `probe` 唯一 PASS;多个候选或零候选均 PAUSED。 - [x] 无回显探测 `DEPLOY_REFRESH_SERVER`,要求 exact forced-command `probe` 唯一 PASS;多个候选或零候选均 PAUSED。
- [x] 修改 production preflight:取消对合法 `trialrun` 命名的单独拒绝,增加部署目标、远端目录、Compose project、systemd contract、公网 origin 的精确身份对账;保留回环/零地址/local regression/staging/shadow/placeholder 防线。 - [x] 修改 production preflight:取消对合法 `trialrun` 命名的单独拒绝,增加部署目标、远端目录、Compose project、systemd contract、公网 origin 的精确身份对账;保留回环/零地址/local regression/staging/shadow/placeholder 防线。
- [x] 增加只读数据库身份 bootstrap:身份组匹配后读取远端 `.env` 的 `POSTGRES_DB`,在远端转换为 SHA-256 比较基线后仅在内存中传给 target gate;数据库名明文禁止离开远端、回显或落盘,读取失败或身份组不一致时不得进入 Prisma status。 - [x] 增加只读数据库身份 bootstrap:身份组匹配后读取远端 `.env` 的 `POSTGRES_DB`,在远端转换为 SHA-256 比较基线后仅在内存中传给 target gate;数据库名明文禁止离开远端、回显或落盘,读取失败或身份组不一致时不得进入 Prisma status。
- [x] 增加 synthetic fixture:合法 `hyb-oa-trialrun` 身份通过;错误目录/project/host/db、回环、staging、占位符和多 refresh 候选 fail-closed;断言 bootstrap 不泄露值且顺序早于 status。 - [x] 增加 synthetic fixture:合法 `hyb-oa-trialrun` 身份通过;错误目录/project/host/db、回环、staging、占位符和多 refresh 候选 fail-closed;断言 bootstrap 不泄露值且顺序早于 status。
- [x] 增加 `PRODUCTION_READ_ONLY_PREFLIGHT=1` 停止点:完成 target/pre-status 与回滚窗口后、进入 `deploy.sh` 前退出;只读证据复核通过前不得运行写入路径。 - [x] 增加 `PRODUCTION_READ_ONLY_PREFLIGHT=1` 停止点:完成 target/pre-status 与回滚窗口后、进入 `deploy.sh` 前退出;只读证据复核通过前不得运行写入路径。
- [x] 增加 `PRODUCTION_IDENTITY_ONLY=1` 停止点:以 contract 定位远端 `.env`/容器并完成 origin 双取样后,冻结 contract `REMOTE_DIR` 为进程内精确基线并立即退出;不得读取数据库名、运行 13 项体检或 Prisma。
- [ ] 重新运行部署安全测试、shell/Node syntax、task/governance/sensitive/diff 门禁,提交并推送新的候选后重新通过 provenance。 - [ ] 重新运行部署安全测试、shell/Node syntax、task/governance/sensitive/diff 门禁,提交并推送新的候选后重新通过 provenance。
## 2. 生产 preflight(全部只读) ## 2. 生产 preflight(全部只读)
@@ -10,6 +10,7 @@
- 生产部署入口是 `deploy/trialrun/deploy-existing.sh`。它读取受控 systemd deployment contract,固定 `SYNC_ENV=0`、`DEPLOY_DATA_MODE=existing`,并在 `deploy.sh` 前调用生产 target/pre-migration gate,部署后再做 post-status gate。 - 生产部署入口是 `deploy/trialrun/deploy-existing.sh`。它读取受控 systemd deployment contract,固定 `SYNC_ENV=0`、`DEPLOY_DATA_MODE=existing`,并在 `deploy.sh` 前调用生产 target/pre-migration gate,部署后再做 post-status gate。
- 当前没有独立预发环境:历史命名为 `hyb-oa-trialrun` 的既有部署就是承载公网 OA 的唯一运行环境。`trialrun` 是遗留命名,不代表另一套非生产实例;这次 schema 迁移无法先在同构预发环境演练。 - 当前没有独立预发环境:历史命名为 `hyb-oa-trialrun` 的既有部署就是承载公网 OA 的唯一运行环境。`trialrun` 是遗留命名,不代表另一套非生产实例;这次 schema 迁移无法先在同构预发环境演练。
- Git 历史中的 `REMOTE_DIR="${REMOTE_DIR:-...}"` 是未证明曾生效的 fallback,不是生产目录事实源。只有历史 SSH target 与 Compose project 继续作为独立基线;`REMOTE_DIR` 必须在远端 `.env` 和运行容器 origin 双取样一致后,从该生产主机的受控 systemd contract 冻结为当前进程精确基线。
- 生产 target gate 必须确认 HTTPS 公网 origin、`NODE_ENV=production`、`COOKIE_SECURE=true`、数据库身份和既有部署合同。不得再以名称中含 `trialrun` 作为拒绝依据;改为精确匹配已确认的部署身份组,并继续拒绝回环、本地回归库、staging、shadow、测试占位符等真实非生产目标。 - 生产 target gate 必须确认 HTTPS 公网 origin、`NODE_ENV=production`、`COOKIE_SECURE=true`、数据库身份和既有部署合同。不得再以名称中含 `trialrun` 作为拒绝依据;改为精确匹配已确认的部署身份组,并继续拒绝回环、本地回归库、staging、shadow、测试占位符等真实非生产目标。
- pre-status 的原始 stdout/stderr 只能留在受控执行现场;任务目录和报告只保存脱敏副本、稳定状态码、迁移 ID、计数和证据引用。 - pre-status 的原始 stdout/stderr 只能留在受控执行现场;任务目录和报告只保存脱敏副本、稳定状态码、迁移 ID、计数和证据引用。
- 候选发布必须是干净、已提交并已进入上游的 commit。规划阶段曾发现 `origin/main` 落后并已通过受控提交解决;严格 wrapper 仍拒绝未发布提交 override,任何规划修订都必须重新提交、推送并复核 provenance。 - 候选发布必须是干净、已提交并已进入上游的 commit。规划阶段曾发现 `origin/main` 落后并已通过受控提交解决;严格 wrapper 仍拒绝未发布提交 override,任何规划修订都必须重新提交、推送并复核 provenance。
@@ -29,10 +30,11 @@
### R1 发布来源与生产目标 ### R1 发布来源与生产目标
- 候选 commit 必须干净、已提交、已进入配置的上游远端;严格 wrapper 不接受 `DEPLOY_ALLOW_UNPUBLISHED_COMMIT` 或 `DEPLOY_UNPUBLISHED_REASON`。 - 候选 commit 必须干净、已提交、已进入配置的上游远端;严格 wrapper 不接受 `DEPLOY_ALLOW_UNPUBLISHED_COMMIT` 或 `DEPLOY_UNPUBLISHED_REASON`。
- 公网 origin 从当前 OPS7 运维交接 Runbook 的现行公网检查基准取得;部署 SSH 目标和远端目录从已审查历史提交 `34181e3` 恢复,Compose project 从首次显式冻结该值的历史提交 `5e2d03893826` 恢复,并要求两份历史记录的远端目录一致。三项只在执行进程内使用,不复制到任务文档或普通日志。 - 公网 origin 从当前 OPS7 运维交接 Runbook 的现行公网检查基准取得;部署 SSH 目标从已审查历史提交 `34181e3` 恢复,Compose project 从首次显式冻结该值的历史提交 `5e2d03893826` 恢复。历史 `REMOTE_DIR` fallback 不参与生产身份判断。
- `DEPLOY_REFRESH_SERVER` 通过本机受控 SSH 配置的 exact forced-command `probe` 唯一识别;零个或多个候选均立即停止,不按别名、顺序或连接速度猜测。 - `DEPLOY_REFRESH_SERVER` 通过本机受控 SSH 配置的 exact forced-command `probe` 唯一识别;零个或多个候选均立即停止,不按别名、顺序或连接速度猜测。
- 生产数据库名只允许在部署目标、远端目录、Compose project、受控 systemd contract 和公网 origin 已精确匹配后,通过一次获批的远端 `.env` 只读读取取得;值仅保留在同一执行进程内并转换为 target gate 所需的比较输入,不回显、不落盘。 - 使用 structurally valid 的 systemd contract 定位远端 `.env` 和运行容器,只读比较两个 `APP_ORIGIN` 取样;两者均与 OPS7 基准一致后,才确认该主机承载生产 origin,并把 contract 的 `REMOTE_DIR` 冻结为当前进程预期值。origin 不一致立即停止,不得采信 contract 目录。
- target gate 必须精确验证 `DEPLOY_SERVER`、`REMOTE_DIR`、`COMPOSE_PROJECT_NAME`、公网 origin 与读取后的 `POSTGRES_DB` 身份组,并继续核对运行时 `DATABASE_URL` host/database、当前数据库、`NODE_ENV` 和 cookie 安全配置;任一身份不确定均 fail-closed。 - 生产数据库名只允许在部署目标、Compose project、受控 systemd contract 和公网 origin 已确认后,通过一次获批的远端 `.env` 只读读取取得;值仅保留在远端并转换为 target gate 所需的比较输入,不回显、不落盘。
- target gate 必须精确验证 `DEPLOY_SERVER`、origin 验证后冻结的 `REMOTE_DIR`、`COMPOSE_PROJECT_NAME`、公网 origin 与读取后的 `POSTGRES_DB` 身份组,并继续核对运行时 `DATABASE_URL` host/database、当前数据库、`NODE_ENV` 和 cookie 安全配置;任一身份不确定均 fail-closed。
- `EXPECTED_PENDING_MIGRATIONS` 可先以空集合运行只读 pre-status 发现实际 pending;只要实际集合非空或不等于批准 allowlist,就必须停止并逐条复核 SQL,不能自动进入部署。 - `EXPECTED_PENDING_MIGRATIONS` 可先以空集合运行只读 pre-status 发现实际 pending;只要实际集合非空或不等于批准 allowlist,就必须停止并逐条复核 SQL,不能自动进入部署。
### R2 pre-status 与回滚窗口先行 ### R2 pre-status 与回滚窗口先行
@@ -63,7 +65,7 @@
## Acceptance Criteria ## Acceptance Criteria
- [ ] AC1:候选 commit 已提交、工作区 clean、已进入上游;来源校验和既有 13 项体检均 PASS。若候选未发布,任务在此处 PAUSED,不使用 override。 - [ ] AC1:候选 commit 已提交、工作区 clean、已进入上游;来源校验和既有 13 项体检均 PASS。若候选未发布,任务在此处 PAUSED,不使用 override。
- [ ] AC2:只读身份引导证明历史部署目标、远端目录、Compose project、受控 systemd contract 与公网 origin 属于同一唯一运行环境;数据库名只在该前提下读取到同一进程。target gate 的原始输出在受控现场可供人工阅读,脱敏副本只证明 PASS/FAIL 和原因,未泄露任何身份值。 - [ ] 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` 判定已记录。 - [ ] 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` 写入。 - [ ] 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 一致。 - [ ] AC5:部署后 status 显示全部候选迁移 applied/schema up to date,`/ready`、build SHA、smoke、external/runtime smoke、bundle scan 结果与候选 commit 一致。
@@ -90,6 +92,7 @@
## Operational Inputs (not committed) ## Operational Inputs (not committed)
- `EXPECTED_PRODUCTION_ORIGIN`(从现行 OPS7 公网基准在进程内取得) - `EXPECTED_PRODUCTION_ORIGIN`(从现行 OPS7 公网基准在进程内取得)
- `PRODUCTION_IDENTITY_ONLY=1`(只完成 origin 双取样与 contract 目录基线冻结,随后在数据库读取和 pre-status 前退出)
- `EXPECTED_PRODUCTION_DB_NAME_SHA256`(仅在精确身份引导通过后,于远端从 `.env` 读取数据库名并转换为不含明文的比较基线;数据库名明文本身不离开远端,也不进入日志或证据文件) - `EXPECTED_PRODUCTION_DB_NAME_SHA256`(仅在精确身份引导通过后,于远端从 `.env` 读取数据库名并转换为不含明文的比较基线;数据库名明文本身不离开远端,也不进入日志或证据文件)
- `EXPECTED_PENDING_MIGRATIONS` - `EXPECTED_PENDING_MIGRATIONS`
- 受控 `DEPLOY_SERVER`(从已审查历史入口恢复)、`DEPLOY_REFRESH_SERVER`(exact probe 唯一识别)、systemd contract 与 live smoke 凭据(如获批提供) - 受控 `DEPLOY_SERVER`(从已审查历史入口恢复)、`DEPLOY_REFRESH_SERVER`(exact probe 唯一识别)、systemd contract 与 live smoke 凭据(如获批提供)
@@ -9,11 +9,16 @@ fail() {
exit 1 exit 1
} }
for key in REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE COMPOSE_PROJECT_NAME \ for key in IDENTITY_BOOTSTRAP_MODE REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE \
EXPECTED_PRODUCTION_ORIGIN_SHA256; do COMPOSE_PROJECT_NAME EXPECTED_PRODUCTION_ORIGIN_SHA256; do
[[ -n "${!key:-}" ]] || fail "${key}_MISSING" [[ -n "${!key:-}" ]] || fail "${key}_MISSING"
done done
case "$IDENTITY_BOOTSTRAP_MODE" in
origin|database) ;;
*) fail "IDENTITY_BOOTSTRAP_MODE_INVALID" ;;
esac
[[ "$EXPECTED_PRODUCTION_ORIGIN_SHA256" =~ ^[0-9a-f]{64}$ ]] \ [[ "$EXPECTED_PRODUCTION_ORIGIN_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|| fail "PROD_TARGET_ORIGIN_HASH_INVALID" || fail "PROD_TARGET_ORIGIN_HASH_INVALID"
cd "$REMOTE_DIR" cd "$REMOTE_DIR"
@@ -62,11 +67,8 @@ reject_non_production_identity() {
} }
actual_origin="$(dotenv_value APP_ORIGIN)" || fail "PROD_TARGET_ORIGIN_MISSING" actual_origin="$(dotenv_value APP_ORIGIN)" || fail "PROD_TARGET_ORIGIN_MISSING"
actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING"
[[ "$(hash_text "$actual_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \ [[ "$(hash_text "$actual_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|| fail "PROD_TARGET_ORIGIN_MISMATCH" || fail "PROD_TARGET_ORIGIN_MISMATCH"
reject_non_production_identity "$actual_db_name" \
|| fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY"
if docker compose version >/dev/null 2>&1; then if docker compose version >/dev/null 2>&1; then
compose_cmd=(docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE") compose_cmd=(docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE")
@@ -90,6 +92,14 @@ runtime_origin="$(
[[ "$(hash_text "$runtime_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \ [[ "$(hash_text "$runtime_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|| fail "PROD_TARGET_RUNTIME_ORIGIN_MISMATCH" || fail "PROD_TARGET_RUNTIME_ORIGIN_MISMATCH"
if [[ "$IDENTITY_BOOTSTRAP_MODE" == "origin" ]]; then
printf '[production-identity-bootstrap][PASS] origin_identity\n'
exit 0
fi
# Only the comparison baseline crosses the SSH channel. The database name # Only the comparison baseline crosses the SSH channel. The database name
# itself remains remote and cannot be reconstructed from task evidence. # itself remains remote and cannot be reconstructed from task evidence.
actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING"
reject_non_production_identity "$actual_db_name" \
|| fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY"
printf '[production-identity-bootstrap][DB_SHA256] %s\n' "$(hash_text "$actual_db_name")" printf '[production-identity-bootstrap][DB_SHA256] %s\n' "$(hash_text "$actual_db_name")"
@@ -60,14 +60,10 @@ function parseHistoricalDefault(source, key) {
export function parseHistoricalDeploymentIdentity(targetSource, projectSource) { export function parseHistoricalDeploymentIdentity(targetSource, projectSource) {
const deployServer = parseHistoricalDefault(targetSource, "DEPLOY_SERVER"); const deployServer = parseHistoricalDefault(targetSource, "DEPLOY_SERVER");
const remoteDir = parseHistoricalDefault(targetSource, "REMOTE_DIR");
const projectRemoteDir = parseHistoricalDefault(projectSource, "REMOTE_DIR");
const composeProjectName = parseHistoricalDefault(projectSource, "COMPOSE_PROJECT_NAME"); const composeProjectName = parseHistoricalDefault(projectSource, "COMPOSE_PROJECT_NAME");
if (projectRemoteDir !== remoteDir) fail("HISTORICAL_REMOTE_DIR_DIVERGED");
if (!SAFE_SSH_TARGET_PATTERN.test(deployServer)) fail("HISTORICAL_DEPLOY_SERVER_INVALID"); if (!SAFE_SSH_TARGET_PATTERN.test(deployServer)) fail("HISTORICAL_DEPLOY_SERVER_INVALID");
if (!SAFE_ABSOLUTE_PATH_PATTERN.test(remoteDir)) fail("HISTORICAL_REMOTE_DIR_INVALID");
if (!SAFE_PROJECT_PATTERN.test(composeProjectName)) fail("HISTORICAL_COMPOSE_PROJECT_INVALID"); if (!SAFE_PROJECT_PATTERN.test(composeProjectName)) fail("HISTORICAL_COMPOSE_PROJECT_INVALID");
return { deployServer, remoteDir, composeProjectName }; return { deployServer, composeProjectName };
} }
export function parseExpectedProductionOrigin(runbook) { export function parseExpectedProductionOrigin(runbook) {
@@ -145,9 +141,8 @@ export function parseDeploymentContract(raw) {
return contract; return contract;
} }
export function assertExactHistoricalContract(identity, contract) { export function assertContractForOriginVerification(identity, contract) {
if (contract.REMOTE_DIR !== identity.remoteDir) fail("SYSTEMD_REMOTE_DIR_MISMATCH"); if (contract.REMOTE_ENV_FILE !== `${contract.REMOTE_DIR}/deploy/trialrun/.env`) {
if (contract.REMOTE_ENV_FILE !== `${identity.remoteDir}/deploy/trialrun/.env`) {
fail("SYSTEMD_REMOTE_ENV_FILE_MISMATCH"); fail("SYSTEMD_REMOTE_ENV_FILE_MISMATCH");
} }
if (contract.COMPOSE_FILE !== "deploy/trialrun/docker-compose.yml") { if (contract.COMPOSE_FILE !== "deploy/trialrun/docker-compose.yml") {
@@ -166,8 +161,9 @@ function shellQuote(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`; return `'${String(value).replaceAll("'", "'\\''")}'`;
} }
function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) { function runRemoteIdentityBootstrap(mode, identity, contract, expectedOrigin) {
const remoteCommand = [ const remoteCommand = [
`IDENTITY_BOOTSTRAP_MODE=${shellQuote(mode)}`,
`REMOTE_DIR=${shellQuote(contract.REMOTE_DIR)}`, `REMOTE_DIR=${shellQuote(contract.REMOTE_DIR)}`,
`REMOTE_ENV_FILE=${shellQuote(contract.REMOTE_ENV_FILE)}`, `REMOTE_ENV_FILE=${shellQuote(contract.REMOTE_ENV_FILE)}`,
`COMPOSE_FILE=${shellQuote(contract.COMPOSE_FILE)}`, `COMPOSE_FILE=${shellQuote(contract.COMPOSE_FILE)}`,
@@ -181,7 +177,18 @@ function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
const code = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/code=([A-Z0-9_]+)/u)?.[1]; const code = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/code=([A-Z0-9_]+)/u)?.[1];
fail(code || "REMOTE_IDENTITY_BOOTSTRAP_FAILED"); fail(code || "REMOTE_IDENTITY_BOOTSTRAP_FAILED");
} }
const output = String(result.stdout ?? "").trim(); return String(result.stdout ?? "").trim();
}
function verifyOriginIdentity(identity, contract, expectedOrigin) {
const output = runRemoteIdentityBootstrap("origin", identity, contract, expectedOrigin);
if (output !== "[production-identity-bootstrap][PASS] origin_identity") {
fail("REMOTE_ORIGIN_IDENTITY_PAYLOAD_INVALID");
}
}
function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
const output = runRemoteIdentityBootstrap("database", identity, contract, expectedOrigin);
const match = output.match(/^\[production-identity-bootstrap\]\[DB_SHA256\] ([0-9a-f]{64})$/u); const match = output.match(/^\[production-identity-bootstrap\]\[DB_SHA256\] ([0-9a-f]{64})$/u);
if (!match) fail("REMOTE_DB_IDENTITY_PAYLOAD_INVALID"); if (!match) fail("REMOTE_DB_IDENTITY_PAYLOAD_INVALID");
return match[1]; return match[1];
@@ -189,6 +196,8 @@ function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
function main() { function main() {
try { try {
const identityOnly = process.env.PRODUCTION_IDENTITY_ONLY ?? "0";
if (!["0", "1"].includes(identityOnly)) fail("PRODUCTION_IDENTITY_ONLY_INVALID");
const historicalTargetScript = requiredCommandOutput( const historicalTargetScript = requiredCommandOutput(
"git", "git",
["show", `${HISTORICAL_TARGET_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`], ["show", `${HISTORICAL_TARGET_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`],
@@ -208,7 +217,14 @@ function main() {
"SYSTEMD_CONTRACT_UNAVAILABLE", "SYSTEMD_CONTRACT_UNAVAILABLE",
); );
const contract = parseDeploymentContract(contractRaw); const contract = parseDeploymentContract(contractRaw);
assertExactHistoricalContract(identity, contract); assertContractForOriginVerification(identity, contract);
verifyOriginIdentity(identity, contract, expectedOrigin);
const expectedRemoteDir = contract.REMOTE_DIR;
if (identityOnly === "1") {
console.log("[production-identity-bootstrap][PASS] origin_verified_remote_dir_baseline=systemd_contract");
return;
}
const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin); const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin);
console.log("[production-identity-bootstrap][PASS] exact_identity_group"); console.log("[production-identity-bootstrap][PASS] exact_identity_group");
@@ -220,7 +236,7 @@ function main() {
DEPLOY_SERVER: identity.deployServer, DEPLOY_SERVER: identity.deployServer,
DEPLOY_REFRESH_SERVER: refreshTarget, DEPLOY_REFRESH_SERVER: refreshTarget,
EXPECTED_DEPLOY_SERVER: identity.deployServer, EXPECTED_DEPLOY_SERVER: identity.deployServer,
EXPECTED_REMOTE_DIR: identity.remoteDir, EXPECTED_REMOTE_DIR: expectedRemoteDir,
EXPECTED_COMPOSE_PROJECT_NAME: identity.composeProjectName, EXPECTED_COMPOSE_PROJECT_NAME: identity.composeProjectName,
EXPECTED_PRODUCTION_ORIGIN: expectedOrigin, EXPECTED_PRODUCTION_ORIGIN: expectedOrigin,
EXPECTED_PRODUCTION_DB_NAME_SHA256: databaseIdentityHash, EXPECTED_PRODUCTION_DB_NAME_SHA256: databaseIdentityHash,
+13 -1
查看文件
@@ -453,6 +453,14 @@ for (const requiredFragment of [
errors.push(`install-warch3-ops-timer.sh 缺少活动部署环境绑定: ${requiredFragment}`); errors.push(`install-warch3-ops-timer.sh 缺少活动部署环境绑定: ${requiredFragment}`);
} }
} }
if (productionIdentityBootstrapScriptContent.indexOf('if (identityOnly === "1")')
> productionIdentityBootstrapScriptContent.indexOf('const databaseIdentityHash = bootstrapDatabaseIdentityHash')) {
errors.push('production-identity-bootstrap.mjs 的 identity-only 停止点必须位于数据库身份读取前');
}
if (productionIdentityBootstrapScriptContent.indexOf('verifyOriginIdentity(identity, contract, expectedOrigin);')
> productionIdentityBootstrapScriptContent.indexOf('const expectedRemoteDir = contract.REMOTE_DIR')) {
errors.push('production-identity-bootstrap.mjs 必须在 origin 双取样后冻结 contract REMOTE_DIR');
}
for (const requiredFragment of [ for (const requiredFragment of [
'MODE="${1:-install}"', 'MODE="${1:-install}"',
@@ -634,8 +642,10 @@ for (const requiredFragment of [
'HISTORICAL_TARGET_COMMIT', 'HISTORICAL_TARGET_COMMIT',
'HISTORICAL_PROJECT_COMMIT', 'HISTORICAL_PROJECT_COMMIT',
'discoverRefreshTarget', 'discoverRefreshTarget',
'assertExactHistoricalContract', 'assertContractForOriginVerification',
'verifyOriginIdentity',
'bootstrapDatabaseIdentityHash', 'bootstrapDatabaseIdentityHash',
'PRODUCTION_IDENTITY_ONLY',
'EXPECTED_PRODUCTION_DB_NAME_SHA256', 'EXPECTED_PRODUCTION_DB_NAME_SHA256',
'DEPLOY_EXISTING_SCRIPT', 'DEPLOY_EXISTING_SCRIPT',
]) { ]) {
@@ -647,6 +657,8 @@ for (const requiredFragment of [
'EXPECTED_PRODUCTION_ORIGIN_SHA256', 'EXPECTED_PRODUCTION_ORIGIN_SHA256',
'PROD_TARGET_RUNTIME_ORIGIN_MISMATCH', 'PROD_TARGET_RUNTIME_ORIGIN_MISMATCH',
'PROD_TARGET_DB_NON_PRODUCTION_IDENTITY', 'PROD_TARGET_DB_NON_PRODUCTION_IDENTITY',
'origin_identity',
'IDENTITY_BOOTSTRAP_MODE',
'DB_SHA256', 'DB_SHA256',
]) { ]) {
if (!productionIdentityBootstrapRemoteScriptContent.includes(requiredFragment)) { if (!productionIdentityBootstrapRemoteScriptContent.includes(requiredFragment)) {
@@ -138,10 +138,19 @@ describe("deployment safety contract", () => {
expect(remote).toContain("rollbackWindow=OPEN"); expect(remote).toContain("rollbackWindow=OPEN");
expect(remote).not.toMatch(/\b(prisma migrate reset|prisma db push|prisma migrate resolve)\b/); expect(remote).not.toMatch(/\b(prisma migrate reset|prisma db push|prisma migrate resolve)\b/);
expect(remote.indexOf("PRISMA_STATUS_BEGIN")).toBeLessThan(remote.indexOf("_prisma_migrations")); expect(remote.indexOf("PRISMA_STATUS_BEGIN")).toBeLessThan(remote.indexOf("_prisma_migrations"));
expect(bootstrap.indexOf("assertExactHistoricalContract")).toBeLessThan( expect(bootstrap.indexOf("assertContractForOriginVerification")).toBeLessThan(
bootstrap.indexOf("bootstrapDatabaseIdentityHash"), bootstrap.indexOf("verifyOriginIdentity(identity, contract, expectedOrigin);"),
); );
expect(bootstrap.indexOf("bootstrapDatabaseIdentityHash(identity")).toBeLessThan( expect(bootstrap.indexOf("verifyOriginIdentity(identity, contract, expectedOrigin);")).toBeLessThan(
bootstrap.indexOf("const expectedRemoteDir = contract.REMOTE_DIR"),
);
expect(bootstrap.indexOf("const expectedRemoteDir = contract.REMOTE_DIR")).toBeLessThan(
bootstrap.indexOf("const databaseIdentityHash = bootstrapDatabaseIdentityHash"),
);
expect(bootstrap.indexOf('if (identityOnly === "1")')).toBeLessThan(
bootstrap.indexOf("const databaseIdentityHash = bootstrapDatabaseIdentityHash"),
);
expect(bootstrap.indexOf("const databaseIdentityHash = bootstrapDatabaseIdentityHash")).toBeLessThan(
bootstrap.indexOf("spawnSync(\"bash\", [DEPLOY_EXISTING_SCRIPT]"), bootstrap.indexOf("spawnSync(\"bash\", [DEPLOY_EXISTING_SCRIPT]"),
); );
}); });
@@ -296,7 +305,6 @@ describe("deployment safety contract", () => {
].join("\n"); ].join("\n");
expect(bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject)).toEqual({ expect(bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject)).toEqual({
deployServer: "deploy.production.invalid", deployServer: "deploy.production.invalid",
remoteDir: "/srv/hyb-oa-trialrun",
composeProjectName: "hyb-oa-trialrun", composeProjectName: "hyb-oa-trialrun",
}); });
expect(bootstrap.selectUniqueRefreshTarget([ expect(bootstrap.selectUniqueRefreshTarget([
@@ -319,18 +327,27 @@ describe("deployment safety contract", () => {
"POSTGRES_DATA_DIR=/srv/data/postgres", "POSTGRES_DATA_DIR=/srv/data/postgres",
"STORAGE_HOST_DIR=/srv/data/storage", "STORAGE_HOST_DIR=/srv/data/storage",
].join(" ")); ].join(" "));
expect(() => bootstrap.assertExactHistoricalContract( expect(() => bootstrap.assertContractForOriginVerification(
bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject), bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject),
contract, contract,
)).not.toThrow(); )).not.toThrow();
expect(() => bootstrap.assertExactHistoricalContract( const relocatedContract = {
...contract,
REMOTE_DIR: "/srv/current-production",
REMOTE_ENV_FILE: "/srv/current-production/deploy/trialrun/.env",
};
expect(() => bootstrap.assertContractForOriginVerification(
bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject),
relocatedContract,
)).not.toThrow();
expect(() => bootstrap.assertContractForOriginVerification(
bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject), bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject),
{ ...contract, COMPOSE_PROJECT_NAME: "different" }, { ...contract, COMPOSE_PROJECT_NAME: "different" },
)).toThrowError(expect.objectContaining({ code: "SYSTEMD_COMPOSE_PROJECT_MISMATCH" })); )).toThrowError(expect.objectContaining({ code: "SYSTEMD_COMPOSE_PROJECT_MISMATCH" }));
expect(() => bootstrap.parseHistoricalDeploymentIdentity( expect(() => bootstrap.assertContractForOriginVerification(
historicalTarget, bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject),
historicalProject.replace("/srv/hyb-oa-trialrun", "/srv/different"), { ...contract, REMOTE_ENV_FILE: "/srv/different/.env" },
)).toThrowError(expect.objectContaining({ code: "HISTORICAL_REMOTE_DIR_DIVERGED" })); )).toThrowError(expect.objectContaining({ code: "SYSTEMD_REMOTE_ENV_FILE_MISMATCH" }));
const bootstrapSource = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP, "utf8"); const bootstrapSource = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP, "utf8");
const remoteSource = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP_REMOTE, "utf8"); const remoteSource = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP_REMOTE, "utf8");
@@ -339,6 +356,46 @@ describe("deployment safety contract", () => {
expect(remoteSource).toContain("DB_SHA256"); expect(remoteSource).toContain("DB_SHA256");
expect(remoteSource).not.toContain("DB_BASE64"); expect(remoteSource).not.toContain("DB_BASE64");
expect(remoteSource).not.toContain("prisma migrate status"); expect(remoteSource).not.toContain("prisma migrate status");
expect(remoteSource.indexOf('IDENTITY_BOOTSTRAP_MODE" == "origin"')).toBeLessThan(
remoteSource.indexOf("actual_db_name="),
);
});
it("verifies both origin samples before reading the remote database name", async () => {
const fakeBin = path.join(fixtureRoot, "origin-bin");
await mkdir(fakeBin, { recursive: true });
await writeFile(path.join(fakeBin, "docker"), [
"#!/usr/bin/env bash",
"set -euo pipefail",
"joined=\"$*\"",
"if [[ \"$1\" == compose && \"${2:-}\" == version ]]; then exit 0; fi",
"if [[ \"$1\" == ps ]]; then printf 'server1\\n'; exit 0; fi",
"if [[ \"$1\" == inspect ]]; then printf 'APP_ORIGIN=https://example.invalid\\n'; exit 0; fi",
"printf 'unexpected fake docker call: %s\\n' \"$joined\" >&2",
"exit 97",
"",
].join("\n"), { mode: 0o700 });
await chmod(path.join(fakeBin, "docker"), 0o700);
const hash = createHash("sha256").update("https://example.invalid", "utf8").digest("hex");
const runBootstrapRemote = (mode) => execFileAsync("bash", [PRODUCTION_IDENTITY_BOOTSTRAP_REMOTE], {
cwd: remoteDir,
env: {
...process.env,
PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`,
IDENTITY_BOOTSTRAP_MODE: mode,
REMOTE_DIR: remoteDir,
REMOTE_ENV_FILE: envFile,
COMPOSE_FILE: "deploy/trialrun/docker-compose.yml",
COMPOSE_PROJECT_NAME: "hyb-oa-trialrun",
EXPECTED_PRODUCTION_ORIGIN_SHA256: hash,
},
});
await expect(runBootstrapRemote("origin")).resolves.toMatchObject({
stdout: "[production-identity-bootstrap][PASS] origin_identity\n",
});
await expect(runBootstrapRemote("database")).rejects.toMatchObject({
stderr: expect.stringContaining("PROD_TARGET_DB_NAME_MISSING"),
});
}); });
it("runs the remote gate against synthetic status and migration history", async () => { it("runs the remote gate against synthetic status and migration history", async () => {