Skip to content

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