mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(ci): preserve Windows gate ordering
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md
|
||||
2026-07-06-parallel-pre-push-gates.md: 189d6c2dfe08a9551037b936fd8015a3e86d1e51
|
||||
2026-07-06-parallel-pre-push-gates.zh.md: 17920b189c30db57f661df41a2664e3e727d1589
|
||||
2026-07-06-parallel-pre-push-gates.md: 2ae08b8c87939085a0f8c7e0cb3ac69fb3ab8e91
|
||||
2026-07-06-parallel-pre-push-gates.zh.md: d31397966e7561cb7edcd9815a57b22a4a3ba8e1
|
||||
|
||||
@@ -12,7 +12,7 @@ Aggregate jobs such as documentation synchronization hide long sequential chains
|
||||
|
||||
## Decision
|
||||
|
||||
[scripts/run-gates.ts](../../../../scripts/run-gates.ts) owns the bounded scheduler used by CI, `doc-sync`, and the opt-in `check:all` command. It expands named modes into leaf gates, rejects empty or ambiguous dependency graphs before starting a child, respects artifact dependencies, buffers attributable output by default, reports exit and signal outcomes independently, and accepts `DSH_GATE_CONCURRENCY` when a caller needs a different worker bound. A gate marked `allowFailure` still reports its result but does not fail the aggregate.
|
||||
[scripts/run-gates.ts](../../../../scripts/run-gates.ts) owns the bounded scheduler used by CI, `doc-sync`, and the opt-in `check:all` command. It expands named modes into leaf gates, rejects empty or ambiguous dependency graphs before starting a child, respects artifact dependencies, buffers attributable output by default, reports exit and signal outcomes independently, and accepts `DSH_GATE_CONCURRENCY` when a caller needs a different worker bound. A `needs` edge requires the predecessor to pass and skips its dependent otherwise; an `after` edge waits for any terminal outcome and then permits the follower to run. A gate marked `allowFailure` still reports its result but does not fail the aggregate.
|
||||
|
||||
Long coordinator gates whose own subprocesses preserve useful attribution may opt into `streamOutput`. Their stdout and stderr reach the parent immediately without being buffered or printed again at completion. Partitioned coverage and parallel Web snapshots use this mode so a mid-run failure is visible without waiting for sibling work.
|
||||
|
||||
@@ -24,7 +24,7 @@ The per-gate package scripts remain the vocabulary for ad hoc local runs. `hygie
|
||||
|
||||
## Verification
|
||||
|
||||
[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) rejects invalid graphs before the executor runs, pins the consumer and native Windows inventories and their dependency or failure semantics, exercises signal termination through a real child process, and proves that streamed output is immediate and unbuffered. [scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) rejects a missing public export before downstream artifact consumers run.
|
||||
[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) rejects invalid graphs before the executor runs, pins pass-required and settle-only ordering, pins the consumer and native Windows inventories and their failure semantics, exercises signal termination through a real child process, and proves that streamed output is immediate and unbuffered. [scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) rejects a missing public export before downstream artifact consumers run.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 拥有 CI、`doc-sync` 和按需启用的 `check:all` 命令所使用的有界调度器。它将具名模式展开为叶子门禁,在启动子进程前拒绝空的或有歧义的依赖图,遵守产物依赖,默认缓冲可归因的输出,分别报告进程退出与信号终止结果,并在调用方需要不同 worker 上限时接受 `DSH_GATE_CONCURRENCY`。标记为 `allowFailure` 的门禁仍会报告结果,但不会使聚合流程失败。
|
||||
[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 拥有 CI、`doc-sync` 和按需启用的 `check:all` 命令所使用的有界调度器。它将具名模式展开为叶子门禁,在启动子进程前拒绝空的或有歧义的依赖图,遵守产物依赖,默认缓冲可归因的输出,分别报告进程退出与信号终止结果,并在调用方需要不同 worker 上限时接受 `DSH_GATE_CONCURRENCY`。`needs` 边要求前置门禁通过,否则跳过依赖方;`after` 边只等待前置门禁以任意结果结算,随后仍允许后继门禁运行。标记为 `allowFailure` 的门禁仍会报告结果,但不会使聚合流程失败。
|
||||
|
||||
自身子进程能够保留有效归因的长时间协调门禁可以选择 `streamOutput`。其 stdout 与 stderr 会立即到达父进程,不会被缓冲,也不会在结束时重复打印。分区覆盖率与并行 Web 快照使用该模式,使运行中途的失败无需等待兄弟工作结束就能显示。
|
||||
|
||||
@@ -24,7 +24,7 @@ Node 24 消费方任务采用单个包含 10 道门禁的模式,而非由 shel
|
||||
|
||||
## 验证
|
||||
|
||||
[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) 在执行器运行前拒绝无效图,锁定消费方与原生 Windows 清单及其依赖或失败语义,通过真实子进程验证信号终止,并证明流式输出会立即显示且不被缓冲。[scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) 在下游产物消费方运行前拒绝缺失的公开导出。
|
||||
[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) 在执行器运行前拒绝无效图,锁定必须通过与只等结算两种顺序,锁定消费方与原生 Windows 清单及其失败语义,通过真实子进程验证信号终止,并证明流式输出会立即显示且不被缓冲。[scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) 在下游产物消费方运行前拒绝缺失的公开导出。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
|
||||
2026-08-08-native-windows-pull-request-ci.md: 7bcdfa7a3e560f247b3041ddf6dd214031c540fc
|
||||
2026-08-08-native-windows-pull-request-ci.zh.md: 12c853364cbfb191e3bf7c06734559dcdd498ab3
|
||||
2026-08-08-native-windows-pull-request-ci.md: d4883cf1363a33a444f1172829149c0c41f21c10
|
||||
2026-08-08-native-windows-pull-request-ci.zh.md: c6eb91f0cbc0f3a97599f0c1bb60b8bf98d9c844
|
||||
|
||||
@@ -18,7 +18,7 @@ Every pull request also starts an ordinary independent `windows-native` job name
|
||||
|
||||
The native job is deliberately absent from `all-checks-passed.needs` and does not use `continue-on-error`: the aggregate neither waits for it nor changes conclusion because of it, while the job retains its own unmasked result. Workspace build, production-site, and 100%-per-file coverage failures make the native job fail. Static, documentation, package, built-artifact, lint, and snapshot inventories run in the same job as observational gates: their failures remain visible without changing the native aggregate result because Linux owns their blocking verdict.
|
||||
|
||||
The 16-core lane admits four concurrent outer gates. Workspace build, production-site validation, instrumented coverage, and exempt-heavy coverage appear first and start together; every observational gate waits for both coverage gates before entering the available slots, so source-scanning tests cannot race static gates that create temporary contract files. [In-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) uses eight single-worker shards, while the exempt-heavy gate receives two workers from `DSH_COVERAGE_MAX_WORKERS=6`; together with build and site, the initial outer schedule has about twelve active execution units instead of exceeding twenty. Every Vitest project uses forked workers because Node 24's CJS lexer fatal reproduced in shared worker threads on Windows and POSIX. Both coverage gates set Vitest's default per-test and polling budgets to 30 seconds because unrelated process, Git, SQLite, watcher, grammar, and static-gate fixtures can exceed 15 seconds only under the complete lane's concurrent Windows instrumentation. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. These lane-scoped budgets preserve asserted outcomes, while the 120-minute job deadline still bounds a stuck run. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform.
|
||||
The 16-core lane admits four concurrent outer gates. Workspace build, production-site validation, and instrumented coverage start immediately. Exempt-heavy coverage waits for the build to pass, so its temporary Oxlint contract probes cannot race source compilation. Every observational gate waits for both coverage gates to settle, regardless of outcome, before entering an available slot; its own `needs` edges still require their predecessors to pass. This also keeps later static gates that create temporary contract files from racing either coverage scan. [In-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) uses eight single-worker shards, while the exempt-heavy gate receives two workers from `DSH_COVERAGE_MAX_WORKERS=6`. The initial phase therefore has about ten active execution units; after build, starting exempt-heavy while build leaves keeps the peak near eleven when site and instrumented coverage are still running. `publint` is capped at eight workers when the observational inventory starts. Every Vitest project uses forked workers because Node 24's CJS lexer fatal reproduced in shared worker threads on Windows and POSIX. Both coverage gates set Vitest's default per-test and polling budgets to 30 seconds because unrelated process, Git, SQLite, watcher, grammar, and static-gate fixtures can exceed 15 seconds only under the complete lane's concurrent Windows instrumentation. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. These lane-scoped budgets preserve asserted outcomes, while the 120-minute job deadline still bounds a stuck run. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform.
|
||||
|
||||
The 16-core allocation is the measured capacity point for this inventory. Six-worker coverage trials produced complete passes in 6 minutes 27 seconds and 7 minutes 50 seconds, while exact-head trials with four, three, and two concurrent workers inside one instrumented Vitest process exposed unreliable fixtures and worker exits. Separate single-worker child processes retain process isolation. Sixteen-shard samples reduced instrumented coverage to 112.66–122.01 seconds, but used the whole host before the exempt, build, and site work was counted; eight shards deliberately trade some latency for headroom. A 32-core comparison reduced aggregate gate time by only 1.47 seconds and still triggered the CJS-lexer fatal inside a fork worker, so additional cores did not provide a reliable wall-clock improvement.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
原生作业被刻意排除在 `all-checks-passed.needs` 之外,且不使用 `continue-on-error`:聚合流程既不等待它,也不会因它改变结论;该作业则保留自身未被掩盖的结果。工作区构建、生产网站和逐文件 100% 覆盖率检查失败会使原生作业失败。静态检查、文档、包、构建产物、lint 与快照清单在同一作业内作为观测性门禁运行;其失败保持可见,但不会改变原生聚合结果,因为这些检查的阻断性判定由 Linux 负责。
|
||||
|
||||
16 核通道最多同时运行 4 道外层门禁。工作区构建、生产网站验证、插桩覆盖率与豁免重型覆盖率排在最前并同时启动;每道观测性门禁都要等待两道覆盖率门禁完成后才进入可用槽位,避免扫描源码的测试与创建临时约定文件的静态门禁发生竞态。[job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.md)使用 8 个单 worker 分片,豁免重型门禁则从 `DSH_COVERAGE_MAX_WORKERS=6` 获得 2 个 worker;再加上构建与网站,初始外层调度约有 12 个活动执行单元,而不是超过 20 个。每个 Vitest 项目都使用 fork worker,因为 Node 24 的 CJS lexer 致命故障可在 Windows 与 POSIX 的共享 worker 中复现。两项覆盖率门禁都将 Vitest 默认的单测试和轮询时间预算设为 30 秒,因为在完整通道并发的 Windows 插桩下,多个互不相关的进程、Git、SQLite、watcher、语法和静态门禁 fixture(测试前置数据)可能超过 15 秒。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。这些只属于该通道的预算保留了原有断言结果,120 分钟的 job 截止时间仍会约束卡死的运行。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。
|
||||
16 核通道最多同时运行 4 道外层门禁。工作区构建、生产网站验证与插桩覆盖率会立即启动。豁免重型覆盖率等待构建通过后再启动,使其临时 Oxlint 约定探针不会与源码编译竞态。每道观测性门禁只等待两道覆盖率门禁以任意结果结算后再进入可用槽位;各门禁自身的 `needs` 边仍要求前置门禁通过。这也使随后创建临时约定文件的静态门禁不会与任一覆盖率扫描竞态。[job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.md)使用 8 个单 worker 分片,豁免重型门禁则从 `DSH_COVERAGE_MAX_WORKERS=6` 获得 2 个 worker。因此初始阶段约有 10 个活动执行单元;构建结束并启动豁免重型门禁后,如果网站与插桩覆盖率仍在运行,峰值约为 11 个。观测性清单启动时,`publint` 最多使用 8 个 worker。每个 Vitest 项目都使用 fork worker,因为 Node 24 的 CJS lexer 致命故障可在 Windows 与 POSIX 的共享 worker 中复现。两项覆盖率门禁都将 Vitest 默认的单测试和轮询时间预算设为 30 秒,因为在完整通道并发的 Windows 插桩下,多个互不相关的进程、Git、SQLite、watcher、语法和静态门禁 fixture(测试前置数据)可能超过 15 秒。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。这些只属于该通道的预算保留了原有断言结果,120 分钟的 job 截止时间仍会约束卡死的运行。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。
|
||||
|
||||
16 核配置是这项清单经实测选定的容量规格。使用 6 个 coverage worker 的试验分别以 6 分 27 秒和 7 分 50 秒跑出完整通过结果,而在单个插桩 Vitest 进程内使用 4 个、3 个和 2 个并发 worker 的分支头精确试验暴露出不稳定的 fixture 与 worker 退出。相互独立的单 worker 子进程保留进程隔离。16 分片样本把插桩覆盖率缩短到 112.66–122.01 秒,但还未计入豁免、构建与网站工作就已经占满整台宿主;8 个分片刻意用部分延迟换取余量。32 核对比仅将聚合门禁时间缩短 1.47 秒,且仍在 fork worker 内触发 CJS lexer 致命故障,因此增加核心数没有带来可靠的墙钟时间改善。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md
|
||||
2026-08-18-in-job-partitioned-coverage.md: b7335c490c4a4921e5c786809e0db492613ef5c8
|
||||
2026-08-18-in-job-partitioned-coverage.zh.md: 5e2a6a60d59751a22c7d664d4bc50d93390c6995
|
||||
2026-08-18-in-job-partitioned-coverage.md: f86c2dffb6d3d30fdccfa445c57043c5217e439b
|
||||
2026-08-18-in-job-partitioned-coverage.zh.md: c7b1df28f1603a558d8dd3a3f0f26c6fb1edc3bd
|
||||
|
||||
@@ -18,7 +18,7 @@ When partitioning is enabled, `scripts/run-gates.ts` selects `pnpm run test:cove
|
||||
|
||||
The coordinator waits for every child, validates that the blob directory contains exactly the expected files, and then runs one `vitest --merge-reports ... --coverage` command. Only that merged command applies the repository's per-file statement, branch, function, and line thresholds, so a partition is never judged against an intentionally partial inventory.
|
||||
|
||||
`DSH_COVERAGE_MAX_WORKERS` continues to size the uninstrumented exempt gate and the ordinary non-partitioned path; it does not resize partition children. Native Windows gives the exempt gate two workers and admits four concurrent outer gates: build, production-site validation, instrumented coverage, and exempt-heavy coverage start first, and the observational inventory waits for both coverage gates before entering the available slots. Linux overlaps four instrumented partition processes with two exempt workers, restoring the ordinary path's former four-way instrumented concurrency while keeping every instrumented process single-worker.
|
||||
`DSH_COVERAGE_MAX_WORKERS` continues to size the uninstrumented exempt gate and the ordinary non-partitioned path; it does not resize partition children. Native Windows gives the exempt gate two workers and admits four concurrent outer gates. Build, production-site validation, and instrumented coverage start immediately; exempt-heavy coverage starts only after build passes, preventing its temporary Oxlint probes from racing source compilation. The observational inventory waits only for both coverage gates to settle, so it still runs after a coverage failure; each gate's `needs` dependencies remain pass-required. Linux overlaps four instrumented partition processes with two exempt workers, restoring the ordinary path's former four-way instrumented concurrency while keeping every instrumented process single-worker.
|
||||
|
||||
## Failure and output semantics
|
||||
|
||||
@@ -38,7 +38,7 @@ Completed native Windows comparisons measured two partitions near 405 seconds an
|
||||
|
||||
**Raise the Vitest worker count inside one instrumented process.** Rejected because completed Windows trials at higher fan-out exposed worker exits, fixture instability, and Node 24 CJS lexer failures. Separate single-worker processes preserve isolation while still executing the selected partitions concurrently.
|
||||
|
||||
**Use one partition count on every host.** Rejected because Linux's two-process run and Windows's eight-process run have different startup costs and resource ceilings. Each fixed configuration requires its own completed end-to-end evidence.
|
||||
**Use one partition count on every host.** Rejected because Linux's four-process run and Windows's eight-process run have different startup costs and resource ceilings. Each fixed configuration requires its own completed end-to-end evidence.
|
||||
|
||||
**Apply thresholds independently in each partition.** Rejected because every partition intentionally sees only part of the suite and would report false uncovered files. Threshold ownership belongs to the merged report.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
协调器等待全部子进程结束,验证 blob 目录只包含预期文件,然后执行一次 `vitest --merge-reports ... --coverage`。只有这条合并命令应用仓库的逐文件语句、分支、函数与行阈值,因此系统不会拿有意不完整的测试清单单独判定任一分区。
|
||||
|
||||
`DSH_COVERAGE_MAX_WORKERS` 继续控制无插桩豁免门禁和普通非分区路径的规模,不会调整分区子进程。原生 Windows 为豁免门禁分配 2 个 worker,并允许 4 道外层门禁并发:构建、生产网站验证、插桩覆盖率与豁免重型覆盖率先启动,观测性清单等待两道覆盖率门禁完成后才进入可用槽位。Linux 让 4 个插桩分区进程与 2 个豁免 worker 重叠运行,在保持每个插桩进程只有 1 个 worker 的同时,恢复普通路径原有的 4 路插桩并发。
|
||||
`DSH_COVERAGE_MAX_WORKERS` 继续控制无插桩豁免门禁和普通非分区路径的规模,不会调整分区子进程。原生 Windows 为豁免门禁分配 2 个 worker,并允许 4 道外层门禁并发。构建、生产网站验证与插桩覆盖率会立即启动;豁免重型覆盖率只在构建通过后启动,避免其临时 Oxlint 探针与源码编译竞态。观测性清单只等待两道覆盖率门禁结算,因此在覆盖率失败后仍会运行;各门禁自身的 `needs` 依赖仍要求前置门禁通过。Linux 让 4 个插桩分区进程与 2 个豁免 worker 重叠运行,在保持每个插桩进程只有 1 个 worker 的同时,恢复普通路径原有的 4 路插桩并发。
|
||||
|
||||
## 失败与输出语义
|
||||
|
||||
@@ -38,7 +38,7 @@ Status: implemented
|
||||
|
||||
**提高单个插桩进程内的 Vitest worker 数。** 不予采用,因为已完成的 Windows 高扇出试验暴露了 worker 退出、fixture(测试前置数据)不稳定和 Node 24 CJS lexer 故障。相互独立的单 worker 进程既保留隔离,也能让所选分区并发执行。
|
||||
|
||||
**在每种宿主上使用相同的分区数量。** 不予采用,因为 Linux 的双进程运行与 Windows 的 8 进程运行具有不同的启动成本与资源上限。每种固定配置都必须取得自己的端到端完整证据。
|
||||
**在每种宿主上使用相同的分区数量。** 不予采用,因为 Linux 的 4 进程运行与 Windows 的 8 进程运行具有不同的启动成本与资源上限。每种固定配置都必须取得自己的端到端完整证据。
|
||||
|
||||
**在每个分区内独立应用阈值。** 不予采用,因为每个分区有意只看到套件的一部分,会误报未覆盖文件。阈值归合并报告所有。
|
||||
|
||||
|
||||
@@ -460,6 +460,7 @@ jobs:
|
||||
# under the complete lane's concurrent gate load.
|
||||
DSH_COVERAGE_TEST_TIMEOUT_MS: '30000'
|
||||
DSH_GATE_CONCURRENCY: '4'
|
||||
DSH_PUBLINT_CONCURRENCY: '8'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
|
||||
@@ -106,6 +106,8 @@ async function runScenario(kind: ManagedKind, trigger: ExitTrigger) {
|
||||
let settled = false
|
||||
let treeGone = false
|
||||
try {
|
||||
// The host validates tree.json before waiting for proceed, so observing it
|
||||
// is sufficient readiness; a second marker only adds a redundant Windows poll.
|
||||
state = await readTree(join(root, 'tree.json'))
|
||||
if (process.platform !== 'win32') identities = await captureIdentities(createProcessInspector(), state)
|
||||
await writeFile(join(root, 'proceed'), 'proceed')
|
||||
|
||||
@@ -109,15 +109,16 @@ describe('gate graph validation', () => {
|
||||
|
||||
expect(byId.get('coverage')?.allowFailure).not.toBe(true)
|
||||
expect(byId.get('coverage-exempt-heavy')?.allowFailure).not.toBe(true)
|
||||
expect(byId.get('coverage-exempt-heavy')?.needs).toContain('build')
|
||||
expect(observational).not.toHaveLength(0)
|
||||
for (const gate of observational) {
|
||||
const completeGate = byId.get(gate.id)
|
||||
expect(completeGate?.allowFailure).toBe(true)
|
||||
expect(completeGate?.needs).toEqual(expect.arrayContaining([
|
||||
expect(completeGate?.after).toEqual(expect.arrayContaining([
|
||||
'coverage',
|
||||
'coverage-exempt-heavy',
|
||||
...(gate.needs ?? []),
|
||||
]))
|
||||
expect(completeGate?.needs).toEqual(gate.needs)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -171,7 +172,9 @@ describe('gate graph validation', () => {
|
||||
['empty', [], /gate graph has no gates/],
|
||||
['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/],
|
||||
['unknown dependencies', [gate('subject', { needs: ['missing'] })], /depends on unknown gate "missing"/],
|
||||
['unknown ordering predecessors', [gate('subject', { after: ['missing'] })], /waits for unknown gate "missing"/],
|
||||
['cycles', [gate('first', { needs: ['second'] }), gate('second', { needs: ['first'] })], /dependency cycle: first -> second -> first/],
|
||||
['mixed cycles', [gate('first', { after: ['second'] }), gate('second', { needs: ['first'] })], /dependency cycle: first -> second -> first/],
|
||||
] as const)('rejects %s before starting a child', async (_label, invalid, message) => {
|
||||
const execute = vi.fn(async (subject: Gate) => resultFor(subject))
|
||||
|
||||
@@ -197,6 +200,29 @@ describe('gate graph validation', () => {
|
||||
expect(execute).toHaveBeenCalledWith(root)
|
||||
expect(results[0]).toMatchObject({ gate: dependent, status: 'skipped', error: 'dependency failed or skipped: root' })
|
||||
})
|
||||
|
||||
it('runs an ordered follower after its predecessor fails', async () => {
|
||||
const follower = gate('follower', { after: ['root'] })
|
||||
const root = gate('root')
|
||||
const execute = vi.fn(async (subject: Gate) => resultFor(subject, subject === root ? 'failed' : 'passed'))
|
||||
|
||||
const results = await runGates([follower, root], 2, execute)
|
||||
|
||||
expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['root', 'follower'])
|
||||
expect(results.map(result => result.status)).toEqual(['passed', 'failed'])
|
||||
})
|
||||
|
||||
it('runs an ordered follower after its predecessor is skipped', async () => {
|
||||
const follower = gate('follower', { after: ['dependent'] })
|
||||
const dependent = gate('dependent', { needs: ['root'] })
|
||||
const root = gate('root')
|
||||
const execute = vi.fn(async (subject: Gate) => resultFor(subject, subject === root ? 'failed' : 'passed'))
|
||||
|
||||
const results = await runGates([follower, dependent, root], 2, execute)
|
||||
|
||||
expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['root', 'follower'])
|
||||
expect(results.map(result => result.status)).toEqual(['passed', 'skipped', 'failed'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Oxlint gate', () => {
|
||||
|
||||
+42
-32
@@ -45,6 +45,8 @@ export interface Gate {
|
||||
command: string
|
||||
args: string[]
|
||||
needs?: string[]
|
||||
/** Gate ids that must settle, regardless of outcome, before this gate starts. */
|
||||
after?: string[]
|
||||
env?: Record<string, string | undefined>
|
||||
/** Keep a failure visible without failing the aggregate. */
|
||||
allowFailure?: boolean
|
||||
@@ -454,8 +456,10 @@ function ciWindowsBlockingGates(): Gate[] {
|
||||
}
|
||||
|
||||
function ciWindowsCompleteGates(): Gate[] {
|
||||
const coverage = coverageGates()
|
||||
const coverageNeeds = coverage.map(gate => gate.id)
|
||||
const coverage = coverageGates().map(gate => gate.id === 'coverage-exempt-heavy'
|
||||
? { ...gate, needs: [...new Set(['build', ...(gate.needs ?? [])])] }
|
||||
: gate)
|
||||
const coverageAfter = coverage.map(gate => gate.id)
|
||||
const observational = ciWindowsObservationalGates()
|
||||
// The required production site replaces the observational MPA build; both
|
||||
// VitePress modes write the same output directory and cannot overlap.
|
||||
@@ -463,7 +467,7 @@ function ciWindowsCompleteGates(): Gate[] {
|
||||
.map(gate => ({
|
||||
...gate,
|
||||
allowFailure: true,
|
||||
needs: [...new Set([...coverageNeeds, ...(gate.needs ?? [])])],
|
||||
after: [...new Set([...coverageAfter, ...(gate.after ?? [])])],
|
||||
}))
|
||||
return [
|
||||
pnpmScript('build', 'build'),
|
||||
@@ -713,6 +717,11 @@ function validateGateGraph(gates: readonly Gate[]): void {
|
||||
throw new Error(`run-gates: gate ${JSON.stringify(gate.id)} depends on unknown gate ${JSON.stringify(dependency)}.`)
|
||||
}
|
||||
}
|
||||
for (const predecessor of gate.after ?? []) {
|
||||
if (!ids.has(predecessor)) {
|
||||
throw new Error(`run-gates: gate ${JSON.stringify(gate.id)} waits for unknown gate ${JSON.stringify(predecessor)}.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cycle = findDependencyCycle(gates)
|
||||
@@ -734,8 +743,8 @@ function findDependencyCycle(gates: readonly Gate[]): string[] | undefined {
|
||||
|
||||
active.set(id, path.length)
|
||||
path.push(id)
|
||||
for (const dependency of gate.needs ?? []) {
|
||||
const cycle = visit(dependency)
|
||||
for (const predecessor of [...(gate.needs ?? []), ...(gate.after ?? [])]) {
|
||||
const cycle = visit(predecessor)
|
||||
if (cycle !== undefined) return cycle
|
||||
}
|
||||
path.pop()
|
||||
@@ -776,7 +785,7 @@ export async function runGates(
|
||||
for (;;) {
|
||||
let madeProgress = false
|
||||
while (running.length < maxActive) {
|
||||
const ready = gates.find(gate => states.get(gate.id) === 'pending' && dependenciesPassed(gate, states))
|
||||
const ready = gates.find(gate => states.get(gate.id) === 'pending' && predecessorsReady(gate, states))
|
||||
if (ready === undefined) break
|
||||
states.set(ready.id, 'running')
|
||||
running.push({ gate: ready, promise: execute(ready) })
|
||||
@@ -785,32 +794,24 @@ export async function runGates(
|
||||
}
|
||||
|
||||
if (running.length === 0) {
|
||||
let pending = gates.filter(gate => states.get(gate.id) === 'pending')
|
||||
while (pending.length > 0) {
|
||||
const gate = pending.find(item => (item.needs ?? []).some((id) => {
|
||||
const state = states.get(id)
|
||||
return state === 'failed' || state === 'skipped'
|
||||
}))
|
||||
if (gate === undefined) throw new Error('run-gates: validated graph stalled without a failed dependency.')
|
||||
const failedDeps = (gate.needs ?? []).filter((id) => {
|
||||
const state = states.get(id)
|
||||
return state === 'failed' || state === 'skipped'
|
||||
})
|
||||
const result: GateResult = {
|
||||
gate,
|
||||
status: 'skipped',
|
||||
durationMs: 0,
|
||||
output: [],
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
error: `dependency failed or skipped: ${failedDeps.join(', ')}`,
|
||||
}
|
||||
states.set(gate.id, 'skipped')
|
||||
results.set(gate.id, result)
|
||||
observe(result)
|
||||
pending = pending.filter(item => item !== gate)
|
||||
const pending = gates.filter(gate => states.get(gate.id) === 'pending')
|
||||
if (pending.length === 0) break
|
||||
const gate = pending.find(item => (item.needs ?? []).some(id => gateFailed(states.get(id))))
|
||||
if (gate === undefined) throw new Error('run-gates: validated graph stalled without a failed dependency.')
|
||||
const failedDeps = (gate.needs ?? []).filter(id => gateFailed(states.get(id)))
|
||||
const result: GateResult = {
|
||||
gate,
|
||||
status: 'skipped',
|
||||
durationMs: 0,
|
||||
output: [],
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
error: `dependency failed or skipped: ${failedDeps.join(', ')}`,
|
||||
}
|
||||
break
|
||||
states.set(gate.id, 'skipped')
|
||||
results.set(gate.id, result)
|
||||
observe(result)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!madeProgress) {
|
||||
@@ -829,8 +830,17 @@ export async function runGates(
|
||||
})
|
||||
}
|
||||
|
||||
function dependenciesPassed(gate: Gate, states: Map<string, GateState>): boolean {
|
||||
function predecessorsReady(gate: Gate, states: Map<string, GateState>): boolean {
|
||||
return (gate.needs ?? []).every(id => states.get(id) === 'passed')
|
||||
&& (gate.after ?? []).every(id => gateSettled(states.get(id)))
|
||||
}
|
||||
|
||||
function gateSettled(state: GateState | undefined): boolean {
|
||||
return state === 'passed' || state === 'failed' || state === 'skipped'
|
||||
}
|
||||
|
||||
function gateFailed(state: GateState | undefined): boolean {
|
||||
return state === 'failed' || state === 'skipped'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user