@@ -12,13 +12,71 @@ name: unittest
1212permissions :
1313 contents : read
1414
15+ # Configurable global environment variables for batching
16+ env :
17+ BATCH_SIZE : 10
18+ TEST_ALL_PACKAGES : " true" # Set to "false" to only run tests for packages with a git diff
19+
1520jobs :
21+ # Dynamic package discovery job to calculate required matrix size automatically
22+ discover-packages :
23+ runs-on : ubuntu-latest
24+ outputs :
25+ batch-indices : ${{ steps.set-matrix.outputs.indices }}
26+ steps :
27+ - name : Checkout
28+ uses : actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
29+ with :
30+ persist-credentials : false
31+ - name : Generate Batch Indices
32+ id : set-matrix
33+ run : |
34+ # Testing all monorepo packages sequentially on a single runner node is
35+ # too slow and creates a severe CI bottleneck as the repository expands.
36+ #
37+ # To scale efficiently, we chunk the workload into parallel batch slices.
38+ # Instead of using a fixed, hardcoded matrix array (which risks silently
39+ # skipping newly added packages if the repository outgrows the array size),
40+ # this step dynamically audits the 'packages/' directory at runtime.
41+ #
42+ # It calculates exactly how many concurrent runners are required based on
43+ # the current repository size and the configured BATCH_SIZE variable,
44+ # ensuring 100% test coverage with zero manual YAML maintenance.
45+
46+ # 1. Count the number of total directories matching the 'packages/*' pattern.
47+ # Redirect stderr to /dev/null so empty repos do not print unneeded errors.
48+ TOTAL_PACKAGES=$(ls -d packages/*/ 2>/dev/null | wc -l | tr -d ' ')
49+
50+ # 2. Safety fallback: If no packages are detected, assign a single slice index [0]
51+ # so subsequent matrix-dependent jobs do not break or fail validation on an empty matrix.
52+ if [ "$TOTAL_PACKAGES" -eq 0 ]; then
53+ echo "indices=[0]" >> "$GITHUB_OUTPUT"
54+ exit 0
55+ fi
56+
57+ # 3. Calculate the number of batches required using ceiling division: ceil(TOTAL_PACKAGES / BATCH_SIZE).
58+ # The formula ((A + B - 1) / B) ensures integer division rounds up if there's any remaining package leftover.
59+ # Example: 251 packages with a batch size of 10 gives ((251 + 10 - 1) / 10) = 260 / 10 = 26 batches.
60+ NUM_BATCHES=$(( (TOTAL_PACKAGES + ${{ env.BATCH_SIZE }} - 1) / ${{ env.BATCH_SIZE }} ))
61+
62+ # 4. Generate a zero-indexed sequence from 0 to (NUM_BATCHES - 1).
63+ # Use jq to securely parse the raw numbers and compile them into a compacted JSON array string.
64+ # Example output format: [0,1,2,3,...,25]
65+ INDICES=$(seq 0 $((NUM_BATCHES - 1)) | jq -R . | jq -s -c .)
66+
67+ # 5. Output the finished JSON string to the GitHub environment outputs pipeline.
68+ # This will safely feed directly into the execution matrix downstream.
69+ echo "indices=${INDICES}" >> "$GITHUB_OUTPUT"
70+
1671 unit :
72+ name : " unit-run (${{ matrix.python }}, Batch ${{ matrix.batch-index }})"
1773 runs-on : ubuntu-22.04
74+ needs : discover-packages
1875 strategy :
19- fail-fast : true
2076 matrix :
21- python : ['3.9', '3.10', "3.11", "3.12", "3.13", "3.14"]
77+ python : ['3.9', '3.10', "3.11", "3.12", "3.13", "3.14", "3.15"]
78+ # Dynamically scales to fit every package perfectly without hardcoding array indices
79+ batch-index : ${{ fromJson(needs.discover-packages.outputs.batch-indices) }}
2280 steps :
2381 - name : Checkout
2482 uses : actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
@@ -39,23 +97,41 @@ jobs:
3997 - name : Run unit tests
4098 env :
4199 COVERAGE_FILE : ${{ github.workspace }}/.coverage-${{ matrix.python }}
42- BUILD_TYPE : presubmit
100+ # Dynamically set BUILD_TYPE to an empty string to skip the diff calculation if TEST_ALL_PACKAGES is true
101+ BUILD_TYPE : ${{ env.TEST_ALL_PACKAGES == 'true' && '' || 'presubmit' }}
43102 TARGET_BRANCH : ${{ github.base_ref || github.event.merge_group.base_ref }}
44103 TEST_TYPE : unit
45104 PY_VERSION : ${{ matrix.python }}
46105 run : |
47- ci/run_conditional_tests.sh
106+ # Gather all packages in alphabetical order
107+ ALL_PACKAGES=($(ls -d packages/*/ | sort))
108+ TOTAL_PACKAGES=${#ALL_PACKAGES[@]}
109+
110+ # Determine this runner's slice window
111+ START_INDEX=$(( ${{ matrix.batch-index }} * ${{ env.BATCH_SIZE }} ))
112+
113+ if [ $START_INDEX -ge $TOTAL_PACKAGES ]; then
114+ exit 0
115+ fi
116+
117+ BATCH_PACKAGES=("${ALL_PACKAGES[@]:$START_INDEX:${{ env.BATCH_SIZE }}}")
118+
119+ # Strip trailing slashes to pass down directly into ci/run_conditional_tests.sh
120+ subdirs=("${BATCH_PACKAGES[@]%/}")
121+
122+ ci/run_conditional_tests.sh "${subdirs[@]}"
48123 - name : Upload coverage results
49124 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
50125 with :
51- name : coverage-artifact-${{ matrix.python }}
126+ # Appended batch-index to separate parallel coverage uploads cleanly
127+ name : coverage-artifact-${{ matrix.python }}-${{ matrix.batch-index }}
52128 path : .coverage-${{ matrix.python }}
53129 include-hidden-files : true
54130
55131 cover :
56132 runs-on : ubuntu-latest
57133 needs :
58- - unit
134+ - unit
59135 steps :
60136 - name : Checkout
61137 uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@@ -167,3 +243,22 @@ jobs:
167243 echo "This usually means the unit tests did not run or failed to upload their coverage files."
168244 exit 1
169245 fi
246+
247+ unittest-runtime-result :
248+ name : " unit (${{ matrix.python }})"
249+ needs : unit
250+ if : always()
251+ strategy :
252+ matrix :
253+ python : ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15']
254+ runs-on : ubuntu-latest
255+ steps :
256+ - name : Check unit tests results
257+ run : |
258+ UNIT_STATUS="${{ needs.unit.result }}"
259+
260+ if [[ "$UNIT_STATUS" == "success" ]]; then
261+ echo "Python ${{ matrix.python }} tests passed."
262+ else
263+ echo "Error : Python ${{ matrix.python }} status is '$UNIT_STATUS'."
264+ exit 1
0 commit comments