Skip to content

ci(workflows): measured timeout-minutes on 17 jobs, PR-only concurrency on 11 workflows, a paths filter on check-links - #26376

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-h8lsmd
Oct 27, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-h8lsmd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
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:

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):

Gate readings
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.

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

…ls; concurrency on 11 PR workflows

Claude-Session: https://claude.ai/code/session_01ADHkmSfkkJZXxR3bPjvRzh
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review October 27, 2026 01:03
@os-zhuang
os-zhuang enabled auto-merge October 27, 2026 01:03
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f02e644343a3ecd2e26b1a9ac961ceb229ca9fad → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · REVISE · devx seat 2 · 2026-10-27T01:06Z

Seat domain:devx#2 (#20163) · session_01Q4MDXRbEJCNdPAP4SiXwp9 · reviewed head 62f0d0f2 · card #26085, claim 6430717688 (wave 8, delta A). Tier H (check-governed-merges: governed, tier H, humanMerge); the readying and auto-merge on this PR were set by a GOVERNED_APPROVERS member, not by the seat or the card thread.

One item to fix (blocking, small):

  1. 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 together with everything before it". This PR gives that job timeout-minutes: 120, so the sentence becomes false on merge, and it is the very line that tells whoever raises the 35-minute budget to re-check the caller. Rewrite it, comment only, to say the publish job's bound is 120 minutes (set in release.yml from the steps' ceilings, this 35-minute budget included) and that raising the budget means re-checking that sum. The claim's file surface is amended to cover this (6430717688). Run node scripts/release-verify-npm.mjs --self-test if the file has one, then push.

Checked and accepted as is:

  • Scope: 18 workflow files, +181/−0, only timeout-minutes, concurrency and the check-links paths: filter. No ci.yml, no job rename, no ui/, no permissions: {} item. skip-changeset is right.
  • Timeouts: each of the 17 carries its own reading in a comment, no blanket number. The three wide ones are argued: release publish 120 (a timeout there is a half-publish, so the bound comes from step ceilings, ~80 min, plus half), docker-publish 30 (two platforms, arm64 under QEMU, cold cache unmeasured), stale 45 (one green 26.6-minute run).
  • Concurrency: all 11 use cancel-in-progress: ${{ github.event_name == 'pull_request' }}, so push to main, merge_group, schedule and dispatch are never cancelled; push groups are keyed on github.event.after, so pushes never share a group; merge_group falls to the unique queue ref. The two opt-in workflows give an unrelated labeled run its own group, so another label cannot cancel a running opt-in run. pr-automation splits labeled/unlabeled/edited from push runs; the thread measured on this PR that the skip-changeset label did not cancel the opened run's size and label jobs.
  • check-links paths: advisory lane (not in check-required-contexts), so paths: cannot strand a required check; the header says to drop the filter if the lane is ever promoted, and records the blind spot on the link-target side.

Noted, not for this PR: check-links does not run when a PR only moves or deletes a link target outside content/ and docs/ (105 such targets); the seat tracks it as a possible follow-up card. The two red scheduled Release runs are not from this diff.

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

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants