To detect hydration errors in CI, install a browser and run
npx hydration-proof test. The command exits non-zero when the configured
failure policy is met. Use the reporters your CI system understands, or
generate a workflow with npx hydration-proof init --ci github (or gitlab).
How to detect hydration errors in CI
- Install your dependencies, then the browser with its system dependencies:
npx hydration-proof install --with-deps. - Run
npx hydration-proof testwith the reporters your CI system reads. - Keep
.hydration-proof/reportas an artifact, so you can open the HTML report when a run fails.
Which reporters does each CI use?
| CI system | Reporters |
|---|---|
| GitHub Actions | list,html,json,github,sarif |
| GitLab CI | list,html,json,junit,gitlab |
| CircleCI, Jenkins, Azure Pipelines, Buildkite, others | list,html,json,junit |
On GitHub Actions the github reporter is added automatically unless you pass
--reporter. Reports explains what each format contains.
GitHub Actions
You do not need a separate hydration-proof GitHub Action. The CLI runs as a normal step. To create a workflow, run:
npx hydration-proof init --ci githubThis writes .github/workflows/hydration.yml for your package manager. It
runs on pull requests and pushes to main. If you prefer to write your own,
start with this npm example:
name: Hydration
on: [pull_request]
permissions:
contents: read
security-events: write # upload-sarif
jobs:
hydration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx hydration-proof install --with-deps
- run: npx hydration-proof test --reporter list,html,json,github,sarif
- uses: github/codeql-action/upload-sarif@v3
if: ${{ !cancelled() && hashFiles('.hydration-proof/report/report.sarif') != '' }}
with:
sarif_file: .hydration-proof/report/report.sarif
category: hydration-proof
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: hydration-report
path: .hydration-proof/reportThe github reporter adds annotations and a job summary. Uploading SARIF
also adds findings to code scanning. That requires code scanning on the
repository; forked pull requests cannot upload with their read-only token.
If you do not use code scanning, remove sarif, security-events: write and
the upload step.
GitLab CI
npx hydration-proof init --ci gitlabThis writes .gitlab/hydration-proof.yml. Include it from your pipeline:
include:
- local: .gitlab/hydration-proof.ymlThe job looks like this:
hydration:
image: node:24
script:
- npm ci
- npx hydration-proof install --with-deps
- npx hydration-proof test --reporter list,html,json,junit,gitlab
artifacts:
when: always
paths:
- .hydration-proof/report
reports:
codequality: .hydration-proof/report/gl-code-quality.json
junit: .hydration-proof/report/junit.xmlThe merge request shows failing pages in the test summary and the problems in
the Code Quality widget. The generated file also sets GIT_DEPTH: 0, so
--changed can compare with the target branch.
CircleCI
version: 2.1
jobs:
hydration:
docker:
- image: cimg/node:lts
steps:
- checkout
- run: npm ci
- run: npx hydration-proof install --with-deps
- run: npx hydration-proof test --reporter list,html,json,junit
- store_test_results:
path: .hydration-proof/report/junit.xml
- store_artifacts:
path: .hydration-proof/report
destination: hydration-report
workflows:
hydration:
jobs:
- hydrationstore_test_results puts tested pages in CircleCI's Tests tab.
store_artifacts keeps the HTML report in the Artifacts tab.
Other CI systems
Run with --reporter list,html,json,junit, publish
.hydration-proof/report/junit.xml as test results, and keep
.hydration-proof/report as an artifact. JUnit test cases follow the page
status, and the exit code applies --fail-on.
Cache the browsers
Playwright stores downloaded browsers in ~/.cache/ms-playwright on Linux.
Cache this folder to skip the download on later runs:
- name: Browser cache
id: browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- if: steps.browsers.outputs.cache-hit != 'true'
run: npx hydration-proof install --with-deps chromiumIf your project already uses Playwright 1.63 or newer, hydration-proof uses the same browsers, so an existing cache step covers both (see Playwright).
Split the run across jobs
Use --shard i/n to split pages across jobs. Each route and scenario goes to
exactly one shard. Merge the results in a final job so the CI policy is
applied to the complete run:
name: Hydration (sharded)
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx hydration-proof install --with-deps chromium
# The merge job decides the outcome, not each shard.
- run: npx hydration-proof test --shard ${{ matrix.shard }}/4 --fail-on never --reporter json
- uses: actions/upload-artifact@v4
with:
name: shard-${{ matrix.shard }}
path: .hydration-proof/report
report:
needs: test
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
pattern: shard-*
path: shards
- run: npx hydration-proof merge-reports shards/shard-* --output .hydration-proof/report --reporter list,html,json,github,sarif
- uses: github/codeql-action/upload-sarif@v3
if: ${{ !cancelled() && hashFiles('.hydration-proof/report/report.sarif') != '' }}
with:
sarif_file: .hydration-proof/report/report.sarif
category: hydration-proof
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: hydration-report
path: .hydration-proof/reportmerge-reports applies the CI policy from the config (failOn, budgets and
ignore rules) to the merged findings, so a budget covers the whole run rather
than each shard. Each job builds and starts the app itself. To build once, build in
an earlier job, pass the build output as an artifact, and keep
buildWhen: 'if-missing'.
If you upload SARIF from each shard instead, give each upload its own
category, or each one replaces the previous shard's results.
Test only what changed
On pull requests, --changed tests only the routes the changed files can
affect. hydration-proof follows the imports of your source files (including
@/ path aliases and workspace packages) from the changed files to the route
files, their layouts and _app. Changes to package.json, lockfiles,
next.config, tsconfig, middleware or environment files test every route.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the base branch must be available
# ...
- run: npx hydration-proof test --changedWithout a value, --changed compares with the pull request's base branch
(from GITHUB_BASE_REF or CI_MERGE_REQUEST_TARGET_BRANCH_NAME), or with the
repository's default branch, usually main. Give a ref to choose another:
--changed origin/develop. Run the full suite on the main branch as well:
--changed cannot see changes in data or on the server.
Decide what fails the build
--fail-on sets the lowest severity that fails the run: error (the default),
warning, info or never. The same setting is ci.failOn in the config.
npx hydration-proof test --fail-on warningAn app that already has findings should not start with a red pipeline. Record a baseline and fail only on new findings, or allow a number of findings with a budget: see baselines and budgets.
What changes when CI=true
When the CI environment variable is set (to anything but an empty string,
0 or false):
- every page that fails to load is retried once (
retriesdefaults to1); - a server that is already running is never reused (
server.reuseExistingdefaults tofalse), so the run always tests the app it started.
More tips
- Test the production build: build in an earlier step and keep the default
buildWhen: 'if-missing', or pass--buildto force a fresh build. - Reports and screenshots show what the tested pages show. Treat the report artifact like other test output with user data; secrets and personal data are redacted by default (see security).
- Keep sign-in credentials in CI secrets and read them in a scenario's
login(see scenarios and sign-in).
Related
- Baselines, budgets and owners for apps with existing problems
- Every report format: JUnit, SARIF, GitHub and GitLab
- Test several apps of a monorepo
- CLI flags and exit codes
- Security and redaction