Recipes / Action mode

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.

Use case
Playwright e2e suite, sharded across the browser pool
Recommended mode
Action
Files
ci.yml, playwright-e2e.run.ts

Source

This recipe ships both triggers — a typed Effect-TS run (*.run.ts) and a GitHub Actions workflow (ci.yml). Use whichever fits; Action mode is the recommended default.

recipes/browser-tests/ci.yml
# 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.
recipes/browser-tests/playwright-e2e.run.ts
// Recipe: browser tests — the `playwright-e2e` Run
//
// The typed Run that ./ci.yml dispatches. Shards a Playwright suite with
// Playwright's native --shard flag — one container per shard, all in
// parallel — and uploads a report per shard.
//
// This recipe rides on two primitives — `sharded` (count-and-index fan-out)
// and `workspace` (acquire + clone + cached install) — so the body is just
// "run Playwright on this shard". See specs/03-dsl.md § Primitives.
//
// This is the shipped `playwright-e2e` run, reproduced here so the recipe is
// self-contained. Spec: specs/02-runs.md § 3. DSL: specs/03-dsl.md.

import { Effect, Schema } from "effect";
import { defineRun, step, sandbox, artifact } from "@flare-dispatch/core";
import { sharded, workspace } from "@flare-dispatch/core/primitives";

const Input = Schema.Struct({
  repo: Schema.String,
  sha: Schema.String,
  baseURL: Schema.String,
  shards: Schema.optional(Schema.Number), // default 4
  project: Schema.optional(Schema.String), // Playwright project name
});

const Output = Schema.Struct({
  shards: Schema.Number,
  passed: Schema.Number,
  failed: Schema.Number,
  shardResults: Schema.Array(
    Schema.Struct({
      index: Schema.Number,
      exitCode: Schema.Number,
      reportUri: Schema.String,
    }),
  ),
});

export const playwrightE2E = defineRun({
  name: "playwright-e2e",
  version: "1.0.0",
  image: "registry.cloudflare.com/openhackersclub/flare-dispatch-playwright:latest",
  inputs: Input,
  outputs: Output,
  limits: { maxDurationSec: 2400, maxConcurrency: 8, requiresBrowser: true },

  run: (input) =>
    Effect.gen(function* () {
      const shards = input.shards ?? 4;
      const projectArg = input.project ? ["--project", input.project] : [];

      // One container per shard, all in parallel. `sharded` hands each shard
      // its { index, total }; `workspace` does the per-shard checkout +
      // cached install. The suite's Playwright config points at CF Browser
      // Rendering — `requiresBrowser` declares the binding.
      const shardResults = yield* step("run-shards", () =>
        sharded({
          count: shards,
          body: ({ index, total }) =>
            Effect.gen(function* () {
              const { container, dir } = yield* workspace({
                repo: input.repo,
                sha: input.sha,
                install: true,
              });
              const exec = yield* sandbox.exec({
                cwd: dir,
                container,
                env: { BASE_URL: input.baseURL },
                command: [
                  "pnpm", "exec", "playwright", "test",
                  "--shard", `${index}/${total}`,
                  ...projectArg,
                ],
              });
              const reportUri = yield* artifact.upload({
                name: `playwright-report-${index}`,
                path: `${dir}/playwright-report/`,
                container,
                signedUrlTTL: "30 days",
              });
              return { index, exitCode: exec.exitCode, reportUri };
            }),
        }),
      );

      const failed = shardResults.filter((r) => r.exitCode !== 0).length;
      return { shards, passed: shards - failed, failed, shardResults };
    }),
});