evOo-local/.github/workflows/playwright.yml
cynarAI 9c9188e657
ci: publish amd64 images under a lowercase name and deploy only after green CI (#97)
* ci: publish amd64 images under a lowercase name and deploy only after green CI

Image publishing never succeeded: GHCR rejects the capitalized repository
slug, and the arm64 jobs failed on the macOS runners, which have no Docker
buildx. Resolve the namespace in lowercase and build amd64 only until an
arm64 runner with buildx exists; the manifest check expects one digest.

The Evo deploy ran on every push to main regardless of the result of
Product CI, so red commits went live. Trigger it from the completed ci
workflow instead and deploy the head it verified; manual dispatch stays.

The Playwright report publish failed every nightly run because the S3
settings are not configured. The script now skips the gallery with a
notice when they are missing instead of failing the job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* ci: give each runner on a shared host its own E2E ports

Two Linux runners live on the same host. Every Web E2E job first frees
"stale" listeners on the fixed ports 3110, 3111, 5180 and 4321 with
fuser -k, so a job starting on the second runner killed the Vite server
of the job already running on the first one. The victim died with
SIGKILL (exit 137), and its remaining tests failed on reloads that could
no longer reach a server. This is what made the Web E2E run on this PR
red, and it explains the recurring reload failures on main whenever two
runs overlapped.

The workflow now derives API_PORT, WEB_PORT and WWW_PORT from the runner
name (suffix -N means an offset of N times 100), and the port-freeing
script follows those settings instead of a hard-coded list. The harness,
Vite, the API and the marketing site already read these variables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* test(testkit): cover the env-driven E2E ports instead of a fixed list

The ports test parsed a hard-coded array that no longer exists. It now
runs the script on a fake host whose fuser finds nothing, and checks the
default ports, the per-runner override, the rejection of a non-numeric
setting, and that the workflow picks the ports before freeing them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* test(updater): expect the amd64-only publish and the lowercase namespace

The workflow test still described the multi-arch publish. It now checks
that only amd64 builds natively, that arm64 is documented as paused, that
one digest feeds the manifest, and that every image name comes from the
lowercase namespace step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* ci: keep every runner off the default E2E ports

Branches that still free the fixed ports 3110, 3111, 5180 and 4321 keep
running on the shared host (main, the nightly run, other open PRs) until
they carry the per-runner ports. A job of ours on the runner without a
numeric suffix used exactly those defaults, so such a neighbour could
still kill it, and its own port freeing could kill the neighbour. The
unsuffixed runner now starts at offset 100, so no job from this workflow
touches the defaults in either direction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(web): recover lazy routes and overlays from a failed module fetch

When the browser aborts a module request while a page loads, the lazy
import rejects and the route throws, leaving a blank page. Chromium does
this whenever a network interface appears or disappears mid-request
(net::ERR_NETWORK_CHANGED), which happens on a laptop that changes
networks and on the CI host every time a container starts or stops. A
Web E2E run hit exactly this: two modules of the onboarding page failed
to load and the suite waited on an empty document.

Lazy routes and overlays now load through a helper that retries the
import once and, when the browser keeps rejecting it from its module
map, reloads the page a single time before surfacing the error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit d5dacb12f14380071ef14ba8845d18c9d39e3fac)

* fix(web): reload once when the module graph is aborted while booting

The lazy-route recovery cannot help when the browser aborts a module of
the static import graph: nothing of the app runs, the root stays empty,
and the only trace is a console error. A Web E2E run hit this right
after a page reload, when four modules of the shell failed with
net::ERR_NETWORK_CHANGED and the shell never rendered.

An inline script in the HTML entry now listens for the error event of
the module script and reloads the page once, guarded by a session flag
that the entry module clears after a successful boot. The existing
preload recovery stays in place; it only covers Vite's chunk preloads of
production builds.

A new E2E spec aborts one module request during boot and one lazy route
request, and expects the sign-in page to render in both cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit e5b7e036e1a60e7e270592c4b9382ee23ed45ccb)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-02 20:54:51 +02:00

197 lines
8.0 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: 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.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