补强生产数据路径与备份来源护栏(07-26-internal-ops-recovery-baseline)
ci-e2e / e2e (push) Waiting to run
ci-minimal / check-minimal (push) Waiting to run

这个提交包含在:
2026-07-27 06:37:47 +08:00
父节点 23342035ee
当前提交 64ed844bfe
共修改 14 个文件,包含 256 行新增和 34 行删除
+59 -12
查看文件
@@ -7,8 +7,7 @@ operations require explicit approval and evidence.
## Rules
- Use `SYNC_ENV=0 bash deploy/trialrun/deploy.sh` only when deployment is
approved; do not overwrite remote `.env`.
- Use `SYNC_ENV=0 POSTGRES_DATA_DIR=<absolute> STORAGE_HOST_DIR=<absolute> DEPLOY_DATA_MODE=<existing|initialize> bash deploy/trialrun/deploy.sh` only when deployment is approved; do not overwrite remote `.env`.
- When `server/src/config.js` reads a new environment key, add the same key to
`deploy/trialrun/.env.example` before deployment; `deploy/trialrun/deploy.sh`
runs `node deploy/trialrun/validate-env-contract.js` at step 0 and must fail
@@ -31,7 +30,8 @@ operations require explicit approval and evidence.
### 2. Signatures
- Command: `node deploy/trialrun/validate-env-contract.js`
- Deployment entry: `SYNC_ENV=0 bash deploy/trialrun/deploy.sh`
- Deployment entry requires `SYNC_ENV=0`, absolute `POSTGRES_DATA_DIR`, absolute
`STORAGE_HOST_DIR` and `DEPLOY_DATA_MODE=existing|initialize`.
- Contract sources:
- `server/src/config.js`
- `deploy/trialrun/.env.example`
@@ -47,6 +47,10 @@ operations require explicit approval and evidence.
allowlist in `validate-env-contract.js`.
- `SYNC_ENV=0` is the default public deploy mode; it does not overwrite remote
`.env`.
- Relative data-path defaults are allowed only for direct local Compose.
`deploy.sh` requires explicit absolute host paths and a data mode; `existing`
verifies the PGDATA directory contains `PG_VERSION`, while `initialize` is
the only mode allowed to create missing directories.
### 4. Validation & Error Matrix
@@ -54,6 +58,10 @@ operations require explicit approval and evidence.
- Service omits `env_file: ./.env` -> contract exits non-zero.
- Unauthorized compose override -> contract exits non-zero.
- Remote `.env` missing -> deploy step 4 exits before migration.
- Missing/relative/unsafe/overlapping data paths or missing data mode -> deploy
exits locally before provenance, synchronization or SSH.
- `existing` target missing `PG_VERSION` or storage directory -> remote deploy
exits before Compose starts DB.
### 5. Good/Base/Bad Cases
@@ -83,7 +91,9 @@ SYNC_ENV=0 bash deploy/trialrun/deploy.sh
```bash
# Add the key to deploy/trialrun/.env.example first.
node deploy/trialrun/validate-env-contract.js
SYNC_ENV=0 bash deploy/trialrun/deploy.sh
SYNC_ENV=0 POSTGRES_DATA_DIR=/srv/hyb-oa/pgdata \
STORAGE_HOST_DIR=/srv/hyb-oa/storage DEPLOY_DATA_MODE=existing \
bash deploy/trialrun/deploy.sh
```
## Scenario: Public Smoke Navigation Labels
@@ -279,7 +289,8 @@ const REQUIRED_ADMIN_NAV_KEYS = ["systemSettings"];
### 2. Signatures
- Provenance gate: `node deploy/trialrun/check-source-provenance.mjs`.
- Standard release: `SYNC_ENV=0 bash deploy/trialrun/deploy.sh`.
- Standard release additionally supplies the explicit absolute data-path and
data-mode contract required by `deploy.sh`.
- Build/runtime inputs: `BUILD_SHA=<full-commit>` and
`VITE_BUILD_SHA=<12-char-commit>`.
- Runtime outputs: `/health.data.buildSha`, `/ready.data.buildSha` and the
@@ -606,6 +617,8 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- Every deploy/timer Compose command uses `-p "$COMPOSE_PROJECT_NAME"`.
- PostgreSQL mount: `${POSTGRES_DATA_DIR:-./data/postgres}`.
- Application storage mount: `${STORAGE_HOST_DIR:-./data/storage}`.
- Formal deploy inputs: absolute `POSTGRES_DATA_DIR`, absolute
`STORAGE_HOST_DIR`, `DEPLOY_DATA_MODE=existing|initialize`.
- DB has no host `ports`; server remains loopback-only.
### 3. Contracts
@@ -615,11 +628,22 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- A project-name change and its DB/storage path selection are one atomic,
separately approved downtime operation. Never start two PostgreSQL instances
against one PGDATA.
- Freeze the old project name from `docker compose ls`, not from a directory
guess. Record immutable image IDs/digests, successful migration count, fixed
business-table counts, resolved mounts and Caddy target before any write.
- Relative defaults preserve local trialrun compatibility, but production must
verify the resolved host paths against the active container mounts before
any start, backup or migration.
use explicit absolute command inputs and verify them against active container
mounts before any start, backup or migration.
- `existing` is the normal migration mode and refuses a missing PGDATA marker
or storage directory. `initialize` is only for an approved empty first start;
only that mode may create data directories.
- Remote readiness may use host curl, but build-SHA JSON validation runs with
Node inside the server container; the Docker host does not need Node.
- For an existing cutover, stop the old PostgreSQL completely, copy PGDATA and
storage into pre-verified empty absolute targets while preserving ownership,
permissions, links and extended attributes, and retain the old directories
untouched and unmounted for the observation period. Do not rename away the
only pre-migration rollback data and do not copy a running PGDATA.
### 4. Validation & Error Matrix
@@ -627,8 +651,12 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- Any fixed `container_name` appears -> environment contract fails.
- Required data mount loses its parameter -> environment contract fails.
- Deploy/timer omits `-p` -> environment contract fails.
- Formal deploy omits paths/mode, uses a relative/broad/overlapping path, or an
`existing` PGDATA lacks `PG_VERSION` -> fail before DB start.
- New project resolves the same PGDATA while the old DB runs -> stop before
Compose start; do not use service health as permission to continue.
- Old project name was inferred, immutable image ID or baseline counts are
missing, or target copy is non-empty -> stop the cutover.
### 5. Tests Required
@@ -641,9 +669,12 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- Good: one approved project resolves one stopped-and-migrated PGDATA and the
matching active storage path; deploy, timer and backup all use that identity.
- Base: local trialrun uses the documented relative defaults under one project.
- Base: direct local Compose uses the documented relative defaults under one
project; those defaults are never accepted by formal `deploy.sh`.
- Bad: start a renamed project before the old DB stops, or let it create a new
empty directory and accept a green health check as proof of data continuity.
- Bad: move the only PGDATA into the new path, migrate it in place, then assume
the old container name alone is a rollback point.
### 7. Wrong vs Correct
@@ -677,7 +708,7 @@ DB_TOOL_MODE=docker DB_CONTAINER="${DB_CONTAINERS[0]}" \
### 2. Signatures
- Paired backup: `DB_TOOL_MODE=docker DB_CONTAINER=<compose-resolved-id> STORAGE_DIR_HOST=<verified-active-directory> ./scripts/backup-all.sh`.
- Paired backup: `DB_TOOL_MODE=docker DB_CONTAINER=<compose-resolved-id> STORAGE_DIR_HOST=<verified-active-directory> BUILD_SHA=<actual-build-sha> SOURCE_IMAGE_DIGEST=<actual-sha256-image-id> ./scripts/backup-all.sh`; at least one source identity is required.
- Optional safe batch identifier: `BACKUP_RUN_ID=<safe-id>`.
- Manifest writer: host `node` is preferred; host `python3` is an equivalent
fallback for Docker-only runtime machines.
@@ -693,7 +724,11 @@ DB_TOOL_MODE=docker DB_CONTAINER="${DB_CONTAINERS[0]}" \
- A complete recovery point is one batch directory containing `database.dump`,
`storage.tar.gz` and a final `manifest.json`. The manifest contains only safe
relative names, byte sizes, SHA-256 hashes, batch time and optional build SHA.
relative names, byte sizes, SHA-256 hashes, batch time, actual build SHA when
known and actual source image digest when supplied.
- `BUILD_SHA` supplied by the caller takes precedence over local Git fallback.
On a non-Git runtime host, at least `SOURCE_IMAGE_DIGEST=sha256:<64-hex>` is
mandatory. Never label a legacy backup with the commit planned for deployment.
- Manifest generation uses exclusive creation and preserves the same schema
under Node and Python 3; if neither runtime exists, fail before publishing the
final batch directory.
@@ -718,6 +753,10 @@ DB_TOOL_MODE=docker DB_CONTAINER="${DB_CONTAINERS[0]}" \
people, rollback point and post-restore `/ready`/permission checks.
- Copy the whole completed batch to approved off-host/off-disk storage; a batch
left only on the application host is not accepted recovery evidence.
- A real-format shadow restore compares successful migration count and a fixed
set of business-table counts with the pre-backup baseline, compares extracted
regular-file count with the active storage source, and runs `/ready` using a
schema-compatible image. `/ready` alone is not recovery evidence.
### 4. Validation & Error Matrix
@@ -735,9 +774,14 @@ DB_TOOL_MODE=docker DB_CONTAINER="${DB_CONTAINERS[0]}" \
only when the explicit target container and required DB variables exist.
- Neither Node nor Python 3 is available -> fail manifest generation and remove
the temporary batch; do not publish a partial recovery point.
- Invalid build SHA/image digest, or neither identity available on a non-Git
host -> fail before DB or storage backup starts.
- Deployment `.env`, PostgreSQL container and active application connection do
not identify the same database/user -> stop the live backup; never guess
credentials or expose values in diagnostics.
- Shadow migration count, any fixed table count, attachment regular-file count
or compatible-image `/ready` differs from baseline -> reject the recovery
point and do not enter production cutover.
### 5. Good/Base/Bad Cases
@@ -745,6 +789,8 @@ DB_TOOL_MODE=docker DB_CONTAINER="${DB_CONTAINERS[0]}" \
host, and a disposable shadow drill restores the DB marker and attachment hash.
- Good: a Docker-only host uses Python 3 to write the same manifest contract
after the active application connection is matched to the database container.
- Good: a legacy container without `/ready.data.buildSha` records its inspected
`sha256:` image ID and leaves `buildSha` null rather than inventing provenance.
- Base: backup succeeds locally but off-host copy and deployed `/ready` evidence
remain explicitly pending; do not claim formal recovery readiness.
- Bad: source a stale deployment `.env` and treat a failed dump as a prompt to
@@ -759,7 +805,7 @@ DB_TOOL_MODE=docker DB_CONTAINER="${DB_CONTAINERS[0]}" \
- Negative assertions for wrong DB confirmation, corrupt DB/tar, non-empty
target and dangerous tar path.
- Validate manifest relative paths, absence of connection secrets and matching
SHA-256 values.
SHA-256 values, explicit build-SHA precedence and image-digest preservation.
- Run the synthetic drill with Node available and with a `PATH` that excludes
Node but includes Python 3; both paths must complete the same round trip.
- Real-instance restore and `/ready` remain separately approved evidence and
@@ -783,6 +829,7 @@ source deploy/trialrun/.env
# Before an approved live backup, compare the selected DB container identity
# with an active app connection using redacted boolean diagnostics.
DB_TOOL_MODE=docker DB_CONTAINER=<verified-container> \
STORAGE_DIR_HOST=<verified-active-storage> ./scripts/backup-all.sh
STORAGE_DIR_HOST=<verified-active-storage> \
SOURCE_IMAGE_DIGEST=<verified-sha256-image-id> ./scripts/backup-all.sh
# Real restore remains an approved incident operation after shadow evidence.
```
@@ -21,7 +21,7 @@ isolated drill
- 一个批次只使用一个 `run_id`,由上层入口传递明确的 `OUT_FILE` 给现有底层脚本。
- 先把产物写入批次临时目录;DB 和附件均成功并通过完整性检查后,再写 manifest 并把批次标记为完整。
- manifest 使用稳定、脱敏的键:`schemaVersion`、`runId`、`createdAt`、`buildSha`(如可安全取得)、两份相对文件名、字节数和 SHA-256。
- manifest 使用稳定、脱敏的键:`schemaVersion`、`runId`、`createdAt`、`buildSha`(真实可知时)、`sourceImageDigest`(实测可知时)、两份相对文件名、字节数和 SHA-256;两类来源标识至少有一项。
- 不把连接串、容器密码、存储源路径、真实业务文件清单或主机绝对路径写入 manifest。
- 本任务不负责异机传输实现;runbook 要求操作者把完整批次复制到异机/异盘,并记录保留周期。
@@ -121,11 +121,14 @@ GET /auth/wecom/login?mode=onboard
- DB 不发布宿主端口;应用仅通过 Compose 内部网络访问 `db`。环境契约必须拒绝 DB `ports`、固定容器名或未参数化数据目录回退。
- Docker 备份/恢复必须显式传入由 `compose ... ps -q db` 得到的已核验容器 ID;附件备份必须显式传入活动挂载源,两个脚本都不猜默认值。
- 远端 `/ready` 构建标识在 server 容器内用 Node 校验,宿主机只负责 curl 和 Compose,不要求安装 Node。
- `deploy.sh` 不解析或 source 远端 `.env` 来猜宿主路径;调用者必须在发布命令中显式传入受限字符集的绝对 `POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 和数据模式。`existing` 要求目录已存在且 PGDATA 含 `PG_VERSION`;`initialize` 才允许创建空目标。
- 本地直接 Compose 可继续使用示例相对默认值,但正式发布入口拒绝相对路径,从而避免项目名变化时 Docker 以 root 静默创建第二套空目录。
- manifest 的 `buildSha` 优先使用调用者显式值,否则仅在本地 Git 工作树存在时推导;另以可选 `sourceImageDigest` 记录活动镜像不可变标识。旧实例 build SHA 不可知时只记录实测 digest,不填写目标提交。
### 生产统一顺序
1. 另行获批后,从活动 DB 和活动附件挂载源重新生成成对异机备份作为灾难回滚点;旧批次因混入非活动附件目录只保留审计,并记录活动数据挂载、环境摘要、容器镜像和 Caddy 端口。
2. 本地先修复 Compose 可配置回环端口及部署脚本动态端口探测,通过全部门禁并提交、发布批准提交。
3. 停机后把非活动的既定部署目录改名保留,将活动数据目录和环境文件迁入既定目录;同一文件系统优先原子改名,不复制运行中的 PostgreSQL 文件。
3. 停机后把活动 PGDATA 和附件复制到新的显式绝对目录,复制前目标必须为空;保留旧目录原位且不再挂载写入,直到观察期结束。复制仅在旧 PostgreSQL 完全停止后执行,并保留所有权、权限、链接和扩展属性。
4. 用新 Compose 重建 db/init/server,应用已提交 migrations;旧容器和旧目录在验收前保留,不清理。
5. 公网构建标识、数据计数、角色、企微入口和关键只读检查通过后更新运维 timer;失败则停止新容器并恢复旧目录/容器和原 Caddy 目标。
@@ -22,6 +22,9 @@
- [ ] 修复部署脚本的现场端口解析,确定唯一部署目录,并在活动 DB/附件挂载、环境摘要和旧镜像回滚点全部确认后才执行获批切换。
- [x] 完成本地 Compose/备份加固:移除固定容器名和 DB 宿主端口,参数化数据目录,deploy/timer 显式 project,备份来源缺失硬失败,远端构建标识改由容器内 Node 核验。
- [x] 增加隐式附件源、隐式 DB 容器、DB 端口与固定容器名回退的负向契约演练;未部署或启用新 Compose。
- [x] 收紧正式 deploy 数据路径:显式绝对路径、`existing|initialize` 模式、既有 PGDATA 标记和禁止 Docker 静默建空目录。
- [x] 让成对 manifest 尊重显式 `BUILD_SHA` 并支持活动镜像 digest;补 Node/Python 等价与非法来源标识负向测试。
- [x] 把旧 Compose project 实测、镜像 digest、58 项迁移与关键表计数、影子逐表/附件计数验收、停机后复制并保留旧目录写入 runbook/checklist/spec。
- [ ] 部署后验证唯一 Compose 来源、构建标识、迁移状态、Caddy 公网 `/ready` 和前端 marker;不因服务可访问而忽略来源漂移。
- [ ] 用户在企微应用详情选择手机、邮箱、企业邮箱敏感字段;部署开关开启后,两名目标分别通过一次性链接本人确认授权。
- [ ] 两人自助开通成功后写入获批 `finance` / `ceo` 职责,复核不同身份、maker-checker、会话撤销、审计和付款零写入。
@@ -63,6 +66,7 @@
- 合成备份恢复在 Node 路径和移除 Node 的 Python 3 回退路径均通过,最近两次本地参考 RTO 均为 6 秒;负向覆盖隐式附件源、隐式 DB 容器、DB 宿主端口、固定容器名和 deploy 缺失 project name。
- 本阶段授权仅覆盖本地文件、测试、规约和提交,不包含生产备份、停机、迁移、部署、timer 安装或企微写入。
- 已确认旧异机批次混用了活动 DB 与非活动附件目录,保留审计但不再作为活动实例成对恢复证据;正确批次须另行批准重做。
- 复核补修后,正式 deploy 的相对/重叠路径负向门禁、显式来源标识、Node 路径以及无 Node/无 Git 的 Python 3 + digest-only 路径均通过;最近两次合成 RTO 均为 7 秒。
## 回滚点
@@ -44,6 +44,8 @@
- 普通 `snsapi_base` 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
- 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
- Compose 项目名、容器身份与 DB/附件目录必须在同一次获批停机窗口切换;DB 不发布宿主端口,备份不得猜测容器名或附件目录。
- 正式 `deploy.sh` 必须由调用者显式提供绝对 `POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 和 `DEPLOY_DATA_MODE=existing|initialize`;既有实例默认只接受包含 `PG_VERSION` 的现存 PGDATA,不得让 Docker 静默创建空目录。
- 成对备份必须尊重调用者显式提供的真实来源标识。当前旧实例没有 build SHA 时记录活动镜像不可变 digest,不得用待部署提交冒充备份来源。
- 部署健康检查必须从 Compose 解析实际宿主端口,并校验后端 `/ready`、前端 marker 与批准提交一致;应用已提交的 Prisma migrations 但禁止 reset 或改写历史。
## Acceptance Criteria
@@ -62,6 +64,7 @@
- [ ] 一次性授权通过 `getuserdetail` 获取成员同意的手机和邮箱,自动创建或补齐 OA 员工与企微绑定;新建账号角色严格等于 `employee`,日志和审计不含票据或完整敏感响应。
- [ ] 部署目录、Compose、环境文件、运维 timer 和活动容器来源统一;活动 DB/附件连续,远端 migrations 与批准提交一致,公网 `/ready` 和前端 marker 返回同一短构建标识。
- [x] 本地 Compose、部署、timer 和备份合同完成加固并覆盖负向回退;未把本地通过误报为生产切换完成。
- [x] 部署入口拒绝隐式/相对数据目录及缺少数据模式,成对 manifest 可记录显式 build SHA 或活动镜像 digest,并有 Node/Python 等价测试。
- [ ] 两名目标成员分别完成一次本人授权后,账号唯一、企微绑定不同,再由管理员写入 `employee + finance` 与 `employee + ceo`;maker-checker、会话撤销和角色审计通过。
## Out of Scope
@@ -91,3 +91,11 @@
- 企微官方文档复核:2022-06-20 后新建的自建应用不再通过 `/user/get` 返回手机、邮箱和企业邮箱;管理员需在应用详情选择敏感字段,成员使用 `snsapi_privateinfo` 手工授权,`/cgi-bin/auth/getuserinfo` 才返回 30 分钟有效的 `user_ticket`,随后 `/cgi-bin/auth/getuserdetail` 可返回成员同意的手机号、邮箱和企业邮箱。
- 当前 OA `server/src/routes/wecomAuth.js` 固定使用 `snsapi_base`,只调用 `getuserinfo` 获取 userid,不调用 `getuserdetail`;因此现状是项目尚未接入官方敏感信息授权链路,而不是企微没有该能力。
- 两名目标成员的脱敏结果为匹配计数 `[1,1]`、身份不同、均 active、数据库匹配 `[0,0]`、计划动作均为 create;两人均缺 `phone` 与 `email`,因此没有员工、角色、会话、付款或其他业务写入。
## 2026-07-27 复核后的执行授权边界
- 外部复核确认提交 `2334203` 的 Compose、端口、容器名、备份来源硬失败和负向契约方向正确,同时发现正式 deploy 仍会创建默认目录、相对默认值缺少生产护栏、非 Git 主机 manifest 无来源标识三项本地缺口。
- 已批准在补齐上述缺口后执行:发布批准提交、生产只读冻结、从活动 DB/附件重做异机成对备份、在隔离影子目标恢复并验收。该授权不包含停机、生产数据目录复制、迁移、新 Compose 启动、Caddy/timer 修改、企微写入或角色写入。
- 冻结证据必须包含实测旧 Compose project、不可变镜像 ID/digest、成功 migration 数和固定关键表计数;影子恢复必须逐表比对计数、比对附件普通文件数并运行 schema 兼容镜像 `/ready`。
- 当前旧实例没有 build SHA,重做备份不得写入待部署提交。manifest 记录活动容器的实测 `sha256:` 镜像 ID;新版本部署后的备份才能记录其真实 build SHA。
- 后续停机预案采用“旧 PostgreSQL 完全停止后复制到空新目录,旧目录原位保留且不再写入”,不把原子改名后的唯一数据副本当作回滚点。