# Detect hydration errors in CI

> Detect hydration errors in CI with GitHub Actions, GitLab CI or CircleCI: complete workflows, SARIF upload, browser caching, sharding and --changed.

Source: https://hydration.jscrate.dev/docs/ci
Last updated: 2026-09-18

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

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 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](https://hydration.jscrate.dev/docs/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:

```bash
npx hydration-proof init --ci github
```

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

```yaml title=".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
```

Problems 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

```bash
npx hydration-proof init --ci gitlab
```

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

```yaml title=".gitlab-ci.yml"
include:
  - local: .gitlab/hydration-proof.yml
```

The job looks like this:

```yaml title=".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

```yaml title=".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` 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:

```yaml title=".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](https://hydration.jscrate.dev/docs/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:

```yaml title=".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.

```yaml title=".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.

```bash
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](https://hydration.jscrate.dev/docs/baselines).

## 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](https://hydration.jscrate.dev/docs/security)).
- Keep sign-in credentials in CI secrets and read them in a scenario's `login`
  (see [scenarios and sign-in](https://hydration.jscrate.dev/docs/scenarios)).

## Related

- [Baselines, budgets and owners](https://hydration.jscrate.dev/docs/baselines) for apps with existing problems
- [Every report format](https://hydration.jscrate.dev/docs/reports): JUnit, SARIF, GitHub and GitLab
- [Test several apps of a monorepo](https://hydration.jscrate.dev/docs/monorepos)
- [CLI flags and exit codes](https://hydration.jscrate.dev/docs/cli)
- [Security and redaction](https://hydration.jscrate.dev/docs/security)
