GitHub Actions Guide

GitHub Actions is the CI/CD platform built directly into GitHub. Because it lives alongside your code, pull requests, and issues, it provides the shortest path from "code pushed" to "quality checks completed." Every public repository gets generous free minutes, and the marketplace offers thousands of reusable actions for everything from linting to deployment. For quality engineering, GitHub Actions is the natural starting point for automating checks across accessibility, security, performance, and code quality.

This lesson walks through the core concepts of GitHub Actions, shows how to build a workflow that runs quality checks on every push and pull request, and covers advanced techniques like matrix builds, caching, and artifact uploads that make your pipelines fast and informative.

How GitHub Actions Works

GitHub Actions is event-driven. You define workflows in YAML files stored in the .github/workflows/ directory of your repository. Each workflow specifies one or more triggers (events that start the workflow), one or more jobs (groups of steps that run on a virtual machine), and the steps within each job (individual commands or reusable actions).

When a trigger event occurs — a push to a branch, a pull request opened, a schedule firing, or a manual dispatch — GitHub spins up a fresh virtual machine (called a runner), checks out your code, and executes the steps you defined. The results appear as status checks on your commits and pull requests.

Your First Workflow

Here is a minimal workflow that runs on every push and pull request to the main branch:

name: Quality Checks

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

Let us break down each section:

  • name: A human-readable name that appears in the GitHub UI.
  • on: The events that trigger the workflow. Here, pushes to main and pull requests targeting main.
  • jobs: A map of job names to job definitions. Each job runs on a separate runner.
  • runs-on: The operating system for the runner. ubuntu-latest is the most common choice.
  • steps: An ordered list of tasks. Each step either uses a reusable action or runs a shell command.
Practical tip: Always use npm ci instead of npm install in CI. The ci command installs from the lockfile exactly, is faster, and fails if the lockfile is out of sync with package.json — which is exactly the behavior you want in a pipeline.

Trigger Events

GitHub Actions supports dozens of trigger events. The most useful for quality engineering are:

  • push — Runs when code is pushed to specified branches.
  • pull_request — Runs when a PR is opened, synchronized (new commits pushed), or reopened. This is the primary trigger for quality checks because results appear as status checks on the PR.
  • schedule — Runs on a cron schedule. Useful for nightly full security scans or dependency audits that are too slow to run on every push.
  • workflow_dispatch — Allows manual triggering from the GitHub UI. Useful for on-demand tasks like generating a full quality report.

You can combine multiple triggers in a single workflow. For example, run quick checks on every PR but a more comprehensive suite nightly:

on:
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Every night at 2:00 AM UTC

Matrix Builds

Matrix builds let you run the same job across multiple configurations — different Node.js versions, operating systems, or browser engines. This is essential for ensuring your application works across the environments your users actually use.

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        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 configuration creates 9 jobs (3 operating systems multiplied by 3 Node.js versions). Each job runs independently and in parallel, and the results are displayed as a matrix in the GitHub UI. If a test passes on Linux but fails on Windows, you will see exactly which combination broke.

Key insight: Matrix builds are also valuable for testing across multiple browsers using tools like Playwright. You can define a matrix of browser: [chromium, firefox, webkit] and run your end-to-end tests across all three engines in parallel.

Caching Dependencies

Installing dependencies from scratch on every run is slow. GitHub Actions provides a built-in caching mechanism that can dramatically speed up your pipelines. The actions/setup-node action supports a cache parameter that handles npm or yarn caching automatically:

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

For more control, you can use the actions/cache action directly:

- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

The key is based on the lockfile hash, so the cache is invalidated whenever dependencies change. The restore-keys provide fallback keys for partial cache hits.

Running Quality Checks in CI

Here is where quality engineering comes alive. A comprehensive workflow runs multiple types of checks, each targeting a different quality dimension:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      # Code quality
      - name: Lint code
        run: npm run lint

      - name: Check formatting
        run: npx prettier --check .

      # Unit and integration tests
      - name: Run tests with coverage
        run: npm test -- --coverage

      # Accessibility
      - name: Start dev server
        run: npm start &
        env:
          PORT: 3000

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run Pa11y CI
        run: npx pa11y-ci

      # Security
      - name: Audit dependencies
        run: npm audit --audit-level=high

      - name: Scan for secrets
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Each step targets a different quality dimension: code quality (linting and formatting), functional correctness (tests), accessibility (Pa11y CI), and security (dependency audit and secrets scanning). If any step fails, the entire workflow fails, and the pull request shows a red status check.

Accessibility Testing with Pa11y CI

Pa11y CI is a tool that runs automated accessibility checks against a list of URLs. It is ideal for CI pipelines because it exits with a non-zero code when violations are found, which causes the GitHub Actions step to fail.

You configure Pa11y CI with a .pa11yci JSON file in your repository root:

{
  "defaults": {
    "standard": "WCAG2AA",
    "timeout": 10000
  },
  "urls": [
    "http://localhost:3000/",
    "http://localhost:3000/about",
    "http://localhost:3000/contact"
  ]
}

The CodeFrog project itself uses GitHub Actions to run Pa11y CI against its landing page on every pull request. This ensures that accessibility regressions are caught before they reach production — a real-world example of quality engineering in practice.

Note: Automated checks (including Pa11y CI) catch only a subset of WCAG issues. Manual testing with assistive technologies and human judgment is required for full conformance.

Artifact Uploads

Sometimes a quality check produces a report (a coverage HTML file, a Lighthouse JSON, a Pa11y report) that you want to preserve. GitHub Actions artifacts let you upload files from a workflow run and download them later:

- name: Run Lighthouse
  run: npx lhci collect --url=http://localhost:3000

- name: Upload Lighthouse report
  uses: actions/upload-artifact@v4
  with:
    name: lighthouse-report
    path: .lighthouseci/
    retention-days: 30

Artifacts appear on the workflow run summary page. Team members can download them to review detailed reports without needing to reproduce the run locally.

Status Checks and Branch Protection

Running quality checks is only half the equation. The other half is preventing merges when checks fail. GitHub branch protection rules let you require specific status checks to pass before a pull request can be merged:

  1. Go to your repository's Settings → Branches.
  2. Add a branch protection rule for main.
  3. Enable "Require status checks to pass before merging."
  4. Search for and select the workflow jobs you want to require (e.g., quality, test).
  5. Optionally enable "Require branches to be up to date before merging" to ensure the checks ran against the latest main.

With branch protection in place, no one can merge code that fails your quality checks — not even repository administrators (if you enable the "Include administrators" option). This is the foundation of an automated quality gate, which we will explore in detail in the Automated Quality Gates lesson.

Best practice: Start with a small, fast workflow that runs linting and unit tests. Once that is stable and the team has adapted, add accessibility, security, and performance checks incrementally. Trying to enforce everything at once often leads to frustration and teams disabling checks rather than fixing issues.

Reusable Workflows and the Marketplace

As your CI/CD setup grows, you may find yourself duplicating workflow logic across repositories. GitHub Actions addresses this with two mechanisms:

  • Reusable workflows: You can define a workflow in one repository and call it from workflows in other repositories using the uses keyword at the job level. This is ideal for organization-wide quality standards.
  • The GitHub Actions Marketplace: Thousands of community-maintained actions are available for common tasks. Before writing a custom step, check the marketplace — there is likely an action for what you need.

Popular marketplace actions for quality engineering include:

  • actions/checkout — Checks out your repository code
  • actions/setup-node — Sets up Node.js with caching
  • actions/cache — Caches dependencies and build outputs
  • actions/upload-artifact — Uploads build artifacts
  • gitleaks/gitleaks-action — Scans for secrets in code
  • treosh/lighthouse-ci-action — Runs Lighthouse performance audits

Putting It All Together

A mature quality engineering workflow on GitHub Actions typically includes multiple jobs that run in parallel for speed, each focused on a specific quality dimension. The workflow produces clear pass/fail signals that feed into branch protection rules, and it uploads detailed reports as artifacts for deeper investigation when something fails.

The key principles to remember are:

  • Fail fast: Put the quickest checks (linting, formatting) first so developers get feedback in seconds, not minutes.
  • Cache aggressively: Cache dependencies, build outputs, and tool binaries to keep pipeline times low.
  • Use matrix builds when you need to test across multiple environments, but be mindful of the total number of jobs — 3 dimensions of 4 values each creates 64 jobs.
  • Upload artifacts for any check that produces a detailed report, so team members can investigate failures without reproducing them locally.
  • Enforce with branch protection: Quality checks only matter if they block bad code from being merged.

Resources