Skip to content

ci: allocate a runner only when its inputs changed - #4461

Merged
Astro-Han merged 13 commits into
apache:mainfrom
Astro-Han:ci/4451-reduce-runner-occupancy
Sep 1, 2026
Merged

ci: allocate a runner only when its inputs changed#4461
Astro-Han merged 13 commits into
apache:mainfrom
Astro-Han:ci/4451-reduce-runner-occupancy

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

A runner slot is scarcer than a runner minute on ASF's shared infrastructure, and this repository was spending slots on work that could not change a verdict. Seven commits, one theme: every allocation must be justified by its own inputs.

  • Merge planning and validation into the required test job. ci: skip heavy validation for documentation-only pull requests #4172 split one verdict across planheavytest, to keep documentation-only pull requests off heavy CI (ci: skip the expensive CI job for documentation-only pull requests #3998). That goal is right and this keeps it; the three-job shape is what costs, because a pull request then queues for a runner 2.875 times to answer one question. Planning is now the first step of a single unconditional job and every later step gates on steps.plan.outputs.*. The job keeps the name test because that is the required context in .asf.yaml — renaming it would leave the check unreported on every open pull request until the rename merged, and nothing could merge while it was unreported.
  • Gate app icon drift on the artwork it verifies. The two drift tests regenerate the icon set, about a minute, and ran on any code change: code was true for 193 of the last 200 commits, while the surface those tests read changed in 41.
  • Fold redundant CLI package validation jobs. release-predecessor did one metadata fetch on its own runner; the three state-root-qualification matrix entries each re-downloaded the same two tarballs; two Linux x64 smoke entries differed only by Node version. 14 → 10 runner slots per pull request, same assertions.
  • Scope the packaged Windows gate to its own inputs. runtime-host-boot.ts was in the 25-minute Windows lane's path filter for a handful of lines of update-feed wiring, and was the sole reason that lane ran 13 of 200 commits; those lines now have an install-free contract test. 55 → 41 of 200.
  • Leave the recovery suites' portable half to test, keep their Windows-only half here. windows-recovery filtered on every source directory in the closure of the crash and owner-death suites, taking a Windows runner on 118 of 200 merges, and across 300 runs never produced a pull-request red that test had not already produced on the same commit. What it does own is the half guarded by process.platform === 'win32', which Linux skips by construction; those seventeen files are named individually. 118 → 17 of 200.

Nothing that can observe a defect was removed. The changes are to when a runner is allocated and how many answer the same question.

Refs #4451. The issue projected cli-package-validation at 14 → 9; the arithmetic is 14 → 10. Its windows-recovery proposal — generate the filter from the suites' import closure — was built and measured at 106 of 200 (the closure is 709 files, because these tests boot a real Runtime Host), so the trigger, not the list, is what this changes instead.

Measured effect

ci.yml, against two three-job runs from the same congested window today:

run allocations queue exec
refactor/goals-controller-scope 3 57m32s 18m52s
feat/desktop-api-key-host-onboarding 3 68m10s 18m02s
this branch 1 10m45s 23m27s

The first of those breaks down as plan queued 12m to execute 18s, heavy queued 19m to execute 18m32s, and test queued 25m to execute 2s. Execution is not comparable across different diffs, but #4400 ran the pre-#4172 single job at 24m27s, so one job at 23m is the normal shape rather than an inflation; merging also drops a duplicate checkout and set-up.

cli-package-validation on this branch opened 10 jobs against 14 on a control branch (run 33498682856), as designed.

Filter rates over the last 200 first-parent commits: windows-recovery 118 → 17, release-windows-check 55 → 41, app icon regeneration 193 → 41.

Review focus

Two things would be silent if wrong.

ci.yml: a leftover needs.plan.outputs.* resolves to the empty string rather than failing, which disables a step instead of erroring — the rebase onto #4427 produced exactly that, in three new state_root_compat gates. scripts/ci-test-plan.test.mjs asserts the workflow contains no such reference, that jobs is exactly ['test'], that the job has no needs: and no job-level if:, and that - id: plan precedes the first use of its outputs.

windows-recovery: re-adding a workspace source directory would quietly put the lane back on most merges, and a hand-listed Windows surface would rot. Two contract tests: the executed workspaces are derived from the dist paths the steps run and closed over the dependency graph, and none of that closure may appear as a directory glob; and every workspace path on the filter must name one file and contain a Windows branch.

Verification

  • node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs scripts/update-test-feed-wiring.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/asf-source-workflow-policy.test.mjs scripts/windows-package-source-closure.test.mjs — 81/81 pass; each new assertion was confirmed to fail against the pre-change workflow.
  • npm run format:check, npm run lint, npm run check:asf-headers — clean.
  • The merged state-root-qualification shell was checked with bash -n; every changed workflow was parsed as YAML to confirm the job and step counts above.
  • Rebased onto 0d3265eaf4; the state_root_compat gates fix(runtime-host): separate the persisted grant record from the authority it derives #4427 added were carried into the merged job.
  • Not run locally: the workflows themselves. ci.yml's merged shape and the Windows lanes are observable only on this pull request's own checks — test was green at 23m27s on the previous head.
  • Trigger rates and sole-driver counts come from replaying each workflow's pull_request.paths against the last 200 first-parent commits. Failure attribution comes from the last 300 runs of release-windows-check and of windows-recovery, cross-referenced against the CI run on the same head SHA.

AI use

Select exactly one:

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

Tool(s) and scope: Claude Code — audited the workflows, measured the trigger rates and failure history, and authored these changes and their tests. Commits carry Generated-by: Claude Code. The maintainer reviewed the diff and owns the merge decision.

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

@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Sep 1, 2026
@Astro-Han
Astro-Han marked this pull request as ready for review September 1, 2026 10:45

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holding approval at head da3b71d8d2b784d0ff0d1ffdd7db392092dedd04: the code review is clean, but the PR is a draft and CI is not yet green at this head (test queued, package/addon builds running). No P0/P1 found in the change.

ci: allocate a runner only when its inputs changed — 5-commit workflow/CI-planner refactor to cut runner occupancy. Verified sound:

  • The merge gate cannot be silently skipped. The required job stays named test (the .asf.yaml required context, deliberately not renamed), and the install/heavy step is gated on a single new heavy output = requiresHeavyValidation(plan), an OR over every test surface (asfSource || astryxSurface || cliPackage || code || e2e || releaseContract || runtimeHost || runtimeSandbox || storybook || standardWorkspaces). code is set for any workspace/scripts/skills/typecheck file, and any unknown path forces a full plan — so any real code change yields heavy=true and the full suite runs in the required lane. A PR can no longer be gated green while skipping its suite.
  • changedFilesBetween is now merge-base correct (merge-base + --diff-filter=ACMRDT incl. type changes), which removes the "PR checks may predate later main" gap the old planner leaned on.
  • Windows recovery PR-lane is narrowed with evidence: over ~300 runs the recovery suites' PR lane never red a commit that the Linux test lane had not already red; its one unique catch (Windows NTFS alternate-stream) came from the unfiltered main-push trigger. Removing the source-dir path filters makes it run only on Windows-specific inputs (npm ci, patches, installer, Local IPC trust PS1) plus the unfiltered after-merge push. Deliberate, documented trade.
  • release-windows baseline steps move to schedule — correct, since qualifying a transition out of a pinned historical release cannot be changed by a PR diff; keeps runtime-host-boot.ts feed wiring asserted per-change via update-test-feed-wiring.test.mjs in the required lane instead of a 25-min Windows lane.
  • cli-package-validation folds release-predecessor into build — the predecessor lookup rides the runner already waiting on addon builds; outputs re-exposed from build.

Holding only because of the two gate facts (not code): draft PR + CI not yet green at the exact head. Bring it out of draft and get test/package green here, and I'll approve — the change reads correct and the runner-occupancy win is real.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two P1 blockers remain on this exact head, so I did not approve it. The other workflow consolidation paths passed local build, full typecheck, lint/format/ASF checks, release-contract checks, 120 focused workflow/harness tests, and the same focused validation on a clean synthetic merge with current main.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

Comment thread .github/workflows/release-windows-check.yml Outdated
Comment thread .github/workflows/windows-recovery.yml Outdated

@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.

I reviewed this at da3b71d8d2b784d0ff0d1ffdd7db392092dedd04. No P0, P1, P2, or P3.

The required check is still named test, which is what .asf.yaml requires. Planning is the first step of that one job, and later steps read steps.plan.outputs.*. There is no leftover needs.plan.outputs — GitHub would treat that as an empty string and skip the step instead of failing. heavy is the OR of every installed-toolchain surface, so a documentation-only change still reports test green after the install-free steps, and a real code change still installs and runs.

The Windows recovery PR filter no longer names the crash and owner-death sources. That lane was never a required context, and the unfiltered main push still runs those suites. The packaged Windows PR path still packages, verifies, and runs autoupdate; the pinned-baseline upgrade and rollback now run on the schedule, where a pull request cannot change their historical input. The boot-to-feed wiring is asserted on every change in the required job. CLI validation still runs the same three State Root transitions and both Node smokes; they share runners instead of repeating downloads.

Hosted test and package were still pending on this head when I posted. This changes when runners are allocated, not what a required lane can observe. I am not merging it.

简体中文

我审的是 da3b71d8d2b784d0ff0d1ffdd7db392092dedd04。没有 P0/P1/P2/P3。

必选检查还叫 test,和 .asf.yaml 一致。规划是这个 job 的第一步,后面都读 steps.plan.outputs,没有残留的 needs.plan(GitHub 会把它当成空字符串,步骤会静默跳过)。heavy 是所有需要装工具链的面的或。文档-only 仍会报 test 绿;真正的代码改动仍会安装并跑。

Windows recovery 的 PR 过滤不再点名崩溃套件本身;它本来也不是必选检查,合入后的 main push 仍会跑。Windows 打包在 PR 上仍做打包、校验和自动更新;钉死的基线升级改到定时跑。CLI 的三次 State Root 和两个 Node smoke 都还在,只是共用 runner。

发这条时 hosted test / package 还在排队。我不合入。


Automated review notice: This comment was posted by an automated review agent operated by WAWQAQ. It is not an independent human review and does not replace one.

Splitting path planning from validation made every pull request queue for a
scarce runner three times to reach one verdict. `plan` executed for 19
seconds and the `test` aggregation job for four, yet each allocation waited
on its own: across 32 runs with complete timing the three queues averaged
5m57s, 4m46s and 7m57s. The distribution is bimodal, about a minute per
stage while the pool is free and about twenty while it is starved, so the
two extra allocations cost roughly 12m44s of pure waiting per run.

Planning is now the first step of one job and every later step gates on
`steps.plan.outputs`, which is what those steps already did through the job
boundary. Nothing is validated that was not validated before, and a
documentation-only change costs one short allocation rather than the two it
previously paid for the planning and aggregation jobs.

The job keeps the name `test` because that is the required context in
`.asf.yaml`. Renaming it would leave that check unreported on every open
pull request until the rename merged, and nothing could merge while it was
unreported.

The longest validation observed is 25m37s with every lane selected, so a
single job stays well inside the 45-minute limit.

Generated-by: Claude Code
The two app-icon drift tests regenerate the shipped PNGs through Python and
take about 52 seconds, but they were selected by `code`, so every change
anywhere in the tree paid for them. Replayed over the last 200 commits on
main, `code` is true for 193 of them while the surface those tests actually
read is true for 41.

That surface is now its own planner selection, derived from what the tests
open: the committed artwork, the generator that must still reproduce it, the
`APP_ICONS` catalog they check it against, and the packaged-resource list
that has to keep naming every file. Coverage is unchanged — every input that
could make these tests fail still selects them.

Each of those inputs lives in a workspace or under `scripts/`, so an app-icon
selection still implies `code` and the build the tests need has already run.
A test asserts that implication rather than leaving it to be rediscovered.

Generated-by: Claude Code
This lane opened fourteen runners per pull request to produce evidence that
four of them were not needed to hold.

`release-predecessor` resolved one npm version in fifteen seconds and then
released its runner. It now runs on `build`, which already waits about
sixteen minutes on the addon builds, so the resolution costs nothing it was
not already waiting through. The exported `release_predecessor_*` names are
unchanged, so `npm-publication`, `release-cli-stage` and `asf-npm-candidate`
consume exactly what they consumed before.

`state-root-qualification` ran three matrix jobs. Two of them qualify a
transition between tarballs that were published and frozen, so nothing in a
pull request can change their outcome except the qualifier itself, and the
third reads the candidate this run built. They are three invocations of one
script against one sandbox, so they now share a runner. The scenario table
survives as three calls with the same digests, epoch relations and identity
flags; each writes its own report and all three are uploaded together.

The Linux x64 smoke ran twice on two runners for two Node versions. Same
machine, same tarball, same architecture assertion — it now installs the
second version and repeats the smoke in place.

Pull requests allocate ten runners instead of fourteen. Nothing that was
verified before is unverified now.

Generated-by: Claude Code
This lane packages, installs and updates Maka on Windows. It takes about
25 minutes on a runner class that is scarce on shared infrastructure, and
it was being allocated for two reasons that its own steps cannot justify.

`apps/desktop/src/main/runtime-host-boot.ts` was in the path filter
because the packaged updater is driven through `MAKA_UPDATE_TEST_FEED`,
which that file hands to the update service. The wiring is a handful of
lines; the module is 1900 of them, and over the last 200 commits on main
it was the sole reason this lane ran 13 times. It is now asserted by
`scripts/update-test-feed-wiring.test.mjs`, which runs on every change in
the required job before any toolchain is installed and costs
milliseconds. The filter matches 41 of those 200 commits, down from 55.

The pinned-baseline steps qualify a transition out of an already-released
installer, so a pull request's diff is not their input and cannot change
their outcome. Downloading that baseline is also this lane's most common
failure: 14 of 36 failures across the last 300 runs, every one of them on
a branch that could not have caused it, each costing another 25-minute
Windows allocation. Those three steps move to a nightly schedule, which
is still far ahead of release day, the moment they exist to precede.

Packaging, release verification and the end-to-end autoupdate check still
run on every matching pull request.

Generated-by: Claude Code
This lane's pull-request filter named every source directory in the
workspace closure of the crash and owner-death tests it runs, so it took
a Windows runner on 118 of the last 200 merges. The filter was accurate —
those suites really do import most of `runtime`, `runtime-host`,
`storage` and `core`, and a generated import closure measured at 106 of
the same 200, because they boot a real Runtime Host. The list was not the
problem.

What the run history shows is that the pull-request trigger was. Across
300 runs, 241 of them pull requests, this lane produced 12 reds and not
one of them was unique: every pull-request failure sat on a commit whose
`test` run had already failed, usually at the very same step — six at
`Install dependencies`, one at the workspace tests that run the same
owner-death suite on Linux. Its single unique catch in that window was a
Windows-only NTFS alternate stream regression found by the unfiltered
main push, which is the trigger that exists for exactly that.

So the filter now names only what a Windows runner can prove and `test`
cannot: how `npm ci` resolves and what the dependency patches and the
Electron installer produce there, what the clean step removes there, and
the Local IPC trust boundary, a PowerShell script with no other caller.
That is 14 of 200 merges instead of 118. The install and build steps stay
unconditional, because proving those suites still build and run on
Windows is what the lane is for once it does run.

The recovery authorities are now covered after merge rather than before
it, by the unfiltered main push minutes later and by the nightly.
`windows_recovery` is deliberately not a required context in `.asf.yaml`,
so it was never what stood between a regression and `main` in any case.

Generated-by: Claude Code
…equests

Both steps were moved to the schedule on the grounds that they qualify a
transition out of a pinned historical release, so a pull request could not
change their outcome. That is only true of the baseline. The other input
to `verify:windows-installer` is the installer this run just built, which
it installs over the pinned one, and `verify:windows-installer-rollback`
does not read the baseline at all: it takes the candidate installer and
the version-bumped one, and it is the only place the `installer.nsh`
Abort path is exercised. `installer.nsh` and both verifiers stayed in the
path filter the whole time, so a change to them scheduled this lane and
then skipped the steps that read them.

Both run on pull requests again. The flake that motivated the move —
14 of 36 failures across 300 runs, always on branches that could not have
caused it — is handled where it belongs: the baseline is pinned by
version, tag, asset name and SHA-256 in a committed manifest, so it is
immutable and cacheable on that manifest's hash. A hit skips the network
and a corrupt entry still fails, because the download step verifies the
checksum either way.

The schedule stays. Its reason is now the one the other two Windows lanes
give for theirs: the path list is a pre-filter, not this lane's import
closure, so a transitive edit it cannot match would otherwise first be
observed on release day.

Generated-by: Claude Code
Dropping the workspace source directories dropped something with them.
Part of these suites is portable TypeScript that Linux fails first, which
is why removing them cost no observed pull-request signal — but part is
guarded by `process.platform === 'win32'` and is skipped off Windows by
construction, so `test` cannot go red on it however carefully it runs.
`assertNoWindowsAlternateStreams` and its three NTFS alternate stream
regressions are exactly that, and they are the one thing this lane has
caught that no other lane could have.

The Windows-branching modules under the executed suites are named again,
individually: seventeen files, which the last 200 merges touch twice more
than the filter already matched. That buys back the whole Windows-only
half for the price of two runs, where readmitting the closure would cost
118 of 200.

The rule is now checkable rather than remembered, so a stale entry cannot
sit here: every workspace path on this filter must be one file, not a
glob, and must contain a Windows branch. A module that stops forking on
the platform leaves the list, and a portable file cannot enter it.

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the ci/4451-reduce-runner-occupancy branch from da3b71d to 0d4301c Compare September 1, 2026 11:23
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Rebased onto 0d3265eaf4, both P1s addressed, and the effect is now measured on this pull request's own runs rather than projected.

Both P1s

a6c3e1e843 — accepted in full. The rollback step never read the pinned baseline at all (verify-windows-installer-rollback.mjs:609 takes the candidate installer and the version-bumped one), and the upgrade step reads the candidate as well as the baseline, so a pull request does decide both outcomes. Both run on pull requests again; the download flake is handled by caching the baseline, which is immutable — pinned by version, tag, asset name and SHA-256 in a committed manifest — with the checksum still verified on every run.

0d4301c6e0 — principle accepted, remedy narrowed. The Windows-only half of these suites is skipped off Windows by construction, so test cannot cover it; the seventeen Windows-branching files under the executed suites are named again, individually, and a new contract test requires every workspace path on this filter to name one file and contain a Windows branch. That is 15 → 17 of the last 200 merges, against 118 for the source directories. Reasoning for not readmitting the directories is in the thread.

What the rebase caught

#4427 added three state_root_compat gates to the heavy job. They merged cleanly into the single job still spelled needs.plan.outputs.*, which GitHub resolves to the empty string — the steps would have been silently skipped rather than erroring. scripts/ci-test-plan.test.mjs fails on exactly that, which is how they were found.

Measured, not projected

ci.yml against two three-job runs from the same congested window:

run allocations queue exec
refactor/goals-controller-scope 3 57m32s 18m52s
feat/desktop-api-key-host-onboarding 3 68m10s 18m02s
this branch 1 10m45s 23m27s

The first breaks down as plan queued 12m to execute 18s, heavy queued 19m to execute 18m32s, and test queued 25m to execute 2s. Execution is not comparable across different diffs, but #4400 ran the pre-#4172 single job at 24m27s, so 23m is the normal shape for one job rather than an inflation.

cli-package-validation opened 10 jobs here against 14 on a control branch (run 33498682856). test was green at 23m27s on the previous head.

Filter rates over the last 200 first-parent commits: windows-recovery 118 → 17, release-windows-check 55 → 41, app icon regeneration 193 → 41.

@ARE404 both gate facts you held on are resolved: the pull request is out of draft, and test was green on the previous head. Checks are running again on this one.

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at the current head 0d4301c6e05e48f86c2891535b81e714c590bb4d (PR came out of draft; supersedes the head in my prior hold note). No P0/P1 — the design is clean and, after two refinement commits, coverage-preserving.

The two refinements since the earlier head materially strengthen it:

  • 0d4301c6 restores the genuinely Windows-only recovery surface to the PR lane by name (the process.platform === 'win32'-guarded modules — assertNoWindowsAlternateStreams + its NTFS regressions, which no other lane can catch), instead of dropping it wholesale with the portable-TS redundancy. The rule is now checkable rather than remembered: every workspace entry on that filter must be a single file (not a glob) that actually contains a Windows branch, with tests pinning it — a portable file can no longer slip in. Its sharpest note is empirical: #4400 matched the old wide closure, ran this lane green 3× on the PR, and the alternate-stream failure still surfaced only on the main push — i.e., the wide filter it replaces didn't even stop the one regression it's tempting to cite, because that was a stale-base interaction no path filter sees. So the narrowing loses no pre-merge signal that the wide filter actually provided.
  • a6c3e1e8 keeps the candidate installer's own upgrade and rollback on pull requests (only the pinned-historical-release baseline qualification stays on the schedule, which a PR cannot affect); the runtime-host-boot.ts feed wiring stays asserted per-change in the required test lane via update-test-feed-wiring.test.mjs.

Core guarantees still hold from my prior review: the required job keeps its test name (the .asf.yaml required context) and its install/heavy step gates on requiresHeavyValidation(plan) (an OR over every surface, with code set for any real code file and unknown paths forcing a full run) — so no pull request can go green while skipping its suite; changedFilesBetween is merge-base correct; cli-package-validation folds the redundant predecessor runner.

CI is running at this head (rebased/advanced branch; the required test lane + package/addons in progress). Out of draft, MERGEABLE, review already APPROVED. Approving on code; the remaining item is exact-head CI green before merge.

…s them

`durableStateLocations` builds its paths with `join`, so on Windows the
State Root is `\qualification-scope\state-root`. The assertion compared it
to a literal `'/qualification-scope/state-root'` and failed there, taking
`npm run check:release` and the packaged Windows lane with it — the
account-local assertion beside it already used `join` and passed.

The nesting assertion below had the same blind spot without failing:
`golden.startsWith(`${live}/`)` cannot match a Windows path, so it would
have accepted a golden copy nested inside its live directory, which is the
one thing it exists to reject. Both now spell the separator the way the
platform does.

Generated-by: Claude Code
@Astro-Han

Copy link
Copy Markdown
Contributor Author

package failed on this head, before any step this pull request changed, and the cause is worth recording because it is on main.

npm run check:release runs scripts/qualify-released-cli-state-root.test.mjs, added by #4427. Its durable state covers the control namespace case builds paths with join and then compares one to a literal '/qualification-scope/state-root', so it passes on Linux and fails on Windows, where the value is \qualification-scope\state-root. The assertion directly beneath it already used join and passed. Fixed in 54d8164.

The nesting assertion two lines further down had the same blind spot without failing: golden.startsWith(`${live}/`) cannot match a Windows path, so it would have accepted a golden copy nested inside its live directory — the one arrangement it exists to reject. That is fixed in the same commit.

This is out of scope for a runner-occupancy pull request, but it blocks this lane on every branch that reaches it, so it is here rather than in a follow-up.

It is also a live example of the point @hqhq1025 made in the windows-recovery thread. This defect cannot go red on Linux by construction, and a Windows lane running before merge is what caught it — which is why the seventeen win32-branching files went back on that filter rather than the lane's pull-request trigger being narrowed further.

@M4n5ter M4n5ter 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.

English

Re-review at 54d81647: the latest path-separator fix is correct and the Windows package job is now green, but it does not affect the three findings below. I do not think this head is ready to merge.

[Blocking] The Windows recovery allowlist is not complete

windows-recovery.yml lists selected Windows-branching files, while its contract only proves that files already on the list contain win32. It cannot detect omissions.

There are omissions on the current head. The workflow directly executes tests whose source files contain Windows-only paths but are absent from the filter, including control-endpoint.test.ts, runtime-resume-crash.test.ts, runtime-continuation-crash.test.ts, and four Skill catalog tests. stable-storage.ts is another omitted Windows-branching dependency directly used by the listed root-authority.ts and marker-file.ts.

A change to these paths runs only the Linux required lane before merge; its Windows branch is skipped, and this Windows workflow is not scheduled. The previous Windows-coverage P1 is therefore only partially resolved. Please verify filter completeness from the test entrypoints the workflow actually executes and their Windows-branching dependency owners, and add the currently missing paths.

[Blocking] The app-icon surface omits a direct test input

APP_ICON_FILES omits apps/desktop/electron-builder.config.mjs, although the gated test reads that file directly to keep the macOS/Windows bundle icon aligned with DEFAULT_APP_ICON.

The planner currently returns appIcons=false for that config, and check:release does not run this test. Pointing the config at another existing icon can therefore pass build/package while silently skipping the drift assertion. Please add the config to APP_ICON_FILES and the planner regression inputs.

[Important] The restored Windows baseline cache is deleted before use

The workflow restores artifacts/windows-upgrade-baseline, but prepareWindowsUpgradeBaseline() immediately removes that directory and always invokes gh release download.

A targeted reproduction with a valid cached installer produced downloads=1 and cachedFileSurvived=false. Cache hits therefore do not avoid the network failure this change intends to remove. The smallest fix is to verify and return an existing exact installer, download only when it is absent, and reject a checksum mismatch. A focused cache-hit/cache-miss test would preserve that contract.

The overall consolidation direction is a net simplification; I found no separate simplify-audit recommendation. The earlier candidate-sensitive upgrade/rollback P1 is resolved.

Local verification: build, typecheck, lint, format, ASF headers, 82 focused workflow/contract tests, and Linux check:release all passed. The new path-separator test passes 8/8 at this exact head. Hosted Windows package, Windows recovery, CLI smokes, and State Root qualification are green; required test was still queued when this review was posted.

中文

54d81647 上重新审查:最新的路径分隔符修复是正确的,Windows package 也已经转绿,但它没有触及下面三项问题。当前 head 仍不适合合并。

[Blocking] Windows recovery allowlist 仍不完整

windows-recovery.yml 手工列出部分 Windows 分支文件,而对应 contract 只验证“已经列入的文件包含 win32”,无法发现遗漏。

当前 head 已经存在遗漏。Workflow 直接执行、包含 Windows-only 路径但未进入 filter 的测试包括 control-endpoint.test.tsruntime-resume-crash.test.tsruntime-continuation-crash.test.ts 和四个 Skill catalog tests。stable-storage.ts 也包含 Windows 分支,并被已列入的 root-authority.tsmarker-file.ts 直接调用,但它同样不在 filter 中。

修改这些路径时,合并前只会运行 Linux required lane;Windows 分支被跳过,而 Windows workflow 不会启动。之前的 Windows coverage P1 因此只修复了一部分。请从 workflow 实际执行的测试入口及其 Windows-branching dependency owners 验证 filter 完整性,并补齐当前遗漏。

[Blocking] App icon surface 漏掉了直接测试输入

APP_ICON_FILES 没有包含 apps/desktop/electron-builder.config.mjs,但被 gate 的测试会直接读取它,用于保持 macOS/Windows bundle icon 与 DEFAULT_APP_ICON 一致。

Planner 对该配置当前返回 appIcons=false,而 check:release 也不运行这项测试。因此,将配置指向另一个已经存在的 icon 时,build/package 可以成功,但 drift assertion 会被静默跳过。请将该配置加入 APP_ICON_FILES 和 planner regression inputs。

[Important] 恢复出来的 Windows baseline cache 在使用前就被删除

Workflow 恢复了 artifacts/windows-upgrade-baseline,但 prepareWindowsUpgradeBaseline() 随后立即删除该目录,并始终调用 gh release download

定向复现中,预置有效缓存后仍得到 downloads=1cachedFileSurvived=false。所以 cache hit 无法避开本次修改想消除的网络失败。最小修复是:已存在精确 installer 时校验并直接返回;不存在时才下载;checksum 不匹配则明确失败。建议增加一个聚焦 cache hit/cache miss 的测试。

整体 consolidation 方向确实是净简化,没有额外的 simplify-audit 建议。之前 candidate-sensitive upgrade/rollback 的 P1 已解决。

本地验证:build、typecheck、lint、format、ASF headers、82 项 workflow/contract tests 和 Linux check:release 均通过;新路径分隔符测试在当前 exact head 上 8/8 通过。Hosted Windows package、Windows recovery、CLI smokes 和 State Root qualification 已转绿;发出本 review 时 required test 仍在排队。

…g of it

Three reviewers found three holes in this branch, and they are one hole.
Every gate it narrowed took its new input set from my reading of what the
gated work consumes, and every contract test I wrote to defend that set
proved only that what I kept belonged — never that what I dropped was
unnecessary. Containment in the easy direction. The other direction is an
absence, and an absence has no line number to check.

So each list was short by whatever my reading could not see. The recovery
filter missed `stable-storage.ts`, reached transitively through the two
lock authorities it did name, along with six test files. `APP_ICON_FILES`
missed `electron-builder.config.mjs`, which the drift test opens by path
rather than imports. And the baseline cache missed that
`prepareWindowsUpgradeBaseline` deletes its output directory before
downloading unconditionally, so a restored entry was never once consulted.

Each is now computed from the authority and asserted as set equality:

- The recovery filter is the import closure of the dist suites its steps
  execute, restricted to files that branch on `win32` — 56 files, checked
  with `deepEqual` so an omission and a stale entry both fail. That is 40
  of the last 200 merges rather than 17, and unlike 17 it can be shown to
  be complete. `collectWindowsPackageSourceClosure` is now a caller of a
  general `collectWorkspaceSourceClosure`, which the packaged Windows lane
  already used for exactly this purpose.
- The app icon surface is read off the step: whatever `App icon artwork
  drift` runs is scanned for the repository paths it opens, and each must
  select the gate.
- `prepareWindowsUpgradeBaseline` reuses a copy that hashes to the pinned
  digest and downloads only otherwise. A cache entry is never an authority
  on what the run installs: a mismatch or an unreadable file goes to the
  network, and only a fresh download that fails the digest fails the run.

Applying the same rule to the gate none of the three reviews reached found
a fourth instance, in the required job. `heavy` decides whether the
toolchain is installed, and nothing asserted that a selection gating a
later step is one of its disjuncts — so `app_icons`, added by this branch,
gated a step that imports `@maka/core/settings` without selecting the
install it needs. It reached green only because every icon input happens
to select `code` as well. That disjunct is now present, and the workflow
is scanned for the rest.

Generated-by: Claude Code
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Thank you — this was the most useful review on the pull request. All three are fixed in b915970c6b, and you were right in a way I had not seen: they are one finding rather than three.

Every gate this branch narrowed took its input set from my reading of the gated work, and every contract test I wrote proved containment in the easy direction — that what I kept belonged — never that what I dropped was unnecessary. An omission is an absence, and an absence has no line number to check. So all three lists were short by exactly what my reading could not see: a transitive dependency, a path opened rather than imported, and a consumer that discards its own input.

Each is now computed from the authority and asserted as set equality.

Recovery filter. No longer curated. scripts/ci-test-plan.test.mjs derives the dist suites the steps execute, maps them back to source, takes their import closure through collectWorkspaceSourceClosure — the function the packaged Windows lane already used for exactly this — and restricts it to files that branch on win32. That is 56 files, compared with deepEqual, so an omission and a stale entry both fail. It covers everything you named, stable-storage.ts included. The lane now matches 40 of the last 200 merges rather than 17; unlike 17, it can be shown to be complete.

App icon surface. apps/desktop/electron-builder.config.mjs is in APP_ICON_FILES, but the list is no longer the contract: the test reads which suites App icon artwork drift runs, scans them for the repository paths they open, and requires each to select the gate. Removing the config makes it fail with verify-packaged-app-icons.test.mjs reads apps/desktop/electron-builder.config.mjs.

Baseline cache. prepareWindowsUpgradeBaseline now returns a copy that hashes to the pinned digest and downloads only otherwise. A cache entry is never an authority on what the run installs: a mismatch or an unreadable file goes to the network, and only a fresh download failing the digest fails the run. scripts/prepare-windows-upgrade-baseline.test.mjs covers hit, miss, stale, unreadable and mismatch. Your reproduction was exact — downloads=1, cachedFileSurvived=false — and it is now the second and third cases in that file.

Applying the same rule where none of the three reviews reached found a fourth instance, in the required job. heavy decides whether the toolchain is installed at all, and nothing asserted that a selection gating a later step is one of its disjuncts. app_icons — added by this branch — gated a step that imports @maka/core/settings without selecting the install it needs. It was green only because every icon input happens to select code as well. That disjunct is now present, and the workflow is scanned for the rest rather than the rule being restated.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found one P1 and one P2 on exact head b915970c6b86b57052a8ba9b5b38888cb2052137, so the conditional approval threshold is not met.

This head replaces the hand-maintained Windows recovery allowlist with a generated Windows-branching import closure, adds authority-derived app-icon and heavy-selection contracts, and makes the pinned Windows baseline cache reusable after checksum verification. The previous current-head findings about the incomplete recovery surface, omitted app-icon config, and discarded cache entry are addressed in the implementation.

The new planner contract cannot run in the clean pre-install CI phase because it statically imports installed packages, and the new cache regression file is not invoked by any hosted command; see the inline findings.

Validation after dependency installation: npm run build:test, production build, full workspace typecheck, lint, format, ASF headers, release contracts 155/155, and the focused planner/Windows/cache suites 116/116 passed. git diff --check passed. In a fresh exact-head worktree before npm ci, the required CI command node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs scripts/verify-windows-harness.test.mjs failed immediately with ERR_MODULE_NOT_FOUND for esbuild. At publication, hosted windows_recovery was green while required test was still queued and package was running.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

Comment thread scripts/ci-test-plan.test.mjs Outdated
Comment thread scripts/prepare-windows-upgrade-baseline.test.mjs
`ci-test-plan.test.mjs` was the only suite running before `npm ci`, so
every assertion that needed a bare checkout accreted there regardless of
subject: 66 tests, of which 35 read `.github/workflows/*.yml` and had
nothing to do with the test planner. The file name carried no hint that
its one hard rule is "import nothing that is not installed yet", which is
how a closure assertion needing esbuild was very nearly added to it. That
import would have thrown `ERR_MODULE_NOT_FOUND` in the sole required
context and frozen every merge in the repository.

Split by what a test reads. `ci-test-plan.test.mjs` keeps the 30 tests
that exercise the planner against an in-memory graph; the 36 that read a
workflow move to `ci-workflow-policy.test.mjs`, beside the two policy
suites that already follow that name.

Naming the constraint is not enforcing it, so derive it: the new suite
reads which files the steps above `setup-node` run and walks their
transitive local imports, failing on any specifier that is neither a
`node:` builtin nor a repository module holding the same rule. Moving a
step below the install lifts the constraint and adding one above imposes
it, with no list to maintain.

The closure assertion itself now lives in
`windows-package-source-closure.test.mjs`, which `check:release` runs
after installing, and `windows-recovery.yml` joined the release-contract
inputs so editing that filter selects the gate that checks it.

Two app-icon tests iterated a hand-written `APP_ICON_INPUTS`; the derived
test computes that same set from the suites the step runs, so they folded
into it and the list is gone. `prepare-windows-upgrade-baseline.test.mjs`
joined `check:release` without joining the planner's release set — the
guardrail that exists for exactly that caught it.

Generated-by: Claude Code
Two filters in this branch were proved only in the easy direction, which
is the direction that cannot see an omission.

`release-windows-check.yml` asserted that its import closure is covered by
the filter. Nothing asserted the converse, so a `packages/` entry backed by
nothing stayed in the list forever and booked a 25-minute Windows runner
every time it matched, with no test able to report it. Five of its twenty
five entries turn out to be underived — peer dependency manifests, Runtime
Host candidate election, the script that builds the worker the closure
starts from. Each is legitimate, and each is now declared with its reason,
so the set is exact in both directions and a stale exception fails as
loudly as a missing entry.

Three hand-written tests asserted that a lane pairs its path filter with a
nightly run, one lane each. The rule they encode is not lane-specific: a
filter is a pre-filter, not an import closure, so something has to run the
lane without consulting it. Replaced by one test enumerating the workflow
directory and requiring a schedule, an unfiltered push, or a `workflow_call`
caller. The three pairings covered three lanes; the rule covers eight and
would have caught `release-windows-check.yml`, whose schedule this branch
added without an assertion to hold it there.

That enumeration also surfaces three lanes that have no escape at all:
`gitoxide-helper-admission` and `runtime-host-peer-admission` pair the pull
request with a `push: main` carrying the same filter, and
`runtime-host-owner-platform` pairs it with `workflow_dispatch`, which
nothing fires on its own. They predate the gates narrowed here, so they are
declared rather than changed — the point is that the gap is now countable
instead of invisible.

Generated-by: Claude Code
Four leftovers from the consolidations earlier in this branch, each one a
guarantee the old shape supplied and the new shape does not.

`release-windows-check.yml` keyed its concurrency group on `github.ref`,
which is `refs/heads/main` for the nightly and for a dispatch alike, so a
dispatch could queue behind the nightly and be cancelled while still
pending. Keyed on the pull request number like the other two Windows lanes.

The three State Root transitions used to be three matrix jobs, so one
failing left the others to upload their reports. Folded into one step they
share a `set -e`, and the reports are wanted most on the run that failed,
so the upload now runs with `if: always()`. `tee` has already written the
failing transition's own output by then, and `if-no-files-found` stays
`error` so a broken path is still caught on a green run.

Moving predecessor resolution onto `build` rewrote the assertions that
bound it, and `PREDECESSOR_TARBALL_URL` lost its only one — the third
transition would still be spelled correctly while pointing at nothing.
Both env bindings and the `workflow_call` output are asserted again.

`.asf.yaml` still described the required context as an aggregation job
propagating failures from a plan lane and a heavy job, none of which exist
since the three merged into one. It now describes what is there, and names
splitting the work back across jobs as the third way to freeze the queue.

Also bounds `update-test-feed-wiring.test.mjs` to the argument object it
means: an unbounded span would have accepted a `testFeedUrl` from any later
call in a 1900-line module.

Generated-by: Claude Code
Nine assertions restated `packages/` entries of
`release-windows-check.yml` as literals. Every one could only ever catch
"this exact line was deleted", and the previous commit made that case fail
already: the filter's `packages/` half is computed from the import closure
and compared as a set, so removing an entry fails there whether or not
anyone remembered to also name it here. Verified by deleting
`connect-or-spawn.ts` from the filter and watching the closure test fail
with these gone.

Two of the nine carried a reason for being in the filter without being in
the closure; that reason now lives on the declared exception beside the
path it explains, which is where someone auditing the entry will look.

`copy-runtime-filesystem-worker.mjs` stays asserted by hand. It is the one
entry outside the derived half: the desktop app copies the built worker in
rather than importing it, so no closure reaches it and only naming it keeps
it on the lane.

Kept as well are the assertions that encode a rule rather than a copy —
that the gate triggers on `release.yml` and on itself, and that Windows
recovery still runs the three regression suites it exists for. Those state
something no derivation produces.

Generated-by: Claude Code

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review of exact head 83a56953350c16728bd23bc2a6cc648c08703acc: no P0-P3 findings.

The previous blockers are fixed. The dependency-free pre-install planner/workflow suites now run successfully in a clean checkout with no node_modules; the dependency-backed Windows source-closure contract runs after installation through check:release; and the reusable Windows baseline regression is both included in that gate and self-selects the release-contract lane.

I reviewed the complete workflow/planner delta and the Windows recovery, package, CLI validation, State Root, app-icon, and release-contract paths. On the exact head, npm ci, build:test, the production build, full workspace typecheck, check:release (161 tests), focused workflow/contract tests (93/93), lint, format, ASF headers, and git diff --check passed. A clean synthetic merge with current main dfe457eac9fcef811174754111650923ebcbd3f9 also passed npm ci, build:test, full typecheck, the 93 focused tests, the protocol epoch guard, and diff checks.

Hosted platform and CLI validation checks currently reported success; required test, package, and audit were still pending when this review was submitted, so their terminal results remain part of the merge gate. I did not independently execute the Windows-only package lifecycle locally.

Review notice: This review was prepared by an automated review agent operated by hqhq1025 and is published at the direction of AstroHan, who has read these findings and is the human accountable for them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants