Repository navigation
Commit ab4e30a
ci(governed-surface-guard): the pull_request leg checks out commits and trees only and installs the toolchain on demand (#26311)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8DRxWpNGUz2gv8QFn8EE47u)_
Fixes #26201
Clause-②: no
Metadata-protocol: none
## 维护者速读(草稿)
**改了什么**
受管面守卫(`Governed Surface Queue Guard`)在 PR
上的那一跑变轻了:只拉提交历史和目录树(不拉文件内容),工作区只铺开 `scripts/` 与 `.github/`
两个目录,默认不装依赖。合并队列那一跑(真正会拒绝的那条腿)一字未改;检查名不变;翻 ready 时守卫仍会重跑,这是它的设计初衷,保留。
**为什么改**
在手机上批准受管 PR 后马上点合并,会被「required status check is in progress」拒掉:批准后 PR 被翻成
ready,守卫重跑,那一跑约 95 秒里只有 1 秒在判定,其余是拉代码和装依赖。现在这一跑只做判定需要的事,窗口缩到二十秒以内:本 PR
自己那一跑实测 19 秒(此前约 94 秒)。
**风险与代价(含回滚)**
- 判定不变:PR #25965(普通受管 PR)与 PR #25813(带生成型 skills
索引文件)上,新旧两种检出跑出的判定逐字节相同。
- 碰到 skills 下生成型索引文件的 PR(近 30 天约占 main 提交的
0.45%)仍会补回整棵树并装依赖,那一跑和以前一样慢,换来判定准确。
- 回滚:revert 本 PR 即恢复原样,无数据或配置残留。
**席位意见**
**你要做的**
审阅后批准或直接合并。本 PR 改了 `.github/workflows/`,属 Tier H,落地需要你本人。
## What changed
`.github/workflows/governed-surface-guard.yml`, the `pull_request` leg
only:
- **Checkout.** The single checkout step is now two, keyed on the event.
`merge_group` keeps the unchanged step (`Checkout repository`,
`fetch-depth: 0`, full tree). `pull_request` gets `Checkout repository
(commits and trees only)`: `fetch-depth: 0`, `filter: blob:none`,
`sparse-checkout` of `scripts` and `.github`.
- **Toolchain on demand.** A new step asks the script
`--toolchain-needed`. It runs on the `pull_request` leg only and appends
`needed=true|false` to `$GITHUB_OUTPUT`. Every install step is gated on
the output being NOT `'false'`, and so is a `git sparse-checkout
disable` step that restores the full tree first. A skipped question (the
queue leg) or a failed one therefore leaves the output empty and
installs, which is the previous behaviour.
- **Unchanged:** `types: [opened, synchronize, reopened,
ready_for_review]`, the `merge_group` trigger, the job id
`governed-surface-guard`, the check-run name `Governed Surface Queue
Guard`, permissions, concurrency, timeouts and the judgment step.
- **Header.** The `ready_for_review` note gains the reason this leg is
kept cheap rather than dropped. The "Both legs install" paragraph now
reads "wherever the recompute can change the answer".
`scripts/pm/check-governed-queue-guard.mjs`:
- `toolchainNeededFor(rows)` is the one predicate.
`liftGeneratedExceptions` now consults the recompute iff it answers
true, and the workflow installs iff `--toolchain-needed` answers true.
Membership is still the register's `generatedExceptionFor`; no path list
is restated.
- `readEventDiff(env)` is extracted from `main()` so the judgment and
the toolchain question read the event and the diff one way. The
judgment's messages and exit codes are unchanged.
- `toolchainNeededAnswer({ eventName, read })` is pure. It answers
`false` only for a `pull_request` diff that was read and touches no
register path.
- New self-test battery, 20 cases, roster floor 28 → 29. Its pure half
covers the predicate, the lift spy agreeing with it, and the answer per
event and read state. Its YAML half covers exactly two checkouts (queue
one unchanged; PR one blobless and sparse), the question wired to
`$GITHUB_OUTPUT` on the PR leg only, every install step gated on
not-`false` with no `== 'true'` spelling anywhere, the restore step
ahead of the install, and an unconditional judgment step.
`installStepAudit` gains a `stepBodies` field with comment lines
dropped.
`scripts/pm/dispatch-gates.self-test.mjs`: the LIVE pin "exactly one
family classifies CI-measured" became false because of this change.
`--toolchain-needed` reads the event payload, so the live derivation now
sees two CI-measured invocations of the one guard. The pin now names
both.
## Route taken against the dispatch hypotheses
- **Route 2 taken; route 1 not taken.** `ready_for_review` stays in
`types:`, and the header's reason for it stands.
- **The dispatch's framing of route 2 was "no checkout of the full tree,
no pnpm install". Measured, both halves needed a qualifier.**
- The `pull_request` leg is not API-only. Its judgment reads git
history: `merge-base`, `log --first-parent`, `diff-tree --name-only`.
Those need commits and trees, not blobs. So the leg keeps `fetch-depth:
0` (the #9902 reason holds on this leg too) and drops only the blobs.
- The workflow header gives a stated reason for installing on both legs:
the `pull_request` leg forecasts the queue verdict, and without the
generator toolchain it forecasts a refusal the queue will not make. So
the install is not dropped. It is acquired exactly when the lift would
recompute, decided by the same predicate the lift uses.
- **Not done:** no copy of the script is vendored, and
`actions/github-script` is not used.
## Premises (re-measured on `be531f685`, the branch base)
- **P1 holds.** `git grep -n 'ready_for_review'` on the workflow finds 2
lines: the `types:` line and one header comment. Control `synchronize`:
1 line.
- **P2 holds.** The check-runs on `e7b12fdd`, filtered to `Governed
Surface Queue Guard`, are two runs. Run 121428970465 ran 06:21:40Z →
06:22:56Z, before the ready flip. The ready-fired run 121656663464 ran
15:12:37Z → 15:14:11Z (94 s).
- **P3 measured.** Tree reads: the event payload, `.github/CODEOWNERS`,
the workflow file and its own sources under `scripts/`, plus git objects
(commits and trees only). API reads: reviews, the pull object, comments
and the timeline, all at the existing `pull-requests: read`. The static
and lazy import graph is `node:` built-ins plus relative `scripts/**`
modules only. Measured by running `--self-test` on an archive of just
`scripts/` and `.github/` with no `node_modules`: 373 cases pass at
base, exit 0.
## Measurements (dev container, 2026-10-26, repository
`objectstack-ai/objectstack`)
- **Clone cost of full history.**
- blobless: 7 s, 105 MiB
- treeless: 3 s, 64 MiB
- with contents: 58 s, 956 MiB
- **Sparse checkout** of `scripts` and `.github` on the blobless clone:
2.7 s.
- **Restoring the full tree** (`git sparse-checkout disable`) for the
toolchain case: 26 s. Then `pnpm install --frozen-lockfile` on a warm
store: 13 s.
- **Judgment, PR #25965** (governed workflow file, no register path):
blobless sparse checkout against a full checkout gives byte-identical
output, exit 0 on both. `--toolchain-needed` answers `needed=false` on
`pull_request` and `needed=true` on `merge_group`.
- **Judgment, PR #25813** (two `skills/*/references/_index.md` files):
`--toolchain-needed` answers `needed=true` and names both paths. After
the restore step and the install, the judgment is byte-identical to a
full checkout with an install, and the same rows LIFTED. Without the
toolchain on the sparse tree, every row stays governed: fail-closed, as
before.
- **Frequency.** 14 of the 3142 commits reachable from `main` at
`be531f685` since 2026-09-26 touched a register path. Counted on a full,
non-shallow clone with `git rev-list --count --since` and a path-limited
`git log`.
## Acceptance
- **"Approving and readying a governed PR starts no new guard run" is
not met by design.** Route 1 is what would meet it, and the dispatch
kept `ready_for_review` (route 2). The ready flip still starts a run,
now a short one. This is the card's second acceptance arm.
- **"The `pull_request` leg's wall time is under 20 s": met.** This PR's
own guard run, job 121820715758 (2026-10-26T20:12:44Z → 20:13:03Z), took
19 s wall: blobless sparse checkout 10 s, Node 3 s, install steps
skipped. The ready-fired run on PR #25965 took about 94 s.
- **`Governed Surface Queue Guard` remains the check-run name on both
legs.** The job and its `name:` are untouched. The self-test's WIRING
pin and `check:required-contexts` both pass.
## Tests and gates (final head `8cd23c77`, after merging `origin/main`)
- `node scripts/pm/check-governed-queue-guard.mjs --self-test`: exit 0,
393 cases pass.
- `pnpm check:pm-dispatch-gates`: 1899 cases pass (fast tier), 313.4 s.
Run detached; the verdict line is quoted and there is no `ELIFECYCLE`.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 52 local commands. All 52 exit 0 on
`8cd23c77`, each exit code captured before any pipe. That set includes
`check:required-contexts`, `check-ci-filter-parity`,
`check-step-collectors` (658 steps, 40 workflows),
`check:pm-expected-skips`, `check:workflow-step-name-quoting` and
`check:nul-bytes`.
- Two runs were added outside the derivation: `node
scripts/check-required-contexts.mjs --self-test` (174 assertions) and
`node scripts/pm/check-expected-skips.mjs --self-test` (173 cases), both
exit 0.
- `--ran` reconciliation: 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN.
It is a derived zero, because every family recorded an exit code.
- No ablation: a workflow and a gate script are changed, and their pins
are the self-test cases above.
## Acceptance notes
- The `skip-changeset` label is declared at create. Nothing under
`.github/workflows/**` or `scripts/pm/**` ships in any package's
`files[]`.
- The rare toolchain case pays about 26 s to restore the tree, in place
of about 48 s of full-content checkout. Its wall stays roughly where it
was.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Cpku5P4EYCqaaEGHuMcxik)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 844ea10 commit ab4e30a
3 files changed
Lines changed: 259 additions & 29 deletions
File tree
- .github/workflows
- scripts/pm
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
39 | 44 | | |
40 | 45 | | |
41 | 46 | | |
| |||
87 | 92 | | |
88 | 93 | | |
89 | 94 | | |
| 95 | + | |
| 96 | + | |
90 | 97 | | |
| 98 | + | |
91 | 99 | | |
92 | 100 | | |
93 | 101 | | |
| |||
100 | 108 | | |
101 | 109 | | |
102 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
103 | 132 | | |
104 | 133 | | |
105 | 134 | | |
| |||
167 | 196 | | |
168 | 197 | | |
169 | 198 | | |
170 | | - | |
171 | | - | |
172 | | - | |
173 | | - | |
174 | | - | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
175 | 223 | | |
| 224 | + | |
176 | 225 | | |
177 | 226 | | |
178 | 227 | | |
179 | 228 | | |
| 229 | + | |
180 | 230 | | |
181 | 231 | | |
182 | 232 | | |
| |||
185 | 235 | | |
186 | 236 | | |
187 | 237 | | |
| 238 | + | |
188 | 239 | | |
189 | 240 | | |
190 | 241 | | |
| |||
194 | 245 | | |
195 | 246 | | |
196 | 247 | | |
| 248 | + | |
197 | 249 | | |
198 | 250 | | |
199 | 251 | | |
| |||
0 commit comments