From 6103c99ea65e769c90c102f718a33b066b6d59d1 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 1 Nov 2026 17:59:54 +0000 Subject: [PATCH] perf(ci): typecheck only the packages a pull request affects The Build job typechecked all 39 workspace packages on every pull request, in dependency order, even when ci-changes.cjs had already found a smaller affected set. Fork pull requests cannot read the shared task cache, so each of those checks ran cold. Pass the affected set to the typecheck step, as the Test job already does, and run it with check:changed's command, which drops dependency ordering that tsc --noEmit does not need. Pushes to main and changes that reach every package still typecheck everything. Add two reads the server's typecheck and tests make without a declared dependency, so the set stays complete. Co-Authored-By: Claude Opus 5.5 --- .github/actions/ci-changes/action.yml | 2 ++ .github/scripts/ci-changes.cjs | 9 +++++++- .github/scripts/ci-changes.d.cts | 1 + .github/workflows/ci.yml | 13 +++++++++-- scripts/check-changed.test.ts | 31 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/.github/actions/ci-changes/action.yml b/.github/actions/ci-changes/action.yml index ecf787404657a..4005e44b6508e 100644 --- a/.github/actions/ci-changes/action.yml +++ b/.github/actions/ci-changes/action.yml @@ -3,6 +3,8 @@ description: Decide which CI jobs a change needs. See .github/scripts/ci-changes outputs: typecheck: value: ${{ steps.detect.outputs.typecheck }} + typecheck_packages: + value: ${{ steps.detect.outputs.typecheck_packages }} build: value: ${{ steps.detect.outputs.build }} test: diff --git a/.github/scripts/ci-changes.cjs b/.github/scripts/ci-changes.cjs index 0f817be60c35d..18a1e698a1e6b 100644 --- a/.github/scripts/ci-changes.cjs +++ b/.github/scripts/ci-changes.cjs @@ -27,6 +27,8 @@ const EXTRA_OWNERS = [ [/^native\/browser-secret\//, ["@t3tools/desktop"]], [/^native\/kde-snap-shot\//, ["@t3tools/desktop"]], [/^native\/libghostty-vt\//, ["@t3tools/web"]], + // apps/server typechecks the root vite.config.ts, which imports this. + [/^infra\/task-cache\/src\/remoteCache\.ts$/, ["t3"]], // apps/server/src/cloud/ConnectReachability.test.ts checks the guide's headings. [/^docs\/user\/t3-connect-troubleshooting\.md$/, ["t3"]], ]; @@ -36,6 +38,8 @@ const RUST = /^native\/(resource-monitor|kde-snap-shot|hyprland-snap-shot)\//; const TEST_ONLY_DEPS = { // Evals import the server's source to test its real model-facing text. "@t3tools/evals": ["t3"], + // Orchestration tests apply events through the client projection. + t3: ["@t3tools/client-runtime"], "@t3tools/provider-pi": ["@t3tools/client-runtime"], "@t3tools/scripts": [ "@t3tools/desktop", @@ -151,11 +155,14 @@ function decide(files, packages, full) { (pkg) => pkg.hasTest && !OWN_TEST_JOB.has(pkg.name) && affected.has(pkg.name), ); const has = (name) => affected.has(name); + const names = [...affected].sort(); return { - affected: [...affected].sort(), + affected: names, outputs: { typecheck: affected.size > 0, + // Empty means every package. + typecheck_packages: everything ? "" : names.join(" "), build: has("@t3tools/desktop") || has("t3") || has("@t3tools/web"), test: tested.length > 0, // Empty means every package the Test job covers. diff --git a/.github/scripts/ci-changes.d.cts b/.github/scripts/ci-changes.d.cts index afe6f61339362..9bacf3553b4b8 100644 --- a/.github/scripts/ci-changes.d.cts +++ b/.github/scripts/ci-changes.d.cts @@ -15,6 +15,7 @@ export function decide( affected: Array; outputs: { typecheck: boolean; + typecheck_packages: string; build: boolean; test: boolean; test_packages: string; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4c45728c2342..f6e41abd61ee9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ jobs: timeout-minutes: 5 outputs: typecheck: ${{ steps.detect.outputs.typecheck }} + typecheck_packages: ${{ steps.detect.outputs.typecheck_packages }} build: ${{ steps.detect.outputs.build }} test: ${{ steps.detect.outputs.test }} test_packages: ${{ steps.detect.outputs.test_packages }} @@ -163,14 +164,22 @@ jobs: run: vp run --filter @t3tools/desktop ensure:electron # Runs in the background while the build runs, and reports in the last - # step: neither result hides the other. + # step: neither result hides the other. `changes` lists the affected + # packages, or nothing when all of them are. Same command as check:changed. - name: Start typecheck id: typecheck if: ${{ !cancelled() && steps.electron.outcome == 'success' && needs.changes.outputs.typecheck != 'false' }} + env: + TYPECHECK_PACKAGES: ${{ needs.changes.outputs.typecheck_packages }} run: | + filters=(-r) + if [[ -n "$TYPECHECK_PACKAGES" ]]; then + filters=() + for package in $TYPECHECK_PACKAGES; do filters+=(--filter "$package"); done + fi ( set +e - vpr typecheck + vp run --cache --parallel --concurrency-limit 4 "${filters[@]}" typecheck echo "$?" > "$RUNNER_TEMP/typecheck-status" ) > "$RUNNER_TEMP/typecheck.log" 2>&1 < /dev/null & diff --git a/scripts/check-changed.test.ts b/scripts/check-changed.test.ts index b0e0fe01e851b..16961680b0926 100644 --- a/scripts/check-changed.test.ts +++ b/scripts/check-changed.test.ts @@ -35,6 +35,37 @@ describe("check:changed", () => { assert.strictEqual(defaultConcurrency(1, 64 * GiB), 1); }); + it("typechecks the changed packages and their dependents, or every package for shared inputs", () => { + const packages = readWorkspace(); + const typecheck = (files: ReadonlyArray | null, full = false) => + decide(files, packages, full).outputs.typecheck_packages; + assert.strictEqual( + typecheck(["oxlint-plugin-t3code/src/index.ts"]), + "@t3tools/oxlint-plugin-t3code", + ); + assert.includeMembers(typecheck(["packages/provider-grok/src/index.ts"]).split(" "), [ + "@t3tools/provider-grok", + "@t3tools/client-runtime", + "t3", + ]); + // The server reads these without depending on their packages. + for (const file of [ + "packages/client-runtime/src/state/orchestrationV2Projection.ts", + "infra/task-cache/src/remoteCache.ts", + ]) { + assert.include(typecheck([file]).split(" "), "t3"); + } + for (const files of [ + ["packages/shared/package.json"], + ["pnpm-lock.yaml"], + ["packages/shared/src/HostProcess.ts"], + null, + ]) { + assert.strictEqual(typecheck(files), ""); + } + assert.strictEqual(typecheck(["oxlint-plugin-t3code/src/index.ts"], true), ""); + }); + it("runs the server tests for the one guide they read and leaves other docs inert", () => { const packages = readWorkspace(); assert.isTrue(