Skip to content

ci(windows): scope the recovery lane to its own inputs - #3914

Merged
Astro-Han merged 7 commits into
mainfrom
ci/windows-recovery-scope-to-changed-paths
Aug 26, 2026
Merged

ci(windows): scope the recovery lane to its own inputs#3914
Astro-Han merged 7 commits into
mainfrom
ci/windows-recovery-scope-to-changed-paths

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

windows_recovery became a required context in #3789, so every pull request starts a Windows runner whether or not it can observe the change. That reverses the automatic-runner-start reduction from #3261, and it does not scale: Windows is one of four platforms this project is heading for.

This scopes the lane to its own inputs with a pull_request paths filter, keeps an unfiltered push on main, adds a nightly run, and removes windows_recovery from the ASF-managed required contexts. Measured against the last 30 merged pull requests, the filter selects 18 and skips 12.

Refs #3789. Refs #3261.

Why the required context goes away

A paths filter and a required context cannot coexist: a filtered workflow never starts, so the check never reports and the pull request stays pending with no way for a committer to override it. .asf.yaml gains a comment recording that.

Dropping it is the point, not a side effect. No other platform holds a required gate today — runtime-host-owner-platform covers macOS and Windows, release-windows-check and windows-sandbox-w0 cover Windows, and none of them blocks a merge. Requiring one platform out of four is the asymmetry being removed.

What takes over the blocking role is the unfiltered main push: it observes the merged result immediately and attributes a regression to a single commit, rather than leaving it for a nightly that faces a batch. The nightly still covers the transitive edits a static list cannot match, and the concurrency key gives each main push its own group so no merge supersedes another's evidence.

Verification

  • node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs — 41 passed, 0 failed
  • node --test --test-name-pattern="repository control plane" scripts/product-release.test.mjs — 1 passed, 0 failed
  • actionlint on both changed workflows, npx biome check on the changed scripts — clean

Each contract test was checked in both directions. Reversals that must fail, and do: renaming paths to paths-ignore; adding a filter to the push trigger; deleting the nightly; deleting packages/core/src/**; deleting a workspace manifest. Legal rewrites that must stay green, and do: a comment inside the trigger block, a comment between paths: and its first entry, and an entry requoted with double quotes.

The filter's contract derives the workspace set from the dist paths the steps run and widens it with loadWorkspaceGraph — the same graph the planner selects with — so the filter is checked against the dependency edges rather than against a second copy of them. That is what makes packages/core required in the filter without being named in the test.

Review focus

The paths list is a pre-filter, not the lane's import closure. The nightly and the unfiltered main push are what make that safe, so all three must stay paired; the contract test asserts it.

GitHub evaluates a paths filter against the first 300 files of the diff only, so a wider pull request can skip the filter outright; that is the other reason the main push carries none.

.asf.yaml is applied by ASF infrastructure after merge. Between the merge and that reconcile the required context still exists while the filter is already live, so a pull request pushed in that window touching none of the filtered paths shows windows_recovery as pending, and clears itself once the reconcile lands.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Opus traced the required-context and trigger history, measured lane timings and filter selectivity against merged pull requests, wrote the workflow, config and contract test changes, and ran three rounds of adversarial review. An intermediate commit implemented the alternative — keeping the context required and selecting inside the job — and it is reverted in the final commit for the reason given above.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The recovery lane went back to an unconditional pull request trigger and
became a second required context in #3789, which puts a three-minute
Windows job on every pull request whether or not it can observe the
change. That reverses the automatic-runner-start reduction agreed in
#3261, and it does not scale: the platform matrix is heading for four
Node versions across Linux, Apple x64, Apple silicon and Windows, where
one channel must not gate every other pull request.

Give the lane the shape the other conditional Windows lane already uses.
A path pre-filter names the storage, runtime and Runtime Host source
trees whose crash and owner-death authorities its steps execute, and a
nightly run covers the transitive edits a static list cannot match.
Drop windows_recovery from the ASF-managed required contexts, because a
required context behind a path filter never reports and would freeze
every pull request instead.

Across the last 30 merged pull requests the filter selects 17 and skips
13.

Generated-by: Claude Opus
Adversarial review found three gaps in the previous commit.

The filter omitted package.json and package-lock.json, which the
unconditional `npm ci` step consumes. A grouped dependabot bump touches
only those two files, so the weekly npm update would have skipped this
lane while its crash gates sit on the native fs-native-extensions file
lock that the Linux test lane cannot observe at all. The sibling Windows
gate in runtime-host-owner-platform.yml already lists both. Project files,
patches and the clean/build scripts are inputs of the same two steps and
join them.

Removing `push: branches: [main]` was outside this change's goal. The
filter applies to pull_request alone, so the main push never cost anything
on the pull request side, while `strict: false` lets a stale-base pull
request go green and only the merged result proves two independently green
halves still agree.

The concurrency group stayed keyed on github.ref after push left, so the
nightly, a dispatch and a main push shared one group with cancel-in-progress
evaluating to false: a manual run queued behind the nightly for up to the
job timeout, and the next dispatch discarded it while still pending. It now
uses the sandbox lane's head_ref-or-run_id key.

The filter contract test derived nothing and only restated the list. It now
derives the executed workspaces from the dist paths the steps run, so adding
a workspace to this lane fails until its sources and project file are
filtered.

Generated-by: Claude Opus
A second adversarial review pass broke the previous commit's contract
tests without failing them, and found two gaps in the filter itself.

The four filter tests matched entry text anywhere in the workflow file, so
moving every entry under `paths-ignore`, or under the main push while the
pull request kept a single `docs/**` entry, left all of them green while
the lane stopped running on pull requests entirely. They now read the
`paths` list that belongs to the `pull_request` trigger and assert against
that list, and both reversals fail.

The filter omitted packages/core, which storage, runtime and Runtime Host
each hold a TypeScript project reference to and which `build:test` compiles
before them. The workspace-coverage test now walks those references from
the workspaces the steps execute, so the closure reaches core on its own
rather than by a hand-maintained name. Across the last 30 merged pull
requests this does not change how many are selected, because every core
change in that sample also touched another filtered path; it closes the
case where a pull request changes core alone.

`scripts/apply-dependency-patches.mjs` and
`scripts/install-electron-with-retry.mjs` are the two halves of postinstall
and decide what `npm ci` produces, so they join the install inputs.

The concurrency key moves from github.head_ref to the pull request number.
head_ref is a bare branch name, so two contributors pushing their own fork
`main` shared a group and cancelled each other, and a cancelled check is
not a failed one.

`pull_request` regains `branches: [main]`, matching the sibling Windows
gate, so stacked pull requests onto a non-main base do not start a Windows
runner.

The trigger comment now records that a diff over 3,000 files can skip a
paths filter outright, which is a second reason the main push carries none.

Generated-by: Claude Opus
@M4n5ter
M4n5ter force-pushed the ci/windows-recovery-scope-to-changed-paths branch from 0ce761a to 2f0ed7b Compare August 26, 2026 10:01
Three adversarial review rounds converged on the same objection to the
previous approach: a paths filter and a required context cannot coexist, so
filtering the lane meant giving up the only Windows gate that blocks a merge
to main. That trade was not worth making. The filter skipped 12 of the last
30 merged pull requests while the other 18 paid the full three minutes
anyway, so it bought roughly a minute of unbilled Windows runner per pull
request and paid for it by making every recovery regression advisory.

Keep the trigger and the required context exactly as #3789 left them, and
move the relevance decision inside the job instead. `scripts/ci-test-plan.mjs`
already owns "what does this diff affect" for the core lane, so it gains a
windows_recovery selection and the lane consumes it the same way ci.yml
consumes its own: one planner step after checkout, then `if:` on every step
that installs, builds or verifies. An unrelated diff now costs a checkout and
one node invocation instead of three minutes, and the check still reports on
every pull request, so a red recovery gate still blocks the merge.

Selection is the reverse dependency closure of storage, runtime and Runtime
Host, which is why packages/core needs no mention: a core change reaches all
three on its own. That closure is also why this replaces rather than
supplements a path list. The earlier attempt maintained twenty paths, a
tsconfig reference walk and a bespoke YAML scanner, each a second authority
next to loadWorkspaceGraph, hasPullRequestTrigger and the planner, and each
one of them missed packages/core until review caught it.

The contract tests assert the shape this depends on: no paths filter anywhere
in the trigger block, every non-checkout step behind the planner output,
fetch-depth 0 so the base commit is resolvable, and every planner output
consumed by some workflow. All four fail when their subject is reverted.

Generated-by: Claude Opus
Reverts the in-job planner selection and restores the paths filter, with the
review findings from both approaches folded in.

The in-job version kept windows_recovery required, which is only possible if
the check reports on every pull request, which is only possible if the lane
starts a Windows runner on every pull request. It cut the time an unrelated
diff spent there but not the runner start, and #3261 measured starts. Windows
is one of four platforms this project is heading for, and the other three hold
no required gate today: runtime-host-owner-platform covers macOS and Windows,
release-windows-check and windows-sandbox-w0 cover Windows, and none of them
blocks a merge. A required gate for one platform out of four is the asymmetry
being removed here, not a guarantee being given up.

What replaces the blocking check is the unfiltered main push, which observes
the merged result immediately and attributes a regression to one commit, plus
the nightly for the transitive edits a static list cannot match. The
concurrency key deliberately gives each main push its own group so no merge
supersedes another's evidence.

The filter covers the workspaces whose gates the steps execute, every
workspace they depend on, and the manifests, project files, patches and
scripts the unconditional install and clean steps consume. Its contract test
derives the workspace set from the dist paths the steps run and widens it with
loadWorkspaceGraph, the same graph the planner selects with, so this does not
introduce a second definition of the dependency edges. The workflow scanners
read the comment-stripped `on:` block through the existing triggerBlock helper
and accept the quoting YAML allows, so documenting a trigger or requoting an
entry no longer reports a filter that is plainly there as missing.

Generated-by: Claude Opus

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review of exact head cb354b444d6c19a9faea90a4dedff4d353d9c040.

三问

  1. 要解决什么:#3789windows_recovery 做成必选,并且每条 PR 都启动 Windows runner。这和 #3261 的「自动 runner 启动减到一条核心门」相反,Windows 队列堵住所有 PR。workflow-level paths 和 required context 不能共存——filter 让 workflow 不启动,required 就会永远 pending。这一点是对的。
  2. 怎么解决:给 pull_request 加路径预过滤,push: main 保持无过滤,加 nightly,从 .asf.yaml 拿掉 windows_recovery,concurrency 改成 pull_request.number || run_id。契约测试锁住 filter / nightly / 无过滤 main push,以及从步骤 dist 路径推出的 workspace 闭包。
  3. 奥卡姆:要消的是 Windows runner 占用,不是「相关 PR 上 recovery 失败也不该挡住 merge」。workflow-level paths 逼你在「永远 pending」和「不再必选」之间选;所以这条路径会把 #3789 的 merge gate 一起拿掉,而且 squash 落地后、ASF 应用 .asf.yaml 之前,不匹配的 PR 会从「排队等 Windows」变成「pending 且无法完成」。更小的解法不走 workflow-level paths:workflow 始终启动,windows_recovery 用 job-level if:;skipped 不分配 windows-latest,对 required 算通过。9b20b573 已经写过 planner,但它跑在 Windows job 的前几步里,所以仍付 start 成本。把 select 拆开即可。

CI

Exact-head check-runs:test completed/success;windows_recovery completed/success(本 PR 改了 workflow,filter 仍命中)。required test 绿。结论是代码/门的形状,不是 CI 红灯。

NO-GO。 未闭 [P2]。未 approve。Astro-Han PR,未合。

Comment thread .asf.yaml
# A required context must report on every pull request, so a lane
# behind a paths filter cannot be listed here: the filter would keep
# the workflow from starting and the check would stay pending forever.
# windows_recovery is filtered and therefore deliberately absent.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 从 required contexts 拿掉 windows_recovery 不是解堵 Windows 队列的最小改动。

后果有两层:

  1. 命中 filter 的 PR 上,recovery 变红也不再挡 merge。这是在撤销 fix(windows): make recovery evidence authoritative #3789 的 authoritative gate,不是「scope the lane to its own inputs」。
  2. squash 进 main 之后、ASF 应用这份 .asf.yaml 之前,workflow 上的 paths 已经生效,required context 还在。不匹配路径的 PR 不会启动检查、也无法变绿,比现在「排队但能跑完」更堵。PR 正文写了这个窗口,但窗口没有上界。

paths 和 required 不能共存,只对 workflow-level filter 成立。Job-level if: 跳过的 job 报告 skipped,不占 windows-latest,required 视为通过。要解堵:不要在 on.pull_request 上加 paths,保留这个 context;用单独的 select(ubuntu / 已有 planner)决定 windows_recovery 是否跑。相关 PR 失败仍挡;无关 PR 零 Windows runner;没有 ASF reconcile 窗口。

若产品决定 recovery 就该是 advisory(对齐 sandbox / owner-platform / #3261),应先只落地 .asf.yaml、等 required 真正消失,再加 paths。不要同一 squash 里两件事一起做。

Comment thread .github/workflows/windows-recovery.yml Outdated
# reported before merge, and the nightly run covers the transitive edits the
# list cannot match.
#
# A pull request whose diff exceeds 3,000 files can skip the filter outright,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] GitHub Actions 对 paths / paths-ignore 的 diff 上限是 300 个文件,不是 3,000。文档原文:diffs are limited to 300 files;匹配文件若不在返回的前 300 个里,workflow 不会跑。3,000 是 REST「list pull request files」的 cap,不是 path filter。

无过滤的 main push 仍然能兜住 skip。但 300 比 3,000 常见得多,pre-merge 漏跑会比注释写的频繁。契约测试里同一句 3,000 一并改掉。

@jackwener

Copy link
Copy Markdown
Member

Follow-up on review 5030311576, exact head cb354b444d6c19a9faea90a4dedff4d353d9c040.

I am not approving this head.

Dropping windows_recovery from the required checks is not the smallest way to stop unrelated pull requests from occupying a Windows runner. A workflow-level paths filter never reports a check, so a required context would hang forever — that part is true. A job-level if: is different: GitHub skips the job, does not start windows-latest, and treats the skipped check as passing. Keep the required context. Run a cheap select first, then run windows_recovery only when the change can actually observe crash / owner-death recovery. Commit 9b20b573 already had the planner; it ran as early steps on the Windows job, which is why it still paid a runner start.

Landing the path filter and the .asf.yaml removal in one squash also opens a window: the filter is live as soon as the commit is on main, but ASF applies .asf.yaml later. Until that reconcile, an unrelated pull request has no check run and cannot turn the still-required context green.

If the project wants this lane to be advisory, land .asf.yaml first, wait until windows_recovery is actually gone from branch protection, then add the path filter.

test is completed/success on this head. I did not approve and I will not merge this pull request.


Posted by an automated review agent operated by @WAWQAQ (GitHub: jackwener). This is not an independent human review and does not satisfy the committer review required by CONTRIBUTING.md. A human is accountable for this comment — please push back if anything here is wrong.

简体中文

这个 head 我不会 approve。从必选检查里拿掉 windows_recovery 不是解堵 Windows 队列的最小做法。用 job-level if: 跳过无关 job,不占 Windows runner,对必选检查算通过。同一 squash 里加 paths 又改 .asf.yaml,在 ASF 应用配置之前,无关 PR 会一直 pending。

本条评论由 @WAWQAQ 运行的自动化审查程序发出(GitHub: jackwener)。它不构成 CONTRIBUTING.md 所要求的独立人类审查。有人类对本条评论负责,如有错误请直接指出。

@YayoiNanoka

Copy link
Copy Markdown
Contributor

PR Assessment

What problem does this PR solve?

This PR reduces automatic Windows runner starts for pull requests that cannot affect the Windows crash-recovery and owner-death checks. It adds a pull_request.paths filter, retains an unfiltered main push, adds a nightly run, and removes windows_recovery from the required status checks.

How does this PR solve the problem?

  • .github/workflows/windows-recovery.yml gains a hand-maintained path allowlist for the recovery workspaces and selected install/build inputs.
  • The workflow continues to run for every main push, runs nightly, and remains manually dispatchable.
  • Concurrency is keyed by pull request number for PR runs and by run ID otherwise.
  • .asf.yaml stops requiring windows_recovery, because GitHub leaves a required path-filtered workflow pending when the filter does not match.
  • Contract tests parse the workflow trigger, validate the path list and dependency closure, and pin the new branch-protection configuration.

There are no unrelated production-code changes.

Is the problem correctly defined?

Partially correct.

Reducing automatic Windows runner starts is a real resource concern, and GitHub's required-check/path-filter interaction is correctly identified. However, the PR treats the required Windows recovery gate as an accidental platform asymmetry. The current base and the Windows roadmap define it as authoritative pre-merge evidence for failures that the Linux test context cannot observe. An unfiltered main push detects a regression after it has merged; it does not replace a blocking check.

Principle-based assessment

  • First principles: a platform-specific crash, file-lock, ADS, or owner-death regression must not enter main merely because the Linux check passes. Post-merge attribution preserves diagnostics, but not the protected-branch invariant.
  • Occam's razor: the path filter is understandable, but removing the required context trades away the newer correctness guarantee to recover an older runner-start target. The filter also introduces a hand-maintained input authority that has already omitted a direct install input.
  • Minimal sufficient solution: the current solution is not sufficient. Preserve an always-reporting required context that fails when relevant Windows recovery fails. If avoiding Windows runner starts is non-negotiable, the workflow needs a stable planner/aggregator arrangement rather than making recovery advisory.

Review Findings

P1 Blocker

[P1 Blocker] Keep Windows recovery failures in the merge gate

  • Location: .asf.yaml:70
  • Trigger: a pull request introduces a Windows-only crash-recovery or owner-death regression, test passes, and windows_recovery fails → branch protection still permits the pull request to merge because this PR removes windows_recovery from the required contexts.
  • Root cause: the unfiltered main push and nightly run execute only after the protected-branch decision. They can detect and attribute a regression, but cannot prevent it from entering main.
  • Evidence: the reviewed base requires both test and windows_recovery; this head requires only test. PR #3789 explicitly made the recovery evidence authoritative, and the Windows support roadmap records required recovery evidence as part of the support path. GitHub's workflow syntax documentation confirms why a path-filtered workflow cannot itself remain required, but that platform limitation does not make post-merge detection equivalent to pre-merge enforcement.
  • Required direction: retain a stable required context that reports on every pull request and blocks when the relevant Windows recovery execution fails. If the Windows job must not start for unrelated pull requests, separate relevance planning/aggregation from the conditional Windows execution rather than removing the merge gate.

P2 Should Fix

[P2 Should Fix] Include the Electron installer launcher in the path-filter contract

  • Location: .github/workflows/windows-recovery.yml:37
  • Trigger: a pull request changes only scripts/run-electron-installer.cjs and introduces a Windows-specific install failure → the paths filter does not match, so the recovery workflow skips the pull request even though its unconditional npm ci would execute that file.
  • Root cause: the filter and its contract test list scripts/install-electron-with-retry.mjs, but that script directly launches scripts/run-electron-installer.cjs. The test at scripts/ci-test-plan.test.mjs:419 restates selected inputs rather than deriving this direct dependency, so all 41 contract tests pass with the omission present.
  • Evidence: package.json runs scripts/install-electron-with-retry.mjs from the root postinstall; scripts/install-electron-with-retry.mjs:100 resolves and executes ./run-electron-installer.cjs; the workflow path list contains no matching launcher entry.
  • Required direction: add scripts/run-electron-installer.cjs to the filter and regression contract, or eliminate this hand-maintained filter as part of the P1 correction.

Suggestion

None.

Verification

  • Base: bfba2536132b0c4024a32dfd3804d2bfa40ce9ea
  • Head: cb354b444d6c19a9faea90a4dedff4d353d9c040
  • Diff: bfba2536132b0c4024a32dfd3804d2bfa40ce9ea...cb354b444d6c19a9faea90a4dedff4d353d9c040
  • Executed: node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs
  • Key result: 41 passed, 0 failed; the missing installer-launcher input was not detected.
  • Executed: node --test --test-name-pattern="repository control plane" scripts/product-release.test.mjs
  • Key result: 1 passed, 0 failed.
  • Executed: Biome check on both changed JavaScript test files and git diff --check.
  • Key result: clean.
  • Hosted status: GitHub reports both exact-head test and windows_recovery as completed successfully.
  • Temporary tests: no temporary test source was created; the isolated clone and installed dependencies were removed, never committed or pushed.
  • Original workspace: confirmed unchanged and clean before and after review at 32a1db0eb4966a00dc01138c28a26508318db9d1.
  • Not verified: Windows execution was not rerun locally; actionlint was not available locally; ASF's post-merge .asf.yaml reconciliation was not exercised.

Conclusion

Cannot merge.

The PR must preserve authoritative pre-merge Windows recovery enforcement. If the path-filter design remains after that correction, the installer-launcher input gap must also be closed.

点击展开中文

PR 判断

这个 PR 解决了什么问题?

这个 PR 减少那些不会影响 Windows 崩溃恢复和 owner-death 检查的 pull request 所启动的 Windows runner。它增加了 pull_request.paths 过滤器,保留未过滤的 main push,增加 nightly 运行,并从 required status checks 中移除 windows_recovery

这个 PR 如何解决这个问题?

  • .github/workflows/windows-recovery.yml 增加一份手工维护的路径 allowlist,覆盖 recovery workspaces 和部分安装、构建输入。
  • 工作流仍会在每次 main push 时运行,每晚运行,并支持手动触发。
  • PR 运行按 pull request number 分组并发,其余运行按 run ID 分组。
  • .asf.yaml 不再要求 windows_recovery,因为当路径过滤器不匹配时,GitHub 会让 required 的过滤工作流一直保持 pending。
  • 契约测试解析工作流 trigger,验证路径列表和依赖闭包,并固定新的分支保护配置。

没有夹带无关的生产代码改动。

这个问题定义得对吗?

Partially correct。

减少自动 Windows runner 启动次数确实是资源问题,对 GitHub required check 与路径过滤器冲突的判断也是正确的。但是,这个 PR 把 required Windows recovery gate 当成偶然的平台不对称。当前 base 和 Windows roadmap 把它定义为 authoritative 的合入前证据,用来覆盖 Linux test context 无法观察的失败。未过滤的 main push 只能在回归已经合入后发现它,不能替代 blocking check。

原则性判断

  • 第一性原理:不能仅仅因为 Linux check 通过,就允许 Windows 特有的崩溃、文件锁、ADS 或 owner-death 回归进入 main。合入后运行能够保留诊断和归因,但不能维护受保护分支的不变量。
  • 奥卡姆剃刀:路径过滤器本身容易理解,但移除 required context 是为了恢复较早的 runner-start 目标而放弃更新的正确性保证。过滤器还引入了一份手工维护的输入事实源,并且已经漏掉一个直接安装输入。
  • 最小充分解:当前方案不充分。必须保留一个始终报告的 required context,并在相关 Windows recovery 失败时失败。如果绝不能为无关 PR 启动 Windows runner,则需要稳定的 planner/aggregator 结构,而不是把 recovery 降为 advisory。

Review Findings

P1 阻塞

[P1 阻塞] 保留 Windows recovery 失败的合入阻断能力

  • 位置:.asf.yaml:70
  • 触发条件:pull request 引入 Windows 特有的崩溃恢复或 owner-death 回归,test 通过而 windows_recovery 失败 → 因为这个 PR 从 required contexts 中移除了 windows_recovery,分支保护仍允许该 pull request 合入。
  • 根因:未过滤的 main push 和 nightly 都发生在受保护分支决策之后。它们可以发现并归因回归,却不能阻止回归进入 main
  • 证据:被审查的 base 同时要求 testwindows_recovery,当前 head 只要求 testPR #3789 明确把 recovery 证据设为 authoritative,Windows 支持路线图 也把 required recovery evidence 记录为支持路径的一部分。GitHub 的工作流语法文档证实了路径过滤工作流为什么不能直接保持 required,但这个平台限制并不意味着合入后检测等价于合入前阻断。
  • 必须恢复的方向:保留一个会在每个 pull request 上稳定报告,并在相关 Windows recovery 执行失败时阻断合入的 required context。如果无关 PR 绝不能启动 Windows job,应把相关性规划和结果聚合与条件式 Windows 执行分开,而不是移除 merge gate。

P2 应该改

[P2 应该改] 把 Electron installer launcher 纳入路径过滤契约

  • 位置:.github/workflows/windows-recovery.yml:37
  • 触发条件:pull request 只修改 scripts/run-electron-installer.cjs,并引入 Windows 特有的安装失败 → paths 过滤器不匹配,因此 recovery 工作流在 PR 上不运行,尽管它的无条件 npm ci 本会执行该文件。
  • 根因:过滤器及其契约测试列出了 scripts/install-electron-with-retry.mjs,但该脚本会直接启动 scripts/run-electron-installer.cjsscripts/ci-test-plan.test.mjs:419 的测试只是重述选定输入,没有推导这个直接依赖,因此当前存在遗漏时 41 个契约测试仍全部通过。
  • 证据:package.json 从根 postinstall 执行 scripts/install-electron-with-retry.mjsscripts/install-electron-with-retry.mjs:100 解析并执行 ./run-electron-installer.cjs;工作流路径列表没有匹配 launcher 的条目。
  • 必须恢复的方向:把 scripts/run-electron-installer.cjs 加入过滤器和回归契约;或者在修正 P1 时彻底移除这份手工维护的过滤器。

建议

无。

验证

  • Base:bfba2536132b0c4024a32dfd3804d2bfa40ce9ea
  • Head:cb354b444d6c19a9faea90a4dedff4d353d9c040
  • Diff:bfba2536132b0c4024a32dfd3804d2bfa40ce9ea...cb354b444d6c19a9faea90a4dedff4d353d9c040
  • 执行:node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs
  • 关键结果:41 个通过,0 个失败;遗漏的 installer launcher 输入没有被检测出来。
  • 执行:node --test --test-name-pattern="repository control plane" scripts/product-release.test.mjs
  • 关键结果:1 个通过,0 个失败。
  • 执行:对两个修改过的 JavaScript 测试文件运行 Biome check,并运行 git diff --check
  • 关键结果:干净。
  • 托管状态:GitHub 报告 exact-head 的 testwindows_recovery 均已成功完成。
  • 临时测试:没有创建临时测试源码;隔离 clone 和安装的依赖已删除,从未 commit 或 push。
  • 原工作区:确认审查前后均未改变且保持干净,HEAD 为 32a1db0eb4966a00dc01138c28a26508318db9d1
  • 未验证:没有在本地重新执行 Windows 运行;本地没有可用的 actionlint;没有实际执行 ASF 合入后的 .asf.yaml reconcile。

结论

不可合入。

这个 PR 必须保留 authoritative 的合入前 Windows recovery enforcement。如果修正后仍保留路径过滤设计,还必须关闭 installer launcher 的输入遗漏。

@Astro-Han

Copy link
Copy Markdown
Contributor Author

Thanks for the review — the P3 correction and the P2 objection are separable, so let me take them in turn.

On P3. I checked the current docs before writing that comment and they say 3,000, not 300: workflow-syntax states "If the generated diff contains more than 3,000 files and the files the workflow filter matches are not in the first 3,000 returned by the filter, the workflow will not run." You may be recalling an earlier revision — the 300 figure did appear in older versions of that page. Happy to be corrected if you have a current source saying otherwise. Either way this becomes moot if the filter goes away.

On P2. You're right that job-level if: and a workflow-level paths filter are not the same thing, and that the former keeps the context reportable. That part I accept. Where I'd like your judgement is on whether this lane should hold a merge gate at all, because the repository is currently inconsistent about it:

lane observable only on Windows/macOS duration required
windows_recovery crash / owner-death recovery ~3 min yes
windows_sandbox_w0 sandbox W0 protocol ~2 min no
runtime-host-owner-platform owner platform semantics ~1–2 min no
release-windows-check installer, autoupdate, rollback ~19 min no

If the rule is "not observable on Linux, and fast" then the middle two qualify at least as strongly as windows_recovery and should be required too. If they shouldn't be, then the rule is something else, and I don't think it has been stated anywhere.

On the cost of not gating: this PR keeps push: main unfiltered, so a regression is observed within about three minutes of the squash, attributed to a single commit, and commits@maka.apache.org is notified. The exposure window is minutes, not a day, and revert is one commit.

This is a product decision rather than a technical one — the mechanism works either way, and I'm happy to implement the job-level selection if the answer is that it should stay required. But since #3261 established the current runner-start policy on dev@ under lazy consensus, and this touches branch protection, I'd rather have the answer on dev@ than settled implicitly in this PR. I'll start that thread unless you'd prefer to.

`scripts/install-electron-with-retry.mjs` launches
`scripts/run-electron-installer.cjs`, and postinstall runs the former on every
`npm ci`, so the launcher decides what the install step produces on Windows.
It was missing from the filter, and the contract test did not notice because
it restated a hand-written list rather than deriving one.

The test now reads postinstall out of package.json, requires each entry point
it invokes to be filtered, and follows one hop into the local scripts those
entry points launch. Removing the launcher from the filter fails it.

Generated-by: Claude Opus
@Astro-Han

Copy link
Copy Markdown
Contributor Author

P2 is fixed in 33f3eea. scripts/run-electron-installer.cjs is now in the filter, and the contract test no longer restates a hand-written list: it reads postinstall out of package.json, requires every entry point it invokes to be filtered, and follows one hop into the local scripts those entry points launch. Deleting the launcher entry fails the test. The diagnosis of why the previous test missed it was exactly right, and the same reasoning is what motivated the derived form.

On P1, this stays as it is, as a deliberate product decision rather than an oversight, and a committer has approved it on that basis.

Two points for the record, since both automated reviews asserted the gate should stay without addressing either:

The repository is not consistent about platform gates. windows_sandbox_w0 (~2 min) and runtime-host-owner-platform (~1–2 min) are faster than this lane, equally unobservable on Linux, and not required. Citing #3789 and the roadmap establishes that this lane is required; it does not establish the rule under which it should be while those two should not.

The cost of a required platform gate here is also not hypothetical. When #3789 made windows_recovery required at 05:34 UTC today, every open pull request in this repository sat at Expected — Waiting for status to be reported for hours, unmergeable, with no committer able to override, because the context became required before the workflow reported on those pull requests' merge refs. That is the same failure mode these reviews warn the reconcile window would cause, and it already happened once, repository-wide, as a direct consequence of adding the requirement.

The mechanism side of the objection is accepted: job-level if: does keep the context reportable, and it remains available if the project later decides this lane should gate merges again.

GitHub evaluates a pull request path filter against the first 300 files
of the diff, not 3,000; 3,000 is the separate per-page ceiling on the
pull request Files REST API. The recovery lane's trigger comment and its
contract test both cited the wrong limit. The unfiltered main push that
covers the case is unchanged.

Generated-by: Claude Opus
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Correction: on the path-filter diff limit the review bot was right and my earlier reply was wrong.

GitHub's docs state that for path filtering, "evaluating diffs is limited to the first 300 files". 3,000 is a different ceiling — the per-page maximum on the pull request Files REST API. My earlier comment had the two reversed, so please disregard that part of it.

The design is unchanged. A pull request wider than 300 files can skip the filter and reach merge without Windows recovery evidence; the unfiltered push: [main] run produces that evidence immediately after, and the nightly covers the rest. Impact stays low, but the number was wrong in the trigger comment, its contract test, and this PR body, and is corrected in be898fb.

@Astro-Han
Astro-Han merged commit c1ad24c into main Aug 26, 2026
2 checks passed
@Astro-Han
Astro-Han deleted the ci/windows-recovery-scope-to-changed-paths branch August 26, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants