To detect hydration errors in CI, install the browser and run
npx hydration-proof test. It exits with a non-zero code when it finds a
problem, so it works in any CI system. Add the reporters your CI understands,
or let npx hydration-proof init --ci github (or --ci gitlab) write the
workflow for you.
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 an ordinary step. Generate the workflow:
npx hydration-proof init --ci githubIt writes .github/workflows/hydration.yml for your package manager (npm,
pnpm, Yarn or Bun), and runs on pull requests and on pushes to main. Or write
it yourself:
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/reportProblems appear as annotations on the changed files and in the job summary.
The SARIF upload also adds them to the repository's code scanning alerts. Code
scanning must be available (public repositories, or GitHub Advanced Security),
and pull requests from forks cannot upload. Without it, leave out sarif, the
security-events permission 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 shows each tested page as a test in the Tests tab.
store_artifacts keeps the HTML report, which you open from 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
Downloading the browser is the slowest step. Playwright keeps browsers in
~/.cache/ms-playwright on Linux; cache that folder and skip the install on a
hit:
- 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
--shard i/n tests one part of the pages. The split depends only on the route
and the scenario, so the parts never overlap and together cover every page. A
final job merges the shard reports and applies the CI policy once, over every
finding:
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