How to Run Selenium Tests With GitHub Actions
Set up a GitHub Actions workflow for Selenium, choose a runner and browser, and keep the reports and screenshots you need to diagnose failures.
Run Selenium from a GitHub Actions workflow by adding a YAML file under .github/workflows, selecting an operating system runner, installing your project’s pinned dependencies, and invoking the test command your repository already uses. Keep reports, logs, and failure screenshots as workflow artifacts so you can inspect them after a run.
The exact setup depends on the project’s language, test framework, and target browser. The Python example below is a complete small starting point; the workflow is illustrative and should be adapted to your repository. GitHub describes workflows as event-triggered YAML files made up of jobs and steps. GitHub’s workflow overview and workflow syntax reference document the available structure and settings.
1. Choose the trigger, runner, browser, and test command
Before writing YAML, make four decisions:
- When should tests run? Use pull requests for proposed changes, pushes for branch integration, and optionally manual or scheduled runs.
- Which runner and browser should represent your users? GitHub-hosted runners provide Linux, Windows, and macOS environments. Check the current runner image documentation for the browser and system packages you need; do not assume every browser is preinstalled on every image.
- How does this repository install and run tests? Reuse its normal dependency lockfile and test command. There is no universal Selenium command across languages and frameworks.
- What should remain available after a failure? Save test reports, logs, and screenshots as artifacts.
Each GitHub Actions job runs in its own virtual machine or container. Selenium WebDriver controls a browser through the WebDriver interface; choose an OS/browser pair that matches the coverage you need. See GitHub-hosted runners and Selenium’s supported browser documentation.
2. Add a minimal Python Selenium test
If your project already has Selenium tests, keep its existing test files and command. If you need a small working example, create requirements.txt:
selenium==4.27.1
Pin the Selenium version to the version your project has adopted. Create tests/test_homepage.py using Python’s built-in unittest runner:
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class HomepageTest(unittest.TestCase):
def setUp(self):
options = webdriver.ChromeOptions()
options.add_argument("--headless")
self.driver = webdriver.Chrome(options=options)
def test_homepage_has_title(self):
self.driver.get("https://www.selenium.dev/")
self.assertIn("Selenium", self.driver.title)
self.assertTrue(self.driver.find_elements(By.TAG_NAME, "body"))
def tearDown(self):
if hasattr(self, "driver"):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
For a real project, replace the example URL and assertions with checks against your application. If setup can fail after opening the browser, use a fixture or cleanup pattern that still calls quit(). Selenium’s current Python bindings can use Selenium Manager to manage browser drivers when they are not already available. See Selenium Manager documentation.
3. Create the GitHub Actions workflow
Save this example as .github/workflows/selenium.yml. It installs Python dependencies, runs unittest discovery, and uploads the report directory and failure screenshots even if the test command fails.
name: Selenium tests
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
jobs:
selenium:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: python -m pip install -r requirements.txt
- name: Run Selenium tests
run: python -m unittest discover -s tests -v
- name: Upload test evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: selenium-test-evidence
path: |
test-results/
screenshots/
if-no-files-found: ignore
retention-days: 7
This illustrates the workflow shape, not a verified, universal copy-and-paste recipe. Check current action versions, the Python version supported by the repository, and the runner image contents before adopting it. Replace the install and run steps to match the project. If your framework writes reports elsewhere, change the artifact paths to those actual files.
The example uses always() so artifact upload is attempted after either a passing or failing test step. Artifacts are files produced during a workflow run; GitHub lists test results and screenshots among common uses. Retention can be configured, subject to repository or organization limits. See GitHub’s artifact documentation.
4. Capture a screenshot when a test fails
Write screenshots to the directory uploaded by the workflow. With unittest, a simple helper can save the current browser state before cleanup:
import os
import unittest
from selenium import webdriver
class BrowserTest(unittest.TestCase):
def setUp(self):
os.makedirs("screenshots", exist_ok=True)
self.driver = webdriver.Chrome()
def tearDown(self):
if self._outcome.result.errors or self._outcome.result.failures:
name = self.id().replace(".", "_")
self.driver.save_screenshot(f"screenshots/{name}.png")
self.driver.quit()
Framework teardown APIs differ, and a browser startup error may leave no session from which to take a screenshot. Adapt the failure hook to your test framework and guard cleanup if driver creation can fail. Keep browser console output, server logs, and framework reports when those help explain failures. Avoid placing credentials, session cookies, or other secrets in uploaded artifacts.
5. Adapt the workflow to your project
Language and dependency setup
Keep the workflow’s install step aligned with the repository’s lockfile and standard local command. For example, Java projects typically set up the required JDK and invoke the project’s Maven or Gradle wrapper; JavaScript projects set up Node and use the package manager’s frozen-lockfile command. Do not replace a repository’s established test command with an invented generic one.
Runner operating system and browser
Use runs-on to choose a GitHub-hosted runner label, or configure a self-hosted runner when your project needs a controlled environment. GitHub-hosted Linux, Windows, and macOS runners are available, but browser availability and image details can change. Consult the runner-images project for the actual image definition. Selenium supports several browser families, but that does not mean each is present on each runner.
For a cross-browser matrix, create separate jobs or a matrix with an explicit browser dimension. Confirm each selected runner image has the browser and required libraries, and install or provision them as appropriate. A matrix increases the number of executions and can make failures harder to compare if browser versions differ.
Headless mode and display
Headless browser mode is commonly useful on CI runners without a desktop session. Keep the same viewport and relevant browser options between local and CI runs where possible. If a test depends on rendering behavior that differs in headless mode, run it in an environment with a display or use a browser configuration that matches the intended coverage.
Runner host or job container
By default, steps run on the selected runner host unless an action runs inside a container. GitHub also supports a job-level container. A container can standardize language dependencies, but it must include or obtain a compatible browser, driver, and system libraries. The container choice does not remove browser provisioning work. See GitHub’s job container documentation.
Triggers and schedules
Use pull_request for review feedback and push for integration on the branches that matter. workflow_dispatch supports manual runs. A schedule can provide periodic coverage, but should complement change-triggered tests rather than replace them. Review GitHub’s event documentation and schedule event details, including schedule lifecycle behavior.
Dependencies, caching, and outputs
Pin dependencies and use the cache associated with your package manager when it is supported by the setup action. Cache reusable dependencies, not test evidence: reports and screenshots should be uploaded as artifacts. Give artifacts useful names, set retention to suit debugging needs and storage constraints, and upload only files needed to investigate the run.
6. Understand reliability, runtime, and cost
- Make failures diagnosable. Preserve reports, screenshots, and relevant logs with artifact upload conditions that run after failures.
- Limit hangs. Set a job timeout appropriate to suite duration, and configure Selenium page-load and explicit waits around real application conditions instead of relying on arbitrary long sleeps.
- Reduce avoidable variability. Pin language dependencies, avoid relying on undocumented runner contents, and explicitly provision browser versions when reproducibility requires it.
- Manage parallelism deliberately. Browser matrices and concurrent test workers can shorten wall-clock time but consume more runner minutes and may stress shared test data or environments. Isolate test data and account for application rate limits.
- Consider network and browser setup. Selenium Manager may need to discover or download browsers or drivers. Restricted egress, proxies, or changing browser versions can affect startup; preprovision compatible components when needed.
- Review Actions usage. Workflow cost depends on the repository’s GitHub plan, runner type, and minutes consumed. Check current billing documentation for applicable rates and included usage rather than assuming a fixed cost.
GitHub-hosted runner jobs receive fresh environments, while caches can reuse dependencies across runs. Neither a cache nor a passing prior run guarantees the next browser test behaves identically; the app, browser, network, and runner image can all affect outcomes.
7. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser or driver cannot be found | The browser is absent, Selenium is outdated, Selenium Manager cannot download a driver, or network access is restricted. | Check the runner image and Selenium version. Confirm network access to required vendor endpoints or explicitly provision a compatible browser and driver. |
| Browser and driver version mismatch | A manually installed driver does not match the browser’s major version. | Provision matching versions or let a current Selenium binding’s Selenium Manager resolve the driver where the environment permits. |
| Browser exits immediately in CI | Browser options, sandbox permissions, missing libraries, or the execution environment differ from local. | Inspect browser and driver logs, verify required system libraries, and compare the runner/container environment. Avoid copying browser flags without understanding the environment. |
| Tests pass locally but fail on Actions | Timing assumptions, different browser/OS versions, missing environment variables, or test data dependencies. | Use condition-based waits, define required configuration in the workflow, record browser details, and isolate test data. |
| Element not found or stale element | The page has not reached the expected state, or the DOM changed after lookup. | Wait for the relevant element/state and locate it again after navigation or rerender. Prefer stable selectors. |
| Page navigation times out | The app is slow, network access is blocked, or the chosen page-load strategy waits for more than the test needs. | Check app availability from the runner, set appropriate timeouts, and wait for the specific state under test rather than assuming every resource must finish. |
| Artifact upload says no files found | The test command writes outputs to a different directory, or no failure hook created a screenshot. | Verify paths and working directory, create the output directory, and set the artifact action to the actual report and screenshot locations. |
| Workflow does not start | The file is outside .github/workflows, YAML is invalid, or the event/branch filter does not match. |
Check the path, indentation, workflow syntax, and trigger filters in the Actions tab. |
| Scheduled workflow did not run when expected | Schedules follow GitHub’s schedule semantics and may be affected by repository or workflow lifecycle conditions. | Review the schedule event documentation, confirm the workflow is active, and use push or pull-request triggers for change feedback. |
8. Or skip the browser setup
If the task is to capture a page image for a report, preview, or documentation workflow rather than interactively test application behavior, ScreenshotNeo can return a screenshot or PDF from one GET request. Selenium remains the right fit when tests must click, type, or assert application behavior in a browser.
See the ScreenshotNeo API documentation for request options. This cURL example saves a WebP screenshot:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and responses report page verdict and billing headers. An MCP server provides screenshot tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Do I need to install ChromeDriver separately?
Often not with a modern Selenium binding and a network-accessible environment: Selenium Manager can manage drivers when they are unavailable. Explicit provisioning still makes sense when versions must be fixed or network access is restricted.
Should every Selenium test run on every pull request?
Choose coverage based on feedback time and runner usage. A smaller pull-request suite plus broader scheduled or branch coverage can be appropriate, provided important change-triggered checks remain in place.
Can GitHub Actions test a site behind a VPN or firewall?
Only if the selected runner can reach it. A self-hosted runner inside the required network may be necessary; protect credentials and restrict access to the runner and workflow.
Where do I find a failed run’s screenshots?
Open the completed workflow run and download its artifact, provided the workflow wrote screenshots to the configured path and the upload step ran.


