Skip to content

How to add a quality gate to your CI pipeline

To add a quality gate, pick one measurable condition — pass rate or flaky-test count — compute it from your test results in a pipeline step, and exit non-zero when it's breached. Do that in GitHub Actions, GitLab CI, or Jenkins below, then flip the platform setting that turns a failing job into an actual merge block. For why quality gates matter and how SonarQube's built-in gate works, see quality gates in CI/CD — this page is the copy-paste companion.

Every example below assumes the Qualflare CLI is already authenticated in CI (qf login <project> $QF_TOKEN, stored as a CI secret — see set up flaky test detection), that results are tagged with a consistent --environment value the gate step can query back (ci below — collect defaults to staging if you omit it, which silently breaks the flaky-count query), and that jq is available on the runner — GitHub's ubuntu-latest ships it, but a plain node:20 image (as used in the GitLab example below) does not — install it explicitly rather than assuming it's there.

  1. 1

    Decide what should block a merge

    Before touching any CI config, write down the one or two conditions that should actually stop a merge — a pass-rate floor (say, at least 95% of tests passing) or a flaky-test ceiling (no more than 3 known-flaky tests on the branch). A single run only tells you pass/fail for that run; a flaky-test count needs history across builds, which is what makes it worth gating on separately. Pick a number you can defend, then wire it into whichever platform you use below.

  2. 2

    Add a required status check in GitHub Actions

    Add a workflow job that runs your tests, computes the gate condition from the results, and exits non-zero when it fails — then require that job by name in the target branch's protection settings. GitHub matches on the job name, not the workflow file, so keep the name stable once people depend on it.

    .github/workflows/ci.yml
    name: CI
    
    on:
      pull_request:
        branches: [main]
    
    jobs:
      quality-gate:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - uses: actions/setup-node@v4
            with:
              node-version: 20
    
          - run: npm ci
    
          - name: Run tests
            run: npx jest --json --outputFile=test-summary.json
            continue-on-error: true  # let the gate step below decide pass/fail
    
          - name: Enforce the pass-rate threshold
            run: |
              PASSED=$(jq '.numPassedTests' test-summary.json)
              TOTAL=$(jq '.numTotalTests' test-summary.json)
              PASS_RATE=$(( PASSED * 100 / TOTAL ))
              echo "Pass rate: ${PASS_RATE}%"
    
              if [ "$PASS_RATE" -lt 95 ]; then
                echo "::error::Quality gate failed - pass rate ${PASS_RATE}% is below the 95% threshold"
                exit 1
              fi
    
          - name: Upload results to Qualflare
            if: always()
            run: qf my-app collect test-summary.json --environment ci --branch "${{ github.head_ref }}" --commit "${{ github.sha }}"
    
          - name: Enforce the flaky-test ceiling
            if: always()
            run: |
              FLAKY=$(qf my-app launches list --environment ci | jq '.launches | max_by(.createdAt) | .flakyCount')
              echo "Flaky tests on this branch: ${FLAKY}"
    
              if [ "$FLAKY" -gt 3 ]; then
                echo "::error::Quality gate failed - ${FLAKY} flaky tests exceed the limit of 3"
                exit 1
              fi
    • In the repo, go to Settings → Branches → Add branch protection rule (or edit the rule on your default branch).
    • Check “Require status checks to pass before merging,” then search for and select the quality-gate job. GitHub only lists jobs it has already seen run once, so push the workflow before configuring this.
    • Optionally turn on a merge queue so the check re-runs against the latest target-branch state instead of a stale snapshot.
  3. 3

    Add a gate job in GitLab CI

    Add a job in .gitlab-ci.yml with the same pass-rate and flaky-count logic, then flip the project setting that actually makes a failing job block the merge — a red pipeline by itself doesn't stop anyone from clicking merge until that setting is on.

    .gitlab-ci.yml
    # Avoid duplicate pipelines (branch-push + MR) firing for the same commit
    workflow:
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    
    stages:
      - test
      - quality-gate
    
    test:
      stage: test
      image: node:20
      script:
        - npm ci
        - npx jest --json --outputFile=test-summary.json || true
        - qf my-app collect test-summary.json --environment ci --branch "$CI_COMMIT_REF_NAME" --commit "$CI_COMMIT_SHA"
      artifacts:
        when: always
        paths:
          - test-summary.json
    
    quality-gate:
      stage: quality-gate
      image: node:20
      needs: ["test"]
      before_script:
        - apt-get update -qq && apt-get install -y -qq jq  # node:20 is Debian-based and does not ship jq
      script:
        - PASSED=$(jq '.numPassedTests' test-summary.json)
        - TOTAL=$(jq '.numTotalTests' test-summary.json)
        - PASS_RATE=$(( PASSED * 100 / TOTAL ))
        - echo "Pass rate ${PASS_RATE}%"
        - |
          if [ "$PASS_RATE" -lt 95 ]; then
            echo "Quality gate failed - pass rate ${PASS_RATE}% is below the 95% threshold"
            exit 1
          fi
        - FLAKY=$(qf my-app launches list --environment ci | jq '.launches | max_by(.createdAt) | .flakyCount')
        - 'echo "Flaky tests: ${FLAKY}"'
        - |
          if [ "$FLAKY" -gt 3 ]; then
            echo "Quality gate failed - ${FLAKY} flaky tests exceed the limit of 3"
            exit 1
          fi
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    • Go to Settings → Merge requests → Merge checks, and enable “Pipelines must succeed” so a failing quality-gate job blocks the merge button, not just the pipeline view.
    • Add a merge request approval rule (Settings → Merge requests → Merge request approval rules) if you also want a required reviewer alongside the automated check.
  4. 4

    Add a quality-gate stage in Jenkins

    Jenkins doesn't have a generic, framework-agnostic quality-gate plugin the way GitHub and GitLab build required checks in natively — every plugin actually named “Quality Gates” in the Jenkins plugin index is a SonarQube companion that reads a Sonar analysis result, not a general-purpose threshold gate. For a custom condition like pass rate or flaky count, current practice is a scripted stage: parse a JSON summary with the Pipeline Utility Steps plugin's readJSON, then call the built-in error() step to fail the build when a threshold is breached.

    Jenkinsfile
    pipeline {
        agent any
    
        stages {
            stage('Test') {
                steps {
                    sh 'npm ci'
                    sh 'npx jest --json --outputFile=test-summary.json || true'
                    sh 'qf my-app collect test-summary.json --environment ci --branch "$GIT_BRANCH" --commit "$GIT_COMMIT"'
                }
            }
    
            stage('Quality Gate') {
                steps {
                    script {
                        def summary = readJSON file: 'test-summary.json'
                        def passRate = (summary.numPassedTests * 100) / summary.numTotalTests
                        echo "Pass rate: ${passRate}%"
    
                        if (passRate < 95) {
                            error("Quality gate failed: pass rate ${passRate}% is below the 95% threshold")
                        }
    
                        def launchesJson = sh(
                            script: 'qf my-app launches list --environment ci',
                            returnStdout: true
                        )
                        def launches = readJSON text: launchesJson
                        def flakyCount = launches.launches.max { it.createdAt }.flakyCount
                        echo "Flaky tests on this branch: ${flakyCount}"
    
                        if (flakyCount > 3) {
                            error("Quality gate failed: ${flakyCount} flaky tests exceed the limit of 3")
                        }
                    }
                }
            }
        }
    }
    • Install the “Pipeline Utility Steps” plugin if readJSON isn’t already available (Manage Jenkins → Plugins).
    • On a Multibranch Pipeline using the GitHub Branch Source plugin, Jenkins posts this stage’s result back to GitHub as a commit status automatically — select that check under “Require status checks to pass before merging” the same way you would a GitHub Actions job.
    • Without that integration, error() only fails the Jenkins build itself — pair it with your Git host's own branch protection to actually block a merge on it.
  5. 5

    Test the gate on a deliberately bad build before rolling out broadly

    Push a change on a throwaway branch that you know breaks the condition — drop a passing assertion to lower the pass rate, or fake a flaky-count bump — and open a PR/MR against the protected branch. Confirm the merge button is actually disabled, not just that the job shows red, before making the check required for the whole team. A gate nobody has watched fail is a gate you're guessing about.

New to the terms? See quality gate and release readiness. Not collecting results yet? Start with how to set up flaky test detection in CI.

Frequently asked questions

Do I need SonarQube or another dedicated tool to add a quality gate?

No. GitHub Actions, GitLab CI, and Jenkins can all fail a build from a plain shell or Groovy check against your own test-runner output — a JSON summary and an exit code are enough. SonarQube's built-in gate is a good option if you already run it for static analysis, but it isn't a prerequisite for gating on pass rate or flaky-test count.

Is there a Jenkins plugin for generic quality gates, the way GitHub has required status checks?

Not a generic one. Every plugin named "Quality Gates" or similar in the Jenkins plugin index is a SonarQube integration that reads a Sonar analysis result — there is no framework-agnostic equivalent. For a custom condition, teams use a scripted stage with the Pipeline Utility Steps plugin's readJSON and the built-in error() step instead.

Why gate on flaky-test count instead of just retrying failed tests until they pass?

Retries keep the build green but hide the signal a gate is supposed to catch — a test that only passes on retry is still flaky. That is also why a flaky-test ceiling needs history across builds rather than one run; see how to set up flaky test detection for the upload step that makes the count available in the first place.

Does a failing CI job automatically block the merge?

No — a red job is just a status until you turn on the platform setting that makes it required: "Require status checks to pass before merging" in GitHub, "Pipelines must succeed" in GitLab, or a reported commit status plus your Git host's own branch protection for Jenkins. Test that the block actually happens before relying on it.

Turn test results into a merge-blocking signal

Upload CI results with the Qualflare CLI and query pass rate and flaky-test count straight into your quality gate — free to start.

Start free with Qualflare