Browser tests
Offload a Playwright e2e suite to the shipped `playwright-e2e` run — sharded across the Browser Rendering pool, results posted back as a Check Run.
Fires on Every PR push — wire it any of these ways:
- Action recommended A ci.yml step dispatches the run — when it must interleave with other CI jobs or gate the PR.
- Webhook The GitHub App webhook fires the run directly — no .github/workflows file, no GHA minutes. (opt-in via the request-e2e label; baseURL from CONFIG_KV)
- Schedule pattern A Cloudflare Cron Trigger fires it on a wall-clock cadence — no GitHub event, no workflow file. (fan out against fixed environments on a cron)
- Use case
- Playwright e2e suite, sharded across the browser pool
- FlareDispatch shape
- 1 typed run + dispatch
- Plain GHA shape
- 1 workflow · matrix + merge-reports job
Source
The recommended FlareDispatch shape is shown first. Toggle to Without FlareDispatch to see the full GitHub Actions workflow a team would maintain to do the same job without it.
Action mode — triggered by a GitHub Actions workflow that calls the shipped run.
# Recipe: browser tests (Playwright e2e) on Cloudflare
#
# Use case: a Playwright suite that is slow and burns GHA minutes. Offload it
# to the shipped `playwright-e2e` run, which shards the suite across the
# Cloudflare Browser Rendering pool. GHA only fires the dispatch (~10 s) and
# the result comes back as a check-run.
#
# Mode: Action mode, fire-and-forget. See specs/04-gha-integration.md.
# Run: playwright-e2e — see specs/02-runs.md#3-playwright-e2e.
name: e2e
on:
pull_request:
paths: ["apps/**", "packages/**"]
jobs:
browser-tests:
runs-on: ubuntu-latest
steps:
- uses: openhackersclub/flare-dispatch-action@v1
with:
run: playwright-e2e
endpoint: ${{ vars.FLAREDISPATCH_ENDPOINT }}
hmac-secret: ${{ secrets.FLAREDISPATCH_HMAC }}
inputs: |
{
"repo": "${{ github.repository }}",
"sha": "${{ github.sha }}",
"baseURL": "https://staging.example.com",
"shards": 4,
"browserMode": "cf-browser-rendering",
"uploadReport": true
}
# Branch protection: require the check-run `flare-dispatch/playwright-e2e`,
# not this GHA job — the job succeeds the moment dispatch is accepted. // Recipe: browser tests — the `playwright-e2e` Run
//
// Re-exports the canonical run from runs/playwright-e2e.ts (the Dispatcher's
// registered implementation). Copy the runs/playwright-e2e.ts file verbatim
// into your own repo when forking this recipe — the indirection here keeps
// the recipe page in sync with the registered run without duplicating code.
//
// Spec: specs/02-runs.md § 3. DSL: specs/03-dsl.md.
export { playwrightE2E } from "@flare-dispatch/runs/playwright-e2e";
A faithful, runnable GHA workflow — what a team actually
maintains to do this job without FlareDispatch.
# Recipe: browser tests (Playwright e2e) — BASELINE (without FlareDispatch)
#
# This is what you'd maintain on plain GitHub Actions to shard a Playwright
# suite across N runners with per-shard HTML reports and a merged final
# report. Shown here ONLY as a comparison — see ./playwright-e2e.run.ts for
# the FlareDispatch version (~50 lines of typed Effect-TS, two primitives).
#
# Notable costs of the GHA-only path:
# - Every shard pays the GHA runner cold-start tax (~30–45 s) — for an N=4
# shard run, that's ~2–3 minutes of GHA minutes burned before tests start.
# - You install the browser binary (`pnpm exec playwright install --with-deps`)
# on every shard, because there is no shared image like the
# `flare-dispatch-playwright` container that already has Chromium baked in.
# This alone is ~60–90 s per shard.
# - Reports per shard are uploaded as separate `actions/upload-artifact@v4`
# entries, then a merge job downloads them all and re-glues them. The
# blob-storage round-trip is pure overhead.
# - "Done" requires a third job (`merge`) with `if: always()` because
# Playwright's official `playwright-merge-reports` only runs after all
# shards complete, even failing ones.
# - The "branch protection" check has to be the `merge` job — NOT the
# dispatch — and you must remember to make it required.
# - Total wall-clock for a 4-shard suite: ~2–3 min overhead + test time.
# FlareDispatch's `playwright-e2e` run boots in <2 s per shard (warm
# container pool) and reports a single first-class check-run.
name: e2e-baseline
on:
pull_request:
paths: ["apps/**", "packages/**"]
jobs:
# --------------------------------------------------------------------------
# 1. The shard matrix. 1-based indexing because Playwright's `--shard=i/N`
# is 1-based; GHA's matrix is just a literal list.
# --------------------------------------------------------------------------
e2e:
name: e2e (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# CONFIGURE: keep in sync with the divisor you pass to --shard.
shard: [1, 2, 3, 4]
timeout-minutes: 40
env:
BASE_URL: https://staging.example.com
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Cache the Playwright browser binaries — without this, every shard
# re-downloads ~150 MB of Chromium/Firefox/Webkit.
- name: Cache Playwright browsers
id: pw-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install Playwright browsers
if: steps.pw-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Run Playwright tests (shard ${{ matrix.shard }}/4)
run: |
pnpm exec playwright test \
--shard=${{ matrix.shard }}/4 \
--reporter=blob,html
# Per-shard HTML report — uploaded so the merge job below can re-glue
# them. Without merging, you get 4 disconnected reports that humans have
# to open one by one.
- name: Upload blob report
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 1
- name: Upload HTML report (per shard)
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report
retention-days: 14
# --------------------------------------------------------------------------
# 2. Merge the blob reports into one HTML report. This job is the
# branch-protection check — the matrix above succeeds even on partial
# pass, so you can't gate on it directly.
# --------------------------------------------------------------------------
merge-reports:
name: e2e
if: always()
needs: [e2e]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download all blob reports
uses: actions/download-artifact@v4
with:
pattern: blob-report-*
path: all-blob-reports
merge-multiple: true
- name: Merge into HTML report
run: pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
- name: Upload merged HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-report-merged
path: playwright-report
retention-days: 30
# Fail this job if any shard failed — the matrix uses `fail-fast: false`,
# so a failure in one shard does NOT propagate without this manual gate.
- name: Propagate shard failures
if: contains(needs.*.result, 'failure')
run: |
echo "::error::One or more Playwright shards failed."
exit 1