Merge pull request #718 from Wikid82/nightly #404
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # E2E Tests Workflow (Reorganized: Security Isolation + Parallel Sharding) | |
| # | |
| # Architecture: 15 Total Jobs | |
| # - 3 Security Enforcement Jobs (1 shard per browser, serial execution, 30min timeout) | |
| # - 12 Non-Security Jobs (4 shards per browser, parallel execution, 20min timeout) | |
| # | |
| # Problem Solved: Cross-shard contamination from security middleware state changes | |
| # Solution: Isolate security enforcement tests in dedicated jobs with Cerberus enabled, | |
| # run all other tests with Cerberus OFF to prevent ACL/rate limit interference | |
| # | |
| # See docs/implementation/E2E_TEST_REORGANIZATION_IMPLEMENTATION.md for full details | |
| name: 'E2E Tests' | |
| on: | |
| workflow_call: | |
| inputs: | |
| browser: | |
| description: 'Browser to test' | |
| required: false | |
| default: 'all' | |
| type: string | |
| test_category: | |
| description: 'Test category' | |
| required: false | |
| default: 'all' | |
| type: string | |
| image_ref: | |
| description: 'Image reference (digest) to test, e.g. docker.io/wikid82/charon@sha256:...' | |
| required: false | |
| type: string | |
| image_tag: | |
| description: 'Local image tag for compose usage (default: charon:e2e-test)' | |
| required: false | |
| type: string | |
| playwright_coverage: | |
| description: 'Enable Playwright coverage (V8)' | |
| required: false | |
| default: false | |
| type: boolean | |
| secrets: | |
| CHARON_EMERGENCY_TOKEN: | |
| required: false | |
| DOCKERHUB_USERNAME: | |
| required: false | |
| DOCKERHUB_TOKEN: | |
| required: false | |
| workflow_dispatch: | |
| inputs: | |
| browser: | |
| description: 'Browser to test' | |
| required: false | |
| default: 'all' | |
| type: choice | |
| options: | |
| - chromium | |
| - firefox | |
| - webkit | |
| - all | |
| test_category: | |
| description: 'Test category' | |
| required: false | |
| default: 'all' | |
| type: choice | |
| options: | |
| - all | |
| - security | |
| - non-security | |
| image_ref: | |
| description: 'Image reference (digest) to test, e.g. docker.io/wikid82/charon@sha256:...' | |
| required: false | |
| type: string | |
| image_tag: | |
| description: 'Local image tag for compose usage (default: charon:e2e-test)' | |
| required: false | |
| type: string | |
| playwright_coverage: | |
| description: 'Enable Playwright coverage (V8)' | |
| required: false | |
| default: false | |
| type: boolean | |
| pull_request: | |
| push: | |
| env: | |
| NODE_VERSION: '20' | |
| GO_VERSION: '1.26.0' | |
| GOTOOLCHAIN: auto | |
| DOCKERHUB_REGISTRY: docker.io | |
| IMAGE_NAME: ${{ github.repository_owner }}/charon | |
| E2E_BROWSER: ${{ inputs.browser || 'all' }} | |
| E2E_TEST_CATEGORY: ${{ inputs.test_category || 'all' }} | |
| PLAYWRIGHT_COVERAGE: ${{ (inputs.playwright_coverage && '1') || (vars.PLAYWRIGHT_COVERAGE || '0') }} | |
| DEBUG: 'charon:*,charon-test:*' | |
| PLAYWRIGHT_DEBUG: '1' | |
| CI_LOG_LEVEL: 'verbose' | |
| concurrency: | |
| group: e2e-split-${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.head.sha || github.sha }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Prepare application image once, share across all browser jobs | |
| build: | |
| name: Prepare Application Image | |
| runs-on: ubuntu-latest | |
| outputs: | |
| image_source: ${{ steps.resolve-image.outputs.image_source }} | |
| image_ref: ${{ steps.resolve-image.outputs.image_ref }} | |
| image_tag: ${{ steps.resolve-image.outputs.image_tag }} | |
| image_digest: ${{ steps.resolve-image.outputs.image_digest != '' && steps.resolve-image.outputs.image_digest || steps.build-image.outputs.digest }} | |
| steps: | |
| - name: Resolve image inputs | |
| id: resolve-image | |
| run: | | |
| IMAGE_REF="${{ inputs.image_ref }}" | |
| IMAGE_TAG="${{ inputs.image_tag || 'charon:e2e-test' }}" | |
| if [ -n "$IMAGE_REF" ]; then | |
| { | |
| echo "image_source=registry" | |
| echo "image_ref=$IMAGE_REF" | |
| echo "image_tag=$IMAGE_TAG" | |
| if [[ "$IMAGE_REF" == *@* ]]; then | |
| echo "image_digest=${IMAGE_REF#*@}" | |
| else | |
| echo "image_digest=" | |
| fi | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| { | |
| echo "image_source=build" | |
| echo "image_ref=" | |
| echo "image_tag=$IMAGE_TAG" | |
| echo "image_digest=" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Checkout repository | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Go | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| cache-dependency-path: backend/go.sum | |
| - name: Set up Node.js | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Cache npm dependencies | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 | |
| with: | |
| path: ~/.npm | |
| key: npm-${{ hashFiles('package-lock.json') }} | |
| restore-keys: npm- | |
| - name: Install dependencies | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| run: npm ci | |
| - name: Set up Docker Buildx | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Build Docker image | |
| id: build-image | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| push: false | |
| load: true | |
| tags: ${{ steps.resolve-image.outputs.image_tag }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Save Docker image | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| run: docker save ${{ steps.resolve-image.outputs.image_tag }} -o charon-e2e-image.tar | |
| - name: Upload Docker image artifact | |
| if: steps.resolve-image.outputs.image_source == 'build' | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: docker-image | |
| path: charon-e2e-image.tar | |
| retention-days: 1 | |
| # ================================================================================== | |
| # SECURITY ENFORCEMENT TESTS (3 jobs: 1 per browser, serial execution) | |
| # ================================================================================== | |
| # These tests enable Cerberus middleware and verify security enforcement | |
| # Run serially to avoid cross-test contamination from global state changes | |
| # ================================================================================== | |
| e2e-chromium-security: | |
| name: E2E Chromium (Security Enforcement) | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: | | |
| ((inputs.browser || 'all') == 'chromium' || (inputs.browser || 'all') == 'all') && | |
| ((inputs.test_category || 'all') == 'security' || (inputs.test_category || 'all') == 'all') | |
| timeout-minutes: 60 | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| CHARON_EMERGENCY_SERVER_ENABLED: "true" | |
| CHARON_SECURITY_TESTS_ENABLED: "true" # Cerberus ON for enforcement tests | |
| CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Log in to Docker Hub | |
| if: needs.build.outputs.image_source == 'registry' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull shared Docker image | |
| if: needs.build.outputs.image_source == 'registry' | |
| run: | | |
| docker pull "${{ needs.build.outputs.image_ref }}" | |
| docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}" | |
| docker images | grep charon | |
| - name: Download Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: docker-image | |
| - name: Validate Emergency Token Configuration | |
| run: | | |
| echo "🔐 Validating emergency token configuration..." | |
| if [ -z "$CHARON_EMERGENCY_TOKEN" ]; then | |
| echo "::error title=Missing Secret::CHARON_EMERGENCY_TOKEN secret not configured" | |
| exit 1 | |
| fi | |
| TOKEN_LENGTH=${#CHARON_EMERGENCY_TOKEN} | |
| if [ "$TOKEN_LENGTH" -lt 64 ]; then | |
| echo "::error title=Invalid Token Length::CHARON_EMERGENCY_TOKEN must be at least 64 characters" | |
| exit 1 | |
| fi | |
| MASKED_TOKEN="${CHARON_EMERGENCY_TOKEN:0:8}...${CHARON_EMERGENCY_TOKEN: -4}" | |
| echo "::notice::Emergency token validated (length: $TOKEN_LENGTH, preview: $MASKED_TOKEN)" | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| - name: Load Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| run: | | |
| docker load -i charon-e2e-image.tar | |
| docker images | grep charon | |
| - name: Generate ephemeral encryption key | |
| run: echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" | |
| - name: Start test environment (Security Tests Profile) | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml --profile security-tests up -d | |
| echo "✅ Container started for Chromium security enforcement tests" | |
| - 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://127.0.0.1:8080/api/v1/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| curl -s http://127.0.0.1:8080/api/v1/health | jq . | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Health check failed" | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs | |
| exit 1 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright Chromium | |
| run: | | |
| echo "📦 Installing Chromium..." | |
| npx playwright install --with-deps chromium | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Run Chromium Security Enforcement Tests | |
| run: | | |
| set -euo pipefail | |
| STATUS=0 | |
| echo "════════════════════════════════════════════" | |
| echo "Chromium Security Enforcement Tests" | |
| echo "Cerberus: ENABLED" | |
| echo "Execution: SERIAL (no sharding)" | |
| echo "Start Time: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| echo "════════════════════════════════════════════" | |
| SHARD_START=$(date +%s) | |
| echo "SHARD_START=$SHARD_START" >> "$GITHUB_ENV" | |
| npx playwright test \ | |
| --project=chromium \ | |
| --output=playwright-output/security-chromium \ | |
| tests/security-enforcement/ \ | |
| tests/security/ \ | |
| tests/integration/multi-feature-workflows.spec.ts || STATUS=$? | |
| SHARD_END=$(date +%s) | |
| echo "SHARD_END=$SHARD_END" >> "$GITHUB_ENV" | |
| SHARD_DURATION=$((SHARD_END - SHARD_START)) | |
| echo "════════════════════════════════════════════" | |
| echo "Chromium Security Complete | Duration: ${SHARD_DURATION}s" | |
| echo "════════════════════════════════════════════" | |
| echo "PLAYWRIGHT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| exit "$STATUS" | |
| env: | |
| PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 | |
| CI: true | |
| - name: Upload HTML report (Chromium Security) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-report-chromium-security | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Chromium Security coverage (if enabled) | |
| if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1') | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-coverage-chromium-security | |
| path: coverage/e2e/ | |
| retention-days: 7 | |
| - name: Upload test traces on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: traces-chromium-security | |
| path: test-results/**/*.zip | |
| retention-days: 7 | |
| - name: Collect diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| uptime > diagnostics/uptime.txt | |
| free -m > diagnostics/free-m.txt | |
| df -h > diagnostics/df-h.txt | |
| ps aux > diagnostics/ps-aux.txt | |
| docker ps -a > diagnostics/docker-ps.txt || true | |
| docker logs --tail 500 charon-e2e > diagnostics/docker-charon-e2e.log 2>&1 || true | |
| - name: Upload diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-diagnostics-chromium-security | |
| path: diagnostics/ | |
| retention-days: 7 | |
| - name: Collect Docker logs on failure | |
| if: failure() | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs > docker-logs-chromium-security.txt 2>&1 | |
| - name: Upload Docker logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: docker-logs-chromium-security | |
| path: docker-logs-chromium-security.txt | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose -f .docker/compose/docker-compose.playwright-ci.yml down -v 2>/dev/null || true | |
| e2e-firefox-security: | |
| name: E2E Firefox (Security Enforcement) | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: | | |
| ((inputs.browser || 'all') == 'firefox' || (inputs.browser || 'all') == 'all') && | |
| ((inputs.test_category || 'all') == 'security' || (inputs.test_category || 'all') == 'all') | |
| timeout-minutes: 60 | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| CHARON_EMERGENCY_SERVER_ENABLED: "true" | |
| CHARON_SECURITY_TESTS_ENABLED: "true" # Cerberus ON for enforcement tests | |
| CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Log in to Docker Hub | |
| if: needs.build.outputs.image_source == 'registry' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull shared Docker image | |
| if: needs.build.outputs.image_source == 'registry' | |
| run: | | |
| docker pull "${{ needs.build.outputs.image_ref }}" | |
| docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}" | |
| docker images | grep charon | |
| - name: Download Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: docker-image | |
| - name: Validate Emergency Token Configuration | |
| run: | | |
| echo "🔐 Validating emergency token configuration..." | |
| if [ -z "$CHARON_EMERGENCY_TOKEN" ]; then | |
| echo "::error title=Missing Secret::CHARON_EMERGENCY_TOKEN secret not configured" | |
| exit 1 | |
| fi | |
| TOKEN_LENGTH=${#CHARON_EMERGENCY_TOKEN} | |
| if [ "$TOKEN_LENGTH" -lt 64 ]; then | |
| echo "::error title=Invalid Token Length::CHARON_EMERGENCY_TOKEN must be at least 64 characters" | |
| exit 1 | |
| fi | |
| MASKED_TOKEN="${CHARON_EMERGENCY_TOKEN:0:8}...${CHARON_EMERGENCY_TOKEN: -4}" | |
| echo "::notice::Emergency token validated (length: $TOKEN_LENGTH, preview: $MASKED_TOKEN)" | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| - name: Load Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| run: | | |
| docker load -i charon-e2e-image.tar | |
| docker images | grep charon | |
| - name: Generate ephemeral encryption key | |
| run: echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" | |
| - name: Start test environment (Security Tests Profile) | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml --profile security-tests up -d | |
| echo "✅ Container started for Firefox security enforcement tests" | |
| - 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://127.0.0.1:8080/api/v1/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| curl -s http://127.0.0.1:8080/api/v1/health | jq . | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Health check failed" | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs | |
| exit 1 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright Chromium (required by security-tests dependency) | |
| run: | | |
| echo "📦 Installing Chromium (required by security-tests dependency)..." | |
| npx playwright install --with-deps chromium | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Install Playwright Firefox | |
| run: | | |
| echo "📦 Installing Firefox..." | |
| npx playwright install --with-deps firefox | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Run Firefox Security Enforcement Tests | |
| run: | | |
| set -euo pipefail | |
| STATUS=0 | |
| echo "════════════════════════════════════════════" | |
| echo "Firefox Security Enforcement Tests" | |
| echo "Cerberus: ENABLED" | |
| echo "Execution: SERIAL (no sharding)" | |
| echo "Start Time: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| echo "════════════════════════════════════════════" | |
| SHARD_START=$(date +%s) | |
| echo "SHARD_START=$SHARD_START" >> "$GITHUB_ENV" | |
| npx playwright test \ | |
| --project=firefox \ | |
| --output=playwright-output/security-firefox \ | |
| tests/security-enforcement/ \ | |
| tests/security/ \ | |
| tests/integration/multi-feature-workflows.spec.ts || STATUS=$? | |
| SHARD_END=$(date +%s) | |
| echo "SHARD_END=$SHARD_END" >> "$GITHUB_ENV" | |
| SHARD_DURATION=$((SHARD_END - SHARD_START)) | |
| echo "════════════════════════════════════════════" | |
| echo "Firefox Security Complete | Duration: ${SHARD_DURATION}s" | |
| echo "════════════════════════════════════════════" | |
| echo "PLAYWRIGHT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| exit "$STATUS" | |
| env: | |
| PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 | |
| CI: true | |
| - name: Upload HTML report (Firefox Security) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-report-firefox-security | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Firefox Security coverage (if enabled) | |
| if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1') | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-coverage-firefox-security | |
| path: coverage/e2e/ | |
| retention-days: 7 | |
| - name: Upload test traces on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: traces-firefox-security | |
| path: test-results/**/*.zip | |
| retention-days: 7 | |
| - name: Collect diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| uptime > diagnostics/uptime.txt | |
| free -m > diagnostics/free-m.txt | |
| df -h > diagnostics/df-h.txt | |
| ps aux > diagnostics/ps-aux.txt | |
| docker ps -a > diagnostics/docker-ps.txt || true | |
| docker logs --tail 500 charon-e2e > diagnostics/docker-charon-e2e.log 2>&1 || true | |
| - name: Upload diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-diagnostics-firefox-security | |
| path: diagnostics/ | |
| retention-days: 7 | |
| - name: Collect Docker logs on failure | |
| if: failure() | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs > docker-logs-firefox-security.txt 2>&1 | |
| - name: Upload Docker logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: docker-logs-firefox-security | |
| path: docker-logs-firefox-security.txt | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose -f .docker/compose/docker-compose.playwright-ci.yml down -v 2>/dev/null || true | |
| e2e-webkit-security: | |
| name: E2E WebKit (Security Enforcement) | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: | | |
| ((inputs.browser || 'all') == 'webkit' || (inputs.browser || 'all') == 'all') && | |
| ((inputs.test_category || 'all') == 'security' || (inputs.test_category || 'all') == 'all') | |
| timeout-minutes: 60 | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| CHARON_EMERGENCY_SERVER_ENABLED: "true" | |
| CHARON_SECURITY_TESTS_ENABLED: "true" # Cerberus ON for enforcement tests | |
| CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Log in to Docker Hub | |
| if: needs.build.outputs.image_source == 'registry' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull shared Docker image | |
| if: needs.build.outputs.image_source == 'registry' | |
| run: | | |
| docker pull "${{ needs.build.outputs.image_ref }}" | |
| docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}" | |
| docker images | grep charon | |
| - name: Download Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: docker-image | |
| - name: Validate Emergency Token Configuration | |
| run: | | |
| echo "🔐 Validating emergency token configuration..." | |
| if [ -z "$CHARON_EMERGENCY_TOKEN" ]; then | |
| echo "::error title=Missing Secret::CHARON_EMERGENCY_TOKEN secret not configured" | |
| exit 1 | |
| fi | |
| TOKEN_LENGTH=${#CHARON_EMERGENCY_TOKEN} | |
| if [ "$TOKEN_LENGTH" -lt 64 ]; then | |
| echo "::error title=Invalid Token Length::CHARON_EMERGENCY_TOKEN must be at least 64 characters" | |
| exit 1 | |
| fi | |
| MASKED_TOKEN="${CHARON_EMERGENCY_TOKEN:0:8}...${CHARON_EMERGENCY_TOKEN: -4}" | |
| echo "::notice::Emergency token validated (length: $TOKEN_LENGTH, preview: $MASKED_TOKEN)" | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| - name: Load Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| run: | | |
| docker load -i charon-e2e-image.tar | |
| docker images | grep charon | |
| - name: Generate ephemeral encryption key | |
| run: echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" | |
| - name: Start test environment (Security Tests Profile) | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml --profile security-tests up -d | |
| echo "✅ Container started for WebKit security enforcement tests" | |
| - 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://127.0.0.1:8080/api/v1/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| curl -s http://127.0.0.1:8080/api/v1/health | jq . | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Health check failed" | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs | |
| exit 1 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright Chromium (required by security-tests dependency) | |
| run: | | |
| echo "📦 Installing Chromium (required by security-tests dependency)..." | |
| npx playwright install --with-deps chromium | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Install Playwright WebKit | |
| run: | | |
| echo "📦 Installing WebKit..." | |
| npx playwright install --with-deps webkit | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Run WebKit Security Enforcement Tests | |
| run: | | |
| set -euo pipefail | |
| STATUS=0 | |
| echo "════════════════════════════════════════════" | |
| echo "WebKit Security Enforcement Tests" | |
| echo "Cerberus: ENABLED" | |
| echo "Execution: SERIAL (no sharding)" | |
| echo "Start Time: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| echo "════════════════════════════════════════════" | |
| SHARD_START=$(date +%s) | |
| echo "SHARD_START=$SHARD_START" >> "$GITHUB_ENV" | |
| npx playwright test \ | |
| --project=webkit \ | |
| --output=playwright-output/security-webkit \ | |
| tests/security-enforcement/ \ | |
| tests/security/ \ | |
| tests/integration/multi-feature-workflows.spec.ts || STATUS=$? | |
| SHARD_END=$(date +%s) | |
| echo "SHARD_END=$SHARD_END" >> "$GITHUB_ENV" | |
| SHARD_DURATION=$((SHARD_END - SHARD_START)) | |
| echo "════════════════════════════════════════════" | |
| echo "WebKit Security Complete | Duration: ${SHARD_DURATION}s" | |
| echo "════════════════════════════════════════════" | |
| echo "PLAYWRIGHT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| exit "$STATUS" | |
| env: | |
| PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 | |
| CI: true | |
| - name: Upload HTML report (WebKit Security) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-report-webkit-security | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload WebKit Security coverage (if enabled) | |
| if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1') | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-coverage-webkit-security | |
| path: coverage/e2e/ | |
| retention-days: 7 | |
| - name: Upload test traces on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: traces-webkit-security | |
| path: test-results/**/*.zip | |
| retention-days: 7 | |
| - name: Collect diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| uptime > diagnostics/uptime.txt | |
| free -m > diagnostics/free-m.txt | |
| df -h > diagnostics/df-h.txt | |
| ps aux > diagnostics/ps-aux.txt | |
| docker ps -a > diagnostics/docker-ps.txt || true | |
| docker logs --tail 500 charon-e2e > diagnostics/docker-charon-e2e.log 2>&1 || true | |
| - name: Upload diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-diagnostics-webkit-security | |
| path: diagnostics/ | |
| retention-days: 7 | |
| - name: Collect Docker logs on failure | |
| if: failure() | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs > docker-logs-webkit-security.txt 2>&1 | |
| - name: Upload Docker logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: docker-logs-webkit-security | |
| path: docker-logs-webkit-security.txt | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose -f .docker/compose/docker-compose.playwright-ci.yml down -v 2>/dev/null || true | |
| # ================================================================================== | |
| # NON-SECURITY TESTS (12 jobs: 4 shards × 3 browsers, parallel execution) | |
| # ==================================================================================================== | |
| # These tests run with Cerberus DISABLED to prevent ACL/rate limit interference | |
| # Sharded for performance: 4 shards per browser for faster execution | |
| # ================================================================================== | |
| e2e-chromium: | |
| name: E2E Chromium (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: | | |
| ((inputs.browser || 'all') == 'chromium' || (inputs.browser || 'all') == 'all') && | |
| ((inputs.test_category || 'all') == 'non-security' || (inputs.test_category || 'all') == 'all') | |
| timeout-minutes: 60 | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| CHARON_EMERGENCY_SERVER_ENABLED: "true" | |
| CHARON_SECURITY_TESTS_ENABLED: "false" # Cerberus OFF for non-security tests | |
| CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| total-shards: [4] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Log in to Docker Hub | |
| if: needs.build.outputs.image_source == 'registry' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull shared Docker image | |
| if: needs.build.outputs.image_source == 'registry' | |
| run: | | |
| docker pull "${{ needs.build.outputs.image_ref }}" | |
| docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}" | |
| docker images | grep charon | |
| - name: Download Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: docker-image | |
| - name: Load Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| run: | | |
| docker load -i charon-e2e-image.tar | |
| docker images | grep charon | |
| - name: Generate ephemeral encryption key | |
| run: echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" | |
| - name: Start test environment (Non-Security Profile) | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml up -d | |
| echo "✅ Container started for Chromium non-security tests (Cerberus OFF)" | |
| - 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://127.0.0.1:8080/api/v1/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| curl -s http://127.0.0.1:8080/api/v1/health | jq . | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Health check failed" | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs | |
| exit 1 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright Chromium | |
| run: | | |
| echo "📦 Installing Chromium..." | |
| npx playwright install --with-deps chromium | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Run Chromium Non-Security Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) | |
| run: | | |
| set -euo pipefail | |
| STATUS=0 | |
| echo "════════════════════════════════════════════" | |
| echo "Chromium Non-Security Tests - Shard ${{ matrix.shard }}/${{ matrix.total-shards }}" | |
| echo "Cerberus: DISABLED" | |
| echo "Execution: PARALLEL (sharded)" | |
| echo "Start Time: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| echo "════════════════════════════════════════════" | |
| SHARD_START=$(date +%s) | |
| echo "SHARD_START=$SHARD_START" >> "$GITHUB_ENV" | |
| npx playwright test \ | |
| --project=chromium \ | |
| --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ | |
| --output=playwright-output/chromium-shard-${{ matrix.shard }} \ | |
| tests/core \ | |
| tests/dns-provider-crud.spec.ts \ | |
| tests/dns-provider-types.spec.ts \ | |
| tests/integration \ | |
| tests/manual-dns-provider.spec.ts \ | |
| tests/monitoring \ | |
| tests/settings \ | |
| tests/tasks || STATUS=$? | |
| SHARD_END=$(date +%s) | |
| echo "SHARD_END=$SHARD_END" >> "$GITHUB_ENV" | |
| SHARD_DURATION=$((SHARD_END - SHARD_START)) | |
| echo "════════════════════════════════════════════" | |
| echo "Chromium Shard ${{ matrix.shard }} Complete | Duration: ${SHARD_DURATION}s" | |
| echo "════════════════════════════════════════════" | |
| echo "PLAYWRIGHT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| exit "$STATUS" | |
| env: | |
| PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 | |
| CI: true | |
| TEST_WORKER_INDEX: ${{ matrix.shard }} | |
| - name: Upload HTML report (Chromium shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-report-chromium-shard-${{ matrix.shard }} | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright output (Chromium shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-output-chromium-shard-${{ matrix.shard }} | |
| path: playwright-output/chromium-shard-${{ matrix.shard }}/ | |
| retention-days: 7 | |
| - name: Upload Chromium coverage (if enabled) | |
| if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1') | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-coverage-chromium-shard-${{ matrix.shard }} | |
| path: coverage/e2e/ | |
| retention-days: 7 | |
| - name: Upload test traces on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: traces-chromium-shard-${{ matrix.shard }} | |
| path: test-results/**/*.zip | |
| retention-days: 7 | |
| - name: Collect diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| uptime > diagnostics/uptime.txt | |
| free -m > diagnostics/free-m.txt | |
| df -h > diagnostics/df-h.txt | |
| ps aux > diagnostics/ps-aux.txt | |
| docker ps -a > diagnostics/docker-ps.txt || true | |
| docker logs --tail 500 charon-e2e > diagnostics/docker-charon-e2e.log 2>&1 || true | |
| - name: Upload diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-diagnostics-chromium-shard-${{ matrix.shard }} | |
| path: diagnostics/ | |
| retention-days: 7 | |
| - name: Collect Docker logs on failure | |
| if: failure() | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs > docker-logs-chromium-shard-${{ matrix.shard }}.txt 2>&1 | |
| - name: Upload Docker logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: docker-logs-chromium-shard-${{ matrix.shard }} | |
| path: docker-logs-chromium-shard-${{ matrix.shard }}.txt | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose -f .docker/compose/docker-compose.playwright-ci.yml down -v 2>/dev/null || true | |
| e2e-firefox: | |
| name: E2E Firefox (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: | | |
| ((inputs.browser || 'all') == 'firefox' || (inputs.browser || 'all') == 'all') && | |
| ((inputs.test_category || 'all') == 'non-security' || (inputs.test_category || 'all') == 'all') | |
| timeout-minutes: 60 | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| CHARON_EMERGENCY_SERVER_ENABLED: "true" | |
| CHARON_SECURITY_TESTS_ENABLED: "false" # Cerberus OFF for non-security tests | |
| CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| total-shards: [4] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Log in to Docker Hub | |
| if: needs.build.outputs.image_source == 'registry' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull shared Docker image | |
| if: needs.build.outputs.image_source == 'registry' | |
| run: | | |
| docker pull "${{ needs.build.outputs.image_ref }}" | |
| docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}" | |
| docker images | grep charon | |
| - name: Download Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: docker-image | |
| - name: Load Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| run: | | |
| docker load -i charon-e2e-image.tar | |
| docker images | grep charon | |
| - name: Generate ephemeral encryption key | |
| run: echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" | |
| - name: Start test environment (Non-Security Profile) | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml up -d | |
| echo "✅ Container started for Firefox non-security tests (Cerberus OFF)" | |
| - 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://127.0.0.1:8080/api/v1/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| curl -s http://127.0.0.1:8080/api/v1/health | jq . | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Health check failed" | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs | |
| exit 1 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright Chromium (required by security-tests dependency) | |
| run: | | |
| echo "📦 Installing Chromium (required by security-tests dependency)..." | |
| npx playwright install --with-deps chromium | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Install Playwright Firefox | |
| run: | | |
| echo "📦 Installing Firefox..." | |
| npx playwright install --with-deps firefox | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Run Firefox Non-Security Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) | |
| run: | | |
| set -euo pipefail | |
| STATUS=0 | |
| echo "════════════════════════════════════════════" | |
| echo "Firefox Non-Security Tests - Shard ${{ matrix.shard }}/${{ matrix.total-shards }}" | |
| echo "Cerberus: DISABLED" | |
| echo "Execution: PARALLEL (sharded)" | |
| echo "Start Time: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| echo "════════════════════════════════════════════" | |
| SHARD_START=$(date +%s) | |
| echo "SHARD_START=$SHARD_START" >> "$GITHUB_ENV" | |
| npx playwright test \ | |
| --project=firefox \ | |
| --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ | |
| --output=playwright-output/firefox-shard-${{ matrix.shard }} \ | |
| tests/core \ | |
| tests/dns-provider-crud.spec.ts \ | |
| tests/dns-provider-types.spec.ts \ | |
| tests/integration \ | |
| tests/manual-dns-provider.spec.ts \ | |
| tests/monitoring \ | |
| tests/settings \ | |
| tests/tasks || STATUS=$? | |
| SHARD_END=$(date +%s) | |
| echo "SHARD_END=$SHARD_END" >> "$GITHUB_ENV" | |
| SHARD_DURATION=$((SHARD_END - SHARD_START)) | |
| echo "════════════════════════════════════════════" | |
| echo "Firefox Shard ${{ matrix.shard }} Complete | Duration: ${SHARD_DURATION}s" | |
| echo "════════════════════════════════════════════" | |
| echo "PLAYWRIGHT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| exit "$STATUS" | |
| env: | |
| PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 | |
| CI: true | |
| TEST_WORKER_INDEX: ${{ matrix.shard }} | |
| - name: Upload HTML report (Firefox shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-report-firefox-shard-${{ matrix.shard }} | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright output (Firefox shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: playwright-output-firefox-shard-${{ matrix.shard }} | |
| path: playwright-output/firefox-shard-${{ matrix.shard }}/ | |
| retention-days: 7 | |
| - name: Upload Firefox coverage (if enabled) | |
| if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1') | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-coverage-firefox-shard-${{ matrix.shard }} | |
| path: coverage/e2e/ | |
| retention-days: 7 | |
| - name: Upload test traces on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: traces-firefox-shard-${{ matrix.shard }} | |
| path: test-results/**/*.zip | |
| retention-days: 7 | |
| - name: Collect diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| uptime > diagnostics/uptime.txt | |
| free -m > diagnostics/free-m.txt | |
| df -h > diagnostics/df-h.txt | |
| ps aux > diagnostics/ps-aux.txt | |
| docker ps -a > diagnostics/docker-ps.txt || true | |
| docker logs --tail 500 charon-e2e > diagnostics/docker-charon-e2e.log 2>&1 || true | |
| - name: Upload diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: e2e-diagnostics-firefox-shard-${{ matrix.shard }} | |
| path: diagnostics/ | |
| retention-days: 7 | |
| - name: Collect Docker logs on failure | |
| if: failure() | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs > docker-logs-firefox-shard-${{ matrix.shard }}.txt 2>&1 | |
| - name: Upload Docker logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: docker-logs-firefox-shard-${{ matrix.shard }} | |
| path: docker-logs-firefox-shard-${{ matrix.shard }}.txt | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose -f .docker/compose/docker-compose.playwright-ci.yml down -v 2>/dev/null || true | |
| e2e-webkit: | |
| name: E2E WebKit (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: | | |
| ((inputs.browser || 'all') == 'webkit' || (inputs.browser || 'all') == 'all') && | |
| ((inputs.test_category || 'all') == 'non-security' || (inputs.test_category || 'all') == 'all') | |
| timeout-minutes: 60 | |
| env: | |
| CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} | |
| CHARON_EMERGENCY_SERVER_ENABLED: "true" | |
| CHARON_SECURITY_TESTS_ENABLED: "false" # Cerberus OFF for non-security tests | |
| CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| total-shards: [4] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Log in to Docker Hub | |
| if: needs.build.outputs.image_source == 'registry' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Pull shared Docker image | |
| if: needs.build.outputs.image_source == 'registry' | |
| run: | | |
| docker pull "${{ needs.build.outputs.image_ref }}" | |
| docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}" | |
| docker images | grep charon | |
| - name: Download Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: docker-image | |
| - name: Load Docker image artifact | |
| if: needs.build.outputs.image_source == 'build' | |
| run: | | |
| docker load -i charon-e2e-image.tar | |
| docker images | grep charon | |
| - name: Generate ephemeral encryption key | |
| run: echo "CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" | |
| - name: Start test environment (Non-Security Profile) | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml up -d | |
| echo "✅ Container started for WebKit non-security tests (Cerberus OFF)" | |
| - 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://127.0.0.1:8080/api/v1/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| curl -s http://127.0.0.1:8080/api/v1/health | jq . | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Health check failed" | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs | |
| exit 1 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright Chromium (required by security-tests dependency) | |
| run: | | |
| echo "📦 Installing Chromium (required by security-tests dependency)..." | |
| npx playwright install --with-deps chromium | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Install Playwright WebKit | |
| run: | | |
| echo "📦 Installing WebKit..." | |
| npx playwright install --with-deps webkit | |
| EXIT_CODE=$? | |
| echo "✅ Install command completed (exit code: $EXIT_CODE)" | |
| exit "$EXIT_CODE" | |
| - name: Run WebKit Non-Security Tests (Shard ${{ matrix.shard }}/${{ matrix.total-shards }}) | |
| run: | | |
| set -euo pipefail | |
| STATUS=0 | |
| echo "════════════════════════════════════════════" | |
| echo "WebKit Non-Security Tests - Shard ${{ matrix.shard }}/${{ matrix.total-shards }}" | |
| echo "Cerberus: DISABLED" | |
| echo "Execution: PARALLEL (sharded)" | |
| echo "Start Time: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| echo "════════════════════════════════════════════" | |
| SHARD_START=$(date +%s) | |
| echo "SHARD_START=$SHARD_START" >> "$GITHUB_ENV" | |
| npx playwright test \ | |
| --project=webkit \ | |
| --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ | |
| --output=playwright-output/webkit-shard-${{ matrix.shard }} \ | |
| tests/core \ | |
| tests/dns-provider-crud.spec.ts \ | |
| tests/dns-provider-types.spec.ts \ | |
| tests/integration \ | |
| tests/manual-dns-provider.spec.ts \ | |
| tests/monitoring \ | |
| tests/settings \ | |
| tests/tasks || STATUS=$? | |
| SHARD_END=$(date +%s) | |
| echo "SHARD_END=$SHARD_END" >> "$GITHUB_ENV" | |
| SHARD_DURATION=$((SHARD_END - SHARD_START)) | |
| echo "════════════════════════════════════════════" | |
| echo "WebKit Shard ${{ matrix.shard }} Complete | Duration: ${SHARD_DURATION}s" | |
| echo "════════════════════════════════════════════" | |
| echo "PLAYWRIGHT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| exit "$STATUS" | |
| env: | |
| PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 | |
| CI: true | |
| TEST_WORKER_INDEX: ${{ matrix.shard }} | |
| - name: Upload HTML report (WebKit shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: playwright-report-webkit-shard-${{ matrix.shard }} | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright output (WebKit shard ${{ matrix.shard }}) | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: playwright-output-webkit-shard-${{ matrix.shard }} | |
| path: playwright-output/webkit-shard-${{ matrix.shard }}/ | |
| retention-days: 7 | |
| - name: Upload WebKit coverage (if enabled) | |
| if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1') | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: e2e-coverage-webkit-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-webkit-shard-${{ matrix.shard }} | |
| path: test-results/**/*.zip | |
| retention-days: 7 | |
| - name: Collect diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| uptime > diagnostics/uptime.txt | |
| free -m > diagnostics/free-m.txt | |
| df -h > diagnostics/df-h.txt | |
| ps aux > diagnostics/ps-aux.txt | |
| docker ps -a > diagnostics/docker-ps.txt || true | |
| docker logs --tail 500 charon-e2e > diagnostics/docker-charon-e2e.log 2>&1 || true | |
| - name: Upload diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: e2e-diagnostics-webkit-shard-${{ matrix.shard }} | |
| path: diagnostics/ | |
| retention-days: 7 | |
| - name: Collect Docker logs on failure | |
| if: failure() | |
| run: | | |
| docker compose -f .docker/compose/docker-compose.playwright-ci.yml logs > docker-logs-webkit-shard-${{ matrix.shard }}.txt 2>&1 | |
| - name: Upload Docker logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: docker-logs-webkit-shard-${{ matrix.shard }} | |
| path: docker-logs-webkit-shard-${{ matrix.shard }}.txt | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose -f .docker/compose/docker-compose.playwright-ci.yml down -v 2>/dev/null || true | |
| # Test summary job | |
| test-summary: | |
| name: E2E Test Summary | |
| runs-on: ubuntu-latest | |
| needs: [e2e-chromium-security, e2e-firefox-security, e2e-webkit-security, e2e-chromium, e2e-firefox, e2e-webkit] | |
| if: always() | |
| steps: | |
| - name: Generate job summary | |
| run: | | |
| { | |
| echo "## 📊 E2E Test Results (Split: Security + Sharded)" | |
| echo "" | |
| echo "### Architecture: 15 Total Jobs" | |
| echo "" | |
| echo "#### Security Enforcement (3 jobs)" | |
| echo "| Browser | Status | Shards | Timeout | Cerberus |" | |
| echo "|---------|--------|--------|---------|----------|" | |
| echo "| Chromium | ${{ needs.e2e-chromium-security.result }} | 1 | 30min | ON |" | |
| echo "| Firefox | ${{ needs.e2e-firefox-security.result }} | 1 | 30min | ON |" | |
| echo "| WebKit | ${{ needs.e2e-webkit-security.result }} | 1 | 30min | ON |" | |
| echo "" | |
| echo "#### Non-Security Tests (12 jobs)" | |
| echo "| Browser | Status | Shards | Timeout | Cerberus |" | |
| echo "|---------|--------|--------|---------|----------|" | |
| echo "| Chromium | ${{ needs.e2e-chromium.result }} | 4 | 20min | OFF |" | |
| echo "| Firefox | ${{ needs.e2e-firefox.result }} | 4 | 20min | OFF |" | |
| echo "| WebKit | ${{ needs.e2e-webkit.result }} | 4 | 20min | OFF |" | |
| echo "" | |
| echo "### Benefits" | |
| echo "" | |
| echo "- ✅ **Isolation:** Security tests run independently without ACL/rate limit interference" | |
| echo "- ✅ **Performance:** Non-security tests sharded 4-way for faster execution" | |
| echo "- ✅ **Reliability:** Cerberus OFF by default prevents cross-shard contamination" | |
| echo "- ✅ **Clarity:** Separate artifacts for security vs non-security test results" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # Final status check | |
| e2e-results: | |
| name: E2E Test Results (Final) | |
| runs-on: ubuntu-latest | |
| needs: [e2e-chromium-security, e2e-firefox-security, e2e-webkit-security, e2e-chromium, e2e-firefox, e2e-webkit] | |
| if: always() | |
| steps: | |
| - name: Check test results | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| EFFECTIVE_BROWSER: ${{ inputs.browser || 'all' }} | |
| EFFECTIVE_CATEGORY: ${{ inputs.test_category || 'all' }} | |
| NEEDS_JSON: ${{ toJson(needs) }} | |
| with: | |
| script: | | |
| const needs = JSON.parse(process.env.NEEDS_JSON || '{}'); | |
| const effectiveBrowser = process.env.EFFECTIVE_BROWSER || 'all'; | |
| const effectiveCategory = process.env.EFFECTIVE_CATEGORY || 'all'; | |
| const shouldRunSecurity = effectiveCategory === 'security' || effectiveCategory === 'all'; | |
| const shouldRunNonSecurity = effectiveCategory === 'non-security' || effectiveCategory === 'all'; | |
| const shouldRun = { | |
| chromiumSecurity: (effectiveBrowser === 'chromium' || effectiveBrowser === 'all') && shouldRunSecurity, | |
| firefoxSecurity: (effectiveBrowser === 'firefox' || effectiveBrowser === 'all') && shouldRunSecurity, | |
| webkitSecurity: (effectiveBrowser === 'webkit' || effectiveBrowser === 'all') && shouldRunSecurity, | |
| chromium: (effectiveBrowser === 'chromium' || effectiveBrowser === 'all') && shouldRunNonSecurity, | |
| firefox: (effectiveBrowser === 'firefox' || effectiveBrowser === 'all') && shouldRunNonSecurity, | |
| webkit: (effectiveBrowser === 'webkit' || effectiveBrowser === 'all') && shouldRunNonSecurity, | |
| }; | |
| const results = { | |
| chromiumSecurity: needs['e2e-chromium-security']?.result || 'skipped', | |
| firefoxSecurity: needs['e2e-firefox-security']?.result || 'skipped', | |
| webkitSecurity: needs['e2e-webkit-security']?.result || 'skipped', | |
| chromium: needs['e2e-chromium']?.result || 'skipped', | |
| firefox: needs['e2e-firefox']?.result || 'skipped', | |
| webkit: needs['e2e-webkit']?.result || 'skipped', | |
| }; | |
| core.info('Security Enforcement Results:'); | |
| core.info(` Chromium Security: ${results.chromiumSecurity}`); | |
| core.info(` Firefox Security: ${results.firefoxSecurity}`); | |
| core.info(` WebKit Security: ${results.webkitSecurity}`); | |
| core.info(''); | |
| core.info('Non-Security Results:'); | |
| core.info(` Chromium: ${results.chromium}`); | |
| core.info(` Firefox: ${results.firefox}`); | |
| core.info(` WebKit: ${results.webkit}`); | |
| const failures = []; | |
| const invalidResults = new Set(['skipped', 'failure', 'cancelled']); | |
| const labels = { | |
| chromiumSecurity: 'Chromium Security', | |
| firefoxSecurity: 'Firefox Security', | |
| webkitSecurity: 'WebKit Security', | |
| chromium: 'Chromium', | |
| firefox: 'Firefox', | |
| webkit: 'WebKit', | |
| }; | |
| for (const [key, shouldRunJob] of Object.entries(shouldRun)) { | |
| const result = results[key]; | |
| if (shouldRunJob && invalidResults.has(result)) { | |
| failures.push(`${labels[key]} expected to run but result was ${result}`); | |
| } | |
| } | |
| if (failures.length > 0) { | |
| core.error('One or more expected browser jobs did not succeed:'); | |
| failures.forEach((failure) => core.error(`- ${failure}`)); | |
| core.setFailed('Expected E2E jobs did not complete successfully.'); | |
| } else { | |
| core.info('All expected browser tests succeeded'); | |
| } |