GitHub Actions is GitHub's built-in CI/CD system. Workflows are YAML files in your repo, triggered by GitHub events, executed on hosted or self-hosted runners.
The File Layout
.github/
└── workflows/
├── ci.yml # runs on every push and PR
├── release.yml # runs on tag
└── nightly.yml # runs on schedule
Each .yml file is one workflow. You can have as many as you like.
A Minimal Workflow
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'
- run: npm ci
- run: npm test
That's a complete CI pipeline. Push to GitHub, head to the Actions tab, watch it run.
Anatomy
| Field | Meaning |
|---|---|
name | Display name in the UI |
on | Triggers — events that start the workflow |
jobs | One or more jobs; default they run in parallel |
runs-on | The runner OS — ubuntu-latest, windows-latest, macos-latest |
steps | Sequential commands or actions inside a job |
uses | Reference a marketplace action |
run | Run a shell command on the runner |
Triggers
on:
push:
branches: [main, 'release/**']
paths-ignore: ['docs/**', '*.md']
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '0 6 * * *' # 6am UTC daily
workflow_dispatch: # manual run from the UI
inputs:
environment:
description: Target environment
type: choice
options: [dev, staging, prod]
release:
types: [published]
The richest set of CI triggers in the industry. Combine them; one workflow can be triggered many ways.
Multiple Jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
build:
needs: [lint, test] # only runs if lint and test pass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
needs: creates dependencies between jobs. Without it, jobs run in parallel. upload-artifact / download-artifact share files between jobs.
Matrix Builds
Run the same job across many configurations:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
3 OSes × 3 Node versions = 9 parallel jobs. fail-fast: false lets all jobs finish even if one fails — handy for cross-platform debugging.
Caching
Reinstalling dependencies on every run is wasteful. Use the cache action — most setup actions (setup-node, setup-python, setup-go) handle it for you with one option:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # caches based on package-lock.json
For arbitrary caching:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
Expressions and Contexts
Use ${{ ... }} to access workflow context:
- name: Print branch
run: echo "Branch is ${{ github.ref_name }}"
- name: Run only on main
if: github.ref == 'refs/heads/main'
run: echo "Deploying"
- name: Always run
if: always()
run: echo "Even after failure"
- name: Skip on docs-only PRs
if: ${{ !contains(github.event.head_commit.message, 'docs:') }}
run: ./script.sh
Available contexts include github, env, secrets, matrix, steps, jobs, runner.
Environment Variables
env:
NODE_ENV: production # workflow-level
jobs:
build:
env:
DEPLOY_BUCKET: my-app-prod # job-level
steps:
- run: env | grep NODE_ENV
Outputs Between Steps and Jobs
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.value }}
steps:
- id: ver
run: echo "value=$(date +%Y%m%d-%H%M)" >> "$GITHUB_OUTPUT"
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.build.outputs.version }}"
Marketplace Actions
Don't write what's already written. The Actions Marketplace has thousands of reusable steps:
actions/checkout— clone the repoactions/setup-node,setup-python,setup-go,setup-javaactions/cacheactions/upload-artifact/download-artifactaws-actions/configure-aws-credentialsazure/login/google-github-actions/authdocker/build-push-actioncodecov/codecov-action
Pin third-party actions to a commit SHA, not a floating tag — a compromised tag could exfiltrate secrets:
- uses: someorg/some-action@a1b2c3d4e5f6... # safer than @v1
Runners
- GitHub-hosted runners — ubuntu, windows, macOS; pay-per-minute on private repos, free on public
- Larger runners — 4-, 8-, 16-vCPU, GPU runners for heavy builds
- Self-hosted runners — your own VMs / Kubernetes (ARC) for special hardware, on-prem networks, large fleets
What's Next
The next lesson covers reusable workflows and composite actions — how to package pipeline logic so you don't copy-paste the same steps into 50 repos.