docs(reimbursement-payment-batch-deployment): 记录受控部署验收
这个提交包含在:
@@ -19,6 +19,103 @@ operations require explicit approval and evidence.
|
|||||||
- Do not execute real payments, import bank statements, connect payment
|
- Do not execute real payments, import bank statements, connect payment
|
||||||
providers or write ERP without a separate approved Trellis task.
|
providers or write ERP without a separate approved Trellis task.
|
||||||
|
|
||||||
|
## Scenario: Existing Deployment SSH Identity Disambiguation
|
||||||
|
|
||||||
|
### 1. Scope / Trigger
|
||||||
|
|
||||||
|
- Trigger: `DEPLOY_SERVER` / `DEPLOY_REFRESH_SERVER` are not injected and local
|
||||||
|
SSH configuration exposes more than one alias that can read the frozen
|
||||||
|
deployment contract.
|
||||||
|
- Purpose: select the existing least-privilege deployment account without
|
||||||
|
guessing from alias names or accidentally publishing as unrestricted root.
|
||||||
|
|
||||||
|
### 2. Signatures
|
||||||
|
|
||||||
|
- Contract reader: `deployment_contract_load <ssh-target> <systemd-unit> <assoc-array>`.
|
||||||
|
- Aggregate preflight:
|
||||||
|
`DEPLOY_SERVER=<target> DEPLOY_REFRESH_SERVER=<target> bash deploy/trialrun/check-existing-deployment.sh`.
|
||||||
|
- Refresh probe: `ssh <refresh-target> probe` must return exactly
|
||||||
|
`[deployment-contract-refresh][PASS] probe`.
|
||||||
|
- Deployment entry: `bash deploy/trialrun/deploy-existing.sh` with the two
|
||||||
|
resolved targets kept only in process environment.
|
||||||
|
|
||||||
|
### 3. Contracts
|
||||||
|
|
||||||
|
- Never choose a target from its alias name, apparent hostname or ordering in
|
||||||
|
SSH config, and never print candidate targets while probing.
|
||||||
|
- A normal deployment candidate must be non-root, own the contracted
|
||||||
|
`REMOTE_DIR`, `REMOTE_ENV_FILE` and synchronized source files, and have
|
||||||
|
Docker access. These checks use numeric UID equality on the remote host.
|
||||||
|
- An unrestricted root alias is not a valid normal deployment target even if
|
||||||
|
it can read the same contract and pass the aggregate checks. Publishing as
|
||||||
|
root can change synchronized-file ownership and break the intended
|
||||||
|
least-privilege path.
|
||||||
|
- Multiple aliases may point to the same machine and contract. Select a target
|
||||||
|
automatically only when exactly one candidate satisfies the full non-root
|
||||||
|
ownership contract; otherwise stop for explicit local configuration
|
||||||
|
guidance.
|
||||||
|
- The refresh identity is separate and valid only when the exact forced-command
|
||||||
|
`probe` response matches. Do not reuse an unrestricted root login as the
|
||||||
|
refresh identity.
|
||||||
|
- After deployment, assert the source files still belong to the selected
|
||||||
|
deployment UID before recording success.
|
||||||
|
|
||||||
|
### 4. Validation & Error Matrix
|
||||||
|
|
||||||
|
- No contract-readable candidate -> stop before remote mutation.
|
||||||
|
- Exactly one non-root owner candidate + one exact refresh probe -> continue to
|
||||||
|
13/13 aggregate preflight.
|
||||||
|
- Several contract-readable aliases, but only one non-root owner -> use the
|
||||||
|
owner candidate; do not treat alias count alone as ambiguity.
|
||||||
|
- Several non-root owner candidates -> stop and request local SSH guidance;
|
||||||
|
do not choose by config order or connection speed.
|
||||||
|
- Root candidate passes 13/13 -> exclude it from normal deployment selection.
|
||||||
|
- Post-deploy ownership differs from the selected UID -> fail closeout and
|
||||||
|
investigate; do not report the release as clean.
|
||||||
|
|
||||||
|
### 5. Good/Base/Bad Cases
|
||||||
|
|
||||||
|
- Good: two aliases reach the same host and contract; the directory-owning
|
||||||
|
non-root account is uniquely selected and ownership remains unchanged.
|
||||||
|
- Base: one normal deployment identity and one forced-command refresh identity
|
||||||
|
are injected explicitly; no discovery is needed.
|
||||||
|
- Bad: use an unrestricted root alias because it also returns 13/13 PASS.
|
||||||
|
- Bad: select the first matching `Host` entry or expose aliases, usernames,
|
||||||
|
paths or SSH configuration in task evidence.
|
||||||
|
|
||||||
|
### 6. Tests Required
|
||||||
|
|
||||||
|
- Confirm the selected normal identity has a non-zero UID and numerically owns
|
||||||
|
the contracted repository, environment file and a synchronized source file.
|
||||||
|
- Confirm Docker access succeeds without sudo and the aggregate preflight is
|
||||||
|
exactly `PASS=13 FAIL=0`.
|
||||||
|
- Confirm the refresh identity returns the exact probe token and the normal
|
||||||
|
identity is not substituted for it.
|
||||||
|
- After release, compare the synchronized source owner UID with the selected
|
||||||
|
identity UID and require equality.
|
||||||
|
- Persist only PASS/FAIL counts and the selection rationale; run the sensitive
|
||||||
|
content gate before commit.
|
||||||
|
|
||||||
|
### 7. Wrong vs Correct
|
||||||
|
|
||||||
|
#### Wrong
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Both aliases connect, so choose the first one (which may be root).
|
||||||
|
DEPLOY_SERVER="${candidates[0]}" bash deploy/trialrun/deploy-existing.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Correct
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resolve exactly one non-root candidate that owns the frozen contract paths,
|
||||||
|
# then run the aggregate gate before the controlled deployment wrapper.
|
||||||
|
test "${#owner_candidates[@]}" -eq 1
|
||||||
|
DEPLOY_SERVER="${owner_candidates[0]}" \
|
||||||
|
DEPLOY_REFRESH_SERVER="$forced_refresh_target" \
|
||||||
|
bash deploy/trialrun/check-existing-deployment.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Scenario: Deployment Env Contract
|
## Scenario: Deployment Env Contract
|
||||||
|
|
||||||
### 1. Scope / Trigger
|
### 1. Scope / Trigger
|
||||||
|
|||||||
@@ -2,34 +2,34 @@
|
|||||||
|
|
||||||
## 1. Activation and publication
|
## 1. Activation and publication
|
||||||
|
|
||||||
- [ ] 用户批准本计划后启动 Trellis task。
|
- [x] 用户批准本计划后启动 Trellis task。
|
||||||
- [ ] 提交任务启动记录,确认工作区干净,将 `main` 推送到 `origin/main`。
|
- [x] 提交任务启动记录,确认工作区干净,将 `main` 推送到 `origin/main`。
|
||||||
- [ ] 记录明确发布提交并确认无 schema、migration、配置键或部署脚本变化。
|
- [x] 记录明确发布提交并确认无 schema、migration、配置键或部署脚本变化。
|
||||||
|
|
||||||
## 2. Preflight
|
## 2. Preflight
|
||||||
|
|
||||||
- [ ] 运行环境合同、Trellis 治理、敏感信息和 Git 来源检查。
|
- [x] 运行环境合同、Trellis 治理、敏感信息和 Git 来源检查。
|
||||||
- [ ] 从本机 SSH 配置无回显解析唯一部署身份与 refresh forced-command 身份。
|
- [x] 从本机 SSH 配置无回显解析唯一部署身份与 refresh forced-command 身份。
|
||||||
- [ ] 运行 `check-existing-deployment.sh`,要求 13/13 聚合合同全部 PASS。
|
- [x] 运行 `check-existing-deployment.sh`,要求 13/13 聚合合同全部 PASS。
|
||||||
|
|
||||||
## 3. Deployment
|
## 3. Deployment
|
||||||
|
|
||||||
- [ ] 使用 `DEPLOY_SERVER`、`DEPLOY_REFRESH_SERVER` 调用 `deploy-existing.sh`。
|
- [x] 使用 `DEPLOY_SERVER`、`DEPLOY_REFRESH_SERVER` 调用 `deploy-existing.sh`。
|
||||||
- [ ] 确认 `SYNC_ENV=0`、existing data contract、无生产 `.env` 覆盖、无新增 migration。
|
- [x] 确认 `SYNC_ENV=0`、existing data contract、无生产 `.env` 覆盖、无新增 migration。
|
||||||
- [ ] 确认应用发布和备份合同 refresh 均通过;若部分成功,按设计中的失败分类停止。
|
- [x] 确认应用发布和备份合同 refresh 均通过;若部分成功,按设计中的失败分类停止。
|
||||||
|
|
||||||
## 4. Public read-only verification
|
## 4. Public read-only verification
|
||||||
|
|
||||||
- [ ] 验证公网 `/ready`、运行时 build SHA 和前端短 SHA命中发布提交。
|
- [x] 验证公网 `/ready`、运行时 build SHA 和前端短 SHA命中发布提交。
|
||||||
- [ ] 运行基础公网 smoke、外部依赖/功能开关检查和公开资源扫描。
|
- [x] 运行基础公网 smoke、外部依赖/功能开关检查和公开资源扫描。
|
||||||
- [ ] 仅在专用 `LIVE_SMOKE_*` 凭据已配置时运行登录态 smoke;否则记录明确 SKIP。
|
- [x] 仅在专用 `LIVE_SMOKE_*` 凭据已配置时运行登录态 smoke;否则记录明确 SKIP。
|
||||||
- [ ] 扫描公开前端资源确认本轮关键文案/入口合同已发布,不读取真实回单或银行资料。
|
- [x] 扫描公开前端资源确认本轮关键文案/入口合同已发布,不读取真实回单或银行资料。
|
||||||
|
|
||||||
## 5. Closeout
|
## 5. Closeout
|
||||||
|
|
||||||
- [ ] 将脱敏部署结果写回本任务,不新建平行报告。
|
- [x] 将脱敏部署结果写回本任务,不新建平行报告。
|
||||||
- [ ] 运行 task validate、Trellis 治理、敏感信息和 `git diff --check`。
|
- [x] 运行 task validate、Trellis 治理、敏感信息和 `git diff --check`。
|
||||||
- [ ] 使用中文提交结果记录并引用 `reimbursement-payment-batch-deployment`。
|
- [x] 使用中文提交结果记录并引用 `reimbursement-payment-batch-deployment`。
|
||||||
- [ ] 归档任务并仅在全部公网门禁通过后告知用户可以测试。
|
- [ ] 归档任务并仅在全部公网门禁通过后告知用户可以测试。
|
||||||
|
|
||||||
## Rollback points
|
## Rollback points
|
||||||
@@ -38,3 +38,17 @@
|
|||||||
- 发布中:应用未健康时使用既有入口回退上一已发布提交。
|
- 发布中:应用未健康时使用既有入口回退上一已发布提交。
|
||||||
- 发布后:仅备份合同 refresh 失败时不重复部署应用,单独恢复合同。
|
- 发布后:仅备份合同 refresh 失败时不重复部署应用,单独恢复合同。
|
||||||
- 全程不修改生产 `.env`、数据库结构、PGDATA、附件存储或真实业务数据。
|
- 全程不修改生产 `.env`、数据库结构、PGDATA、附件存储或真实业务数据。
|
||||||
|
|
||||||
|
## Deployment result (2026-08-05)
|
||||||
|
|
||||||
|
- 发布提交:`566c37679a6e9a2581533d40cdcfe7cdd5421b5b`,已由上游包含,未使用 unpublished override。
|
||||||
|
- 身份消歧:两个 SSH 别名连接同一主机、读取同一合同;唯一非 root 候选是部署目录、环境文件和源码所有者,另一候选为 root 管理身份。本轮使用实际所有者发布,发布后所有权保持不变;refresh forced-command 身份唯一。
|
||||||
|
- 本地门禁:环境合同、任务合同、Trellis 治理、敏感信息、Git 来源和 clean worktree 均 PASS。
|
||||||
|
- 远端预检:`check-existing-deployment.sh` 两次均为 13/13 PASS。
|
||||||
|
- 发布入口:`deploy-existing.sh` PASS;`SYNC_ENV=0`,existing data contract,生产 `.env` 未同步;64 个 migration 已存在且无待执行项。
|
||||||
|
- 运行时:数据库、外部依赖、13 项导航合同和运行时 smoke PASS;容器完整 build SHA、公开 `/ready` 12 位 SHA、前端 12 位 SHA 均命中发布提交。
|
||||||
|
- 功能开关:`payment_batch_v1=force_on`、`erp_sync=force_off`,ERP 环境开关保持关闭。
|
||||||
|
- 公开资源:扫描 98 个资源,build marker PASS、旧文案 findings=0;`本批次报销单`、`费用明细`、`明细小计`、`下载需说明用途`、`在线查看` 均命中。
|
||||||
|
- 备份合同:forced-command refresh PASS。
|
||||||
|
- 登录态 smoke:SKIP,当前进程未提供完整专用 `LIVE_SMOKE_*` 凭据;匿名 readiness 无 blocker。真实 OCR 同样因无专用凭据和授权 PDF 样票而 SKIP,未读取或写入真实票据。
|
||||||
|
- 业务写入:未执行真实报销、审批、付款批次、回单查看/上传、银行、ERP、企业微信或生产配置写入。
|
||||||
|
|||||||
@@ -48,12 +48,12 @@
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- [ ] AC1:候选提交已推送到 `origin/main`,工作区干净,来源和部署前聚合体检通过。
|
- [x] AC1:候选提交已推送到 `origin/main`,工作区干净,来源和部署前聚合体检通过。
|
||||||
- [ ] AC2:`deploy-existing.sh` 完整通过,生产 `.env` 未同步或覆盖,数据库无新增 migration,未发生真实业务或付款写入。
|
- [x] AC2:`deploy-existing.sh` 完整通过,生产 `.env` 未同步或覆盖,数据库无新增 migration,未发生真实业务或付款写入。
|
||||||
- [ ] AC3:公网 `/ready`、运行时 build 与前端短 SHA 均命中本次发布提交。
|
- [x] AC3:公网 `/ready`、运行时 build 与前端短 SHA 均命中本次发布提交。
|
||||||
- [ ] AC4:基础公网 smoke、部署运行时 smoke、外部依赖/功能开关和前端资源扫描通过;登录态 smoke 按凭据实际状态记录 PASS 或明确 SKIP。
|
- [x] AC4:基础公网 smoke、部署运行时 smoke、外部依赖/功能开关和前端资源扫描通过;登录态 smoke 按凭据实际状态记录 PASS 或明确 SKIP。
|
||||||
- [ ] AC5:公开资源可确认差旅事由去重、付款批次费用明细/小计和“下载需说明用途”等新前端合同已发布,不暴露真实回单、账号或存储资料。
|
- [x] AC5:公开资源可确认差旅事由去重、付款批次费用明细/小计和“下载需说明用途”等新前端合同已发布,不暴露真实回单、账号或存储资料。
|
||||||
- [ ] AC6:任务记录、Trellis 治理、敏感信息检查和 `git diff --check` 通过;只在全部部署与公网门禁通过后告知用户可以测试。
|
- [x] AC6:任务记录、Trellis 治理、敏感信息检查和 `git diff --check` 通过;只在全部部署与公网门禁通过后告知用户可以测试。
|
||||||
|
|
||||||
## Out of Scope
|
## Out of Scope
|
||||||
|
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户