Skip to main content

GitHub Actions Basics

GitHub Actions is a CI/CD platform built into GitHub. You define workflows in YAML files, and GitHub runs them on virtual machines whenever a trigger fires. This lesson covers the essential syntax and patterns you need to build test pipelines.

Workflow Files

Workflows live in .github/workflows/ as YAML files. Every repository can have multiple workflows, each triggered independently.

# .github/workflows/ci.yml
name: CI

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
      - run: npm ci
      - run: npm test

This workflow has one job called test that runs on every push to main and every pull request targeting main.

Triggers

The on key defines when the workflow runs. Common triggers:

on:
  # Run on push to specific branches
  push:
    branches: [main, develop]

  # Run on PRs targeting specific branches
  pull_request:
    branches: [main]

  # Run on a schedule (cron syntax)
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 6 AM UTC

  # Run manually from the GitHub UI
  workflow_dispatch:

  # Run when another workflow completes
  workflow_run:
    workflows: [Build]
    types: [completed]

You can also filter by file paths — only run when certain files change:

on:
  push:
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package.json'
    paths-ignore:
      - '**.md'
      - 'docs/**'

Jobs and Steps

A workflow contains one or more jobs. Each job runs on a fresh virtual machine. Within a job, steps execute sequentially.

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  # wait for lint to pass
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

By default, jobs run in parallel. The needs keyword creates dependencies — test will not start until lint succeeds.

Steps: run vs uses

Steps can either run a shell command (run) or use a pre-built action (uses):

steps:
  # Use a pre-built action
  - uses: actions/checkout@v4

  # Run a shell command
  - run: echo "Hello from CI"

  # Multi-line shell command
  - run: |
      echo "Installing dependencies..."
      npm ci
      echo "Running tests..."
      npm test

  # Named step with working directory
  - name: Run unit tests
    run: npm test -- --coverage
    working-directory: ./packages/core

Essential Actions

Several official and community actions are used in almost every workflow:

actions/checkout

Clones your repository into the runner:

- uses: actions/checkout@v4

actions/setup-node

Installs a specific Node.js version:

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'  # caches node_modules based on package-lock.json

The cache option speeds up subsequent runs by reusing node_modules from previous runs when package-lock.json has not changed.

actions/cache

Cache any directory for reuse across workflow runs:

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

The key is based on a hash of package-lock.json. When dependencies change, the cache is rebuilt. The restore-keys provide fallback — if no exact match, use the most recent npm- cache.

Environment Variables and Secrets

Set environment variables at different scopes:

env:
  NODE_ENV: test  # available to all jobs

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: postgres://localhost/test  # available to all steps in this job
    steps:
      - run: echo $NODE_ENV  # "test"
      - name: Run with secrets
        run: npm test
        env:
          API_KEY: ${{ secrets.API_KEY }}  # from GitHub repository secrets

Secrets are encrypted and never printed in logs. Add them in your repository settings under Settings > Secrets and variables > Actions.

Matrix Builds

Run the same job across multiple configurations:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

This creates three parallel jobs, one for each Node.js version. If any fails, the workflow reports a failure.

Key Takeaways

  • Workflows are YAML files in .github/workflows/ triggered by events like push and pull_request.
  • Jobs run on fresh VMs in parallel by default; use needs for dependencies.
  • Use actions/setup-node with cache: 'npm' to speed up installs.
  • Filter triggers with paths to avoid running tests when only docs change.
  • Use strategy.matrix to test across multiple Node.js versions or operating systems.