Pārlūkot izejas kodu

Merge pull request #3878 from deepseek-harness/fix/ci-cancel-superseded

fix(ci): cancel superseded PR and master validation
Tianyi Cui 2 nedēļas atpakaļ
vecāks
revīzija
f05e0fb6b9

+ 2 - 2
.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml

@@ -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-26-ci-failover-runbook.md
-2026-07-26-ci-failover-runbook.md: f24cb8b8239141cd1ccf468a566dba620dd3cfdd
-2026-07-26-ci-failover-runbook.zh.md: 57c4a92a3af720d9b11b7a1ce7a1515b83c77339
+2026-07-26-ci-failover-runbook.md: 559fa61bcf416bed3bd58b3ffcbc145038cdce22
+2026-07-26-ci-failover-runbook.zh.md: e2098b928b1a1f158bcbfabd940ca23a5cd0e28e

+ 3 - 7
.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md

@@ -12,11 +12,7 @@ The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.ym
 
 The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the three `node-compat` matrix entries, and `all-checks-passed` resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. A platform switch does not redirect the other platform. Set to `selfhosted` by a repository writer, the applicable trusted jobs select `vm-backup` or `dsh-win-ci`; otherwise they retain their workflow-defined hosted fallbacks. Node compatibility jobs require a same-repository, non-fork head and a non-Dependabot author, use isolated runtime setup, and retain `ubuntu-latest` fallback. Linux failover bounds snapshot concurrency and skips hosted package-cache restores. The verdict follows its workers so it does not remain queued on an unavailable hosted pool. Each switch is writer-manageable repository state, not a merge, so it works while checks are red. The `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes re-prove the complete unsharded aggregates on master pushes.
 
-`ci-master.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so one master push does not cancel the drill still running from the previous one. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check.
-
-The exemption is narrower than "a drill always finishes", in two ways. GitHub keeps a single pending entry per group, so a newer pending run displaces an older one and intermediate push runs still end as `cancelled` during busy periods. And the expression is evaluated against the *newly triggered* run, so a run whose own event is not `push` — a benchmark dispatched on master within `ci-master.yml`, sharing its group `CI master-<ref>` — evaluates to `true` and does cancel a drill that is mid-flight. That is a rare manual action and the next master push restores the evidence, so it does not warrant further mechanism. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence.
-
-The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push in `ci-master.yml` carries the [post-merge runtime and Wine checks](2026-09-06-master-only-platform-ci.md) and these two drills; the pull-request jobs live in the separate `ci.yml` (which does not see `push`), and the benchmarks are `workflow_dispatch`-gated within `ci-master.yml`. `scripts/ci-workflow.spec.ts` pins that push-reachable set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs.
+The [superseded-CI cancellation policy](2026-09-09-cancel-superseded-ci.md) governs master pushes and manual runs in the same workflow/ref group, including standby drills. Rapid master updates can starve a drill before it reaches a verdict. Use the latest completed standby verdict and check its age and commit before treating it as readiness evidence; a cancelled or merely scheduled run is not proof of readiness.
 
 ### Release rehearsals share the Linux switch
 
@@ -44,7 +40,7 @@ The two switches are independent: flip only the one whose platform is degraded.
 
 ## Capacity during failover
 
-Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds three Node compatibility jobs at gate concurrency one, including the build-backed Node 22 leg and cold temporary runtime downloads. The release workflows do not cancel running rehearsals when another run arrives, so overlapping refs can add sustained build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory.
+Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds three Node compatibility jobs at gate concurrency one, including the build-backed Node 22 leg and cold temporary runtime downloads. The release rehearsal workflows cancel superseded runs within each workflow/ref group under the [cancellation policy](2026-09-09-cancel-superseded-ci.md); different refs can still add concurrent build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory.
 
 
 ### Switch back
@@ -63,4 +59,4 @@ The variables are writer-manageable repository state; a pull request event itsel
 
 ## Consequences
 
-Recovering from a hosted-pool outage is flipping the affected platform's variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology per platform to keep working: the standby lanes exercise them on every master push so the failover targets never go stale, and the snapshot-concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg (Linux only) that must stay in step with the hosted leg. Splitting the switch by platform adds one more variable to manage but bounds the blast radius of each switch to the jobs of a single platform.
+Recovering from a hosted-pool outage is flipping the affected platform's variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology per platform to keep working: master pushes schedule the standby lanes, but only completed verdicts establish readiness under the [cancellation policy](2026-09-09-cancel-superseded-ci.md), and the snapshot-concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg (Linux only) that must stay in step with the hosted leg. Splitting the switch by platform adds one more variable to manage but bounds the blast radius of each switch to the jobs of a single platform.

+ 3 - 7
.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md

@@ -12,11 +12,7 @@ Status: implemented
 
 三个主要 Linux 作业(`node-24`、`node-24-coverage`、`node-24-consumers`)、三个 `node-compat` 矩阵条目和 `all-checks-passed` 通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;否则保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并保留 `ubuntu-latest` 回退。Linux 故障切换限制快照并发,并跳过托管软件包缓存恢复。判定作业跟随工作作业,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。
 
-`ci-master.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此一次 master 推送不会取消上一次推送留下的、仍在运行的演练。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。
-
-这项豁免比「演练总能跑完」要窄,有两点限制。其一,GitHub 每个组只保留一个待运行条目,更新的待运行条目会顶掉更早的,繁忙时段中间的推送运行仍会以 `cancelled` 结束。其二,该表达式是针对**新触发的运行**求值的,因此自身事件不是 `push` 的运行——例如在 `ci-master.yml` 内的 master 上派发的基准测试,与其演练共用 `CI master-<ref>` 组——求值为 `true`,会取消正在运行中的演练。这属于罕见的手动操作,且下一次 master 推送即可恢复证据,因此不值得为它再加机制。这项豁免换来的是该通道**周期性**地得出结论,而这正是它能作为证据的前提。
-
-这个决定必须放在工作流级:取消作用于被取代的整个运行,作业级 `concurrency` 组并不能豁免其所属作业。采用否定式写法而非仅指名 `pull_request`,是有实质作用的:后者会连 `workflow_dispatch` 一起停止取消,而每次运行器基准测试会在 master 上的同一并发组内同时占用 12 台大规格运行器、最长 15 分钟,届时重复派发会排在演练之前,而不是替换掉已过时的测量。成本之所以可控,是因为 `ci-master.yml` 中一次 master 推送承载[合并后的运行时与 Wine 检查](2026-09-06-master-only-platform-ci.zh.md)和这两条演练;拉取请求作业位于独立的 `ci.yml`(不监听 `push`),而基准测试在 `ci-master.yml` 内受 `workflow_dispatch` 门控。`scripts/ci-workflow.spec.ts` 会锁定这个推送可达集合——按条件精确匹配,因为否定式事件判断会包含它所排除的事件名——使新的推送可达作业无法悄悄开始累积未取消的运行。
+[被取代 CI 的取消策略](2026-09-09-cancel-superseded-ci.zh.md) 管理同一工作流/引用组内的 master 推送和手动运行,包括热备演练。master 快速更新可能让演练因反复被取消而始终无法得出结论。判断就绪状态时,使用最近一次已完成的热备结论,并核对其时间和提交;已取消或仅被调度的运行不构成就绪证据。
 
 ### 发布演练共用 Linux 开关
 
@@ -44,7 +40,7 @@ Status: implemented
 
 ## 切换期间的容量
 
-Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加三个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 22 条目和冷临时运行时下载。发布工作流不会因为新运行到来而取消正在执行的演练,因此不同引用的重叠运行会增加持续的构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。
+Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加三个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 22 条目和冷临时运行时下载。发布演练工作流依据[取消策略](2026-09-09-cancel-superseded-ci.zh.md)取消各工作流/引用组内被取代的运行;不同引用仍可能增加并发构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。
 
 
 ### 切回
@@ -63,4 +59,4 @@ Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以
 
 ## 后果
 
-从托管池故障中恢复只需切换受影响平台的变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是每个平台都要维护第二套运行器拓扑:热备通道在每次 master 推送时都运行它们,避免故障切换目标变得陈旧;而 `ci.yml` 中的快照并发与缓存恢复分支带有一条 `selfhosted` 支路(仅 Linux),必须与托管支路保持同步。按平台拆分开关多了一个需要管理的变量,但把每个开关的影响范围限定在单个平台的作业上。
+从托管池故障中恢复只需切换受影响平台的变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是每个平台都要维护第二套运行器拓扑:master 推送会调度热备通道,但依据[取消策略](2026-09-09-cancel-superseded-ci.zh.md),只有已完成的结论才能证明就绪状态;而 `ci.yml` 中的快照并发与缓存恢复分支带有一条 `selfhosted` 支路(仅 Linux),必须与托管支路保持同步。按平台拆分开关多了一个需要管理的变量,但把每个开关的影响范围限定在单个平台的作业上。

+ 2 - 2
.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml

@@ -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-09-06-master-only-platform-ci.md
-2026-09-06-master-only-platform-ci.md: 28284206c8c6d3fbb5de8ecadbcdf2035a5bb8c0
-2026-09-06-master-only-platform-ci.zh.md: eed843b0d235c80256343890e91b1de84f482174
+2026-09-06-master-only-platform-ci.md: d32864a4eafa81433c6d4fd0e47892b4d17091b1
+2026-09-06-master-only-platform-ci.zh.md: 4fb55d5e95f8fe75e6d1efb8b57295b8e8469e8d

+ 1 - 1
.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md

@@ -14,7 +14,7 @@ Python runtime builds on macOS Intel and ARM and Linux ARM64, plus Windows build
 
 Wine runs once as an independent hosted Ubuntu master job. Its existing image-keyed apt cache restore/save also supplies default-branch cache production, so it needs no separate cache-seeding job. The native Linux and Windows serial aggregates do not invoke Wine. Keeping Wine hosted avoids shared-host apt transactions and shared Wine-prefix cleanup on the persistent Linux VM. The script owns a scratch snapshot, a checkout-local Wine prefix, and a checksum-verified Windows Node cache; provisioning, failure propagation, and always-run cleanup remain intact.
 
-The parent and reusable runtime workflows preserve running master-push checks against subsequent master pushes. GitHub concurrency still permits replacement of pending runs; manual benchmarks can cancel the parent run. A master push schedules all three selected carriers but does not guarantee every intermediate commit reaches a result. PR, manual, and release cancellation retain their existing behavior.
+The [superseded-CI cancellation policy](2026-09-09-cancel-superseded-ci.md) applies to the parent and reusable runtime workflows: newer master pushes or manual runs cancel older validation in the same workflow/ref group, while release-owned builds remain protected. A master push schedules all three selected carriers but does not guarantee every intermediate commit reaches a result.
 
 This decision partially supersedes scheduling in the [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md), [native Windows CI](2026-08-08-native-windows-pull-request-ci.md), [serial references](2026-07-21-serial-cross-platform-ci-reference.md), and [failover runbook](2026-07-26-ci-failover-runbook.md). Those notes remain active for artifact provenance, platform fidelity, serial completeness, and trust rules.
 

+ 1 - 1
.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md

@@ -14,7 +14,7 @@ macOS Intel、ARM 与 Linux ARM64 上的 Python 运行时构建,以及通过 W
 
 Wine 作为独立的托管 Ubuntu master 作业运行一次。其现有的按镜像标识的 apt 缓存恢复和保存也负责生成默认分支缓存,因此不需要单独的缓存预热作业。原生 Linux 与 Windows 串行聚合不调用 Wine。Wine 保持托管运行,避免在持久 Linux VM 上执行共享宿主机 apt 事务和共享 Wine prefix 清理。脚本负责临时快照、checkout 内的 Wine prefix 和经过校验和验证的 Windows Node 缓存;环境准备、失败传播及始终执行的清理保持不变。
 
-父工作流与可复用运行时工作流均保留正在执行的 master 推送检查,不被后续 master 推送取消。GitHub 并发机制仍允许替换待执行的运行;手动基准测试可以取消父工作流。master 推送会调度全部三个选定载体,但不保证每个中间提交都得到结果。PR(Pull Request)、手动和发布运行的取消行为保持不变。
+[被取代 CI 的取消策略](2026-09-09-cancel-superseded-ci.zh.md) 适用于父工作流与可复用运行时工作流:更新的 master 推送或手动运行会取消同一工作流/引用组内的旧验证,而发布所属的构建仍受保护。master 推送会调度全部三个选定载体,但不保证每个中间提交都得到结果。
 
 本决策部分取代[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)、[原生 Windows CI](2026-08-08-native-windows-pull-request-ci.zh.md)、[串行参考](2026-07-21-serial-cross-platform-ci-reference.zh.md)和[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)中的调度策略。这些记录仍保留产物来源、平台保真度、串行完整性与信任规则的决策价值。
 

+ 6 - 0
.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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-09-09-cancel-superseded-ci.md
+2026-09-09-cancel-superseded-ci.md: 8de1f757870a2b18852a224ec2f1cc33d9f6a95e
+2026-09-09-cancel-superseded-ci.zh.md: 3ae6ec6ad58b0dfc94e803a33ba92ded4e239485

+ 39 - 0
.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.md

@@ -0,0 +1,39 @@
+# Agent Note: Cancel superseded CI validation
+
+Status: implemented
+
+English | [中文](2026-09-09-cancel-superseded-ci.zh.md)
+
+## Problem
+
+Validation of an obsolete PR revision or master commit consumes runner capacity without establishing the newest revision’s status. Unconditional aggregate verdicts and coverage-history uploads can also keep cancelled runs doing bookkeeping. Preserving older post-merge runs favors historical completion over current validation, especially on the shared self-hosted pools.
+
+## Decision
+
+Validation favors the newest run within each workflow/ref group. [CI](../../../../.github/workflows/ci.yml), [CI master](../../../../.github/workflows/ci-master.yml), [real-API e2e](../../../../.github/workflows/e2e.yml), and the credential-free [dsh](../../../../.github/workflows/release.yml) and [vendor](../../../../.github/workflows/release-vendor.yml) pack validations use `cancel-in-progress: true` with `${{ github.workflow }}-${{ github.ref }}`. Different PR refs and different workflows do not cancel each other. Event type is not part of the group: master pushes and manual benchmarks can supersede each other in CI master, and e2e pushes, scheduled runs, and manual runs can supersede each other on the same ref.
+
+The [reusable Python runtime builder](../../../../.github/workflows/build-exe-for-python-sdk.yml) uses `${{ !inputs.release }}`. Its `build-single-exe-${{ github.workflow }}-${{ github.ref }}` group remains distinct from its caller’s group, and the caller workflow name isolates ordinary CI from release-owned builds. Release-owned builds are exempt because they belong to an intentional publication transaction. Publication, deployment, and metadata workflows retain their own policies; this decision does not apply cancellation indiscriminately across workflows.
+
+The PR aggregate uses `${{ !cancelled() && github.event_name == 'pull_request' }}`. The explicit status function preserves evaluation after failed or skipped dependencies rather than accepting GitHub’s default success-only condition. The aggregate still fails on any failure, cancellation, or skip among its dependencies when the workflow itself is not cancelled; cancellation of the whole workflow suppresses its obsolete verdict. Coverage duration history uses `!cancelled()` too: failed coverage can still save useful measurements, but cancelled coverage does not upload them. Wine’s `always()` cleanup remains necessary resource cleanup rather than optional bookkeeping.
+
+This reverses the cancellation exemption in the [failover runbook](2026-07-26-ci-failover-runbook.md), [master-only platform CI](2026-09-06-master-only-platform-ci.md), and [real-API e2e decision](../testing/2026-06-19-real-api-e2e-ci.md). Those notes retain independent value for pool trust and switching, platform coverage, and secret exposure. The [release rehearsal decision](2026-09-06-release-rehearsal-selfhosted.md) retains runner selection and isolation ownership. None is fully superseded or archived.
+
+## Alternatives considered
+
+**Preserve running master-push drills.** The former `${{ github.event_name != 'push' }}` exemption favored periodic readiness evidence: each standby executes its complete unsharded aggregate with one gate worker and can outlast the interval between master merges. Even that policy did not guarantee every drill completed. GitHub retains one pending run per group, replacing intermediate pending pushes; cancellation is evaluated on the newly triggered run, so a manual benchmark sharing the master group could still cancel a drill. That rare manual interruption was accepted on the expectation of evidence from a subsequent push. The exemption’s cost was bounded by the master-only runtime checks, Wine, and two drills; PR jobs remained in a separate workflow, and exact-condition regression checks pinned the push-reachable job set. This policy is rejected in favor of freeing capacity for current validation, explicitly accepting standby starvation.
+
+**Protect a drill with job-level concurrency, or cancel only PR events.** A job-level group cannot exempt a job from cancellation of its entire workflow. A PR-only cancellation condition also exempts manual dispatch: a repeated runner benchmark can occupy twelve larger runners for up to fifteen minutes rather than replacing an obsolete measurement. Workflow-level cancellation covers both pushes and manual runs.
+
+**Keep every post-merge, nightly, and pack run.** Historical completion provides more per-commit and per-trigger evidence, but obsolete validation competes with the newest run. These validations do not publish packages, so preserving every run is not the same requirement as protecting an intentional publication transaction.
+
+**Replace every `always()` condition.** Failure aggregation and resource cleanup have different obligations. A success-only aggregate can hide failed dependencies behind a skipped required check; removing unconditional Wine cleanup can leave resources running. Only cancelled-run bookkeeping is suppressed.
+
+## Consequences
+
+Rapid master updates can repeatedly cancel the longer standby drills before they produce a verdict. Operators use the latest completed standby verdict, checking its age and commit before relying on it for failover readiness; a scheduled, running, or cancelled drill is not readiness evidence. The policy does not guarantee that every intermediate commit, nightly trigger, or benchmark completes. Different refs can still compete for shared host capacity.
+
+Cancellation is a request handled by GitHub Actions and its runners, not a guarantee of immediate termination or bounded queue delay. Cleanup can still take time. The policy makes obsolete validation cancellable; it does not promise a fixed runtime or cancellation latency.
+
+## Verification
+
+[Workflow regressions](../../../../scripts/ci-workflow.spec.ts) pin workflow/ref isolation, release-owned exemptions, aggregate status conditions, coverage-history cancellation, and retained Wine cleanup. [Platform routing regressions](../../../../scripts/tests/ci-master-platforms.spec.ts) preserve the master/PR target split and release matrix; [release rehearsal regressions](../../../../scripts/tests/ci-release-selfhosted.spec.ts) preserve cancellation alongside runner eligibility and publication isolation. These configuration checks do not reproduce GitHub scheduling or runner shutdown. Live supersession and completed standby evidence remain CI verification responsibilities.

+ 39 - 0
.agents/notes/implemented/process/2026-09-09-cancel-superseded-ci.zh.md

@@ -0,0 +1,39 @@
+# Agent Note: 取消被取代的 CI 验证
+
+Status: implemented
+
+[English](2026-09-09-cancel-superseded-ci.md) | 中文
+
+## 问题
+
+验证已被取代的 PR(Pull Request)修订或 master 提交会消耗运行器容量,却不能确定最新修订的状态。无条件执行的聚合判定和覆盖率耗时历史上传,还可能让已取消的运行继续处理记账任务。保留旧的合并后运行,意味着优先完成历史验证而非当前验证,在共享自托管池上尤其如此。
+
+## 决策
+
+验证优先保留各工作流/引用组内最新的运行。[CI](../../../../.github/workflows/ci.yml)、[CI master](../../../../.github/workflows/ci-master.yml)、[真实 API e2e](../../../../.github/workflows/e2e.yml),以及无凭据的 [dsh](../../../../.github/workflows/release.yml) 和 [vendor](../../../../.github/workflows/release-vendor.yml) 打包验证,均在 `${{ github.workflow }}-${{ github.ref }}` 组中使用 `cancel-in-progress: true`。不同 PR 引用和不同工作流不会相互取消。事件类型不参与分组:CI master 中的 master 推送与手动基准测试可以相互取代,e2e 的推送、定时运行和手动运行也可以在同一引用上相互取代。
+
+[可复用 Python 运行时构建器](../../../../.github/workflows/build-exe-for-python-sdk.yml)使用 `${{ !inputs.release }}`。其 `build-single-exe-${{ github.workflow }}-${{ github.ref }}` 组与调用方的组保持区分,调用方工作流名称将普通 CI 与发布所属的构建隔离。发布所属的构建获得豁免,因为它们属于一次有意发起的发布事务。发布、部署和元数据工作流保留各自的策略;本决策不会不加区分地对所有工作流应用取消。
+
+PR 聚合使用 `${{ !cancelled() && github.event_name == 'pull_request' }}`。显式状态函数使其在依赖失败或跳过后仍然求值,而非采用 GitHub 默认的仅成功条件。当工作流本身未被取消时,聚合仍会因任意依赖失败、取消或跳过而失败;整个工作流被取消时则抑制其已失去用途的判定。覆盖率耗时历史也使用 `!cancelled()`:覆盖率失败时仍可保存有用的测量数据,但被取消的覆盖率运行不上传。Wine 的 `always()` 清理仍是必要的资源清理,而非可选记账任务。
+
+本决策推翻[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)、[仅 master 执行的平台 CI](2026-09-06-master-only-platform-ci.zh.md) 和[真实 API e2e 决策](../testing/2026-06-19-real-api-e2e-ci.zh.md)中的取消豁免。这些记录对运行器池信任与切换、平台覆盖和密钥暴露仍有独立价值。[发布演练决策](2026-09-06-release-rehearsal-selfhosted.zh.md)仍负责运行器选择与隔离。没有记录被完全取代或归档。
+
+## 曾考虑的替代方案
+
+**保留正在执行的 master 推送演练。** 原有 `${{ github.event_name != 'push' }}` 豁免优先保证周期性就绪证据:每条热备以单门禁工作进程执行完整的未分片聚合流程,耗时可能长于 master 合并间隔。即便该策略也不保证每次演练完成。GitHub 每个组仅保留一个待运行条目,会替换中间的待执行推送;取消条件针对新触发的运行求值,因此共享 master 组的手动基准测试仍可取消演练。当时接受了这种罕见的手动中断,期望后续推送提供证据。豁免成本被限定为仅 master 执行的运行时检查、Wine 和两条演练;PR 作业仍在独立工作流中,按精确条件匹配的回归检查固定推送可达作业集合。为释放容量给当前验证而否决该策略,明确接受热备因反复被取消而无法完成。
+
+**用作业级并发保护演练,或仅取消 PR 事件。** 作业级分组不能让作业免于整个工作流的取消。仅针对 PR 的取消条件还会豁免手动触发:重复派发的运行器基准测试可能占用十二台大型运行器长达十五分钟,而非替换陈旧的测量。工作流级取消同时覆盖推送和手动运行。
+
+**保留每次合并后、每夜和打包运行。** 完成历史运行能提供更多按提交和触发划分的证据,但已被取代的验证会与最新运行竞争。这些验证不发布包,因此保留每次运行与保护有意发起的发布事务并不是同一项要求。
+
+**替换每个 `always()` 条件。** 失败聚合与资源清理承担不同义务。仅成功时执行的聚合可能把依赖失败隐藏为跳过的必需检查;移除无条件 Wine 清理则可能留下仍在运行的资源。只有已取消运行的记账任务被抑制。
+
+## 后果
+
+master 快速更新可能反复取消耗时更长的热备演练,使其无法产出结论。运维人员使用最近一次已完成的热备结论,并在依赖它判断故障切换就绪状态前核对其时间和提交;已调度、正在执行或已取消的演练都不构成就绪证据。该策略不保证每个中间提交、每夜触发或基准测试都能完成。不同引用仍会竞争共享主机容量。
+
+取消是由 GitHub Actions 及其运行器处理的请求,不保证立即终止或限定排队时长。清理仍可能耗时。该策略让已被取代的验证可以被取消,但不承诺固定运行时长或取消延迟。
+
+## 验证
+
+[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)固定工作流/引用隔离、发布所属构建豁免、聚合状态条件、覆盖率历史取消及保留的 Wine 清理。[平台路由回归测试](../../../../scripts/tests/ci-master-platforms.spec.ts)保留 master/PR 目标划分与发布矩阵;[发布演练回归测试](../../../../scripts/tests/ci-release-selfhosted.spec.ts)在验证取消策略的同时保留运行器准入与发布隔离。这些配置检查不重现 GitHub 调度或运行器停止过程。真实运行取代行为与已完成热备证据仍由 CI 负责验证。

+ 2 - 2
.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml

@@ -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/testing/2026-06-19-real-api-e2e-ci.md
-2026-06-19-real-api-e2e-ci.md: 4f51032d90b6773d01db7bbfdced6328bd882ad0
-2026-06-19-real-api-e2e-ci.zh.md: 390df23aebec3a6a54bc48eb52022fe02e872164
+2026-06-19-real-api-e2e-ci.md: b218d77f0817416b01459fbb70f5cacffe048ee8
+2026-06-19-real-api-e2e-ci.zh.md: 52c988dad00ddb3ee7f0cd2d23da9771f0900808

+ 1 - 1
.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md

@@ -54,7 +54,7 @@ The repo secret is named `DEEPSEEK_API_KEY_EXTERNAL`; it is mapped to the `DEEPS
 
 ### Scope, runtime shape
 
-The job runs only `test:e2e` on Node 24; keyless gates and version compatibility belong to the main CI workflow. Tests run unbuilt through the workspace paths map with a bounded configurable worker pool, per-test retries, and a job timeout. Superseded PR runs are cancelled, while push and scheduled runs complete for post-merge signal.
+The job runs only `test:e2e` on Node 24; keyless gates and version compatibility belong to the main CI workflow. Tests run unbuilt through the workspace paths map with a bounded configurable worker pool, per-test retries, and a job timeout. The [superseded-CI cancellation policy](../process/2026-09-09-cancel-superseded-ci.md) cancels older runs in the same workflow/ref group across PR, push, schedule, and manual triggers; a post-merge or nightly trigger does not guarantee completion.
 
 The DeepSeek native `web_search` probe is registered but skipped. The live Anthropic-compatible endpoint can return a successful response without structured source blocks, so its positive-source assertion is not a reliable merge signal; unit coverage still pins response parsing, but CI does not prove the live source-block wire shape.
 

+ 1 - 1
.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md

@@ -54,7 +54,7 @@ repo secret 命名为 `DEEPSEEK_API_KEY_EXTERNAL`;映射到适配器和测试
 
 ### 范围与运行时形态
 
-job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。被取代的 PR 运行会被取消,而 push 和 schedule 运行完整执行以提供合并后信号
+job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。[被取代 CI 的取消策略](../process/2026-09-09-cancel-superseded-ci.zh.md)会在 PR、push、schedule 和手动触发之间取消同一工作流/引用组内的旧运行;合并后或每夜触发并不保证完成
 
 DeepSeek 原生 `web_search` 探测已注册但会跳过。线上 Anthropic 兼容端点可能返回成功响应却没有结构化来源块,因此对来源存在性的正向断言不是可靠的合并信号;单元测试仍会锁定响应解析行为,但 CI 不会验证线上端点返回的来源块协议格式(wire format)。
 

+ 2 - 1
.github/workflows/build-exe-for-python-sdk.yml

@@ -46,7 +46,8 @@ concurrency:
   # github.workflow identifies the caller inside a reusable workflow and keeps
   # an ordinary CI run from cancelling a full release validation on the same ref.
   group: build-single-exe-${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }}
+  # Release-owned builds are part of an intentional publication transaction.
+  cancel-in-progress: ${{ !inputs.release }}
 
 permissions:
   contents: read

+ 3 - 6
.github/workflows/ci-master.yml

@@ -14,14 +14,11 @@ on:
           - larger-runner-benchmark
           - consolidated-runner-benchmark
 
-# Master runs platform runtime checks, Wine, and two self-hosted standby drills.
-# The drills outlast the interval between master merges, so
-# push is exempt from cancellation (see ci-failover-runbook). workflow_dispatch
-# keeps cancelling: a re-dispatched runner benchmark holds up to 12 larger
-# runners for 15 minutes in this same group.
+# New master pushes and manual runs replace obsolete checks in this workflow/ref.
+# Standby drills share cancellation; use completed runs as readiness evidence.
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: ${{ github.event_name != 'push' }}
+  cancel-in-progress: true
 
 permissions:
   contents: read

+ 4 - 6
.github/workflows/ci.yml

@@ -562,7 +562,7 @@ jobs:
         # Coverage flakes must not prevent the cache from building; the
         # measured durations remain useful even when a partition failed. The
         # per-run key keeps every save a fresh immutable cache entry.
-        if: always()
+        if: ${{ !cancelled() }}
         uses: actions/cache/save@v4
         with:
           path: .coverage-times.json
@@ -666,10 +666,8 @@ jobs:
   # `needs`. Native Windows build and process checks are required; Wine and
   # the deferred Python runtime targets live in ci-master.yml and do not
   # participate in this PR verdict. `needs` cannot cross workflow files.
-  # `if: always()` is load-bearing: without it a failed dependency
-  # would SKIP this job, and GitHub counts a skipped required check as passing
-  # — so this job always runs and fails on any non-success result, including
-  # 'cancelled' and 'skipped'.
+  # An explicit status function runs the verdict after failed/skipped needs
+  # without keeping a cancelled workflow alive for an obsolete verdict.
   all-checks-passed:
     name: all checks passed
     # This bookkeeping-only verdict must not depend on custom-pool
@@ -684,7 +682,7 @@ jobs:
           && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
           || 'ubuntu-latest' }}
     needs: [node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, python-sdk, python-runtime, windows-build, windows-native-tests]
-    if: always() && github.event_name == 'pull_request'
+    if: ${{ !cancelled() && github.event_name == 'pull_request' }}
     steps:
       - name: Fail if any needed job did not succeed
         if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')

+ 3 - 3
.github/workflows/e2e.yml

@@ -36,11 +36,11 @@ on:
     # 00:17 UTC nightly = 08:17 Asia/Shanghai — off the top-of-hour cron stampede.
     - cron: '17 0 * * *'
 
-# Cancel a superseded PR run (it is on a stale commit); never cancel a
-# push/schedule run — it is already producing the post-merge/nightly signal.
+# Keep only the newest validation in each workflow/ref, including master
+# pushes, nightly runs, and manual dispatches.
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+  cancel-in-progress: true
 
 # Least privilege: this job only reads the repo to run tests.
 permissions:

+ 1 - 1
.github/workflows/release-vendor.yml

@@ -20,7 +20,7 @@ permissions:
 concurrency:
   # Pack runs per ref so concurrent pull requests never displace each other.
   group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: false
+  cancel-in-progress: true
 
 env:
   PRIMARY_NODE_VERSION: '24'

+ 1 - 1
.github/workflows/release.yml

@@ -19,7 +19,7 @@ permissions:
 concurrency:
   # Pack runs per ref so concurrent pull requests never displace each other.
   group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: false
+  cancel-in-progress: true
 
 env:
   PRIMARY_NODE_VERSION: '24'

+ 44 - 21
scripts/ci-workflow.spec.ts

@@ -8,6 +8,43 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{
 const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
 
 describe('CI workflow', () => {
+  it.each(['ci.yml', 'ci-master.yml', 'e2e.yml', 'release.yml', 'release-vendor.yml'])(
+    '%s cancels superseded validation runs without crossing workflow or ref boundaries', (name) => {
+      const workflow = loadWorkflow('.github/workflows/' + name)
+      expect(workflow.concurrency).toEqual({
+        group: '${{ github.workflow }}-${{ github.ref }}',
+        'cancel-in-progress': true,
+      })
+    },
+  )
+
+  it('cancels reusable CI builds without cancelling release-owned builds', () => {
+    const workflow = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml')
+    expect(workflow.concurrency).toEqual({
+      group: 'build-single-exe-${{ github.workflow }}-${{ github.ref }}',
+      'cancel-in-progress': '${{ !inputs.release }}',
+    })
+  })
+
+  it('does not cancel protected publication or deployment transactions', () => {
+    for (const name of ['release-publish.yml', 'release-vendor-publish.yml']) {
+      const publish = workflowJob(loadWorkflow('.github/workflows/' + name), 'publish')
+      expect(publish.concurrency).toMatchObject({ 'cancel-in-progress': false })
+    }
+    for (const name of ['python-release.yml', 'node-addon-system-release.yml', 'docs-pages.yml']) {
+      expect(loadWorkflow('.github/workflows/' + name).concurrency).toMatchObject({ 'cancel-in-progress': false })
+    }
+  })
+
+  it('skips coverage-history uploads on cancellation but retains Wine cleanup', () => {
+    const coverage = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'windows-coverage')
+    const wine = workflowJob(loadWorkflow('.github/workflows/ci-master.yml'), 'windows')
+    expect(coverage.steps).toContainEqual(expect.objectContaining({
+      name: 'Save coverage duration history', if: '${{ !cancelled() }}',
+    }))
+    expect(wine.steps).toContainEqual(expect.objectContaining({ name: 'Shut down wineserver', if: 'always()' }))
+  })
+
   it('isolates every pnpm action setup destination per runner', () => {
     const files = ['.github/workflows/ci.yml', '.github/workflows/ci-master.yml']
     const setups: Array<{ jobName: string; step: unknown }> = []
@@ -346,7 +383,7 @@ describe('CI workflow', () => {
     )
   })
 
-  it('exempts push from cancellation in ci-master, so one master merge does not cancel the running drill', () => {
+  it('cancels superseded master runs without changing the post-merge job inventory', () => {
     const workflow = loadWorkflow('.github/workflows/ci-master.yml')
     const prWorkflow = loadWorkflow('.github/workflows/ci.yml')
     if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) {
@@ -356,21 +393,11 @@ describe('CI workflow', () => {
       throw new TypeError('ci workflow must define jobs')
     }
 
-    // Cancellation applies to the whole superseded RUN, so this has to be
-    // decided at workflow level and gated on the event: a job-level group
-    // cannot exempt its job from its run being cancelled. Only push is exempt —
-    // a drill takes longer than the interval between master merges. The negated
-    // form is load-bearing: `== 'pull_request'` would also stop cancelling
-    // workflow_dispatch, and a re-dispatched runner benchmark holds up to 12
-    // larger runners for 15 minutes in this same group on master.
-    expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}")
-
-    // The PR-only ci.yml still cancels a superseded run on a new push, so a
-    // fresh head does not stack a second full 9-job run behind a stale one.
-    // Unlike ci-master it has no push carve-out: every PR event supersedes.
-    expect(prWorkflow.concurrency).toMatchObject({
+    expect(workflow.concurrency).toEqual({
+      group: '${{ github.workflow }}-${{ github.ref }}',
       'cancel-in-progress': true,
     })
+    expect(prWorkflow.concurrency).toEqual(workflow.concurrency)
 
     // The exact event sets are what keep master-only jobs out of the PR check
     // panel: ci-master triggers only on push(master) + workflow_dispatch and
@@ -382,13 +409,12 @@ describe('CI workflow', () => {
     expect(Object.keys(workflow.on).sort()).toEqual(['push', 'workflow_dispatch'])
     expect(Object.keys(prWorkflow.on)).toEqual(['pull_request'])
 
-    // Neither drill may carry a job-level group: it would not exempt the job
-    // from run-scoped cancellation.
+    // Drills share the parent run’s supersession policy.
     for (const name of ['serial-linux-selfhosted', 'serial-windows']) {
       const job = workflow.jobs[name]
       if (!isRecord(job)) throw new TypeError(`${name} must be defined`)
       expect(job.concurrency).toBeUndefined()
-      // Both stay master-push-only; that is what makes the push carve-out safe.
+      // Standby drills remain post-merge work, but share run cancellation.
       expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
     }
 
@@ -409,10 +435,7 @@ describe('CI workflow', () => {
       .sort()
     expect(pushReachable).toEqual(['python-runtime', 'serial-linux-selfhosted', 'serial-windows', 'windows'])
 
-    // Why workflow_dispatch must keep cancelling: each benchmark fans out to a
-    // dozen larger runners at once, in this same group on master. If it stopped
-    // cancelling, a re-dispatch would queue ahead of a drill instead of
-    // replacing the stale measurement.
+    // Manual benchmarks retain their bounded fan-out.
     for (const name of ['larger-runner-benchmark', 'consolidated-runner-benchmark']) {
       const job = workflow.jobs[name]
       if (!isRecord(job) || !isRecord(job.strategy)) {

+ 39 - 2
scripts/tests/ci-master-platforms.spec.ts

@@ -1,6 +1,7 @@
 /** Scheduling policy for post-merge native runtime carriers and Wine. */
 import { readFileSync } from 'node:fs'
 import { resolve } from 'node:path'
+import { runInNewContext } from 'node:vm'
 import { load } from 'js-yaml'
 import { describe, expect, it } from 'vitest'
 import { gatesForMode } from '../run-gates.ts'
@@ -34,7 +35,43 @@ function commands(job: Job): string[] {
   return (job.steps ?? []).flatMap(step => step.run ? [step.run] : [])
 }
 
+// These boolean/string cases share Actions and JavaScript semantics. GitHub
+// supplies status functions; this probe is not a general Actions interpreter.
+function evaluateCondition(expression: string, cancelled: boolean, results: string[], event = 'pull_request'): boolean {
+  const source = expression.trim().replace(/^[$][{][{]|[}][}]$/g, '')
+    .replaceAll('needs.*.result', 'results')
+  return runInNewContext(source, {
+    cancelled: () => cancelled,
+    always: () => true,
+    contains: (values: string[], value: string) => values.includes(value),
+    results,
+    github: { event_name: event },
+  }, { timeout: 1000 }) as boolean
+}
+
 describe('master-only platform scheduling', () => {
+  it.each(['success', 'failure', 'skipped', 'cancelled'])(
+    'reports %s dependencies in active runs but never starts a cancelled-run verdict', (result) => {
+      const aggregate = workflow('ci.yml').jobs['all-checks-passed']!
+      const results = aggregate.needs!.map(() => 'success')
+      results[0] = result
+      const condition = aggregate.if as string
+      // A status function prevents Actions from implicitly gating on success().
+      expect(condition).toContain('!cancelled()')
+      expect(evaluateCondition(condition, false, results)).toBe(true)
+      expect(evaluateCondition(condition, true, results)).toBe(false)
+      expect(evaluateCondition(condition, false, results, 'push')).toBe(false)
+      const failureStep = aggregate.steps!.find(step => step.name === 'Fail if any needed job did not succeed')!
+      expect(evaluateCondition(failureStep.if!, false, results)).toBe(result !== 'success')
+      expect(failureStep.run).toContain('exit 1')
+    },
+  )
+
+  it('distinguishes the obsolete always verdict from the cancellable status guard', () => {
+    expect(evaluateCondition("always() && github.event_name == 'pull_request'", true, ['success'])).toBe(true)
+    expect(evaluateCondition(workflow('ci.yml').jobs['all-checks-passed']!.if as string, true, ['success'])).toBe(false)
+  })
+
   it('keeps only Linux and Windows x64 runtimes in required PR CI', () => {
     const pr = workflow('ci.yml')
     expect(Object.keys(pr.on)).toEqual(['pull_request'])
@@ -49,7 +86,7 @@ describe('master-only platform scheduling', () => {
     expect(aggregate.needs).toContain('python-runtime')
     expect(aggregate.needs).not.toContain('windows')
     expect(aggregate.needs!.every(id => id in pr.jobs)).toBe(true)
-    expect(aggregate.if).toBe("always() && github.event_name == 'pull_request'")
+    expect(aggregate.if).toBe("${{ !cancelled() && github.event_name == 'pull_request' }}")
     expect(aggregate.steps).toContainEqual(expect.objectContaining({
       if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')",
     }))
@@ -70,7 +107,7 @@ describe('master-only platform scheduling', () => {
     expect(runtime['continue-on-error']).toBeUndefined()
     const builder = workflow('build-exe-for-python-sdk.yml')
     expect(builder.concurrency?.['cancel-in-progress']).toBe(
-      "${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }}",
+      '${{ !inputs.release }}',
     )
     const build = builder.jobs.build!
     const preflight = build.steps!.find(step => step.name === 'Preflight installed-wheel real API test (POSIX)')!

+ 1 - 1
scripts/tests/ci-release-selfhosted.spec.ts

@@ -88,7 +88,7 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea
       expect(Object.keys(release.jobs)).toEqual(jobIds)
       expect(release.on).toEqual({ pull_request: null, push: { branches: ['master'] }, workflow_dispatch: null })
       expect(release.permissions).toEqual({ contents: 'read' })
-      expect(release.concurrency).toEqual({ group: '${{ github.workflow }}-${{ github.ref }}', 'cancel-in-progress': false })
+      expect(release.concurrency).toEqual({ group: '${{ github.workflow }}-${{ github.ref }}', 'cancel-in-progress': true })
     })
     for (const jobId of jobIds) {
       describe(jobId, () => {