diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index 1500c41b..00000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,318 +0,0 @@ -# Playwright E2E Tests -# Runs Playwright tests against PR Docker images after the build workflow completes -name: Playwright E2E Tests - -on: - workflow_run: - workflows: ["Docker Build, Publish & Test"] - types: - - completed - - workflow_dispatch: - inputs: - pr_number: - description: 'PR number to test (optional)' - required: false - type: string - -concurrency: - group: playwright-${{ github.event.workflow_run.head_branch || github.ref }} - cancel-in-progress: true - -jobs: - playwright: - name: E2E Tests - runs-on: ubuntu-latest - timeout-minutes: 20 - # Run for: manual dispatch, PR builds, or any push builds from docker-build - if: >- - github.event_name == 'workflow_dispatch' || - ((github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.event == 'push') && - github.event.workflow_run.conclusion == 'success') - - env: - CHARON_ENV: development - CHARON_DEBUG: "1" - CHARON_ENCRYPTION_KEY: ${{ secrets.CHARON_CI_ENCRYPTION_KEY }} - # Emergency server enabled for triage; token supplied via GitHub secret (redacted) - CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} - CHARON_EMERGENCY_SERVER_ENABLED: "true" - PLAYWRIGHT_BASE_URL: http://localhost:8080 - - steps: - - name: Checkout repository - # actions/checkout v4.2.2 - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - - - name: Extract PR number from workflow_run - id: pr-info - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - # Manual dispatch - use input or fail gracefully - if [[ -n "${{ inputs.pr_number }}" ]]; then - echo "pr_number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT" - echo "โœ… Using manually provided PR number: ${{ inputs.pr_number }}" - else - echo "โš ๏ธ No PR number provided for manual dispatch" - echo "pr_number=" >> "$GITHUB_OUTPUT" - fi - exit 0 - fi - - # Extract PR number from workflow_run context - HEAD_SHA="${{ github.event.workflow_run.head_sha }}" - echo "๐Ÿ” Looking for PR with head SHA: ${HEAD_SHA}" - - # Query GitHub API for PR associated with this commit - PR_NUMBER=$(gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" \ - --jq '.[0].number // empty' 2>/dev/null || echo "") - - if [[ -n "${PR_NUMBER}" ]]; then - echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - echo "โœ… Found PR number: ${PR_NUMBER}" - else - echo "โš ๏ธ Could not find PR number for SHA: ${HEAD_SHA}" - echo "pr_number=" >> "$GITHUB_OUTPUT" - fi - - # Check if this is a push event (not a PR) - if [[ "${{ github.event.workflow_run.event }}" == "push" ]]; then - echo "is_push=true" >> "$GITHUB_OUTPUT" - echo "โœ… Detected push build from branch: ${{ github.event.workflow_run.head_branch }}" - else - echo "is_push=false" >> "$GITHUB_OUTPUT" - fi - - - name: Sanitize branch name - id: sanitize - run: | - # Sanitize branch name for use in Docker tags and artifact names - # Replace / with - to avoid invalid reference format errors - BRANCH="${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}" - SANITIZED=$(echo "$BRANCH" | tr '/' '-') - echo "branch=${SANITIZED}" >> "$GITHUB_OUTPUT" - echo "๐Ÿ“‹ Sanitized branch name: ${BRANCH} -> ${SANITIZED}" - - - name: Check for PR image artifact - id: check-artifact - if: steps.pr-info.outputs.pr_number != '' || steps.pr-info.outputs.is_push == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Determine artifact name based on event type - if [[ "${{ steps.pr-info.outputs.is_push }}" == "true" ]]; then - ARTIFACT_NAME="push-image" - else - PR_NUMBER="${{ steps.pr-info.outputs.pr_number }}" - ARTIFACT_NAME="pr-image-${PR_NUMBER}" - fi - RUN_ID="${{ github.event.workflow_run.id }}" - - echo "๐Ÿ” Checking for artifact: ${ARTIFACT_NAME}" - - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - # For manual dispatch, find the most recent workflow run with this artifact - RUN_ID=$(gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/workflows/docker-build.yml/runs?status=success&per_page=10" \ - --jq '.workflow_runs[0].id // empty' 2>/dev/null || echo "") - - if [[ -z "${RUN_ID}" ]]; then - echo "โš ๏ธ No successful workflow runs found" - echo "artifact_exists=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - fi - - echo "run_id=${RUN_ID}" >> "$GITHUB_OUTPUT" - - # Check if the artifact exists in the workflow run - ARTIFACT_ID=$(gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \ - --jq ".artifacts[] | select(.name == \"${ARTIFACT_NAME}\") | .id" 2>/dev/null || echo "") - - if [[ -n "${ARTIFACT_ID}" ]]; then - echo "artifact_exists=true" >> "$GITHUB_OUTPUT" - echo "artifact_id=${ARTIFACT_ID}" >> "$GITHUB_OUTPUT" - echo "โœ… Found artifact: ${ARTIFACT_NAME} (ID: ${ARTIFACT_ID})" - else - echo "artifact_exists=false" >> "$GITHUB_OUTPUT" - echo "โš ๏ธ Artifact not found: ${ARTIFACT_NAME}" - echo "โ„น๏ธ This is expected for non-PR builds or if the image was not uploaded" - fi - - - name: Skip if no artifact - if: (steps.pr-info.outputs.pr_number == '' && steps.pr-info.outputs.is_push != 'true') || steps.check-artifact.outputs.artifact_exists != 'true' - run: | - echo "โ„น๏ธ Skipping Playwright tests - no PR image artifact available" - echo "This is expected for:" - echo " - Pushes to main/release branches" - echo " - PRs where Docker build failed" - echo " - Manual dispatch without PR number" - exit 0 - - - name: Guard triage from coverage/Vite mode - if: steps.check-artifact.outputs.artifact_exists == 'true' - run: | - if [[ "${PLAYWRIGHT_BASE_URL:-}" =~ 5173 ]]; then - echo "โŒ Coverage/Vite base URL is disabled during triage: ${PLAYWRIGHT_BASE_URL}" - exit 1 - fi - case "${PLAYWRIGHT_COVERAGE:-}" in - 1|true|TRUE|True|yes|YES) - echo "โŒ Coverage collection is disabled during triage (PLAYWRIGHT_COVERAGE=${PLAYWRIGHT_COVERAGE})" - exit 1 - ;; - esac - echo "โœ… Coverage/Vite guard passed (PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-unset})" - - - name: Log triage environment (non-secret) - if: steps.check-artifact.outputs.artifact_exists == 'true' - run: | - echo "CHARON_EMERGENCY_SERVER_ENABLED=${CHARON_EMERGENCY_SERVER_ENABLED}" - if [[ -n "${CHARON_EMERGENCY_TOKEN:-}" ]]; then - echo "CHARON_EMERGENCY_TOKEN=*** (GitHub secret configured)" - else - echo "CHARON_EMERGENCY_TOKEN not set; container will fall back to image default" - fi - echo "Ports bound: 8080 (app), 2019 (admin), 2020 (tier-2) on IPv4/IPv6 loopback" - echo "PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL}" - - - name: Download PR image artifact - if: steps.check-artifact.outputs.artifact_exists == 'true' - # actions/download-artifact v4.1.8 - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 - with: - name: ${{ steps.pr-info.outputs.is_push == 'true' && 'push-image' || format('pr-image-{0}', steps.pr-info.outputs.pr_number) }} - run-id: ${{ steps.check-artifact.outputs.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Load Docker image - if: steps.check-artifact.outputs.artifact_exists == 'true' - run: | - echo "๐Ÿ“ฆ Loading Docker image..." - docker load < charon-pr-image.tar - echo "โœ… Docker image loaded" - docker images | grep charon - - - name: Start Charon container - if: steps.check-artifact.outputs.artifact_exists == 'true' - run: | - echo "๐Ÿš€ Starting Charon container..." - - # Normalize image name (GitHub lowercases repository owner names in GHCR) - IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]') - if [[ "${{ steps.pr-info.outputs.is_push }}" == "true" ]]; then - # Use sanitized branch name for Docker tag (/ is invalid in tags) - IMAGE_REF="ghcr.io/${IMAGE_NAME}:${{ steps.sanitize.outputs.branch }}" - elif [[ -n "${{ steps.pr-info.outputs.pr_number }}" ]]; then - IMAGE_REF="ghcr.io/${IMAGE_NAME}:pr-${{ steps.pr-info.outputs.pr_number }}" - else - echo "โŒ ERROR: Cannot determine image reference" - echo " - is_push: ${{ steps.pr-info.outputs.is_push }}" - echo " - pr_number: ${{ steps.pr-info.outputs.pr_number }}" - echo " - branch: ${{ steps.sanitize.outputs.branch }}" - echo "" - echo "This can happen when:" - echo " 1. workflow_dispatch without pr_number input" - echo " 2. workflow_run triggered by non-PR, non-push event" - exit 1 - fi - - # Validate the image reference format - if [[ ! "${IMAGE_REF}" =~ ^ghcr\.io/[a-z0-9_-]+/[a-z0-9_-]+:[a-zA-Z0-9._-]+$ ]]; then - echo "โŒ ERROR: Invalid image reference format: ${IMAGE_REF}" - exit 1 - fi - - echo "๐Ÿ“ฆ Starting container with image: ${IMAGE_REF}" - docker run -d \ - --name charon-test \ - -p 8080:8080 \ - -p 127.0.0.1:2019:2019 \ - -p "[::1]:2019:2019" \ - -p 127.0.0.1:2020:2020 \ - -p "[::1]:2020:2020" \ - -e CHARON_ENV="${CHARON_ENV}" \ - -e CHARON_DEBUG="${CHARON_DEBUG}" \ - -e CHARON_ENCRYPTION_KEY="${CHARON_ENCRYPTION_KEY}" \ - -e CHARON_EMERGENCY_TOKEN="${CHARON_EMERGENCY_TOKEN}" \ - -e CHARON_EMERGENCY_SERVER_ENABLED="${CHARON_EMERGENCY_SERVER_ENABLED}" \ - -e CHARON_EMERGENCY_BIND="0.0.0.0:2020" \ - -e CHARON_EMERGENCY_USERNAME="admin" \ - -e CHARON_EMERGENCY_PASSWORD="changeme" \ - -e CHARON_SECURITY_TESTS_ENABLED="true" \ - "${IMAGE_REF}" - - echo "โœ… Container started" - - - name: Wait for health endpoint - if: steps.check-artifact.outputs.artifact_exists == 'true' - 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!" - exit 0 - fi - - sleep 2 - done - - echo "โŒ Health check failed after ${MAX_ATTEMPTS} attempts" - echo "๐Ÿ“‹ Container logs:" - docker logs charon-test - exit 1 - - - name: Setup Node.js - if: steps.check-artifact.outputs.artifact_exists == 'true' - # actions/setup-node v4.1.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 - with: - node-version: 'lts/*' - cache: 'npm' - - - name: Install dependencies - if: steps.check-artifact.outputs.artifact_exists == 'true' - run: npm ci - - - name: Install Playwright browsers - if: steps.check-artifact.outputs.artifact_exists == 'true' - run: npx playwright install --with-deps chromium - - - name: Run Playwright tests - if: steps.check-artifact.outputs.artifact_exists == 'true' - env: - PLAYWRIGHT_BASE_URL: http://localhost:8080 - run: npx playwright test --project=chromium - - - name: Upload Playwright report - if: always() && steps.check-artifact.outputs.artifact_exists == 'true' - # actions/upload-artifact v4.4.3 - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 - with: - name: ${{ steps.pr-info.outputs.is_push == 'true' && format('playwright-report-{0}', steps.sanitize.outputs.branch) || format('playwright-report-pr-{0}', steps.pr-info.outputs.pr_number) }} - path: playwright-report/ - retention-days: 14 - - - name: Cleanup - if: always() && steps.check-artifact.outputs.artifact_exists == 'true' - run: | - echo "๐Ÿงน Cleaning up..." - docker stop charon-test 2>/dev/null || true - docker rm charon-test 2>/dev/null || true - echo "โœ… Cleanup complete"