Module 4: Deployment Automation

Project: Complete Deployment Workflow

Project: Complete Deployment Workflow

Project overview

This project integrates everything learned in Module 4: automatic changelog, readiness validation, staging → production flow with approval gates. You'll build a working end-to-end deployment workflow, from the merge to main to the deploy to production, with notifications to the team at each stage.

It's not a theoretical exercise. The deliverable is a repository with:

  1. A working pipeline (tested at least on mock staging)
  2. Operational documentation for the team
  3. A cost and time analysis
  4. An extension plan for your real case

By completing it, you'll have a deployment template you can adapt to any professional project you touch.


Project Goal

Build a GitHub Actions workflow that runs the complete deployment cycle with Claude Code assisting at each critical step.

By completing it:

  • ✅ A pipeline executable from merge to main all the way to production
  • ✅ A changelog generated automatically with each release
  • ✅ Readiness validation before each deploy
  • ✅ Automatic deploy to staging with smoke tests
  • ✅ A human approval gate before production
  • ✅ A pre-promotion analysis that informs the reviewer
  • ✅ Post-deploy notifications to the team
  • ✅ Complete operational documentation

Technical Specifications

Project structure

deployment-pipeline-project/
├── .github/
│   └── workflows/
│       ├── release-changelog.yml        # capsule 02
│       ├── readiness-validation.yml     # capsule 03
│       └── staged-deployment.yml        # capsule 04 + integration
├── scripts/
│   ├── generate_changelog.py            # capsule 02
│   ├── validate_readiness.py            # capsule 03
│   ├── validate_staging.py              # capsule 04
│   ├── deploy.sh                         # mock deploy script
│   ├── smoke_tests.sh                    # mock smoke tests
│   ├── post_deploy_notification.py      # new
│   └── platform_helpers.py               # common helpers
├── docs/
│   ├── DEPLOYMENT.md                    # how the pipeline operates
│   ├── RUNBOOK.md                       # what to do if X fails
│   └── METRICS.md                       # cost analysis
├── CLAUDE.md
├── CHANGELOG.md
└── README.md

Initial setup

mkdir deployment-pipeline-project && cd deployment-pipeline-project
git init
git checkout -b main

# Configure GitHub
gh repo create my-deployment-project --public

# Configure environments in GitHub Settings:
# - staging (no required reviewers)
# - production (with required reviewers + branch protection)

The Integrated Workflow

# .github/workflows/staged-deployment.yml
name: Staged Deployment

on:
  push:
    branches: [main]

permissions:
  contents: read
  pull-requests: write
  deployments: write

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

env:
  PYTHON_VERSION: '3.11'

jobs:
  # ============================================
  # JOB 1: Readiness Validation
  # ============================================
  validate:
    runs-on: ubuntu-latest
    outputs:
      readiness_status: ${{ steps.validate.outputs.status }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: 'pip'
      
      - run: pip install anthropic requests
      
      - name: Run readiness validation
        id: validate
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
        run: |
          python scripts/validate_readiness.py
          if [ $? -eq 0 ]; then
            echo "status=ready" >> $GITHUB_OUTPUT
          else
            echo "status=blocked" >> $GITHUB_OUTPUT
            exit 1
          fi
      
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: readiness-report
          path: readiness_report.json

  # ============================================
  # JOB 2: Generate Changelog
  # ============================================
  generate-changelog:
    needs: validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: 'pip'
      
      - run: pip install anthropic requests
      
      - name: Generate changelog from last release
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          NEW_TAG: ${{ github.sha }}
        run: python scripts/generate_changelog.py
      
      - uses: actions/upload-artifact@v4
        with:
          name: changelog
          path: CHANGELOG_*.md

  # ============================================
  # JOB 3: Deploy to Staging
  # ============================================
  deploy-staging:
    needs: [validate, generate-changelog]
    runs-on: ubuntu-latest
    environment: staging
    outputs:
      deploy_url: ${{ steps.deploy.outputs.url }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to staging
        id: deploy
        env:
          DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
        run: |
          ./scripts/deploy.sh staging
          echo "url=https://staging.example.com" >> $GITHUB_OUTPUT
      
      - name: Smoke tests
        run: ./scripts/smoke_tests.sh https://staging.example.com
      
      - name: Validate staging metrics with Claude
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
        run: python scripts/validate_staging.py

  # ============================================
  # JOB 4: Deploy to Production (with approval gate)
  # ============================================
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # ← required reviewers configured
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to production
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
        run: ./scripts/deploy.sh production
      
      - name: Smoke tests on production
        run: ./scripts/smoke_tests.sh https://app.example.com
      
      - name: Notify team
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: python scripts/post_deploy_notification.py

Mock Script: deploy.sh

#!/bin/bash
# scripts/deploy.sh — mock deploy for testing the pipeline
set -euo pipefail

ENV="${1:?'Usage: deploy.sh <staging|production>'}"

echo "[deploy.sh] Deploying to $ENV..."
echo "[deploy.sh] Pulling latest image..."
sleep 2
echo "[deploy.sh] Running migrations..."
sleep 1
echo "[deploy.sh] Updating containers..."
sleep 2
echo "[deploy.sh] Health check..."
sleep 1
echo "[deploy.sh] Deploy to $ENV complete."

# In real production, this script would invoke:
# kubectl rollout restart deployment/app -n $ENV
# or
# terraform apply -auto-approve -var "env=$ENV"
# or your platform's specific mechanism

Make the script executable: chmod +x scripts/deploy.sh


Mock Script: smoke_tests.sh

#!/bin/bash
# scripts/smoke_tests.sh — mock smoke tests
set -euo pipefail

URL="${1:?'Usage: smoke_tests.sh <url>'}"

echo "[smoke] Testing $URL..."

# In real production, run real requests:
# curl -f $URL/health || exit 1
# curl -f $URL/api/status || exit 1

# Mock: simulate tests
PASSED=0
FAILED=0
SKIPPED=0

for test in health api status auth payments; do
    sleep 0.5
    if [ $((RANDOM % 10)) -lt 9 ]; then
        echo "  ✓ $test"
        PASSED=$((PASSED + 1))
    else
        echo "  ✗ $test (FAILED)"
        FAILED=$((FAILED + 1))
    fi
done

echo "{\"passed\": $PASSED, \"failed\": $FAILED, \"skipped\": $SKIPPED}" > smoke_results.json

if [ "$FAILED" -gt 0 ]; then
    echo "[smoke] $FAILED tests failed"
    exit 1
fi

echo "[smoke] All $PASSED tests passed"

New Script: post_deploy_notification.py

"""scripts/post_deploy_notification.py

Notifies the team of the successful deploy with a useful summary.
"""
import json
import os
import sys
from pathlib import Path
import requests
from anthropic import Anthropic


def get_changelog() -> str:
    """Read the last generated changelog."""
    changelogs = sorted(Path(".").glob("CHANGELOG_*.md"))
    if changelogs:
        return changelogs[-1].read_text()
    return "No changelog available"


def generate_notification_summary(changelog: str) -> str:
    """Generate an executive summary for Slack/email."""
    prompt = f"""Generate a brief message (maximum 150 words) to notify the
team of a successful deploy to production. The message must:

1. Start with "🚀 Deployed to production"
2. Summarize the changelog's main changes in 2-3 bullets
3. Mention whether there are breaking changes
4. Close with "Monitoring in progress. Check [dashboard link]."

CHANGELOG:
{changelog[:3000]}

Output: only the message, with no additional text.
"""
    
    client = Anthropic()
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}],
    )
    
    return response.content[0].text.strip()


def send_slack_notification(message: str, webhook: str):
    """Send to Slack via a webhook."""
    payload = {
        "text": message,
        "username": "Deploy Bot",
        "icon_emoji": ":rocket:",
    }
    
    r = requests.post(webhook, json=payload, timeout=10)
    if r.status_code != 200:
        print(f"WARNING: Slack notification failed: {r.status_code}", file=sys.stderr)


def main() -> int:
    changelog = get_changelog()
    summary = generate_notification_summary(changelog)
    
    print("--- Notification preview ---")
    print(summary)
    print("---")
    
    webhook = os.environ.get("SLACK_WEBHOOK")
    if webhook:
        send_slack_notification(summary, webhook)
        print("Notification sent to Slack")
    else:
        print("SLACK_WEBHOOK not configured, skipping notification")
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

Documentation: docs/DEPLOYMENT.md

# Deployment Pipeline

This document describes the project's deployment flow.

## When it triggers

- **Trigger:** push to `main` (typically via a PR merge)
- **Branches:** only `main` deploys. Other branches only run CI.

## Stages

### 1. Validate (~30s)
- Tests pass
- Linting + type checking
- Readiness check (env vars, migrations, deps)
- Analysis with Claude Code to detect issues

### 2. Generate Changelog (~30s)
- Lists commits since the last deploy
- Generates a structured changelog
- Uploads as an artifact

### 3. Deploy Staging (~3-5 min)
- Automatic deploy
- Smoke tests
- Metrics validation with Claude Code

### 4. Approval Gate (variable)
- Human reviewer evaluates the deploy
- Has info pre-loaded (Job 3's analysis)
- Click "Approve" or investigate

### 5. Deploy Production (~3-5 min)
- Deploy
- Smoke tests
- Notification to the team

## Expected total time

- Without issues: 8-15 minutes
- With human investigation: variable

## Who approves

Reviewers configured in the `production` environment:
- @tech-lead
- @sre-lead

Anyone can approve.

## When NOT to deploy

- Weekends (no SRE on-call)
- Before critical business events
- When staging shows persistent issues

Documentation: docs/RUNBOOK.md

# Runbook: What to Do When X Fails

## Validate Failure

**Symptoms:** The `validate` job fails with a readiness check error.

**Action:**
1. Download the `readiness-report.json` artifact
2. Identify the check that failed
3. If it's a missing env var in production: add it in Settings → Environments → production
4. If it's a migration: verify it's applied and then re-trigger

## Staging Deploy Failure

**Symptoms:** The `deploy-staging` job fails.

**Action:**
1. Look at the job logs
2. If it's an infrastructure error: re-run the job
3. If it's an application error: revert the commit on `main`, investigate offline
4. Do NOT promote to production while staging fails

## Smoke Tests Fail on Staging

**Symptoms:** Smoke tests report failures.

**Action:**
1. Verify the URL/endpoints are accessible
2. If a specific endpoint fails: review the change that affects that endpoint
3. If EVERYTHING fails: probable infrastructure issue
4. Do NOT approve production

## Approval Gate "Stuck"

**Symptoms:** The production job has been waiting a long time for approval.

**Action:**
1. Verify the reviewers are notified
2. If urgent: contact a reviewer directly
3. If nobody is available: revert the merge and retry

## Production Deploy Failure

**Symptoms:** Production fails post-deploy.

**Action:**
1. **Immediate:** trigger a rollback (manual or automatic depending on the setup)
2. Verify metrics in the monitoring dashboard
3. Postmortem
4. Do NOT re-deploy the same change without investigation

Analysis: docs/METRICS.md

# Pipeline Metrics

Pipeline data after [N runs].

## Average times

| Stage | Average | P95 |
|-------|----------|-----|
| Validate | 35s | 45s |
| Generate changelog | 25s | 40s |
| Deploy staging | 4 min | 6 min |
| Smoke tests | 90s | 2 min |
| Approval wait | 12 min | 1 hour |
| Deploy production | 4 min | 6 min |
| **Total (without wait)** | **11 min** | **17 min** |

## Estimated costs

Per run:
- Anthropic API: ~$0.05 (several calls to Claude)
- GitHub Actions runner: ~$0.04 (5 min × $0.008)
- **Total: ~$0.09 per run**

Monthly (assuming 50 deploys/month):
- ~$4.50/month in CI/CD

## Frequency

- Average deploys/week: 12
- Manual steps before vs now:
  - Before: 8 (manual changelog, manual readiness, communication, etc.)
  - Now: 1 (approval click)

## Time saved

- Per deploy: ~25 min of human time
- Monthly: ~25 min × 50 deploys = ~20 hours

ROI: ~$2K/month saved (with a developer cost of ~$100/h)

Project Deliverables

  1. A working repository with:
    • A workflow running end-to-end (at least staging)
    • Scripts (Python + bash)
    • Environment configuration
  2. Documentation:
    • docs/DEPLOYMENT.md
    • docs/RUNBOOK.md
    • docs/METRICS.md
  3. A demonstration:
    • Screenshot/video of the pipeline running
    • The complete pipeline (all jobs green)
    • The approval gate working
  4. An extension plan documented:
    • How to apply it to your real project
    • Which mock scripts to replace with real ones
    • What specific adaptations it needs

Evaluation Rubric (100 points)

Working Pipeline (40 pts)

  • ✅ (10 pts) Validate runs and blocks correctly
  • ✅ (5 pts) The changelog is generated automatically
  • ✅ (10 pts) Deploy to staging is completely automatic
  • ✅ (10 pts) The approval gate before production works
  • ✅ (5 pts) The post-deploy notification is sent

Code Quality (20 pts)

  • ✅ (8 pts) Python scripts with error handling
  • ✅ (4 pts) Appropriate exit codes
  • ✅ (4 pts) Clear logs for debugging
  • ✅ (4 pts) Clean structure (config + main + helpers)

Documentation (25 pts)

  • ✅ (10 pts) DEPLOYMENT.md explains the complete flow
  • ✅ (10 pts) RUNBOOK.md covers the main failure modes
  • ✅ (5 pts) METRICS.md has quantitative analysis

Professional Setup (10 pts)

  • ✅ (5 pts) Environments configured correctly (staging vs production)
  • ✅ (5 pts) Secrets separated per environment

Reflection and Plan (5 pts)

  • ✅ (3 pts) A plan to adapt it to a real project
  • ✅ (2 pts) Lessons learned documented

Extra Credit (+15 pts)

  • ✅ (+5 pts) Implement an additional canary deploy
  • ✅ (+3 pts) Integration with a real observability system (Datadog, Prometheus, etc.)
  • ✅ (+3 pts) Test the rollback (simulate a post-deploy failure)
  • ✅ (+2 pts) Multi-region deploy (us-east + eu-west, etc.)
  • ✅ (+2 pts) Interactive Slack bot (react with an emoji to approve)

Common Errors in the Project

1. Not testing the complete flow

Symptom: You implemented each job but never ran the pipeline end-to-end.

Why it happens: The separate jobs seem to work.

How to fix it: Make a test PR, merge it, watch the pipeline run completely. Any inconsistency comes out here.

2. Approval gate without an assigned reviewer

Symptom: The deploy-production job stays "waiting for approval" but nobody is configured.

Why it happens: You forgot to add required reviewers to the production environment.

How to fix it: Settings → Environments → production → Required reviewers. Add yourself for testing.

3. Non-executable scripts

Symptom: ./scripts/deploy.sh: Permission denied.

Why it happens: Bash scripts need the +x flag.

How to fix it:

chmod +x scripts/*.sh
git update-index --chmod=+x scripts/*.sh
git commit -am "fix: make scripts executable"

4. Superficial documentation

Symptom: RUNBOOK.md has "what to do if it fails: investigate".

Why it happens: Documentation written quickly without thinking about real cases.

How to fix it: For each failure mode, write concrete steps: exact command, where to look at logs, who to contact. The runbook must be usable under pressure.

5. Ignoring costs in METRICS

Symptom: The project is complete but you didn't analyze how much it costs.

Why it happens: Costs seem secondary when something works.

How to fix it: Calculate cost per run, monthly, ROI vs the manual process. It's what justifies the pipeline to the team.


Final Reflection

When you finish this project, you have a professional template of deployment automation that:

  1. Applies the 6 modules of the path (CI/CD, code review, SDK, scripts, secrets, observability)
  2. Is portfolio-worthy — it demonstrates senior-level thinking about deployment
  3. Is transferable — the pattern applies to any project

The most important thing: you learned to balance automation with human judgment. Automation eliminates repetitive work. The human adds context and authority. That balance is the difference between a pipeline the team trusts and one the team disables.


Connection with Module 5

The next module adds post-deploy resilience: what happens when production fails? Automatic rollback, security scanning as an additional gate, smart diagnosis of incidents. Your current pipeline deploys correctly; Module 5 makes it resistant to failures.


Resources for the Project

  1. GitHub Actions Workflow Examples — Official templates
  2. The DevOps Handbook — A classic reference
  3. Kubernetes Deployment Strategies — If you use K8s
  4. Slack Incoming Webhooks — For notifications
  5. Datadog GitHub Integration — Modern observability
  6. Anthropic API Best Practices — Applicable to deployment