{"slug":"github-actions-cicd","title":"GitHub Actions: CI/CD Workflows, Security, and Caching","tags":["github-actions","cicd","devops","automation","deployment"],"agent_summary":"Production GitHub Actions patterns: workflow syntax, secrets management, caching strategies, matrix testing, reusable workflows, and combined CI/CD pipelines.","trigger_phrases":["GitHub Actions","CI/CD pipeline","workflow yaml","GitHub secrets","actions/cache","matrix testing","reusable workflow"],"runnable":false,"markdown":"\n## Overview\n\nGitHub Actions for automated testing, building, and deployment. Covers the patterns that appear in production Next.js and Node.js projects.\n\n## Core Workflow Structure\n\n```yaml\nname: CI/CD Pipeline\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main]\n  schedule:\n    - cron: \"0 8 * * 1-5\"    # Weekdays at 8am UTC\n\nenv:\n  NODE_VERSION: \"20\"\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: \"npm\"\n      - run: npm ci\n      - run: npm run lint\n      - run: npm run type-check\n      - run: npm test\n```\n\n## Security Best Practices (Non-Negotiable)\n\n```yaml\n# NEVER put secrets directly in workflow files\n# BAD:\n# env:\n#   API_KEY: \"sk-actual-key-here\"\n\n# GOOD: Reference secrets from repository/org settings\nenv:\n  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n  DATABASE_URL: ${{ secrets.DATABASE_URL }}\n\n# Limit permissions\npermissions:\n  contents: read\n  pull-requests: write    # Only grant what's needed\n\n# Pin third-party actions to SHA (not tag)\n- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2\n```\n\n## Caching Strategies\n\n```yaml\n# Node modules cache (most common)\n- uses: actions/cache@v4\n  with:\n    path: ~/.npm\n    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}\n    restore-keys: |\n      ${{ runner.os }}-npm-\n\n# Next.js build cache\n- uses: actions/cache@v4\n  with:\n    path: .next/cache\n    key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}\n    restore-keys: |\n      ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-\n      ${{ runner.os }}-nextjs-\n```\n\n## Matrix Testing\n\n```yaml\njobs:\n  test:\n    strategy:\n      matrix:\n        node-version: [18, 20, 22]\n        os: [ubuntu-latest, windows-latest]\n      fail-fast: false    # Don't cancel other jobs on first failure\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n```\n\n## Conditional Execution\n\n```yaml\nsteps:\n  # Only on main branch\n  - name: Deploy to Production\n    if: github.ref == 'refs/heads/main' && github.event_name == 'push'\n    run: vercel --prod\n\n  # Only on PRs\n  - name: Comment PR\n    if: github.event_name == 'pull_request'\n    uses: actions/github-script@v7\n\n  # Skip if commit message contains [skip ci]\n  - name: Run tests\n    if: \"!contains(github.event.head_commit.message, '[skip ci]')\"\n    run: npm test\n```\n\n## Complete CI/CD Pattern (Next.js + Vercel)\n\n```yaml\nname: Deploy\non:\n  push:\n    branches: [main]\n\njobs:\n  ci:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: npm\n      - run: npm ci\n      - run: npm run lint\n      - run: npm run type-check\n      - run: npm run build\n\n  deploy:\n    needs: ci\n    runs-on: ubuntu-latest\n    environment: production\n    steps:\n      - uses: actions/checkout@v4\n      - run: npm install -g vercel\n      - name: Pull Vercel environment\n        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}\n        env:\n          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}\n          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}\n      - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}\n      - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}\n```\n\n## Reusable Workflows\n\n```yaml\n# .github/workflows/test.yml (reusable)\non:\n  workflow_call:\n    inputs:\n      node-version:\n        required: false\n        type: string\n        default: \"20\"\n    secrets:\n      DATABASE_URL:\n        required: true\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ inputs.node-version }}\n      - run: npm ci && npm test\n        env:\n          DATABASE_URL: ${{ secrets.DATABASE_URL }}\n```\n\n```yaml\n# Caller workflow\njobs:\n  run-tests:\n    uses: ./.github/workflows/test.yml\n    with:\n      node-version: \"20\"\n    secrets:\n      DATABASE_URL: ${{ secrets.DATABASE_URL }}\n```\n\n## Useful Cron Schedules\n\n```\n\"0 8 * * 1-5\"    # 8am UTC weekdays\n\"0 0 * * *\"      # Daily midnight UTC\n\"0 */6 * * *\"    # Every 6 hours\n\"0 0 1 * *\"      # First day of month\n```\n","html":"<h2>Overview</h2>\n<p>GitHub Actions for automated testing, building, and deployment. Covers the patterns that appear in production Next.js and Node.js projects.</p>\n<h2>Core Workflow Structure</h2>\n<pre><code class=\"language-yaml\">name: CI/CD Pipeline\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main]\n  schedule:\n    - cron: \"0 8 * * 1-5\"    # Weekdays at 8am UTC\n\nenv:\n  NODE_VERSION: \"20\"\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: \"npm\"\n      - run: npm ci\n      - run: npm run lint\n      - run: npm run type-check\n      - run: npm test\n</code></pre>\n<h2>Security Best Practices (Non-Negotiable)</h2>\n<pre><code class=\"language-yaml\"># NEVER put secrets directly in workflow files\n# BAD:\n# env:\n#   API_KEY: \"sk-actual-key-here\"\n\n# GOOD: Reference secrets from repository/org settings\nenv:\n  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n  DATABASE_URL: ${{ secrets.DATABASE_URL }}\n\n# Limit permissions\npermissions:\n  contents: read\n  pull-requests: write    # Only grant what's needed\n\n# Pin third-party actions to SHA (not tag)\n- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2\n</code></pre>\n<h2>Caching Strategies</h2>\n<pre><code class=\"language-yaml\"># Node modules cache (most common)\n- uses: actions/cache@v4\n  with:\n    path: ~/.npm\n    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}\n    restore-keys: |\n      ${{ runner.os }}-npm-\n\n# Next.js build cache\n- uses: actions/cache@v4\n  with:\n    path: .next/cache\n    key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}\n    restore-keys: |\n      ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-\n      ${{ runner.os }}-nextjs-\n</code></pre>\n<h2>Matrix Testing</h2>\n<pre><code class=\"language-yaml\">jobs:\n  test:\n    strategy:\n      matrix:\n        node-version: [18, 20, 22]\n        os: [ubuntu-latest, windows-latest]\n      fail-fast: false    # Don't cancel other jobs on first failure\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n</code></pre>\n<h2>Conditional Execution</h2>\n<pre><code class=\"language-yaml\">steps:\n  # Only on main branch\n  - name: Deploy to Production\n    if: github.ref == 'refs/heads/main' &#x26;&#x26; github.event_name == 'push'\n    run: vercel --prod\n\n  # Only on PRs\n  - name: Comment PR\n    if: github.event_name == 'pull_request'\n    uses: actions/github-script@v7\n\n  # Skip if commit message contains [skip ci]\n  - name: Run tests\n    if: \"!contains(github.event.head_commit.message, '[skip ci]')\"\n    run: npm test\n</code></pre>\n<h2>Complete CI/CD Pattern (Next.js + Vercel)</h2>\n<pre><code class=\"language-yaml\">name: Deploy\non:\n  push:\n    branches: [main]\n\njobs:\n  ci:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: npm\n      - run: npm ci\n      - run: npm run lint\n      - run: npm run type-check\n      - run: npm run build\n\n  deploy:\n    needs: ci\n    runs-on: ubuntu-latest\n    environment: production\n    steps:\n      - uses: actions/checkout@v4\n      - run: npm install -g vercel\n      - name: Pull Vercel environment\n        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}\n        env:\n          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}\n          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}\n      - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}\n      - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}\n</code></pre>\n<h2>Reusable Workflows</h2>\n<pre><code class=\"language-yaml\"># .github/workflows/test.yml (reusable)\non:\n  workflow_call:\n    inputs:\n      node-version:\n        required: false\n        type: string\n        default: \"20\"\n    secrets:\n      DATABASE_URL:\n        required: true\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ inputs.node-version }}\n      - run: npm ci &#x26;&#x26; npm test\n        env:\n          DATABASE_URL: ${{ secrets.DATABASE_URL }}\n</code></pre>\n<pre><code class=\"language-yaml\"># Caller workflow\njobs:\n  run-tests:\n    uses: ./.github/workflows/test.yml\n    with:\n      node-version: \"20\"\n    secrets:\n      DATABASE_URL: ${{ secrets.DATABASE_URL }}\n</code></pre>\n<h2>Useful Cron Schedules</h2>\n<pre><code>\"0 8 * * 1-5\"    # 8am UTC weekdays\n\"0 0 * * *\"      # Daily midnight UTC\n\"0 */6 * * *\"    # Every 6 hours\n\"0 0 1 * *\"      # First day of month\n</code></pre>\n"}