GitHub Actions¶
GitHub Actions is the CI/CD engine baked into every repo you already have — no Jenkins server to maintain , no GitLab instance to babysit , just YAML files in .github/workflows/ that run on every push if you set them up right Every workflow runs on GitHub's infrastructure (or your own self-hosted runners) and costs nothing for public repos. Private repos get 2000 free minutes/month on the free tier before they start charging
Workflow YAML — The Anatomy¶
# .github/workflows/ci.yml
name: CI # Display name in GitHub UI
on: # Trigger — when this workflow runs
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: # Manual trigger from GitHub UI
jobs:
test:
runs-on: ubuntu-latest # Runner environment
defaults:
run:
working-directory: ./backend # Default dir for all steps
services: # Background services (like Docker Compose)
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4 # Checkout repo
- uses: actions/setup-node@v4 # Setup Node.js
with:
node-version: '20'
cache: 'npm'
- run: npm ci # Install dependencies
- run: npm run lint # Lint
- run: npm test # Test
env: # Override env per step
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
Triggers — When Your Pipeline Fires¶
on:
push: # Every push to any branch
branches: [main, develop] # Only these branches
paths-ignore: # Skip if only docs changed
- '**.md'
- 'docs/**'
pull_request: # Every PR
branches: [main]
types: [opened, synchronize, reopened]
schedule: # Cron schedule
- cron: '0 2 * * *' # Daily at 2AM UTC
workflow_run: # After another workflow
workflows: ["Build"]
types: [completed]
release: # On release publish
types: [published]
Jobs — Parallel Work With Dependencies¶
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 # Save build output
with:
name: dist
path: dist/
deploy:
needs: [build] # Only runs after build succeeds
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only deploy from main
steps:
- run: echo "Deploying..."
Matrix Builds — Test Everything at Once¶
strategy:
matrix:
node-version: [18, 20, 22] # Test multiple Node versions
os: [ubuntu-latest, windows-latest] # Multiple OS
include: # Add specific extra tests
- node-version: 20
os: ubuntu-latest
experimental: false
exclude: # Remove combos that don't make sense
- node-version: 18
os: windows-latest
fail-fast: false # Don't cancel other matrix jobs on failure
Matrix builds catch cross-version compatibility issues before your users do. fail-fast: false ensures you see all failures in one run instead of fixing one then discovering the next
Caching Dependencies — Stop Reinstalling Everything¶
Without caching , every CI run downloads and installs everything from scratch A Node project with 500 dependencies takes 2 minutes to install. With caching , it takes 5 seconds
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Auto-caches ~/.npm
# Manual cache for Docker layers or other tools
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
Docker Build with GitHub Actions¶
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Self-Hosted Runners — When GitHub's Aren't Enough¶
Reasons to run your own: you need GPU access , you need internal network connectivity , you have massive dependency caches you don't want to re-download every time
jobs:
deploy:
runs-on: self-hosted # Your own machine
steps:
- uses: actions/checkout@v4
- run: ./deploy-to-prod.sh
# Register a self-hosted runner
# Go to repo Settings > Actions > Runners > Add runner
# Then run the provided commands on your machine
Full Production Pipeline — The Gold Standard¶
name: Full Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
quality:
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 run typecheck
- run: npm test
- run: npm audit --audit-level=high
docker-build:
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
deploy-staging:
needs: docker-build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploy to staging..."
deploy-production:
needs: deploy-staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "Deploy to production..."
next → devops_07_env_management.md