Checkpoint Lab — CI/CD Performance Gates in GitHub Actions, GitLab CI, and Jenkins
The checkpoint proves the gate before CI trusts it. Degraded mode keeps all HTTP responses successful but changes target delay to 180 ms, so JMeter execution succeeds while the raw p95 policy fails. Restoring only target delay to 40 ms makes the exact same workload and SLO pass.
Learning objectives
- Complete the chapter checkpoint for CI/CD Performance Gates in GitHub Actions, GitLab CI, and Jenkins as one reviewable, bounded experiment.
- State the workload, predictions, acceptance criteria, authorization boundary, and abort conditions before execution.
- Reconcile configured versus achieved work with JTL, jmeter.log, target evidence, and generator validity before making a conclusion.
- Produce an evidence packet that records the exact inputs, results, diagnosis or gate outcome, and any material limitations.
- Perform cleanup or rollback and explain how the checkpoint evidence hands off to the next chapter or operating practice.
1. Exact assumptions and ceilings
| Item | Checkpoint |
|---|---|
| Runtime | JMeter 5.6.3 / Java17 / Python3 stdlib / no plugins. |
| Target | 127.0.0.1:8028 launcher-owned fixture. |
| Workload | 5 threads ×20 loops =100 samples/run; 100 ms pacing. |
| Degraded | 180 ms delay, 0 intentional HTTP errors. |
| Baseline | 40 ms delay, 0 intentional HTTP errors. |
| Policy | 100 JTL +100 target events, nearest-rank p95≤120 ms, errors≤1%. |
| Expected degraded | engine=0, gate/final=10 SLO_FAIL. |
| Expected baseline | engine=0, gate/final=0 PASS. |
| Artifacts | JTL, jmeter.log, dashboard, target events/summary, gate/launcher JSON, versions/input manifest, consoles. |
2. Freeze inputs
Hash JMX, properties, SLO, evaluator, launcher and fixture. The launcher writes an input manifest automatically. Do not edit them between the two phases.
3. Predictions
- Both runs: exactly 100 JTL Work rows and 100 target events.
- Degraded: engine execution 0, p95≈180 ms, error=0%, gate exit10.
- Baseline: engine 0, p95≈40–60 ms, error=0%, gate exit0.
- Both dashboards/logs survive; degraded evidence is never overwritten.
4. Provision and inspect
python .\tools\provision_jmeter.py --dest .tools
$env:JMETER_HOME = (Resolve-Path ".\.tools\apache-jmeter-5.6.3").Path
& "$env:JMETER_HOME\bin\jmeter.bat" -v
java -version
python --version
Get-Content .\policy\slo.json
5. Execute expected SLO failure
python .\tools\run_performance_gate.py `
--fixture-mode degraded `
--run-id p28-check-degraded `
--out .\results\p28-check-degraded
$DegradedExit = $LASTEXITCODE
if ($DegradedExit -ne 10) {
throw "Expected SLO exit 10, got $DegradedExit"
}
6. Preserve and inspect before repair
Get-Content .\results\p28-check-degraded\launcher.json
Get-Content .\results\p28-check-degraded\gate.json
Get-Content .\results\p28-check-degraded\target-summary.json
Get-Item .\results\p28-check-degraded\results.jtl,
.\results\p28-check-degraded\jmeter.log,
.\results\p28-check-degraded\dashboard\index.html,
.\results\p28-check-degraded\target-events.jsonl
Require engine_exit_code=0,
gate_exit_code=10, 100 JTL, 100 target, p95>120 and
zero errors.
7. Prove the exit-code trap
launcher.json:
engine_exit_code: 0
gate_exit_code: 10
final_exit_code: 10
gate.json:
status: SLO_FAIL
validity_failures: []
slo_failures:
- p95 ... > 120ms
This is the deliberately broken case that a JMeter-exit-only pipeline would falsely pass.
8. Restore only the fixture
python .\tools\run_performance_gate.py `
--fixture-mode baseline `
--run-id p28-check-baseline `
--out .\results\p28-check-baseline
if ($LASTEXITCODE -ne 0) {
throw "Expected PASS exit 0"
}
9. Before/after verification
| Evidence | Degraded | Restored | Meaning |
|---|---|---|---|
| Configured | 5×20=100 | 5×20=100 | Same workload. |
| Achieved | 100 JTL /100 target | 100/100 | Valid both. |
| Errors | 0% | 0% | Latency-only gate. |
| Fixture delay | 180 ms | 40 ms | Only causal change. |
| p95 | ~180+ | ~40–60 | Crosses fixed 120 ms policy. |
| JMeter engine | 0 | 0 | Execution healthy both. |
| Gate/final | 10 | 0 | Policy catches/restores. |
| Artifacts | retained | retained | Failure diagnosable. |
10. Map the same command to CI
GitHub Actions:
name: jmeter-smoke-gate
on:
pull_request:
permissions:
contents: read
jobs:
performance-gate:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
- name: Provision JMeter 5.6.3
run: python3 tools/provision_jmeter.py --dest .tools
- name: Run portable performance gate
env:
JMETER_HOME: ${{ github.workspace }}/.tools/apache-jmeter-5.6.3
run: >
python3 tools/run_performance_gate.py
--fixture-mode baseline
--run-id pr-${{ github.run_id }}
--out results/p28-ci
- name: Upload performance evidence
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: jmeter-performance-evidence
path: results/p28-ci/
retention-days: 7
GitLab CI:
stages:
- test
performance_gate:
stage: test
timeout: 10m
script:
- java -version
- python3 tools/provision_jmeter.py --dest .tools
- export JMETER_HOME="$CI_PROJECT_DIR/.tools/apache-jmeter-5.6.3"
- python3 tools/run_performance_gate.py --fixture-mode baseline --run-id "gitlab-$CI_PIPELINE_ID" --out results/p28-ci
artifacts:
when: always
expire_in: 7 days
paths:
- results/p28-ci/
Jenkins:
pipeline {
agent { label 'java17-python3' }
options {
timeout(time: 10, unit: 'MINUTES')
}
stages {
stage('Performance gate') {
steps {
sh 'java -version'
sh 'python3 tools/provision_jmeter.py --dest .tools'
sh '''
export JMETER_HOME="$WORKSPACE/.tools/apache-jmeter-5.6.3"
python3 tools/run_performance_gate.py \
--fixture-mode baseline \
--run-id "jenkins-${BUILD_NUMBER}" \
--out results/p28-ci
'''
}
}
}
post {
always {
archiveArtifacts artifacts: 'results/p28-ci/**',
allowEmptyArchive: true,
fingerprint: true
}
}
}
The real PR job should use the approved baseline/real authorized target mode, not permanently run synthetic degraded mode. A dedicated gate-self-test can deliberately expect exit10.
11. Evidence packet
| Artifact | Required |
|---|---|
| Launcher/runtime | Common command + JMeter5.6.3/Java17 + input/version manifest. |
| Raw evidence | JTL + matching jmeter.log + dashboard for both phases. |
| Policy | slo.json; optional reviewed baseline. |
| Gate | gate.json + launcher.json with separate engine/gate/final exits. |
| Target | 100 events/run, mode/delay/service evidence. |
| Provider | Three thin snippets with artifacts retained on failure. |
| Validity | Configured/achieved counts + runner/generator/target limitations. |
12. Validity statement
13. Checklist
- Only 127.0.0.1:8028.
- JMeter5.6.3 / Java17 / no plugins.
- Same JMX/properties/SLO hashes.
- 100 configured and 100/100 achieved each phase.
- Degraded engine0/gate10; baseline engine0/gate0.
- Only target delay 180→40 changes.
- p95≤120/error≤1 policy explicit.
- Artifacts survive failure in all provider patterns.
- No baseline auto-update or retry-until-green.
14. Cleanup / rollback
-
Launcher terminates its fixture in
finally; confirm no listener remains on port 8028. - Keep degraded/baseline evidence until review.
-
Remove local
.tools/apache-jmeter-5.6.3only if desired; reprovisioning re-verifies SHA-512. - Delete result directories after retention policy.
- No production/shared target, real credential, cloud CI resource, RMI engine, container cluster or OS/JVM global tuning was modified.
15. Production operating-model addition and Chapter 29 bridge
Chapter 28 adds a CI performance-gate contract: pinned runtime, common launcher, exact workload, JTL/target validity, explicit percentile/error formula, reviewed SLO/baseline, separate engine/gate/invalid-run semantics, runner/generator evidence, unconditional artifact retention, gate cadence/severity and baseline/secret governance.
Chapter 29 moves to Latency Percentiles, Throughput, Errors, Saturation, and Bottleneck Diagnosis, where the metrics behind these gates are interpreted causally rather than as isolated threshold numbers.
Knowledge check
Why is degraded exit10 expected?
The workload is valid and JMeter succeeds, but the 180 ms target delay violates p95≤120 ms.
What remains identical between phases?
Runtime, JMX, properties, workload, evaluator and SLO; only fixture delay changes.
Why validate target count?
It independently proves achieved workload before latency policy is trusted.
A failed CI job has no JTL artifact. What failed?
The provider artifact-retention contract.
What is the Chapter29 bridge?
Learn how p95/throughput/errors/saturation relate causally to bottlenecks before designing deeper gates.
Official references and version notes
- Apache JMeter downloads — JMeter 5.6.3 and Java 8+ requirement.
- JMeter execution guidance — GUI for authoring/debug and CLI/non-GUI for load.
-
JMeter Dashboard Report
—
-e -o/-g -oand dashboard CSV requirements. -
GitHub Actions build/test examples
— artifact upload with
always()after test failure. -
GitHub setup-java
— stable Java setup action; example uses
v5with Temurin 17. -
GitLab CI YAML reference
—
artifacts:when: alwayssemantics. -
Jenkins Declarative Pipeline syntax
—
post/always, timeout, agent behavior. -
Jenkins tests and artifacts
—
archiveArtifactsin an always-run post block.
Statements were rechecked against current primary documentation on
2026-09-05. The mandatory runtime is
Apache JMeter 5.6.3 with Java 17; no third-party
JMeter plugin is required. The provider-neutral provisioner
verifies Apache's published SHA-512 before installing JMeter.
Meaningful execution stays in CLI mode and produces the HTML
dashboard using -e -o. The gate evaluator is
Python-standard-library only and calculates p95 from raw CSV JTL
using an explicitly documented nearest-rank method. The dashboard
remains diagnostic evidence rather than the policy parser.
GitHub's minimal pattern uses actions/checkout@v6,
stable actions/setup-java@v5 with Temurin 17, and
actions/upload-artifact@v4 guarded by
always(). GitLab explicitly sets
artifacts:when: always, and Jenkins archives in
Declarative post { always { ... } }. Provider
YAML/Groovy stays thin; performance formulas and exit
classification live in the shared launcher/evaluator.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.