GitHub Actions
The minimum
flow export github-actions > .github/workflows/flowwright.yml
name: CI
on: [push, pull_request]
jobs:
flow:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24
- run: npx @flowwright/cli run
That works, and for a pipeline with no dependencies it's all you need. The reporter is
selected automatically — GITHUB_ACTIONS is always true on a runner.
A realistic workflow
Add dependency installation, cache persistence and a test report:
name: CI
on: [push, pull_request]
jobs:
flow:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
# Persist FlowWright's own cache between runs.
- uses: actions/cache@v4
with:
path: .flowwright/cache
key: ${{ runner.os }}-flowwright-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-flowwright-
- run: pnpm exec flow run -j 4 --junit report.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: junit
path: report.xml
if-no-files-found: ignore
if: always() on the upload matters — without it you lose the report on exactly the
runs you wanted it for. Two caches are in play and they're unrelated: cache: pnpm on
setup-node handles the package store, actions/cache handles what your pipeline
stashed with cache.save.
What the log looks like
Each stage becomes a collapsible group:
FlowWright · acme-api
::group::Install
$ npm ci
[07:28:52] up to date, audited 1 package in 134ms
::endgroup::
::group::Lint
$ npm run lint
[07:28:52] lint: 0 problems
::endgroup::
success in 381ms
Under -j the stages run concurrently but each group is still emitted whole, when its
stage finishes — GitHub can't nest log groups, so buffering is the only way to keep
them readable.
Failures
A failed stage produces an annotation, which GitHub renders against the diff and in the run summary:
::group::Test
$ node -e process.exit(2)
::endgroup::
::error title=Stage test failed::node -e process.exit(2) (exit 2)
::notice title=Stage deploy skipped::upstream-failed
failed in 109ms
::error::FlowWright run failed
The annotation names the stage, the command that failed and its exit code. Skipped stages get a notice, so a stage that didn't run is still visible.
The job summary
The github reporter writes a markdown table to the run's summary page:
## FlowWright — acme-api
| Stage | Status | Time |
| ------ | ---------------------------- | ---- |
| lint | ✅ success | 86ms |
| test | ❌ failed | 22ms |
| deploy | ⏭️ skipped (upstream-failed) | 0ms |
_Total: 109ms_
It's appended to $GITHUB_STEP_SUMMARY, which the runner sets for you.
If a job sets GITHUB_STEP_SUMMARY: "" — a common trick to silence another tool's
summary — FlowWright treats that as "summary off" and writes nothing. A genuine write
failure warns on stderr rather than disappearing.
Test reports
Point any JUnit-consuming action at the --junit path. The XML carries the failing
command and exit code:
<testcase name="test" classname="acme-api" time="0.022">
<failure message="stage failed">command: node -e process.exit(2)
exit code: 2</failure>
</testcase>
<testcase name="deploy" classname="acme-api" time="0.000">
<skipped message="upstream-failed"/>
</testcase>
Cancelling a run
Cancelling the workflow sends SIGTERM. FlowWright stops, writes the job summary and
the JUnit file, and exits 130 — so a cancelled run still shows which stages had
finished.
Matrix builds
Resist the urge. A FlowWright pipeline already fans out — see Matrix stages — and doing it in the pipeline means it also works locally. Use a GitHub matrix for things that genuinely need separate runners: different operating systems, or different runner labels.