Recording Pest browser tests in GitHub Actions
Record selected Pest browser tests on pull requests and publish videos to Diff Stage with a two-job GitHub Actions workflow and OIDC authentication.
Guide 2 of 34 min readPest 5 · Browser 5.1.2 · PHP 8.5
On this page
Before you start
This example uses Pest 5, Browser 5.1.2, PHP 8.5 and Playwright 1.63.0.
- Install the recorder and connect your repository
- At least one Pest browser test that records locally
Add a separate evidence workflow that records selected browser tests at the pull request’s head commit. Diff Stage posts a player link on the PR so reviewers can watch the change.
Choose a useful browser journey#
Use a test that reaches the changed behaviour, performs the relevant interaction and asserts the visible outcome. Keep the full regression suite in its own jobs.
For example, a checkout message change needs a checkout journey. A homepage smoke test cannot prove that change. One selected file can contain several tests, so inspect every journey it runs.
Pass paths to Pest to limit execution:
./vendor/bin/pest tests/Browser/CheckoutTest.php --record-videos--record-videos-only limits recording; it does not limit which tests execute. Do not add arbitrary waits to make footage readable: the recorder and hosted processing provide reading time.
Save the workflow#
Save the following as .github/workflows/browser-evidence.yml. Adapt Prepare Laravel and Playwright and the test environment to your application. The example assumes committed Composer and npm lockfiles, Playwright in package.json, a Vite build and SQLite-compatible migrations.
name: Browser evidence
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
concurrency:
group: browser-evidence-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
record:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
tests: ${{ steps.existing.outputs.tests }}
env:
APP_ENV: testing
APP_DEBUG: 'false'
DB_CONNECTION: sqlite
DB_DATABASE: ${{ github.workspace }}/database/database.sqlite
DB_URL: ''
FILESYSTEM_DISK: local
CACHE_STORE: array
SESSION_DRIVER: array
MAIL_MAILER: array
QUEUE_CONNECTION: sync
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- id: select
uses: diff-stage/recorder/select@10e02da13722139b2f872fc155eb264056e42e3b
- id: existing
name: Keep existing selected browser tests
env:
TESTS: ${{ steps.select.outputs.tests }}
run: |
IFS=, read -ra selected <<< "$TESTS"
tests=()
for test in "${selected[@]}"; do
if [[ "$test" =~ ^tests/Browser/[a-zA-Z0-9_/-]+Test\.php$ && "$test" != *..* && -f "$test" ]]; then
tests+=("$test")
fi
done
IFS=,
echo "tests=${tests[*]}" >> "$GITHUB_OUTPUT"
if (( ${#tests[@]} == 0 )); then
echo 'No existing selected browser tests; skipping reviewer recordings.'
fi
- uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2
if: steps.existing.outputs.tests != ''
with:
php-version: '8.5'
extensions: pdo_sqlite, sockets
coverage: none
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: steps.existing.outputs.tests != ''
with:
node-version: '24'
- name: Prepare Laravel and Playwright
if: steps.existing.outputs.tests != ''
run: |
composer install --no-interaction --prefer-dist
cp .env.example .env
php artisan key:generate --no-interaction
touch database/database.sqlite
php artisan migrate --force --no-interaction
npm ci
npx --no-install playwright install --with-deps chromium
npm run build
- name: Record selected flows
if: steps.existing.outputs.tests != ''
env:
TESTS: ${{ steps.existing.outputs.tests }}
run: |
rm -rf tests/Browser/Videos
mkdir -p tests/Browser/Videos
git rev-parse HEAD > tests/Browser/Videos/sha.txt
IFS=, read -ra tests <<< "$TESTS"
./vendor/bin/pest "${tests[@]}" --record-videos --record-videos-fast
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: steps.existing.outputs.tests != ''
with:
name: browser-videos-${{ github.event.pull_request.head.sha }}
path: tests/Browser/Videos
if-no-files-found: error
retention-days: 7
publish:
needs: record
if: needs.record.outputs.tests != '' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: browser-videos-${{ github.event.pull_request.head.sha }}
path: tests/Browser/Videos
- name: Check recording provenance
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
[[ "$(cat tests/Browser/Videos/sha.txt)" == "$HEAD_SHA" ]]
compgen -G 'tests/Browser/Videos/*.webm' >/dev/null
- uses: diff-stage/recorder/publish@dcbfd210e743beec326e16d8ba2bc1b74b4306c8
with:
url: https://diffstage.comThe recording job checks out the PR’s head SHA and runs without publishing rights. The publishing job downloads that job’s artifact and checks its recorded SHA before upload. It never checks out or runs application code.
Only publishing receives id-token: write and permission to comment on the PR. GitHub Actions identity authenticates the upload to the connected repository; no upload secret is required. GitHub explains OIDC authentication here.
Select recordings in the PR description#
Add a line with exact browser test paths:
Browser videos: tests/Browser/CheckoutTest.php, tests/Browser/BookingTest.phpThe selector uses this explicit list. Changed test files are not selected automatically. Paths must match tests/Browser/...Test.php; globs and individual test names are not supported by this selection.
If no browser journey demonstrates the change, leave the line empty or omit it and explain your verification in the PR. That skips evidence recording and publication, while your separate regression jobs still run.
Tell reviewers what the videos prove#
After a local recording, take each flow key from the actual .webm filename without its extension:
Browser review:
1. `actual-recording-filename-without-extension`: Check the new validation message after submitting an empty form.Replace the example key with yours. The publisher orders matching recordings by this list and includes the notes in its PR comment. Unmatched keys produce a warning.
Check the published result#
Open the workflow run. Both jobs must succeed. Then open the Diff Stage comment, confirm its commit matches the PR head and play every selected recording. Fast recordings may still be processing after CI finishes.
Fork and Dependabot PRs retain recordings as GitHub Actions artifacts without hosted publication in this workflow.
To compare a PR against approved behaviour, record the same flows on your default branch and publish with mode: baseline. Keep that recording job separate from the publisher too. Use a push trigger for your actual default branch, record its checked-out SHA in sha.txt, and publish only after recording succeeds. Keep baseline recordings in one consistent workflow so github.run_number gives them a consistent order.
The baseline recording step can use:
rm -rf tests/Browser/Videos
mkdir -p tests/Browser/Videos
git rev-parse HEAD > tests/Browser/Videos/sha.txt
./vendor/bin/pest tests/Browser --record-videos --record-videos-fastAfter the baseline publisher downloads and verifies that artifact, its final step is:
- uses: diff-stage/recorder/publish@dcbfd210e743beec326e16d8ba2bc1b74b4306c8
with:
mode: baseline
url: https://diffstage.comNext, compare the recordings before merging.
Troubleshooting#
- Recording is skipped: check the
Browser videos:line and that each selected file exists. An empty selection is expected to skip publication. - Upload authentication fails: check the repository’s GitHub App connection, team plan and
id-token: writeon the publisher. Upload tokens are not supported. - SHA mismatch: rerun recording for the current PR head. Do not edit
sha.txtto make older footage pass the check. - CI passes but no player is ready: inspect the publisher result and Diff Stage’s processing status. An artifact alone is not a hosted recording.
- A previous comment remains: check its SHA. Removing the evidence selection does not make an older recording evidence for the new commit.
Related guides
Installing Diff Stage in a Laravel project
Connect your repository, install the Pest browser recorder and check your first local recording.
Comparing browser test videos before merging a PR
Watch a PR’s videos beside the approved default-branch recording, then check actions and browser problems.
Show your reviewers what changed
Add the recorder to your test suite, and your next pull request gets a video.