The Dockerfile already centralizes all version pins into top-level ARGs
(GO_VERSION, ALPINE_IMAGE, CROWDSEC_VERSION, EXPR_LANG_VERSION, XNET_VERSION).
This change closes the remaining gaps so those ARGs are the single source of
truth end-to-end:
- nightly-build.yml now resolves the Alpine image digest at build time and
passes ALPINE_IMAGE as a build-arg, matching the docker-build.yml pattern.
Previously, nightly images were built with the Dockerfile ARG default and
without a pinned digest, making runtime Alpine differ from docker-build.yml.
- six CI workflows (quality-checks, codecov-upload, benchmark, e2e-tests-split,
release-goreleaser, codeql) declared a GO_VERSION env var but their setup-go
steps ignored it and hardcoded the version string directly. They now reference
${{ env.GO_VERSION }}, so Renovate only needs to update one value per file
and the env var actually serves its purpose.
- codeql.yml had no GO_VERSION env var at all; one is now added alongside the
existing GOTOOLCHAIN: auto entry.
When Renovate bumps Go, it updates the env var at the top of each workflow and
the Dockerfile ARG — zero manual hunting required.
1677 lines
67 KiB
YAML
1677 lines
67 KiB
YAML
# 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:
|
||
|
||
env:
|
||
NODE_VERSION: '20'
|
||
GO_VERSION: '1.26.1'
|
||
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: ${{ github.workflow }}-${{ github.ref }}
|
||
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@4b73464bb391d4059bd26b0524d20df3927bd417 # 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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||
|
||
- name: Build Docker image
|
||
id: build-image
|
||
if: steps.resolve-image.outputs.image_source == 'build'
|
||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||
with:
|
||
node-version: ${{ env.NODE_VERSION }}
|
||
cache: 'npm'
|
||
|
||
- name: Preflight disk diagnostics (before cleanup)
|
||
run: |
|
||
echo "Disk usage before cleanup"
|
||
df -h
|
||
docker system df || true
|
||
|
||
- name: Preflight cleanup (best effort)
|
||
run: |
|
||
echo "Best-effort cleanup for CI runner"
|
||
docker system prune -af || true
|
||
rm -rf playwright-report playwright-output coverage/e2e test-results diagnostics || true
|
||
rm -f docker-logs-*.txt charon-e2e-image.tar || true
|
||
|
||
- name: Preflight disk diagnostics and threshold gate
|
||
run: |
|
||
set -euo pipefail
|
||
MIN_FREE_BYTES=$((5 * 1024 * 1024 * 1024))
|
||
echo "Disk usage after cleanup"
|
||
df -h
|
||
docker system df || true
|
||
|
||
WORKSPACE_PATH="${GITHUB_WORKSPACE:-$PWD}"
|
||
FREE_ROOT_BYTES=$(df -PB1 / | awk 'NR==2 {print $4}')
|
||
FREE_WORKSPACE_BYTES=$(df -PB1 "$WORKSPACE_PATH" | awk 'NR==2 {print $4}')
|
||
|
||
echo "Free bytes on /: $FREE_ROOT_BYTES"
|
||
echo "Free bytes on workspace ($WORKSPACE_PATH): $FREE_WORKSPACE_BYTES"
|
||
|
||
if [ "$FREE_ROOT_BYTES" -lt "$MIN_FREE_BYTES" ] || [ "$FREE_WORKSPACE_BYTES" -lt "$MIN_FREE_BYTES" ]; then
|
||
echo "::error::[CI_DISK_PRESSURE] Insufficient free disk after cleanup. Required >= 5GiB on both / and workspace. root=${FREE_ROOT_BYTES}B workspace=${FREE_WORKSPACE_BYTES}B"
|
||
exit 42
|
||
fi
|
||
|
||
- name: Log in to Docker Hub
|
||
if: needs.build.outputs.image_source == 'registry'
|
||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||
with:
|
||
node-version: ${{ env.NODE_VERSION }}
|
||
cache: 'npm'
|
||
|
||
- name: Preflight disk diagnostics (before cleanup)
|
||
run: |
|
||
echo "Disk usage before cleanup"
|
||
df -h
|
||
docker system df || true
|
||
|
||
- name: Preflight cleanup (best effort)
|
||
run: |
|
||
echo "Best-effort cleanup for CI runner"
|
||
docker system prune -af || true
|
||
rm -rf playwright-report playwright-output coverage/e2e test-results diagnostics || true
|
||
rm -f docker-logs-*.txt charon-e2e-image.tar || true
|
||
|
||
- name: Preflight disk diagnostics and threshold gate
|
||
run: |
|
||
set -euo pipefail
|
||
MIN_FREE_BYTES=$((5 * 1024 * 1024 * 1024))
|
||
echo "Disk usage after cleanup"
|
||
df -h
|
||
docker system df || true
|
||
|
||
WORKSPACE_PATH="${GITHUB_WORKSPACE:-$PWD}"
|
||
FREE_ROOT_BYTES=$(df -PB1 / | awk 'NR==2 {print $4}')
|
||
FREE_WORKSPACE_BYTES=$(df -PB1 "$WORKSPACE_PATH" | awk 'NR==2 {print $4}')
|
||
|
||
echo "Free bytes on /: $FREE_ROOT_BYTES"
|
||
echo "Free bytes on workspace ($WORKSPACE_PATH): $FREE_WORKSPACE_BYTES"
|
||
|
||
if [ "$FREE_ROOT_BYTES" -lt "$MIN_FREE_BYTES" ] || [ "$FREE_WORKSPACE_BYTES" -lt "$MIN_FREE_BYTES" ]; then
|
||
echo "::error::[CI_DISK_PRESSURE] Insufficient free disk after cleanup. Required >= 5GiB on both / and workspace. root=${FREE_ROOT_BYTES}B workspace=${FREE_WORKSPACE_BYTES}B"
|
||
exit 42
|
||
fi
|
||
|
||
- name: Log in to Docker Hub
|
||
if: needs.build.outputs.image_source == 'registry'
|
||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||
with:
|
||
node-version: ${{ env.NODE_VERSION }}
|
||
cache: 'npm'
|
||
|
||
- name: Preflight disk diagnostics (before cleanup)
|
||
run: |
|
||
echo "Disk usage before cleanup"
|
||
df -h
|
||
docker system df || true
|
||
|
||
- name: Preflight cleanup (best effort)
|
||
run: |
|
||
echo "Best-effort cleanup for CI runner"
|
||
docker system prune -af || true
|
||
rm -rf playwright-report playwright-output coverage/e2e test-results diagnostics || true
|
||
rm -f docker-logs-*.txt charon-e2e-image.tar || true
|
||
|
||
- name: Preflight disk diagnostics and threshold gate
|
||
run: |
|
||
set -euo pipefail
|
||
MIN_FREE_BYTES=$((5 * 1024 * 1024 * 1024))
|
||
echo "Disk usage after cleanup"
|
||
df -h
|
||
docker system df || true
|
||
|
||
WORKSPACE_PATH="${GITHUB_WORKSPACE:-$PWD}"
|
||
FREE_ROOT_BYTES=$(df -PB1 / | awk 'NR==2 {print $4}')
|
||
FREE_WORKSPACE_BYTES=$(df -PB1 "$WORKSPACE_PATH" | awk 'NR==2 {print $4}')
|
||
|
||
echo "Free bytes on /: $FREE_ROOT_BYTES"
|
||
echo "Free bytes on workspace ($WORKSPACE_PATH): $FREE_WORKSPACE_BYTES"
|
||
|
||
if [ "$FREE_ROOT_BYTES" -lt "$MIN_FREE_BYTES" ] || [ "$FREE_WORKSPACE_BYTES" -lt "$MIN_FREE_BYTES" ]; then
|
||
echo "::error::[CI_DISK_PRESSURE] Insufficient free disk after cleanup. Required >= 5GiB on both / and workspace. root=${FREE_ROOT_BYTES}B workspace=${FREE_WORKSPACE_BYTES}B"
|
||
exit 42
|
||
fi
|
||
|
||
- name: Log in to Docker Hub
|
||
if: needs.build.outputs.image_source == 'registry'
|
||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||
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');
|
||
}
|