-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathaction.yml
More file actions
570 lines (472 loc) · 26.2 KB
/
action.yml
File metadata and controls
570 lines (472 loc) · 26.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
name: "RunWright"
author: "Pramod Yadav"
description: "A GitHub Action to run Playwright tests optimally and within time limits"
# GitHub Actions branding configuration
# https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#branding
branding:
icon: "fast-forward"
color: "yellow"
# ==========================================
# INPUTS
# ==========================================
inputs:
total-run-time-in-mins:
description: "Desired total test run time in minutes (minimum 1 min)"
required: true
type: string
pw-command-to-execute:
description: 'Playwright command to run tests (e.g., "npx playwright test")'
required: true
type: string
fully-parallel:
description: "Whether Playwright is configured with fullyParallel=true (default: true). Set to false if fullyParallel=false in playwright.config"
required: false
default: "true"
type: string
# ==========================================
# OUTPUTS
# ==========================================
outputs:
dynamic-matrix:
description: "Dynamic matrix array for parallel runner strategy"
value: ${{ steps.set-matrix.outputs.dynamic_matrix }}
test-load-distribution-json:
description: "JSON object containing test distribution across runners"
value: ${{ steps.calculate-required-runners.outputs.test_load_json }}
recommended-workers:
description: "Optimal number of workers per runner based on CPU cores"
value: ${{ steps.get-number-of-cpu-cores-to-decide-on-worker-count.outputs.RECOMMENDED_WORKERS }}
parallelism-mode:
description: "The parallelism mode being used (individual for fullyParallel=true, file-level for fullyParallel=false)"
value: ${{ steps.detect-playwright-config.outputs.DISTRIBUTION_MODE }}
# ==========================================
# COMPOSITE ACTION STEPS
# ==========================================
runs:
using: "composite"
steps:
# ------------------------------------------
# SETUP PHASE: Environment preparation
# ------------------------------------------
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install jq (JSON processor)
run: sudo apt-get update && sudo apt-get install -y jq
shell: bash
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install project dependencies
run: npm ci
shell: bash
# ------------------------------------------
# BROWSER CACHING: Optimize Playwright setup
# ------------------------------------------
- name: Cache Playwright Browsers
id: cache-browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('playwright.config.js', 'package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
if: steps.cache-browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
shell: bash
# ------------------------------------------
# PLAYWRIGHT CONFIGURATION: Detect parallelism mode
# ------------------------------------------
- name: Detect Playwright Configuration
id: detect-playwright-config
run: |
echo "🔍 Detecting Playwright configuration..."
FULLY_PARALLEL="${{ inputs.fully-parallel }}"
echo "📝 Parallelism Mode: $FULLY_PARALLEL"
if [ "$FULLY_PARALLEL" = "true" ]; then
echo "✅ Running in fully parallel mode - individual test distribution"
echo "DISTRIBUTION_MODE=individual" >> $GITHUB_ENV
echo "DISTRIBUTION_MODE=individual" >> $GITHUB_OUTPUT
else
echo "⚠️ Running in sequential mode - file-level distribution"
echo "DISTRIBUTION_MODE=file-level" >> $GITHUB_ENV
echo "DISTRIBUTION_MODE=file-level" >> $GITHUB_OUTPUT
fi
echo "FULLY_PARALLEL=$FULLY_PARALLEL" >> $GITHUB_ENV
shell: bash
# ------------------------------------------
# TEST DISCOVERY: Discover and process tests
# ------------------------------------------
- name: List of Playwright Tests to Run
id: list-tests
run: |
echo "🔍 Discovering Playwright tests..."
# Execute Playwright test list command
test_list=$(${{ inputs.pw-command-to-execute }} --list --reporter=list 2>&1 || true)
# Handle case when no tests are found
if echo "$test_list" | grep -q "Error: No tests found"; then
echo "⚠️ No tests found. Setting RUNNER_COUNT to 1 and exiting gracefully."
echo "RUNNER_COUNT=1" >> $GITHUB_ENV
echo "RUNNER_COUNT=1" >> $GITHUB_OUTPUT
exit 0
fi
# Debug: Display the raw output
echo "📋 RAW test_list:"
echo "::debug::test_list: $test_list"
# Filter output to include only valid test lines (starting with '[')
echo "🔧 Filtering test list..."
filtered_list=$(echo "$test_list" | grep -E '^\s*\[' | sed 's/^\s*//; s/\s*$//')
# Convert filtered output into a JSON array
echo "📦 Converting to JSON array..."
json_array=$(echo "$filtered_list" | jq -R -s 'split("\n") | map(select(length > 0))')
# Debug: Display the JSON array
echo "✅ JSON array created:"
echo "::debug::json_array: $json_array"
# Save the JSON array to GITHUB_OUTPUT (compact format for transfer)
TEST_LIST=$(echo "$json_array" | jq -c)
echo "TEST_LIST=$TEST_LIST" >> $GITHUB_OUTPUT
echo "✅ Test discovery completed. Found $(echo "$json_array" | jq 'length') tests."
shell: bash
# ------------------------------------------
# RESOURCE OPTIMIZATION: Calculate optimal worker count
# ------------------------------------------
# References:
# - Runner CORES: https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners
# - CORES vs Workers: https://learn.microsoft.com/en-us/azure/playwright-testing/concept-determine-optimal-configuration#run-tests-locally
- name: Calculate Optimal Worker Count
id: get-number-of-cpu-cores-to-decide-on-worker-count
run: |
echo "🖥️ Calculating optimal worker count based on CPU cores..."
# Get number of CPU cores available on the runner
NUM_CORES=$(nproc) # For Linux runners
# Calculate optimal worker count (half of available cores for stability)
RECOMMENDED_WORKERS=$((NUM_CORES / 2))
# Ensure minimum of 1 worker
if [ $RECOMMENDED_WORKERS -lt 1 ]; then
RECOMMENDED_WORKERS=1
fi
# Display mode-specific information
if [ "$FULLY_PARALLEL" = "true" ]; then
echo "✅ Fully parallel mode: Individual tests run in parallel"
else
echo "✅ File-level mode: Multiple files can run in parallel, tests within each file run sequentially"
fi
echo "📊 System Information:"
echo " CPU Cores: $NUM_CORES"
echo " Parallelism Mode: $FULLY_PARALLEL"
echo " Recommended Workers: $RECOMMENDED_WORKERS"
# Export variables for use in subsequent steps
echo "RECOMMENDED_WORKERS=$RECOMMENDED_WORKERS" >> $GITHUB_ENV
echo "RECOMMENDED_WORKERS=$RECOMMENDED_WORKERS" >> $GITHUB_OUTPUT
shell: bash
# ------------------------------------------
# CORE ALGORITHM: Dynamic test distribution across runners
# ------------------------------------------
- name: Calculate Required Runners and Distribute Tests
id: calculate-required-runners
run: |
echo "🧮 Starting dynamic test distribution algorithm..."
echo "📋 Distribution Mode: $DISTRIBUTION_MODE"
# ================================
# INITIALIZE VARIABLES
# ================================
RUNNER_COUNT=1
TOTAL_TESTS_IN_A_RUNNER=0
TOTAL_RUNNER_EXECUTION_TIME=0
# ================================
# INPUT VALIDATION
# ================================
# Validate minimum time requirement
REQUESTED_TIME="${{ inputs.total-run-time-in-mins }}"
# Use awk for floating-point comparison since bash only handles integers
if [ $(echo "$REQUESTED_TIME < 1" | awk '{print ($1 < $3)}') -eq 1 ]; then
echo "::error title=Invalid Time Input::Total run time must be at least 1 minute. Received: $REQUESTED_TIME minutes"
echo "::notice title=Minimum Time Requirement::Please set total-run-time-in-mins to 1 or higher for optimal test distribution."
exit 1
fi
# Additional validation to ensure it's a valid number
if ! echo "$REQUESTED_TIME" | grep -E '^[0-9]+(\.[0-9]+)?$' > /dev/null; then
echo "::error title=Invalid Input Format::Total run time must be a valid number. Received: '$REQUESTED_TIME'"
echo "::notice title=Valid Format Examples::Use values like: 1, 1.5, 2, 5.25, etc."
exit 1
fi
echo "✅ Input validation passed. Target time: $REQUESTED_TIME minutes"
# Convert user input to milliseconds for calculations
MAX_RUN_TIME=$(( ${{ inputs.total-run-time-in-mins }} * 60 * 1000))
echo "📝 Configuration:"
echo " Max Run Time: $MAX_RUN_TIME ms (${{ inputs.total-run-time-in-mins }} minutes)"
echo " Recommended Workers: $RECOMMENDED_WORKERS"
echo " Fully Parallel: $FULLY_PARALLEL"
# Get the test list from previous step
test_list='${{ steps.list-tests.outputs.TEST_LIST }}'
# ================================
# CHOOSE DISTRIBUTION STRATEGY
# ================================
if [ "$DISTRIBUTION_MODE" = "individual" ]; then
echo "🔄 Processing tests for individual distribution (fullyParallel=true)..."
# Parse and iterate through each test individually
echo "$test_list" | jq -r '.[]' | {
declare -A RUNNER_TESTS # Associative array to store test distribution
while IFS= read -r test; do
echo "::debug::Processing test: $test"
# Lookup execution time from state.json
time=$(jq -r --arg test "$test" '.[$test]' state.json)
# Extract project/browser name (e.g., "chromium" from "[chromium] › test.spec.ts")
project=$(echo "$test" | awk -F'›' '{print $1}' | xargs | sed 's/^\[//;s/\]$//')
echo "::debug::Project: $project"
# Extract test identifier (e.g., "demo-todo-app.spec.ts:45:7")
test_identifier=$(echo "$test" | awk -F'›' '{print $2}' | xargs)
echo "::debug::Test Identifier: $test_identifier"
# ================================
# PROCESS TESTS WITH KNOWN TIMES
# ================================
if [ "$time" != "null" ]; then
# Calculate if adding this test would exceed time limit
# Formula: (current_time + new_test_time) / workers <= max_time
if (( (TOTAL_RUNNER_EXECUTION_TIME + time) / RECOMMENDED_WORKERS <= MAX_RUN_TIME )); then
# Test fits in current runner
TOTAL_RUNNER_EXECUTION_TIME=$((TOTAL_RUNNER_EXECUTION_TIME + time))
TOTAL_TESTS_IN_A_RUNNER=$((TOTAL_TESTS_IN_A_RUNNER + 1))
echo "::debug::✅ Adding test to runner $RUNNER_COUNT: $test (${time}ms)"
else
# Current runner is full - finalize it and create a new one
echo "🏁 Finalizing Runner $RUNNER_COUNT..."
# Calculate and display runner statistics
TOTAL_TIME_MS=$((TOTAL_RUNNER_EXECUTION_TIME / RECOMMENDED_WORKERS))
TOTAL_TIME_SEC=$((TOTAL_TIME_MS / 1000))
MINUTES=$((TOTAL_TIME_SEC / 60))
SECONDS=$((TOTAL_TIME_SEC % 60))
echo "::notice title=Runner $RUNNER_COUNT Summary::Runner $RUNNER_COUNT will execute $TOTAL_TESTS_IN_A_RUNNER tests using $RECOMMENDED_WORKERS workers in approximately ${MINUTES}m ${SECONDS}s"
# Initialize new runner
RUNNER_COUNT=$((RUNNER_COUNT + 1))
TOTAL_TESTS_IN_A_RUNNER=1
TOTAL_RUNNER_EXECUTION_TIME=$time
echo "🆕 Creating Runner $RUNNER_COUNT for test: $test (${time}ms)"
fi
# ================================
# BUILD JSON STRUCTURE (INDIVIDUAL)
# ================================
RUNNER_KEY="$RUNNER_COUNT"
echo "::debug::Assigning to RUNNER_KEY: $RUNNER_KEY"
# Initialize runner's JSON structure if not already set
if [ -z "${RUNNER_TESTS[$RUNNER_KEY]}" ]; then
RUNNER_TESTS["$RUNNER_KEY"]="{\"$project\": [\"$test_identifier\"]}"
else
# Update existing runner's project grouping
if echo "${RUNNER_TESTS[$RUNNER_KEY]}" | jq -e ".\"$project\"" > /dev/null; then
# Append to existing project
RUNNER_TESTS["$RUNNER_KEY"]=$(echo "${RUNNER_TESTS[$RUNNER_KEY]}" | jq ".\"$project\" += [\"$test_identifier\"]")
else
# Add new project to runner
RUNNER_TESTS["$RUNNER_KEY"]=$(echo "${RUNNER_TESTS[$RUNNER_KEY]}" | jq ". + {\"$project\": [\"$test_identifier\"]}")
fi
fi
else
# ================================
# ERROR HANDLING: Missing test data
# ================================
echo "::error title=Test Not Found::Test not found in state.json 👉 $test"
echo "::notice title=Directory Structure Updated::It seems the test directory structure was updated locally but not pushed to state.json."
echo "::notice title=Post-commit Hook Suggestion::💡 This can be avoided by adding a post-commit hook to automatically update state.json on commit. Refer to the README for implementation details."
echo "::notice title=Next Steps::For now, you can either 'run missing tests only' or 'run all tests locally' to update the state.json snapshot file and upload it again."
exit 1
fi
done
# ================================
# FINALIZE DISTRIBUTION (INDIVIDUAL)
# ================================
echo "::debug::Distribution complete. Runner keys: ${!RUNNER_TESTS[@]}"
# Build the final JSON structure for all runners
echo "🔧 Building final test distribution JSON..."
TEST_LOAD_DISTRIBUTION_JSON="{"
for runner in "${!RUNNER_TESTS[@]}"; do
if [[ "$TEST_LOAD_DISTRIBUTION_JSON" != "{" ]]; then
TEST_LOAD_DISTRIBUTION_JSON+=","
fi
TEST_LOAD_DISTRIBUTION_JSON+="\"$runner\": $(echo "${RUNNER_TESTS[$runner]}" | jq -c)"
done
TEST_LOAD_DISTRIBUTION_JSON+="}"
# Display final distribution (pretty-printed)
echo "📊 Final Test Load Distribution:"
echo "$TEST_LOAD_DISTRIBUTION_JSON" | jq
# Export for use in subsequent steps
echo "test_load_json=$TEST_LOAD_DISTRIBUTION_JSON" >> $GITHUB_OUTPUT
# Finalize the last runner's statistics
TOTAL_TIME_MS=$((TOTAL_RUNNER_EXECUTION_TIME / RECOMMENDED_WORKERS))
TOTAL_TIME_SEC=$((TOTAL_TIME_MS / 1000))
MINUTES=$((TOTAL_TIME_SEC / 60))
SECONDS=$((TOTAL_TIME_SEC % 60))
echo "::notice title=Runner $RUNNER_COUNT Summary::Runner $RUNNER_COUNT will execute $TOTAL_TESTS_IN_A_RUNNER tests using $RECOMMENDED_WORKERS workers in approximately ${MINUTES}m ${SECONDS}s"
# Export runner count
echo "RUNNER_COUNT=$RUNNER_COUNT" >> $GITHUB_ENV
echo "RUNNER_COUNT=$RUNNER_COUNT" >> $GITHUB_OUTPUT
echo "✅ Test distribution completed successfully!"
echo "📈 Summary: $RUNNER_COUNT runner(s) created for optimal test execution"
}
else
echo "🔄 Processing tests for file-level distribution (fullyParallel=false)..."
# ================================
# FILE-LEVEL DISTRIBUTION LOGIC
# ================================
# Create temporary files to store file grouping data
temp_file_times=$(mktemp)
temp_file_tests=$(mktemp)
temp_file_projects=$(mktemp)
# First, group tests by file and calculate total time per file
echo "$test_list" | jq -r '.[]' | while IFS= read -r test; do
echo "::debug::Processing test for file grouping: $test"
# Lookup execution time from state.json
time=$(jq -r --arg test "$test" '.[$test]' state.json)
# Extract project/browser name (example: [webkit] › load-test/load-tests-sample.spec.ts:241:7 › Editing › should trim entered text) should give [webkit]
project=$(echo "$test" | awk -F'›' '{print $1}' | xargs | sed 's/^\[//;s/\]$//')
# Extract test identifier and file path (example: [webkit] › load-test/load-tests-sample.spec.ts:241:7 › Editing › should trim entered text) should give load-test/load-tests-sample.spec.ts
test_identifier=$(echo "$test" | awk -F'›' '{print $2}' | xargs)
file_path=$(echo "$test_identifier" | awk -F':' '{print $1}')
# Create unique key combining project and file
file_key="${project}:::${file_path}"
if [ "$time" != "null" ]; then
# Check if file_key already exists in temp files
if grep -q "^$file_key " "$temp_file_times"; then
# Update existing entry
current_time=$(grep "^$file_key " "$temp_file_times" | cut -d' ' -f2)
new_time=$((current_time + time))
# Use awk to safely replace the line (avoids sed delimiter issues)
awk -v key="$file_key" -v old_time="$current_time" -v new_time="$new_time" '
$0 ~ "^" key " " old_time "$" { gsub(old_time "$", new_time); print; next }
{ print }
' "$temp_file_times" > "${temp_file_times}.tmp" && mv "${temp_file_times}.tmp" "$temp_file_times"
# Safely append to test list using awk
awk -v key="$file_key" -v test="$test_identifier" '
$0 ~ "^" key " " { print $0 "|" test; next }
{ print }
' "$temp_file_tests" > "${temp_file_tests}.tmp" && mv "${temp_file_tests}.tmp" "$temp_file_tests"
else
# Add new entry
echo "$file_key $time" >> "$temp_file_times"
echo "$file_key $test_identifier" >> "$temp_file_tests"
echo "$file_key $project" >> "$temp_file_projects"
fi
echo "::debug::Added to file $file_key: $time ms"
else
echo "::error title=Test Not Found::Test not found in state.json 👉 $test"
rm -f "$temp_file_times" "$temp_file_tests" "$temp_file_projects"
exit 1
fi
done
# Now distribute files across runners
declare -A RUNNER_TESTS
while IFS=' ' read -r file_key file_time; do
file_tests=$(grep "^$file_key " "$temp_file_tests" | cut -d' ' -f2-)
file_project=$(grep "^$file_key " "$temp_file_projects" | cut -d' ' -f2)
echo "::debug::Processing file: $file_key with total time: $file_time ms"
# For file-level distribution, multiple files can run in parallel
# So we divide by workers to calculate if adding this file would exceed time limit
if (( (TOTAL_RUNNER_EXECUTION_TIME + file_time) / RECOMMENDED_WORKERS <= MAX_RUN_TIME )); then
# File fits in current runner
TOTAL_RUNNER_EXECUTION_TIME=$((TOTAL_RUNNER_EXECUTION_TIME + file_time))
file_test_count=$(echo "$file_tests" | tr '|' '\n' | wc -l | xargs)
TOTAL_TESTS_IN_A_RUNNER=$((TOTAL_TESTS_IN_A_RUNNER + file_test_count))
echo "::debug::✅ Adding file to runner $RUNNER_COUNT: $file_key (${file_time}ms, $file_test_count tests)"
else
# Current runner is full - finalize it and create a new one
if [ $TOTAL_TESTS_IN_A_RUNNER -gt 0 ]; then
echo "🏁 Finalizing Runner $RUNNER_COUNT..."
# Calculate and display runner statistics (with workers division for file-level)
TOTAL_TIME_MS=$((TOTAL_RUNNER_EXECUTION_TIME / RECOMMENDED_WORKERS))
TOTAL_TIME_SEC=$((TOTAL_TIME_MS / 1000))
MINUTES=$((TOTAL_TIME_SEC / 60))
SECONDS=$((TOTAL_TIME_SEC % 60))
echo "::notice title=Runner $RUNNER_COUNT Summary::Runner $RUNNER_COUNT will execute $TOTAL_TESTS_IN_A_RUNNER tests (file-level) using $RECOMMENDED_WORKERS workers in approximately ${MINUTES}m ${SECONDS}s"
# Initialize new runner
RUNNER_COUNT=$((RUNNER_COUNT + 1))
fi
file_test_count=$(echo "$file_tests" | tr '|' '\n' | wc -l | xargs)
TOTAL_TESTS_IN_A_RUNNER=$file_test_count
TOTAL_RUNNER_EXECUTION_TIME=$file_time
echo "🆕 Creating Runner $RUNNER_COUNT for file: $file_key (${file_time}ms, $file_test_count tests)"
fi
# ================================
# BUILD JSON STRUCTURE (FILE-LEVEL)
# ================================
RUNNER_KEY="$RUNNER_COUNT"
# Convert file tests from pipe-separated to JSON array
if [ -n "$file_tests" ]; then
test_json_array=$(echo "$file_tests" | tr '|' '\n' | jq -R . | jq -s .)
# Initialize runner's JSON structure if not already set
if [ -z "${RUNNER_TESTS[$RUNNER_KEY]}" ]; then
RUNNER_TESTS["$RUNNER_KEY"]="{\"$file_project\": $test_json_array}"
else
# Update existing runner's project grouping
if echo "${RUNNER_TESTS[$RUNNER_KEY]}" | jq -e ".\"$file_project\"" > /dev/null 2>&1; then
# Append to existing project
RUNNER_TESTS["$RUNNER_KEY"]=$(echo "${RUNNER_TESTS[$RUNNER_KEY]}" | jq ".\"$file_project\" += $test_json_array")
else
# Add new project to runner
RUNNER_TESTS["$RUNNER_KEY"]=$(echo "${RUNNER_TESTS[$RUNNER_KEY]}" | jq ". + {\"$file_project\": $test_json_array}")
fi
fi
fi
done < "$temp_file_times"
# Clean up temporary files
rm -f "$temp_file_times" "$temp_file_tests" "$temp_file_projects"
# ================================
# FINALIZE DISTRIBUTION (FILE-LEVEL)
# ================================
echo "::debug::File-level distribution complete. Runner keys: ${!RUNNER_TESTS[@]}"
# Build the final JSON structure for all runners
echo "🔧 Building final test distribution JSON..."
TEST_LOAD_DISTRIBUTION_JSON="{"
first_runner=true
for runner in "${!RUNNER_TESTS[@]}"; do
if [ "$first_runner" = false ]; then
TEST_LOAD_DISTRIBUTION_JSON+=","
fi
TEST_LOAD_DISTRIBUTION_JSON+="\"$runner\": $(echo "${RUNNER_TESTS[$runner]}" | jq -c)"
first_runner=false
done
TEST_LOAD_DISTRIBUTION_JSON+="}"
# Display final distribution (pretty-printed)
echo "📊 Final Test Load Distribution (File-Level):"
echo "$TEST_LOAD_DISTRIBUTION_JSON" | jq
# Export for use in subsequent steps
echo "test_load_json=$TEST_LOAD_DISTRIBUTION_JSON" >> $GITHUB_OUTPUT
# Finalize the last runner's statistics
if [ $TOTAL_TESTS_IN_A_RUNNER -gt 0 ]; then
TOTAL_TIME_MS=$((TOTAL_RUNNER_EXECUTION_TIME / RECOMMENDED_WORKERS))
TOTAL_TIME_SEC=$((TOTAL_TIME_MS / 1000))
MINUTES=$((TOTAL_TIME_SEC / 60))
SECONDS=$((TOTAL_TIME_SEC % 60))
echo "::notice title=Runner $RUNNER_COUNT Summary::Runner $RUNNER_COUNT will execute $TOTAL_TESTS_IN_A_RUNNER tests (file-level) using $RECOMMENDED_WORKERS workers in approximately ${MINUTES}m ${SECONDS}s"
fi
# Export runner count
echo "RUNNER_COUNT=$RUNNER_COUNT" >> $GITHUB_ENV
echo "RUNNER_COUNT=$RUNNER_COUNT" >> $GITHUB_OUTPUT
echo "✅ File-level test distribution completed successfully!"
echo "📈 Summary: $RUNNER_COUNT runner(s) created for file-level execution"
fi
shell: bash
# ------------------------------------------
# MATRIX GENERATION: Create GitHub Actions matrix strategy
# ------------------------------------------
- name: Generate Dynamic Matrix for Parallel Execution
id: set-matrix
run: |
echo "🎯 Generating dynamic matrix for $RUNNER_COUNT runner(s)..."
# Build JSON array for GitHub Actions matrix strategy
# Format: ["1", "2", "3"] for runner IDs
MATRIX_JSON="["
for i in $(seq 1 $RUNNER_COUNT); do
# Add comma separator for all but first element
if [ $i -gt 1 ]; then
MATRIX_JSON+=","
fi
MATRIX_JSON+="\"$i\""
done
MATRIX_JSON+="]"
echo "📋 Generated matrix: $MATRIX_JSON"
echo " This will create $RUNNER_COUNT parallel runner(s)"
# Export for GitHub Actions matrix strategy
echo "dynamic_matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
echo "✅ Matrix generation completed successfully!"
shell: bash