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.
Pipeline Stages
Section titled “Pipeline Stages”A complete performance pipeline has four stages:
- Lint (
perf-lint) — catch script anti-patterns before they reach production - Execute (
perf-containers) — run the test with the correct Docker image - Report (
perf-report) — Apdex and SLA checks, with an exit code the build can gate on - Regression check (
perf-report compare) — fail the build if this run is worse than recent ones
Required Secrets/Variables
Section titled “Required Secrets/Variables”Set these in your CI/CD secrets store before configuring pipelines:
| Secret/Variable | Description |
|---|---|
TARGET_HOST | URL of the system under test (staging) |
PERF_REPORTING_LICENSE | perf-reporting Pro or Enterprise key — the CLI, SLA gating and compare are the paid capability |
GitHub Actions
Section titled “GitHub Actions”Complete pipeline
Section titled “Complete pipeline”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: 30GitLab CI
Section titled “GitLab CI”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: alwaysJenkins (Declarative)
Section titled “Jenkins (Declarative)”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 DevOps
Section titled “Azure DevOps”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.