Files
Charon/.github/workflows/e2e-tests.yml
Jeremy 8e354aeb47 Merge pull request #558 from Wikid82/renovate/feature/beta-release-actions-github-script-8.x
chore(deps): update actions/github-script action to v8 (feature/beta-release)
2026-01-25 09:35:41 -05:00

561 lines
18 KiB
YAML

# E2E Tests Workflow
# Runs Playwright E2E tests with sharding for faster execution
# and collects frontend code coverage via @bgotink/playwright-coverage
#
# Coverage Architecture:
# - Backend: Docker container at localhost:8080 (API)
# - Frontend: Vite dev server at localhost:3000 (serves source files)
# - Tests hit Vite, which proxies API calls to Docker
# - V8 coverage maps directly to source files for accurate reporting
#
# 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 with coverage
# 3. merge-reports: Combine HTML reports from all shards
# 4. comment-results: Post test results as PR comment
# 5. upload-coverage: Merge and upload E2E coverage to Codecov
# 6. 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- 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@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Cache npm dependencies
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
- name: Load Docker image
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon
- name: Generate ephemeral encryption key
run: |
# Generate a unique, ephemeral encryption key for this CI run
# Key is 32 bytes, base64-encoded as required by CHARON_ENCRYPTION_KEY
echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> $GITHUB_ENV
echo "✅ Generated ephemeral encryption key for E2E tests"
- 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@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
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: Start Vite dev server for coverage
run: |
echo "🚀 Starting Vite dev server for E2E coverage..."
cd frontend
# Use port 5173 (Vite default) with --strictPort to fail if busy
VITE_PORT=5173
npx vite --port ${VITE_PORT} --strictPort > /tmp/vite.log 2>&1 &
VITE_PID=$!
echo "VITE_PID=${VITE_PID}" >> $GITHUB_ENV
echo "VITE_PORT=${VITE_PORT}" >> $GITHUB_ENV
# Wait for Vite to be ready
echo "⏳ Waiting for Vite to start on port ${VITE_PORT}..."
MAX_WAIT=60
WAITED=0
while [[ ${WAITED} -lt ${MAX_WAIT} ]]; do
if curl -sf http://localhost:${VITE_PORT} > /dev/null 2>&1; then
echo "✅ Vite dev server ready at http://localhost:${VITE_PORT}"
exit 0
fi
sleep 1
WAITED=$((WAITED + 1))
done
echo "❌ Vite failed to start"
cat /tmp/vite.log
exit 1
- 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:
# Use Vite dev server for coverage (proxies API to Docker at 8080)
PLAYWRIGHT_BASE_URL: http://localhost:${{ env.VITE_PORT }}
CI: true
TEST_WORKER_INDEX: ${{ matrix.shard }}
- name: Stop Vite dev server
if: always()
run: |
if [[ -n "${VITE_PID}" ]]; then
kill ${VITE_PID} 2>/dev/null || true
fi
- name: Upload test results
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: test-results-${{ matrix.browser }}-shard-${{ matrix.shard }}
path: |
playwright-report/
test-results/
retention-days: 7
- name: Upload E2E coverage artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-coverage-shard-${{ matrix.shard }}
path: coverage/e2e/
retention-days: 7
- name: Upload test traces on failure
if: failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Download all test results
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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})
---
<sub>🤖 This comment was automatically generated by the E2E Tests workflow.</sub>`;
// 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
});
}
# Upload merged E2E coverage to Codecov
upload-coverage:
name: Upload E2E Coverage
runs-on: ubuntu-latest
needs: e2e-tests
if: always() && needs.e2e-tests.result == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download all coverage artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: e2e-coverage-*
path: all-coverage
merge-multiple: false
- name: Merge LCOV coverage files
run: |
# Install lcov for merging
sudo apt-get update && sudo apt-get install -y lcov
# Create merged coverage directory
mkdir -p coverage/e2e-merged
# Find all lcov.info files and merge them
LCOV_FILES=$(find all-coverage -name "lcov.info" -type f)
if [[ -n "$LCOV_FILES" ]]; then
# Build merge command
MERGE_ARGS=""
for file in $LCOV_FILES; do
MERGE_ARGS="$MERGE_ARGS -a $file"
done
lcov $MERGE_ARGS -o coverage/e2e-merged/lcov.info
echo "✅ Merged $(echo "$LCOV_FILES" | wc -w) coverage files"
else
echo "⚠️ No coverage files found to merge"
exit 0
fi
- name: Upload E2E coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/e2e-merged/lcov.info
flags: e2e
name: e2e-coverage
fail_ci_if_error: false
- name: Upload merged coverage artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-coverage-merged
path: coverage/e2e-merged/
retention-days: 30
# 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