接入发布候选回执强制校验(09-06-release-gate-integrity)

这个提交包含在:
2026-09-19 14:25:33 +08:00
父节点 1f8d5b658a
当前提交 16b9cad98c
共修改 15 个文件,包含 722 行新增和 91 行删除
+6 -1
查看文件
@@ -2,13 +2,18 @@
## Convention: Local Release Candidate Receipt
- `bash scripts/qa/run-release-gate.sh verify-candidate` reuses existing static and managed full-local stages and never calls deployment. R1 enforcement in existing deployment/bootstrap entries remains a separately approved change.
- `bash scripts/qa/run-release-gate.sh verify-candidate` reuses existing static and managed full-local stages and never calls deployment. The approved R1 local integration requires this receipt in every write entry; actual push/deployment still needs separate authorization.
- `release-gate-result.mjs` owns an exclusive empty directory under `artifacts/release-gate/`, records INCOMPLETE before execution, and derives final results from fixed ordered phases, source/index/tracked and untracked input digests, current policy/toolchain, owned environment/cleanup, G19 full discovery and every local result plus witnesses/acceptance specs. Missing, truncated, linked, changed, interrupted or partial evidence is not PASS.
- `qa-stage-record.mjs` records full-local phases through failure cleanup. Keep plain shell calls under errexit: wrapping compound helpers in `if`/`||` can suppress their internal failures. Preserve the original failing exit code if evidence cleanup also fails.
- Persist actual browser selection separately from discovery: exact derived spec arguments, supplementary arguments, mode, retries, forbid-only and worker count. Ordinary release verification rejects supplementary filters, shards, missing projects, changed budgets, skips, retries, expected failures and inconsistent statistics/time ranges.
- A dirty development tree may have `localChecksPassed=true`, but returns PARTIAL/exit 2 and `deploymentEligible=false`. Offline `verify` re-reads and hashes evidence and recomputes against the current checkout. Local proof does not discharge the 12 same-candidate public obligations, independent reproduction, or operator approval.
- Existing project-wide skip flags accept only exact 0/1. Each or both require nonempty reason/operator/approval reference, persist affected stages, and end PARTIAL/exit 2. Other release modes reject them before stage side effects. These metadata do not grant authorization; no approval time is invented.
- Validate with pure receipt counterexamples, actual shell/fake-tool invocation, source mutations, and existing deployment lifecycle/safety regressions. Fake tools must intercept every external action. Evidence is auditable, not a cryptographic signature.
- Explicit consumption uses `RELEASE_GATE_RECEIPT_DIR` (absolute or repository-relative, confined beneath `artifacts/release-gate/`); `RELEASE_GATE_ARTIFACT_DIR` only selects new output. Never search for a latest receipt. `release-gate-result.mjs require` re-reads all evidence and current source/policy/toolchain; caller PASS/skip flags do not grant eligibility. The three deployment entries are part of the policy digest.
- `ci-release/all` reuse a supplied eligible receipt or run the verification-only child once, then call production identity bootstrap. `all` retains its extra smoke-local before that verification. Explicit invalid/empty receipts fail without fallback tests. `deploy` and direct wrappers require a receipt and never start full tests themselves. Repeated lower-level assertions only revalidate evidence.
- Pin the returned commit across bootstrap and wrapper preflights with `RELEASE_GATE_EXPECTED_COMMIT`; it is an additional equality constraint, never an override. Match provenance/archived `SOURCE_COMMIT` to it. Recheck before each lower entry and before the first remote write (the mkdir preceding rsync), not merely before container work. Initial rejection must precede SSH/config/provenance-fetch access; do not invoke failure collectors that read real configuration for a rejected publication.
- Both read-only flags accept only exact 0/1. Bootstrap identity-only exits before database lookup; bootstrap/existing preflight-only exits before deploy.sh. Release-runner publication modes and raw deploy.sh reject either read-only flag rather than treating it as write permission or publication success.
- Add boundary checks with synthetic disposable repos, private child HOME/PATH, audited fake config and fail-closed SSH/rsync/curl/git-fetch/Docker tools. Execute actual local entry scripts; remote-body lifecycle tests remain separate. Source mutations must expose an observable violation, not just remove a matching string. Freeze and commit the final reviewed candidate before the single full release run; task-only records and old partial receipts cannot substitute for it.
## Convention: Managed OCR And Concrete Visual Coverage
@@ -1,3 +1,72 @@
## 2026-09-19 F1诊断处置及最终冻结
未改执行输入的单文件52项一次通过;独立复核承认三次R1校验不在该测试执行范围内。保留原失败与根因未知,不做推测性修复/预算扩容,不合并分阶段重验。定向证据足以支持本地提交,不能替代完整候选验收。
按已批准顺序:本次本地提交封存15路径及验收记录;最终clean SHA仅运行一次既有verify-candidate,沿现有受管环境/原浏览器版本/并发/预算/零重试/33页7组/分层覆盖,不另造验收流程。使用新独占证据目录和已有浏览器缓存的物理副本,无下载、无历史覆盖;运行期间仅写ignored证据。失败即保留并回报,不自动重试。具体SHA/命令与环境边界在提交后的ignored交接中固定,不再改本提交的受管文件。推送/部署未授权。
## 2026-09-19 F1超时最小诊断(沿用已批准定向验证范围)
静态提取与摘要已排除“三次R1 require启动开销进入生命周期用例”的假设:测试只执行REMOTE片段,三个校验都在片段外,片段及测试夹具未变。不改变生产校验、测试预算、并发、重试或断言,不凭一次耗时比较修改产品。
Claude仅运行未改的deployment-db-lifecycle.test.js完整52项一次;ignored封装器核执行输入前后SHA、记录真实exit/UTC/耗时、每秒采样本地load/CPU与内存压力/cgroup计数。采样只描述当次环境,不追认前次因果。成功也保留原exit1并交主会话判断;失败停报,不循环重跑。此前125/另外46项/独立代码结论沿原输入保留,本轮不重跑。该诊断属于既有失败验证,不改变验收范围;真实部署/推送仍未授权。
## R1 接入方案(2026-09-19,已批准本地实施)
### 当前事实与授权
最新:用户已明确回复“是”,批准本节本地实施及新版执行顺序;以下规划时点描述保留追溯。测试交Claude,推送与部署未授权。
基线为本地分支codex/20260919-reviewed-candidate的1f8d5b65,前两笔本地提交已完成。用户同意推进R1方案;本轮仅源码只读分析和任务合同更新,没有执行测试、真实发布入口、生产访问或提交。本地接入/启用已获用户具体方案批准(实现结果与验收状态见implement顶部),生产部署仍独立授权。
旧r6全绿属于旧输入,budget-full-r1中止FAIL/130;当前候选不能借它们取得资格。本节建议将“本地接入拒绝未验候选”与“真正对外发布”分开:本地补丁与定向验证先完成,最终clean SHA只跑一次完整门禁,合格后才可在另获部署授权时使用真实入口。该顺序取代旧设计中容易导致先全量、再改入口、又全量的执行歧义,不降低完整通过前不得发布的要求。
### 最小修改范围与行为
| 文件 | 接入位置与行为 |
| --- | --- |
| scripts/qa/release-gate-result.mjs | 复用verifyReleaseEvidence/readBundle/facts等,增加供入口使用的导出断言及require CLI;重算资格,失败非零,不信任回执中的PASS或调用者布尔值。明确校验期望发布commit。 |
| scripts/qa/run-release-gate.sh | verify-candidate维持无部署;ci-release/all复用同一验证路径或消费显式指定有效回执,再走production-identity-bootstrap;deploy只消费回执,不隐式跑全量。保留all额外smoke-local,安排在唯一verify-candidate之前;不将它冒充必需阶段,也不重复static/full-local。 |
| deploy/trialrun/production-identity-bootstrap.mjs | 正常发布在读取SSH配置/探测真实目标前要求回执;保留身份发现、origin核验和数据库身份。进入deploy-existing前再核对。identity-only/preflight-only明确只读分支不产生发布资格。 |
| deploy/trialrun/deploy-existing.sh | 正常发布在远端聚合体检前要求回执;保留来源/身份/迁移/回滚窗口preflight,并在调用deploy.sh前用实际source_commit再校验。只读preflight仍能单独运行且必须退出。 |
| deploy/trialrun/deploy.sh | 参数合法性检查后、来源fetch或环境读取前验证;取得SOURCE_COMMIT后绑定同一SHA,并在第一条远端写入mkdir之前再次重验。保留git archive不可变同步、existing/initialize边界、数据库生命周期和部署健康检查。 |
策略摘要显式纳入新增消费入口;全仓源码快照、工具链/CI并发、证据文件摘要、分层清单、清理完成与clean判据全部保留。无schema/业务/迁移/账号/编号/差旅状态变更。仅这5个执行文件及直接配套测试、已有部署规约/任务记录;若暴露新的业务风险另列。
### 回执输入与无重复执行合同
- 拟用RELEASE_GATE_RECEIPT_DIR传递显式回执目录,仅允许工作树artifacts/release-gate下的受限真实目录;沿用反路径逃逸/符号链接/证据SHA检查。RELEASE_GATE_ARTIFACT_DIR继续只表示生成新回执的位置,两者职责不混用。
- 共享断言重算完整release-predeploy资格,并把当前HEAD、回执commit及实际SOURCE_COMMIT绑定;缺失、失效、dirty、工具/策略变化或不完整均拒绝。不得产生GATE_PASSED、SKIP_RELEASE_GATE等信任开关,不自动选择“最新”或历史通过回执。
- verify-candidate仅验证。ci-release/all是显式完整发布命令:有显式回执则重验复用;无回执才调用verify-candidate一次。bootstrap/deploy-existing/deploy.sh及deploy模式无回执立即拒绝并提示先验证,不自动启动耗时全量;这替代旧§2“高层缺回执自动验证”的默认行为。
- 下层重复的是廉价回执校验,不是测试执行。verify-candidate绝不回调部署,生产引导器绝不回调ci-release/all;成功路径部署链只调用一次。
- 对PRODUCTION_IDENTITY_ONLY/PRODUCTION_READ_ONLY_PREFLIGHT严格校验0/1。只读入口不要求发布回执,但任何只读标记都不能继续进入写入;ci-release/all/deploy拒绝以只读标记伪装发布成功。原纯只读脚本不变,本轮也不实际执行它们。
- 阻断时只输出稳定原因与下一步,不回显环境/SSH配置/原始payload。回执不是部署授权,也不是不可伪造签名;源码有写权限的操作者不在防伪承诺内。
### Claude负责的限定验证(获批实施后)
主会话实现后交Claude执行测试与只读review;现在不运行。本轮新增/扩充现有release-gate-result.test.mjs、release-gate-shell.test.mjs,必要的跨入口执行测试放release-gate-enforcement.test.mjs及私有fixture;复用已有部署生命周期/来源测试,不新增生产测试开关。
1. 表驱动覆盖6种入口:ci-release、all、deploy、bootstrap、deploy-existing、deploy.sh。缺/旧/错profile/伪PASS/dirty/不完整回执时,正常发布不得读取真实配置或触发远端命令;写动作计数必须0。
2. 合成有效回执只允许原生产链一次;核static/full-local至多一次、显式回执复用不再跑static/full-local(all仍保留额外smoke-local)、无递归;仅捕获fake命令,不真实发布。
3. 上层已过、下层调用前改HEAD/index/受控文件或篡改证据时仍拒绝;实际归档SOURCE_COMMIT与受验SHA不同必须拒绝。
4. 独立只读分支可完成fake只读检查但写动作0;各种内部/skip/只读标记不能变成写入豁免。来源、迁移、init/server/worker隔离与120秒健康检查合同保留。
5. 对真实入口/判定源码作隔离副本变异:去掉上层前置、下层重验或放宽commit/clean判定时,对应负例必须失败;不只grep字符串。所有SSH/Docker/rsync/git-fetch、SSH配置和真实env读取必须受测试替身封闭,禁止真实外联/配置读取,临时文件仅清理本轮owner。
定向命令(实际补丁落地后Claude执行,数量按实际记录,不提前写通过):
```bash
node --test scripts/qa/tests/release-gate-result.test.mjs scripts/qa/tests/release-gate-shell.test.mjs scripts/qa/tests/release-gate-enforcement.test.mjs
npm test --prefix server -- tests/unit/scripts/deployment-db-lifecycle.test.js tests/unit/scripts/deployment-safety-contract.test.js tests/unit/scripts/check-source-provenance.test.js
```
仅因修改相关文件才纳入对应测试,不跑浏览器/PG/业务全量。规划时新enforcement测试尚不存在;现已落地,命令待Claude执行,不能当作已通过。若验证需要超出这里的范围,先报告缺口;不恢复冻结差旅、不修无关旧警告。
### 提交、完整验收与回退
- 本地补丁获批→主会话实施→Claude定向测试及只读复核→主会话按批准范围提交最终输入;不先跑完整再改产品/QA代码。
- 最终候选冻结后,完整门禁交Claude一次执行;期间不改受管代码、测试、任务或规约,只写ignored证据。失败保留并按具体缺陷处理,不盲目重跑。
- 完整通过后仍须另获推送/部署授权;首次部署独立执行/回执比对及同候选12项公网义务保留,不能把NOT_RUN_LOCAL或本地PASS当成公网通过。生产编号与银行接受性另列。
- 若R1补丁有误,停止发布并保留失败,修复或按授权回退本补丁;不增加disable开关,不借回退无门禁发布。没有数据库/真实配置变更,无须数据恢复;既有线上版本不受本地规划/修复影响。
## 2026-09-19 r5-fixed-r1登录复用边界
四完整文件31项30通过/1失败,采购整链、R75及视觉25项全部通过;付款在经理复核登录后按已有redirect参数返回付款页,而共享login仍应验portal。最小修正仅允许显式reuseLoginPage且当前为无search/hash的规范/login时省略goto;带redirect的登录页必须走原goto('/login')清除返回目标。保留真实表单和portal/profile断言、全部付款下游及300秒;不修改认证产品或接受任意登录落点。原失败保留,类型检查后相同四完整文件复验,再从static完整候选。
@@ -1,4 +1,64 @@
## 2026-09-19 用户已授权主会话执行两笔本地提交(最新)
## 2026-09-19 R1定向验收完成,保存本地候选后进入一次完整门禁(最新)
Claude单次诊断52/52通过、exit0、57.515秒,原三例2.027/1.837/1.940秒,5秒预算不变;27条执行输入前后零漂移。主会话实算与原125项及另两份46项所依赖代码一致,六项独立代码review均PASS。F1原“三次require导致这52项超时”的归因被独立更正;原exit1/3次超时保留,实际成因仍未确定,不宣称已根治或整组98曾一次全绿。
决策:不修改无证据的生产/测试逻辑,不合并三处校验,不提高预算。F1不作为已知R1产品缺陷阻止本地提交,保留时序观察;最终完整门禁仍必须独立通过。证据见r1-enforcement-20260919/f1-triage-20260919/diagnostic-reception.json。
- [x] 五执行文件接入、跨入口保护及源码变异测试、部署规约完成。
- [x] 125项定向通过、六项独立代码复核通过;原失败文件52项补充验证通过,历史失败保留。
- [x] 主会话决定按用户既有明确授权提交本轮15路径,不含ignored证据。
- [ ] 本地提交执行/候选SHA及clean核验以artifacts/qa-governance/r1-enforcement-20260919/local-commit/result.json为准。
- [ ] 提交后冻结整树(含任务记录),Claude一次verify-candidate;不重复前置定向测试,不拼接旧结果。结果只写ignored路径,避免记录变更使候选dirty。
- [ ] 推送/部署另行授权;12项同候选公网验收、生产编号/银行真实接受性及差旅冻结等外部边界保持。
本轮受测执行/测试/spec代码未在独立验证后改变,仅本任务四份接收/决策记录变化。本提交用于固定最终待验输入,不能宣称新SHA已有完整通过。完整门禁具体执行说明在artifacts/qa-governance/r1-enforcement-20260919/final-gate-claude-request.md;主会话不启动测试或部署。
## 2026-09-19 R1独立结果接回;F1归因更正与单文件诊断(最新)
Claude组1为125/125通过、无跳过;组2为98项中95通过/3个5000ms超时,exit1;组3治理及diff检查通过。六项只读代码复核均PASS。接收前实算原15份after及28条输入全部匹配;报告/日志原样保存,reception登记其SHA及四份后续任务记录变更。
主会话静态核实:生命周期测试只提取deploy.sh的REMOTE heredoc执行;before/HEAD/current片段均8419字节、SHA=d47bdba0ae64f6ca31e5af7d08b067671434fc1c42090d04050296d3a88baff1。三处新增require都在片段外、在该套件中不执行;测试及Python/CJS夹具、Vitest/Compose输入亦未变。因此原F1“新增三次node校验导致这52项超时”的归因不成立,不能据此合并保护;三项超时事实与原因未定状态保留。
- [x] 接收125通过、六项独立静态PASS及原组2失败;旧证据不覆盖。
- [x] 排除F1指称的直接执行路径,保留三处发布重验和原5秒预算。
- [ ] Claude仅对未改的deployment-db-lifecycle.test.js 52项执行一次带本地资源采样的定向诊断;不重跑另外已通过125/46项或全量。
- [ ] 接回诊断后决定必要修复/提交;本批仍未提交,deploymentEligible=false。
本轮只改这四份任务记录并追加ignored证据/诊断封装器;执行/测试/spec源码均保持Claude审查后的字节。主会话不运行测试,不凭当前压力猜测前次原因,不以一次补充通过改写原失败。执行边界、精确命令及输入摘要见artifacts/qa-governance/r1-enforcement-20260919/f1-triage-20260919/claude-request.md。
## 2026-09-19 R1本地实现完成,待Claude定向验证(最新)
已完成五个执行文件接入:共享require断言重算资格并绑定HEAD/回执/来源提交;正常发布入口缺回执立即拒绝;ci-release/all无显式回执时只调用一次verify-candidate,有回执则重验;all仍保留额外smoke-local。bootstrap→deploy-existing→deploy.sh各层独立复验,最后复验在远端mkdir之前。只读标记严格0/1,空值无豁免,只读分支保留并终止。拒绝发布时不调用可能读真实配置的失败收集器。回执路径统一相对仓库解析。
原shell测试共用fixture已抽出,新增跨入口行为/源码变异测试;fake环境只用合成HOME/SSH配置、私有PATH和受控命令,不执行真实远端脚本。生命周期仍由既有独立测试验证。没有放宽断言、重试、业务范围或全量门禁。部署规约首节同步稳定接口与时序。
主会话仅执行node --check、bash -n和git diff --check,结果见syntax-checks.json;未执行任何unit/browser/full-local/PG测试,未运行真实发布入口。测试结果及独立复核仍PENDING,当前候选deploymentEligible=false。本批未提交,待Claude通过后按已有授权本地提交;之后才冻结最终候选,安排一次完整门禁。旧budget-full-r1 FAIL/130、各历史通过及三条例外均保持原归属。
交接:artifacts/qa-governance/r1-enforcement-20260919/claude-request.md。包含before快照、15路径增量清单/SHA、incremental-review.diff与受测输入摘要。Claude仅执行六份指定测试及治理检查、只读审增量,不改代码/任务/原始证据、不接触线上。独立报告回收后再登记真实计数,不预填PASS。
## 2026-09-19 R1本地实施已获批准(最新)
用户明确回复“是”,批准design顶部五文件接入、Claude定向测试/只读复核及最终clean候选一次完整门禁顺序。主会话实施与自查,测试执行交Claude。无推送/生产部署授权。底层首个远端写入实际是mkdir,最后复验必须位于它之前,不能只放在rsync前。
- [x] 具体方案及执行顺序获批。
- [x] 五入口文件与直接配套测试落地;仅语法/格式检查通过,运行验证交Claude。
- [ ] Claude定向验证和独立复核。
- [ ] 验证通过后本地提交最终候选。
- [ ] 冻结候选一次完整门禁;外部义务独立保留。
## 2026-09-19 R1具体方案已完成(最新)
用户同意先准备R1接入方案。已只读核对统一runner、共享回执、bootstrap、deploy-existing、底层deploy.sh与现有fake测试。方案落design顶部:5个执行文件复用一个断言,显式回执目录/commit绑定,正常写入多层重验;保留只读分支、既有生产安全合同及零豁免。直接包装器缺回执拒绝,不偷偷启动全量;ci-release/all只进行一次明确的完整验证或复用有效回执。
- [x] 具体入口、变更范围、输入/拒绝/只读行为与回退方案已落盘。
- [x] Claude定向测试矩阵及预计两条命令已列明;不提前执行不存在的新测试,不重跑浏览器/业务全量。
- [ ] 单独批准R1本地接入与新版执行顺序;目前没有修改执行代码或实际启用。
- [ ] 主会话实施,Claude定向验证与独立复核,然后保存最终候选。
- [ ] 最终clean SHA一次完整门禁;推送/部署及12项公网验收按原独立授权边界执行。
本次只更新本任务PRD/design/implement/task.json,不提交、不测试、不连接生产/ARM,不读取真实配置密钥备份。此前本地提交、原回执和冻结包保持。
## 2026-09-19 用户已授权主会话执行两笔本地提交(已完成)
用户明确要求“提交就你来提交吧,不用发给claude了”。主会话接管最终分组及本地提交,之前Claude打包请求不再等待执行;授权限本地提交,不含推送、R1启用、部署、生产编号或真实业务动作。
@@ -1,3 +1,25 @@
## 2026-09-19 R1本地补丁可提交,最终完整验收待执行
独立代码复核六项及125项定向检查通过;生命周期52项在原5秒预算下补充通过,原3项超时与未知成因保持记录。没有已证实的R1缺陷需要改代码,按已有授权保存最终候选。本地定向完成不勾销完整验收:冻结clean提交后一次verify-candidate,外部部署/公网义务仍分别授权与验收。
## 2026-09-19 R1限定验收最新余额
125项强制门禁测试与六项独立代码复核PASS;部署生命周期文件3项5秒超时仍开放。该测试执行的REMOTE片段修改前后完全相同,新增R1校验未进入其执行路径,不采纳合并校验建议。按design最新诊断计划仅查这一文件,保留原预算/失败,不启动全量或发布。
## 2026-09-19 R1当前状态:本地实现完成,未运行验证
五文件接入与跨入口合成测试已落地,主会话语法/格式检查通过;Claude定向测试和独立复核待回。不勾销R1最终验收,不具备发布资格;本批未提交/推送/部署。最新清单与交接见implement顶部,以下均保留各自历史时点。
## 2026-09-19 R1本地接入及新版顺序已批准
用户明确确认design顶部方案:先本地接入拒绝未验候选、Claude定向验证及独立复核,再提交最终候选、一次完整验收;完整通过前不实际发布。此段替代旧R1“本地接入前先完整全绿”的时序限制,部署与推送仍另行授权。历史结果不抵扣新输入。
## 2026-09-19 当前阶段:R1接入方案已细化,待单独批准实施
两笔本地提交已完成(729d9057、1f8d5b65);预算补丁独立复核及两项必修关闭。当前R1仍未接入,当前候选完整门禁仍未通过,12项公网未执行。本轮用户同意先准备R1方案,未运行测试或真实入口;具体5文件范围、回执接口、只读边界、Claude定向矩阵和一次最终完整验收顺序见design.md顶部R1接入方案。
待批准的具体变化:在本地接入拒绝未验候选的校验,最终clean SHA完整通过后才允许实际发布;直接部署包装器缺回执立即拒绝,不隐式启动全量。现行“真实启用另行授权”边界保留;本轮只有规划批准,不等于接入、推送或部署授权。以下旧“最新”记录仅为各历史时点,现状以本段及implement顶部为准。
## 2026-09-19 最新本地验收
g20-full-r6同一冻结候选完成static及12个full-local阶段,4437后端通过/1原条件跳过、372本地浏览器全过,零失败/跳过/重试。7组33页完整;PARTIAL仅dirty_candidate,localChecksPassed=true,无部署资格。R1真实入口启用及独立、公网12义务、旧registry保护例外仍开放;完整阶段/计时/来源边界与余额见implement顶部。历史FAIL原样保留,不能把本地通过写为所有交付完成。
@@ -2,7 +2,7 @@
"id": "release-gate-integrity",
"name": "release-gate-integrity",
"title": "部署门禁完整性与候选验收回执",
"description": "G18在用覆盖和三处产品修复已本地验证;原四文件并发4/4、认证/OCR18/18通过。全量合同、WebKit、R1、独立及真实操作仍开放。",
"description": "R1本地接入、125项定向测试及六项独立复核通过;生命周期52项补充通过,原3项超时保留且归因更正。按授权保存最终本地提交,等待同一clean候选一次完整门禁及独立授权后的公网义务。",
"status": "in_progress",
"dev_type": null,
"scope": "release-gate",
@@ -21,7 +21,7 @@
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "2026-09-19 根据用户对重复全浏览器成本的反馈,中止budget-full-r1;后端4446与PG28通过保留,浏览器29通过/4中断/339未运行,回执FAIL/130,环境清理PASS。待Claude实际补丁复核,发布完整门禁延期,不再重跑无关测试。\n2026-09-19 预算并发修复已定向通过 PG28/单测526;待新完整候选与独立审查,旧r6为历史输入。\n2026-09-19 用户独立核实确认4437/r6时序及累计diff基线,并报告当前采购测试75 passed/EXIT=0、SHA一致;数量与证据归属争议闭合。完整独立业务review、发布边界及两条例外保持。\n2026-09-19 用户转述局部审查已核对:UTC/北京时间混用及累计diff误作运行后diff;r6实际在P10之后,4437准确,1162源码快照条目与当前一致。证据review-provenance-20260919.json,完整独立review仍PENDING,原证据不改。\n2026-09-19 g20-full-r6:4437后端通过/1原跳过,372本地浏览器全过;PARTIAL仅dirty_candidate,独立PENDING/无部署,旧registry保护例外保留且无新增漂移。当前余额见implement顶部。\n2026-09-18 G19分层验收已批准并本地实施验证,74+7合成检查和2条冻结保护通过;公网12义务/完整门禁/WebKit/R1/独立及真实事实保持开放。\n2026-09-18 G18:QA39、产品131、并发4/4、认证/OCR18/18,33路由项与R108通过;G18时点发现64/363;G19最新发现见g19-discovery/summary.json,旧完整full-r2仍失败。候选与余项见local-coverage-g18-20260918。无提交部署,独立PENDING。\n2026-09-18 G16/G17:普通 full-r2 65/359仍失败;14原失败及2串行缺失已有定向通过,1WebKit/2冻结差旅保留。当前发现64/358,有临时SHA采集告警;QA/治理/保护与增量候选见 full-gates-followup-20260918。整体in_progress、独立PENDING,六组余项见implement顶部,无提交部署/外部操作。\n2026-09-18 G12/G13本地收口,G14真实PWA通过但WebKit1条未验;原58现53重验/4排除/1未验,原历史不改。G15复核及外部事实清单与新候选在retained-gates-closeout-20260917;普通full-local/AC7完整/R1和独立PENDING,不提交部署或真实操作。\n2026-09-17 W8本地对账:四处迁移状态谓词修复已随b87cc55发布;该事实仅闭合追加预检阶段,不代表R1普通完整门禁、阶段B合同用例或58条诊断观测已通过。R1/阶段B继续原冻结,先按保留在用场景分类再另定限定批次,不盲修待精简旧测试。 原回执从主副本接回,见implement顶部;本轮无新的远端/提交发布,原验收未补勾。\n2026-09-07 §10已实施并完成一次环境有效的全套诊断,增量待用户独立复核。原21文件加6个QA环境/测试文件,共27文件;本轮产品源码和业务spec零改动,51项针对性验证通过。前三次启动在环境前置拒绝,资源已核对清理;修复跨user namespace读取父namespace的EACCES,改由父runner经本轮Unix IPC即时实测,不降级。第四次环境PASS、源码前后不变:65文件275项,199 passed/54 failed/4 timedOut/18 skipped,58条观测涉及36文件,不是独立产品缺陷计数;诊断仍PARTIAL/2。服务端193文件1692项通过,workflow场景8两个来源仍失败。R102 6/6通过;合同仍卡旧更多,续约/归档未执行;PWA在serviceWorker.ready超时且该失败无trace附件。诊断67分03秒不是普通全绿成本。旧207撤销决策资格但保留原件;阶段1、阶段B、场景8、经理缺口和R1均未扩大执行。无提交/push/归档/部署/生产操作,整体任务未完成。",
"notes": "2026-09-19 R1定向125通过、另外两份46通过、生命周期52补充通过;原组2 exit1及3次超时保留,新增require不在其执行片段内。六项代码独立复核PASS,无理由合并保护或增预算。允许本地提交,最终完整门禁尚未执行;不具备发布资格。\n2026-09-19 根据用户对重复全浏览器成本的反馈,中止budget-full-r1;后端4446与PG28通过保留,浏览器29通过/4中断/339未运行,回执FAIL/130,环境清理PASS。待Claude实际补丁复核,发布完整门禁延期,不再重跑无关测试。\n2026-09-19 预算并发修复已定向通过 PG28/单测526;待新完整候选与独立审查,旧r6为历史输入。\n2026-09-19 用户独立核实确认4437/r6时序及累计diff基线,并报告当前采购测试75 passed/EXIT=0、SHA一致;数量与证据归属争议闭合。完整独立业务review、发布边界及两条例外保持。\n2026-09-19 用户转述局部审查已核对:UTC/北京时间混用及累计diff误作运行后diff;r6实际在P10之后,4437准确,1162源码快照条目与当前一致。证据review-provenance-20260919.json,完整独立review仍PENDING,原证据不改。\n2026-09-19 g20-full-r6:4437后端通过/1原跳过,372本地浏览器全过;PARTIAL仅dirty_candidate,独立PENDING/无部署,旧registry保护例外保留且无新增漂移。当前余额见implement顶部。\n2026-09-18 G19分层验收已批准并本地实施验证,74+7合成检查和2条冻结保护通过;公网12义务/完整门禁/WebKit/R1/独立及真实事实保持开放。\n2026-09-18 G18:QA39、产品131、并发4/4、认证/OCR18/18,33路由项与R108通过;G18时点发现64/363;G19最新发现见g19-discovery/summary.json,旧完整full-r2仍失败。候选与余项见local-coverage-g18-20260918。无提交部署,独立PENDING。\n2026-09-18 G16/G17:普通 full-r2 65/359仍失败;14原失败及2串行缺失已有定向通过,1WebKit/2冻结差旅保留。当前发现64/358,有临时SHA采集告警;QA/治理/保护与增量候选见 full-gates-followup-20260918。整体in_progress、独立PENDING,六组余项见implement顶部,无提交部署/外部操作。\n2026-09-18 G12/G13本地收口,G14真实PWA通过但WebKit1条未验;原58现53重验/4排除/1未验,原历史不改。G15复核及外部事实清单与新候选在retained-gates-closeout-20260917;普通full-local/AC7完整/R1和独立PENDING,不提交部署或真实操作。\n2026-09-17 W8本地对账:四处迁移状态谓词修复已随b87cc55发布;该事实仅闭合追加预检阶段,不代表R1普通完整门禁、阶段B合同用例或58条诊断观测已通过。R1/阶段B继续原冻结,先按保留在用场景分类再另定限定批次,不盲修待精简旧测试。 原回执从主副本接回,见implement顶部;本轮无新的远端/提交发布,原验收未补勾。\n2026-09-07 §10已实施并完成一次环境有效的全套诊断,增量待用户独立复核。原21文件加6个QA环境/测试文件,共27文件;本轮产品源码和业务spec零改动,51项针对性验证通过。前三次启动在环境前置拒绝,资源已核对清理;修复跨user namespace读取父namespace的EACCES,改由父runner经本轮Unix IPC即时实测,不降级。第四次环境PASS、源码前后不变:65文件275项,199 passed/54 failed/4 timedOut/18 skipped,58条观测涉及36文件,不是独立产品缺陷计数;诊断仍PARTIAL/2。服务端193文件1692项通过,workflow场景8两个来源仍失败。R102 6/6通过;合同仍卡旧更多,续约/归档未执行;PWA在serviceWorker.ready超时且该失败无trace附件。诊断67分03秒不是普通全绿成本。旧207撤销决策资格但保留原件;阶段1、阶段B、场景8、经理缺口和R1均未扩大执行。无提交/push/归档/部署/生产操作,整体任务未完成。",
"meta": {
"archiveOutputProtection": "implemented_pending_independent_review",
"registeredLocalEvidenceVerified": 14,
@@ -150,7 +150,7 @@
"remediationStatus": "LOCAL_VERIFIED_INDEPENDENT_REVIEWED"
},
"releasePreparation": {
"status": "LOCAL_COMMIT_AUTHORIZED_RELEASE_PENDING",
"status": "LOCAL_COMMITS_COMPLETE_R1_PLAN_READY",
"executor": "main Codex session",
"request": "artifacts/qa-governance/release-preparation-claude-request-20260919.md",
"scope": "read-only candidate inventory and actionable release preparation",
@@ -170,6 +170,50 @@
"commitEvidence": "artifacts/qa-governance/local-commits-20260919",
"firstLocalCommit": "729d90576f25ba658656508af1af32a179aa7057",
"commitResultReceipt": "artifacts/qa-governance/local-commits-20260919/result.json"
},
"r1IntegrationPlan": {
"status": "LOCAL_REVIEWED_FINAL_FULL_GATE_PENDING",
"planningApproved": true,
"implementationApproved": true,
"enforcementEnabled": true,
"baselineCommit": "1f8d5b658a9fcae15be192af8c82ab557f88a046",
"designSection": "R1 接入方案(2026-09-19,已批准本地实施)",
"executionFiles": [
"scripts/qa/release-gate-result.mjs",
"scripts/qa/run-release-gate.sh",
"deploy/trialrun/production-identity-bootstrap.mjs",
"deploy/trialrun/deploy-existing.sh",
"deploy/trialrun/deploy.sh"
],
"testExecutor": "Claude independent session",
"testsRun": true,
"pushAuthorized": false,
"deploymentAuthorized": false,
"approval": "用户确认本地R1接入与先实施再最终一次完整门禁的顺序;不含推送或部署",
"enforcementScope": "local uncommitted code only; no real deployment executed",
"localEnforcementImplemented": true,
"testStatus": "SCOPED_PASS_WITH_RECORDED_PRIOR_TIMEOUT",
"independentReview": "PASS_6_POINTS_CAUSE_CORRECTION_RECEIVED",
"deploymentEligible": false,
"evidenceDirectory": "artifacts/qa-governance/r1-enforcement-20260919",
"handoff": "artifacts/qa-governance/r1-enforcement-20260919/claude-request.md",
"reviewReception": "artifacts/qa-governance/r1-enforcement-20260919/review-reception.json",
"nextHandoff": "artifacts/qa-governance/r1-enforcement-20260919/final-gate-claude-request.md",
"f1": {
"discoveryStatus": "THREE_TIMEOUTS_CONFIRMED",
"attribution": "R1_DIRECT_CAUSE_DISPROVED",
"remediationStatus": "NO_CODE_FIX_INDICATED_SCOPED_REVALIDATION_PASS",
"historicalRootCause": "UNDETERMINED",
"supplementalRun": {
"tests": 52,
"passed": 52,
"exitCode": 0,
"budgetMs": 5000
},
"productionChecksPreserved": true
},
"diagnosticReception": "artifacts/qa-governance/r1-enforcement-20260919/f1-triage-20260919/diagnostic-reception.json",
"localCommitEvidence": "artifacts/qa-governance/r1-enforcement-20260919/local-commit/result.json"
}
}
}
+16 -1
查看文件
@@ -29,6 +29,19 @@ if [[ "${DEPLOY_ALLOW_UNPUBLISHED_COMMIT:-0}" == "1" || -n "${DEPLOY_UNPUBLISHED
fail "生产部署验证不接受未进入上游的提交;灾备 override 必须走独立获批流程"
fi
case "${PRODUCTION_IDENTITY_ONLY-0}" in
0) ;;
*) fail "identity-only 必须使用生产身份引导器,不能进入发布包装器" ;;
esac
case "${PRODUCTION_READ_ONLY_PREFLIGHT-0}" in
0)
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
;;
1) ;;
*) fail "PRODUCTION_READ_ONLY_PREFLIGHT 只能为 0 或 1" ;;
esac
declare -A contract=()
echo "[deploy-existing] 开始只读聚合体检"
DEPLOY_SERVER="$DEPLOY_SERVER" \
@@ -60,7 +73,7 @@ fi
if [[ -z "${EXPECTED_COMPOSE_PROJECT_NAME:-}" ]]; then
fail "生产部署必须由身份引导器提供 EXPECTED_COMPOSE_PROJECT_NAME"
fi
case "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" in
case "${PRODUCTION_READ_ONLY_PREFLIGHT-0}" in
0|1) ;;
*) fail "PRODUCTION_READ_ONLY_PREFLIGHT 只能为 0 或 1" ;;
esac
@@ -99,6 +112,8 @@ if [[ "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" == "1" ]]; then
exit 0
fi
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$source_commit" >/dev/null
DEPLOY_SERVER="$DEPLOY_SERVER" \
REMOTE_DIR="${contract[REMOTE_DIR]}" \
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
+6
查看文件
@@ -125,6 +125,9 @@ if [[ ! -f "${ROOT_DIR}/${PROVENANCE_SCRIPT}" ]]; then
exit 1
fi
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
echo ">>> 0. 校验 Git 来源与发布提交"
PROVENANCE_JSON="$(node "${ROOT_DIR}/${PROVENANCE_SCRIPT}")"
SOURCE_BUILD_ENV_LINES="$(node -e '
@@ -155,6 +158,7 @@ SOURCE_PUBLISHED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout
SOURCE_REMOTE_VERIFIED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.remoteVerified))' "$PROVENANCE_JSON")"
SOURCE_OVERRIDE_USED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.overrideUsed))' "$PROVENANCE_JSON")"
SOURCE_OVERRIDE_REASON="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.overrideReason || ""))' "$PROVENANCE_JSON")"
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$SOURCE_COMMIT" >/dev/null
echo "[deploy-source] commit=${SOURCE_COMMIT} branch=${SOURCE_BRANCH} published=${SOURCE_PUBLISHED} remoteVerified=${SOURCE_REMOTE_VERIFIED}"
if [[ "$SOURCE_OVERRIDE_USED" == "true" || "$SOURCE_REMOTE_VERIFIED" != "true" ]]; then
echo "[deploy-source][UNPUBLISHED-OVERRIDE] reason=${SOURCE_OVERRIDE_REASON} remoteVerified=${SOURCE_REMOTE_VERIFIED}" >&2
@@ -202,6 +206,8 @@ ssh "${DEPLOY_SERVER}" \
"REMOTE_DIR='${REMOTE_DIR}' REMOTE_ENV_FILE='${REMOTE_ENV_FILE}' COMPOSE_FILE='${COMPOSE_FILE}' POSTGRES_DATA_DIR='${POSTGRES_DATA_DIR}' STORAGE_HOST_DIR='${STORAGE_HOST_DIR}' DEPLOY_DATA_MODE='${DEPLOY_DATA_MODE}' SYNC_ENV='${SYNC_ENV}' bash -s" \
< "${ROOT_DIR}/${REMOTE_PREFLIGHT_SCRIPT}"
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$SOURCE_COMMIT" >/dev/null
echo ">>> 3.1 创建远端目录"
ssh "${DEPLOY_SERVER}" "mkdir -p '${REMOTE_DIR}'"
@@ -7,6 +7,7 @@ import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { deploymentReadOnlyMode, requireReleaseReceipt, ReleaseGateError } from "../../scripts/qa/release-gate-result.mjs";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(SCRIPT_DIR, "../..");
@@ -198,6 +199,9 @@ function main() {
try {
const identityOnly = process.env.PRODUCTION_IDENTITY_ONLY ?? "0";
if (!["0", "1"].includes(identityOnly)) fail("PRODUCTION_IDENTITY_ONLY_INVALID");
const { preflightOnly } = deploymentReadOnlyMode(process.env, true);
// Reject before SSH configuration discovery, target probes or database reads.
const release = identityOnly === "1" || preflightOnly ? null : requireReleaseReceipt();
const historicalTargetScript = requiredCommandOutput(
"git",
["show", `${HISTORICAL_TARGET_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`],
@@ -228,11 +232,13 @@ function main() {
const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin);
console.log("[production-identity-bootstrap][PASS] exact_identity_group");
if (release) requireReleaseReceipt(release.directory, release.commit);
const result = spawnSync("bash", [DEPLOY_EXISTING_SCRIPT], {
cwd: ROOT_DIR,
stdio: "inherit",
env: {
...process.env,
...(release ? { RELEASE_GATE_RECEIPT_DIR: release.directory, RELEASE_GATE_EXPECTED_COMMIT: release.commit } : {}),
DEPLOY_SERVER: identity.deployServer,
DEPLOY_REFRESH_SERVER: refreshTarget,
EXPECTED_DEPLOY_SERVER: identity.deployServer,
@@ -246,7 +252,7 @@ function main() {
});
if (result.status !== 0) fail("DEPLOY_EXISTING_FAILED");
} catch (error) {
const code = error instanceof ProductionIdentityBootstrapError
const code = error instanceof ProductionIdentityBootstrapError || error instanceof ReleaseGateError
? error.code
: "PRODUCTION_IDENTITY_BOOTSTRAP_UNEXPECTED_ERROR";
console.error(`[production-identity-bootstrap][PAUSED] code=${code}`);
+51 -4
查看文件
@@ -11,6 +11,7 @@ import { verifyLocalCandidateResults } from './verify-local-candidate-results.mj
import { FULL_LOCAL_STAGES, newStageRecord, updateStageRecord, validateStages, validTime } from './qa-stage-record.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const DEPLOYMENT_ENTRIES = ['deploy/trialrun/production-identity-bootstrap.mjs','deploy/trialrun/deploy-existing.sh','deploy/trialrun/deploy.sh'];
const hash = value => createHash('sha256').update(value).digest('hex');
const read = file => JSON.parse(readFileSync(file,'utf8'));
const PHASES = ['static','full-local'];
@@ -37,7 +38,7 @@ export function captureReleaseSource(root=ROOT) {
return {...source,untrackedDigest:hash(JSON.stringify(files))};
}
export function currentPolicy(root=ROOT) {
return {digest:hash(JSON.stringify(POLICY_FILES.map(file=>[file,hash(readFileSync(path.join(root,file)))]))), environmentDigest:environmentPolicyDigest(), runnerDigest:hash(readFileSync(path.join(root,'scripts/qa/full-local-environment.mjs'))), probeDigest:hash(readFileSync(path.join(root,'scripts/qa/browser-environment-probe.mjs'))), toolchain:{node:process.version,playwright:read(path.join(root,'client/node_modules/@playwright/test/package.json')).version,browserRegistry:hash(readFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'))),browserVersions:Object.fromEntries(read(path.join(root,'client/node_modules/playwright-core/browsers.json')).browsers.filter(browser=>['chromium','webkit'].includes(browser.name)).map(browser=>[browser.name,browser.browserVersion])),platform:process.platform,arch:process.arch,ci:Boolean(process.env.CI),workers:process.env.CI?2:4}};
return {digest:hash(JSON.stringify([...POLICY_FILES,...DEPLOYMENT_ENTRIES].map(file=>[file,hash(readFileSync(path.join(root,file)))]))), environmentDigest:environmentPolicyDigest(), runnerDigest:hash(readFileSync(path.join(root,'scripts/qa/full-local-environment.mjs'))), probeDigest:hash(readFileSync(path.join(root,'scripts/qa/browser-environment-probe.mjs'))), toolchain:{node:process.version,playwright:read(path.join(root,'client/node_modules/@playwright/test/package.json')).version,browserRegistry:hash(readFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'))),browserVersions:Object.fromEntries(read(path.join(root,'client/node_modules/playwright-core/browsers.json')).browsers.filter(browser=>['chromium','webkit'].includes(browser.name)).map(browser=>[browser.name,browser.browserVersion])),platform:process.platform,arch:process.arch,ci:Boolean(process.env.CI),workers:process.env.CI?2:4}};
}
function saneSource(source) {
return /^[a-f0-9]{40}$/.test(source?.commit ?? '') && /^[a-f0-9]{40}$/.test(source?.tree ?? '') && /^[a-f0-9]{64}$/.test(source?.digest ?? '') && source?.repository?.trackedCoverageComplete === true && typeof source.repository.porcelainV1Z === 'string' && source.dirty === Boolean(source.repository.porcelainV1Z.length);
@@ -122,7 +123,7 @@ function readBundle(record,seal=false) {
}
function facts() {return {root:ROOT,source:captureReleaseSource(),policy:currentPolicy(),contract:read(path.join(ROOT,'scripts/qa/full-local-coverage-contract.json')),manifest:read(path.join(ROOT,'scripts/qa/route-manifest.json'))};}
function ownedDirectory(input,create=false) {
const relative=path.relative(ROOT,path.resolve(input)).split(path.sep).join('/');
const relative=path.relative(ROOT,path.resolve(ROOT,input)).split(path.sep).join('/');
if (!relative.startsWith('artifacts/release-gate/')) throw Error('release_report_outside_artifacts');
const dir=confinedPath(ROOT,relative);
if (create && existsSync(dir) && readdirSync(dir).length) throw Error('release_report_not_empty');
@@ -130,10 +131,52 @@ function ownedDirectory(input,create=false) {
if (realpathSync(dir)!==dir) throw Error('release_report_symlink');
return dir;
}
export class ReleaseGateError extends Error {
constructor(code) { super(code); this.code=code; }
}
export function deploymentReadOnlyMode(env=process.env, allowReadOnly=false) {
const flags=['PRODUCTION_IDENTITY_ONLY','PRODUCTION_READ_ONLY_PREFLIGHT'].map(key=>env[key]??'0');
if (flags.some(value=>!['0','1'].includes(value))) throw new ReleaseGateError('read_only_flag_invalid');
if (!allowReadOnly && flags.includes('1')) throw new ReleaseGateError('read_only_cannot_deploy');
return {identityOnly:flags[0]==='1',preflightOnly:flags[1]==='1'};
}
// Read-only assertion: never generates evidence or invokes a test/deploy runner.
// A returned commit is pinned by each caller across its subsequent preflights.
export function requireReleaseReceipt(input=process.env.RELEASE_GATE_RECEIPT_DIR, expectedCommit) {
try {
deploymentReadOnlyMode();
validateSkipRequest('deploy',process.env);
if (typeof input!=='string' || !input.trim()) throw new ReleaseGateError('receipt_required');
const dir=ownedDirectory(input);
const file=confinedPath(dir,'release-result.json');
if (!lstatSync(file).isFile()) throw new ReleaseGateError('receipt_invalid');
const record=read(file), current=facts();
for (const expected of [expectedCommit,process.env.RELEASE_GATE_EXPECTED_COMMIT]) {
if (expected!==undefined && (!/^[a-f0-9]{40}$/.test(expected) || expected!==current.source.commit)) {
throw new ReleaseGateError('candidate_commit_mismatch');
}
}
const result=verifyReleaseEvidence(record,readBundle(record),current);
if (!result.deploymentEligible) throw new ReleaseGateError('receipt_not_eligible');
return {commit:current.source.commit,directory:dir};
} catch (error) {
if (error instanceof ReleaseGateError) throw error;
throw new ReleaseGateError('receipt_invalid');
}
}
if (process.argv[1] && import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href) {
try {
const [action,input,arg,rawExit]=process.argv.slice(2);
if (action==='skip') {
if (action==='require') {
console.log(requireReleaseReceipt(input,arg).commit);
} else if (action==='write-mode') {
deploymentReadOnlyMode();
validateSkipRequest('deploy',process.env);
} else if (action==='skip') {
const result=validateSkipRequest(arg,process.env);
if (result) { const dir=ownedDirectory(input,true);atomic(confinedPath(dir,'skip-receipt.json'),result);console.log('[release-gate] approved diagnostic skips: PARTIAL'); }
} else {
@@ -162,5 +205,9 @@ if (process.argv[1] && import.meta.url===pathToFileURL(path.resolve(process.argv
} else throw Error('action_invalid');
}
}
} catch {console.error('[release-receipt] invalid_or_incomplete');process.exitCode=1;}
} catch (error) {
const code=error instanceof ReleaseGateError?error.code:'invalid_or_incomplete';
console.error(`[release-receipt] ${code}; run verify-candidate on the final clean candidate before deployment`);
process.exitCode=1;
}
}
+35 -10
查看文件
@@ -2,6 +2,7 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
ARTIFACT_ROOT="${RELEASE_GATE_ARTIFACT_DIR:-${ROOT_DIR}/artifacts/release-gate/$(date +%Y%m%d-%H%M%S)}"
MODE="${1:-all}"
CURRENT_STAGE=""
@@ -16,13 +17,14 @@ Usage: bash scripts/qa/run-release-gate.sh <verify-candidate|all|static|smoke-lo
smoke-local 运行本地网页 smoke 门禁
full-local 运行本地全量门禁(server regression + client full E2E)
project-wide 项目级门禁:static + 覆盖扫描 + UI/移动契约 + 迁移漂移诊断 + 业务流 + 全页面视觉 + live readiness
deploy 调用 deploy/trialrun/deploy.sh,内部会串联 /ready、external deps、runtime smoke 与 live smoke
all 依次执行 static -> smoke-local -> full-local -> deploy
ci-release 依次执行 static -> full-local -> deploy
deploy 只消费显式回执,经生产身份引导器发布;不隐式运行测试
all smoke-local -> 单次 verify-candidate(或重验显式回执)-> 生产发布链
ci-release 单次 verify-candidate(或重验显式回执)-> 生产发布链
r66-trialrun R66 小团队试点门禁:static -> server regression-all -> smoke-local
可选环境变量:
RELEASE_GATE_ARTIFACT_DIR 失败产物输出目录
RELEASE_GATE_RECEIPT_DIR 显式消费的完整候选回执目录;不自动选择历史回执
QA_* 透传给 scripts/qa/run-web-gates.sh
LIVE_SMOKE_* 透传给 deploy.sh 与 live smoke
PROJECT_WIDE_SKIP_BUSINESS_FLOW=1 跳过业务流 E2E
@@ -45,7 +47,11 @@ collect_failure_artifacts() {
handle_error() {
local exit_code="$?"
echo "[release-gate][FAIL] 阶段 ${CURRENT_STAGE:-unknown} 失败(exit=${exit_code})" >&2
collect_failure_artifacts
# Rejected publication must not trigger diagnostic reads of real configuration.
case "$MODE" in
ci-release|all|deploy) ;;
*) collect_failure_artifacts ;;
esac
exit "$exit_code"
}
trap handle_error ERR
@@ -129,10 +135,21 @@ stage_project_wide_live_readiness() {
}
stage_deploy() {
bash "$ROOT_DIR/deploy/trialrun/deploy.sh"
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
node "$ROOT_DIR/deploy/trialrun/production-identity-bootstrap.mjs"
}
# This entry does not install enforcement in any existing deployment path.
prepare_release_receipt() {
if [[ -z "${RELEASE_GATE_RECEIPT_DIR+x}" ]]; then
# This verification-only child cannot call back into the deployment chain.
RELEASE_GATE_ARTIFACT_DIR="$ARTIFACT_ROOT" bash "$ROOT_DIR/scripts/qa/run-release-gate.sh" verify-candidate
RELEASE_GATE_RECEIPT_DIR="$ARTIFACT_ROOT"
export RELEASE_GATE_RECEIPT_DIR
fi
}
# The verification-only mode records its own original exit and never deploys.
finish_candidate() {
local original_status=$? receipt_status=0
trap - EXIT ERR INT TERM
@@ -145,6 +162,16 @@ if [[ "$MODE" != "help" && "$MODE" != "-h" && "$MODE" != "--help" ]]; then
# Validate before static, remote readiness or deployment side effects.
trap - ERR
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" skip "$ARTIFACT_ROOT" "$MODE"
case "$MODE" in
ci-release|all|deploy)
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" write-mode
# Invalid explicit receipts fail before smoke or other side effects.
if [[ -n "${RELEASE_GATE_RECEIPT_DIR+x}" || "$MODE" == "deploy" ]]; then
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
fi
;;
esac
if [[ "$MODE" != "verify-candidate" ]]; then trap handle_error ERR; fi
fi
@@ -186,8 +213,7 @@ case "$MODE" in
run_stage deploy stage_deploy
;;
ci-release)
run_stage static stage_static
run_stage full-local stage_full_local
prepare_release_receipt
run_stage deploy stage_deploy
;;
r66-trialrun)
@@ -196,9 +222,8 @@ case "$MODE" in
run_stage smoke-local stage_smoke_local
;;
all)
run_stage static stage_static
run_stage smoke-local stage_smoke_local
run_stage full-local stage_full_local
prepare_release_receipt
run_stage deploy stage_deploy
;;
*)
+89
查看文件
@@ -0,0 +1,89 @@
// Private subprocess fixture. There is deliberately no real-network fallback.
const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const root = process.env.HYB_RELEASE_FIXTURE_ROOT;
if (!root || !path.basename(root).startsWith('hyb-release-shell-')) process.exit(97);
const args = process.argv.slice(2), name = path.basename(process.argv[1]);
const trace = path.join(root, 'artifacts/commands.jsonl');
fs.mkdirSync(path.dirname(trace), { recursive: true });
fs.appendFileSync(trace, JSON.stringify({ name, args }) + '\n');
function run(command, values) {
const result = spawnSync(command, values, { cwd: root, env: process.env, stdio: 'inherit' });
process.exit(result.status ?? 97);
}
function output(value) { process.stdout.write(value + '\n'); process.exit(0); }
function git(values) {
const result = spawnSync('/usr/bin/git', values, { cwd: root, env: process.env, encoding: 'utf8' });
if (result.status !== 0) process.exit(97);
return result.stdout.trim();
}
function mutate(point) {
if (process.env.HYB_MUTATE_AT !== point || fs.existsSync(path.join(root, 'artifacts/mutated'))) return;
fs.writeFileSync(path.join(root, 'artifacts/mutated'), point);
const kind = process.env.HYB_MUTATE_KIND || 'tracked';
if (kind === 'receipt') {
const file = path.join(process.env.RELEASE_GATE_RECEIPT_DIR, 'release-result.json');
const record = JSON.parse(fs.readFileSync(file)); record.executionExit = 23;
fs.writeFileSync(file, JSON.stringify(record));
} else if (kind === 'head') {
git(['-c', 'user.name=Fixture', '-c', '[email protected]', 'commit', '--allow-empty', '-qm', 'changed']);
} else {
fs.appendFileSync(path.join(root, 'client/package.json'), ' ');
if (kind === 'index') git(['add', 'client/package.json']);
}
}
const fullCommit = () => git(['rev-parse', 'HEAD']);
const contract = 'REMOTE_DIR=/fixture/app REMOTE_ENV_FILE=/fixture/app/deploy/trialrun/.env COMPOSE_FILE=deploy/trialrun/docker-compose.yml COMPOSE_PROJECT_NAME=fixture-project POSTGRES_DATA_DIR=/fixture/db STORAGE_HOST_DIR=/fixture/storage';
if (name === 'git') {
if (args[0] === 'show' && args[1] === '34181e3:deploy/trialrun/deploy.sh') output('DEPLOY_SERVER="${DEPLOY_SERVER:-fixture-target}"');
if (args[0] === 'show' && args[1] === '5e2d038938265c926e06d3cc6d0841f6dcc06148:deploy/trialrun/deploy.sh') output('COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-fixture-project}"');
const values = args[0] === '-C' && args[1] === root ? args.slice(2) : args;
if (['ls-files', 'status', 'rev-parse', 'archive'].includes(values[0])) run('/usr/bin/git', values);
process.exit(97);
}
if (name === 'node') {
if (args[0] === '-e') run(process.execPath, args);
const file = path.basename(args[0] || '');
if (file === 'release-gate-result.mjs' || file === 'production-identity-bootstrap.mjs') {
run(process.execPath, ['--require', path.join(root, 'bin/fs-guard.cjs'), ...args]);
}
if (file === 'check-source-provenance.mjs') {
mutate('provenance');
const commit = process.env.HYB_WRONG_SOURCE ? 'f'.repeat(40) : fullCommit();
output(JSON.stringify({ commit, branch: 'fixture', published: true, remoteVerified: true, overrideUsed: false, buildEnv: { BUILD_SHA: commit, VITE_BUILD_SHA: commit.slice(0, 12) } }));
}
if (file === 'production-preflight.mjs') { mutate('preflight'); process.exit(0); }
if (file === 'validate-env-contract.js' && process.env.FAIL_STATIC) process.exit(23);
if (['validate-env-contract.js', 'check-public-bundle-copy.mjs', 'check-ui-design-rules.mjs', 'check-error-reason-contract.mjs', 'check-mobile-route-contract.mjs', 'check-flow-coverage.mjs', 'check-live-smoke-env.mjs'].includes(file)) process.exit(0);
process.exit(97);
}
if (name === 'bash') {
const file = path.basename(args[0] || '');
if (['run-release-gate.sh', 'deploy-existing.sh', 'deploy.sh'].includes(file)) run('/bin/bash', args);
if (file === 'run-web-gates.sh' && args[1] === 'full-local') {
if (process.env.OMIT_EVIDENCE) process.exit(0);
run(process.execPath, [path.join(root, 'bin/evidence.mjs')]);
}
if (['run-web-gates.sh', 'run-business-flow-gates.sh', 'run-live-smoke-readiness.sh', 'check-local-migration-drift.sh', 'collect-failure-artifacts.sh', 'check-existing-deployment.sh'].includes(file)) process.exit(0);
process.exit(97);
}
if (name === 'ssh') {
const last = args.at(-1) || '';
if (last === 'probe') output('[deployment-contract-refresh][PASS] probe');
if (last === 'refresh') output('[deployment-contract-refresh][PASS] refreshed');
if (last.startsWith('systemctl show ')) output(contract);
if (last.includes('bash -s')) {
fs.readFileSync(0); // Drain only the synthetic script; never execute it.
if (last.includes("IDENTITY_BOOTSTRAP_MODE='origin'")) { mutate('origin'); output('[production-identity-bootstrap][PASS] origin_identity'); }
if (last.includes("IDENTITY_BOOTSTRAP_MODE='database'")) output('[production-identity-bootstrap][DB_SHA256] ' + 'a'.repeat(64));
if (!last.includes('BUILD_SHA=')) mutate('remote-preflight');
process.exit(0);
}
if (last === "mkdir -p '/fixture/app'") process.exit(0);
process.exit(97);
}
if (name === 'rsync') process.exit(0);
if (name === 'curl') output(JSON.stringify({ data: { buildSha: fullCommit().slice(0, 12) } }));
if (name === 'npm' || name === 'python3') process.exit(0);
process.exit(97);
+15
查看文件
@@ -0,0 +1,15 @@
// Loaded only in private fixture children; audit config access without real HOME.
const fs = require('node:fs');
const path = require('node:path');
const { syncBuiltinESMExports } = require('node:module');
const root = process.env.HYB_RELEASE_FIXTURE_ROOT;
const original = fs.readFileSync;
fs.readFileSync = function(file, ...args) {
const text = String(file);
if (text.endsWith('/.ssh/config') || /\/\.env(?:$|\.)/.test(text)) {
if (text !== path.join(root, 'bin/home/.ssh/config')) throw Error('fixture_config_read_forbidden');
fs.appendFileSync(path.join(root, 'artifacts/commands.jsonl'), JSON.stringify({ name: 'config-read', args: ['synthetic-ssh'] }) + '\n');
}
return original.call(this, file, ...args);
};
syncBuiltinESMExports();
+85
查看文件
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
const projectRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../../..');
const receiptFiles={environment:'environment/result.json',phases:'results/full-local-stages.json',discovery:'results/discovery.json',plan:'results/coverage-plan.json',execution:'results/playwright-results.json',selection:'results/execution-selection.json'};
export function fixture(context,overrides={}) {
const root=mkdtempSync(path.join(os.tmpdir(),'hyb-release-shell-'));
context.after(()=>rmSync(root,{recursive:true,force:true}));
for(const dir of ['scripts/qa','deploy/trialrun','client/tests/e2e','server','bin','client/node_modules/@playwright/test','client/node_modules/playwright-core'])mkdirSync(path.join(root,dir),{recursive:true});
for(const file of readdirSync(path.join(projectRoot,'scripts/qa')).filter(file=>file.endsWith('.mjs')||file.endsWith('.sh')||file.endsWith('.py')))copyFileSync(path.join(projectRoot,'scripts/qa',file),path.join(root,'scripts/qa',file));
for(const file of readdirSync(path.join(projectRoot,'deploy/trialrun')).filter(file=>/\.(mjs|sh|yml)$/.test(file)))copyFileSync(path.join(projectRoot,'deploy/trialrun',file),path.join(root,'deploy/trialrun',file));
for(const dir of ['docs/trialrun','server/src/scripts','bin/home/.ssh'])mkdirSync(path.join(root,dir),{recursive:true});
writeFileSync(path.join(root,'bin/home/.ssh/config'),'Host fixture-refresh\n');
writeFileSync(path.join(root,'docs/trialrun/OPS7运维交接Runbook.md'),'curl -fsS https://fixture.invalid/ready\n');
for(const file of ['smoke-trialrun-runtime.js','smoke-external-deps.js'])writeFileSync(path.join(root,'server/src/scripts',file),'// synthetic');
const rows=Array.from({length:10},(_,i)=>({file:`client/tests/e2e/flow-${i}.spec.ts`,id:`flow-${i}`,title:`flow-${i} @local-candidate:flow-${i}`,line:1}));
rows.push({file:'client/tests/e2e/ordinary.spec.ts',id:'ordinary',title:'ordinary',line:1});
for(const row of rows)writeFileSync(path.join(root,row.file),'synthetic');
const manifest={localCandidateFlows:rows.slice(0,10).map((row,i)=>({bundle:`flow-${i}`,witnessTag:`@local-candidate:flow-${i}`,witnessSpec:row.file})),acceptanceSpecs:rows.map(row=>row.file)};
const contract={schemaVersion:1,approvalReference:'synthetic',exceptions:[]};
writeFileSync(path.join(root,'scripts/qa/route-manifest.json'),JSON.stringify(manifest));
writeFileSync(path.join(root,'scripts/qa/full-local-coverage-contract.json'),JSON.stringify(contract));
for(const file of ['client/package.json','client/package-lock.json','server/package.json','server/package-lock.json','client/node_modules/playwright-core/browsers.json'])writeFileSync(path.join(root,file),'{}');
writeFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'),JSON.stringify({browsers:['chromium','webkit'].map(name=>({name,browserVersion:'synthetic'}))}));
writeFileSync(path.join(root,'client/node_modules/@playwright/test/package.json'),JSON.stringify({version:'1.58.2'}));
writeFileSync(path.join(root,'client/playwright.config.ts'),'// synthetic policy');
writeFileSync(path.join(root,'deploy/trialrun/validate-env-contract.js'),'// fake tooling owns this command');
writeFileSync(path.join(root,'.gitignore'),'artifacts/\nbin/\nnode_modules/\n');
// Private child environment: no real HOME/config, credentials or fallback PATH.
const env={PATH:path.join(root,'bin'),HOME:path.join(root,'bin/home'),GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_CONFIG_SYSTEM:'/dev/null',LANG:'C.UTF-8',HYB_RELEASE_FIXTURE_ROOT:root,RELEASE_GATE_ARTIFACT_DIR:path.join(root,'artifacts/release-gate/run'),...overrides};
const fake=`#!${process.execPath}\nrequire('./deploy-fake.cjs');\n`;
writeFileSync(path.join(root,'bin/fake'),fake,{mode:0o755});
for(const name of ['node','npm','bash','python3','git','ssh','rsync','curl','docker'])symlinkSync('fake',path.join(root,'bin',name));
for(const name of ['dirname','date','mkdir','mktemp','tar','rm'])symlinkSync('/usr/bin/'+name,path.join(root,'bin',name));
copyFileSync(path.join(projectRoot,'scripts/qa/tests/fixtures/release-deploy-fake.cjs'),path.join(root,'bin/deploy-fake.cjs'));
copyFileSync(path.join(projectRoot,'scripts/qa/tests/fixtures/release-fs-guard.cjs'),path.join(root,'bin/fs-guard.cjs'));
writeFileSync(path.join(root,'bin/evidence.mjs'),`
import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';
import path from 'node:path';
import {captureReleaseSource,currentPolicy} from '../scripts/qa/release-gate-result.mjs';
import {FULL_LOCAL_STAGES,newStageRecord,updateStageRecord} from '../scripts/qa/qa-stage-record.mjs';
import {coveragePlan,localSpecArgs} from '../scripts/qa/full-local-coverage.mjs';
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,base=process.env.QA_FULL_REPORT_DIR,now=()=>new Date().toISOString();
const source=captureReleaseSource(root),policy=currentPolicy(root),startedAt=now();
const phases=newStageRecord(FULL_LOCAL_STAGES,'full-local-stages');
const rows=${JSON.stringify(rows)},contract=${JSON.stringify(contract)};
const report=executed=>({errors:[],suites:rows.map(row=>({file:row.file,specs:[{...row,tests:[{projectName:'',repeatEachIndex:0,...(executed?{expectedStatus:'passed',status:'expected',results:[{status:'passed',retry:0}]}:{})}]}]}))});
const discovery=report(false),execution=report(true);
execution.config={forbidOnly:true,projects:[{name:'',retries:0,repeatEach:1,timeout:180000,testIgnore:['**/manual/**']}],shard:null,maxFailures:0,workers:policy.toolchain.workers,version:policy.toolchain.playwright};
for(const stage of FULL_LOCAL_STAGES){updateStageRecord(phases,'start',stage);if(stage==='browser')execution.stats={startTime:now(),duration:0,expected:rows.length,unexpected:0,flaky:0,skipped:0};updateStageRecord(phases,'end',stage,0);}updateStageRecord(phases,'finish',null,0);
const environment={schemaVersion:1,profile:'isolated-online-v1',mode:'full-local',policyDigest:policy.environmentDigest,platform:process.platform,status:'PASS',exitCode:0,childExit:0,childSignal:null,interrupted:null,startedAt,endedAt:now(),sourceBefore:source,sourceAfter:source,sourceUnchanged:true,repositoryUnchanged:true,candidateIntegrityStatus:'UNCHANGED',cleanup:{containerOwnerVerified:true,containerRemoved:true,scratchRemoved:true},environment:{schemaVersion:1,profile:'isolated-online-v1',policyDigest:policy.environmentDigest,platform:process.platform,runnerDigest:policy.runnerDigest,probeDigest:policy.probeDigest,supervisorObservation:{source:'live-supervisor-unix-ipc',sampledAt:now(),namespace:'parent'},namespace:{inheritedRunner:'parent',ownedChild:'child',runnerCreated:true,currentMatchesOwnedChild:true,topologyMatched:true},nativeOnlineProbe:{valid:true,online:true,browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,engines:['chromium','webkit'].map(name=>({name,version:'synthetic',online:true,valid:true,noNavigatorOverride:true,browserNamespace:'child',browserMatchesOwnedChild:true}))},bootstrapObservation:{valid:true,observationPoint:'guest-entry-black-box',freshContext:true,initialSessionHint:false,listenerBeforeNavigation:true,noRouteMocks:true,loginFormVisible:true,refreshRequestCount:0,loginRequestCount:0,nativeOnlineSamples:{beforeNavigation:true,documentStart:true,afterForm:true},browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,documentStatus:200,stayedOnLogin:true}}};
const bundle={environment,phases,discovery,execution,plan:coveragePlan(root,discovery,contract,source)};
bundle.selection={schemaVersion:1,profile:'full-local-selection',specArgs:localSpecArgs(bundle.plan),additionalArgs:[],retries:0,forbidOnly:true,workers:policy.toolchain.workers,mode:'local'};
if(process.env.BAD_PROFILE)environment.mode='retained-local';
if(process.env.BAD_SOURCE)environment.sourceBefore={...source,digest:'0'.repeat(64)};
if(process.env.BAD_COVERAGE)execution.suites.pop();
if(process.env.BAD_CLEANUP)environment.cleanup.containerRemoved=false;
if(process.env.BAD_STAGE)phases.stages.splice(2,1);
for(const [key,relative] of Object.entries(${JSON.stringify(receiptFiles)})){const file=path.join(base,relative);mkdirSync(path.dirname(file),{recursive:true});writeFileSync(file,JSON.stringify(bundle[key]));}
if(process.env.FAIL_FULL)process.exit(17);
`);
for(const args of [['init','-q'],['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic fixture']])assert.equal(spawnSync('/usr/bin/git',args,{cwd:root,env}).status,0);
const commands=()=>existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').filter(Boolean).map(JSON.parse):[];
return {root,env,commands,commit() {
for(const args of [['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic mutation']])assert.equal(spawnSync('/usr/bin/git',args,{cwd:root,env}).status,0);
},invoke(entry,overrides={}) {
rmSync(path.join(root,'artifacts/commands.jsonl'),{force:true});
const script=entry==='bootstrap'?'deploy/trialrun/production-identity-bootstrap.mjs':entry==='existing'?'deploy/trialrun/deploy-existing.sh':entry==='raw'?'deploy/trialrun/deploy.sh':'scripts/qa/run-release-gate.sh';
const argv=[path.join(root,script),...(['ci-release','all','deploy'].includes(entry)?[entry]:[])];
const childEnv={...env,...overrides};
const executable=entry==='bootstrap'?process.execPath:'/bin/bash';
if(entry==='bootstrap')argv.unshift('--require',path.join(root,'bin/fs-guard.cjs'));
const result=spawnSync(executable,argv,{cwd:root,env:childEnv,encoding:'utf8',timeout:30000});
return {...result,commands:commands()};
},run(mode='verify-candidate') {
rmSync(path.join(root,'artifacts'),{recursive:true,force:true});
const result=spawnSync('/bin/bash',[path.join(root,'scripts/qa/run-release-gate.sh'),mode],{cwd:root,env,encoding:'utf8',timeout:30000});
const file=path.join(env.RELEASE_GATE_ARTIFACT_DIR,'release-result.json');
return {...result,record:existsSync(file)?JSON.parse(readFileSync(file)):null,commands:existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').map(JSON.parse):[]};
},verify() {return spawnSync(process.execPath,[path.join(root,'scripts/qa/release-gate-result.mjs'),'verify',env.RELEASE_GATE_ARTIFACT_DIR],{cwd:root,env,encoding:'utf8',timeout:30000});}};
}
@@ -0,0 +1,211 @@
import assert from 'node:assert/strict';
import { readFileSync, writeFileSync, symlinkSync, rmSync, existsSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { spawnSync } from 'node:child_process';
import { fixture } from './fixtures/release-gate-fixture.mjs';
const entries=['ci-release','all','deploy','bootstrap','existing','raw'];
const syntheticEnv={
DEPLOY_SERVER:'fixture-target',DEPLOY_REFRESH_SERVER:'fixture-refresh',
REMOTE_DIR:'/fixture/app',POSTGRES_DATA_DIR:'/fixture/db',STORAGE_HOST_DIR:'/fixture/storage',
COMPOSE_PROJECT_NAME:'fixture-project',DEPLOY_DATA_MODE:'existing',
EXPECTED_PRODUCTION_ORIGIN:'https://fixture.invalid',EXPECTED_PRODUCTION_DB_NAME_SHA256:'a'.repeat(64),
EXPECTED_PENDING_MIGRATIONS:'',EXPECTED_DEPLOY_SERVER:'fixture-target',EXPECTED_REMOTE_DIR:'/fixture/app',
EXPECTED_COMPOSE_PROJECT_NAME:'fixture-project',
};
const output=r=>r.stdout+r.stderr;
const named=(r,name,suffix)=>r.commands.filter(c=>c.name===name&&(!suffix||c.args[0]?.endsWith(suffix)));
const writes=r=>r.commands.filter(c=>c.name==='rsync'||c.name==='ssh'&&(/mkdir -p|BUILD_SHA=|^refresh$/.test(c.args.at(-1))));
function blockedBeforeAccess(r) {
assert.notEqual(r.status,0,output(r));
assert.equal(writes(r).length,0);
assert.equal(named(r,'ssh').length,0);
assert.equal(named(r,'config-read').length,0);
assert.equal(named(r,'node','check-source-provenance.mjs').length,0);
}
function complete(f) {
const r=f.run();assert.equal(r.status,0,output(r));
f.env.RELEASE_GATE_RECEIPT_DIR=f.env.RELEASE_GATE_ARTIFACT_DIR;
return r;
}
function replace(f,relative,from,to) {
const file=path.join(f.root,relative),source=readFileSync(file,'utf8');
assert.equal(source.split(from).length,2,'mutation must hit exactly once');
writeFileSync(file,source.replace(from,to));f.commit();
}
for(const entry of entries)test(`${entry}: explicit bad receipts cannot reach config, provenance or remote actions`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const file=path.join(f.env.RELEASE_GATE_RECEIPT_DIR,'release-result.json'),original=readFileSync(file,'utf8');
const mutations=[
record=>{record.profile='focused';},
record=>{record.sourceBefore.commit='f'.repeat(40);},
record=>{record.executionExit=130;record.status='PASS';record.deploymentEligible=true;},
record=>{record.phases.stages.pop();},
record=>{record.policy.toolchain.node='stale';},
];
for(const mutate of mutations) {
const record=JSON.parse(original);mutate(record);writeFileSync(file,JSON.stringify(record));
blockedBeforeAccess(f.invoke(entry));
}
writeFileSync(file,original);
blockedBeforeAccess(f.invoke(entry,{RELEASE_GATE_RECEIPT_DIR:''}));
blockedBeforeAccess(f.invoke(entry,{RELEASE_GATE_RECEIPT_DIR:path.join(f.root,'missing')}));
writeFileSync(file,'{');blockedBeforeAccess(f.invoke(entry));writeFileSync(file,original);
writeFileSync(path.join(f.root,'client/package.json'),'{"changed":true}');
blockedBeforeAccess(f.invoke(entry));writeFileSync(path.join(f.root,'client/package.json'),'{}');
for(const value of ['yes','01','']) {
blockedBeforeAccess(f.invoke(entry,{PRODUCTION_READ_ONLY_PREFLIGHT:value}));
blockedBeforeAccess(f.invoke(entry,{PRODUCTION_IDENTITY_ONLY:value,PRODUCTION_READ_ONLY_PREFLIGHT:'1'}));
}
});
for(const entry of ['deploy','bootstrap','existing','raw'])test(`${entry}: missing receipt refuses without starting tests`,context=>{
const f=fixture(context,syntheticEnv),r=f.invoke(entry);
blockedBeforeAccess(r);assert.equal(named(r,'npm').length,0);
});
for(const entry of entries)test(`${entry}: valid explicit receipt reaches exactly one synthetic deployment`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke(entry);assert.equal(r.status,0,output(r));
assert.equal(named(r,'rsync').length,1);
assert.equal(named(r,'npm').length,0);
assert.equal(named(r,'bash','run-web-gates.sh').filter(c=>c.args[1]==='full-local').length,0);
assert.equal(r.commands.filter(c=>c.name==='ssh'&&c.args.at(-1).includes('BUILD_SHA=')).length,1);
const firstWrite=r.commands.findIndex(c=>writes({commands:[c]}).length);
const checks=r.commands.slice(0,firstWrite).filter(c=>c.name==='node'&&c.args[0]?.endsWith('release-gate-result.mjs')&&c.args[1]==='require');
assert.ok(checks.length>=3,'raw deployment must recheck receipt before every write boundary');
});
for(const entry of ['ci-release','all'])test(`${entry}: no explicit receipt runs static/full-local once with no recursive deployment`,context=>{
const f=fixture(context,syntheticEnv),r=f.invoke(entry);assert.equal(r.status,0,output(r));
assert.equal(named(r,'bash','run-release-gate.sh').length,1);
assert.equal(named(r,'bash','run-web-gates.sh').filter(c=>c.args[1]==='full-local').length,1);
assert.equal(named(r,'npm').filter(c=>c.args[0]==='test').length,1);
assert.equal(named(r,'node','production-identity-bootstrap.mjs').length,1);
assert.equal(named(r,'rsync').length,1);
const web=named(r,'bash','run-web-gates.sh').map(c=>c.args[1]);
assert.deepEqual(web,entry==='all'?['smoke-local','full-local']:['full-local']);
});
for(const entry of ['ci-release','all'])test(`${entry}: failed verification never enters production chain`,context=>{
const f=fixture(context,{...syntheticEnv,FAIL_STATIC:'1'}),r=f.invoke(entry);
blockedBeforeAccess(r);assert.equal(named(r,'node','production-identity-bootstrap.mjs').length,0);
assert.equal(named(r,'bash','run-release-gate.sh').length,1);
});
for(const [entry,flag] of [['bootstrap','PRODUCTION_IDENTITY_ONLY'],['bootstrap','PRODUCTION_READ_ONLY_PREFLIGHT'],['existing','PRODUCTION_READ_ONLY_PREFLIGHT']])test(`${entry}/${flag}: synthetic read-only route terminates without receipt or writes`,context=>{
const f=fixture(context,syntheticEnv),r=f.invoke(entry,{[flag]:'1'});
assert.equal(r.status,0,output(r));assert.equal(writes(r).length,0);
assert.equal(named(r,'bash','deploy.sh').length,0);
if(flag==='PRODUCTION_IDENTITY_ONLY') {
assert.equal(named(r,'bash','deploy-existing.sh').length,0);
assert.ok(!named(r,'ssh').some(c=>c.args.at(-1).includes("IDENTITY_BOOTSTRAP_MODE='database'")));
}
});
for(const entry of ['ci-release','all','deploy','raw'])test(`${entry}: read-only flags cannot masquerade as successful publication`,context=>{
const f=fixture(context,syntheticEnv);
for(const flag of ['PRODUCTION_IDENTITY_ONLY','PRODUCTION_READ_ONLY_PREFLIGHT'])blockedBeforeAccess(f.invoke(entry,{[flag]:'1'}));
});
for(const [entry,point] of [['bootstrap','origin'],['existing','preflight'],['raw','remote-preflight']])test(`${entry}: source or receipt changed after initial check cannot write`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke(entry,{HYB_MUTATE_AT:point});
assert.notEqual(r.status,0,output(r));assert.equal(writes(r).length,0);
assert.equal(named(r,'bash','deploy.sh').length,0);
});
for(const kind of ['head','index','receipt'])test(`raw: ${kind} drift during remote preflight fails final recheck`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke('raw',{HYB_MUTATE_AT:'remote-preflight',HYB_MUTATE_KIND:kind});
assert.notEqual(r.status,0,output(r));assert.equal(writes(r).length,0);
});
test('raw: provenance commit must equal receipt/HEAD before archive or environment reads',context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke('raw',{HYB_WRONG_SOURCE:'1'});
assert.notEqual(r.status,0,output(r));assert.equal(writes(r).length,0);
assert.equal(named(r,'node','validate-env-contract.js').length,0);
});
test('caller expected commit is an additional constraint, never an override',context=>{
const f=fixture(context,syntheticEnv);complete(f);
blockedBeforeAccess(f.invoke('bootstrap',{RELEASE_GATE_EXPECTED_COMMIT:'f'.repeat(40)}));
blockedBeforeAccess(f.invoke('raw',{RELEASE_GATE_EXPECTED_COMMIT:''}));
});
test('skip/internal flags never authorize a lower-level write',context=>{
const f=fixture(context,syntheticEnv);complete(f);
for(const entry of entries)blockedBeforeAccess(f.invoke(entry,{PROJECT_WIDE_SKIP_VISUAL:'1',PROJECT_WIDE_SKIP_REASON:'synthetic',PROJECT_WIDE_SKIP_OPERATOR:'fixture',PROJECT_WIDE_SKIP_APPROVAL_REF:'fixture'}));
delete f.env.RELEASE_GATE_RECEIPT_DIR;
for(const entry of ['bootstrap','existing','raw'])blockedBeforeAccess(f.invoke(entry,{GATE_PASSED:'1',SKIP_RELEASE_GATE:'1',DEPLOY_DEFER_FINAL_SUCCESS:'1'}));
});
test('receipt directory or record symlinks and outside paths are refused',context=>{
const f=fixture(context,syntheticEnv);complete(f);
const link=path.join(f.root,'artifacts/release-gate/linked');
symlinkSync(f.env.RELEASE_GATE_RECEIPT_DIR,link);
blockedBeforeAccess(f.invoke('raw',{RELEASE_GATE_RECEIPT_DIR:link}));
blockedBeforeAccess(f.invoke('raw',{RELEASE_GATE_RECEIPT_DIR:f.root}));
const file=path.join(f.env.RELEASE_GATE_RECEIPT_DIR,'release-result.json');
rmSync(file);symlinkSync('/synthetic-missing-target',file);
blockedBeforeAccess(f.invoke('raw'));
});
test('removing bootstrap first check is caught by config-access assertion',context=>{
const f=fixture(context,syntheticEnv);blockedBeforeAccess(f.invoke('bootstrap'));
replace(f,'deploy/trialrun/production-identity-bootstrap.mjs',
'const release = identityOnly === "1" || preflightOnly ? null : requireReleaseReceipt();',
'const release = null; // isolated mutation');
assert.throws(()=>blockedBeforeAccess(f.invoke('bootstrap')),assert.AssertionError);
});
test('removing final raw check makes the no-write counterexample fail',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'deploy/trialrun/deploy.sh',
'node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$SOURCE_COMMIT" >/dev/null\n\necho ">>> 3.1',
': # isolated mutation\n\necho ">>> 3.1');
complete(f);const r=f.invoke('raw',{HYB_MUTATE_AT:'remote-preflight'});
assert.throws(()=>assert.equal(writes(r).length,0),assert.AssertionError);
});
test('removing existing-wrapper recheck is caught before lower-level invocation',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'deploy/trialrun/deploy-existing.sh',
'node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$source_commit" >/dev/null',
': # isolated mutation');
complete(f);const r=f.invoke('existing',{HYB_MUTATE_AT:'preflight'});
assert.equal(writes(r).length,0); // Raw entry still independently protects writes.
assert.throws(()=>assert.equal(named(r,'bash','deploy.sh').length,0),assert.AssertionError);
});
test('relaxing expected commit binding is caught before environment/archive',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'scripts/qa/release-gate-result.mjs',
"expected!==undefined && (!/^[a-f0-9]{40}$/.test(expected) || expected!==current.source.commit)",
'false');
complete(f);const r=f.invoke('raw',{HYB_WRONG_SOURCE:'1'});
assert.throws(()=>assert.equal(named(r,'node','validate-env-contract.js').length,0),assert.AssertionError);
});
test('removing bootstrap second check is caught before wrapper invocation',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'deploy/trialrun/production-identity-bootstrap.mjs',
'if (release) requireReleaseReceipt(release.directory, release.commit);',
'// isolated mutation');
complete(f);const r=f.invoke('bootstrap',{HYB_MUTATE_AT:'origin'});
assert.equal(writes(r).length,0);
assert.throws(()=>assert.equal(named(r,'bash','deploy-existing.sh').length,0),assert.AssertionError);
});
test('relaxing clean-candidate validation makes the dirty receipt counterexample fail',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'scripts/qa/release-gate-result.mjs','source.dirty===false','true');
writeFileSync(path.join(f.root,'client/package.json'),'{"dirty":true}');
const r=f.run();
// Deliberate verifier mutation admits a consistently dirty fixture. The
// ordinary shell suite independently requires PARTIAL/2 for this same case.
assert.throws(()=>assert.equal(r.status,2,output(r)),assert.AssertionError);
assert.equal(r.status,0,output(r));
});
test('private fixture rejects unknown tools and never falls back to real SSH/Docker',context=>{
const f=fixture(context,syntheticEnv);
assert.equal(existsSync(path.join(f.root,'bin/docker')),true);
assert.equal(f.env.PATH,path.join(f.root,'bin'));
assert.equal(f.env.HOME,path.join(f.root,'bin/home'));
for(const command of ['ssh','docker','git']) {
const r=spawnSync(path.join(f.root,'bin',command),['unexpected'],{env:f.env,cwd:f.root,encoding:'utf8'});
assert.equal(r.status,97,r.stdout+r.stderr);
}
});
+2 -70
查看文件
@@ -1,77 +1,9 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { fixture } from './fixtures/release-gate-fixture.mjs';
const projectRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../..');
const receiptFiles={environment:'environment/result.json',phases:'results/full-local-stages.json',discovery:'results/discovery.json',plan:'results/coverage-plan.json',execution:'results/playwright-results.json',selection:'results/execution-selection.json'};
function fixture(context,overrides={}) {
const root=mkdtempSync(path.join(os.tmpdir(),'hyb-release-shell-'));
context.after(()=>rmSync(root,{recursive:true,force:true}));
for(const dir of ['scripts/qa','deploy/trialrun','client/tests/e2e','server','bin','client/node_modules/@playwright/test','client/node_modules/playwright-core'])mkdirSync(path.join(root,dir),{recursive:true});
for(const file of readdirSync(path.join(projectRoot,'scripts/qa')).filter(file=>file.endsWith('.mjs')||file.endsWith('.sh')||file.endsWith('.py')))copyFileSync(path.join(projectRoot,'scripts/qa',file),path.join(root,'scripts/qa',file));
const rows=Array.from({length:10},(_,i)=>({file:`client/tests/e2e/flow-${i}.spec.ts`,id:`flow-${i}`,title:`flow-${i} @local-candidate:flow-${i}`,line:1}));
rows.push({file:'client/tests/e2e/ordinary.spec.ts',id:'ordinary',title:'ordinary',line:1});
for(const row of rows)writeFileSync(path.join(root,row.file),'synthetic');
const manifest={localCandidateFlows:rows.slice(0,10).map((row,i)=>({bundle:`flow-${i}`,witnessTag:`@local-candidate:flow-${i}`,witnessSpec:row.file})),acceptanceSpecs:rows.map(row=>row.file)};
const contract={schemaVersion:1,approvalReference:'synthetic',exceptions:[]};
writeFileSync(path.join(root,'scripts/qa/route-manifest.json'),JSON.stringify(manifest));
writeFileSync(path.join(root,'scripts/qa/full-local-coverage-contract.json'),JSON.stringify(contract));
for(const file of ['client/package.json','client/package-lock.json','server/package.json','server/package-lock.json','client/node_modules/playwright-core/browsers.json'])writeFileSync(path.join(root,file),'{}');
writeFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'),JSON.stringify({browsers:['chromium','webkit'].map(name=>({name,browserVersion:'synthetic'}))}));
writeFileSync(path.join(root,'client/node_modules/@playwright/test/package.json'),JSON.stringify({version:'1.58.2'}));
writeFileSync(path.join(root,'client/playwright.config.ts'),'// synthetic policy');
writeFileSync(path.join(root,'deploy/trialrun/validate-env-contract.js'),'// fake tooling owns this command');
writeFileSync(path.join(root,'.gitignore'),'artifacts/\nbin/\nnode_modules/\n');
const env={PATH:`${path.join(root,'bin')}:${process.env.PATH}`,LANG:'C.UTF-8',HYB_RELEASE_FIXTURE_ROOT:root,RELEASE_GATE_ARTIFACT_DIR:path.join(root,'artifacts/release-gate/run'),...overrides};
const fake=`#!${process.execPath}
const fs=require('node:fs'),path=require('node:path'),{spawnSync}=require('node:child_process');
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,args=process.argv.slice(2),name=path.basename(process.argv[1]);
fs.mkdirSync(path.join(root,'artifacts'),{recursive:true});fs.appendFileSync(path.join(root,'artifacts/commands.jsonl'),JSON.stringify({name,args})+'\\n');
if(name==='node' && args[0]?.endsWith('release-gate-result.mjs')){const result=spawnSync(process.execPath,args,{env:process.env,stdio:'inherit'});process.exit(result.status??1);}
if(name==='node' && args[0]?.endsWith('validate-env-contract.js') && process.env.FAIL_STATIC)process.exit(23);
if(name==='bash' && args[0]?.endsWith('run-web-gates.sh') && args[1]==='full-local'){if(process.env.OMIT_EVIDENCE)process.exit(0);const result=spawnSync(process.execPath,[path.join(root,'bin/evidence.mjs')],{env:process.env,stdio:'inherit'});process.exit(result.status??1);}
if(name==='bash' && args[0]?.includes('deploy/trialrun'))process.exit(91);
if(!['node','npm','bash','python3'].includes(name))throw Error('unexpected command');
`;
writeFileSync(path.join(root,'bin/fake'),fake,{mode:0o755});
for(const name of ['node','npm','bash','python3'])symlinkSync('fake',path.join(root,'bin',name));
writeFileSync(path.join(root,'bin/evidence.mjs'),`
import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';
import path from 'node:path';
import {captureReleaseSource,currentPolicy} from '../scripts/qa/release-gate-result.mjs';
import {FULL_LOCAL_STAGES,newStageRecord,updateStageRecord} from '../scripts/qa/qa-stage-record.mjs';
import {coveragePlan,localSpecArgs} from '../scripts/qa/full-local-coverage.mjs';
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,base=process.env.QA_FULL_REPORT_DIR,now=()=>new Date().toISOString();
const source=captureReleaseSource(root),policy=currentPolicy(root),startedAt=now();
const phases=newStageRecord(FULL_LOCAL_STAGES,'full-local-stages');
const rows=${JSON.stringify(rows)},contract=${JSON.stringify(contract)};
const report=executed=>({errors:[],suites:rows.map(row=>({file:row.file,specs:[{...row,tests:[{projectName:'',repeatEachIndex:0,...(executed?{expectedStatus:'passed',status:'expected',results:[{status:'passed',retry:0}]}:{})}]}]}))});
const discovery=report(false),execution=report(true);
execution.config={forbidOnly:true,projects:[{name:'',retries:0,repeatEach:1,timeout:180000,testIgnore:['**/manual/**']}],shard:null,maxFailures:0,workers:policy.toolchain.workers,version:policy.toolchain.playwright};
for(const stage of FULL_LOCAL_STAGES){updateStageRecord(phases,'start',stage);if(stage==='browser')execution.stats={startTime:now(),duration:0,expected:rows.length,unexpected:0,flaky:0,skipped:0};updateStageRecord(phases,'end',stage,0);}updateStageRecord(phases,'finish',null,0);
const environment={schemaVersion:1,profile:'isolated-online-v1',mode:'full-local',policyDigest:policy.environmentDigest,platform:process.platform,status:'PASS',exitCode:0,childExit:0,childSignal:null,interrupted:null,startedAt,endedAt:now(),sourceBefore:source,sourceAfter:source,sourceUnchanged:true,repositoryUnchanged:true,candidateIntegrityStatus:'UNCHANGED',cleanup:{containerOwnerVerified:true,containerRemoved:true,scratchRemoved:true},environment:{schemaVersion:1,profile:'isolated-online-v1',policyDigest:policy.environmentDigest,platform:process.platform,runnerDigest:policy.runnerDigest,probeDigest:policy.probeDigest,supervisorObservation:{source:'live-supervisor-unix-ipc',sampledAt:now(),namespace:'parent'},namespace:{inheritedRunner:'parent',ownedChild:'child',runnerCreated:true,currentMatchesOwnedChild:true,topologyMatched:true},nativeOnlineProbe:{valid:true,online:true,browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,engines:['chromium','webkit'].map(name=>({name,version:'synthetic',online:true,valid:true,noNavigatorOverride:true,browserNamespace:'child',browserMatchesOwnedChild:true}))},bootstrapObservation:{valid:true,observationPoint:'guest-entry-black-box',freshContext:true,initialSessionHint:false,listenerBeforeNavigation:true,noRouteMocks:true,loginFormVisible:true,refreshRequestCount:0,loginRequestCount:0,nativeOnlineSamples:{beforeNavigation:true,documentStart:true,afterForm:true},browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,documentStatus:200,stayedOnLogin:true}}};
const bundle={environment,phases,discovery,execution,plan:coveragePlan(root,discovery,contract,source)};
bundle.selection={schemaVersion:1,profile:'full-local-selection',specArgs:localSpecArgs(bundle.plan),additionalArgs:[],retries:0,forbidOnly:true,workers:policy.toolchain.workers,mode:'local'};
if(process.env.BAD_PROFILE)environment.mode='retained-local';
if(process.env.BAD_SOURCE)environment.sourceBefore={...source,digest:'0'.repeat(64)};
if(process.env.BAD_COVERAGE)execution.suites.pop();
if(process.env.BAD_CLEANUP)environment.cleanup.containerRemoved=false;
if(process.env.BAD_STAGE)phases.stages.splice(2,1);
for(const [key,relative] of Object.entries(${JSON.stringify(receiptFiles)})){const file=path.join(base,relative);mkdirSync(path.dirname(file),{recursive:true});writeFileSync(file,JSON.stringify(bundle[key]));}
if(process.env.FAIL_FULL)process.exit(17);
`);
for(const args of [['init','-q'],['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic fixture']])assert.equal(spawnSync('git',args,{cwd:root}).status,0);
return {root,env,run(mode='verify-candidate') {
rmSync(path.join(root,'artifacts'),{recursive:true,force:true});
const result=spawnSync('/bin/bash',[path.join(root,'scripts/qa/run-release-gate.sh'),mode],{cwd:root,env,encoding:'utf8',timeout:30000});
const file=path.join(env.RELEASE_GATE_ARTIFACT_DIR,'release-result.json');
return {...result,record:existsSync(file)?JSON.parse(readFileSync(file)):null,commands:existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').map(JSON.parse):[]};
},verify() {return spawnSync(process.execPath,[path.join(root,'scripts/qa/release-gate-result.mjs'),'verify',env.RELEASE_GATE_ARTIFACT_DIR],{cwd:root,env,encoding:'utf8',timeout:30000});}};
}
function noDeploy(result) {assert.ok(!result.commands.some(c=>c.args.some(arg=>arg.includes('deploy/trialrun/deploy'))));}
test('真实 shell 复用 static/full-local、回执离线重验、部署命令零调用',context=>{