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.
```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 linesContinuous 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
- Repository secrets: Available to all workflows
- Environment secrets: Scoped to a specific environment
- 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
- No concurrency control: Multiple deploys running simultaneously can cause race conditions. Always use
concurrencygroups. - Hardcoded secrets in workflows: Even in
env:blocks at the step level, use${{ secrets.X }}, never literal values. - No rollback plan: If your workflow only goes forward, you're stuck when things break.
- Skipping tests before deploy: The
needs: testdependency is critical. - Not using
fail-fast: falsein multi-region deploys: One region failing shouldn't cancel others. - Caching
node_modulesdirectly: Cache the npm/pnpm/yarn cache directory, notnode_modules. Use the built-incacheoption insetup-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
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.
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.
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.
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.
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.
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.