Repository navigation
Commit fa5d72e
ci(workflows): measured timeout-minutes on 17 jobs, PR-only concurrency on 11 workflows, a paths filter on check-links (#26376)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9BFPQCAwNbKfEL5SeQxNa8m)_
Fixes #26085
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:给 17 个没有超时的 CI job 各配一个按自己近期运行时长定的 `timeout-minutes`;给 11 个
PR 触发的工作流加 `concurrency`,同一 PR 的新推送取消旧运行;`check-links` 只在改到它读的文档时才跑。
- **为什么改**:今天一个卡死的 job 会占住 runner 6 小时;一个 PR 连推三次就跑三遍这 11 个工作流;不碰文档的 PR
也跑链接检查。
- **风险与回滚**:风险在 `release.yml` 的 publish 超时(设 120 分钟,按各步骤上限求和再加一半,杀在
`changeset publish` 中途就是半发布);取消只对 `pull_request`
生效,push、merge_group、schedule、dispatch 一律不取消。回滚 = revert 本 PR,纯 YAML。
- **先看**:`.github/workflows/release.yml`(publish 的 120
分钟及注释)、`.github/workflows/pr-automation.yml`(concurrency 按「推送 /
标签与编辑」分两组)、`.github/workflows/check-links.yml`(paths 与目标侧盲区说明)。
- **两分钟验证**:看本 PR 自己的 Actions 页:开 PR 时贴标签触发的 `PR Automation` 运行没有取消
`opened` 那次运行(两次都完成);下一次推送后,上一轮 PR 工作流显示 cancelled。
- **需要你做的**:受管面(`.github/workflows/**`,Tier H):请
GOVERNED_APPROVERS(os-zhuang 或 hotlong)批准,批准后由席位落地。
## What changes
**1. `timeout-minutes` on the 17 jobs the card lists.** Re-read on
`origin/main` 6390fe5 first: exactly the card's 17 jobs lacked one (no
job moved), and after this PR every non-reusable job in
`.github/workflows/` declares one (read below). Each value comes from
that job's own recent runs and is written beside it in the YAML. Where
only a run-level time exists, the run wall (`updated_at −
run_started_at`, queue included) is used as an upper bound for the job.
| Job | Readings (completed runs, success + failure) | p99 | max | Set |
|:---|:---|---:|---:|---:|
| `check-links / link-checker` | 100 runs, 2026-10-26..27 | 0.8 | 1.0 |
10 |
| `codeql / analyze` | 22 green analyses in the last 100 runs (78 were
cancelled), 2026-10-24..26 | 59.0 | 63.1 | 90 |
| `cross-repo-issue-closer / close-foreign-issues` | 100 runs | 0.2 |
0.3 | 5 |
| `docker-publish / publish` | all 4 job readings on record: 3.5, 3.5
(dispatch runs), 4.3 (18.1.0 attempt 2), 1.4 (attempt 1, red) | 4.3 |
4.3 | 30 |
| `docs-drift-check / docs-drift` | 100 runs | 2.1 | 6.2 | 15 |
| `duplicate-fix-guard / duplicate-fix-guard` | 100 runs | 0.3 | 0.8 | 5
|
| `partof-closing-keyword-guard / partof-closing-keyword` | 100 runs |
0.8 | 0.8 | 5 |
| `pr-automation / pr-size` | 100 runs (run wall, all three jobs) | 2.4
| 2.5 | 10 |
| `pr-automation / auto-label` | same | 2.4 | 2.5 | 10 |
| `pr-automation / changeset-check` | same | 2.4 | 2.5 | 10 |
| `release / version-pr` | the 23 schedule + dispatch runs among the 400
most recent (2026-10-22..27) | 2.4 | 2.4 | 20 |
| `release / release-integrity` | the 376 push runs among the 400 most
recent (run wall, upper bound) | 4.6 | 6.9 | 20 |
| `release / stale-prompts` | same 376 push runs; own job reading on
18.1.0 = 1.0 | 4.6 | 6.9 | 10 |
| `release / publish` | 1 job reading: 18.1.0, run 40078805767 = 32.6
(publish step 15.5) | n/a | 32.6 | 120 |
| `single-claim-path-guard / single-claim-path` | 100 runs | 0.8 | 0.9 |
5 |
| `spec-liveness-check / liveness` | 100 runs (22 PR, 78 merge queue) |
1.3 | 1.4 | 10 |
| `stale / stale` | 100 runs, 2026-07-19..10-26 | 0.6 | 26.6 | 45 |
All minutes. Three values are deliberately wider than "p99 plus a bit",
and each says why in the YAML:
- **`publish` = 120.** One reading cannot give a p99, and a timeout here
cancels mid-`changeset publish` — the half-published state that job's
own concurrency note exists to prevent. So it is set from the steps'
ceilings: build + Console build about 15 min cold (6.6 + 7.3 on 18.1.0),
`release-verify-npm.mjs`'s retry budget of up to 35 min after `changeset
publish`, and the dist-tags step's own `timeout-minutes: 15` — about 80
min in all, plus half again. The approval wait at the `release`
environment is not job time.
- **`docker-publish` = 30.** Four readings; the image is built for two
platforms with arm64 under QEMU, and a cold layer cache was never
measured.
- **`stale` = 45.** p99 is 0.6 min, but one green run took 26.6 min (run
33285517150, 2026-08-30); the bound keeps it.
**2. `concurrency` on the 11 PR-triggered workflows** (`check-links`,
`docs-drift-check`, `duplicate-fix-guard`, `engine-split-metric`,
`os-create-smoke`, `pack-smoke-optin`, `pr-automation`, `scaffold-e2e`,
`spec-liveness-check`, `upgrade-harness-optin`, `validate-deps` —
re-read on 6390fe5, the same 11). The group key is `ci.yml:112-114`'s,
prefixed with the file stem:
```yaml
concurrency:
group: STEM-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.after || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
```
`cancel-in-progress` is an expression rather than ci.yml's `true`, so
only a `pull_request` run is ever cancelled: none of the 11 runs on
`push`, and the one with `merge_group` (`spec-liveness-check`) can never
cancel a queue run — its key is also the queue ref, unique per entry.
Schedule and dispatch runs share a group per event and wait instead of
cancelling.
Two key refinements, because these workflows fire on PR activity other
than a push:
- **`pr-automation`** runs on `labeled` / `unlabeled` / `edited` too,
and on those only `Check Changeset` runs (`pr-size` and `auto-label`
skip). One group would let a label cancel a push run's size and label
jobs — and the relay's `pr_create` writes labels right after the draft
opens, so that would happen on almost every PR. The key gets a last
segment, `meta` for those three actions and `code` otherwise, so each
kind only supersedes its own.
- **`pack-smoke-optin` / `upgrade-harness-optin`** run on `labeled`, but
the job runs only for their own opt-in label. A run for any other label
goes to a group of its own (`github.run_id`), so adding an unrelated
label never cancels a 30–45 min opt-in run.
**3. `check-links.yml` gets a `paths:` filter** (`workflow_dispatch`
kept): the whole `content/` and `docs/` trees, the eight root pages in
`LYCHEE_ARGS` since #26313, `lychee.toml`, `.lycheeignore`, and the ADR
step's `scripts/check-adr-links.mjs` (+ its one import) and
`docs/adr/**`, plus the workflow itself. Checked with lychee 0.24.2's
`--dump-inputs` under CI's exact argv: 521 inputs, 0 outside the filter.
On the last 49 landings of `main`, 10 touched one of these paths. The
YAML states the blind spot (below) and that promoting the lane to
required removes the filter in the same change.
Not in this PR: the card's optional `permissions: {}` (out of the
claim), `ci.yml` (untouched), and no job was renamed.
## Tests
- `node scripts/ci/scheduled-full-run.mjs --check-concurrency` → exit 0
(ci.yml and lint.yml groups unchanged; this gate reads only those two
files).
- No job without `timeout-minutes`: a YAML walk of every
`.github/workflows/*.yml` listing jobs with neither `timeout-minutes`
nor `uses:` → empty (it listed the card's 17 on 6390fe5). The one
`uses:` job, `release / docker`, is bounded by the callee's new
`docker-publish / publish` value.
- Every workflow parses as YAML.
- lychee 0.24.2 (built from crates.io) with `LYCHEE_ARGS`: 2883 links, 0
errors; `--dump-inputs` 521 files, all inside the new `paths:`.
## Acceptance notes
- **Target-side blind spot of the `check-links` filter.** Lychee also
reads the files links point AT. Measured on 6390fe5, links from the
inputs resolve to paths outside `content/` and `docs/` (about 100 links:
`packages/*` directories from CONTRIBUTING.md, `LICENSE` and
`ui/LICENSE` from LICENSING.md, `examples/*`, `skills/…`,
`.github/workflows/…`). A PR that only renames or deletes one of those
now skips this advisory lane; the break shows on the next PR that
touches a doc or on a dispatch run. Covering it would mean `packages/**`
in the filter (which nearly every PR touches) or a scheduled run;
neither is in this card's scope.
- `scripts/release-verify-npm.mjs` L80-82 says "the publish job declares
no `timeout-minutes`, so GitHub's 360-minute job default bounds this
step". After this PR the publish job's 120 min bounds it. That file is
outside this claim's file surface, so the comment is left as is and
reported to the seat.
- The two 2026-10-27 scheduled Release runs (40426048088, 40375928411)
are red in `version-pr`; not looked into here.
## Verification
Local gate subset (`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, 55 commands at HEAD 62f0d0f), each exit
taken before any pipe, `--ran` reconciliation `55 derived, 55 run, 0
NOT-MEASURED, 0 UNRUN` (a DERIVED zero):
<details><summary>Gate readings</summary>
| Command | Exit |
|:---|---:|
| `node scripts/check-adr-links.mjs` | 0 |
| `node scripts/check-adr-links.mjs --self-test` | 0 |
| `node scripts/check-aggregator-roster.mjs` | 0 |
| `node scripts/check-aggregator-roster.mjs --self-test` | 0 |
| `node scripts/check-changeset-fixed.mjs` | 0 |
| `node scripts/check-ci-filter-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `node scripts/check-cross-repo-closer-outcome.mjs` | 0 |
| `node scripts/check-cross-repo-closer-outcome.mjs --self-test` | 0 |
| `node scripts/check-engine-split-ratio.mjs --days 90` | 0 |
| `node scripts/check-osv-exemptions.mjs` | 0 |
| `node scripts/check-osv-exemptions.mjs --self-test` | 0 |
| `node scripts/check-self-test-wired.mjs` | 0 |
| `node scripts/check-self-test-wired.mjs --self-test` | 0 |
| `node scripts/check-self-test-workflow-commands.mjs` | 0 |
| `node scripts/check-self-test-workflow-commands.mjs --self-test` | 0 |
| `node scripts/check-step-collectors.mjs` | 0 |
| `node scripts/check-step-collectors.mjs --self-test` | 0 |
| `node scripts/check-whole-set-label-write.mjs` | 0 |
| `node scripts/check-whole-set-label-write.mjs --self-test` | 0 |
| `node scripts/ci/scheduled-full-run.mjs --self-test` | 0 |
| `node scripts/docs-audit/check-affected-docs.mjs` | 0 |
| `node scripts/docs-audit/check-drift-comment.mjs` | 0 |
| `node scripts/osv-base-relative.mjs --self-test` | 0 |
| `node scripts/pm/ci-failure.mjs --self-test` | 0 |
| `node scripts/release-dist-tags.mjs --self-test` | 0 |
| `node scripts/release-verify-npm.mjs --self-test` | 0 |
| `pnpm check:agent-test-spelling` | 0 |
| `pnpm check:cross-package-test-inputs` | 0 |
| `pnpm check:declared-population-live` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:gitlink-declared` | 0 |
| `pnpm check:node-version` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:override-consistency` | 0 |
| `pnpm check:partof-closing-keyword` | 0 |
| `pnpm check:pm-dispatch-gates` | 0 |
| `pnpm check:pm-expected-skips` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:pm-post-stamped` | 0 |
| `pnpm check:pm-record-recognisers` | 0 |
| `pnpm check:pnpm-acquisition` | 0 |
| `pnpm check:pnpm-filter-targets` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:required-contexts` | 0 |
| `pnpm check:shard-attestation` | 0 |
| `pnpm check:single-claim-paths` | 0 |
| `pnpm check:stall-guard-budget` | 0 |
| `pnpm check:stall-guard-headroom` | 0 |
| `pnpm check:vendor-export-contract-resolve` | 0 |
| `pnpm check:watch-hint-literal` | 0 |
| `pnpm check:workflow-status-functions` | 0 |
| `pnpm check:workflow-step-name-quoting` | 0 |
`node scripts/check-engine-split-ratio.mjs --days 90` first exited 2
(PREREQUISITE: the shallow clone sat inside its 90-day window); after
`git fetch --shallow-since=2026-07-22 origin main` it exited 0 (ratio
97.6%).
Also run, from the roster block that keeps its list under
`.github/workflows`: `check-platform-checklist-watchdog` (+
`--self-test`), `check-ci-filter-parity --self-test`,
`ci/select-shard-timings-run --self-test`, `pr-labels --self-test`,
`check:pm-governed-prose`, `check:select-gate-families` → all exit 0.
`check-governed-queue-guard.mjs` exits 1 locally because it reads only
`GITHUB_EVENT_PATH`; its verdict is the required `Governed Surface Queue
Guard` check on this PR.
</details>
Families that take a value from the workflow
(`check-partof-closing-keyword`, `check-single-claim-paths`,
`check-required-contexts --verify-required-set`, the shard attestations)
are NOT MEASURED locally and are read from this PR's own check-runs.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmSfkkJZXxR3bPjvRzh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3acbeff commit fa5d72e
18 files changed
Lines changed: 181 additions & 0 deletions
File tree
- .github/workflows
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
23 | 36 | | |
24 | 37 | | |
25 | 38 | | |
26 | 39 | | |
27 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
28 | 64 | | |
29 | 65 | | |
30 | 66 | | |
31 | 67 | | |
32 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
33 | 73 | | |
34 | 74 | | |
35 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
30 | 33 | | |
31 | 34 | | |
32 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
127 | 130 | | |
128 | 131 | | |
129 | 132 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
39 | 44 | | |
40 | 45 | | |
41 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
75 | 82 | | |
76 | 83 | | |
77 | 84 | | |
78 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
79 | 89 | | |
80 | 90 | | |
81 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
30 | 37 | | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
34 | 44 | | |
35 | 45 | | |
36 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
21 | 29 | | |
22 | 30 | | |
23 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
69 | 77 | | |
70 | 78 | | |
71 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
91 | 100 | | |
92 | 101 | | |
93 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
53 | 56 | | |
54 | 57 | | |
55 | 58 | | |
| |||
0 commit comments