GitHub Actions is what most modern projects reach for first. There's no server to install, no plugins to manage, no agents to provision — push a YAML file under .github/workflows/, and GitHub runs your tests on every push and PR. The free tier covers public repos generously and is plenty for small private repos. This lesson takes the same Selenium suite from the previous lesson and runs it on GitHub Actions: matrix builds across browsers, artefact uploads, PR-comment reporters, secret management, and a Selenium Grid in a Docker services block. By the end you'll have a .github/workflows/selenium.yml ready to drop into any Java/Selenium repo.
Why GitHub Actions
Three honest advantages over Jenkins:
- Zero infrastructure. No server. GitHub provisions a fresh Ubuntu (or Windows or macOS) runner for every job. When the job ends, the runner is destroyed. Reproducibility comes for free.
- Native PR integration. Test results appear inline on the pull request. Status checks can block merges. Comment-on-PR with the failure summary is one action away.
- Free for public repos. Open-source Selenium projects pay nothing. Private repos get a generous monthly minute allowance on the free tier.
The trade-off: less customisation than Jenkins, no built-in plugin ecosystem, fewer agent-management knobs. For most teams in 2026, the trade-off favours Actions.
A complete workflow file
.github/workflows/selenium.yml:
name: Selenium Tests
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * *" # nightly at 06:00 UTC
workflow_dispatch: # manual trigger via UI
inputs:
browser:
description: "Browser"
type: choice
options: [chrome, firefox, edge]
default: chrome
suite:
description: "Suite file"
type: choice
options: [smoke.xml, regression.xml]
default: smoke.xml
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: "21"
distribution: "temurin"
cache: maven
- name: Run tests
run: |
mvn -B clean test \
-DsuiteFile=${{ inputs.suite || 'smoke.xml' }} \
-Dbrowser=${{ inputs.browser || 'chrome' }} \
-Dheadless=true
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Upload Surefire results
if: always()
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: target/surefire-reports/
retention-days: 7
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: failure-screenshots
path: target/screenshots/
retention-days: 30
- name: Publish TestNG report on PR
if: always()
uses: dorny/test-reporter@v1
with:
name: TestNG Results
path: "target/surefire-reports/testng-results.xml"
reporter: java-junitWalking through it:
on:— triggers. Push tomain, every PR, scheduled cron, andworkflow_dispatchfor manual UI-triggered runs with input choices.runs-on: ubuntu-latest— GitHub provides a fresh Ubuntu VM per job. Chrome, Firefox, and Edge are pre-installed onubuntu-latestrunners — no extra installation step needed.actions/setup-java@v4— installs Java 21 (Temurin distribution) and caches the Maven local repo across runs. The cache is the single biggest speed-up; without it every build re-downloads Selenium dependencies.mvn -B clean test ...— same Maven invocation as Jenkins, with the suite/browser/headless system properties passed through.if: always()vsif: failure()— control when each step runs. Always upload Surefire reports (so you can read them on green builds too); only upload screenshots on failure (no clutter on success).dorny/test-reporter@v1— third-party action that reads TestNG/JUnit XML and renders inline on the PR. Failed tests appear as PR-level annotations.
Matrix builds — cross-browser parallel
Run the same job against multiple browsers in parallel:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false # don't cancel other browsers if one fails
matrix:
browser: [chrome, firefox]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: "21", distribution: "temurin", cache: maven }
- run: mvn -B test -Dbrowser=${{ matrix.browser }} -Dheadless=trueTwo parallel jobs — test (chrome) and test (firefox) — show up on the PR check list. fail-fast: false is critical: without it, if Chrome fails first, GitHub cancels Firefox before it finishes, hiding any Firefox-specific bugs you need to know about.
For a fuller matrix:
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
java: ["17", "21"]
os: [ubuntu-latest, windows-latest]Now you have 2 × 2 × 2 = 8 parallel jobs. Use sparingly — minutes add up.
The Actions flow
Step 1 of 5
Push / PR
GitHub fires the workflow on push to main or any PR. Runner is provisioned fresh — no leftover state from previous builds.
Selenium Grid in a services block
For tests that depend on a Selenium Grid, GitHub Actions runs Docker containers alongside your job. They're addressable by their service name on the same Docker network:
jobs:
test:
runs-on: ubuntu-latest
services:
selenium:
image: selenium/standalone-chrome:4.21.0
ports: ["4444:4444"]
options: >-
--shm-size=2gb
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: "21", distribution: "temurin", cache: maven }
- name: Wait for Grid to be ready
run: |
for i in {1..30}; do
curl -fs http://localhost:4444/wd/hub/status && break || sleep 1
done
- run: mvn -B test -Dgrid.url=http://localhost:4444 -Dheadless=trueThe --shm-size=2gb option is mandatory for Chrome containers (chapter 7's gotcha). The wait loop guards against starting tests before the Grid is ready — services come up asynchronously.
Secrets management
Anything that's a credential lives in repo Settings → Secrets and variables → Actions. In the workflow:
env:
BASE_URL: ${{ secrets.STAGING_URL }}
BS_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BS_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}Secrets are masked in logs (any value matching the secret is replaced with *** automatically). Don't echo them, don't print them to artefacts — even masked, you don't want them anywhere visible.
PR-blocking status checks
The workflow's status (success/failure) appears as a check on each PR. Settings → Branches → Branch protection rules → Require status checks before merging → pick "Selenium Tests / test" — the PR can no longer be merged while tests are red. This is the simplest, highest-leverage QA gate any project can put in place. Configure it once, every future PR is gated automatically.
A complete real workflow — three parallel tracks
A more realistic file that runs smoke on every push, full regression on PRs, and cross-browser nightly:
name: Selenium
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * *"
jobs:
smoke:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: "21", distribution: "temurin", cache: maven }
- run: mvn -B test -DsuiteFile=smoke.xml -Dheadless=true
regression:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: "21", distribution: "temurin", cache: maven }
- run: mvn -B test -DsuiteFile=regression.xml -Dheadless=true
cross-browser:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: "21", distribution: "temurin", cache: maven }
- run: mvn -B test -DsuiteFile=cross-browser.xml -Dbrowser=${{ matrix.browser }} -Dheadless=trueThree jobs, each gated by event type. PRs get the slow regression suite; commits to main get fast smoke; nightly cron gets cross-browser. The same Maven invocations you used in the previous lesson, wrapped in three different conditions.
Comparison with Jenkins
| GitHub Actions | Jenkins | |
|---|---|---|
| Setup | Push a YAML file | Install server, configure agents |
| Cost | Free for public repos; metered for private | Free if you host; infrastructure cost |
| PR integration | Native, instant | Requires plugins + webhooks |
| Plugin ecosystem | Marketplace | Vast — anything you need exists |
| RBAC | Tied to GitHub permissions | Granular built-in |
| Self-hosting | Self-hosted runners possible | Standard mode |
| Best for | OSS, modern projects, small/medium teams | Enterprises, on-prem requirements |
Many teams use both — Actions for public-facing repos, Jenkins for internal/legacy systems.
The Selenium tool entry covers the test side; the CI/CD for Testers cheat sheet lists the workflow patterns and Maven flags this lesson uses.
⚠️ Common mistakes
- Forgetting
cache: mavenonsetup-java. Without it, every build re-downloads Selenium, TestNG, Jackson, POI, etc. — adding 30–60 seconds per run. With it, the second build onwards skips the download entirely. It's a one-line change worth thousands of CI minutes annually. fail-fast: trueon a cross-browser matrix. GitHub's default istrue— if Chrome fails, Firefox is cancelled. You miss the Firefox-only bug you needed to find. Always setfail-fast: falsewhen running matrix jobs that should be independent.- Hardcoding URLs and credentials in the workflow file.
mvn test -Dbase.url=https://staging.example.com -Duser=admin -Dpass=secret123in YAML is a public secret on a public repo. Usesecrets.STAGING_URL,secrets.STAGING_USER,secrets.STAGING_PASS— even on private repos, treat secrets like secrets.
🎯 Practice task
Stand up a real GitHub Actions pipeline. 40–55 minutes.
- Push your Selenium project to a (public or private) GitHub repo.
- Add
.github/workflows/selenium.ymlfrom this lesson's first complete example. Commit and push. - Open the repo's Actions tab. The workflow runs on the push you just made. Watch the steps in real time. The first run is slow (download + install); the second uses the Maven cache and is much faster.
- Open a PR with a deliberate test failure. Confirm:
- The workflow runs automatically on the PR
- The test-reporter action posts annotations on the PR
- Screenshots from the failure are downloadable from the workflow run page
- Add a matrix. Convert the single-browser job to a matrix of
[chrome, firefox]withfail-fast: false. Push. Confirm both jobs run in parallel and both appear as PR checks. - Make it a required check. In repo Settings → Branches → Branch protection, add "Selenium / test" as a required status check on PRs targeting
main. Try merging the PR with a failing test — GitHub blocks the merge until the check is green. - Stretch — Grid in a
servicesblock. Replace the local-Chrome run with a Selenium Gridservicesblock from this lesson. Wait-loop included. Run a test against the in-CI Grid. The whole Grid lives only for the duration of the workflow — no infrastructure to maintain.
Next lesson: the parallel-execution and ThreadLocal machinery that makes any of these CI configurations actually fast. The same suite that takes 30 minutes serial finishes in 5 with the right plumbing — and the wrong plumbing produces flake fast.