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 testThis 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 testBy 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/coreEssential Actions
Several official and community actions are used in almost every workflow:
actions/checkout
Clones your repository into the runner:
- uses: actions/checkout@v4actions/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.jsonThe 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 secretsSecrets 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 testThis 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
needsfor dependencies. - Use
actions/setup-nodewithcache: 'npm'to speed up installs. - Filter triggers with
pathsto avoid running tests when only docs change. - Use
strategy.matrixto test across multiple Node.js versions or operating systems.