You wired performance tests into Jenkins, watched the gate go green a few times, and then it started lying to you. A build failed with a p95 spike that vanished on a re-run. A “passing” load test was actually capped by a load generator pinned at 98% CPU, not by your application. Someone added disableConcurrentBuilds()… after two simultaneous runs trashed each other’s numbers. Eventually the team did what teams do with gates they don’t trust: quietly set it to non-blocking and stopped reading the report.
That failure pattern isn’t a tooling problem. It’s a missing playbook. Most of what’s out there is either a single-tool tutorial (one stage, one threshold, no scaling) or a dense vendor PDF that buries the useful parts under marketing. Neither tells you how to build a gate that holds up under real conditions – where generators exhaust, environments drift, and percentiles wobble.
This guide fixes that. You’ll get a tool-neutral integration blueprint paired with complete, copy-paste implementations using WebLOAD by RadView: how to author a production-grade Jenkinsfile, drive load tests via API and CLI, derive defensible performance gates from your SLOs, and – critically – diagnose the three failure modes that sabotage most performance pipelines. Everything below was last validated on 2026-06-15 against Jenkins LTS 2.452.x and the WebLOAD plugin line current at that date. Treat the version notes as reproducibility anchors, not decoration.
- Where Performance Testing Fits in a Jenkins CI/CD Pipeline
- Setting Up WebLOAD by RadView with Jenkins: Plugin and API/CLI Paths
- A Production-Ready Jenkinsfile for Load Testing (Copy-Paste Template)
- Defining Evidence-Based Performance Gates and Quality Criteria
- Automating the Gate: Query, Stabilize, Compare, Fail
- Diagnosing the Failure Modes That Quietly Sabotage Performance Pipelines
- Test Data and Environment Management for Reliable Runs
- Advanced Patterns: Multi-Branch, Canary, and Nightly Regression Suites
- Frequently Asked Questions
- References
Where Performance Testing Fits in a Jenkins CI/CD Pipeline

A mature pipeline runs five stages in order: build → unit test → integration test → performance gate → deploy. The performance gate sits deliberately after integration tests pass (no point load-testing a broken build) and before deploy (the whole point is catching regressions pre-production). Place it anywhere else and you either waste generator time on builds that won’t ship or let latency regressions slip past the only stage that measures them.
The real design tension is build-time cost versus early detection. A full 12-minute inline load run adds 12 minutes to every pull-request feedback loop – painful when developers expect sub-five-minute CI. But catching a 40% p95 latency regression at PR time, rather than in a production incident three days later, changes the cost equation entirely. DORA’s research on continuous delivery is blunt about this: teams with fast, reliable automated testing in their deployment pipeline show measurably better change-failure rates and recovery times [1]. The resolution isn’t “test everything inline” or “test nothing inline” – it’s tiering, which the next section covers.
When you start encoding pass/fail criteria into that gate, anchor them to service objectives rather than gut feel. Google’s SRE guidance on Service Level Objectives is the canonical reference for doing this without picking arbitrary numbers [2]. We’ll build the full methodology later; for now, just know the gate’s thresholds should trace back to something you actually promised users.
Shift-Left vs. Scheduled: When to Gate on Every Commit
Don’t run your heavy suite on every commit. The workable split is: a 1-minute smoke test at ~50 virtual users on every PR (does the critical path still respond under nominal concurrency?), and a 30-minute soak or full load suite on a nightly schedule. The per-commit smoke catches gross regressions – a new N+1 query, a blocking call added to a hot path – within the fast-feedback window developers tolerate. The nightly suite catches slow-burn issues (memory leaks, connection pool exhaustion) that only surface under sustained load, without taxing every PR.
This mirrors DORA’s fast-feedback principle: keep the inline check cheap enough that nobody is tempted to skip it, and push expensive validation to a schedule where a 30-minute runtime costs nobody their flow. This approach reflects broader shift-left and shift-right methodologies in performance engineering.
Load vs. Performance vs. Stress: Terminology That Drives Pipeline Design
These three words get used interchangeably, and the conflation produces badly designed pipelines. Load testing measures behavior under sustained, expected concurrent volume (e.g., 2,000 VUs for 20 minutes). Performance testing measures responsiveness under defined conditions – latency percentiles and throughput at a target load. Stress testing pushes past expected limits to find the breaking point. If you’re unsure which to run when, the different types of performance testing breaks down each approach with concrete examples.
Each maps to a different Jenkins pattern. Load and performance testing belong in your gated stages because they validate against committed thresholds. Stress testing – ramping VUs until your error rate crosses, say, 5% – is destructive by design and produces a capacity number, not a pass/fail. Run stress nightly or on-demand only; gating a PR on a breaking-point test means every PR “fails” by definition, which is meaningless.
Declarative vs. Scripted Pipelines for Load Workloads
Default to Declarative. It gives you the post{} section for guaranteed archiving, a clean parameters{} block for environment and load-profile injection, and options{} for concurrency controls – the three things load pipelines need most. The structure is readable enough that a QA lead who isn’t a Groovy programmer can audit the gate logic.
Reach for Scripted (or script{} blocks inside Declarative) only when you need programmatic behavior Declarative can’t express: computing thresholds dynamically from a baseline query, generating a variable number of parallel generator stages from a list, or branching gate logic on runtime conditions. In practice, most teams write Declarative with a few script{} escape hatches for the threshold math. The official Jenkins Pipeline syntax documentation is the reference for both styles.
Setting Up WebLOAD by RadView with Jenkins: Plugin and API/CLI Paths
There are two ways to connect WebLOAD to Jenkins, and you’ll likely use both: the plugin for GUI-managed jobs and quick setup, and API/CLI invocation for headless, fully-coded automation. This walkthrough was validated against Jenkins LTS 2.452.x; pin your own versions and stamp a “last validated” date in your runbook so future-you can reproduce the setup.
Whichever path you take, never hardcode credentials. Store the WebLOAD console endpoint, API token, and any database passwords in the Jenkins credential store and bind them at runtime:
withCredentials([
string(credentialsId: 'webload-api-token', variable: 'WL_TOKEN'),
string(credentialsId: 'webload-console-url', variable: 'WL_CONSOLE')
]) {
sh 'curl -sf -H "Authorization: Bearer $WL_TOKEN" "$WL_CONSOLE/api/v1/health"'
}
This keeps secrets out of build logs and the Jenkinsfile itself – a baseline production requirement, not a nicety. The credentials binding syntax is documented in the official Jenkins Pipeline syntax documentation.
Plugin Installation, Verification, and Troubleshooting
Install via Manage Jenkins > Plugins > Available plugins, search for the WebLOAD integration, select it, and install. The plugin loads without a controller restart in current LTS lines for the install itself, but a restart is required before the new build step and post-build actions appear in job configuration – so check “Restart Jenkins when installation is complete and no jobs are running.” Verify success under Manage Jenkins > Plugins > Installed plugins, then open any pipeline job’s configuration and confirm the WebLOAD step is selectable.
Common install issues and their fixes, drawn from real setups:
- Plugin missing from the Available tab → Your controller can’t reach the update center. Click “Check now” on the Advanced tab, and verify the controller’s outbound proxy settings if you’re behind a corporate firewall.
- Step doesn’t appear after install → You skipped the restart. Restart the controller; the post-build actions register on boot.
- “Cannot connect to console” at runtime → The agent running the build can’t reach the WebLOAD console host/port. Confirm network path from the agent, not the controller – they’re often on different subnets.
- Credentials not resolving → The credential ID in the job doesn’t match the store, or the credential is scoped to a folder the job isn’t in.
The RadView WebLOAD Automation User Guide [3] is the authoritative reference for plugin-specific configuration fields. Jenkins’ own plugin management behavior – including when restarts are mandatory – is documented at jenkins.io [4].
WebLOAD-as-Code: Triggering Tests via API and Command Line
The GUI plugin is fine for stable, long-lived jobs. But for ephemeral containerized agents, headless runs, and full pipeline-as-code, drive WebLOAD through its API or command line. The pattern is: authenticate, trigger a session, poll for completion, retrieve the SLA result, and act on it. RadView documents this end-to-end approach in its guide on how to automate load testing for DevOps using the WebLOAD API.
stage('Run WebLOAD Test') {
steps {
withCredentials([string(credentialsId: 'webload-api-token', variable: 'WL_TOKEN')]) {
script {
// 1. Trigger the load session, capturing the session ID
def sessionId = sh(returnStdout: true, script: '''
curl -sf -X POST "$WL_CONSOLE/api/v1/sessions" \
-H "Authorization: Bearer $WL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"script":"checkout_flow.wlp","vusers":2000,"duration":1200}' \
| jq -r .sessionId
''').trim()
// 2. Poll for completion every 10 seconds
timeout(time: 30, unit: 'MINUTES') {
waitUntil {
def status = sh(returnStdout: true, script: """
curl -sf "\$WL_CONSOLE/api/v1/sessions/${sessionId}" \
-H "Authorization: Bearer \$WL_TOKEN" | jq -r .status
""").trim()
echo "Session ${sessionId} status: ${status}"
sleep(10)
return status == 'COMPLETED' || status == 'FAILED'
}
}
// 3. Retrieve SLA pass/fail result for later gate evaluation
env.WL_RESULT = sh(returnStdout: true, script: """
curl -sf "\$WL_CONSOLE/api/v1/sessions/${sessionId}/results" \
-H "Authorization: Bearer \$WL_TOKEN" > results.json
cat results.json | jq -r .slaStatus
""").trim()
}
}
}
}
The 10-second poll interval balances responsiveness against hammering the console API. Wrap the whole thing in a timeout so a hung session fails the build instead of blocking an executor indefinitely. Endpoint paths and the full result schema live in the WebLOAD Automation documentation [3].
Plugin vs. API: Choosing Your Integration Point
| Scenario | Use the plugin | Use API/CLI |
|---|---|---|
| Stable long-lived job, GUI-managed config | ✓ | |
| Ephemeral / containerized agents | ✓ | |
| Dynamic load profiles computed at runtime | ✓ | |
| Quick proof-of-concept setup | ✓ | |
| Fully version-controlled pipeline-as-code | ✓ |
WebLOAD’s API-first design means the CLI and REST endpoints expose the same capabilities as the GUI, so you’re never locked into clicking through a console for automation. When your agents spin up and tear down per build, the API path is the only sane choice.
A Production-Ready Jenkinsfile for Load Testing (Copy-Paste Template)
Here’s the centerpiece. “Production-ready” means four things tutorial snippets skip: secrets management (no hardcoded credentials), idempotency (each run sets up and tears down its own state), failure handling (results archive even when the build fails), and observability (the breaching metric is visible without digging through logs). Commercial Jenkins template packs sell for around $39; this one is free, and you can extend it across teams via Jenkins Shared Libraries.
pipeline {
agent { label 'load-runner' }
parameters {
choice(name: 'TARGET_ENV', choices: ['staging', 'preprod'], description: 'Target environment')
string(name: 'VU_COUNT', defaultValue: '2000', description: 'Total virtual users')
string(name: 'RAMP_DURATION', defaultValue: '300', description: 'Ramp-up seconds')
}
options {
disableConcurrentBuilds() // prevent two runs colliding on shared resources
timeout(time: 45, unit: 'MINUTES')
}
stages {
stage('Setup & Seed') {
steps {
// idempotent: create representative test data for this run
sh './scripts/seed-data.sh ${TARGET_ENV} 500000'
}
}
stage('Execute Load Test') {
steps {
lock(resource: "loadenv-${params.TARGET_ENV}") { // serialize shared env access
withCredentials([string(credentialsId: 'webload-api-token', variable: 'WL_TOKEN')]) {
sh './scripts/run-webload.sh ${VU_COUNT} ${RAMP_DURATION} > results.json'
}
}
}
}
stage('Evaluate Gate') {
steps {
script {
def r = readJSON file: 'results.json'
// hard-fail on p99; soft-warn on p95 (derivation explained later)
if (r.p99 > 800) { error "HARD FAIL: p99 ${r.p99}ms > 800ms" }
else if (r.p95 > 400) { unstable("WARN: p95 ${r.p95}ms > 400ms") }
if (r.errorRate > 1.0) { error "HARD FAIL: error rate ${r.errorRate}% > 1%" }
}
}
}
}
post {
always { archiveArtifacts artifacts: 'results.json', allowEmptyArchive: true
publishHTML(target: [reportDir: 'report', reportFiles: 'index.html', reportName: 'Load Report']) }
failure { slackSend(channel: '#perf-alerts',
message: "FAILED ${env.JOB_NAME} #${env.BUILD_NUMBER}: ${currentBuild.description}") }
success { echo 'All performance gates passed.' }
cleanup { sh './scripts/teardown-data.sh ${TARGET_ENV}' } // idempotent teardown
}
}
Stage Anatomy: Setup, Execute, Evaluate, Report

Each stage earns its place. Setup & Seed creates the run’s own data so results stay comparable build-over-build. Execute Load Test wraps the run in a lock() so a second pipeline can’t contaminate measurements (more on that in the failure-modes section). Evaluate Gate parses results and translates breaches into build status. The post{} block is where engineering maturity shows: always archives results and publishes the HTML report even when the build fails – exactly when you most need the data; failure fires a Slack alert with the build context; and cleanup tears down seeded data so the next run starts clean. Without always, a failed build often discards the very evidence you need to diagnose it. The post{} conditions are detailed in the Jenkins Pipeline syntax documentation.
Parameterization and Parallel Multi-Generator Stages
The parameters{} block lets one template serve staging and preprod at different VU counts without forking the file. To generate load beyond a single generator’s capacity, fan out across agents by label:
stage('Distributed Load') {
parallel {
stage('Generator A') { agent { label 'loadgen-1' }
steps { sh './run-webload.sh 1000 300' } }
stage('Generator B') { agent { label 'loadgen-2' }
steps { sh './run-webload.sh 1000 300' } }
}
}
This splits 2,000 VUs across two dedicated agents – 1,000 each – so neither generator becomes the bottleneck. WebLOAD’s distributed load generation coordinates multiple generators against one target while aggregating results centrally [3]. Define shared parameters once in a Shared Library so every team’s pipeline inherits the same conventions.
Webhook and Token Triggers for Commit-Driven Runs
To fire the smoke test on every push, configure a webhook from your SCM to https://<jenkins-host>/github-webhook/ (or the generic webhook endpoint for other providers) and add a trigger to the Jenkinsfile:
triggers { githubPush() }
Generate a Jenkins API token under the user’s configuration for authenticated trigger calls, and store it as a credential rather than embedding it in the webhook URL. The triggers directive options are documented in the Jenkins Pipeline syntax reference.
Defining Evidence-Based Performance Gates and Quality Criteria
A gate is only as trustworthy as the numbers behind it. The fastest way to lose team buy-in is to fail builds on thresholds someone made up in a meeting. The fix is to derive every threshold from a documented service objective.
Use percentiles, not averages. As Google’s SRE guidance puts it, “a high-order percentile, such as the 99th or 99.9th, shows you a plausible worst-case value, while using the 50th percentile (also known as the median) emphasizes the typical case” [2]. An average hides the tail where your unhappy users live. Choosing the right indicators here matters – the performance metrics that matter in performance engineering covers response time, throughput, and error rates in depth. And don’t anchor targets to today’s numbers: the same guidance warns “Don’t pick a target based on current performance” – because that just enshrines whatever you happen to be doing, regression and all [2].
A concrete multi-tier gate, like the one in the template above: p95 < 400ms (soft warning), p99 < 800ms (hard fail), error rate < 1% (hard fail). The p95 warning catches typical-case drift before it’s user-visible; the p99 hard-fail blocks worst-case regressions from shipping.
From SLI to SLO to Gate: A Threshold-Setting Framework
The chain runs SLI → SLO → gate. Say your SLA promises “99.9% of checkout requests complete in under 1 second.” That’s your contract. Set your internal SLO tighter – commonly 20 – 40% stricter – so you have headroom before breaching the customer-facing number: 99.9% under 700ms. Now encode that as a gate. Querying a metrics backend, the Site Reliability Workbook shows computing the percentile directly: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[7d])) [5]. Your gate fails if that value exceeds 700ms. The number isn’t arbitrary – it’s traceable straight back to the SLA, with engineered margin.
Soft-Warning vs. Hard-Fail Tiers
The Workbook validates tiering directly: “if 90% of users’ requests return within 100 ms, but the remaining 10% take 10 seconds, many users will be unhappy. A latency SLO can capture this user base by setting multiple thresholds: 90% of requests are faster than 100 ms, and 99% of requests are faster than 400 ms” [5]. In Jenkins, that maps to two build states:
if (r.p99 > 800) { currentBuild.result = 'FAILURE'; error "p99 breach" } // blocks
else if (r.p95 > 400) { unstable("p95 breach") } // warns, doesn't block
A soft breach marks the build UNSTABLE – visible, tracked, but non-blocking. A hard breach sets FAILURE and stops promotion. Use soft tiers for metrics where occasional drift is tolerable and hard tiers for objectives tied to your SLA.
Baseline Comparison and Regression Deltas
Absolute thresholds miss gradual decay. A run at p95 = 390ms passes a 400ms gate – but if last week it was 250ms, you have a 56% regression hiding under the limit. Persist metrics build-over-build (via the Performance Plugin or an InfluxDB/Grafana backend) and add a relative gate: fail if p95 rises more than 10% versus the 7-day rolling baseline. This catches the slow creep that absolute gates wave through, and it’s the single most-overlooked check in competing guides. For the broader context of catching regressions as code evolves, see understanding regression testing.
Automating the Gate: Query, Stabilize, Compare, Fail
The gate’s runtime path is: deploy to staging, wait for metrics to stabilize, query the backend, compare against thresholds, set build status. That stabilization wait is the step almost every tutorial skips – and it’s why so many gates flake.
Right after a deploy, you’re measuring noise: cold caches, JIT compilation warming up, connection pools filling. Query immediately and you’ll catch a transient spike that has nothing to do with your code. Insert an explicit wait:
stage('Stabilize & Evaluate') {
steps {
sleep(time: 60, unit: 'SECONDS') // let caches warm, JIT settle, pools fill
script {
def p99 = sh(returnStdout: true, script: '''
curl -sf "$PROM/api/v1/query" --data-urlencode \
'query=histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[2m]))' \
| jq -r '.data.result[0].value[1]'
''').trim().toFloat() * 1000 // seconds -> ms
if (p99 > 800) { error "p99 ${p99}ms exceeds 800ms gate" }
}
}
}
Sixty seconds is a reasonable default for JVM-based services; tune it to your warm-up profile. The vendor-neutral way to collect the metrics you’re querying is via OpenTelemetry metrics, so your gate logic stays portable across observability backends.
Parsing WebLOAD Results vs. Querying a Metrics Backend
You have two data sources. Parse WebLOAD reports directly when the load tool is your source of truth – extract a field like results.transactions.checkout.p99 from the JSON report and compare it. Query a metrics backend when you want application-side measurements correlated with the load: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[7d])) against Prometheus [5]. WebLOAD’s reporting output gives you the load-generator’s view (what the client experienced); the backend gives you the server’s view (what the app actually did). Comparing both is how you catch a generator that’s masking the real response time. WebLOAD report field structure is documented in the Automation guide [3].
Notification and Ticketing on Gate Breach
When a gate trips, the alert should carry the specific breaching metric, not a generic “build failed”:
post {
failure {
slackSend(channel: '#perf-alerts',
message: "🔴 ${env.JOB_NAME} #${env.BUILD_NUMBER} — ${env.BREACH_DETAIL}")
}
}
Set env.BREACH_DETAIL = "p99=910ms (limit 800ms)" at the point of failure so whoever picks up the alert knows exactly what broke before they open the console. The post{ failure } condition is part of the Pipeline syntax.
Diagnosing the Failure Modes That Quietly Sabotage Performance Pipelines
This is where pipelines live or die. Three failure modes erode trust in gates faster than any flaky functional test, and most guides ignore all three. Each follows the same diagnostic shape: symptom → root cause → fix.
The tell-tale example: throughput plateaus at 4,200 req/s and stays there even as you add virtual users, while your application’s CPU sits at 40%. That’s not your app’s ceiling – that’s your generator’s. Reading that plateau as “the system maxes out at 4,200 req/s” is one of the most common and most damaging misdiagnoses in load testing.
Is It the Generator or the System Under Test? Detecting Load Generator Exhaustion

When throughput flatlines despite added load, check the generator before blaming the app. Watch these four resources on the load generator itself:
- CPU – warning above 80%; a saturated generator can’t issue requests fast enough, capping observed throughput artificially.
- Memory – climbing steadily through a long run signals a leak; the generator slows or crashes mid-test.
- Open sockets – approaching the OS
ulimit(often 1,024 by default) starves new connections; raise the limit or distribute load. - File descriptors – same ceiling problem; each connection and open log file consumes one.
If the generator is healthy (CPU < 80%, sockets well under limit) and throughput still plateaus while app CPU climbs, now you’ve found a real SUT bottleneck. For a deeper treatment of separating generator limits from application limits, see RadView’s guide on how to detect overloaded load generators in load testing. Capacity planning for WebLOAD’s distributed generators is documented to help you size headroom before trusting any result [3]. Monitor these generator metrics the same vendor-neutral way you monitor the app, via OpenTelemetry metrics.
Distributing Load Across Jenkins Agents to Prevent Exhaustion
The structural fix for generator exhaustion is horizontal scale. Need 10,000 VUs but one agent tops out around 2,500? Split across four labeled agents at 2,500 each, running in parallel. Each agent stays in its comfort zone, and the aggregated load hits the target without any single generator saturating. Agent labels and executor distribution are covered in the Jenkins Pipeline syntax documentation.
A Taxonomy of Flaky Performance Tests (and How to Stabilize Each)

Performance flakiness has distinct causes from functional flakiness, and each maps to a specific mitigation:
- Cold caches / JIT warmup → Add a warm-up stage before measurement. In one JVM service, prepending a 2-minute warm-up run cut p95 run-to-run variance from roughly ±35% to ±6% – turning a gate teams ignored into one they trusted.
- Noisy neighbors (shared cloud infra) → Use dedicated agents for load generation, or run on isolated infrastructure during the test window.
- Overly tight thresholds → Gate on percentiles (p95/p99) rather than max, which is dominated by a single outlier. A
maxgate fails the moment one request hits a GC pause. - Shared environments → Isolate the test environment, or serialize access (next section), so a parallel deploy doesn’t perturb your measurements.
For genuinely noisy metrics, Google’s SRE practice of building tolerance into SLO-based thresholds applies directly – set the gate to absorb legitimate variance rather than chasing every blip [2]. And for tests that flake intermittently despite mitigation, quarantine them (run, but don’t block) until stabilized, rather than disabling the entire gate.
Serializing Shared Resources: Solving Concurrent Pipeline Conflicts
Two performance runs hitting the same staging environment or generator pool at once is doubly destructive: each adds load the other measures as application latency, so both produce inflated, meaningless numbers. The first run sees the second’s traffic as a slowdown and vice versa.
Two Jenkins-native controls solve this. For a single pipeline, prevent overlapping runs:
options { disableConcurrentBuilds() }
For a shared resource that multiple different pipelines contend for, use the Lockable Resources plugin. Per its official documentation, “if a build requires a resource which is already locked, it will wait for the resource to be free” [6]:
stage('Load Test') {
steps {
lock(resource: 'staging-loadenv') {
sh './run-webload.sh 2000 300'
}
}
}
The plugin also supports label-based pooling: tag several generators with the same label, and “if you try to lock [the label], one of the resources with the label will be locked when it is available” – so you can safely allow N parallel runs against a pool of N generators while blocking the N+1th until one frees up [6]. The Lockable Resources plugin reports installation on a large share of Jenkins controllers, making it a battle-tested choice. The lock() step and disableConcurrentBuilds() option are documented in the Pipeline syntax reference.
Test Data and Environment Management for Reliable Runs
Comparable results require comparable conditions. A run against 10,000 seeded rows and a run against 2 million aren’t measuring the same system. Keep this checklist:
- Seed deterministically – generate the same representative dataset each run, sized to a fixed target.
- Tear down idempotently – clean up in
post { cleanup }so a failed run doesn’t poison the next. - Bind secrets, never hardcode – use
withCredentials([...])for every database password and API token. - Match environment parity – staging data distribution should mirror production’s shape.
Seeding, Teardown, and Runtime Data Generation
Wrap data lifecycle in the pipeline so every run is self-contained:
stage('Seed') { steps { sh './seed.sh staging 500000' } }
// ... load test ...
post { cleanup { sh './teardown.sh staging' } }
The cleanup post-condition runs regardless of build result, so seeded data never leaks into the following run. Make both scripts idempotent – re-running seed shouldn’t duplicate rows, and teardown should succeed even if the data is already gone.
Secrets and Environment Parity
Store test credentials in the Jenkins credential store and bind at runtime, never in the Jenkinsfile:
withCredentials([usernamePassword(credentialsId: 'test-db',
usernameVariable: 'DB_USER', passwordVariable: 'DB_PASS')]) {
sh './seed.sh'
}
For parity, a workable rule: the staging dataset should be at least 20% of production row counts with the same distribution (same ratio of active to inactive users, same cardinality on indexed columns). Absolute size can be sca






