归档(08-09-deployment-contract-single-source-harness): 收口部署工具加固
这个提交包含在:
@@ -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,116 @@
|
||||
# 部署合同单一事实源与失败路径覆盖:设计
|
||||
|
||||
## 1. 根因与设计目标
|
||||
|
||||
历史缺陷不是 Compose 本身无法校验,而是调用链在不同位置重新定义“候选构建身份”:
|
||||
|
||||
```text
|
||||
deploy.sh
|
||||
-> provenance.commit + provenance.shortCommit
|
||||
-> BUILD_SHA / VITE_BUILD_SHA
|
||||
|
||||
production-preflight.mjs
|
||||
-> CANDIDATE_COMMIT
|
||||
-> 自行 slice(0, 12)
|
||||
-> BUILD_SHA / VITE_BUILD_SHA
|
||||
```
|
||||
|
||||
两条路径的键名和派生规则相同,却没有共享可执行来源。新增 Compose 必填值或调整短标识规则时,其中一条可能漏改。设计目标是把该合同收敛到已有 provenance 模块,同时保留远端消费校验,并让 synthetic harness 执行真实 orchestration/remote helper 进程。
|
||||
|
||||
## 2. 单一合同形态
|
||||
|
||||
在 `check-source-provenance.mjs` 导出:
|
||||
|
||||
```js
|
||||
export function deriveCandidateBuildEnv(commit) {
|
||||
// validate 40..64 lowercase hex
|
||||
return Object.freeze({
|
||||
BUILD_SHA: commit,
|
||||
VITE_BUILD_SHA: commit.slice(0, 12),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
具体实现可在不改变合同的前提下调整错误类或冻结方式,但调用方不得自己派生 `VITE_BUILD_SHA`。
|
||||
|
||||
`collectSourceProvenance` 先调用该函数,再以同一结果形成:
|
||||
|
||||
```text
|
||||
commit = buildEnv.BUILD_SHA
|
||||
shortCommit = buildEnv.VITE_BUILD_SHA # 兼容字段
|
||||
buildEnv = { BUILD_SHA, VITE_BUILD_SHA }
|
||||
```
|
||||
|
||||
共享 API 只导出一次返回完整对象的派生函数,不另行导出可能与对象漂移的键数组,也不提供 `getBuildSha()` / `getViteBuildSha()` 之类可分别调用的接口。两个调用方必须通过 `Object.entries(buildEnv)` 或等价的完整对象遍历生成整组远端环境赋值;单独选择某个键注入属于合同失败。
|
||||
|
||||
非法输入在共享函数处失败。provenance 自身使用既有 `SourceProvenanceError`;production preflight 将该失败稳定映射为既有 `CANDIDATE_COMMIT_INVALID`,不把内部错误名暴露为新的操作合同。
|
||||
|
||||
## 3. 调用链
|
||||
|
||||
### 3.1 正式部署
|
||||
|
||||
```text
|
||||
check-source-provenance CLI
|
||||
-> buildEnv
|
||||
-> deploy.sh 读取 BUILD_SHA / VITE_BUILD_SHA
|
||||
-> 远端 shell env
|
||||
-> docker compose config/build/runtime
|
||||
```
|
||||
|
||||
`deploy.sh` 可继续保留 `SOURCE_COMMIT` / `SOURCE_SHORT_COMMIT` 局部名称用于日志和公网检查,但值必须来自 `buildEnv`,不能从 `commit` 再截取。
|
||||
|
||||
### 3.2 生产 preflight
|
||||
|
||||
```text
|
||||
CANDIDATE_COMMIT
|
||||
-> deriveCandidateBuildEnv
|
||||
-> remote command env assignments
|
||||
-> production-preflight-remote.sh
|
||||
-> 格式 + 前缀 + compose config 消费校验
|
||||
```
|
||||
|
||||
远端 helper 的校验不是第二个来源:它不派生值,只验证接收到的值符合共享合同。这一层用于防止命令组装、环境传输或未来调用方漂移。
|
||||
|
||||
## 4. Harness 设计
|
||||
|
||||
继续使用 `deployment-safety-contract.test.js` 的临时目录:
|
||||
|
||||
- 假 `ssh` 捕获命令、按 mode 输出 marker/error 并返回指定状态;
|
||||
- 假 `docker` 只识别明确 allowlist 的 compose/ps/inspect/exec 调用;
|
||||
- 所有数据库、origin、migration 和 SHA 均为 synthetic;
|
||||
- 测试进程不得解析真实 SSH config、调用真实 Docker 或读取仓库外 secret。
|
||||
|
||||
### 4.1 Orchestrator 失败矩阵
|
||||
|
||||
| SSH 退出 | Prisma marker | stable code | 预期 |
|
||||
| --- | --- | --- | --- |
|
||||
| 非零 | 缺失 | 有 | 传播 stable code;summary/report 先落地 |
|
||||
| 非零 | 任意 | 无 | `PRODUCTION_PREFLIGHT_REMOTE_FAILED` |
|
||||
| 零 | 缺失 | 无 | `PRISMA_STATUS_OUTPUT_MISSING` |
|
||||
| 零 | 存在 | 无 | 进入现有 status/reconciliation 成功路径 |
|
||||
|
||||
纯函数四组合可保留,但四个象限还必须全部通过真实 `node production-preflight.mjs` + fake SSH 子进程验证,防止 main 中的证据顺序或 exit handling 漂移。测试必须对真实缺陷敏感:将远端状态判断与 marker 判断次序调回错误实现时至少一个用例变红;去掉共享对象中的任一远端赋值时,成功/缺失合同用例变红。
|
||||
|
||||
### 4.2 Remote helper 失败矩阵
|
||||
|
||||
| 条件 | 预期码 | 必须早于 |
|
||||
| --- | --- | --- |
|
||||
| `BUILD_SHA` 缺失 | `BUILD_SHA_MISSING` | Compose / Prisma |
|
||||
| 完整 SHA 非法 | `PROD_TARGET_BUILD_SHA_INVALID` | Compose / Prisma |
|
||||
| 12 位 SHA 非法 | `PROD_TARGET_VITE_BUILD_SHA_INVALID` | Compose / Prisma |
|
||||
| 前缀不一致 | `PROD_TARGET_BUILD_SHA_MISMATCH` | Compose / Prisma |
|
||||
| Compose config 非零 | `PROD_TARGET_COMPOSE_CONFIG_INVALID` | runtime/Prisma |
|
||||
|
||||
假 Docker 对意外调用返回固定失败码,使测试能证明早停,而不是仅匹配最终 stderr。
|
||||
|
||||
## 5. 兼容性与停止条件
|
||||
|
||||
- provenance JSON 保留既有顶层字段;新增 `buildEnv` 是向后兼容扩展。
|
||||
- production preflight 的外部 env、报告文件、稳定错误码和远端脚本输入语义不变。
|
||||
- 若实现需要让 preflight import 会触发 provenance `main()`、执行 Git/SSH,必须停止并修正模块边界;导入必须无副作用。
|
||||
- 若共享 helper 迫使 production preflight 接受 unpublished HEAD、减少远端校验或把 secret 写入报告,停止并回到规划。
|
||||
- 本任务不在生产验证。上线收益通过本地合同和 reviewer 证明,下一次获批部署自然消费该改动。
|
||||
|
||||
## 6. Rollback
|
||||
|
||||
本任务只改本地 tooling。回滚为恢复三个调用点及测试到前一提交;不涉及 schema、生产配置或数据。若共享合同在本地门禁中暴露兼容问题,提交前直接修正,不以生产试跑验证。
|
||||
+1
@@ -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."}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# 部署合同单一事实源与失败路径覆盖:实施计划
|
||||
|
||||
## Phase A:共享合同
|
||||
|
||||
- [x] 在 `check-source-provenance.mjs` 添加一次返回完整 `{ BUILD_SHA, VITE_BUILD_SHA }` 对象的派生函数,导入无副作用;不提供两个独立 getter。
|
||||
- [x] 让 `collectSourceProvenance` 从共享结果生成 `commit`、`shortCommit` 与兼容的 `buildEnv`。
|
||||
- [x] 在 provenance focused tests 中覆盖合法结果、固定键、12 位前缀、非法提交。
|
||||
|
||||
## Phase B:调用点去重
|
||||
|
||||
- [x] `deploy.sh` 从 provenance `buildEnv` 读取并完整注入对象中的两值;禁止自行截取候选提交或只消费一个键。
|
||||
- [x] `production-preflight.mjs` 导入共享函数,映射非法候选为既有 `CANDIDATE_COMMIT_INVALID`,并从共享完整对象生成两项远端构建 env assignments。
|
||||
- [x] 保留 `production-preflight-remote.sh` 的缺失、格式、前缀和 Compose config fail-closed 校验。
|
||||
- [x] 更新 `validate-env-contract.js` 的静态合同:要求两个调用点消费共享来源,而不是只搜索任意 `BUILD_SHA=` 文本。
|
||||
|
||||
## Phase C:Harness
|
||||
|
||||
- [x] 提取/复用 fake SSH 运行 helper,避免每个失败用例重复搭建临时二进制。
|
||||
- [x] 通过真实 production-preflight 子进程覆盖完整四象限:非零+code+无 marker、非零+code+有 marker、非零+无 code+无 marker、零退出+无 marker;验证失败前已有稳定 summary/reportRef。
|
||||
- [x] 扩展 fake Docker mode 覆盖 Compose config 失败,并证明未继续到 Prisma/status 查询。
|
||||
- [x] 补齐构建值缺失、完整/短格式非法、前缀不一致;成功 SSH fixture 对账共享 `buildEnv`。
|
||||
|
||||
实施证据:focused tests 共 31 项通过。另在两个独立临时 detached worktree 中分别回退错误码判断次序、删除共享对象的 `VITE_BUILD_SHA`;对应回归测试均变红,证明 AC4 用例对两个历史缺陷敏感。临时 worktree 已删除,本轮未连接生产。
|
||||
|
||||
## Phase D:验证与 Review
|
||||
|
||||
- [x] `node --check deploy/trialrun/check-source-provenance.mjs deploy/trialrun/production-preflight.mjs`
|
||||
- [x] `bash -n deploy/trialrun/deploy.sh deploy/trialrun/production-preflight-remote.sh`
|
||||
- [x] `cd server && npm test -- tests/unit/scripts/check-source-provenance.test.js tests/unit/scripts/deployment-safety-contract.test.js`
|
||||
- [x] `node deploy/trialrun/validate-env-contract.js`
|
||||
- [x] task/process/governance/sensitive/diff 门禁。
|
||||
- [x] 启动只读 reviewer,重点核对共享来源是否真实被两个调用点消费、远端校验是否保留、harness 是否执行真实失败分支且无生产连接。
|
||||
|
||||
## Phase E:规约与提交
|
||||
|
||||
- [x] 使用 `trellis-update-spec` 更新现有 `deployment-trialrun.md`,记录候选构建身份的单一来源与远端消费验证职责。
|
||||
- [x] 中文 commit 引用 `08-09-deployment-contract-single-source-harness`(`621934a`)。
|
||||
- [x] 不部署、不运行生产 preflight;任务证据明确为 local tooling hardening。
|
||||
|
||||
## 质量检查记录
|
||||
|
||||
- 语法与 focused tests:`31/31 PASS`。
|
||||
- 环境合同:`R26 PASS`。
|
||||
- task validation、process baseline、Trellis governance、sensitive scan、docs registry、`git diff --check`:全部退出 0;docs registry 仅保留既有 `unregistered docs files: 22` warning。
|
||||
- 边界:所有验证均使用本地 synthetic fixture;未执行 SSH、真实 Docker、生产 preflight、部署或迁移。
|
||||
- reviewer 首轮发现“非零远端 + stable code + 不可读 marker”仍可能在证据前被 marker 校验覆盖;修复后增加真实子进程回归,复核 `PASS`、无新 finding。
|
||||
- residual:`deploy.sh` 的整对象解析主要由静态合同覆盖,尚无完整 fake deployment 子进程;键白名单、去重与恰好两项均 fail-closed,保留为低风险后续改进,不扩大本任务。
|
||||
|
||||
## Risky Files / Rollback Points
|
||||
|
||||
- `deploy/trialrun/check-source-provenance.mjs`:发布来源核心;任何 CLI JSON 兼容破坏都必须在 focused tests 阻断。
|
||||
- `deploy/trialrun/deploy.sh`:正式部署入口;只改变输入来源,不改变步骤顺序或远端动作。
|
||||
- `deploy/trialrun/production-preflight.mjs`:错误码/证据顺序不可回退。
|
||||
- `server/tests/unit/scripts/deployment-safety-contract.test.js`:fake 命令必须默认拒绝未知调用,避免宽松 mock 制造假 PASS。
|
||||
@@ -0,0 +1,84 @@
|
||||
# 部署合同单一事实源与失败路径覆盖
|
||||
|
||||
## Goal
|
||||
|
||||
消除正式 `deploy.sh` 与生产 preflight 对候选构建身份的重复定义,使完整候选提交、12 位公开前缀以及 `BUILD_SHA` / `VITE_BUILD_SHA` 映射由同一可执行合同派生;再用完全本地的假 SSH / 假 Docker harness 覆盖历史生产暴露的失败路径,让同类漂移在提交前失败,而不是通过真实 SSH 往返发现。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- 生产部署刚实际暴露过两类工具缺陷:`production-preflight.mjs` 曾先检查 Prisma marker、覆盖远端真实错误码;preflight 还曾漏注入 Compose 强制要求的 `BUILD_SHA` / `VITE_BUILD_SHA`。
|
||||
- 正式 `deploy.sh` 从 `check-source-provenance.mjs` 输出取得 `commit` 与 `shortCommit`;production preflight 又独立校验候选 SHA 并执行 `slice(0, 12)`。候选构建身份的派生逻辑因此存在两个实现点。
|
||||
- `production-preflight-remote.sh` 对完整/短 SHA 格式和前缀关系的校验是远端消费端纵深防御,不属于应删除的重复定义。
|
||||
- `server/tests/unit/scripts/deployment-safety-contract.test.js` 已有临时目录、假 SSH 和假 Docker 基础,也已有历史缺陷的部分回归断言;缺口是单一合同本身以及更多真实子进程失败分支,而不是再建一套测试框架。
|
||||
- 项目生产拓扑只有一套承载公网的 legacy `trialrun` 环境,没有同构预发;因此本任务只运行本地 synthetic harness,不连接生产、不执行部署。
|
||||
|
||||
## Scope
|
||||
|
||||
- 共享合同:`check-source-provenance.mjs` 中候选完整 SHA、12 位前缀和两个 Compose 构建环境键的可导入派生合同。
|
||||
- 消费调用点:`deploy.sh` 与 `production-preflight.mjs`;远端 helper 只保留消费端 fail-closed 校验。
|
||||
- 验证范围:provenance focused tests、现有 deployment safety fake SSH/Docker harness、env contract 与 Trellis 本地门禁。
|
||||
- 环境边界:全部使用本地 synthetic 数据和临时目录,不运行任何真实 SSH、Docker 部署或生产命令。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 候选构建身份单一事实源
|
||||
|
||||
- 在现有 `deploy/trialrun/check-source-provenance.mjs` 中提供可导入的候选构建身份函数和固定环境键合同。
|
||||
- 输入只接受 40–64 位十六进制完整提交;一次调用必须返回完整 `{ BUILD_SHA, VITE_BUILD_SHA }` 环境对象,固定派生完整 `BUILD_SHA` 与其前 12 位 `VITE_BUILD_SHA`。不得拆成两个可独立调用/修改的 getter,调用方不得自行重新截取、重新定义键名或只注入其中一项。
|
||||
- `collectSourceProvenance` 的 CLI 结果保留既有 `commit` / `shortCommit` 兼容字段,同时提供由同一函数产生的构建环境对象;不得破坏既有部署调用与审计输出。
|
||||
- `deploy.sh` 必须从 provenance 的构建环境对象读取并注入远端 Compose;`production-preflight.mjs` 必须导入同一函数生成远端命令输入。禁止任一调用方回退到 `git rev-parse HEAD` 或自行 `slice(0, 12)`。
|
||||
- `production-preflight-remote.sh` 继续在 `docker compose config` 前校验两值存在、格式正确且前缀一致;这是 fail-closed 消费验证,不得因单一来源而删除。
|
||||
|
||||
### R2 本地失败路径 harness
|
||||
|
||||
- 扩展既有 `deployment-safety-contract.test.js`,复用临时假 SSH / 假 Docker,不连接网络或真实 Docker daemon。
|
||||
- production preflight 的真实子进程路径必须覆盖完整四象限:远端非零且有稳定错误码/无 Prisma marker;远端非零且有稳定错误码/有 marker;远端非零且无可解析错误码/无 marker;远端零退出但无 marker。前三种分别传播原码或返回 `PRODUCTION_PREFLIGHT_REMOTE_FAILED`,最后一种返回 `PRISMA_STATUS_OUTPUT_MISSING`。
|
||||
- 失败路径仍须先写稳定脱敏 summary 与相对 report reference,再返回失败;不得为了测试错误码而跳过证据合同。
|
||||
- 远端 helper 必须覆盖 Compose config 失败、缺失构建值、完整/短 SHA 格式错误和前缀不一致,并断言每种情况均在 Prisma status / 数据库写入前 fail-closed。
|
||||
- provenance 测试必须覆盖完整/短 SHA 与固定环境键的同源性,以及非法提交输入;受控 SSH 成功 fixture 必须断言远端收到的两值等于共享合同输出。
|
||||
|
||||
### R3 边界与兼容性
|
||||
|
||||
- 不改变 production target gate、migration reconciliation、rollback window、错误码语义、部署顺序或公网 smoke 行为。
|
||||
- 不把备份、WARCH3 timer、自动备份脚本的全部 build-SHA 校验并入本轮;它们是不同运行合同,后续如需统一另行规划。
|
||||
- 不执行 SSH、生产 preflight、部署、迁移、配置修改、账号操作或业务写入。
|
||||
- 不以删除远端防御、降低 Compose 插值、放宽 SHA 格式或只加静态文本断言换取测试通过。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] AC1:候选完整 SHA、12 位前缀及两个 Compose 环境键由一次函数调用返回的完整 `{ BUILD_SHA, VITE_BUILD_SHA }` 对象产生;没有两个独立 getter。`deploy.sh` 与 production preflight 均按整对象消费,不再各自派生短 SHA或只选择性注入一个键。
|
||||
- [x] AC2:既有 provenance CLI 字段保持兼容,部署脚本仍从已验证且已发布的候选提交生成远端构建输入;没有本地 HEAD fallback。
|
||||
- [x] AC3:远端 helper 保留格式/前缀/Compose fail-closed 校验,且失败发生在 Prisma status、迁移或任何业务写入之前。
|
||||
- [x] AC4:本地真实子进程 harness 覆盖远端错误码优先级完整四象限、Compose config 失败及构建身份缺失/非法/不一致;回退错误判断次序或任一调用点的完整对象注入时,对应回归用例必须失败。
|
||||
- [x] AC5:focused Vitest、provenance tests、`node --check`、`bash -n`、env contract、task/process/governance/sensitive 与 `git diff --check` 全部通过。
|
||||
- [x] AC6:只读 reviewer 核对单一事实源、调用点、fail-closed 顺序与测试有效性;reviewer 不修改代码。
|
||||
- [x] AC7:任务结论明确仅为本地部署 tooling hardening;未连接或验证生产,不宣称完成任何新部署。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 生产发布、预发建设、生产邮件、身份引导拓扑或 secret 持久化方式变更。
|
||||
- 数据库 schema/migration、产品功能、认证、权限或合同业务状态机修改。
|
||||
- 统一所有 backup/timer/monitor 的 build identity 合同,或重写全部部署脚本为单一语言。
|
||||
- 处理 8 条会话夹具失败、员工负向验收、合同 UI 样本、管理员手册附件承诺。
|
||||
|
||||
## Risks and Deferred Items
|
||||
|
||||
- Bash 与 Node 的远端命令格式仍然不同;本轮统一的是候选构建身份的来源、键和派生,不以跨语言模板引擎扩大风险。
|
||||
- 远端验证与本地派生看似重复,但承担不同责任:前者防传输/调用漂移,后者建立单一来源。测试必须保留两层。
|
||||
- 现有 deployment-safety-contract 测试文件较大;本轮优先复用 helper,避免继续复制 fake SSH/Docker 搭建逻辑。是否拆分测试文件不作为验收条件。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- `node --check deploy/trialrun/check-source-provenance.mjs deploy/trialrun/production-preflight.mjs`
|
||||
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/production-preflight-remote.sh`
|
||||
- `cd server && npm test -- tests/unit/scripts/check-source-provenance.test.js tests/unit/scripts/deployment-safety-contract.test.js`
|
||||
- `node deploy/trialrun/validate-env-contract.js`
|
||||
- `python3 ./.trellis/scripts/task.py validate 08-09-deployment-contract-single-source-harness`
|
||||
- `node scripts/qa/check-process-contract.mjs --baseline --json`
|
||||
- `node scripts/qa/check-trellis-governance.mjs --json`
|
||||
- `node scripts/qa/check-sensitive-content.mjs`
|
||||
- `git diff --check`
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 无。范围、兼容性和风险边界已由现有代码、生产故障证据及用户确认的实施顺序确定。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "deployment-contract-single-source-harness",
|
||||
"name": "deployment-contract-single-source-harness",
|
||||
"title": "部署合同单一事实源与失败路径覆盖",
|
||||
"description": "收敛 deploy.sh 与 production preflight 的候选构建身份合同,并以本地 fake SSH/Docker harness 覆盖历史失败路径;不连接或修改生产。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "deploy/trialrun/check-source-provenance.mjs, deploy.sh, production-preflight.mjs/remote helper, validate-env-contract.js and focused deployment safety tests",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-09",
|
||||
"completedAt": "2026-08-09",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "本地 tooling hardening 已实现并通过 31/31 focused tests、全部门禁与只读 reviewer 复核;未连接生产。低风险 residual:deploy.sh 整对象解析尚无完整 fake deployment 子进程。",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户