Why GitHub Actions for QA
GitHub Actions is GitHub’s built-in CI/CD platform. If your project lives on GitHub, Actions eliminates the need for external CI/CD services. Workflows run directly within GitHub, with tight integration into pull requests, issues, and the GitHub ecosystem.
For QA engineers, this tight integration is a major advantage. Test results appear directly in pull requests. Failed checks block merges. Test artifacts are accessible from the same interface where you review code.
Workflow Basics
A GitHub Actions workflow is a YAML file in .github/workflows/. Here is a minimal workflow that runs tests on every push:
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
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
Triggers
GitHub Actions supports many trigger events:
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
workflow_dispatch: # Manual trigger from UI
For QA, the most important triggers are:
- pull_request: Run tests on every PR to catch issues before merge
- push to main: Run full regression after merge to verify the main branch
- schedule: Nightly full test suites, performance tests, or security scans
Jobs and Steps
A workflow contains one or more jobs. Each job runs on a fresh virtual machine (runner). Jobs run in parallel by default but can depend on each other:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:unit
e2e-tests:
needs: unit-tests # Runs after unit-tests completes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
Matrix Strategy for Cross-Browser Testing
Matrix strategies are one of GitHub Actions’ most powerful features for QA. They let you run the same test suite across multiple configurations automatically:
jobs:
e2e-tests:
strategy:
matrix:
browser: [chromium, firefox, webkit]
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps ${{ matrix.browser }}
- run: npx playwright test --project=${{ matrix.browser }}
This creates 6 parallel jobs (3 browsers x 2 operating systems). Setting fail-fast: false ensures all combinations run even if one fails — you want to know all failures, not just the first one.
Sharding Large Test Suites
For large test suites, you can shard tests across multiple runners:
jobs:
e2e-tests:
strategy:
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}/4
This splits the test suite into 4 equal parts running in parallel. A 40-minute suite becomes 10 minutes.
Artifacts and Test Reports
Uploading Test Artifacts
Save test reports, screenshots, and videos from test runs:
- name: Run E2E tests
run: npx playwright test
continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: |
playwright-report/
test-results/
retention-days: 14
The if: always() condition ensures artifacts are uploaded even when tests fail — this is when you need them most.
Publishing Test Results in PRs
Use third-party actions to post test results as PR comments:
- name: Publish Test Results
uses: dorny/test-reporter@v1
if: always()
with:
name: Playwright Tests
path: test-results/junit.xml
reporter: java-junit
This creates a check run with detailed test results visible directly in the pull request.
Caching for Faster Builds
Caching dependencies dramatically reduces pipeline time:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
# For Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install Playwright
run: npx playwright install --with-deps
Without caching, npm ci and browser installation might take 2-3 minutes. With caching, they take seconds.
Secrets and Environment Variables
jobs:
test:
runs-on: ubuntu-latest
env:
BASE_URL: https://staging.example.com
NODE_ENV: test
steps:
- run: npm run test:e2e
env:
API_KEY: ${{ secrets.STAGING_API_KEY }}
DB_URL: ${{ secrets.TEST_DATABASE_URL }}
Secrets are configured in repository settings (Settings > Secrets and variables > Actions). They are masked in logs and never exposed in workflow files.
Exercise: Build a Complete QA Workflow
Create a GitHub Actions workflow that:
- Runs unit tests on every push
- Runs E2E tests across 3 browsers on pull requests
- Uploads test reports as artifacts
- Posts test results as a PR check
- Runs a nightly full regression
Solution
name: QA Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
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 run test:unit -- --coverage --ci
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: coverage/
retention-days: 7
e2e-tests:
if: github.event_name == 'pull_request' || github.event_name == 'schedule'
needs: unit-tests
strategy:
matrix:
browser: [chromium, firefox, webkit]
fail-fast: false
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
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps ${{ matrix.browser }}
- run: npx playwright test --project=${{ matrix.browser }}
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.browser }}
path: |
playwright-report/
test-results/
retention-days: 14
- name: Test Report
uses: dorny/test-reporter@v1
if: always()
with:
name: E2E ${{ matrix.browser }}
path: test-results/junit.xml
reporter: java-junit
nightly-regression:
if: github.event_name == 'schedule'
needs: e2e-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 run test:regression
- run: npm run test:performance
Reusable Workflows
If multiple repositories need the same test pipeline, create reusable workflows:
# .github/workflows/reusable-e2e.yml
name: Reusable E2E Tests
on:
workflow_call:
inputs:
base-url:
required: true
type: string
browsers:
required: false
type: string
default: '["chromium"]'
secrets:
api-key:
required: true
jobs:
e2e:
strategy:
matrix:
browser: ${{ fromJson(inputs.browsers) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --project=${{ matrix.browser }}
env:
BASE_URL: ${{ inputs.base-url }}
API_KEY: ${{ secrets.api-key }}
Calling it from another workflow:
jobs:
test:
uses: your-org/.github/.github/workflows/reusable-e2e.yml@main
with:
base-url: https://staging.example.com
browsers: '["chromium", "firefox"]'
secrets:
api-key: ${{ secrets.API_KEY }}
GitHub Actions vs Jenkins: QA Perspective
| Aspect | GitHub Actions | Jenkins |
|---|---|---|
| Setup | Zero setup for GitHub repos | Requires server installation and maintenance |
| Cost | Free for public repos; paid minutes for private | Free but you pay for servers |
| Flexibility | Good with marketplace actions | Maximum with 1800+ plugins |
| Container support | Native Docker and service containers | Via Docker Pipeline plugin |
| PR integration | Native — checks, status, comments | Requires plugins and webhooks |
| Self-hosted runners | Supported | Core feature |
| Matrix builds | First-class with strategy.matrix | Requires scripted pipeline |
| Secrets management | Built-in repository/org secrets | Credentials plugin |
Best Practices
Use
actions/checkout@v4withfetch-depth: 0if your tests depend on git history (for change detection or blame).Set
fail-fast: falseon matrix strategies for test jobs. You want to see all failures, not just the first one.Cache aggressively — npm dependencies, Playwright browsers, and any other downloadable assets.
Use
continue-on-error: trueon test steps combined withif: always()on artifact upload. This ensures test reports are always available.Pin action versions to specific SHA hashes in production, not just major versions. This prevents supply chain attacks.
Use concurrency groups to cancel outdated workflow runs when a new commit is pushed:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true