evOo-local/.github/workflows/playwright.yml
cynarAI 5cd1bc906f ci: Product CI merge gate and Autopilot auto-merge (#84)
* ci: add Product CI merge gate and Autopilot auto-merge path

Aggregate lint, typecheck, builds, unit, Postgres journeys, and Web E2E
into a single Product CI check for branch protection. Soft-fail artifact
uploads and skip headless macOS screenshots so infra noise does not block
merge. Add autopilot/auto-merge workflow and MERGE_GATES.md.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(ci): keep autopilot auto-merge inert without label

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(ci): run autopilot auto-merge on self-hosted runner

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(ci): free stale E2E ports before Playwright bind on Linux

Self-hosted evoo-ci runners can retain listeners on harness ports
(3110 API, 5180 web, 4321 marketing, 3111 device control) after a
failed run, causing EADDRINUSE before the next job. Add a guarded
free-e2e-ports.sh that uses fuser on Linux only and never touches
live dev ports 3100/5173. Wire it into the Playwright workflow and
the E2E harness with clear logging.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): stabilize message More menu Escape in hover E2E

The desktop hover spec assumed Escape always closed the first page-level
menu within one keypress. On self-hosted Chromium the Copy menu could stay
open while the rail remained visible, failing getByRole('menu') count.

Wait for the Copy item to prove the menu opened, then retry Escape until
that item hides while asserting the More control stays on the hover rail.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): retry sign-up navigation in Playwright helpers

Late-suite specs can miss the sign-up heading when Vite is still
warming on self-hosted runners. Retry goto until the form renders
instead of failing once at the default timeout.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-24 00:57:10 +02:00

179 lines
7.2 KiB
YAML

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
upload_artifacts:
description: Retain the report and diagnostics as GitHub artifacts
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: 30
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
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Clear stale Vite dependency cache
run: rm -rf apps/web/node_modules/.vite node_modules/.vite
- uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install Chromium
timeout-minutes: 5
# 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: 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.upload_artifacts
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: Upload Playwright artifacts
if: always() && inputs.upload_artifacts
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts-${{ github.run_id }}-${{ github.run_attempt }}
path: |
playwright-report
playwright-report-www
apps/web/test-results
test-report/e2e
retention-days: 7
if-no-files-found: warn
- 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