Skip to content

Commit 683180b

Browse files
authored
Centralize test timings to fix shard divergence in CI (#90672)
## Summary For context we discovered jobs were not reliably always running in #90668 as addapters + turbopack group ran more tests than just turbopack group. - Each sharded test sub-job independently fetched test timings from KV via a turbo task. Because turbo cache keys can vary across job groups and KV can return slightly different data at different times, the sharding algorithm produced different test-to-shard assignments across groups — causing some tests to be missing entirely and others to run in multiple shards. - Adds a `--require-timings` flag to `run-tests.js` that fails loudly if `test-timings.json` can't be loaded from disk, preventing silent fallback to KV or round-robin. - Adds a `testTimingsArtifact` input to `build_reusable.yml` that downloads a pre-built timings artifact instead of fetching via turbo. When unset (default), existing behavior is preserved. - In both `test_e2e_deploy_release.yml` and `build_and_test.yml`, timings are now fetched once in a setup job, uploaded as a GitHub artifact, and downloaded by all sub-jobs — guaranteeing identical shard assignments across all job groups. ## Test plan - [ ] Verify `build_and_test.yml` jobs pick up the shared `test-timings` artifact - [ ] Verify `test_e2e_deploy_release.yml` deploy jobs pick up the shared `test-timings` artifact - [ ] Verify workflows that don't pass `testTimingsArtifact` (e.g. `integration_tests_reusable.yml`, `pull_request_stats.yml`) still use the turbo fallback path unchanged
1 parent 39e8fb2 commit 683180b

4 files changed

Lines changed: 246 additions & 20 deletions

File tree

.github/workflows/build_and_test.yml

Lines changed: 167 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,53 @@ jobs:
9494
stepName: 'build-next'
9595
secrets: inherit
9696

97+
fetch-test-timings:
98+
name: fetch test timings
99+
runs-on: ubuntu-latest
100+
needs: ['changes']
101+
if: ${{ needs.changes.outputs.docs-only == 'false' }}
102+
steps:
103+
- name: Setup Node.js
104+
uses: actions/setup-node@v4
105+
with:
106+
node-version: ${{ env.NODE_LTS_VERSION }}
107+
check-latest: true
108+
109+
- name: Setup pnpm
110+
run: |
111+
112+
corepack enable
113+
114+
- name: Checkout
115+
uses: actions/checkout@v4
116+
with:
117+
fetch-depth: 25
118+
119+
- name: Install dependencies
120+
run: pnpm install
121+
122+
- name: Fetch test timings
123+
run: node run-tests.js --timings --write-timings -g 1/1
124+
continue-on-error: true
125+
env:
126+
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
127+
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
128+
129+
- name: Ensure test timings file exists
130+
run: |
131+
if [ ! -f test-timings.json ]; then
132+
echo "No timings fetched, creating empty timings file"
133+
echo '{}' > test-timings.json
134+
fi
135+
136+
- name: Upload test timings
137+
uses: actions/upload-artifact@v4
138+
with:
139+
name: test-timings
140+
path: test-timings.json
141+
retention-days: 1
142+
if-no-files-found: error
143+
97144
lint:
98145
name: lint
99146
needs: ['build-next']
@@ -236,7 +283,14 @@ jobs:
236283

237284
test-turbopack-dev:
238285
name: test turbopack dev
239-
needs: ['optimize-ci', 'changes', 'build-next', 'build-native']
286+
needs:
287+
[
288+
'optimize-ci',
289+
'changes',
290+
'build-next',
291+
'build-native',
292+
'fetch-test-timings',
293+
]
240294
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
241295

242296
strategy:
@@ -261,13 +315,22 @@ jobs:
261315
node run-tests.js \
262316
--test-pattern '^(test\/(development|e2e))/.*\.test\.(js|jsx|ts|tsx)$' \
263317
--timings \
318+
--require-timings \
264319
-g ${{ matrix.group }}
320+
testTimingsArtifact: 'test-timings'
265321
stepName: 'test-turbopack-dev-react-${{ matrix.react }}-${{ matrix.group }}'
266322
secrets: inherit
267323

268324
test-turbopack-integration:
269325
name: test turbopack integration
270-
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
326+
needs:
327+
[
328+
'optimize-ci',
329+
'changes',
330+
'build-native',
331+
'build-next',
332+
'fetch-test-timings',
333+
]
271334
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
272335

273336
strategy:
@@ -304,14 +367,23 @@ jobs:
304367
305368
node run-tests.js \
306369
--timings \
370+
--require-timings \
307371
-g ${{ matrix.group }} \
308372
--type integration
373+
testTimingsArtifact: 'test-timings'
309374
stepName: 'test-turbopack-integration-react-${{ matrix.react }}-${{ matrix.group }}'
310375
secrets: inherit
311376

312377
test-turbopack-production:
313378
name: test turbopack production
314-
needs: ['optimize-ci', 'changes', 'build-next', 'build-native']
379+
needs:
380+
[
381+
'optimize-ci',
382+
'changes',
383+
'build-next',
384+
'build-native',
385+
'fetch-test-timings',
386+
]
315387
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
316388

317389
strategy:
@@ -334,13 +406,21 @@ jobs:
334406
export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true
335407
export RUST_BACKTRACE=1
336408
337-
node run-tests.js --timings -g ${{ matrix.group }} --type production
409+
node run-tests.js --timings --require-timings -g ${{ matrix.group }} --type production
410+
testTimingsArtifact: 'test-timings'
338411
stepName: 'test-turbopack-production-react-${{ matrix.react }}-${{ matrix.group }}'
339412
secrets: inherit
340413

341414
test-rspack-dev:
342415
name: test rspack dev
343-
needs: ['optimize-ci', 'changes', 'build-next', 'build-native']
416+
needs:
417+
[
418+
'optimize-ci',
419+
'changes',
420+
'build-next',
421+
'build-native',
422+
'fetch-test-timings',
423+
]
344424
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.rspack == 'true' }}
345425
strategy:
346426
fail-fast: false
@@ -370,13 +450,22 @@ jobs:
370450
node run-tests.js \
371451
--test-pattern '^(test\/(development|e2e))/.*\.test\.(js|jsx|ts|tsx)$' \
372452
--timings \
453+
--require-timings \
373454
-g ${{ matrix.group }}
455+
testTimingsArtifact: 'test-timings'
374456
stepName: 'test-rspack-dev-react-${{ matrix.react }}-${{ matrix.group }}'
375457
secrets: inherit
376458

377459
test-rspack-integration:
378460
name: test rspack development integration
379-
needs: ['optimize-ci', 'changes', 'build-next', 'build-native']
461+
needs:
462+
[
463+
'optimize-ci',
464+
'changes',
465+
'build-next',
466+
'build-native',
467+
'fetch-test-timings',
468+
]
380469
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.rspack == 'true' }}
381470
strategy:
382471
fail-fast: false
@@ -406,14 +495,23 @@ jobs:
406495
407496
node run-tests.js \
408497
--timings \
498+
--require-timings \
409499
-g ${{ matrix.group }} \
410500
--type integration
501+
testTimingsArtifact: 'test-timings'
411502
stepName: 'test-rspack-integration-react-${{ matrix.react }}-${{ matrix.group }}'
412503
secrets: inherit
413504

414505
test-rspack-production:
415506
name: test rspack production
416-
needs: ['optimize-ci', 'changes', 'build-next', 'build-native']
507+
needs:
508+
[
509+
'optimize-ci',
510+
'changes',
511+
'build-next',
512+
'build-native',
513+
'fetch-test-timings',
514+
]
417515
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.rspack == 'true' }}
418516
strategy:
419517
fail-fast: false
@@ -441,13 +539,21 @@ jobs:
441539
# tests, so it's applicable to rspack
442540
export TURBOPACK_BUILD=1
443541
444-
node run-tests.js --timings -g ${{ matrix.group }} --type production
542+
node run-tests.js --timings --require-timings -g ${{ matrix.group }} --type production
543+
testTimingsArtifact: 'test-timings'
445544
stepName: 'test-rspack-production-react-${{ matrix.react }}-${{ matrix.group }}'
446545
secrets: inherit
447546

448547
test-rspack-production-integration:
449548
name: test rspack production integration
450-
needs: ['optimize-ci', 'changes', 'build-next', 'build-native']
549+
needs:
550+
[
551+
'optimize-ci',
552+
'changes',
553+
'build-next',
554+
'build-native',
555+
'fetch-test-timings',
556+
]
451557
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.rspack == 'true' }}
452558
strategy:
453559
fail-fast: false
@@ -477,8 +583,10 @@ jobs:
477583
478584
node run-tests.js \
479585
--timings \
586+
--require-timings \
480587
-g ${{ matrix.group }} \
481588
--type integration
589+
testTimingsArtifact: 'test-timings'
482590
stepName: 'test-rspack-production-integration-react-${{ matrix.react }}-${{ matrix.group }}'
483591
secrets: inherit
484592

@@ -719,7 +827,14 @@ jobs:
719827

720828
test-dev: # TODO: rename to include webpack
721829
name: test dev
722-
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
830+
needs:
831+
[
832+
'optimize-ci',
833+
'changes',
834+
'build-native',
835+
'build-next',
836+
'fetch-test-timings',
837+
]
723838
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
724839

725840
strategy:
@@ -741,8 +856,10 @@ jobs:
741856
742857
node run-tests.js \
743858
--timings \
859+
--require-timings \
744860
-g ${{ matrix.group }} \
745861
--type development
862+
testTimingsArtifact: 'test-timings'
746863
stepName: 'test-dev-react-${{ matrix.react }}-${{ matrix.group }}'
747864
secrets: inherit
748865

@@ -838,7 +955,14 @@ jobs:
838955

839956
test-prod:
840957
name: test prod
841-
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
958+
needs:
959+
[
960+
'optimize-ci',
961+
'changes',
962+
'build-native',
963+
'build-next',
964+
'fetch-test-timings',
965+
]
842966
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
843967

844968
strategy:
@@ -858,13 +982,21 @@ jobs:
858982
export NEXT_TEST_REACT_VERSION="${{ matrix.react }}"
859983
export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true
860984
861-
node run-tests.js --timings -g ${{ matrix.group }} --type production
985+
node run-tests.js --timings --require-timings -g ${{ matrix.group }} --type production
986+
testTimingsArtifact: 'test-timings'
862987
stepName: 'test-prod-react-${{ matrix.react }}-${{ matrix.group }}'
863988
secrets: inherit
864989

865990
test-integration:
866991
name: test integration
867-
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
992+
needs:
993+
[
994+
'optimize-ci',
995+
'changes',
996+
'build-native',
997+
'build-next',
998+
'fetch-test-timings',
999+
]
8681000
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
8691001

8701002
strategy:
@@ -900,8 +1032,10 @@ jobs:
9001032
9011033
node run-tests.js \
9021034
--timings \
1035+
--require-timings \
9031036
-g ${{ matrix.group }} \
9041037
--type integration
1038+
testTimingsArtifact: 'test-timings'
9051039
stepName: 'test-integration-${{ matrix.group }}-react-${{ matrix.react }}'
9061040
secrets: inherit
9071041

@@ -958,7 +1092,14 @@ jobs:
9581092

9591093
test-cache-components-dev:
9601094
name: test cache components dev
961-
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
1095+
needs:
1096+
[
1097+
'optimize-ci',
1098+
'changes',
1099+
'build-native',
1100+
'build-next',
1101+
'fetch-test-timings',
1102+
]
9621103
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
9631104

9641105
strategy:
@@ -976,14 +1117,23 @@ jobs:
9761117
9771118
node run-tests.js \
9781119
--timings \
1120+
--require-timings \
9791121
-g ${{ matrix.group }} \
9801122
--type development
1123+
testTimingsArtifact: 'test-timings'
9811124
stepName: 'test-cache-components-dev-${{ matrix.group }}'
9821125
secrets: inherit
9831126

9841127
test-cache-components-prod:
9851128
name: test cache components prod
986-
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
1129+
needs:
1130+
[
1131+
'optimize-ci',
1132+
'changes',
1133+
'build-native',
1134+
'build-next',
1135+
'fetch-test-timings',
1136+
]
9871137
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
9881138

9891139
strategy:
@@ -1001,8 +1151,10 @@ jobs:
10011151
10021152
node run-tests.js \
10031153
--timings \
1154+
--require-timings \
10041155
-g ${{ matrix.group }} \
10051156
--type production
1157+
testTimingsArtifact: 'test-timings'
10061158
stepName: 'test-cache-components-prod-${{ matrix.group }}'
10071159
secrets: inherit
10081160

.github/workflows/build_reusable.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ on:
8080
type: string
8181
default: ''
8282

83+
testTimingsArtifact:
84+
description: 'Name of an uploaded artifact containing test-timings.json. When set, download it instead of fetching via turbo.'
85+
required: false
86+
type: string
87+
default: ''
88+
8389
browser:
8490
description: 'Browser to use for tests'
8591
required: false
@@ -307,7 +313,24 @@ jobs:
307313
- run: pnpm playwright install --with-deps ${{ inputs.browser }}
308314
if: ${{ inputs.skipInstallBuild != 'yes' }}
309315

310-
- run: pnpm dlx turbo@${TURBO_VERSION} run get-test-timings -- --build ${{ github.sha }}
316+
- name: Download pre-built test timings
317+
if: ${{ inputs.testTimingsArtifact != '' }}
318+
uses: actions/download-artifact@v4
319+
with:
320+
name: ${{ inputs.testTimingsArtifact }}
321+
322+
- name: Verify test timings
323+
if: ${{ inputs.testTimingsArtifact != '' }}
324+
run: |
325+
if [ ! -f test-timings.json ]; then
326+
echo "::error::test-timings.json not found"
327+
exit 1
328+
fi
329+
echo "Test timings loaded ($(wc -c < test-timings.json) bytes)"
330+
331+
- name: Fetch test timings via turbo
332+
if: ${{ inputs.testTimingsArtifact == '' }}
333+
run: pnpm dlx turbo@${TURBO_VERSION} run get-test-timings -- --build ${{ github.sha }}
311334

312335
- run: ${{ inputs.afterBuild }}
313336
# defaults.run.shell sets a stronger options (`-leo pipefail`)

0 commit comments

Comments
 (0)