My GitHub Actions CI/CD Pipeline Template for Multi-Environment Deployments

35 min read
By Atul Shukla

After countless iterations, failed deployments, and late-night debugging sessions, I've refined a GitHub Actions CI/CD pipeline that's been running reliably in production for over a year. This template handles everything from code linting to multi-environment deployments across development, staging, and production.

Today, I'm sharing the complete setup—every workflow file, secret configuration, and lesson learned the hard way.

Table of Contents

  • Why GitHub Actions?
  • Architecture Overview
  • Repository Structure
  • The Complete Workflow Files
  • Environment Configuration
  • Secrets Management
  • Advanced Patterns
  • Monitoring and Notifications
  • Common Pitfalls and Solutions

Why GitHub Actions?

Before we dive in, here's why I chose GitHub Actions over alternatives like Jenkins, GitLab CI, or CircleCI:

  • Native GitHub integration (no external service needed)
  • Free for public repos, generous free tier for private repos
  • Massive ecosystem of pre-built actions
  • Matrix builds for testing across multiple versions
  • Self-hosted runners for sensitive workloads
  • Built-in secrets management
  • Verbose YAML syntax
  • Limited to 6 hours per job
  • Debugging can be painful (but we'll fix that)
  • Cost can add up for private repos at scale

For most projects, especially those already on GitHub, the benefits far outweigh the drawbacks.

Architecture Overview

Here's the complete CI/CD flow we'll be building:

┌─────────────────────────────────────────────────────────────┐
│                    GitHub Actions Workflow                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Pull Request → Lint → Test → Build → Security Scan         │
│                             ↓                               │
│  Push to main → All Above + Deploy to Dev → E2E Tests       │
│                             ↓                               │
│  Tag (v*.*.*) → Deploy to Staging → Smoke Tests             │
│                             ↓                               │
│  Manual Approval → Deploy to Production → Health Check      │
│                             ↓                               │
│  Rollback on Failure                                        │
└─────────────────────────────────────────────────────────────┘
  • Automatic deployment to dev on every merge to main
  • Tag-based deployment to staging
  • Manual approval gate for production
  • Automatic rollback on deployment failure
  • Parallel test execution
  • Docker layer caching
  • Slack/Discord notifications
  • Deployment metrics to Prometheus

Repository Structure

.
├── .github/
│   ├── workflows/
│   │   ├── ci.yml                    # Main CI pipeline
│   │   ├── deploy.yml                # Deployment workflow
│   │   ├── reusable-test.yml         # Reusable test workflow
│   │   └── reusable-deploy.yml       # Reusable deploy workflow
│   └── actions/
│       ├── setup-node/               # Custom composite action
│       └── docker-build-push/        # Custom Docker action
├── src/
├── tests/
├── infrastructure/
│   ├── kubernetes/
│   │   ├── base/
│   │   └── overlays/
│   │       ├── dev/
│   │       ├── staging/
│   │       └── production/
│   └── terraform/
├── scripts/
│   ├── deploy.sh
│   ├── rollback.sh
│   └── health-check.sh
├── Dockerfile
├── docker-compose.yml
└── package.json

The Complete Workflow Files

1. Main CI Workflow

This runs on every pull request and push to main:

name: CI Pipeline

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main, develop]
  workflow_dispatch: # Manual trigger

env:
  NODE_VERSION: '20'
  PYTHON_VERSION: '3.11'
  GO_VERSION: '1.21'

jobs:
  # Job 1: Code Quality Checks
  lint:
    name: Lint and Format Check
    runs-on: ubuntu-latest
    timeout-minutes: 10
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - 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 ESLint
        run: npm run lint
      
      - name: Run Prettier
        run: npm run format:check
      
      - name: TypeScript type check
        run: npm run type-check

2. Reusable Deployment Workflow

This reusable workflow handles deployments to any environment:

name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
        description: 'Target environment (development, staging, production)'
      image-tag:
        required: true
        type: string
        description: 'Docker image tag to deploy'

jobs:
  deploy:
    name: Deploy to ${{ inputs.environment }}
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment }}
      url: ${{ steps.deploy.outputs.app-url }}
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup kubectl
        uses: azure/setup-kubectl@v4
        with:
          version: 'v1.28.0'
      
      - name: Deploy to Kubernetes
        id: deploy
        run: |
          kubectl apply -f k8s/
          kubectl rollout status deployment/myapp

Environment Configuration

Setting Up GitHub Environments

  • Navigate to Settings → Environments in your repository
  • Create three environments: development, staging, production

Development Environment: No protection rules, automatically deploys on merge to main

Staging Environment: Required reviewers: 1 (optional), Wait timer: 0 minutes, Deployment branches: Only protected branches + tags

Production Environment: Required reviewers: 2+ team leads, Wait timer: 30 minutes (cooling period), Deployment branches: Only tags matching v*.*.*

Secrets Management

Navigate to Settings → Secrets and variables → Actions to configure:

# Docker Registry
DOCKER_USERNAME=your_dockerhub_username
DOCKER_PASSWORD=your_dockerhub_token

# AWS Credentials
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...

# External Services
SNYK_TOKEN=...
CODECOV_TOKEN=...
SLACK_WEBHOOK=https://hooks.slack.com/services/...

# Database (per environment)
DEV_DATABASE_URL=postgres://...
STAGING_DATABASE_URL=postgres://...
PROD_DATABASE_URL=postgres://...

Using GitHub CLI to Add Secrets

# Install GitHub CLI
brew install gh  # macOS

# Authenticate
gh auth login

# Add secrets
gh secret set DOCKER_USERNAME
gh secret set DOCKER_PASSWORD
gh secret set AWS_ACCESS_KEY_ID

# Add environment-specific secrets
gh secret set DATABASE_URL --env development
gh secret set DATABASE_URL --env staging
gh secret set DATABASE_URL --env production

Advanced Patterns

Pattern 1: Conditional Deployment Based on Changed Files

Only deploy services that have changed:

name: Smart Deploy

on:
  push:
    branches: [main]

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

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

Pattern 2: Parallel Test Execution

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    
    - name: Run tests (shard ${{ matrix.shard }}/4)
      run: |
        npm run test -- --shard=${{ matrix.shard }}/4

Monitoring and Notifications

Slack Notifications

notify:
  runs-on: ubuntu-latest
  if: always()
  needs: [deploy]
  
  steps:
    - name: Slack Notification
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        text: |
          Deployment Status: ${{ job.status }}
          Commit: ${{ github.sha }}
          Author: ${{ github.actor }}
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Common Pitfalls and Solutions

Pitfall 1: Secrets Not Available in Pull Requests from Forks

Problem: Secrets aren't accessible in PRs from forks for security reasons.

Solution: Use pull_request_target with caution, or skip secret-dependent jobs for fork PRs.

Pitfall 2: Docker Build Context Too Large

Problem: Slow builds due to large context being sent to Docker daemon.

Solution: Use .dockerignore to exclude unnecessary files like node_modules, .git, coverage, etc.

Pitfall 3: Workflow Runs Taking Too Long

Problem: Jobs timing out or taking 20+ minutes.

Solution: Parallelize independent jobs instead of running them sequentially. Break test suites into parallel shards.

Pitfall 4: Caching Not Working

Problem: Cache never hits, builds still slow.

Solution: Debug cache keys by logging the hash values and verify the cache paths are correct.

Pitfall 5: Security - Exposing Secrets in Logs

Problem: Accidentally logging sensitive data.

Solution: Always mask secrets using ::add-mask:: and never echo them directly in run commands.

Complete Example: Production Node.js API

Here's everything together for a production Node.js + PostgreSQL + Redis API:

name: Production Pipeline

on:
  push:
    branches: [main]
    tags: ['v*.*.*']
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '20'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  validate:
    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 audit --production

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
      redis:
        image: redis:7-alpine
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --coverage

  build:
    needs: [validate, test]
    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Conclusion

This CI/CD pipeline template has served me well across multiple production applications. The key principles that make it work:

  • Fail fast: Run quick checks (linting, type checking) before expensive operations
  • Parallelize: Run independent jobs concurrently
  • Cache aggressively: Dependencies, build artifacts, Docker layers
  • Secure by default: Never log secrets, use environment protection rules
  • Make it observable: Notifications, metrics, deployment tracking
  • Test your rollbacks: They will save you during incidents
  • Keep it DRY: Reusable workflows reduce duplication and errors

Feel free to fork this template and adapt it to your needs. The complete code is available in my GitHub repository.

What's Next?

  • Add integration with feature flag systems (LaunchDarkly, Unleash)
  • Implement canary deployments with progressive rollout
  • Add automatic performance regression testing
  • Integrate with incident management (PagerDuty, Opsgenie)

Have questions or improvements? Drop a comment below or open an issue on GitHub!

Resources

  • GitHub Actions Documentation: https://docs.github.com/en/actions
  • Awesome Actions - Curated List: https://github.com/sdras/awesome-actions
  • GitHub Actions Samples: https://github.com/actions/starter-workflows
  • Security Best Practices: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
CI/CDGitHub ActionsDevOpsAutomationDeploymentKubernetesDockerBest Practices

Enjoyed this article?

Share it with your network!

My GitHub Actions CI/CD Pipeline Template for Multi-Environment Deployments | Atul Shukla