Skip to content

CI/CD Integration

This guide covers the complete CI/CD pipeline: lint → execute → upload → regression check. Templates are provided for all four supported CI platforms.

A complete performance pipeline has four stages:

  1. Lint (perf-lint) — catch script anti-patterns before they reach production
  2. Execute (perf-containers) — run the test with the correct Docker image
  3. Report (perf-report) — Apdex and SLA checks, with an exit code the build can gate on
  4. Regression check (perf-report compare) — fail the build if this run is worse than recent ones

Set these in your CI/CD secrets store before configuring pipelines:

Secret/VariableDescription
TARGET_HOSTURL of the system under test (staging)
PERF_REPORTING_LICENSEperf-reporting Pro or Enterprise key — the CLI, SLA gating and compare are the paid capability

.github/workflows/performance.yml
name: Performance Tests
on:
push:
branches: [main, develop]
paths:
- 'tests/performance/**'
schedule:
- cron: '0 2 * * *' # nightly at 02:00 UTC
workflow_dispatch:
inputs:
vus:
description: 'Virtual users'
default: '100'
duration:
description: 'Test duration (e.g. 5m)'
default: '5m'
jobs:
performance:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
# Stage 1: Lint
- name: Lint performance scripts
uses: markslilley/perf-lint-action@v1
with:
path: tests/performance/
tool: k6
fail-on: error
# Stage 2: Execute
- name: Run load test
run: |
docker run --rm \
-v ${{ github.workspace }}/tests/performance:/tests \
-e TARGET_HOST=${{ vars.TARGET_HOST }} \
ghcr.io/markslilley/perf-k6:latest \
k6 run /tests/load-test.js \
-e VUS=${{ github.event.inputs.vus || '100' }} \
-e DURATION=${{ github.event.inputs.duration || '5m' }} \
--out json=/tests/results.json
# Stage 3: Report — fails the build on SLA breach or errors in the run
- name: Generate report
if: always()
run: |
perf-report \
-i tests/performance/results.json \
--tool k6 \
-o report.json \
--summary \
--fail-on-error
env:
PERF_REPORTING_LICENSE: ${{ secrets.PERF_REPORTING_LICENSE }}
# Stage 4: Regression check against recent runs
#
# Compares runs, not samples: each report contributes one value per metric, so this
# asks whether today's p95 is worse than recent builds' p95. Keep the baselines as
# a build artefact or in the repo — around eight are needed before the verdict is
# worth gating on, and perf-report says so rather than pretending otherwise.
- name: Check for regressions
run: |
perf-report compare \
--baseline 'baselines/*.json' \
--current report.json \
--alpha 0.05
env:
PERF_REPORTING_LICENSE: ${{ secrets.PERF_REPORTING_LICENSE }}
- name: Render an HTML report for humans
if: always()
run: perf-report -i tests/performance/results.json --tool k6 -f html -o report.html
env:
PERF_REPORTING_LICENSE: ${{ secrets.PERF_REPORTING_LICENSE }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: perf-report-${{ github.run_number }}
path: report.html
retention-days: 30

.gitlab-ci.yml
stages:
- lint
- perf-test
- analyse
variables:
VUS: "100"
DURATION: "5m"
perf-lint:
stage: lint
image: python:3.12-slim
script:
- pip install --quiet perf-lint-tool
- perf-lint check tests/performance/ --format json > lint-results.json
- perf-lint check tests/performance/ # human output for CI log
artifacts:
paths:
- lint-results.json
expire_in: 7 days
rules:
- changes:
- tests/performance/**
load-test:
stage: perf-test
image: ghcr.io/markslilley/perf-k6:latest
variables:
GIT_STRATEGY: fetch
script:
- k6 run tests/performance/load-test.js
-e TARGET_HOST=${TARGET_HOST}
-e VUS=${VUS}
-e DURATION=${DURATION}
--out json=results.json
artifacts:
paths:
- results.json
expire_in: 14 days
report-and-check:
stage: analyse
dependencies:
- load-test
script:
# Report — non-zero exit on errors in the run or an SLA breach
- perf-report -i results.json --tool k6 -o report.json --summary --fail-on-error
# Regression check against recent runs
- perf-report compare --baseline 'baselines/*.json' --current report.json
artifacts:
paths:
- report.json
when: always

pipeline {
agent {
docker {
image 'node:20-alpine'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
TARGET_HOST = credentials('target-host')
PERF_REPORTING_LICENSE = credentials('perf-reporting-license')
}
stages {
stage('Lint') {
steps {
sh 'pip install perf-lint-tool && perf-lint check tests/performance/'
}
}
stage('Load Test') {
steps {
sh '''
docker run --rm \
-v ${WORKSPACE}/tests/performance:/tests \
-e TARGET_HOST=${TARGET_HOST} \
ghcr.io/markslilley/perf-k6:latest \
k6 run /tests/load-test.js \
-e VUS=${VUS:-100} -e DURATION=${DURATION:-5m} \
--out json=/tests/results.json
'''
}
}
stage('Report & Check') {
steps {
sh '''
perf-report -i tests/performance/results.json --tool k6 \
-o report.json --summary --fail-on-error
'''
sh '''
perf-report compare --baseline 'baselines/*.json' --current report.json
'''
}
}
}
post {
always {
sh '''
perf-report -i tests/performance/results.json --tool k6 \
-f html -o perf-report.html
'''
archiveArtifacts artifacts: 'perf-report.html', fingerprint: true
}
}
}

azure-pipelines.yml
trigger:
branches:
include:
- main
paths:
include:
- tests/performance
schedules:
- cron: '0 2 * * *'
displayName: Nightly performance run
branches:
include:
- main
pool:
vmImage: ubuntu-latest
variables:
VUS: 100
DURATION: 5m
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.12'
- script: pip install perf-lint-tool && perf-lint check tests/performance/
displayName: Lint
- script: |
docker run --rm \
-v $(Build.SourcesDirectory)/tests/performance:/tests \
-e TARGET_HOST=$(TARGET_HOST) \
ghcr.io/markslilley/perf-k6:latest \
k6 run /tests/load-test.js \
-e VUS=$(VUS) -e DURATION=$(DURATION) \
--out json=/tests/results.json
displayName: Load test
- script: |
perf-report -i tests/performance/results.json --tool k6 \
-o report.json --summary --fail-on-error
displayName: Report
env:
PERF_REPORTING_LICENSE: $(PERF_REPORTING_LICENSE)
- script: |
perf-report compare --baseline 'baselines/*.json' --current report.json
displayName: Regression check
env:
PERF_REPORTING_LICENSE: $(PERF_REPORTING_LICENSE)
- script: |
perf-report -i tests/performance/results.json --tool k6 \
-f html -o $(Build.ArtifactStagingDirectory)/perf-report.html
displayName: Render an HTML report
condition: always()
env:
PERF_REPORTING_LICENSE: $(PERF_REPORTING_LICENSE)
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: $(Build.ArtifactStagingDirectory)
ArtifactName: perf-report
condition: always()

Gate on nightly, not on every commit: Statistical regression detection requires enough runs to be meaningful. Use commit-triggered runs for smoke tests only; full load tests should be nightly or on release branches.

Separate staging environments for CI performance tests: Never run load tests against production from CI without explicit controls. Always target a staging or load-test environment.

Parallelise where safe: Lint and smoke tests can run in parallel. Load tests should run sequentially to avoid resource contention affecting results.