Automating Apache JMeter Performance Testing with GitHub Actions — A Beginner’s Guide
Introduction
Performance testing is an important part of software development. Instead of manually running Apache JMeter every time you make changes, GitHub Actions can automatically execute your JMeter test plan whenever you push code to GitHub.
In this tutorial, you’ll learn how to:
- Create a GitHub repository
- Add a JMeter test plan
- Create a GitHub Actions workflow
- Commit and push the code
- Automatically execute the JMeter test
- Download the HTML performance report
No prior GitHub Actions experience is required.
Prerequisites
Before starting, make sure you have:
- Git installed
- A GitHub account
- Apache JMeter installed (used to create the test plan)
- Visual Studio Code (recommended)
- A JMeter test plan (
test-plan.jmx)
Project Structure
Create a project with the following structure:
JMeter-GitHub-Actions/
│
├── .github/
│ └── workflows/
│ └── performance-test.yml
│
├── test-plan.jmx
│
└── README.mdYour uploaded file is named test-plan.jmx, so place it in the project root as shown above.
Step 1: Create a GitHub Repository
- Log in to GitHub.
- Click New Repository.
- Enter a repository name.
- Choose Public or Private.
- Click Create Repository.
Example:
Repository Name:
jmeter-github-actionsStep 2: Clone the Repository
Copy the repository URL and run:
git clone https://github.com/your-username/jmeter-github-actions.gitcd jmeter-github-actionsStep 3: Copy the JMeter Test Plan
Copy your JMeter file into the repository:
test-plan.jmx<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan">
<stringProp name="TestPlan.comments">This test runs strictly for 2 minutes (120 seconds)</stringProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group - 5 Minutes">
<intProp name="ThreadGroup.num_threads">10</intProp>
<intProp name="ThreadGroup.ramp_time">0</intProp>
<longProp name="ThreadGroup.duration">60</longProp>
<longProp name="ThreadGroup.delay">0</longProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController">
<intProp name="LoopController.loops">-1</intProp>
<boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp>
</ThreadGroup>
<hashTree>
<CacheManager guiclass="CacheManagerGui" testclass="CacheManager" testname="HTTP Cache Manager" enabled="true">
<boolProp name="clearEachIteration">false</boolProp>
<boolProp name="useExpires">true</boolProp>
<boolProp name="CacheManager.controlledByThread">false</boolProp>
</CacheManager>
<hashTree/>
<CacheManager guiclass="CacheManagerGui" testclass="CacheManager" testname="HTTP Cache Manager" enabled="true">
<boolProp name="clearEachIteration">false</boolProp>
<boolProp name="useExpires">true</boolProp>
<boolProp name="CacheManager.controlledByThread">false</boolProp>
</CacheManager>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP GET Request" enabled="true">
<stringProp name="HTTPSampler.domain">test.k6.io</stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.path">/</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
<collectionProp name="Arguments.arguments"/>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">1</intProp>
</ResponseAssertion>
<hashTree/>
<DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="Duration Assertion" enabled="true">
<stringProp name="DurationAssertion.duration">10000</stringProp>
</DurationAssertion>
<hashTree/>
</hashTree>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="Aggregate Report" enabled="false">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>Your project should now look like:
jmeter-github-actions
│
├── test-plan.jmxStep 4: Create the GitHub Actions Workflow
Create the following folders:
.github/Inside it:
workflows/Inside workflows, create:
performance-test.ymlname: JMeter Performance Test
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: jmeter-performance
cancel-in-progress: true
env:
JMETER_VERSION: "5.6.3"
MAX_ERROR_RATE: "20"
MAX_AVG_RESPONSE: "100"
jobs:
performance-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
##############################################################
# Cache JMeter
##############################################################
- name: Cache JMeter
id: cache-jmeter
uses: actions/cache@v4
with:
path: apache-jmeter-${{ env.JMETER_VERSION }}
key: jmeter-${{ env.JMETER_VERSION }}
##############################################################
# Download only if cache misses
##############################################################
- name: Install JMeter
if: steps.cache-jmeter.outputs.cache-hit != 'true'
run: |
wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz
tar -xzf apache-jmeter-${JMETER_VERSION}.tgz
##############################################################
# Verify installation
##############################################################
- name: Verify JMeter
run: |
./apache-jmeter-${JMETER_VERSION}/bin/jmeter --version
##############################################################
# Execute Test Plan
##############################################################
- name: Run Performance Test
run: |
rm -rf report
./apache-jmeter-${JMETER_VERSION}/bin/jmeter \
-n \
-t test-plan.jmx \
-l results.jtl \
-e \
-o report
##############################################################
# Performance Quality Gate
##############################################################
- name: Validate Performance Thresholds
run: |
set -euo pipefail
TOTAL=$(awk 'NR>1{count++} END{print count+0}' results.jtl)
ERRORS=$(awk -F',' '
NR>1 && $8=="false" {count++}
END {print count+0}
' results.jtl)
AVG_TIME=$(awk -F',' '
NR>1 {
sum += $2
count++
}
END {
if(count>0)
print int(sum/count)
else
print 0
}
' results.jtl)
if [ "$TOTAL" -eq 0 ]; then
echo "No samples found."
exit 1
fi
ERROR_PERCENT=$((ERRORS * 100 / TOTAL))
echo "--------------------------------"
echo "Performance Summary"
echo "--------------------------------"
echo "Total Requests : $TOTAL"
echo "Failed Requests: $ERRORS"
echo "Error Rate : ${ERROR_PERCENT}%"
echo "Average Time : ${AVG_TIME} ms"
echo "--------------------------------"
{
echo "## Performance Test Summary"
echo ""
echo "| Metric | Value |"
echo "|--------|------:|"
echo "| Total Requests | $TOTAL |"
echo "| Failed Requests | $ERRORS |"
echo "| Error Rate | ${ERROR_PERCENT}% |"
echo "| Average Response Time | ${AVG_TIME} ms |"
} >> "$GITHUB_STEP_SUMMARY"
if [ "$ERROR_PERCENT" -gt "$MAX_ERROR_RATE" ]; then
echo "Performance Quality Gate Failed"
echo "Error Rate exceeded ${MAX_ERROR_RATE}%"
exit 1
fi
if [ "$AVG_TIME" -gt "$MAX_AVG_RESPONSE" ]; then
echo "Performance Quality Gate Failed"
echo "Average Response Time exceeded ${MAX_AVG_RESPONSE} ms"
exit 1
fi
echo "Performance Quality Gate Passed"
##############################################################
# Upload HTML Report
##############################################################
- name: Upload HTML Report
if: always()
uses: actions/upload-artifact@v4
with:
name: jmeter-html-report
path: report/
retention-days: 14
##############################################################
# Upload JTL File
##############################################################
- name: Upload Results
if: always()
uses: actions/upload-artifact@v4
with:
name: jmeter-results
path: results.jtl
retention-days: 14Final structure:
.github/
└── workflows/
└── performance-test.ymlPaste the GitHub Actions YAML workflow into performance-test.yml.
GitHub automatically detects any YAML files placed inside:
.github/workflows/Step 5: Verify Your Project Structure
jmeter-github-actions/.github/
└── workflows/
└── performance-test.ymltest-plan.jmxREADME.md
This is all GitHub needs to start running your workflow.
Step 6: Check Git Status
Open a terminal and run:
git statusExample output:
On branch mainUntracked files:.github/
test-plan.jmx
README.md
Step 7: Add the Files
git add .Step 8: Commit the Changes
git commit -m "Add JMeter performance test workflow"Example output:
3 files changedStep 9: Push to GitHub
git push origin mainOnce the push completes, GitHub detects the new workflow.
Step 10: Workflow Starts Automatically
Go to your repository.
Get Pavankumarsayannagari’s stories in your inbox
Join Medium for free to get updates from this writer.
Click:
ActionsYou’ll see something similar to:
JMeter Performance TestRunning...GitHub launches a fresh Ubuntu virtual machine, installs Java, downloads JMeter (or restores it from cache), runs test-plan.jmx, generates a performance report, evaluates the quality gates, and uploads the results as artifacts.
Step 11: Monitor the Workflow
Select the workflow run to see each step:
- Checkout Repository
- Setup Java
- Cache JMeter
- Install JMeter
- Verify JMeter
- Run Performance Test
- Validate Thresholds
- Upload HTML Report
- Upload Results
Green check marks indicate success.
Step 12: View the Performance Summary
When the workflow completes, GitHub displays a summary similar to:
Performance Test SummaryTotal Requests: 600
Failed Requests: 0
Error Rate: 0%
Average Response Time: 75 msIf your configured thresholds are exceeded, the workflow fails and clearly reports why.
Step 13: Download the HTML Report
At the bottom of the workflow page, open the Artifacts section.
You’ll find:
jmeter-html-reportjmeter-resultsDownload jmeter-html-report, extract it, and open:
index.htmlin your browser to explore detailed performance metrics, response times, throughput, and error analysis.
How the Workflow Works
Developer
│ ▼Git Commit │ ▼Git Push │ ▼GitHub Repository │ ▼GitHub Actions Trigger │ ▼Ubuntu Runner │ ▼Install Java │ ▼Install/Restore JMeter │ ▼Run test-plan.jmx │ ▼Generate HTML Report │ ▼Validate Performance Thresholds │ ▼Upload Artifacts
Advantages of GitHub Actions for Performance Testing
- Automatic execution on every push.
- No need to run JMeter manually.
- Performance regressions are detected early.
- HTML reports are archived with each run.
- Consistent execution environment using GitHub-hosted runners.
- Easy collaboration within development teams.
- Supports CI/CD pipelines.
Source Code
To make it easier to follow along, I’ve created a complete demo project that includes the Apache JMeter test plan and the GitHub Actions workflow used in this tutorial.
You can clone the repository and try it yourself:
GitHub Repository:
https://github.com/pavanKumar043/performance-testing-demo
Clone the project using:
git clone https://github.com/pavanKumar043/performance-testing-demo.gitcd performance-testing-demoAfter cloning, explore the project files, make any small change, commit your changes, and push them to GitHub. The GitHub Actions workflow will trigger automatically, execute the JMeter performance test, generate the HTML report, validate the configured performance thresholds, and upload the test results as workflow artifacts.
Conclusion
By combining Apache JMeter with GitHub Actions, you can build a simple yet powerful continuous performance testing pipeline. Every code push automatically runs your JMeter test plan, validates key performance thresholds, and generates downloadable reports. This helps teams identify performance issues early and maintain application quality without manual effort.









