Learn k6 - k6 in CI/CD and Automation Workflows
Series/Learn k6/Episode 16
Episode 16 of 19

Learn k6 - k6 in CI/CD and Automation Workflows

Integrating k6 into automation pipelines with GitHub Actions, GitLab CI, and Jenkins, understanding k6 exit codes as the universal language of test results, leveraging report artifacts, and structuring a smoke test strategy on every merge request and a full load test on release.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 15, you learned to handle degradation measurably: distinguishing flakes from fails, writing retries with backoff, and dissecting root causes from the metric breakdown. All those skills, however sophisticated, are only valuable if they are run automatically. A test run manually on a laptop protects nothing — its results stay in the terminal instead of becoming a decision.

Episode 16 changes k6's position from a diagnostic tool into a pipeline watchdog: your scripts are run automatically by CI/CD on every merge request and every release, and performance failures stop the process before they hurt users. We'll cover GitHub Actions, GitLab CI, and Jenkins, plus the universal language that connects k6 to all of them: the exit code.

Why k6 Should Be in the Pipeline

Think of k6 as a security guard at the gate. If the guard only stands watch when someone reminds him, many uninvited guests get in. The pipeline is consistent guarding: every code change passes through the same gate, with the same standards, without relying on human memory.

Three strong reasons to put k6 in CI/CD:

  • Detect performance regressions early. A bug that makes p95 double could surface two weeks after the merge, and by then separating the cause from dozens of other changes is much harder. In the pipeline, the regression is detected on the day it's born.
  • Consistent environment. The CI runner is a clean machine — test results aren't affected by other applications that happen to run on your laptop.
  • Performance becomes a decision, not an opinion. A violated threshold means a failed build. No negotiation: the numbers speak.

Exit Code: The Universal Language of Test Results

k6 ends its process with an exit code that any CI can read. Because all pipelines determine success or failure from a step's exit code, a violated threshold fails the build without a single line of extra script:

Exit codeMeaningCI action
0Test finished, all thresholds metContinue to the next stage
1General error: file not found, execution failedFix the setup and script
99One or more thresholds violatedFail the build and upload the report
107Exception inside the scriptFix the bug in the script

Notice 107 and 99: both fail the build, but they mean different things. 107 indicates your script is broken — fix it before continuing. 99 indicates the system under test didn't meet performance targets — this is actually valuable information, because it's a decision result, not a bug.

GitHub Actions: grafana/k6-action@v0.3.1

GitHub Actions is the most common choice for repositories that already live on GitHub. Its ecosystem provides the official grafana/k6-action@v0.3.1 action that wraps k6: you just point it at a script file and the action runs k6 run script.js, then forwards the exit code (including 99) so a failed threshold automatically fails the job. An example workflow that runs a smoke test on every pull request and uploads a report:

.github/workflows/load-test.yml
name: Performance Smoke Test
 
on:
  pull_request:
    paths:
      - 'api/**'
      - 'tests/load/**'
 
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Run k6 smoke test
        uses: grafana/k6-action@v0.3.1
        with:
          filename: ./tests/load/smoke.js
          flags: --out json=reports/smoke.json
 
      - name: Upload k6 report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: k6-report
          path: reports/smoke.json

There are two important details. First, with.filename points to the script relative to the repo root; flags can contain anything you'd normally pass to k6 run — including --out to store JSON results. Second, if: always() on the upload step ensures the report is still uploaded even when the build fails — precisely when it's most needed for debugging. Without it, the report is lost on a discarded runner.

If your team doesn't want to add an action dependency, k6 can be run directly through the official image:

Running k6 via the official image in CI
docker run --rm -i grafana/k6 run - < script.js

The run - pattern reads the script from stdin so no file mounting is needed — concise for a single script, although mounting a folder is more practical when the script depends on other files.

GitLab CI: loadimpact/k6 Image

In GitLab CI, the concept is the same, only the shape is jobs inside .gitlab-ci.yml. The loadimpact/k6 image (the old name still published for compatibility; the grafana/k6 image also works) already contains the k6 binary, so the script just calls the k6 run script.js command:

.gitlab-ci.yml
stages:
  - test
 
load-test:
  stage: test
  image: loadimpact/k6
  script:
    - k6 run tests/load/smoke.js --out json=results.json
  artifacts:
    when: always
    paths:
      - results.json
    expire_in: 30 days

When a threshold is violated, k6 run exits with code 99 and GitLab marks the job as failed — no extra logic needed. The artifacts block with when: always saves the JSON report even when the job fails, and expire_in keeps pipeline storage clean.

Jenkins (Overview)

In Jenkins, the same pattern is expressed in a Jenkinsfile. The following declarative pipeline runs k6 inside a Docker container and archives the report:

JenkinsJenkinsfile
pipeline {
  agent {
    docker {
      image 'grafana/k6'
    }
  }
  stages {
    stage('Load Test') {
      steps {
        sh 'k6 run tests/load/smoke.js --out json=results.json'
      }
    }
  }
  post {
    always {
      archiveArtifacts artifacts: 'results.json'
    }
  }
}

When a threshold fails, the sh step receives exit code 99 and the pipeline stops with a red status; the post block ensures the report is archived whether the run succeeds or fails.

Strategy: Smoke Test on Merge Request, Load Test on Release

Running all types of tests at every stage is a waste of CI minutes and makes the team reluctant to touch the pipeline. Distribute the load according to frequency and cost:

  • Every merge request / pull request → a short smoke test (a few VUs, 1-2 minutes). Its purpose: validate that the script still runs and catch coarse regressions quickly.
  • Merge into main or staging → a small load test with moderate load to ensure cross-feature integration doesn't break performance.
  • Before release → a full load test with production load and duration, run as the last gate before deploy.
  • Scheduled (e.g., nightly) → a soak test to detect degradation that appears slowly.

A smoke test on a PR works like a clutch check before a race: cheap, fast, and filters out the most obvious problems. A full load test at release is the thorough inspection just before the start.

Common Mistakes in CI/CD

MistakeImpactFix
Not uploading artifacts with when: alwaysReports are lost when the build failsUpload the report in an always block
Full load test on every PRSlow, expensive CI; the team avoids testsTrim it down to a smoke test on PRs
Confusing 99 and 107Misdiagnosis: system vs scriptRead the exit code before blaming
Thresholds so tight they flakeThe team disables the test unilaterallyHandle flakes with retry (episode 15)
Runner with unstable resourcesNon-reproducible resultsPin the runner type or CI machine

Conclusion

Episode 16 closes the loop you started drawing in episode 15: test results no longer stop at the terminal, but become an automated gate in GitHub Actions, GitLab CI, and Jenkins. You understand the exit codes 0, 1, 99, and 107 as a universal language, use artifacts to store reports, and structure a tiered strategy from smoke tests on PRs to full load tests at release.

However, a single machine in the pipeline can only generate limited load. What if your target is hundreds of thousands of simultaneous users? In episode 17 we step beyond the limits of a single machine: distributed load testing and cloud execution with k6 Cloud, load zones, and the self-managed alternative using Kubernetes. See you there!

Learn k6 - k6 in CI/CD and Automation Workflows | Learn k6