Skip to main content
Technology & EngineeringDeployment Patterns469 lines

github-actions-cd

Comprehensive guide to implementing continuous deployment with GitHub Actions, covering deploy workflows, environment protection rules, secrets management, matrix builds, dependency caching, artifact management, and deploying to multiple targets including Vercel, Fly.io, AWS, and container registries.

Quick Summary36 lines
```yaml
name: Deploy

## Key Points

- **production**: Required reviewers, wait timer, branch restrictions
- **staging**: Auto-deploy, no restrictions
- **preview**: Ephemeral, per-PR
- **Required reviewers**: 1-6 people must approve the deploy
- **Wait timer**: 0-43200 minutes delay before deploy proceeds
- **Deployment branches**: Restrict which branches can deploy (e.g., only `main`)
- name: Deploy
1. **Repository secrets**: Available to all workflows
2. **Environment secrets**: Scoped to a specific environment
3. **Organization secrets**: Shared across repos, with access policies
- name: Deploy
- name: Configure AWS

## Quick Example

```yaml
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'  # or 'pnpm' or 'yarn'
```

```yaml
- name: Deploy to Fly.io
  uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
  env:
    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
```
skilldb get deployment-patterns-skills/github-actions-cdFull skill: 469 lines
Paste into your CLAUDE.md or agent config

Continuous Deployment with GitHub Actions

Basic Deploy Workflow

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

on:
  push:
    branches: [main]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    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 test
      - run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - name: Deploy to production
        run: npx vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }}

Environment Protection Rules

Setting Up Environments

Configure in repo Settings > Environments:

  • production: Required reviewers, wait timer, branch restrictions
  • staging: Auto-deploy, no restrictions
  • preview: Ephemeral, per-PR
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to staging
        run: ./deploy.sh staging

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - name: Deploy to production
        run: ./deploy.sh production

Branch Protection

In environment settings:

  • Required reviewers: 1-6 people must approve the deploy
  • Wait timer: 0-43200 minutes delay before deploy proceeds
  • Deployment branches: Restrict which branches can deploy (e.g., only main)

Environment-Specific Secrets

Each environment has its own secrets namespace:

steps:
  - name: Deploy
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}        # Environment-specific
      API_KEY: ${{ secrets.API_KEY }}                   # Environment-specific
      SHARED_TOKEN: ${{ secrets.SHARED_TOKEN }}         # Repository-level fallback

Anti-pattern: Using a single set of secrets for all environments. Production secrets should never be accessible from staging or preview workflows.

Secrets Management

Types of Secrets

  1. Repository secrets: Available to all workflows
  2. Environment secrets: Scoped to a specific environment
  3. Organization secrets: Shared across repos, with access policies

Using Secrets Safely

steps:
  - name: Deploy
    env:
      # Secrets are masked in logs automatically
      DB_URL: ${{ secrets.DATABASE_URL }}
    run: |
      # NEVER echo secrets
      # BAD: echo "Deploying with $DB_URL"
      # GOOD: echo "Deploying to production database"
      ./deploy.sh

  - name: Configure AWS
    uses: aws-actions/configure-aws-credentials@v4
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: us-east-1

OIDC Authentication (No Stored Secrets)

permissions:
  id-token: write
  contents: read

steps:
  - name: Configure AWS with OIDC
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions
      aws-region: us-east-1
      # No access keys needed!

Anti-pattern: Storing long-lived credentials as secrets. Use OIDC federation with AWS, GCP, and Azure for short-lived, auto-rotating tokens.

Matrix Builds

Multi-Platform Build

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]
        exclude:
          - os: windows-latest
            node: 18
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

Multi-Target Deploy

jobs:
  deploy:
    strategy:
      matrix:
        target:
          - name: us-east
            region: iad
            url: https://us.example.com
          - name: eu-west
            region: cdg
            url: https://eu.example.com
          - name: ap-east
            region: nrt
            url: https://ap.example.com
      fail-fast: false  # Don't cancel other regions if one fails
    runs-on: ubuntu-latest
    environment:
      name: production-${{ matrix.target.name }}
      url: ${{ matrix.target.url }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to ${{ matrix.target.name }}
        run: fly deploy --region ${{ matrix.target.region }}
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Caching

npm / pnpm / yarn

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'  # or 'pnpm' or 'yarn'

Custom Caching

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

Docker Layer Caching

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ghcr.io/org/app:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Anti-pattern: Not caching dependencies. A fresh npm ci on every run wastes 1-3 minutes per workflow.

Artifact Management

Passing Artifacts Between Jobs

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 1

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy
        run: ./deploy.sh dist/

Release Assets

on:
  release:
    types: [published]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: tar -czf dist.tar.gz dist/
      - name: Upload release asset
        uses: softprops/action-gh-release@v2
        with:
          files: dist.tar.gz

Deploy to Multiple Targets

Vercel

- name: Deploy to Vercel
  uses: amondnet/vercel-action@v25
  with:
    vercel-token: ${{ secrets.VERCEL_TOKEN }}
    vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
    vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
    vercel-args: '--prod'

Fly.io

- name: Deploy to Fly.io
  uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
  env:
    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

AWS S3 + CloudFront

- name: Configure AWS
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    aws-region: us-east-1

- name: Sync to S3
  run: aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} --delete

- name: Invalidate CloudFront
  run: |
    aws cloudfront create-invalidation \
      --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
      --paths "/*"

Docker to GHCR

- name: Login to GHCR
  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 }}

Advanced Patterns

Deployment Gate with Manual Approval

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "Deployed to staging"

  approval:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # Has required reviewers
    steps:
      - run: echo "Approved for production"

  deploy-production:
    needs: approval
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to production"

Rollback Workflow

# .github/workflows/rollback.yml
name: Rollback
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to rollback to (git SHA or tag)'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.version }}
      - run: npm ci && npm run build
      - run: ./deploy.sh production

Conditional Deploys (Monorepo)

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      web: ${{ steps.filter.outputs.web }}
      api: ${{ steps.filter.outputs.api }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            web:
              - 'apps/web/**'
              - 'packages/shared/**'
            api:
              - 'apps/api/**'
              - 'packages/shared/**'

  deploy-web:
    needs: changes
    if: needs.changes.outputs.web == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying web"

  deploy-api:
    needs: changes
    if: needs.changes.outputs.api == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying API"

Common Anti-Patterns

  1. No concurrency control: Multiple deploys running simultaneously can cause race conditions. Always use concurrency groups.
  2. Hardcoded secrets in workflows: Even in env: blocks at the step level, use ${{ secrets.X }}, never literal values.
  3. No rollback plan: If your workflow only goes forward, you're stuck when things break.
  4. Skipping tests before deploy: The needs: test dependency is critical.
  5. Not using fail-fast: false in multi-region deploys: One region failing shouldn't cancel others.
  6. Caching node_modules directly: Cache the npm/pnpm/yarn cache directory, not node_modules. Use the built-in cache option in setup-node.

Workflow Checklist

  • Tests run before deploy (needs: test)
  • Concurrency group prevents parallel deploys
  • Environment protection rules configured for production
  • Secrets scoped to appropriate environments
  • OIDC used instead of long-lived credentials where possible
  • Dependency caching enabled
  • Rollback workflow available
  • Deploy notifications configured (Slack, email)
  • Monorepo path filtering prevents unnecessary deploys

Install this skill directly: skilldb add deployment-patterns-skills

Get CLI access →

Related Skills

database-deployment

Comprehensive guide to database deployment for web applications, covering managed database services (PlanetScale, Neon, Supabase, Turso), migration strategies, connection pooling, backup and restore procedures, data seeding, and schema management best practices for production environments.

Deployment Patterns539L

docker-deployment

Comprehensive guide to using Docker for production deployments, covering multi-stage builds, .dockerignore optimization, layer caching strategies, health checks, Docker Compose for local development, container registries, and security scanning best practices.

Deployment Patterns479L

fly-io-deployment

Complete guide to deploying applications on Fly.io, covering flyctl CLI usage, Dockerfile-based deployments, fly.toml configuration, persistent volumes, horizontal and vertical scaling, multi-region deployments, managed Postgres and Redis, private networking, and auto-scaling strategies.

Deployment Patterns412L

monitoring-post-deploy

Comprehensive guide to post-deployment monitoring for web applications, covering uptime checks, error tracking with Sentry, application performance monitoring, log aggregation, alerting strategies, public status pages, and incident response procedures for production systems.

Deployment Patterns572L

netlify-deployment

Complete guide to deploying web applications on Netlify, covering build settings, deploy previews, serverless and edge functions, forms, identity, redirects and rewrites, split testing, and environment variable management for production workflows.

Deployment Patterns399L

railway-deployment

Complete guide to deploying applications on Railway, covering project setup, environment variable management, services and databases (Postgres, Redis, MySQL), persistent volumes, monorepo support, private networking between services, and scheduled cron jobs.

Deployment Patterns434L