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.jsonThe 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-check2. 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/myappEnvironment 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 productionAdvanced 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 }}/4Monitoring 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=maxConclusion
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