CI & Docker
render exits 0 only when the verdict is publishable, so it drops into a pipeline as an end-to-end test that attaches video evidence to every run.
GitHub Actions
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx -p playhead-cli playhead render specs/*.yaml -o out --junit out/junit.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: playhead-evidence
path: |
out/**/*.mp4
out/**/verdict.json
out/**/contact-sheet.png
out/junit.xmlA red build now comes with a video of what broke. When the app itself breaks the flow (exit 1), Playhead writes a failure clip: the run up to the break, ending on a card naming it — which is exactly when video evidence is worth the most.
Machine-readable output
--junit <path>— one test case per verification check, so your CI’s test UI shows what failed--json— the verdict on stdout (human logs stay on stderr)--workers N— render a suite concurrently; the pool exits with the worst outcome
Docker
docker run --rm -v "$PWD:/work" -w /work --shm-size=1g \
playhead render specs/examples/add-user.yaml -o out --jsonThe image builds on the Playwright base, so Chromium, its sandbox dependencies and system fonts are already there. Give it --shm-size=1g or Chromium will run out of shared memory mid-capture.
Determinism
Compose and verify never touch the network, so the same bundle and spec produce the same video. Pin that in CI:
playhead compose out/capture --expect-sha 9f2c…For app-side determinism, freeze the world in the spec: environment: { timezone: UTC, fixedTime: "2026-01-01T12:00:00Z" } stops relative timestamps drifting between runs, and network.block keeps third-party noise out of the frame.