Running Selenium Tests in GitHub Actions

9 min read

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:

  1. 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.
  2. 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.
  3. 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-junit

Walking through it:

  • on: — triggers. Push to main, every PR, scheduled cron, and workflow_dispatch for 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 on ubuntu-latest runners — 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() vs if: 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=true

Two 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=true

The --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=true

Three 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 ActionsJenkins
SetupPush a YAML fileInstall server, configure agents
CostFree for public repos; metered for privateFree if you host; infrastructure cost
PR integrationNative, instantRequires plugins + webhooks
Plugin ecosystemMarketplaceVast — anything you need exists
RBACTied to GitHub permissionsGranular built-in
Self-hostingSelf-hosted runners possibleStandard mode
Best forOSS, modern projects, small/medium teamsEnterprises, 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: maven on setup-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: true on a cross-browser matrix. GitHub's default is true — if Chrome fails, Firefox is cancelled. You miss the Firefox-only bug you needed to find. Always set fail-fast: false when 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=secret123 in YAML is a public secret on a public repo. Use secrets.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.

  1. Push your Selenium project to a (public or private) GitHub repo.
  2. Add .github/workflows/selenium.yml from this lesson's first complete example. Commit and push.
  3. 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.
  4. 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
  5. Add a matrix. Convert the single-browser job to a matrix of [chrome, firefox] with fail-fast: false. Push. Confirm both jobs run in parallel and both appear as PR checks.
  6. 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.
  7. Stretch — Grid in a services block. Replace the local-Chrome run with a Selenium Grid services block 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.

// tip to track lessons you complete and pick up where you left off across devices.