View as Markdown

Jest Integration with Mergify

Report your test results from Jest to Mergify


This guide explains how to integrate Jest with Test Engine using the @mergifyio/jest reporter. The reporter uploads your test results to Test Engine when the run ends, with no JUnit file to write or upload, and applies quarantine and flaky detection inside the Jest run.

If your CI uploads a JUnit report from jest-junit today, see Moving from a JUnit upload.

  • Node.js 22 or later in the job that runs the tests. Older versions are not supported: npm and pnpm only warn at install, Yarn 1 refuses.

  • Jest 29 or later.

  • A Jest configuration file for withMergify to wrap, such as jest.config.js or jest.config.ts. See Configuration.

Install the @mergifyio/jest package alongside jest to automatically upload your test results to Test Engine.

Use the package manager version your project pins (packageManager in package.json, mise.toml, .tool-versions), and check that the lockfile diff only adds the new packages: another version rewrites unrelated entries (older npm releases, for example, drop the libc fields of other packages; npx npm@latest install … avoids it).

Terminal window
npm install --save-dev @mergifyio/jest
Terminal window
yarn add --dev @mergifyio/jest
Terminal window
pnpm add --save-dev @mergifyio/jest

Wrap your Jest configuration with withMergify:

jest.config.js
const { withMergify } = require('@mergifyio/jest');
module.exports = withMergify({
// ... your existing configuration
});

With a TypeScript configuration:

jest.config.ts
import type { Config } from 'jest';
import { withMergify } from '@mergifyio/jest';
const config: Config = {
// ... your existing configuration
};
export default withMergify(config);

withMergify adds the Mergify reporter after the reporters your configuration lists, or after Jest’s default reporter when it lists none, so your console output stays the same. It also gives your configuration Mergify’s test runner, built on jest-circus, which applies quarantine to each test and runs flaky detection’s reruns. It turns on testLocationInResults too, so the results it uploads carry the line each test is declared on.

withMergify is the only supported setup. Some configurations need a change, or limit what it covers:

  • Projects listed inline under projects are covered by the root configuration’s withMergify. A project listed by path is loaded from its own file: wrap that file too.

  • --reporters on the command line replaces the reporters of your configuration, the Mergify one included. Remove those flags from your test command and list the reporters you need in the configuration instead.

  • A testRunner other than jest-circus, such as jest-jasmine2, is kept, and neither quarantine nor flaky detection applies to its tests.

  • A configuration in package.json or jest.config.json has no function to call. Move it to a jest.config.js (or .ts, .mjs, .cjs) and wrap it there.

At the end of the run, the reporter names each project that quarantine and flaky detection could not reach.

The plugin uploads results only when the CI environment variable is set, or when MERGIFY_CI_ENABLE is on. GitHub Actions, CircleCI and Buildkite set CI for you. Jenkins does not: set CI=true, or MERGIFY_CI_ENABLE=true, in the stage that runs your tests. If your tests behave differently when CI is set (some test suites skip or add tests on CI), set MERGIFY_CI_ENABLE=true instead of CI, so the tests that run stay the same.

CI counts as set unless it is empty or a false value (false, no, off, 0, f or n), so true, 1 and a CI system’s name (Woodpecker sets CI to woodpecker) all count. MERGIFY_CI_ENABLE is stricter: only true, yes, on, 1, t or y turn it on. Case does not matter for either.

Without MERGIFY_TOKEN the plugin uploads nothing and your tests run as usual. Pull requests from forks and Dependabot pull requests on GitHub Actions get no repository secrets, so they report nothing: no guard is needed in the workflow. (The RSpec gem logs an authentication error when the variable is set but empty, as on those pull requests; the tests are not affected.) To have Dependabot pull requests report too, store the key as a Dependabot secret as well (gh secret set MERGIFY_TOKEN --app dependabot --repo <owner>/<repository>).

Every test is identified by its name and the name of the CI job that ran it. By default the job name is the job’s identifier in your CI:

CIDefault job name
GitHub Actionsthe job id (GITHUB_JOB), recorded with the workflow name (GITHUB_WORKFLOW)
CircleCICIRCLE_JOB, the expanded job name of each matrix leg
Buildkitethe step label (BUILDKITE_LABEL)
JenkinsJOB_NAME, also used as the pipeline name

Set MERGIFY_TEST_JOB_NAME when that default is not one stable name per set of tests:

  • GitHub Actions matrix: every leg shares one job id. Set it per leg from the matrix values, for example MERGIFY_TEST_JOB_NAME: test (${{ matrix.python-version }}, ${{ matrix.os }}).

  • Two test runs in one job: set a distinct name on each step.

  • Reusable workflows: a called workflow reports under the caller’s workflow name, so two called workflows whose jobs share an id (often test) collide. Set a distinct name in each.

  • Jenkins multibranch pipelines: each branch and each pull request keeps its own test history, and no setting changes that. JOB_NAME includes the branch or pull request (my-repo/PR-123) and is also the pipeline name tests are filed under; MERGIFY_TEST_JOB_NAME replaces only the job name. Do not set it for this purpose.

  • Shards of one test suite are the same tests split across machines: give all shards the same name. Never put a shard index in it.

Tests inside tox, Docker or Docker Compose

Section titled Tests inside tox, Docker or Docker Compose

tox, docker run and Docker Compose start the tests in an environment that does not inherit the CI job’s variables. Pass through MERGIFY_TOKEN, CI, MERGIFY_TEST_JOB_NAME if you set it, and every variable the plugin reads to identify the run. tox’s pass_env accepts globs (GITHUB_*); Docker Compose, docker run -e and the Buildkite Docker Compose plugin need each name:

GitHub Actions: GITHUB_ACTIONS GITHUB_BASE_REF GITHUB_EVENT_NAME GITHUB_EVENT_PATH
GITHUB_HEAD_REF GITHUB_JOB GITHUB_REF_NAME GITHUB_REF_TYPE GITHUB_REPOSITORY
GITHUB_REPOSITORY_ID GITHUB_RUN_ATTEMPT GITHUB_RUN_ID GITHUB_SERVER_URL
GITHUB_SHA GITHUB_WORKFLOW RUNNER_NAME
CircleCI: CIRCLECI CIRCLE_BRANCH CIRCLE_BUILD_URL CIRCLE_JOB
CIRCLE_REPOSITORY_URL CIRCLE_SHA1 CIRCLE_WORKFLOW_ID
Buildkite: BUILDKITE BUILDKITE_AGENT_NAME BUILDKITE_BRANCH BUILDKITE_BUILD_ID
BUILDKITE_BUILD_URL BUILDKITE_COMMIT BUILDKITE_LABEL BUILDKITE_PIPELINE_SLUG
BUILDKITE_PULL_REQUEST BUILDKITE_PULL_REQUEST_BASE_BRANCH BUILDKITE_REPO
BUILDKITE_RETRY_COUNT BUILDKITE_STEP_KEY
Jenkins: JENKINS_URL BUILD_ID BUILD_URL JOB_NAME NODE_NAME GIT_URL GIT_BRANCH
GIT_COMMIT CHANGE_TARGET

Pass them by name, never by writing a value into a file. In a container on GitHub Actions, also mount the file GITHUB_EVENT_PATH points to, at the same path: without it the run is recorded against the merge commit instead of the pull request’s head commit.

Depending on its version and persona, zizmor can report secrets-outside-env on the MERGIFY_TOKEN: ${{ secrets.MERGIFY_TOKEN }} line. Run the repository’s own zizmor configuration on the changed workflow, and if it reports the finding, follow the convention the repository already uses for it: a deployment environment on the job, or an inline # zizmor: ignore[secrets-outside-env] comment.

Your workflow should run your tests as usual while exporting the secret MERGIFY_TOKEN as an environment variable.

Add the following to the GitHub Actions step running your tests:

env:
MERGIFY_TOKEN: ${{ secrets.MERGIFY_TOKEN }}

For example:

- name: Run Tests 🧪
env:
MERGIFY_TOKEN: ${{ secrets.MERGIFY_TOKEN }}
run: npx jest

Set MERGIFY_TOKEN in the environment of the agents running your tests. The step itself then needs no Mergify-specific configuration:

steps:
- label: "Run Tests 🧪"
command: npx jest

The reporter automatically collects your test results and sends them to Test Engine.

Before any test runs, the reporter fetches the list of tests quarantined in Test Engine. When one of them fails, Jest counts it as passed, so it does not change Jest’s exit code, and the test reaches Test Engine as failed and quarantined. This works the same whether Jest runs your files in its workers or in its own process (--runInBand). The exit code of your test step already accounts for quarantine, so the step needs nothing more:

  • Do not add continue-on-error: true. The recipes that upload a JUnit report with the mergifyio/gha-mergify-ci action need it because the action decides the job’s result after the tests. Here nothing does, so it would let every real failure through.

  • There is no step id to set and no test_step_outcome to pass: both belong to that action, which this setup does not use.

A crash cannot pass for a green run either. Quarantine only covers failing tests: a test file that fails to run still makes Jest exit non-zero and fail the step, and so does Jest dying mid-run. The reporter uploads results when the run ends, so a process killed outright before then, such as by the out-of-memory killer, sends nothing to Test Engine.

If the reporter cannot fetch the quarantine list, it quarantines nothing for that run, and a quarantined test that fails makes the step fail as usual.

The reporter reruns tests within the budgets set under Native Flaky Detection, and fetches those budgets before any test runs:

  • On a pull request, a test the target branch does not have yet is new. Once your run is over, the reporter reruns the new tests within the Prevention Budget. A test that both passed and failed is flaky, and the run fails so the pull request does not merge. See Prevention.

  • On a push, the reporter reruns the tests Test Engine knows as unhealthy, within the Detection Budget, to report whether they still are. The run’s result does not change. See Rerunning unhealthy tests.

The reruns happen in Jest’s workers and run each file the way your run did: its hooks run around each attempt, its snapshots are checked and never written, and Jest’s own retries are off. Jest’s summary and your other reporters show only your run, so a new test the reruns found flaky still shows as passed there. The reporter’s Mergify CI section names it, and the run exits with a failure.

The reporter reruns nothing in watch mode, after you interrupt the run, or once --bail has stopped it. A test.concurrent test is never rerun: jest-circus starts it with the other tests of its file and cannot run it again on its own.

Each test reaches Test Engine named by its file, relative to Jest’s rootDir, the describe blocks around it, and its own title:

src/app.test.js > App > header > renders

The file is part of the name because tests declared outside any describe often share a title across files (renders without crashing), and would otherwise be one test in Test Engine. Test Engine tracks a test’s history under that name, so renaming the file, a describe block or the test starts a new test.

The reporter uploads each run’s results under your CI job’s name. On GitHub Actions, that name is the job’s identifier (GITHUB_JOB, recorded with the workflow name), which every leg of a matrix shares. The job name is part of a test’s identity in Test Engine, so when the legs run the same suite on several Node.js versions, their results merge into one test.

Set MERGIFY_TEST_JOB_NAME on each leg and include the matrix value, so each leg reports under its own name:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24]
steps:
# ...
- name: Run Tests 🧪
env:
MERGIFY_TOKEN: ${{ secrets.MERGIFY_TOKEN }}
MERGIFY_TEST_JOB_NAME: test (node ${{ matrix.node-version }})
run: npx jest

If you split the suite with Jest’s --shard, leave the shard index out of the name: the shards run different tests, and a test can move to another shard when tests are added or removed, so a name per shard would split its history.

Setting MERGIFY_TEST_JOB_NAME on a job that already reports changes its name, so its tests start a new history in Test Engine.

If your CI writes a JUnit report with jest-junit and uploads it with the mergifyio/gha-mergify-ci action, the mergifyio/mergify-ci Buildkite plugin or mergify ci junit-process, switch to @mergifyio/jest:

  1. Install @mergifyio/jest and wrap your configuration with withMergify, as shown in Configuration.

  2. Remove --reporters=default --reporters=jest-junit from your test command: reporters given on the command line replace your configuration’s, and Jest would drop the Mergify one. If another tool still reads the JUnit file, list jest-junit in your configuration’s reporters instead: withMergify adds its reporter after the ones listed.

  3. Remove the upload: the mergifyio/gha-mergify-ci step, the mergifyio/mergify-ci plugin on the Buildkite step, or the mergify ci junit-process command. The reporter sends the results itself. If you ran mergify ci junit-process, also drop || exit_code=$? from your test command, so its exit code is the step’s again.

  4. On GitHub Actions, remove continue-on-error: true from the test step. If you added a step for pull requests without the secret that fails the job when no report was uploaded, remove it too.

  5. If your upload set a job_name, as a matrix setup does, move its value to MERGIFY_TEST_JOB_NAME on the step that runs the tests, as shown in Matrix and sharded runs, so your tests keep the job name they report under.

  6. Set MERGIFY_TOKEN on the step that runs the tests, as shown in Update your CI workflow.

What changes in Test Engine once the reporter uploads:

  • Your tests get new names, so their history starts again under those names.

  • Quarantine entries made from your JUnit uploads still apply: the reporter also matches each test under the name jest-junit gives it by default.

  • Flaky detection judges whether a test is new by its name. Until your target branch has run with the reporter, it has none of the new names, so a pull request reruns its tests as new ones within the Prevention Budget, and a test that was already flaky can fail the job as a new flaky test.

Verify and review in Test Engine

Section titled Verify and review in Test Engine

After pushing these changes, your next CI run reports its Jest results automatically.

You can then review your test results, including any failures or flaky tests, directly in the Test Engine dashboard.

VariablePurposeDefault
MERGIFY_API_URL

Base URL of the Mergify API. An API URL given on the command line or in a reporter's configuration takes precedence.

https://api.mergify.com
MERGIFY_CI_DEBUG

Set to true to upload nothing and print the session verdict and the spans instead of sending them.

false
MERGIFY_CI_ENABLE

Set to true to turn the reporter on outside CI. In CI, which the CI variable signals, it is on anyway.

false
MERGIFY_TEST_JOB_NAME

Name the job's test results are reported under, instead of the CI job's own name. Set one per environment, never per shard: a name per shard splits a test's history.

MERGIFY_TEST_RETRY_ENABLE

Set to false to have this job retry no failing test, whatever the repository's test retry configuration. On by default, and test retry only retries on a push to the default branch of a repository that configured it: only a recognised no (false, no, off, 0) turns it off.

true
MERGIFY_TOKEN

The Mergify credential: an application key in CI. Without it the test reporters upload nothing. A mergify command takes it when --token is not given, over the credential mergify auth login stored, and mergify auth tells when it does. The mergify stack commands send it to GitHub as their GitHub token, ahead of GITHUB_TOKEN, unless it holds a Mergify user token (mut_…).

MERGIFY_TRACEPARENT

W3C traceparent of a trace the job already started: the test session's spans join that trace instead of starting their own. A malformed value is ignored.

To check the setup without uploading anything, run your tests with MERGIFY_CI_ENABLE=true MERGIFY_CI_DEBUG=true: the reporter prints the spans it would send.

Was this page helpful?