Skip to content

Commit df7a137

Browse files
ci(lint): Type Check · workspace type-checks the affected packages on PR and queue runs; the whole workspace on push and schedule (#23043)
Fixes #23017 Clause-②: no `Type Check · workspace` (the `typecheck-workspace` lane behind the required `TypeScript Type Check` context) now type-checks only the packages a `pull_request` / `merge_group` diff affects; `schedule`, `push` and `workflow_dispatch` keep the whole workspace (the full battery, #22998). The job name, the `TypeScript Type Check` aggregate and its roster are unchanged. One file changes: `.github/workflows/lint.yml`, one step (`Type check workspace packages`) and the comment above it. ## The card's mechanism, measured first, and refuted The card's hypothesis was that the turbo cache never restores on PR and queue runs because the restore keys are scoped to `github.ref_name`. On `main` at edecd44 the restore step carries FOUR restore keys; the third is the bare prefix `${{ runner.os }}-turbo-typecheck-`, which matches the newest main-push save on every event. Measured on the jobs API and the job logs: - PR run 38239402273 (`claude/issue-22992-showcase-boot-splash`): restore 10 s, `Cached: 138 cached, 139 total`, `Type check workspace packages` 19 s. The cache restores. - The same PR's merge group, run 38242582445, 29 minutes later: `Cached: 64 cached, 139 total`. All 64 hits are `build` tasks; 0 of 74 `typecheck` tasks replayed; step 512 s (turbo `Time: 8m31.023s`). - Every task hash differed between the two runs (132 of 132 common task ids). `@objectstack/types#typecheck` computed locally is `88ca22ad9987a868` at 3071d3e, `5d35a004a079ced5` at 2acff87, and `30eade7d14ef0536` from c7d6037 through edecd44, the last equal to the hash the merge group logged. 2acff87 and c7d6037 both touch `packages/spec`. Nearly every package depends on spec, and a `typecheck` hash includes its dependencies' `build` hashes. So the restore-keys fix the card proposes (a `-typecheck-main-` line) would change nothing: the bare prefix already restores main's seed, and on PR runs main's saves are the only typecheck saves that exist. The seed misses because of hash churn. A merge group is built on entries that have not reached main yet, so its seed is behind by construction. Commit (1) of the suggested route was therefore not made. H4 (the build step misses for the same key reason) is refuted the same way: on `pull_request` the build step reads the remote cache (`remote:r`) and its misses are the PR's own affected packages, which nobody has written yet; on `merge_group` the build step is a `remote:rw` writer and its median is 114 s. ## What changed The step's `run:`, with the comment above it explaining each rule: - `pull_request` / `merge_group`: `scripts/ci/select-shard-packages.sh`, ci.yml Test Core's own selector, called with the same event fields. Both jobs therefore read one rule for where a diff starts: merge-base with `origin/main` on a PR, the group's `base_sha` in the queue, and the whole list with a `::warning::` when neither resolves. The selection includes dependents (`turbo ls --affected`). It also unions in packages whose tests read the changed files across a package boundary (`check-cross-package-test-inputs.mjs --union-into`), because their typecheck programs compile those same tests. For example, lint's and cli's tests import `examples/app-showcase`, which no `^build` edge reaches. The selection is intersected with the unchanged universe (`./packages/*`, `./packages/*/*`, `./apps/*`) and passed to `turbo run typecheck` as one `--filter=NAME` flag per package. - **A root-level change means the whole universe. The workflow states this rule itself instead of leaving it to turbo.** Measured on turbo 2.11.5 at edecd44, one file changed per probe: `tsconfig.json`, `tsup.config.ts` (the `globalDependencies`) and `turbo.json` select all 81 packages under `turbo ls --affected`. Root `package.json`, `pnpm-workspace.yaml`, `.npmrc`, and a change to the ROOT importer's `typescript` version in `pnpm-lock.yaml` select ZERO. So any changed repo-root file (a path with no `/`) means the whole universe, except Markdown, because no tsc program reads a root `*.md`. A changed `scripts/` file that a package's own `package.json` scripts run also means the whole universe. For example, `scripts/check-test-typecheck.mts` is run by the `typecheck` of 72 of the universe's 76 packages. That set is found with `git grep` at run time, not kept as a list. - An empty selection is an honest green and says so in the log. It exits before `turbo run typecheck`, because that command with no `--filter` runs the whole workspace. Every doubt falls back to the whole universe: no changed-file list, a failed union, or a base that cannot be resolved. - `schedule`, `push`, `workflow_dispatch` and any other event: the whole universe, as before. The `push`-only cache save is unchanged, so main's seed stays complete. `turbo.json` is unchanged. H2 found no input that moves on every commit: the `typecheck` hash stays the same across commits that do not touch its dependency closure (measured above). ## Measurements **Before.** The card (29 runs): step median 486 s, p90 627 s; job wall median 11.1 min, p90 14.2 min. Re-read over 300 `lint.yml` runs (2026-10-11T20:38Z to 2026-10-12T11:35Z), counting only successful `Type Check · workspace` jobs: | event | n | step median / p90 | job wall median / p90 | |---|---|---|---| | pull_request | 117 | 479 s / 641 s | 11.7 / 17.8 min | | merge_group | 93 | 510 s / 633 s | 11.7 / 15.0 min | | push | 30 | 417 s / 621 s | 8.2 / 11.8 min | | schedule | 14 | 1 s / 429 s | 1.3 / 8.2 min | **Selection replay.** The final step script was run on the last 60 first-parent commits of `main` ending at edecd44, each as a merge group on its parent. `turbo ls` and the union were real; `turbo run typecheck` was stubbed. Results, out of a 76-package universe: | packages selected | commits | |---|---| | none | 4 | | 1–5 | 20 | | 6–20 | 6 | | 21–50 | 8 | | 51–72 (spec-touching) | 16 | | whole universe | 6 | The 6 whole-universe runs: 3 objectui pin bumps that also touch `packages/spec`, 1 lockfile change, 1 `.gitattributes` change and 1 change to `scripts/sync-scaffold-emission-policy.mjs`. **Local timing.** On a 4-core / 15 GB box (the ubuntu-latest shape), with a fresh turbo cache dir and the dependency closure built beforehand, every selected typecheck task is a cold miss. A one-line diff in `packages/plugins/plugin-email/src/index.ts` selects 11 packages: - The 6 packages the dependency graph reaches type-check in 1m19s. - All 11, including the union's five (spec, core, types, objectql, runtime), type-check in 2m32s. Compare that with the whole universe's 486 s median on the runner. With the seed restored, any of the 11 whose own hash is in the seed replays instead of running. **This PR's own run:** run 38252457300 on `06f9521d` — `Type Check · workspace` job wall 56 s, `Type check workspace packages` 2 s (log: `No workspace package is affected by this diff -- nothing to type-check.`); `TypeScript Type Check` and `Lint & Repo Gates` green. Every turbo restore key missed on that run (`Cache not found for input keys: Linux-turbo-typecheck-23043/merge-..., Linux-turbo-typecheck-, Linux-turbo-build-core-...`), so the run also shows the step is independent of main's seed. **Cache hit count:** the dispatch asked for the turbo summary's hits on untouched packages. With this change, PR and queue runs do not schedule untouched packages at all, so there is no hit count to quote. The pre-change PR run 38239402273 above (138 of 139) shows that the restore already reaches main. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 43 commands from this diff. All 43 ran on `06f9521d` and exited 0, reconciled with `--ran` (`43 run, 0 NOT-MEASURED (a DERIVED zero ...)`). They include `check:required-contexts`, `check-aggregator-roster`, `check:type-check-coverage`, `check:type-check-debt`, `check:cross-package-test-inputs`, `scheduled-full-run --self-test`, `check:pm-dispatch-gates` (fast tier, its off-CI default) and `check:nul-bytes`. No pin script flagged anything (H5): the step keeps its name and its `turbo run typecheck` invocation. The YAML was parsed with `yaml`, and the extracted `run:` passes `bash -n`. The script was also replayed against synthetic merge-group and PR diffs: | diff | selection | |---|---| | leaf package | 11 packages | | root `package.json` | whole universe | | root-importer lockfile | whole universe | | `scripts/check-test-typecheck.mts` | whole universe | | workflow file only | none | | `examples/app-showcase` | cli, dogfood, lint, spec | | `push` / `schedule` event | whole universe | | unresolvable `base_sha` | whole universe, with the selector's warnings | ## Acceptance notes - The card and the claim call `.github/workflows/**` Tier H, but `GOVERNED_SURFACES` in `scripts/pm/check-governed-merges.mjs` has no workflows row. This PR stays a draft as dispatched either way. - `typecheck`'s turbo inputs are package-local. A package scheduled through the union or the root rule still replays a cached green when its own hash is in the seed, on every event including `schedule`. This PR does not change that, and the step comment says so. Declaring `$TURBO_ROOT$` inputs for `typecheck` (as `test` has them) would be the fix. It is not attempted here, because it changes hash semantics for every package. - The union is a superset for tsc: it also names files that tests only read, so spec, core, types, objectql and runtime join most package diffs (+73 s cold in the local timing above). A tsc-only radius would need its own declaration and gate. - Root Markdown is exempt because AGENTS.md alone was 2 of the last 30 commits. Any other root file, including `.objectui-sha`, `.gitattributes` and `sdui.manifest.json`, means the whole universe. - **`lint.yml` is at GitHub's workflow-file size ceiling.** This branch at 513,738 bytes (`0bd33ce6`, `d4265926`, `b39592f3`) produced a failed zero-job workflow-file run on each push (38245964547, 38249293618, 38250844690) and the PR got no Lint & Type Check run at all; `06f9521d` at 510,016 bytes is valid; `main` is 505,924 bytes. The step comment was compacted for that reason, and this body carries the rationale. The ceiling itself is carded by the seat (a gate on the file's size and a split plan). ## 维护者速读(草稿) **改了什么**:`Type Check · workspace` 这条类型检查车道,在 PR 和合并队列上只检查本次改动影响到的包(含依赖它们的包);每小时定时、推到 main 和手动触发仍然全量检查。作业名、必需检查 `TypeScript Type Check` 及其成员都不变。只改了 `.github/workflows/lint.yml` 里的一个步骤。 **为什么改**:这一步的中位耗时约 8 分钟,是合并队列里最长的环节之一。卡片原先以为是缓存键写错,导致 PR 恢复不到 main 的缓存。实测缓存其实能恢复(某个 PR 的 139 个任务命中了 138 个)。真正的原因是几乎所有包都依赖 spec:只要 spec 一变,缓存里的检查结果就全部作废;而队列里的条目总比 main 新,所以几乎每次都全量重跑。按改动范围挑包才能根治。 **风险与代价(含回滚)**:改动仓库根目录的文件(锁文件、根 package.json、tsconfig 等,Markdown 除外),或改动各包类型检查共同调用的脚本时,一律全量检查,不交给 turbo 判断。实测 turbo 对根 package.json 的变化、以及锁文件里 TypeScript 版本的变化,都会判为"没有包受影响"。被其他包的测试跨包引用的包(例如示例应用被 lint/cli 的测试引用)也会被并入检查。代价是:普通的包改动会顺带检查 spec 等五个大包,冷启动时约多 70 秒。改动了 spec 的提交(约占三分之一)仍然接近全量。回滚只需还原这一个步骤,不涉及数据或产物迁移。 **席位意见**: **你要做的**:审阅后决定是否批准这个草稿 PR。落地后,由 PM 读取连续三个合并队列和三个 PR 的实测耗时。 Body amended by the director seat from the dev's report 6124930963 (three stale spots named there: the gates sha, this PR's own run, the size-ceiling note), 2026-10-12T13:13Z. --- _Generated by [Claude Code](https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1802315 commit df7a137

1 file changed

Lines changed: 69 additions & 1 deletion

File tree

‎.github/workflows/lint.yml‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7019,8 +7019,76 @@ jobs:
70197019
# comment): the nested group dirs and apps/ (docs) need their own globs.
70207020
# Examples and the downstream contract keep their dedicated steps below —
70217021
# they are distinct gates, not bulk coverage.
7022+
#
7023+
# ── WHICH PACKAGES, PER EVENT (#23017) ──
7024+
# The three filters are the UNIVERSE. `schedule`, `push`,
7025+
# `workflow_dispatch` and any other event type-check all of it (the full
7026+
# battery, #22998). `pull_request` / `merge_group` type-check what their
7027+
# diff affects, plus dependents, through ci.yml Test Core's selector
7028+
# (same diff base, same cross-package test union, same fall-back to the
7029+
# full list), intersected with the universe. Not the cache alone: the
7030+
# restore above already reaches main's seed, but nearly every package
7031+
# depends on spec, so one spec commit between seed and run moves nearly
7032+
# every typecheck hash (PR run 38239402273 replayed 138 of 139 tasks; its
7033+
# merge group 38242582445, 0 of 74 typecheck tasks).
7034+
# ⛔ A changed repo-root file (Markdown excepted), or a `scripts/` file a
7035+
# package.json script runs, means the whole universe: `turbo ls
7036+
# --affected` selects ZERO packages for a root package.json,
7037+
# pnpm-workspace.yaml, .npmrc or root-importer lockfile change (turbo
7038+
# 2.11.5). ⛔ An empty selection exits here: `turbo run typecheck` with no
7039+
# `--filter` runs the whole workspace. Selection decides which packages
7040+
# run; the package-local turbo cache still decides which execute.
70227041
- name: Type check workspace packages
7023-
run: pnpm exec turbo run typecheck --filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*'
7042+
env:
7043+
OS_SHARD_EVENT_NAME: ${{ github.event_name }}
7044+
OS_SHARD_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
7045+
OS_SHARD_PR_PINNED_BASE_SHA: ${{ github.event.pull_request.base.sha }}
7046+
OS_SHARD_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
7047+
run: |
7048+
set -euo pipefail
7049+
UNIVERSE=(--filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*')
7050+
WHOLE=''
7051+
case "$OS_SHARD_EVENT_NAME" in
7052+
pull_request|merge_group) ;;
7053+
*) WHOLE="the '$OS_SHARD_EVENT_NAME' event carries the full battery" ;;
7054+
esac
7055+
if [ -z "$WHOLE" ]; then
7056+
rm -f "$RUNNER_TEMP/turbo-ls.json" "$RUNNER_TEMP/changed-files.txt"
7057+
bash scripts/ci/select-shard-packages.sh
7058+
if [ ! -s "$RUNNER_TEMP/changed-files.txt" ]; then
7059+
WHOLE='the selector left no changed-file list (missing or empty), so the root-level rule cannot be judged'
7060+
else
7061+
{ git grep -h -o -E '(\.\./)+scripts/[A-Za-z0-9._/-]+' -- \
7062+
'packages/*/package.json' 'packages/*/*/package.json' 'apps/*/package.json' || true; } \
7063+
| sed -E 's#^(\.\./)+##' | sort -u > "$RUNNER_TEMP/package-script-tools.txt"
7064+
ROOT_FILE=$(grep -m1 -v -e / -e '\.md$' "$RUNNER_TEMP/changed-files.txt" || true)
7065+
TOOL_FILE=$(grep -m1 -Fx -f "$RUNNER_TEMP/package-script-tools.txt" "$RUNNER_TEMP/changed-files.txt" || true)
7066+
if [ -n "$ROOT_FILE" ]; then
7067+
WHOLE="root-level change: $ROOT_FILE"
7068+
elif [ -n "$TOOL_FILE" ]; then
7069+
WHOLE="a script the packages' own package.json scripts run changed: $TOOL_FILE"
7070+
fi
7071+
fi
7072+
fi
7073+
if [ -n "$WHOLE" ]; then
7074+
echo "Type-checking the whole workspace -- $WHOLE."
7075+
pnpm exec turbo run typecheck "${UNIVERSE[@]}"
7076+
exit 0
7077+
fi
7078+
pnpm exec turbo ls "${UNIVERSE[@]}" --output=json > "$RUNNER_TEMP/typecheck-universe.json"
7079+
jq -r --slurpfile sel "$RUNNER_TEMP/turbo-ls.json" \
7080+
'($sel[0].packages.items | map(.name)) as $s | .packages.items[].name | select(. as $n | any($s[]; . == $n))' \
7081+
"$RUNNER_TEMP/typecheck-universe.json" > "$RUNNER_TEMP/typecheck-selected.txt"
7082+
mapfile -t SELECTED < "$RUNNER_TEMP/typecheck-selected.txt"
7083+
if [ "${#SELECTED[@]}" -eq 0 ]; then
7084+
echo "No workspace package is affected by this diff -- nothing to type-check."
7085+
exit 0
7086+
fi
7087+
echo "Type-checking ${#SELECTED[@]} affected package(s) of the universe:"
7088+
printf ' %s\n' "${SELECTED[@]}"
7089+
FILTERS=()
7090+
for P in "${SELECTED[@]}"; do FILTERS+=("--filter=$P"); done
7091+
pnpm exec turbo run typecheck "${FILTERS[@]}"
70247092
70257093
# Seed the shared Turbo cache from main only (see the restore step above).
70267094
#

0 commit comments

Comments
 (0)