A test suite that only runs on a developer's laptop is not a CI test suite — it's a local script. The value of automation is continuous: tests run on every push, every pull request, every night without anyone pressing a button. This lesson wires your TestNG suite into both GitHub Actions and Jenkins, covers the configuration decisions that matter (headless mode, thread counts, which suite to run when), and shows the pattern for passing environment secrets into your tests without committing credentials.
GitHub Actions
Create .github/workflows/testng.yml in your project root:
name: TestNG Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *' # nightly regression at 02:00 UTC
jobs:
smoke:
name: Smoke Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven' # cache ~/.m2 between runs
- name: Run smoke suite
run: mvn clean test -DsuiteXmlFile=smoke.xml -Dbrowser=chrome -Dheadless=true
env:
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
ADMIN_PASS: ${{ secrets.ADMIN_PASSWORD }}
- name: Upload test reports
if: always() # upload even when tests fail
uses: actions/upload-artifact@v4
with:
name: smoke-reports-${{ github.run_number }}
path: |
test-output/
reports/
target/surefire-reports/
retention-days: 14
regression:
name: Nightly Regression
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Run regression suite
run: mvn clean test -DsuiteXmlFile=regression.xml -Dbrowser=chrome -Dheadless=true
env:
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
ADMIN_PASS: ${{ secrets.ADMIN_PASSWORD }}
- name: Upload regression reports
if: always()
uses: actions/upload-artifact@v4
with:
name: regression-reports-${{ github.run_number }}
path: |
test-output/
reports/
retention-days: 30Key decisions in this workflow:
if: always() on the upload step. Without it, the upload is skipped when Maven exits with a non-zero code (i.e., when tests fail). You need the report most when tests fail — if: always() ensures it is always uploaded regardless of test outcome.
cache: 'maven' caches ~/.m2/repository between runs. The first run downloads everything; subsequent runs restore the cache and skip network I/O. On a large suite with many dependencies this saves 1–3 minutes per run.
Dheadless=true passed as a system property. Your BaseTest reads this:
@BeforeMethod
@Parameters("browser")
public void setup(@Optional("chrome") String browser) {
boolean headless = Boolean.parseBoolean(
System.getProperty("headless", "false"));
ChromeOptions opts = new ChromeOptions();
if (headless) {
opts.addArguments("--headless=new", "--no-sandbox",
"--disable-dev-shm-usage");
}
DriverManager.initDriver(browser, opts);
}--no-sandbox and --disable-dev-shm-usage are required on GitHub Actions' Ubuntu containers — Chrome crashes without them because the container has limited /dev/shm memory.
Secrets go in GitHub → repository → Settings → Secrets and variables → Actions. Reference them as ${{ secrets.SECRET_NAME }}. They are never logged, never visible in PR forks from external contributors.
Jenkins pipeline
A Jenkinsfile at the project root:
pipeline {
agent any
tools {
maven 'Maven-3.9'
jdk 'JDK-21'
}
environment {
BASE_URL = credentials('staging-base-url')
ADMIN_PASS = credentials('admin-password')
}
triggers {
// Nightly regression at 01:00
cron('0 1 * * *')
}
stages {
stage('Smoke') {
steps {
sh 'mvn clean test -DsuiteXmlFile=smoke.xml -Dbrowser=chrome -Dheadless=true'
}
}
stage('Regression') {
when { triggeredBy 'TimerTrigger' }
steps {
sh 'mvn test -DsuiteXmlFile=regression.xml -Dbrowser=chrome -Dheadless=true'
}
}
}
post {
always {
// TestNG plugin — produces trend graphs in Jenkins UI
testNG reportFilenamePattern: '**/testng-results.xml'
// Archive Extent or Allure report as a linked artefact
publishHTML(target: [
reportDir: 'reports',
reportFiles: 'extent-report.html',
reportName: 'Extent Report',
keepAll: true,
allowMissing: true
])
// Archive raw test-output for download
archiveArtifacts artifacts: 'test-output/**,reports/**',
allowEmptyArchive: true
}
failure {
mail to: 'qa-team@mycompany.com',
subject: "Build ${BUILD_NUMBER} failed — ${JOB_NAME}",
body: "See: ${BUILD_URL}"
}
}
}Install the TestNG Plugin in Jenkins to get the testNG post-build step. It parses testng-results.xml and renders a per-run table and trend graph in the Jenkins job UI. The HTML Publisher Plugin provides publishHTML for linking ExtentReports.
Credentials in Jenkins come from Manage Jenkins → Credentials, referenced with credentials('credential-id'). They are masked in build logs automatically.
Thread counts in CI
Local development can use thread-count="4". CI runners have different constraints:
- GitHub Actions (ubuntu-latest): 2 vCPUs — use
thread-count="2"for Selenium,thread-count="3"for pure API tests - Jenkins (shared agent, 4 cores):
thread-count="2"to leave headroom for the Maven process and Chrome itself - Jenkins (dedicated agent, 8 cores):
thread-count="4"is safe;thread-count="6"if tests are short-lived
Set thread-count as a property so you can override from CI without editing XML:
<suite name="Regression" parallel="classes"
thread-count="${threadCount:-2}">mvn test -DthreadCount=4The CI pipeline flow
Scheduling nightly regression
GitHub Actions cron:
on:
schedule:
- cron: '0 2 * * *' # 02:00 UTC every nightJenkins cron (Triggersblock in Jenkinsfile or UI):
H 1 * * * # 01:00, H = hash-based minute to spread load
Nightly regression runs the full suite, including slow groups excluded from the PR smoke run. A typical pattern:
- On every PR:
smoke.xml— 5–15 tests, under 5 minutes, gates merge - On merge to main:
regression.xml— full coverage, 10–30 minutes - Nightly:
regression.xml+ extended suites — slow tests, cross-browser, performance gates
⚠️ Common mistakes
- Missing
--no-sandboxon GitHub Actions. Chrome on a GitHub-hosted Ubuntu runner crashes silently without this flag. The symptom is all Selenium tests failing withSessionNotCreatedExceptionor a zero-byte screenshot. Add--no-sandbox --disable-dev-shm-usagetoChromeOptionswhenheadless=trueis set. - Uploading artefacts only on success. CI reports are most valuable when tests fail.
if: always()in GitHub Actions (orpost { always {} }in Jenkins) ensures the report is available for every run. Without it you have no evidence to investigate when the nightly run fails. - Committing secrets in
testng.xml. A<parameter name="adminPassword" value="real-password"/>intestng.xmlis committed to the repository. UseSystem.getProperty("adminPass")instead and inject the value via-DadminPass=${{ secrets.ADMIN_PASS }}— the value never touches source control.
🎯 Practice task
Get your suite running in CI. 30–45 minutes.
- Create
.github/workflows/testng.ymlfrom the template. Push to a GitHub repository. Watch the Actions tab — confirm the workflow triggers, Java is installed, andmvn testruns (even if tests fail due to missing credentials). - Add
if: always()to the upload step and confirm the report artefact appears in the run summary after both a passing and a failing run. - Wire secrets. Go to repository Settings → Secrets → Actions. Add
STAGING_BASE_URLwith your test site's URL. Update the workflow to pass-DbaseUrl=${{ secrets.STAGING_BASE_URL }}. Confirm the workflow reads it (echo a masked version in the logs). - Add headless mode. Update
BaseTest.setup()to readSystem.getProperty("headless")and add the Chrome flags when true. Pass-Dheadless=truein the workflow. Confirm tests run (and pass) in the headless container. - Set up the nightly cron. Add
schedule: - cron: '0 2 * * *'and a separateregressionjob that only triggers onschedule. Manually trigger it via "Run workflow" in the Actions UI to test it without waiting until 02:00. - Stretch — Jenkins. If you have a local Jenkins instance, add the
Jenkinsfilefrom this lesson. Install the TestNG and HTML Publisher plugins. Run the pipeline. Confirm the TestNG trend graph appears after two runs.
Next chapter: the capstone project. Everything from this course lands in one framework.