# E2E Tests Workflow # Runs Playwright E2E tests with sharding for faster execution # # Triggers: # - Pull requests to main/develop (with path filters) # - Push to main branch # - Manual dispatch with browser selection # # Jobs: # 1. build: Build Docker image and upload as artifact # 2. e2e-tests: Run tests in parallel shards # 3. merge-reports: Combine HTML reports from all shards # 4. comment-results: Post test results as PR comment # 5. e2e-results: Status check to block merge on failure name: E2E Tests on: pull_request: branches: - main - develop paths: - 'frontend/**' - 'backend/**' - 'tests/**' - 'playwright.config.js' - '.github/workflows/e2e-tests.yml' push: branches: - main paths: - 'frontend/**' - 'backend/**' - 'tests/**' - 'playwright.config.js' workflow_dispatch: inputs: browser: description: 'Browser to test' required: false default: 'chromium' type: choice options: - chromium - firefox - webkit - all env: NODE_VERSION: '18' GO_VERSION: '1.21' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository_owner }}/charon concurrency: group: e2e-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: # Build application once, share across test shards build: name: Build Application runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Cache npm dependencies uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ hashFiles('package-lock.json') }} restore-keys: npm- - name: Install dependencies run: npm ci - name: Build frontend run: npm run build working-directory: frontend - name: Build backend run: make build working-directory: backend - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build Docker image uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile push: false load: true tags: charon:e2e-test cache-from: type=gha cache-to: type=gha,mode=max - name: Save Docker image run: docker save charon:e2e-test -o charon-e2e-image.tar - name: Upload Docker image artifact uses: actions/upload-artifact@v4 with: name: docker-image path: charon-e2e-image.tar retention-days: 1 # Run tests in parallel shards e2e-tests: name: E2E Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) runs-on: ubuntu-latest needs: build timeout-minutes: 30 strategy: fail-fast: false matrix: shard: [1, 2, 3, 4] total-shards: [4] browser: [chromium] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Download Docker image uses: actions/download-artifact@v4 with: name: docker-image - name: Load Docker image run: | docker load -i charon-e2e-image.tar docker images | grep charon - name: Start test environment run: | # Use the committed docker-compose.playwright.yml for E2E testing docker compose -f .docker/compose/docker-compose.playwright.yml up -d --build echo "✅ Container started via docker-compose.playwright.yml" - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." MAX_ATTEMPTS=30 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}..." if curl -sf http://localhost:8080/api/v1/health > /dev/null 2>&1; then echo "✅ Charon is healthy!" curl -s http://localhost:8080/api/v1/health | jq . exit 0 fi sleep 2 done echo "❌ Health check failed" docker compose -f .docker/compose/docker-compose.playwright.yml logs exit 1 - name: Install dependencies run: npm ci - name: Cache Playwright browsers uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ matrix.browser }}-${{ hashFiles('package-lock.json') }} restore-keys: playwright-${{ matrix.browser }}- - name: Install Playwright browsers run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run E2E tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) run: | npx playwright test \ --project=${{ matrix.browser }} \ --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ --reporter=html,json,github env: PLAYWRIGHT_BASE_URL: http://localhost:8080 CI: true TEST_WORKER_INDEX: ${{ matrix.shard }} - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.browser }}-shard-${{ matrix.shard }} path: | playwright-report/ test-results/ retention-days: 7 - name: Upload test traces on failure if: failure() uses: actions/upload-artifact@v4 with: name: traces-${{ matrix.browser }}-shard-${{ matrix.shard }} path: test-results/**/*.zip retention-days: 7 - name: Collect Docker logs on failure if: failure() run: | echo "📋 Container logs:" docker compose -f .docker/compose/docker-compose.playwright.yml logs > docker-logs-shard-${{ matrix.shard }}.txt 2>&1 - name: Upload Docker logs on failure if: failure() uses: actions/upload-artifact@v4 with: name: docker-logs-shard-${{ matrix.shard }} path: docker-logs-shard-${{ matrix.shard }}.txt retention-days: 7 - name: Cleanup if: always() run: | docker compose -f .docker/compose/docker-compose.playwright.yml down -v 2>/dev/null || true # Merge reports from all shards merge-reports: name: Merge Test Reports runs-on: ubuntu-latest needs: e2e-tests if: always() steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Download all test results uses: actions/download-artifact@v4 with: pattern: test-results-* path: all-results merge-multiple: false - name: Merge Playwright HTML reports run: | # Create directory for merged results mkdir -p merged-results # Find and copy all blob reports find all-results -name "*.zip" -exec cp {} merged-results/ \; 2>/dev/null || true # Merge reports if blobs exist if ls merged-results/*.zip 1> /dev/null 2>&1; then npx playwright merge-reports --reporter html merged-results else echo "No blob reports found, copying individual reports" cp -r all-results/test-results-chromium-shard-1/playwright-report playwright-report 2>/dev/null || mkdir -p playwright-report fi - name: Upload merged report uses: actions/upload-artifact@v4 with: name: merged-playwright-report path: playwright-report/ retention-days: 30 - name: Generate job summary run: | echo "## E2E Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Count results from all shards TOTAL=0 PASSED=0 FAILED=0 for dir in all-results/test-results-*/; do if [[ -f "${dir}test-results/.last-run.json" ]]; then SHARD_STATS=$(cat "${dir}test-results/.last-run.json" 2>/dev/null || echo '{}') # Parse stats if available fi done echo "| Shard | Status |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY for i in 1 2 3 4; do if [[ -d "all-results/test-results-chromium-shard-${i}" ]]; then echo "| Shard ${i} | ✅ Complete |" >> $GITHUB_STEP_SUMMARY else echo "| Shard ${i} | ❌ Failed |" >> $GITHUB_STEP_SUMMARY fi done echo "" >> $GITHUB_STEP_SUMMARY echo "[View full Playwright report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY # Comment on PR with results comment-results: name: Comment Test Results runs-on: ubuntu-latest needs: [e2e-tests, merge-reports] if: github.event_name == 'pull_request' && always() permissions: pull-requests: write steps: - name: Download merged report uses: actions/download-artifact@v4 with: name: merged-playwright-report path: playwright-report continue-on-error: true - name: Determine test status id: status run: | if [[ "${{ needs.e2e-tests.result }}" == "success" ]]; then echo "emoji=✅" >> $GITHUB_OUTPUT echo "status=PASSED" >> $GITHUB_OUTPUT echo "message=All E2E tests passed!" >> $GITHUB_OUTPUT elif [[ "${{ needs.e2e-tests.result }}" == "failure" ]]; then echo "emoji=❌" >> $GITHUB_OUTPUT echo "status=FAILED" >> $GITHUB_OUTPUT echo "message=Some E2E tests failed. Please review the failures." >> $GITHUB_OUTPUT else echo "emoji=⚠️" >> $GITHUB_OUTPUT echo "status=UNKNOWN" >> $GITHUB_OUTPUT echo "message=E2E tests did not complete successfully." >> $GITHUB_OUTPUT fi - name: Comment on PR uses: actions/github-script@v7 with: script: | const emoji = '${{ steps.status.outputs.emoji }}'; const status = '${{ steps.status.outputs.status }}'; const message = '${{ steps.status.outputs.message }}'; const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = `## ${emoji} E2E Test Results: ${status} ${message} | Metric | Result | |--------|--------| | Browser | Chromium | | Shards | 4 | | Status | ${status} | [📊 View full Playwright report](${runUrl}) --- 🤖 This comment was automatically generated by the E2E Tests workflow.`; // Find existing comment const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const botComment = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes('E2E Test Results') ); if (botComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: body }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body }); } # Final status check - blocks merge if tests fail e2e-results: name: E2E Test Results runs-on: ubuntu-latest needs: e2e-tests if: always() steps: - name: Check test results run: | if [[ "${{ needs.e2e-tests.result }}" == "success" ]]; then echo "✅ All E2E tests passed" exit 0 elif [[ "${{ needs.e2e-tests.result }}" == "skipped" ]]; then echo "⏭️ E2E tests were skipped" exit 0 else echo "❌ E2E tests failed or were cancelled" echo "Result: ${{ needs.e2e-tests.result }}" exit 1 fi