Chapter 28Lesson 05~310 minutes

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.

CheckpointEngine 0 / Gate 10100 samplesp95≤120 msArtifacts on failure

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.
Abort: non-loopback target, >100 events/run, version mismatch, count mismatch, generator/runner saturation, missing artifacts, real-secret exposure or any attempt to loosen policy instead of restoring the fixture.

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

Example: “JMeter 5.6.3/Java17 executed the same 5-thread ×20-loop CLI plan twice against a launcher-owned localhost fixture. Both runs produced exactly 100 JTL Work samples and 100 independent target events with zero HTTP errors; engine exit was zero and dashboards were generated. Degraded mode changed only service delay to 180 ms, causing nearest-rank p95 to exceed the fixed 120 ms SLO and evaluator exit10. Restoring delay to 40 ms without changing JMX/properties/SLO produced p95 below the limit and exit0. Both JTL/log/dashboard/target/gate/launcher/version/input evidence packets were retained. This validates the gate mechanics in the controlled local workload; it does not establish production latency or a noisy shared-runner baseline.”

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

  1. Launcher terminates its fixture in finally; confirm no listener remains on port 8028.
  2. Keep degraded/baseline evidence until review.
  3. Remove local .tools/apache-jmeter-5.6.3 only if desired; reprovisioning re-verifies SHA-512.
  4. Delete result directories after retention policy.
  5. 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?

What remains identical between phases?

Why validate target count?

A failed CI job has no JTL artifact. What failed?

What is the Chapter29 bridge?

Next chapter

Latency Percentiles, Throughput, Errors, Saturation, and Bottleneck Diagnosis

Chapter 29 deepens the metric semantics behind performance gates and bottleneck conclusions.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.