A GitHub Actions workflow

Triggers, jobs, steps, a build matrix and artefacts, written as one workflow file you can adapt.

Anatomy of a workflow

A workflow is a YAML file in .github/workflows/. It declares when it runs, which jobs run on which machines, and the steps inside each job. Steps in one job share a filesystem; different jobs do not.

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

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 run lint
      - run: npm test
  • on decides which events trigger a run: a push to a branch, a pull request, a tag, a schedule or a manual workflow_dispatch.
  • uses pulls in a published action, while run executes a shell command. Pin third-party actions to a version or a commit SHA.
  • Each job starts on a fresh runner, so anything installed in one job is gone by the next.

Matrix builds and caching

  build:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false          # keep other combinations running
      matrix:
        os: [ubuntu-latest, macos-latest]
        node: [20, 22]
        exclude:
          - os: macos-latest
            node: 20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm test
FeatureNotes
strategy.matrixRuns the job once per combination, in parallel
fail-fast: falseOne failing combination does not cancel the others
exclude / includeTrim or add specific combinations
setup-node with cache: npmCaches the npm download cache, keyed on the lockfile

A four-way matrix multiplies runner minutes by four. Keep the cross-product small and move extra coverage into a nightly run.

Moving files between jobs

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 7

  package:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: tar -czf site.tar.gz -C dist .
💡
needs is what creates the dependency: the runner for package does not start until build has finished. Without it the jobs run in parallel and download-artifact finds nothing.

FAQ

Why did my job run twice on a pull request?
The workflow triggers on both push and pull_request. Restrict push to branches: [main], or add a concurrency group so superseded runs are cancelled.
How do I run a workflow by hand?
Add workflow_dispatch to the on: block, then use the Run workflow button in the Actions tab or the gh workflow run command.

Pipeline stages and artefacts Gates, secrets and deploys

Last refreshed 2026-09-18.