What You'll Build
You will configure a complete three-environment deployment pipeline for CricketPulse: dev (automatic), staging (single reviewer), and production (two reviewers, main branch only, 2-minute wait timer). You will create all three environments in GitHub Settings, configure their protection rules, and write a deployment workflow that flows through all three stages. The exercise includes deliberately approving and rejecting a deployment to experience the reviewer interface, and observing how the deployment history audit trail populates as each stage completes. By the end you will have a production-grade deployment governance system that mirrors the practices at organisations deploying to live user traffic.
Prerequisites
- The CricketPulse repository from previous exercises with an existing `.github/workflows/` directory.
- GitHub account with admin access to the repository — environment creation requires repository admin or owner permissions.
- Optionally: a second GitHub account or a colleague's GitHub account to act as the production approver (you can also approve your own deployments if you configure yourself as the required reviewer).
- Understanding of environment secrets and `environment:` key in workflow YAML from Lessons 13–14.
- The container image from Lesson 12 in `ghcr.io` (optional — the deploy step can be a simulated `echo` if the image isn't available).
Setup & Project Structure
Create the three environments in GitHub Settings and configure their protection rules before writing the workflow. Environment configuration is entirely in the GitHub UI — there is no YAML for environment protection rules. The workflow YAML simply references the environment by name, and GitHub enforces whatever rules are configured for that environment.
# GitHub Settings configuration steps (done in the browser, not in YAML)
# 1. Navigate to: github.com/YOUR_USERNAME/cricketpulse/settings/environments
# 2. Create 'dev' environment:
# Click 'New environment' → Name: dev
# Protection rules: (leave all unchecked — automatic)
# Variables → Add variable: DEPLOY_URL = https://dev.cricketpulse.example.com
# Click 'Save protection rules'
# 3. Create 'staging' environment:
# Click 'New environment' → Name: staging
# Required reviewers: Add yourself (or a colleague)
# Deployment branches: Select 'Selected branches' → Add rule: main
# Variables → Add variable: DEPLOY_URL = https://staging.cricketpulse.example.com
# Wait timer: 0 minutes
# Click 'Save protection rules'
# 4. Create 'production' environment:
# Click 'New environment' → Name: production
# Required reviewers: Add yourself + one other (or just yourself twice won't work — add 2 different accounts)
# Deployment branches: Select 'Selected branches' → Add rule: main
# Wait timer: 2 minutes
# Variables → Add variable: DEPLOY_URL = https://cricketpulse.example.com
# Click 'Save protection rules'
# Verify environments are created:
# Settings → Environments should show: dev, staging, production
echo 'Environments created in GitHub Settings — proceed to workflow creation'Step 1 — Foundation
Create the workflow file with the build job and the dev deployment job. Push to main and observe the workflow trigger — the dev deployment should run automatically without any approval gate, confirming the environment is correctly configured with no protection rules.
# File: .github/workflows/cricketpulse-release.yml — Step 1
name: CricketPulse Release Pipeline
on:
push:
branches: [main]
workflow_dispatch:
inputs:
deploy-to-production:
description: 'Deploy through to production after staging?'
required: false
type: boolean
default: false
reason:
description: 'Deployment reason'
required: false
type: string
concurrency:
group: release-${{ github.sha }}
cancel-in-progress: false
jobs:
build:
name: Build CricketPulse
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm test
- id: ver
run: echo "version=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: cricketpulse-${{ github.sha }}
path: dist/
if-no-files-found: warn
deploy-dev:
name: Deploy to Dev
runs-on: ubuntu-latest
needs: build
environment: dev # No protection rules — runs immediately
steps:
- name: Deploy to dev environment
run: |
echo "=== CricketPulse Dev Deployment ==="
echo "Version: ${{ needs.build.outputs.version }}"
echo "URL: ${{ vars.DEPLOY_URL }}"
echo "Deployer: ${{ github.actor }}"
echo "SHA: ${{ github.sha }}"
echo "Status: Deployed successfully (simulated)"Step 2 — Core Logic
Add the staging deployment job that depends on dev and requires approval. Push to main and observe the workflow pause after dev deployment completes, entering the `waiting` state for staging. Use the GitHub UI to approve the staging deployment and observe the job resume. This approval experience is the core of the exercise.
# Step 2: Add staging job to the workflow (append to jobs section)
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [build, deploy-dev]
environment: staging # Requires reviewer approval
steps:
- uses: actions/download-artifact@v4
with:
name: cricketpulse-${{ github.sha }}
path: dist/
continue-on-error: true # dist/ may not exist for non-build projects
- name: Deploy to staging environment
run: |
echo "=== CricketPulse Staging Deployment ==="
echo "Version: ${{ needs.build.outputs.version }}"
echo "URL: ${{ vars.DEPLOY_URL }}"
echo "Deployer: ${{ github.actor }}"
echo "Approved by: $(echo '${{ github.actor }}') (reviewer)"
echo "Status: Deployed to staging successfully (simulated)"
- name: Run smoke tests against staging
run: |
echo "Smoke test: GET ${{ vars.DEPLOY_URL }}/health → 200 OK (simulated)"
echo "Smoke test: GET ${{ vars.DEPLOY_URL }}/matches → 200 OK (simulated)"
echo "All smoke tests passed"Step 3 — Integration & Enhancement
Add the production deployment job with the 2-minute wait timer. After the staging deployment completes, the production gate activates — requiring approval, then waiting 2 minutes before executing. Observe the full pipeline timeline: build → dev (auto) → staging (approval wait) → production (approval + 2-min timer). The complete pipeline demonstrates the escalating protection levels as deployments approach production.
# Step 3: Add production job and final summary (complete workflow)
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [build, deploy-staging]
# Only deploy to production if manually dispatched with the flag
if: |
github.event_name == 'workflow_dispatch' &&
inputs.deploy-to-production == true
environment: production # 2 required reviewers + 2-min wait timer
steps:
- uses: actions/download-artifact@v4
with:
name: cricketpulse-${{ github.sha }}
path: dist/
continue-on-error: true
- name: Validate production deployment reason
run: |
REASON="${{ inputs.reason }}"
if [[ -z "$REASON" || ${#REASON} -lt 5 ]]; then
echo "ERROR: A meaningful deployment reason is required for production"
exit 1
fi
- name: Deploy to production
run: |
echo "=== CricketPulse Production Deployment ==="
echo "Version: ${{ needs.build.outputs.version }}"
echo "URL: ${{ vars.DEPLOY_URL }}"
echo "Deployer: ${{ github.actor }}"
echo "SHA: ${{ github.sha }}"
echo "Reason: ${{ inputs.reason }}"
echo "Status: Deployed to production successfully (simulated)"
deployment-summary:
name: Deployment Summary
runs-on: ubuntu-latest
needs: [deploy-dev, deploy-staging, deploy-production]
if: always()
steps:
- name: Write deployment summary
run: |
cat >> $GITHUB_STEP_SUMMARY << 'SUMMARY'
## 🏏 CricketPulse Release Summary
| Environment | Status |
|-------------|--------|
| Dev | ${{ needs.deploy-dev.result }} |
| Staging | ${{ needs.deploy-staging.result }} |
| Production | ${{ needs.deploy-production.result }} |
**Version:** ${{ github.sha }}
**Triggered by:** ${{ github.actor }}
SUMMARYStep 4 — Testing & Verification
Test three scenarios: automatic push (dev + staging gate, no production), manual dispatch with production enabled (full three-stage pipeline), and a deliberately rejected staging deployment (observe the rejection flow and how the pipeline stops).
# Test scenarios
# SCENARIO 1: Automatic push — dev auto-deploys, staging waits for approval
git add .github/workflows/cricketpulse-release.yml
git commit -m 'feat: add three-stage gated release pipeline'
git push origin main
# Expected flow:
# build ✅ → deploy-dev ✅ (automatic) → deploy-staging ⏸️ (waiting for approval)
# Go to: github.com/YOUR_USERNAME/cricketpulse/actions
# Click the running workflow → click 'Review pending deployments'
# Approve staging → observe deploy-staging resume and complete
# deploy-production = skipped (push event, not workflow_dispatch)
# SCENARIO 2: Manual dispatch to production
# Go to: Actions → CricketPulse Release Pipeline → Run workflow
# Inputs: deploy-to-production = true, reason = 'Add live score feature for IPL 2024'
# Watch full three-stage pipeline run:
# build ✅ → dev ✅ → staging ⏸️ (approve) → production ⏸️ (approve + 2-min wait)
# SCENARIO 3: Reject staging deployment
# Trigger another push, then click 'Review pending deployments' on staging
# Click 'Reject' with a comment
# Expected: deploy-staging ❌ → deploy-production skipped → summary shows staging failed
# VERIFY DEPLOYMENT HISTORY:
# github.com/YOUR_USERNAME/cricketpulse/deployments
# Should show all environments with their deployment recordsWarning: If you configure yourself as the sole required reviewer, GitHub may still allow you to approve your own deployments — but this defeats the purpose of the approval gate. For meaningful two-person integrity, ensure the required reviewer is a different GitHub account. If a second account isn't available for the exercise, accept this limitation and note it: in a real project, required reviewers must be different from the person who triggered the deployment.
Extension Challenge: Add a Slack notification step to the `deployment-summary` job that posts the deployment summary table to a Slack channel using a webhook URL stored as a repository secret. The notification should include clickable links to the workflow run URL (`${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`). This mirrors the deployment notification pattern used by teams to keep stakeholders informed of release status without requiring them to monitor the GitHub Actions UI.
- Environments are created and configured entirely in GitHub Settings — protection rules (reviewers, branch restrictions, wait timer) are UI configuration, not YAML configuration.
- The reviewer approval UI in GitHub Actions shows a 'Review pending deployments' button on the in-progress workflow run — approvers click this to see pending gates and approve or reject them.
- A rejected deployment gate stops all subsequent jobs in the pipeline — the deployment record shows 'failure' for the rejected environment and all downstream jobs are skipped.
- The deployment history at `github.com/OWNER/REPO/deployments` provides a complete, immutable audit trail of every deployment to every environment including approver identity and timestamps.
- The 2-minute wait timer starts after the required approvals are collected — it is visible as a countdown in the job's pending state, giving approvers a window to abort before execution begins.
- Combining `workflow_dispatch` with `inputs.deploy-to-production == true` as a gate means the production stage only runs on explicit manual dispatch with that flag set — pushing to main never reaches production automatically.