You will build a complete, production-grade CI/CD pipeline for CricketPulse, integrating every major concept from this course into a single cohesive system. The pipeline will span six workflow files coordinating build, test, security scanning, container publication, blue-green Kubernetes deployment, and progressive canary delivery — all governed by environment protection gates and a manual rollback capability. This is not a simplified demo: the pipeline you build here is structurally identical to what engineering teams at production-scale SaaS companies run for their primary services. By completing this capstone, you will have a working reference implementation that you can adapt and deploy for real projects.
Prerequisites
- Completed exercises from Modules 1–5 — the CricketPulse repository with working CI, container build, and gated environment workflows.
- GitHub Container Registry configured and accessible for your repository at `ghcr.io`.
- A Kubernetes cluster with three namespaces: `cricketpulse-dev`, `cricketpulse-staging`, `cricketpulse-prod` — can be a local kind/minikube cluster or any managed Kubernetes service.
- All three GitHub environments configured (dev, staging, production) with appropriate protection rules from the Lesson 16 exercise.
- Understanding of all course modules: pipeline triggers, caching, artifacts, container builds, secrets, environments, Kubernetes deployment, blue-green, canary, matrix builds, and reusable workflows.
The complete CricketPulse pipeline consists of six coordinated workflow files. The `ci.yml` workflow handles all CI concerns: checkout, dependency caching, lint, unit test, integration test, and security scanning. The `container.yml` workflow builds, scans, and publishes the container image to ghcr.io. The `deploy-dev.yml` workflow automatically deploys to dev on every successful container publish. The `deploy-staging.yml` workflow uses a blue-green strategy for staging deployments with an approval gate. The `deploy-production.yml` workflow implements canary progressive delivery to production, gated by two required reviewers. The `rollback.yml` workflow provides emergency instant rollback via manual dispatch for all environments.
# Complete pipeline architecture diagram (as comments)
#
# TRIGGER WORKFLOW ACTION
# ─────────────────────────────────────────────────────────────────
# push to main ──► ci.yml lint + test + security scan
# │
# ▼ (on success)
# container.yml build + scan + push to ghcr.io
# │
# ▼ (on success, via workflow_call)
# deploy-dev.yml kubectl apply → dev namespace
# │
# workflow_dispatch ──────► deploy-staging.yml
# approval gate (1 reviewer)
# blue-green to staging
# smoke tests
# │
# workflow_dispatch ──────► deploy-production.yml
# approval gate (2 reviewers)
# canary 10% → 50% → 100%
# automatic rollback on error rate
# │
# workflow_dispatch ──────► rollback.yml
# instant traffic revert
# all environments
#
# REUSABLE WORKFLOWS (.github/workflows/)
# _deploy-base.yml — shared deploy step called by all deploy workflows
# _notify.yml — shared Slack notification called by all workflows
echo 'Architecture defined — proceeding to implementation'Phase 1 — CI Pipeline
Build the comprehensive CI workflow with parallel test jobs, dependency caching, and security scanning. Use a matrix strategy for Node.js version compatibility testing and upload test results as artifacts for the container workflow to consume as a prerequisite.
# File: .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, 'feature/**', 'release/**']
pull_request:
branches: [main]
jobs:
lint:
name: Lint
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 run lint
test:
name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18', '20', '22']
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node-version }}, cache: 'npm' }
- run: npm ci
- run: npm test -- --reporter=junit --output-file=test-results-${{ matrix.node-version }}.xml
continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-node-${{ matrix.node-version }}
path: test-results-${{ matrix.node-version }}.xml
security:
name: Security Scan (CodeQL)
runs-on: ubuntu-latest
permissions: { contents: read, security-events: write }
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with: { languages: javascript }
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
ci-gate:
name: CI Gate
runs-on: ubuntu-latest
needs: [lint, test, security]
if: always()
steps:
- name: Verify all CI jobs passed
run: |
if [[ "${{ needs.lint.result }}" != "success" ||
"${{ needs.test.result }}" != "success" ||
"${{ needs.security.result }}" != "success" ]]; then
echo "CI gate failed — one or more checks did not pass"
exit 1
fi
echo "All CI checks passed — pipeline cleared for container build"Phase 2 — Container Build and Publish
Build the container workflow with multi-stage Dockerfile, Trivy vulnerability scanning, multi-platform build (amd64 + arm64), and semantic tagging. This workflow is triggered by the CI gate passing and publishes the verified image to ghcr.io for consumption by all deployment workflows.
# File: .github/workflows/container.yml
name: Container Build & Publish
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
jobs:
build-publish:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
permissions: { contents: read, packages: write, security-events: write }
outputs:
image-digest: ${{ steps.push.outputs.digest }}
image-tag: ${{ github.event.workflow_run.head_sha }}
steps:
- uses: actions/checkout@v4
with: { ref: '${{ github.event.workflow_run.head_sha }}' }
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/cricketpulse
tags: |
type=sha,prefix=sha-
type=raw,value=latest
- name: Build for scanning
uses: docker/build-push-action@v5
with:
context: .
load: true
tags: cricketpulse:scan
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: cricketpulse:scan
format: sarif
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: '1'
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: trivy.sarif }
- id: push
name: Push multi-platform image
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=ghaPhase 3 — Deployment Workflows
Build the three deployment workflows — dev (automatic, no gate), staging (blue-green, single reviewer), and production (canary progressive, two reviewers) — plus the emergency rollback workflow. Each deployment workflow calls a shared reusable workflow `_deploy-base.yml` for the common kubectl setup steps, eliminating duplication across the three deploy workflows.
# File: .github/workflows/_deploy-base.yml (reusable workflow)
name: Deploy Base
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
namespace:
required: true
type: string
secrets:
KUBECONFIG_B64:
required: true
outputs:
deploy-status:
value: ${{ jobs.deploy.outputs.status }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
status: ${{ steps.deploy.outcome }}
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
with: { version: 'v1.29.0' }
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- id: deploy
name: Deploy image to ${{ inputs.environment }}
run: |
IMAGE="ghcr.io/${{ github.repository }}/cricketpulse:sha-${{ inputs.image-tag }}"
kubectl set image deployment/cricketpulse \
cricketpulse=${IMAGE} \
-n ${{ inputs.namespace }}
kubectl rollout status deployment/cricketpulse \
-n ${{ inputs.namespace }} \
--timeout=300s
echo "Deployed ${IMAGE} to ${{ inputs.environment }}"
---
# File: .github/workflows/deploy-dev.yml
name: Deploy Dev
on:
workflow_run:
workflows: [Container Build & Publish]
types: [completed]
branches: [main]
jobs:
deploy:
uses: ./.github/workflows/_deploy-base.yml
if: github.event.workflow_run.conclusion == 'success'
with:
environment: dev
image-tag: ${{ github.event.workflow_run.head_sha }}
namespace: cricketpulse-dev
secrets:
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_DEV }}# File: .github/workflows/deploy-staging.yml
name: Deploy Staging (Blue-Green)
on:
workflow_dispatch:
inputs:
image-tag:
description: 'Git SHA to deploy'
required: true
type: string
reason:
description: 'Deployment reason'
required: true
type: string
concurrency:
group: deploy-staging
cancel-in-progress: false
jobs:
blue-green-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
with: { version: 'v1.29.0' }
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 -d > ~/.kube/config
- name: Detect active slot
id: slots
run: |
ACTIVE=$(kubectl get svc cricketpulse -n cricketpulse-staging \
-o jsonpath='{.spec.selector.version}' 2>/dev/null || echo 'blue')
INACTIVE=$([[ "$ACTIVE" == "blue" ]] && echo 'green' || echo 'blue')
echo "active=$ACTIVE" >> $GITHUB_OUTPUT
echo "inactive=$INACTIVE" >> $GITHUB_OUTPUT
- name: Deploy to inactive slot (${{ steps.slots.outputs.inactive }})
run: |
IMAGE="ghcr.io/${{ github.repository }}/cricketpulse:sha-${{ inputs.image-tag }}"
kubectl set image deployment/cricketpulse-${{ steps.slots.outputs.inactive }} \
cricketpulse=${IMAGE} -n cricketpulse-staging
kubectl rollout status \
deployment/cricketpulse-${{ steps.slots.outputs.inactive }} \
-n cricketpulse-staging --timeout=300s
- name: Smoke test inactive slot
run: |
kubectl port-forward \
deployment/cricketpulse-${{ steps.slots.outputs.inactive }} \
8080:3000 -n cricketpulse-staging &
sleep 3
curl -sf http://localhost:8080/health
echo "Smoke tests passed"
- name: Switch traffic to ${{ steps.slots.outputs.inactive }}
run: |
kubectl patch svc cricketpulse -n cricketpulse-staging \
-p '{"spec":{"selector":{"version":"${{ steps.slots.outputs.inactive }}"}}}'
echo "Staging traffic → ${{ steps.slots.outputs.inactive }} (sha: ${{ inputs.image-tag }})"# File: .github/workflows/deploy-production.yml
name: Deploy Production (Canary)
on:
workflow_dispatch:
inputs:
image-tag:
description: 'Git SHA to deploy'
required: true
type: string
reason:
description: 'Deployment reason (required)'
required: true
type: string
canary-steps:
description: 'Canary traffic steps'
required: false
type: choice
options: ['10,50,100', '5,25,50,100', '10,100']
default: '10,50,100'
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
canary-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
with: { version: 'v1.29.0' }
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > ~/.kube/config
- name: Progressive canary rollout
run: |
IMAGE="ghcr.io/${{ github.repository }}/cricketpulse:sha-${{ inputs.image-tag }}"
STEPS="${{ inputs.canary-steps }}"
TOTAL_REPLICAS=$(kubectl get deploy cricketpulse -n cricketpulse-prod \
-o jsonpath='{.spec.replicas}')
IFS=',' read -ra PERCENTAGES <<< "$STEPS"
for PCT in "${PERCENTAGES[@]}"; do
CANARY_COUNT=$(( (TOTAL_REPLICAS * PCT + 99) / 100 ))
echo "=== Canary step: ${PCT}% (${CANARY_COUNT}/${TOTAL_REPLICAS} replicas) ==="
kubectl set image deployment/cricketpulse \
cricketpulse=${IMAGE} -n cricketpulse-prod
kubectl scale deployment/cricketpulse \
--replicas=${CANARY_COUNT} -n cricketpulse-prod
kubectl rollout status deployment/cricketpulse \
-n cricketpulse-prod --timeout=120s
# Error rate check after each step
echo "Checking error rate at ${PCT}% traffic..."
# In production: query Prometheus/Datadog metrics here
# Placeholder: always passes in this exercise
echo "Error rate: 0.2% — within threshold. Proceeding."
if [[ "$PCT" != "100" ]]; then
echo "Waiting 60s at ${PCT}% before next step..."
sleep 60
fi
done
# Final: scale to full replica count
kubectl scale deployment/cricketpulse \
--replicas=${TOTAL_REPLICAS} -n cricketpulse-prod
echo "Canary rollout complete — 100% traffic on sha: ${{ inputs.image-tag }}"
deployment-record:
runs-on: ubuntu-latest
needs: canary-deploy
if: always()
steps:
- name: Write to job summary
run: |
cat >> $GITHUB_STEP_SUMMARY << SUMMARY
## 🏏 CricketPulse Production Deployment
- **SHA**: ${{ inputs.image-tag }}
- **Reason**: ${{ inputs.reason }}
- **Canary steps**: ${{ inputs.canary-steps }}
- **Deployer**: ${{ github.actor }}
- **Result**: ${{ needs.canary-deploy.result }}
- **Time**: $(date -u)
SUMMARY# File: .github/workflows/rollback.yml
name: Emergency Rollback
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to rollback'
required: true
type: choice
options: [dev, staging, production]
reason:
description: 'Rollback reason / incident reference'
required: true
type: string
concurrency:
group: rollback-${{ inputs.environment }}
cancel-in-progress: false
jobs:
rollback:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: azure/setup-kubectl@v4
with: { version: 'v1.29.0' }
- name: Configure kubectl
env:
KUBECONFIG_DEV: ${{ secrets.KUBECONFIG_DEV }}
KUBECONFIG_STAGING: ${{ secrets.KUBECONFIG_STAGING }}
KUBECONFIG_PRODUCTION: ${{ secrets.KUBECONFIG_PRODUCTION }}
run: |
mkdir -p ~/.kube
case "${{ inputs.environment }}" in
dev) echo "$KUBECONFIG_DEV" | base64 -d > ~/.kube/config ;;
staging) echo "$KUBECONFIG_STAGING" | base64 -d > ~/.kube/config ;;
production) echo "$KUBECONFIG_PRODUCTION" | base64 -d > ~/.kube/config ;;
esac
chmod 600 ~/.kube/config
- name: Rollback previous deployment
run: |
NS="cricketpulse-${{ inputs.environment }}"
echo "=== ROLLBACK INITIATED ==="
echo "Environment: ${{ inputs.environment }}"
echo "Reason: ${{ inputs.reason }}"
echo "Initiated by: ${{ github.actor }}"
kubectl rollout undo deployment/cricketpulse -n ${NS}
kubectl rollout status deployment/cricketpulse -n ${NS} --timeout=120s
echo "Rollback complete — reverted to previous image"Phase 4 — Validation & Portfolio Documentation
With all six workflow files in place, validate the complete pipeline end-to-end and document the architecture in the repository README for portfolio presentation. A working CI/CD pipeline with documented architecture demonstrates engineering maturity that is immediately recognisable to hiring teams and senior engineers reviewing your work.
# Validation checklist — run through each pipeline stage
# 1. Trigger CI on a feature branch push
git checkout -b feature/test-pipeline
echo '// Pipeline test' >> src/server.js
git add . && git commit -m 'test: trigger full pipeline validation'
git push origin feature/test-pipeline
# Expected: CI runs (lint + test matrix + security) — NO container build on feature branch
# 2. Merge to main
git checkout main && git merge feature/test-pipeline
git push origin main
# Expected cascade:
# CI ✅ → Container Build ✅ → Deploy Dev ✅ (automatic)
# deploy-staging and deploy-production wait for manual dispatch
# 3. Dispatch staging deployment
# gh workflow run deploy-staging.yml \
# -f image-tag=$(git rev-parse HEAD) \
# -f reason='Pipeline validation — post-capstone'
# Expected: approval gate → blue-green → smoke tests → traffic switch
# 4. Dispatch production deployment (canary)
# gh workflow run deploy-production.yml \
# -f image-tag=$(git rev-parse HEAD) \
# -f reason='Capstone validation deploy' \
# -f canary-steps='10,100'
# Expected: 2 approvers → canary 10% → 60s wait → 100%
# 5. Test rollback
# gh workflow run rollback.yml \
# -f environment=staging \
# -f reason='Rollback test — INC-TEST-001'
# Expected: approval gate → kubectl rollout undo → status check
# 6. Verify deployment history
# Visit: github.com/YOUR_USERNAME/cricketpulse/deployments
# Should show entries for all 3 environments with timestamps and approver info
echo 'Pipeline validation complete'# README.md addition — pipeline architecture documentation
cat >> README.md << 'EOF'
## CI/CD Pipeline Architecture
CricketPulse uses a six-workflow CI/CD pipeline on GitHub Actions:
| Workflow | Trigger | Environment | Strategy |
|----------|---------|-------------|----------|
| `ci.yml` | Push / PR | — | Parallel matrix (Node 18/20/22) + CodeQL |
| `container.yml` | CI passes on main | — | Multi-platform build + Trivy scan + ghcr.io |
| `deploy-dev.yml` | Container published | dev | Automatic kubectl rollout |
| `deploy-staging.yml` | Manual dispatch | staging (1 reviewer) | Blue-green |
| `deploy-production.yml` | Manual dispatch | production (2 reviewers) | Canary progressive |
| `rollback.yml` | Manual dispatch | any | kubectl rollout undo |
### Deployment Flow
```
push to main → CI → Container Build → Dev (auto)
↓ (manual dispatch)
Staging (blue-green, 1 reviewer)
↓ (manual dispatch)
Production (canary 10→50→100%, 2 reviewers)
```
### Environment Protection
- **dev**: No protection rules — automatic deployment
- **staging**: 1 required reviewer, main branch only
- **production**: 2 required reviewers, main branch only, 2-minute wait timer
EOF
git add README.md && git commit -m 'docs: add CI/CD pipeline architecture documentation'
git push origin mainWarning: The canary rollout in this capstone uses a simulated error rate check (`echo "Error rate: 0.2%"`). In production, replace this with a real metrics query — Prometheus (`promtool query instant`), Datadog (`curl https://api.datadoghq.com/api/v1/query`), or any observability platform that exposes API access. The pipeline structure is correct; the metrics integration is environment-specific. Without real metrics, the canary step cannot automatically abort on elevated error rates — always add real error rate monitoring before using canary releases with live user traffic.
Portfolio Tip: Push your completed CricketPulse repository to a public GitHub repository. The pipeline architecture, the six workflow files, and the deployment history are immediately visible to any engineering team reviewing your profile. A working CI/CD pipeline with container builds, blue-green deployments, canary releases, and gated environments is concrete evidence of production-level CI/CD engineering — more compelling than any certification or list of technologies on a resume.
What You've Built
The CricketPulse pipeline you've built over this course is production-grade. It implements every major CI/CD pattern used by engineering teams at companies like GitHub, Stripe, Shopify, and Netflix: parallel matrix testing, dependency caching, multi-stage container builds, vulnerability scanning, environment-gated deployments, blue-green zero-downtime releases, canary progressive delivery, and emergency rollback. Each component was learned in isolation across 23 lessons and assembled here into a cohesive system. This is what CI/CD engineering looks like at scale: not a single YAML file with a few steps, but a pipeline architecture with coordinated workflows, reusable components, graduated deployment strategies, and human governance gates calibrated to the consequence of each environment.
- A production CI/CD pipeline is a coordinated system of multiple workflow files, each handling a distinct concern — CI, container build, environment-specific deployment, and rollback — not a single monolithic YAML.
- Reusable workflows (`workflow_call`) eliminate duplication across deployment workflows — the shared kubectl setup, kubeconfig handling, and rollout verification run from a single source of truth.
- The deployment strategy escalates with environment risk: automatic for dev, blue-green for staging, canary for production — each strategy calibrated to the acceptable risk of that environment.
- `workflow_run` triggers chain workflows across files (CI → container → deploy-dev) without requiring all steps in a single workflow, enabling separation of concerns and independent reruns.
- Emergency rollback via `kubectl rollout undo` reverts to the previous ReplicaSet without redeploying — taking 30–60 seconds versus 5+ minutes for a fresh deployment.
- Documenting pipeline architecture in the repository README transforms a technical implementation into a portable portfolio artefact that demonstrates engineering maturity to any reviewing team.