D
Dev SOPKnowledge Base
Search
← All topics

GitHub Actions: CI/CD Workflows, Security, and Caching

Production GitHub Actions patterns: workflow syntax, secrets management, caching strategies, matrix testing, reusable workflows, and combined CI/CD pipelines.

github-actionscicddevopsautomationdeployment
Agent trigger phrases: GitHub Actions · CI/CD pipeline · workflow yaml · GitHub secrets · actions/cache · matrix testing · reusable workflow

Overview

GitHub Actions for automated testing, building, and deployment. Covers the patterns that appear in production Next.js and Node.js projects.

Core Workflow Structure

name: CI/CD Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 8 * * 1-5"    # Weekdays at 8am UTC

env:
  NODE_VERSION: "20"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm test

Security Best Practices (Non-Negotiable)

# NEVER put secrets directly in workflow files
# BAD:
# env:
#   API_KEY: "sk-actual-key-here"

# GOOD: Reference secrets from repository/org settings
env:
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  DATABASE_URL: ${{ secrets.DATABASE_URL }}

# Limit permissions
permissions:
  contents: read
  pull-requests: write    # Only grant what's needed

# Pin third-party actions to SHA (not tag)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

Caching Strategies

# Node modules cache (most common)
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

# Next.js build cache
- 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') }}-
      ${{ runner.os }}-nextjs-

Matrix Testing

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
      fail-fast: false    # Don't cancel other jobs on first failure
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

Conditional Execution

steps:
  # Only on main branch
  - name: Deploy to Production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: vercel --prod

  # Only on PRs
  - name: Comment PR
    if: github.event_name == 'pull_request'
    uses: actions/github-script@v7

  # Skip if commit message contains [skip ci]
  - name: Run tests
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    run: npm test

Complete CI/CD Pattern (Next.js + Vercel)

name: Deploy
on:
  push:
    branches: [main]

jobs:
  ci:
    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 type-check
      - run: npm run build

  deploy:
    needs: ci
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g vercel
      - name: Pull Vercel environment
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
      - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
      - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

Reusable Workflows

# .github/workflows/test.yml (reusable)
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/test.yml
    with:
      node-version: "20"
    secrets:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}

Useful Cron Schedules

"0 8 * * 1-5"    # 8am UTC weekdays
"0 0 * * *"      # Daily midnight UTC
"0 */6 * * *"    # Every 6 hours
"0 0 1 * *"      # First day of month