name: UI - E2E Tests (Optimized) # This is an optimized version that runs only relevant E2E tests # based on changed files. Falls back to running all tests if # critical paths are changed or if impact analysis fails. on: push: branches: - master - "v5.*" paths: - '.github/workflows/ui-e2e-tests-v2.yml' - '.github/workflows/test-impact-analysis.yml' - '.github/test-impact.yml' - 'ui/**' pull_request: types: - opened - synchronize - reopened - labeled - unlabeled branches: - master - "v5.*" paths: - '.github/workflows/ui-e2e-tests-v2.yml' - '.github/workflows/test-impact-analysis.yml' - '.github/test-impact.yml' - 'ui/**' - 'api/**' # API changes can affect UI E2E concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read jobs: # Trusted PR authors get the opt-in label automatically. This job does not # check out or execute PR code; it only calls the GitHub API for trusted users. auto-label-trusted-pr: if: | github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) && !contains(github.event.pull_request.labels.*.name, 'run-ui-e2e') runs-on: ubuntu-latest permissions: issues: write steps: - name: Add UI E2E opt-in label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: ['run-ui-e2e'], }); # UI E2E consumes cloud credentials, so PR runs require explicit maintainer opt-in. # On protected branch pushes, run independently of PR labels. impact-analysis: if: | github.repository == 'prowler-cloud/prowler' && (github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-ui-e2e') || contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)) uses: ./.github/workflows/test-impact-analysis.yml # Run E2E tests based on impact analysis e2e-tests: needs: impact-analysis if: | github.repository == 'prowler-cloud/prowler' && (github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-ui-e2e') || contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)) && (needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true') runs-on: ubuntu-latest environment: ui-e2e-cloud env: AUTH_SECRET: 'fallback-ci-secret-for-testing' AUTH_TRUST_HOST: true NEXTAUTH_URL: 'http://localhost:3000' AUTH_URL: 'http://localhost:3000' UI_API_BASE_URL: 'http://localhost:8080/api/v1' E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }} E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }} E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }} E2E_AWS_PROVIDER_ACCESS_KEY: ${{ secrets.E2E_AWS_PROVIDER_ACCESS_KEY }} E2E_AWS_PROVIDER_SECRET_KEY: ${{ secrets.E2E_AWS_PROVIDER_SECRET_KEY }} E2E_AWS_PROVIDER_ROLE_ARN: ${{ secrets.E2E_AWS_PROVIDER_ROLE_ARN }} E2E_AZURE_SUBSCRIPTION_ID: ${{ secrets.E2E_AZURE_SUBSCRIPTION_ID }} E2E_AZURE_CLIENT_ID: ${{ secrets.E2E_AZURE_CLIENT_ID }} E2E_AZURE_SECRET_ID: ${{ secrets.E2E_AZURE_SECRET_ID }} E2E_AZURE_TENANT_ID: ${{ secrets.E2E_AZURE_TENANT_ID }} E2E_M365_DOMAIN_ID: ${{ secrets.E2E_M365_DOMAIN_ID }} E2E_M365_CLIENT_ID: ${{ secrets.E2E_M365_CLIENT_ID }} E2E_M365_SECRET_ID: ${{ secrets.E2E_M365_SECRET_ID }} E2E_M365_TENANT_ID: ${{ secrets.E2E_M365_TENANT_ID }} E2E_M365_CERTIFICATE_CONTENT: ${{ secrets.E2E_M365_CERTIFICATE_CONTENT }} E2E_KUBERNETES_CONTEXT: 'kind-kind' E2E_KUBERNETES_KUBECONFIG_PATH: /home/runner/.kube/config E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY: ${{ secrets.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY }} E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }} E2E_GITHUB_APP_ID: ${{ secrets.E2E_GITHUB_APP_ID }} E2E_GITHUB_BASE64_APP_PRIVATE_KEY: ${{ secrets.E2E_GITHUB_BASE64_APP_PRIVATE_KEY }} E2E_GITHUB_USERNAME: ${{ secrets.E2E_GITHUB_USERNAME }} E2E_GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.E2E_GITHUB_PERSONAL_ACCESS_TOKEN }} E2E_GITHUB_ORGANIZATION: ${{ secrets.E2E_GITHUB_ORGANIZATION }} E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN: ${{ secrets.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN }} E2E_ORGANIZATION_ID: ${{ secrets.E2E_ORGANIZATION_ID }} E2E_OCI_TENANCY_ID: ${{ secrets.E2E_OCI_TENANCY_ID }} E2E_OCI_USER_ID: ${{ secrets.E2E_OCI_USER_ID }} E2E_OCI_FINGERPRINT: ${{ secrets.E2E_OCI_FINGERPRINT }} E2E_OCI_KEY_CONTENT: ${{ secrets.E2E_OCI_KEY_CONTENT }} E2E_OCI_REGION: ${{ secrets.E2E_OCI_REGION }} E2E_NEW_USER_PASSWORD: ${{ secrets.E2E_NEW_USER_PASSWORD }} E2E_ALIBABACLOUD_ACCOUNT_ID: ${{ secrets.E2E_ALIBABACLOUD_ACCOUNT_ID }} E2E_ALIBABACLOUD_ACCESS_KEY_ID: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_ID }} E2E_ALIBABACLOUD_ACCESS_KEY_SECRET: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET }} E2E_ALIBABACLOUD_ROLE_ARN: ${{ secrets.E2E_ALIBABACLOUD_ROLE_ARN }} E2E_OKTA_DOMAIN: ${{ secrets.E2E_OKTA_DOMAIN }} E2E_OKTA_CLIENT_ID: ${{ secrets.E2E_OKTA_CLIENT_ID }} E2E_OKTA_BASE64_PRIVATE_KEY: ${{ secrets.E2E_OKTA_BASE64_PRIVATE_KEY }} E2E_GOOGLEWORKSPACE_CUSTOMER_ID: ${{ secrets.E2E_GOOGLEWORKSPACE_CUSTOMER_ID }} E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON: ${{ secrets.E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON }} E2E_GOOGLEWORKSPACE_DELEGATED_USER: ${{ secrets.E2E_GOOGLEWORKSPACE_DELEGATED_USER }} E2E_VERCEL_TEAM_ID: ${{ secrets.E2E_VERCEL_TEAM_ID }} E2E_VERCEL_API_TOKEN: ${{ secrets.E2E_VERCEL_API_TOKEN }} # Pass E2E paths from impact analysis E2E_TEST_PATHS: ${{ needs.impact-analysis.outputs.ui-e2e }} RUN_ALL_TESTS: ${{ needs.impact-analysis.outputs.run-all }} permissions: contents: read steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Show test scope run: | echo "## E2E Test Scope" >> $GITHUB_STEP_SUMMARY if [[ "${RUN_ALL_TESTS}" == "true" ]]; then echo "Running **ALL** E2E tests (critical path changed)" >> $GITHUB_STEP_SUMMARY else echo "Running tests matching: \`${E2E_TEST_PATHS}\`" >> $GITHUB_STEP_SUMMARY fi echo "" echo "Affected modules: \`${NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES}\`" >> $GITHUB_STEP_SUMMARY env: NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }} - name: Create k8s Kind Cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1 with: cluster_name: kind - name: Modify kubeconfig run: | kubectl config set-cluster kind-kind --server=https://kind-control-plane:6443 kubectl config view - name: Add network kind to docker compose run: | yq -i '.networks.kind.external = true' docker-compose.yml yq -i '.services.worker.networks = ["kind","default"]' docker-compose.yml - name: Fix API data directory permissions run: docker run --rm -v $(pwd)/_data/api:/data alpine chown -R 1000:1000 /data - name: Add AWS credentials for testing run: | echo "AWS_ACCESS_KEY_ID=${E2E_AWS_PROVIDER_ACCESS_KEY}" >> .env echo "AWS_SECRET_ACCESS_KEY=${E2E_AWS_PROVIDER_SECRET_KEY}" >> .env - name: Build API image from current code # docker-compose.yml references prowlercloud/prowler-api:latest from the registry, # which lags behind PR changes; build locally so E2E exercises the API image # produced by this PR. # # The image installs the SDK from git@master (api/uv.lock), so a PR changing BOTH the SDK # and the API would run against the OLD SDK and crash on startup. Overlay the checkout's # SDK source so both run together. New SDK dependencies still need an api/uv.lock bump. run: | docker build -t prowlercloud/prowler-api:pr-base ./api docker build -t prowlercloud/prowler-api:latest -f - prowler <<'DOCKERFILE' FROM prowlercloud/prowler-api:pr-base RUN rm -rf /home/prowler/.venv/lib/python3.12/site-packages/prowler COPY --chown=prowler:prowler . /home/prowler/.venv/lib/python3.12/site-packages/prowler DOCKERFILE - name: Start API services run: | export PROWLER_API_VERSION=latest docker compose up -d api worker worker-beat - name: Wait for API to be ready run: | echo "Waiting for prowler-api..." timeout=150 elapsed=0 while [ $elapsed -lt $timeout ]; do if curl -s ${UI_API_BASE_URL}/docs >/dev/null 2>&1; then echo "Prowler API is ready!" exit 0 fi echo "Waiting... (${elapsed}s elapsed)" sleep 5 elapsed=$((elapsed + 5)) done echo "Timeout waiting for prowler-api" exit 1 - name: Load database fixtures run: | docker compose exec -T api sh -c ' for fixture in api/fixtures/dev/*.json; do if [ -f "$fixture" ]; then echo "Loading $fixture" uv run python manage.py loaddata "$fixture" --database admin fi done ' - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: 'ui/.nvmrc' - name: Setup pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 with: package_json_file: ui/package.json run_install: false - name: Get pnpm store directory run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm and Next.js cache uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ${{ env.STORE_PATH }} ./ui/node_modules ./ui/.next/cache key: ${{ runner.os }}-pnpm-nextjs-${{ hashFiles('ui/pnpm-lock.yaml') }}-${{ hashFiles('ui/**/*.ts', 'ui/**/*.tsx', 'ui/**/*.js', 'ui/**/*.jsx') }} restore-keys: | ${{ runner.os }}-pnpm-nextjs-${{ hashFiles('ui/pnpm-lock.yaml') }}- ${{ runner.os }}-pnpm-nextjs- - name: Install UI dependencies working-directory: ./ui run: pnpm install --frozen-lockfile --prefer-offline - name: Build UI application working-directory: ./ui run: pnpm run build - name: Cache Playwright browsers uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: playwright-cache with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('ui/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-playwright- - name: Install Playwright browsers working-directory: ./ui if: steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm run test:e2e:install - name: Run E2E tests working-directory: ./ui run: | if [[ "${RUN_ALL_TESTS}" == "true" ]]; then echo "Running ALL E2E tests..." pnpm run test:e2e else echo "Running targeted E2E tests: ${E2E_TEST_PATHS}" # Convert glob patterns to playwright test paths # e.g., "ui/tests/providers/**" -> "tests/providers" TEST_PATHS="${E2E_TEST_PATHS}" # Remove ui/ prefix and convert ** to empty (playwright handles recursion) TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u) # Drop auth setup helpers (not runnable test suites) TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/') # Safety net: if bare "tests/" appears (from broad patterns like ui/tests/**), # expand to specific subdirs to avoid Playwright discovering setup files if echo "$TEST_PATHS" | grep -qx 'tests/'; then echo "Expanding bare 'tests/' to specific subdirs (excluding setups)..." SPECIFIC_DIRS="" for dir in tests/*/; do [[ "$dir" == "tests/setups/" ]] && continue SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"$'\n' done # Replace "tests/" with specific dirs, keep other paths TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/') TEST_PATHS="${TEST_PATHS}"$'\n'"${SPECIFIC_DIRS}" TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^$' | sort -u) fi if [[ -z "$TEST_PATHS" ]]; then echo "No runnable E2E test paths after filtering setups" exit 0 fi # Filter out directories that don't contain any test files VALID_PATHS="" while IFS= read -r p; do [[ -z "$p" ]] && continue if find "$p" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then VALID_PATHS="${VALID_PATHS}${p}"$'\n' else echo "Skipping empty test directory: $p" fi done <<< "$TEST_PATHS" VALID_PATHS=$(echo "$VALID_PATHS" | grep -v '^$' || true) if [[ -z "$VALID_PATHS" ]]; then echo "No test files found in any resolved paths — skipping E2E" exit 0 fi TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ') echo "Resolved test paths: $TEST_PATHS" pnpm exec playwright test $TEST_PATHS fi - name: Upload test reports uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: playwright-report path: ui/playwright-report/ retention-days: 7 - name: Cleanup services if: always() run: | docker compose down -v || true # Skip job - provides clear feedback when UI E2E is not explicitly authorized. skip-e2e-no-label: if: | github.repository == 'prowler-cloud/prowler' && github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'run-ui-e2e') && !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: UI E2E skipped - opt-in label missing run: | echo "## UI E2E Tests Skipped" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "UI E2E tests consume cloud credentials and are skipped unless a maintainer adds the run-ui-e2e label." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Add the label to opt in; remove it to stop secret-consuming UI E2E jobs." >> $GITHUB_STEP_SUMMARY # Skip job - provides clear feedback when no E2E tests needed after opt-in. skip-e2e-no-tests: needs: impact-analysis if: | github.repository == 'prowler-cloud/prowler' && (github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-ui-e2e') || contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)) && needs.impact-analysis.outputs.has-ui-e2e != 'true' && needs.impact-analysis.outputs.run-all != 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 with: egress-policy: audit - name: No E2E tests needed run: | echo "## E2E Tests Skipped" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "No UI E2E tests needed for this change." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Affected modules: \`${NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "To run all tests, modify a file in a critical path (e.g., \`ui/lib/**\`)." >> $GITHUB_STEP_SUMMARY env: NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }}