Hydration Proof

Search documentation

Find a page or section

Detect hydration errors in CI

GitHub Actions, GitLab CI, CircleCI and everything else that runs a command.

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

  1. Install your dependencies, then the browser with its system dependencies: npx hydration-proof install --with-deps.
  2. Run npx hydration-proof test with the reporters your CI system reads.
  3. Keep .hydration-proof/report as an artifact, so you can open the HTML report when a run fails.

Which reporters does each CI use?

CI systemReporters
GitHub Actionslist,html,json,github,sarif
GitLab CIlist,html,json,junit,gitlab
CircleCI, Jenkins, Azure Pipelines, Buildkite, otherslist,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 github

This 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:

.github/workflows/hydration.yml
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/report

The 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 gitlab

This writes .gitlab/hydration-proof.yml. Include it from your pipeline:

.gitlab-ci.yml
include:
  - local: .gitlab/hydration-proof.yml

The job looks like this:

.gitlab/hydration-proof.yml
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.xml

The 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

.circleci/config.yml
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:
      - hydration

store_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:

.github/workflows/hydration.yml
- 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 chromium

If 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:

.github/workflows/hydration.yml
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/report

merge-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.

.github/workflows/hydration.yml
- uses: actions/checkout@v4
  with:
    fetch-depth: 0 # the base branch must be available
# ...
- run: npx hydration-proof test --changed

Without 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 warning

An 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 (retries defaults to 1);
  • a server that is already running is never reused (server.reuseExisting defaults to false), 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 --build to 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).