GitHub
02 / 10

GitHub Actions

GitHub Actions

GitHub Actions is a CI/CD platform built directly into GitHub. Workflows are YAML files in `.github/workflows/` that run automatically on triggers like pushes, PRs, schedules, or manual dispatch.

Workflow Anatomy

# .github/workflows/ci.yml
name: CI

# Triggers
on:
  push:
    branches: [main, develop]
    paths-ignore: ['**.md', 'docs/**']
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]
  schedule:
    - cron: '0 9 * * 1'   # Every Monday at 9am UTC
  workflow_dispatch:       # Manual trigger from GitHub UI
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

# Environment variables available to all jobs
env:
  NODE_VERSION: '20'
  REGISTRY: ghcr.io

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    # runs-on: [self-hosted, linux, x64]   # Self-hosted runner

    # Job-level environment
    env:
      DATABASE_URL: postgresql://localhost/test

    # Services (Docker containers)
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test -- --coverage
        env:
          CI: true

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7

Secrets & Contexts

# Secrets are set in: Repo → Settings → Secrets and variables → Actions
# Access via ${{ secrets.SECRET_NAME }}

# Common contexts:
# ${{ github.sha }}          - commit SHA
# ${{ github.ref }}          - branch/tag ref  (refs/heads/main)
# ${{ github.ref_name }}     - branch name (main)
# ${{ github.actor }}        - user who triggered the workflow
# ${{ github.repository }}   - owner/repo
# ${{ github.event_name }}   - push, pull_request, etc.
# ${{ runner.os }}           - Linux, Windows, macOS
# ${{ job.status }}          - success, failure, cancelled

steps:
  - name: Deploy to production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: |
      curl -X POST https://api.vercel.com/v1/integrations/deploy \
        -H "Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}" \
        -d '{"target": "production"}'
    env:
      DEPLOY_ENV: production

  - name: Notify Slack on failure
    if: failure()
    uses: slackapi/slack-github-action@v1.26.0
    with:
      payload: |
        {
          "text": "Build failed on ${{ github.ref_name }} by ${{ github.actor }}"
        }
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Test Matrix

# Run tests across multiple Node versions and OSes in parallel
jobs:
  test:
    strategy:
      fail-fast: false   # Don't cancel other jobs on one failure
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest, macos-latest]
        exclude:
          - os: windows-latest
            node-version: 18   # Skip Node 18 on Windows
        include:
          - os: ubuntu-latest
            node-version: 20
            coverage: true     # Only upload coverage for this combo

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test
      - name: Upload coverage
        if: matrix.coverage
        uses: codecov/codecov-action@v4

Deploy Workflow (Next.js to Vercel)

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      # Cache build output
      - uses: actions/cache@v4
        with:
          path: |
            .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-

      - run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}

      - name: Deploy to Vercel
        run: |
          npx vercel --prod \
            --token ${{ secrets.VERCEL_TOKEN }} \
            --scope ${{ vars.VERCEL_ORG_ID }}
        env:
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
          VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}

Reusable Workflows & Composite Actions

# Reusable workflow: .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
    secrets:
      DATABASE_URL:
        required: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci && npm test
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

---
# Caller workflow
jobs:
  run-tests:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '22'
    secrets:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free