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:

  1. Runs unit tests on every push
  2. Runs E2E tests across 3 browsers on pull requests
  3. Uploads test reports as artifacts
  4. Posts test results as a PR check
  5. 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

AspectGitHub ActionsJenkins
SetupZero setup for GitHub reposRequires server installation and maintenance
CostFree for public repos; paid minutes for privateFree but you pay for servers
FlexibilityGood with marketplace actionsMaximum with 1800+ plugins
Container supportNative Docker and service containersVia Docker Pipeline plugin
PR integrationNative — checks, status, commentsRequires plugins and webhooks
Self-hosted runnersSupportedCore feature
Matrix buildsFirst-class with strategy.matrixRequires scripted pipeline
Secrets managementBuilt-in repository/org secretsCredentials plugin

Best Practices

  1. Use actions/checkout@v4 with fetch-depth: 0 if your tests depend on git history (for change detection or blame).

  2. Set fail-fast: false on matrix strategies for test jobs. You want to see all failures, not just the first one.

  3. Cache aggressively — npm dependencies, Playwright browsers, and any other downloadable assets.

  4. Use continue-on-error: true on test steps combined with if: always() on artifact upload. This ensures test reports are always available.

  5. Pin action versions to specific SHA hashes in production, not just major versions. This prevents supply chain attacks.

  6. Use concurrency groups to cancel outdated workflow runs when a new commit is pushed:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true