name: Playwright on: workflow_dispatch: inputs: sandbox_provider: description: Sandbox provider required: true type: choice options: - fake - e2b - daytona - box default: fake workflow_call: inputs: sandbox_provider: description: Sandbox provider required: false type: string default: fake publish_report: description: Publish this run to the persistent visual dashboard required: false type: boolean default: false hand_off_report: description: Leave the report on the runner host for the trusted publish workflow required: false type: boolean default: false secrets: S3_ACCESS_KEY_ID: description: Access key used to publish visual reports required: false S3_SECRET_ACCESS_KEY: description: Secret key used to publish visual reports required: false E2B_API_KEY: description: E2B API key used only when the E2B sandbox is selected required: false DAYTONA_API_KEY: description: Daytona API key used only when the Daytona sandbox is selected required: false BOX_API_KEY: description: Box API key used only when the Box sandbox is selected required: false permissions: contents: read jobs: playwright: name: Web E2E runs-on: [self-hosted, evoo-ci, Linux] timeout-minutes: 60 concurrency: group: ${{ inputs.publish_report && 'playwright-publication' || format('playwright-{0}', github.run_id) }} cancel-in-progress: false steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 id: pnpm-setup with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} - name: Isolate pnpm store and PATH shell: bash run: | set -euo pipefail bin="${{ steps.pnpm-setup.outputs.bin_dest }}" dest="${{ steps.pnpm-setup.outputs.dest }}" if [[ -z "$bin" || -z "$dest" ]]; then echo "pnpm setup did not report install paths" >&2 exit 1 fi echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" echo "${bin}" >> "$GITHUB_PATH" echo "${dest}" >> "$GITHUB_PATH" store="${RUNNER_TOOL_CACHE}/pnpm-store" mkdir -p "$store" echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 - run: pnpm install --frozen-lockfile - name: Clear stale Vite dependency cache run: rm -rf apps/web/node_modules/.vite node_modules/.vite - name: Install Chromium timeout-minutes: 5 # Browsers persist in ~/.cache/ms-playwright on the runner host; no GitHub cache. # Skip --with-deps: evo Linux runners keep Chromium's OS libraries installed, # and apt-get can stall until the job timeout on persistent self-hosted machines. run: pnpm --filter @rakazo/web exec playwright install chromium - name: Validate E2B credentials if: inputs.sandbox_provider == 'e2b' env: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} run: test -n "$E2B_API_KEY" - name: Validate Daytona credentials if: inputs.sandbox_provider == 'daytona' env: DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} run: test -n "$DAYTONA_API_KEY" - name: Validate Box credentials if: inputs.sandbox_provider == 'box' env: BOX_API_KEY: ${{ secrets.BOX_API_KEY }} run: test -n "$BOX_API_KEY" - name: Pick E2E ports for this runner # Several runners share one host. Each runner gets its own port set so the # port-freeing steps and the harness never touch another job's servers. No runner # uses the harness defaults: jobs from branches that still free the fixed ports # cannot kill ours, and we cannot kill theirs. run: | set -euo pipefail suffix="${RUNNER_NAME##*-}" offset=100 if [[ "$suffix" =~ ^[0-9]+$ ]]; then offset=$((suffix * 100)) fi { echo "API_PORT=$((3110 + offset))" echo "WEB_PORT=$((5180 + offset))" echo "WWW_PORT=$((4321 + offset))" } >> "$GITHUB_ENV" echo "E2E ports for ${RUNNER_NAME}: api $((3110 + offset)), web $((5180 + offset)), www $((4321 + offset))" - name: Free stale E2E ports run: bash scripts/free-e2e-ports.sh - name: Run Playwright tests id: playwright_tests env: SANDBOX_TEST_PROVIDER: ${{ inputs.sandbox_provider }} E2B_API_KEY: ${{ inputs.sandbox_provider == 'e2b' && secrets.E2B_API_KEY || '' }} DAYTONA_API_KEY: ${{ inputs.sandbox_provider == 'daytona' && secrets.DAYTONA_API_KEY || '' }} BOX_API_KEY: ${{ inputs.sandbox_provider == 'box' && secrets.BOX_API_KEY || '' }} run: pnpm test:e2e -- --sandbox="$SANDBOX_TEST_PROVIDER" # Marketing homepage only: separate config so product chromium does not start Astro. # Runs after product e2e so shared apps/web/test-results is not wiped by the product run. - name: Free stale marketing E2E ports if: always() && (steps.playwright_tests.outcome == 'success' || steps.playwright_tests.outcome == 'failure') run: bash scripts/free-e2e-ports.sh - name: Run marketing homepage E2E id: www_playwright_tests if: always() && (steps.playwright_tests.outcome == 'success' || steps.playwright_tests.outcome == 'failure') run: pnpm --filter @rakazo/www e2e - name: Record Playwright result if: always() && inputs.hand_off_report env: PLAYWRIGHT_RESULT: ${{ steps.playwright_tests.outcome }} WWW_PLAYWRIGHT_RESULT: ${{ steps.www_playwright_tests.outcome }} run: | mkdir -p test-report/e2e if [[ "$PLAYWRIGHT_RESULT" == "success" && "$WWW_PLAYWRIGHT_RESULT" == "success" ]]; then printf 'success\n' > test-report/e2e/outcome.txt elif [[ "$PLAYWRIGHT_RESULT" == "success" && ( "$WWW_PLAYWRIGHT_RESULT" == "skipped" || -z "$WWW_PLAYWRIGHT_RESULT" ) ]]; then printf 'success\n' > test-report/e2e/outcome.txt else printf 'failure\n' > test-report/e2e/outcome.txt fi - name: Hand off Playwright report on the runner host if: always() && inputs.hand_off_report continue-on-error: true # Never GitHub artifact storage: the publish workflow runs on the same evo host # and reads this archive as untrusted input. env: HANDOFF_ARCHIVE: ${{ github.run_id }}-${{ github.run_attempt }}.zip run: | set -euo pipefail handoff="$HOME/.cache/evoo-ci/playwright-handoff" mkdir -p "$handoff" python3 - "$handoff/$HANDOFF_ARCHIVE.partial" playwright-report playwright-report-www test-report/e2e <<'PY' import os, sys, zipfile with zipfile.ZipFile(sys.argv[1], "w", zipfile.ZIP_DEFLATED) as archive: for root in sys.argv[2:]: for directory, _, files in os.walk(root): for name in files: file = os.path.join(directory, name) if os.path.isfile(file) and not os.path.islink(file): archive.write(file) PY mv "$handoff/$HANDOFF_ARCHIVE.partial" "$handoff/$HANDOFF_ARCHIVE" - name: Publish Playwright visual report if: always() && inputs.publish_report env: AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: ${{ vars.S3_REGION }} AWS_EC2_METADATA_DISABLED: "true" S3_BUCKET: ${{ vars.S3_BUCKET }} S3_ENDPOINT: ${{ vars.S3_ENDPOINT }} PLAYWRIGHT_PUBLIC_BASE_URL: ${{ vars.PLAYWRIGHT_PUBLIC_BASE_URL }} PLAYWRIGHT_RESULT: ${{ steps.playwright_tests.outcome == 'success' && steps.www_playwright_tests.outcome == 'success' && 'success' || 'failure' }} PLAYWRIGHT_RUN_ATTEMPT: ${{ github.run_attempt }} PLAYWRIGHT_RUN_ID: ${{ github.run_id }} PLAYWRIGHT_RUN_NUMBER: ${{ github.run_number }} PLAYWRIGHT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} PLAYWRIGHT_SHA: ${{ github.sha }} PLAYWRIGHT_EVENT: ${{ github.event_name }} PLAYWRIGHT_BRANCH: ${{ github.ref_name }} run: bash scripts/publish-playwright-report.sh