Overview

GitHub Actions is the CI layer for most projects on this site. A workflow file is YAML that declares triggers, jobs, and steps; Actions runs them on managed runners. The rules below cover structure, caching, secrets, concurrency, and reusable workflows. Read github for the branching and merge strategy that these workflows enforce.

Structure workflow files by concern, one file per trigger

Each .github/workflows/ file owns exactly one responsibility. ci.yml runs lint and tests on pull requests. deploy.yml runs build and publish on push to main. release.yml cuts a release on tag push. Mixing concerns in one file creates a workflow that always does the wrong subset of work.

Keep each file under 100 lines. When a job grows past that, extract a composite action in .github/actions/<name>/action.yml or promote it to a reusable workflow.

Jobs are parallel; steps are sequential within a job

A job runs on a fresh runner. Steps inside a job share state: filesystem, environment, and tool outputs. Use multiple jobs when steps are independent and you want parallelism. Use needs: to express dependencies between jobs.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

A job that fails stops downstream jobs that needs: it. Use if: always() on a reporting job when you need it to run regardless.

Matrix builds multiply a job across dimensions

Define a strategy.matrix when you need the same job on multiple values, such as Node versions or operating systems.

strategy:
  matrix:
    node: [20, 22]
    os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}

Set fail-fast: false to let all matrix legs finish even when one fails. This gives you the full failure surface in one run rather than a partial report. Use matrix.include to add one-off combinations and matrix.exclude to drop invalid ones.

Cache dependencies to cut run time

Uncached npm ci or pip install dominates a short workflow. Cache the dependency store and restore it on the next run.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: npm-${{ runner.os }}-

The key must change when the lockfile changes; hash the lockfile directly. Use restore-keys as a prefix fallback so a partial cache is better than no cache. Cache invalidation happens automatically when the key changes.

Scope GITHUB_TOKEN permissions to the minimum

The default GITHUB_TOKEN has write access to most resources. Declare only what the workflow needs at the top level and override per-job if necessary.

permissions:
  contents: read
  pages: write
  id-token: write

For workflows that read code and run tests, contents: read is sufficient. For deployments to GitHub Pages, add pages: write and id-token: write. Store third-party credentials in Actions secrets and reference them as ${{ secrets.NAME }}; see github-secrets for the full rules.

Concurrency guards prevent redundant runs

A fast developer pushing twice in a row starts two deploy runs. The second one makes the first wasteful or harmful.

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

For CI on pull requests, cancel in-progress runs for the same branch. For deployments to production, set cancel-in-progress: false to let the current deploy finish before starting the next. Use github.ref as the group key to isolate concurrency by branch.

Reusable workflows eliminate copy-paste across repos

A workflow that takes inputs and secrets and is called from other workflows lives in .github/workflows/ with on: workflow_call. Consuming repos reference it with uses: org/repo/.github/workflows/build.yml@main.

# .github/workflows/build.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
    secrets:
      NPM_TOKEN:
        required: true

Reusable workflows enforce a single definition of “pass” across a fleet of repos without forking YAML. Pair them with github-branch-protection required status checks so every repo enforces the same bar.