Skip to content
Source
Sign inSign up

ci.yml

692 lines29.2 KB

1name: CI
2​
3on:
4 pull_request:
5 push:
6 branches:
7 - main
8​
9permissions:
10 contents: read
11​
12concurrency:
13 group: ci-${{ github.event.pull_request.number || github.sha }}
14 cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15​
16jobs:
17 # Skips jobs a pull request cannot affect: a web change does not run the
18 # server shards, a server change does not run the mobile tests, and the macOS
19 # lint (~6.7x a Linux minute) runs only for native mobile sources.
20 # .github/scripts/ci-changes.cjs maps changed files to workspace packages and
21 # their dependents. Pushes to main still run every Linux job. Detection fails
22 # open: if the diff cannot be resolved, everything runs.
23 changes:
24 name: Changes
25 runs-on: blacksmith-2vcpu-ubuntu-2404
26 timeout-minutes: 5
27 outputs:
28 typecheck: ${{ steps.detect.outputs.typecheck }}
29 build: ${{ steps.detect.outputs.build }}
30 desktop_launch: ${{ steps.detect.outputs.desktop_launch }}
31 test: ${{ steps.detect.outputs.test }}
32 test_packages: ${{ steps.detect.outputs.test_packages }}
33 test_web: ${{ steps.detect.outputs.test_web }}
34 test_server: ${{ steps.detect.outputs.test_server }}
35 rust: ${{ steps.detect.outputs.rust }}
36 release_smoke: ${{ steps.detect.outputs.release_smoke }}
37 mobile_native: ${{ steps.detect.outputs.mobile_native }}
38 steps:
39 # Only the package manifests the script reads to build the dependency graph.
40 # Depth 2 brings the base commit the change is diffed against.
41 - name: Checkout
42 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
43 with:
44 fetch-depth: 2
45 sparse-checkout: |
46 /pnpm-workspace.yaml
47 /.github/scripts/ci-changes.cjs
48 /.github/actions/ci-changes/
49 /*/package.json
50 /*/*/package.json
51 !/.repos/
52 sparse-checkout-cone-mode: false
53​
54 - name: Detect affected jobs
55 id: detect
56 uses: ./.github/actions/ci-changes
57​
58 # Lint and the desktop build each get a runner so they run side by side.
59 # Typecheck shares the build's install. Lint always runs: it formats every
60 # file, docs included. The `Check` job at the end of this file passes only
61 # when every other job passes or was skipped by `changes`.
62 lint:
63 permissions:
64 contents: read
65 id-token: write
66 name: Lint
67 runs-on: blacksmith-8vcpu-ubuntu-2404
68 timeout-minutes: 10
69 steps:
70 - name: Checkout
71 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
72 with:
73 sparse-checkout: |
74 /*
75 !/.repos/
76 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
77 sparse-checkout-cone-mode: false
78​
79 - name: Reject repository-owned PR assets
80 run: |
81 files="$(git ls-files .github/pr-assets)"
82 if test -n "$files"; then
83 printf 'PR evidence must be uploaded to GitHub, not committed:\n%s\n' "$files" >&2
84 exit 1
85 fi
86​
87 - name: Setup Vite+
88 uses: ./.github/actions/setup-ci-deps
89 with:
90 task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
91​
92 # electron's install.js (run by ensure:electron) reads this cache before it
93 # downloads from GitHub. The key changes only with the Electron version.
94 - name: Cache Electron download
95 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
96 with:
97 path: ~/.cache/electron
98 key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }}
99​
100 - name: Ensure Electron runtime is installed
101 run: vp run --filter @t3tools/desktop ensure:electron
102​
103 # Files/dependencies are repo-wide; export checks cover clean workspaces only.
104 - name: Check unused code
105 run: vp run knip:check
106​
107 - name: Check generated GitHub GraphQL operations
108 run: vp run --filter @t3tools/source-control-github generate --check
109​
110 - name: Check generated Expo GraphQL operations
111 run: vp run --filter t3 generate --check
112​
113 - name: Check
114 run: vp check
115​
116 build:
117 permissions:
118 contents: read
119 id-token: write
120 name: Build and Typecheck
121 needs: changes
122 if: ${{ !cancelled() && (needs.changes.outputs.build != 'false' || needs.changes.outputs.typecheck != 'false') }}
123 runs-on: blacksmith-8vcpu-ubuntu-2404
124 timeout-minutes: 10
125 steps:
126 - name: Checkout
127 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
128 with:
129 sparse-checkout: |
130 /*
131 !/.repos/
132 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
133 sparse-checkout-cone-mode: false
134​
135 - uses: ./.github/actions/setup-apt-mirrors
136 if: needs.changes.outputs.build != 'false'
137​
138 # Runs in the background while Vite+ installs; the step before the build waits for it.
139 # Xvfb is for the desktop launch check below.
140 - name: Start installing browser secret helper build libraries
141 if: needs.changes.outputs.build != 'false'
142 run: |
143 # Fetch only the indexes these main and universe packages need. The full set is ~42 MB,
144 # which a slow mirror can stretch past the wait below.
145 sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \
146 /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
147 (
148 set +e
149 sudo apt-get update -o Acquire::Languages=none \
150 -o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \
151 -o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false &&
152 sudo apt-get install -y libsecret-1-dev pkg-config xvfb
153 echo "$?" > "$RUNNER_TEMP/apt-status"
154 ) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
155​
156 - name: Setup Vite+
157 uses: ./.github/actions/setup-ci-deps
158 with:
159 task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
160​
161 # electron's install.js (run by ensure:electron) reads this cache before it
162 # downloads from GitHub. The key changes only with the Electron version.
163 - name: Cache Electron download
164 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
165 with:
166 path: ~/.cache/electron
167 key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }}
168​
169 - name: Ensure Electron runtime is installed
170 id: electron
171 run: vp run --filter @t3tools/desktop ensure:electron
172​
173 # Runs in the background while the build runs, and reports in the last
174 # step: neither result hides the other.
175 - name: Start typecheck
176 id: typecheck
177 if: ${{ !cancelled() && steps.electron.outcome == 'success' && needs.changes.outputs.typecheck != 'false' }}
178 run: |
179 (
180 set +e
181 vpr typecheck
182 echo "$?" > "$RUNNER_TEMP/typecheck-status"
183 ) > "$RUNNER_TEMP/typecheck.log" 2>&1 < /dev/null &
184​
185 - name: Finish installing browser secret helper build libraries
186 id: apt
187 if: ${{ !cancelled() && needs.changes.outputs.build != 'false' }}
188 timeout-minutes: 5
189 run: |
190 until test -s "$RUNNER_TEMP/apt-status"; do sleep 1; done
191 cat "$RUNNER_TEMP/apt.log"
192 exit "$(cat "$RUNNER_TEMP/apt-status")"
193​
194 - name: Build desktop pipeline
195 id: build
196 if: ${{ !cancelled() && steps.electron.outcome == 'success' && steps.apt.outcome == 'success' && needs.changes.outputs.build != 'false' }}
197 run: vp run build:desktop
198​
199 - name: Verify preload bundle output
200 if: ${{ !cancelled() && steps.build.outcome == 'success' }}
201 run: node apps/desktop/scripts/verify-preload-bundle.mjs
202​
203 # The release launch check, run unpackaged against the same old-state fixture, for
204 # startup and sidebar changes. Packaging and signing stay release-only.
205 - name: Open the seeded thread in the built desktop app
206 id: desktop_launch
207 if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.desktop_launch != 'false' }}
208 timeout-minutes: 4
209 run: |
210 electron=$(node -p "require('./apps/desktop/node_modules/electron')")
211 xvfb-run -a node scripts/smoke-desktop-app.ts --executable "$electron" --app apps/desktop \
212 --artifacts-dir "$RUNNER_TEMP/desktop-launch-check"
213​
214 - name: Upload desktop launch check
215 if: ${{ failure() && steps.desktop_launch.outcome == 'failure' }}
216 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
217 with:
218 name: desktop-launch-check
219 path: ${{ runner.temp }}/desktop-launch-check
220 if-no-files-found: ignore
221​
222 # Browser tests run source modules; this boots the production chunk graph. The
223 # desktop build above already produced apps/web/dist, and the client waits about
224 # 15s for a session that never comes, so this runs here instead of in Test Web,
225 # which gates the whole run. Chromium comes from the cache Test Web shares.
226 - name: Resolve Playwright version
227 id: playwright_version
228 if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
229 run: echo "version=$(node -p "require('./apps/web/node_modules/playwright/package.json').version")" >> "$GITHUB_OUTPUT"
230​
231 - name: Cache Chromium
232 id: chromium_cache
233 if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
234 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
235 with:
236 path: ~/.cache/ms-playwright
237 key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}
238​
239 - name: Install Chromium for the built web client boot check
240 if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' && steps.chromium_cache.outputs.cache-hit != 'true' }}
241 run: node apps/web/node_modules/playwright/cli.js install --only-shell chromium
242​
243 - name: Install missing Chromium system dependencies
244 if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
245 run: |
246 browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
247 test -n "$browser"
248 if ldd "$browser" | grep '=> not found'; then
249 node apps/web/node_modules/playwright/cli.js install-deps chromium
250 fi
251​
252 - name: Boot the built web client
253 if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
254 timeout-minutes: 3
255 run: node apps/web/scripts/verify-built-boot.ts
256​
257 - name: Typecheck
258 if: ${{ !cancelled() && steps.typecheck.outcome == 'success' }}
259 timeout-minutes: 5
260 run: |
261 until test -s "$RUNNER_TEMP/typecheck-status"; do sleep 1; done
262 cat "$RUNNER_TEMP/typecheck.log"
263 exit "$(cat "$RUNNER_TEMP/typecheck-status")"
264​
265 # Everything except `t3` (apps/server) and the web app, which has its own job
266 # below. `--parallel` drops the package dependency ordering that `vp run`
267 # applies by default: these `test` tasks declare no `dependsOn` and resolve
268 # workspace deps from source, so ordering only bought us idle runners between
269 # dependency layers. The concurrency limit stays at the default 4 so peak load
270 # per runner is unchanged.
271 test:
272 permissions:
273 contents: read
274 id-token: write
275 name: Test
276 needs: changes
277 if: ${{ !cancelled() && needs.changes.outputs.test != 'false' }}
278 runs-on: blacksmith-8vcpu-ubuntu-2404
279 timeout-minutes: 10
280 steps:
281 - name: Checkout
282 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
283 with:
284 sparse-checkout: |
285 /*
286 !/.repos/
287 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
288 sparse-checkout-cone-mode: false
289​
290 - uses: ./.github/actions/setup-apt-mirrors
291​
292 # The desktop tests compile the browser secret helper. The install runs in
293 # the background while Vite+ installs; the step before the tests waits for it.
294 - name: Start installing browser secret helper build libraries
295 run: |
296 # Fetch only the indexes these main and universe packages need. The full set is ~42 MB,
297 # which a slow mirror can stretch past the wait below.
298 sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \
299 /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
300 (
301 set +e
302 sudo apt-get update -o Acquire::Languages=none \
303 -o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \
304 -o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false &&
305 sudo apt-get install -y libsecret-1-dev pkg-config
306 echo "$?" > "$RUNNER_TEMP/apt-status"
307 ) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
308​
309 - name: Setup Vite+
310 uses: ./.github/actions/setup-ci-deps
311 with:
312 task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
313​
314 # electron's install.js (run by ensure:electron) reads this cache before it
315 # downloads from GitHub. The key changes only with the Electron version.
316 - name: Cache Electron download
317 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
318 with:
319 path: ~/.cache/electron
320 key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }}
321​
322 - name: Ensure Electron runtime is installed
323 run: vp run --filter @t3tools/desktop ensure:electron
324​
325 - name: Test preview artifact validation
326 run: python3 -B .github/scripts/stage-preview-bundle.test.py
327​
328 - name: Test release checks
329 run: node --test .github/scripts/check-nightly-release.test.cjs .github/scripts/vercel-deployment-output.test.cjs .github/scripts/mobile-preview-comment.test.cjs .github/scripts/test-delta.test.cjs
330​
331 - name: Finish installing browser secret helper build libraries
332 timeout-minutes: 5
333 run: |
334 until test -s "$RUNNER_TEMP/apt-status"; do sleep 1; done
335 cat "$RUNNER_TEMP/apt.log"
336 exit "$(cat "$RUNNER_TEMP/apt-status")"
337​
338 # `changes` lists the affected packages, or nothing when all of them are.
339 - name: Test
340 env:
341 TEST_PACKAGES: ${{ needs.changes.outputs.test_packages }}
342 run: |
343 set -o pipefail
344 filters=(--filter '!t3' --filter '!@t3tools/monorepo' --filter '!@t3tools/web')
345 if [[ -n "$TEST_PACKAGES" ]]; then
346 filters=()
347 for package in $TEST_PACKAGES; do filters+=(--filter "$package"); done
348 fi
349 # The runner's trace writer can crash on surrogate pairs in test output.
350 vp run --parallel --concurrency-limit 4 "${filters[@]}" test 2>&1 | python3 -u -c '
351 import sys
352 for line in sys.stdin:
353 sys.stdout.write(line.encode("ascii", "backslashreplace").decode("ascii"))
354 '
355​
356 # The web suite is the largest outside the server. In the shared Test job it
357 # started last and ran alone for about 40s after everything else finished.
358 # Browser tests are bound by the Vite server transforming modules for each
359 # file, not by Chromium, so a second runner (and Vite server) halves the job
360 # that gated every run. release.yml mirrors this test, test_web, and test_server split.
361 test_web:
362 permissions:
363 contents: read
364 id-token: write
365 name: Test Web ${{ matrix.shard }}
366 needs: changes
367 if: ${{ !cancelled() && needs.changes.outputs.test_web != 'false' }}
368 runs-on: blacksmith-8vcpu-ubuntu-2404
369 timeout-minutes: 10
370 strategy:
371 fail-fast: false
372 matrix:
373 shard: [1, 2]
374 steps:
375 - name: Checkout
376 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
377 with:
378 sparse-checkout: |
379 /*
380 !/.repos/
381 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
382 sparse-checkout-cone-mode: false
383​
384 - name: Setup Vite+
385 uses: ./.github/actions/setup-ci-deps
386 with:
387 task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
388​
389 # The browser project runs React tests in headless Chromium. Same cache key as
390 # test_server, so both jobs share one downloaded browser.
391 - name: Resolve Playwright version
392 id: playwright_version
393 run: echo "version=$(node -p "require('./apps/web/node_modules/playwright/package.json').version")" >> "$GITHUB_OUTPUT"
394​
395 - name: Cache Chromium
396 id: chromium_cache
397 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
398 with:
399 path: ~/.cache/ms-playwright
400 key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}
401​
402 - name: Install Chromium for web browser tests
403 if: steps.chromium_cache.outputs.cache-hit != 'true'
404 run: node apps/web/node_modules/playwright/cli.js install --only-shell chromium
405​
406 - name: Install missing Chromium system dependencies
407 run: |
408 browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
409 test -n "$browser"
410 if ldd "$browser" | grep '=> not found'; then
411 node apps/web/node_modules/playwright/cli.js install-deps chromium
412 fi
413​
414 - name: Test
415 run: vp run --filter @t3tools/web test --shard ${{ matrix.shard }}/${{ strategy.job-total }}
416​
417 # apps/server sets `fileParallelism: false`, so its test files run strictly
418 # one at a time. Sharding spreads them over separate runners instead of
419 # separate workers, so no two server test files ever share a machine and the
420 # isolation that flag buys is preserved exactly. One file at a time keeps
421 # about 1.5 cores busy, so 4 vCPU runners lose nothing against 8. The shards
422 # split files by recorded duration (apps/server/src/testUtils/weightedShardSequencer.ts).
423 # The shards are the longest jobs, so they gate themselves instead of waiting
424 # ~15s for `changes`: a shard the change cannot affect stops after checkout.
425 test_server:
426 permissions:
427 contents: read
428 id-token: write
429 name: Test Server ${{ matrix.shard }}
430 runs-on: blacksmith-4vcpu-ubuntu-2404
431 # Shards take 2-5 minutes. When the shared task cache throttles reads, each
432 # failed fetch stalls vp for 15-90s, which pushed shards past 10 minutes.
433 timeout-minutes: 20
434 strategy:
435 fail-fast: false
436 matrix:
437 shard: [1, 2, 3, 4, 5, 6, 7, 8]
438 steps:
439 # Depth 2 brings the base commit the change is diffed against.
440 - name: Checkout
441 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
442 with:
443 fetch-depth: 2
444 sparse-checkout: |
445 /*
446 !/.repos/
447 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
448 sparse-checkout-cone-mode: false
449​
450 - name: Detect affected jobs
451 id: changes
452 uses: ./.github/actions/ci-changes
453​
454 - name: Setup Vite+
455 if: steps.changes.outputs.test_server != 'false'
456 uses: ./.github/actions/setup-ci-deps
457 with:
458 task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
459​
460 # No Electron setup here: `t3` (apps/server) has no Electron dependency
461 # and none of its tests touch the runtime. Only the non-server `test`
462 # job, which covers @t3tools/desktop, needs the download.
463 # Cache the pinned browser and skip apt unless shared libraries are missing.
464 - name: Resolve Playwright version
465 id: playwright_version
466 if: steps.changes.outputs.test_server != 'false'
467 run: echo "version=$(node -p "require('./apps/server/node_modules/playwright-core/package.json').version")" >> "$GITHUB_OUTPUT"
468​
469 - name: Cache Chromium
470 id: chromium_cache
471 if: steps.changes.outputs.test_server != 'false'
472 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
473 with:
474 path: ~/.cache/ms-playwright
475 key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}
476​
477 - name: Install Chromium for server browser integration tests
478 if: steps.changes.outputs.test_server != 'false' && steps.chromium_cache.outputs.cache-hit != 'true'
479 run: node apps/server/node_modules/playwright-core/cli.js install --only-shell chromium
480​
481 - name: Install missing Chromium system dependencies
482 if: steps.changes.outputs.test_server != 'false'
483 run: |
484 browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
485 test -n "$browser"
486 if ldd "$browser" | grep '=> not found'; then
487 node apps/server/node_modules/playwright-core/cli.js install-deps chromium
488 fi
489​
490 # Each server test file runs in its own Vitest process, and a fresh install starts
491 # Vitest's content-keyed module cache empty, so every file re-transformed its imports.
492 # Only pushes to main save it, so pull requests never evict each other's entries.
493 - name: Restore Vitest module cache
494 if: steps.changes.outputs.test_server != 'false'
495 uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
496 with:
497 path: node_modules/.vitest-cache
498 key: vitest-modules-server-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
499 restore-keys: vitest-modules-server-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
500​
501 - name: Test
502 if: steps.changes.outputs.test_server != 'false'
503 env:
504 T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md
505 T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json
506 T3CODE_TRANSFER_BUDGET_SCENARIOS_PATH: ${{ runner.temp }}/thread-transfer-scenarios.json
507 # One process per file spends ~2s starting before its tests run; one file per vCPU overlaps that.
508 run: >-
509 vp run test:cached -- --shard ${{ matrix.shard }}/${{ strategy.job-total }} --concurrency 4
510 --uncached apps/server/integration/transferBudgetV2.integration.test.ts
511 --verify-hits 1 apps/server
512​
513 - name: Save Vitest module cache
514 if: github.event_name == 'push' && steps.changes.outputs.test_server != 'false'
515 uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
516 with:
517 path: node_modules/.vitest-cache
518 key: vitest-modules-server-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
519​
520 # integration/transferBudgetV2.integration.test.ts writes the report; exactly one shard
521 # produces these files. Gating the upload on their presence keeps a
522 # single `thread-transfer-results` artifact per run, which is the name
523 # thread-transfer-report.yml resolves.
524 - name: Detect transfer budget report
525 id: transfer_budget
526 if: always()
527 run: |
528 if test -f "${{ runner.temp }}/thread-transfer-result.json"; then
529 echo "present=true" >> "$GITHUB_OUTPUT"
530 else
531 echo "present=false" >> "$GITHUB_OUTPUT"
532 fi
533​
534 - name: Publish transfer budget report
535 if: always() && steps.transfer_budget.outputs.present == 'true'
536 run: |
537 if test -f "${{ runner.temp }}/t3code-transfer-budget.md"; then
538 tee -a "$GITHUB_STEP_SUMMARY" < "${{ runner.temp }}/t3code-transfer-budget.md"
539 else
540 echo "Transfer budget report was not produced." >> "$GITHUB_STEP_SUMMARY"
541 fi
542​
543 - name: Upload thread transfer result
544 if: always() && steps.transfer_budget.outputs.present == 'true'
545 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
546 with:
547 name: thread-transfer-results
548 path: |
549 ${{ runner.temp }}/thread-transfer-result.json
550 ${{ runner.temp }}/thread-transfer-scenarios.json
551 if-no-files-found: ignore
552 retention-days: 30
553​
554 # Split out of Check and Test: both paid ~7-9s to install a Rust toolchain
555 # for checks that take under 3s, on the critical path of every PR.
556 rust:
557 name: Rust
558 needs: changes
559 if: ${{ !cancelled() && needs.changes.outputs.rust != 'false' }}
560 runs-on: blacksmith-4vcpu-ubuntu-2404
561 timeout-minutes: 10
562 steps:
563 - name: Checkout
564 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
565 with:
566 sparse-checkout: |
567 /*
568 !/.repos/
569 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
570 sparse-checkout-cone-mode: false
571​
572 - name: Setup Rust
573 uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
574 with:
575 toolchain: stable
576 components: rustfmt
577​
578 - name: Check Rust formatting
579 run: |
580 for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do
581 cargo fmt --manifest-path "native/$crate/Cargo.toml" -- --check
582 done
583​
584 - name: Test Rust crates
585 run: |
586 for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do
587 cargo test --locked --manifest-path "native/$crate/Cargo.toml"
588 done
589​
590 mobile_native_static_analysis:
591 name: Mobile Native Static Analysis
592 needs: changes
593 if: ${{ !cancelled() && needs.changes.outputs.mobile_native != 'false' }}
594 runs-on: blacksmith-6vcpu-macos-26
595 timeout-minutes: 10
596 steps:
597 - name: Checkout
598 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
599 with:
600 sparse-checkout: |
601 /*
602 !/.repos/
603 sparse-checkout-cone-mode: false
604​
605 - name: Setup Vite+
606 uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0
607 with:
608 node-version-file: package.json
609 cache: true
610 run-install: |
611 args:
612 - --filter=@t3tools/scripts...
613​
614 - name: Install mobile native static analysis tools
615 run: brew bundle install --file apps/mobile/Brewfile
616​
617 - name: Lint mobile native sources
618 run: vp run lint:mobile
619​
620 release_smoke:
621 permissions:
622 contents: read
623 id-token: write
624 name: Release Smoke
625 needs: changes
626 if: ${{ !cancelled() && needs.changes.outputs.release_smoke != 'false' }}
627 runs-on: blacksmith-8vcpu-ubuntu-2404
628 timeout-minutes: 15
629 steps:
630 - name: Checkout
631 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
632 with:
633 sparse-checkout: |
634 /*
635 !/.repos/
636 !/apps/mobile/modules/t3-terminal/Vendor/**/*.a
637 sparse-checkout-cone-mode: false
638​
639 - name: Setup Vite+
640 uses: ./.github/actions/setup-ci-deps
641 with:
642 task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
643​
644 - name: Exercise release-only workflow steps
645 run: node scripts/release-smoke.ts
646​
647 # Branch protection requires this job by its name, `Check`. It fails when any
648 # other job fails, is cancelled, or is skipped without a reason, so splitting
649 # or resharding jobs needs no ruleset change. Add every new job to `needs`.
650 # `always()` rather than `!cancelled()`: a skipped job reports success to
651 # branch protection, so a cancelled run must still produce a failing check.
652 check:
653 name: Check
654 if: ${{ always() }}
655 needs:
656 [
657 changes,
658 lint,
659 build,
660 test,
661 test_web,
662 test_server,
663 rust,
664 mobile_native_static_analysis,
665 release_smoke,
666 ]
667 runs-on: blacksmith-2vcpu-ubuntu-2404
668 timeout-minutes: 5
669 steps:
670 - name: Require every job to pass
671 env:
672 RESULTS: ${{ toJSON(needs) }}
673 run: |
674 echo "$RESULTS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
675 # A job may skip only when `changes` said every gate it represents was
676 # not needed. Gates are keyed by job name, except merged Build and the
677 # macOS lint.
678 echo "$RESULTS" | jq -e '.changes.outputs as $gates
679 | {build: ["build", "typecheck", "desktop_launch"], mobile_native_static_analysis: ["mobile_native"]} as $renamed
680 | all(to_entries[];
681 .value.result == "success"
682 or (.value.result == "skipped"
683 and (($renamed[.key] // [.key]) | all(.[]; $gates[.] == "false"))))' > /dev/null
684​
685 # thread-transfer-report.yml reads this artifact after CI, so a run whose
686 # server tests ran must have produced it.
687 - name: Require transfer measurements
688 if: needs.test_server.result == 'success' && needs.changes.outputs.test_server != 'false'
689 uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
690 with:
691 name: thread-transfer-results
692 path: ${{ runner.temp }}/thread-transfer