GitHub Actions for Test Automation
Learn how to build fast, reliable CI pipelines with GitHub Actions. Master workflow syntax, dependency caching, matrix strategies, secrets management, and Playwright integration for scalable test.
This post is part 2 of the CI/CD Quality series. Part 1 covered quality gates — what must block a merge vs. what should inform.
Introduction — Workflows Are Code
GitHub Actions is GitHub’s native CI/CD platform. It runs workflows defined as YAML files in .github/workflows/ on every push, pull request, or scheduled trigger.
The advantage of GitHub Actions over external CI services:
- Integrated — native GitHub UI, no third-party authentication
- Fast — minimal latency between code push and pipeline start
- Generous free tier — 2,000 minutes/month for private repos, unlimited for public repos
- Marketplace — thousands of reusable actions
This post covers the core concepts for building test automation pipelines with GitHub Actions: workflow syntax, caching, parallelisation, secrets, and integrating Playwright.
Workflow Anatomy
A workflow is a YAML file that defines:
- When it runs (triggers)
- Where it runs (runner OS)
- What it runs (jobs and steps)
Basic Workflow Structure
# .github/workflows/ci.yml
name: CI
# When to run
on:
pull_request:
push:
branches: [main]
# What to run
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Breakdown:
name— workflow name shown in GitHub UIon— trigger events (pull request, push to main)jobs— independent units of workruns-on— runner OS (ubuntu-latest, windows-latest, macos-latest)steps— sequential commands within a jobuses— reusable actions from marketplacerun— shell commands
:::tip[Workflow Triggers]
Use pull_request for validation before merge. Use push: branches: [main] for post-merge checks. Use schedule: cron: '0 6 * * *' for daily smoke tests against production.
:::
Caching Dependencies
Installing dependencies on every run is slow. npm ci takes 30–60 seconds. Do this 20 times per day, and you’ve burned 10–20 minutes.
GitHub Actions supports caching. Cache the node_modules directory and restore it on subsequent runs.
Caching with actions/cache
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # Automatically cache npm dependencies
- run: npm ci
- run: npm test
The cache: 'npm' parameter in actions/setup-node automatically caches ~/.npm based on package-lock.json hash.
Result: First run installs dependencies in 45 seconds. Subsequent runs restore cache in 3 seconds.
Caching Playwright Browsers
Playwright downloads browser binaries on first install. These are 300+ MB and take 20–30 seconds to download.
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
Cache key is based on OS and package-lock.json. If Playwright version changes in package.json, cache invalidates and browsers re-download.
:::info[Cache Limits] GitHub Actions cache is limited to 10 GB per repository. Old caches are automatically evicted when the limit is reached. Prioritise caching dependencies over output artifacts. :::
Matrix Strategies — Testing Across Versions
Matrix strategies run the same job multiple times with different configurations. Test across multiple Node versions, operating systems, or browsers.
Testing on Multiple Node Versions
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
This spawns three parallel jobs: one for Node 18, one for Node 20, one for Node 22.
Testing on Multiple Browsers
jobs:
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps ${{ matrix.browser }}
- run: npm run test:e2e -- --project=${{ matrix.browser }}
This runs E2E tests in parallel across Chromium, Firefox, and WebKit.
Cost: Matrix jobs consume parallel runner minutes. Free tier includes 20 parallel jobs for public repos, 5 for private repos.
:::warning[Matrix Explosion] A matrix with 3 OS × 3 Node versions × 3 browsers = 27 parallel jobs. Ensure your use case justifies the cost. For most projects, test on ubuntu-latest with Node 20 and Chromium. Add matrix coverage for LTS libraries or cross-platform desktop apps. :::
Managing Secrets
Test automation often requires credentials: API keys, database URLs, authentication tokens.
Never hardcode secrets in code or commit them to Git.
GitHub provides repository secrets and environment secrets for secure credential storage.
Adding a Secret
- Navigate to Settings → Secrets and variables → Actions
- Click New repository secret
- Add name (e.g.
STRIPE_TEST_KEY) and value - Save
Using Secrets in Workflows
jobs:
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Run API tests
env:
STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npm run test:api
Secrets are injected as environment variables at runtime. They are masked in logs — GitHub automatically redacts secret values from output.
:::tip[Environment-Specific Secrets]
Use environments to separate staging and production secrets. Define staging and production environments in Settings → Environments, then reference them in workflows with environment: staging.
:::
Playwright-Specific Actions
Playwright provides an official GitHub Action: microsoft/playwright-github-action.
Using the Official Playwright Action
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Uploading HTML Reports and Traces
Playwright generates HTML reports and trace files on test failure. Upload them as artifacts for debugging.
- name: Run Playwright tests
run: npm run test:e2e
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
- name: Upload traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: test-results/
if: always()— upload report even if tests failif: failure()— upload traces only on failure (saves storage)
Accessing artifacts: Navigate to the workflow run in GitHub UI → Artifacts section → download zip.
:::info[Trace Viewer]
Download trace files and open them locally with npx playwright show-trace trace.zip. Trace viewer shows full timeline, network activity, console logs, DOM snapshots, and screenshots for every action.
:::
Practical Example — Full CI Workflow
Here’s a production-ready workflow combining build, lint, unit tests, E2E smoke tests, and artifact uploads.
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build
path: dist/
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
smoke-tests:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build
path: dist/
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run smoke tests
run: npm run test:e2e:smoke
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-test-report
path: playwright-report/
Key features:
needs: build— smoke tests wait for build job to complete- Artifacts passed between jobs using
upload-artifactanddownload-artifact - Playwright installs only Chromium for faster smoke tests
- Report uploaded on success or failure (
if: always())
Conclusion — Automation as a First-Class Citizen
GitHub Actions makes test automation a native part of the development workflow. Workflows are versioned with your code, reviewed in pull requests, and executed on GitHub’s infrastructure.
The principles:
- Cache dependencies to minimise install time
- Use matrix strategies judiciously — test what matters, not every combination
- Store secrets securely and never commit them
- Upload artifacts for debugging failed runs
- Integrate Playwright with official actions and trace uploads
A well-configured GitHub Actions pipeline runs in under 5 minutes for smoke tests, provides instant feedback on pull requests, and costs nothing for public repositories.
Action for this week: Add dependency caching to your CI workflow. Measure the time saved on a typical pull request run. If you don’t have CI yet, start with the basic workflow example from this post and add one test command.
Next in this series: Part 3 — E2E in Docker and Ephemeral Environments