diff --git a/.github/workflows/cypress_admin-ui.yml b/.github/workflows/cypress_admin-ui.yml index 556200f6f3..d6303c94dc 100644 --- a/.github/workflows/cypress_admin-ui.yml +++ b/.github/workflows/cypress_admin-ui.yml @@ -55,56 +55,44 @@ jobs: shell: bash run: | cd clients/admin-ui/cypress/e2e - # Group test files into balanced groups based on number of tests in each file + # Group test files into balanced groups based on file size (better proxy for execution time than test count) echo "spec_groups=$(find . -name "*.cy.ts" | python3 -c ' - import sys, os, json, math, re + import sys, os, json # Number of groups to create (adjust based on your needs) - NUM_GROUPS = 5 + NUM_GROUPS = 7 - # Get all test files with their test counts + # Get all test files with their sizes (file size correlates better with + # test complexity than test count, since heavier tests have more intercepts, + # fixtures, assertions, and setup code) files = [] for line in sys.stdin: filepath = line.strip() if os.path.exists(filepath): - # Count the number of test occurrences (it, describe, context) - test_count = 0 - with open(filepath, "r") as f: - content = f.read() - # Count occurrences of test definitions like "it(", "test(", "specify(" - test_count += len(re.findall(r"\bit\s*\(", content)) - test_count += len(re.findall(r"\btest\s*\(", content)) - test_count += len(re.findall(r"\bspecify\s*\(", content)) - # If no tests found, set minimum of 1 to ensure file is included - if test_count == 0: - test_count = 1 - files.append((filepath.replace("./", ""), test_count)) - - # Sort files by test count in descending order + size = os.path.getsize(filepath) + files.append((filepath.replace("./", ""), size)) + + # Sort files by size in descending order files.sort(key=lambda x: x[1], reverse=True) # Initialize groups groups = [[] for _ in range(NUM_GROUPS)] - group_counts = [0] * NUM_GROUPS + group_sizes = [0] * NUM_GROUPS - # Distribute files using greedy algorithm (most tests first) - for file, count in files: - # Find the group with the smallest total test count - min_group_idx = group_counts.index(min(group_counts)) + # Distribute files using greedy bin-packing (largest first) + for file, size in files: + # Find the group with the smallest total size + min_group_idx = group_sizes.index(min(group_sizes)) groups[min_group_idx].append(f"cypress/e2e/{file}") - group_counts[min_group_idx] += count + group_sizes[min_group_idx] += size # Format for GitHub Actions print(json.dumps([",".join(group) for group in groups])) ')" >> $GITHUB_OUTPUT - Admin-UI-Cypress: - needs: [Check-Admin-UI-Changes, prepare-matrix] - if: needs.Check-Admin-UI-Changes.outputs.has_admin_ui_changes == 'true' - strategy: - fail-fast: false # We want every single job to run completely, we don't want one failing job on the matrix to stop the rest of them. - matrix: - spec_group: ${{ fromJson(needs.prepare-matrix.outputs.spec_groups) }} + build: + needs: Check-Admin-UI-Changes + if: needs.Check-Admin-UI-Changes.outputs.has_admin_ui_changes == 'true' && github.event_name != 'merge_group' runs-on: ubuntu-latest defaults: run: @@ -117,9 +105,22 @@ jobs: uses: actions/setup-node@v5 with: node-version: 20.x - cache: "npm" + + - name: Cache node_modules and Cypress binary + id: deps-cache + uses: actions/cache@v4 + with: + path: | + clients/node_modules + clients/admin-ui/node_modules + clients/fides-js/node_modules + clients/fidesui/node_modules + clients/privacy-center/node_modules + ~/.cache/Cypress + key: cypress-deps-${{ runner.os }}-${{ hashFiles('clients/package-lock.json') }} - name: Install dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' run: npm ci - name: Build FidesJS @@ -130,6 +131,58 @@ jobs: working-directory: clients/admin-ui run: npm run build:test + - name: Compress build output + run: tar -czf /tmp/cypress-build.tar.gz admin-ui/.next admin-ui/public/lib fides-js/dist + + - name: Upload build output + uses: actions/upload-artifact@v4 + with: + name: cypress-build + path: /tmp/cypress-build.tar.gz + retention-days: 1 + + Admin-UI-Cypress: + needs: [Check-Admin-UI-Changes, build, prepare-matrix] + if: needs.Check-Admin-UI-Changes.outputs.has_admin_ui_changes == 'true' + strategy: + fail-fast: false # We want every single job to run completely, we don't want one failing job on the matrix to stop the rest of them. + matrix: + spec_group: ${{ fromJson(needs.prepare-matrix.outputs.spec_groups) }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: clients + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node.js 20.x + uses: actions/setup-node@v5 + with: + node-version: 20.x + + - name: Restore node_modules and Cypress binary + uses: actions/cache/restore@v4 + with: + path: | + clients/node_modules + clients/admin-ui/node_modules + clients/fides-js/node_modules + clients/fidesui/node_modules + clients/privacy-center/node_modules + ~/.cache/Cypress + key: cypress-deps-${{ runner.os }}-${{ hashFiles('clients/package-lock.json') }} + fail-on-cache-miss: true + + - name: Download build output + uses: actions/download-artifact@v4 + with: + name: cypress-build + path: /tmp + + - name: Extract build output + run: tar -xzf /tmp/cypress-build.tar.gz + - name: Cypress Admin UI E2E Tests uses: cypress-io/github-action@v6 with: @@ -150,23 +203,27 @@ jobs: runs-on: ubuntu-latest if: always() needs: + - build - prepare-matrix - Admin-UI-Cypress steps: - name: Check job results run: | + echo "build: ${{ needs.build.result }}" echo "prepare-matrix: ${{ needs.prepare-matrix.result }}" echo "Admin-UI-Cypress: ${{ needs.Admin-UI-Cypress.result }}" # Fail only if jobs failed (not if skipped) - if [ "${{ needs.prepare-matrix.result }}" == "failure" ] || \ + if [ "${{ needs.build.result }}" == "failure" ] || \ + [ "${{ needs.prepare-matrix.result }}" == "failure" ] || \ [ "${{ needs.Admin-UI-Cypress.result }}" == "failure" ]; then echo "❌ One or more required jobs failed" exit 1 fi # Check for cancelled jobs (treat as failure) - if [ "${{ needs.prepare-matrix.result }}" == "cancelled" ] || \ + if [ "${{ needs.build.result }}" == "cancelled" ] || \ + [ "${{ needs.prepare-matrix.result }}" == "cancelled" ] || \ [ "${{ needs.Admin-UI-Cypress.result }}" == "cancelled" ]; then echo "❌ One or more required jobs were cancelled" exit 1 diff --git a/changelog/7792-optimize-cypress-ci.yaml b/changelog/7792-optimize-cypress-ci.yaml new file mode 100644 index 0000000000..4c1e478d55 --- /dev/null +++ b/changelog/7792-optimize-cypress-ci.yaml @@ -0,0 +1,4 @@ +type: Developer Experience +description: Optimize Cypress CI with shared build artifacts, file-size-based test sharding, and increased parallelism +pr: 7792 +labels: [] diff --git a/clients/admin-ui/cypress.config.ts b/clients/admin-ui/cypress.config.ts index 811ddff5d0..6500cac5ea 100644 --- a/clients/admin-ui/cypress.config.ts +++ b/clients/admin-ui/cypress.config.ts @@ -47,5 +47,5 @@ export default defineConfig({ }, // Will only run for cy:run, not cy:open - video: true, + video: false, // change to true if you are having failures in CI and not locally, to help debug });