Stage 2 · Tools
Hooks & Automation
GitHub Actions Intro
Automate your workflow — run tests, lint, build, and deploy with every push and pull request.
What Is GitHub Actions?
GitHub Actions is GitHub's built-in CI/CD platform. It runs workflows defined in YAML files in .github/workflows/. Each workflow triggers on events (push, pull request, schedule, etc.) and executes a series of jobs on virtual machines called runners.
Actions replace external CI services like Travis CI, CircleCI, or Jenkins. The workflow file lives in your repository, version-controlled alongside your code. Every push triggers the same pipeline for every contributor.
Because workflow files live in your repo, they are version-controlled and auditable. A change to the CI pipeline goes through the same pull request and review process as any other code change.
Workflow Anatomy
A GitHub Actions workflow has four top-level keys: name (display name), on (triggers), jobs (what to run), and optionally env (global environment variables).
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm testThis workflow runs on pushes to main and pull requests targeting main. It checks out the code and runs npm test. The uses keyword references a pre-built action — here, actions/checkout clones the repository.
Triggers: push & pull_request
The on key defines when the workflow runs. The two most common triggers are push (code is pushed to a branch) and pull_request (a PR is opened, synchronized, or reopened).
on:
push:
branches: [main, 'release/**']
paths:
- 'src/**'
- 'package.json'
- '.github/workflows/**'
pull_request:
branches: [main]
types: [opened, synchronize, reopened]This configuration triggers on pushes to main and release branches, but only when files in src/, package.json, or workflow files change. The paths filter prevents unnecessary CI runs when only documentation changes.
| Trigger | When It Runs | Common Use |
|---|---|---|
| push | Code is pushed to a branch | Run tests, build, deploy |
| pull_request | PR is opened, updated, or reopened | Run tests, lint, preview deploy |
| schedule | Cron schedule (e.g., nightly) | Dependency audits, nightly builds |
| workflow_dispatch | Manual trigger from GitHub UI | Deploy to production, run ad-hoc tasks |
Adding paths filters to your push trigger prevents CI from running when only README or docs change. This saves runner minutes and keeps your CI status clean.
Jobs and Steps
A workflow contains one or more jobs. Each job runs on its own runner and can have multiple steps. Steps can be shell commands (run) or reusable actions (uses). Jobs run in parallel by default; use needs to define dependencies.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lint
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
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run buildThe lint and test jobs run in parallel. The build job depends on both (needs: [lint, test]) and only runs after they succeed. If either lint or test fails, build is skipped.
Each uses step references a pre-built action from the GitHub Marketplace or a public repository. The with key passes inputs to the action.
Matrix Builds
Matrix strategy lets you run the same job across multiple configurations — different Node versions, operating systems, or dependency versions. GitHub creates a separate runner for each combination.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
include:
- node-version: 18
experimental: false
- node-version: 20
experimental: false
- node-version: 22
experimental: true
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testThis matrix runs tests against Node 18, 20, and 22. The fail-fast: false option ensures all matrix combinations complete even if one fails — so you see all failures at once instead of stopping at the first one.
Use include to add extra variables to specific matrix entries, and exclude to remove combinations. For example, you might run Windows tests only on Node 20 to save runner minutes.
Caching Dependencies
Without caching, every workflow run downloads all dependencies from scratch. GitHub Actions provides a cache action and built-in caching in setup actions to speed this up.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ciThe cache: 'npm' option in setup-node automatically caches the npm global cache directory. On subsequent runs, npm ci uses the cache to skip downloading packages that haven't changed.
steps:
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-The manual approach uses actions/cache with a key derived from package-lock.json. When the lockfile changes, a new cache entry is created. The restore-keys fallback restores the most recent cache if an exact match isn't found.
setup-nodewithcache: 'npm'— simplest for Node.js projects.actions/cache— flexible, works for any language or build tool.hashFiles('package-lock.json')— cache key changes when dependencies change.- Cache is immutable once written — a new key creates a new entry.
Full CI Workflow
Here is a complete CI workflow that combines everything: lint, test, build, matrix builds, and caching.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
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
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
build:
needs: [lint, 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 build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/This workflow runs lint and test in parallel (with test running across three Node versions). Build runs only after both pass. The final step uploads the build output as an artifact that can be downloaded or deployed.
npm install modifies package-lock.json if it's out of date, which means your CI might test a different dependency tree than your local setup. npm ci installs exactly what's in the lockfile and fails if it's inconsistent.
Public repositories get unlimited free minutes on GitHub Actions. Private repos get 2,000 minutes per month on the free tier. Minutes are rounded up to the nearest whole minute.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.