| Strategy |
How |
Best For |
Risk |
| Rolling |
Replace instances gradually |
Most deployments |
Low |
| Blue-Green |
Two identical environments, switch traffic |
Zero-downtime, easy rollback |
Medium (2x resources) |
| Canary |
Route % of traffic to new version |
High-traffic, risk-sensitive |
Low (gradual) |
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- run: npm run build
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/deploy.sh
- run: curl -f https://myapp.com/health || exit 1
### CI/CD anti-patterns to catch in review
- **Don't pass secrets across job boundaries.** CI systems strip masked values when they cross jobs (GitHub Actions logs `Skip output '<X>' since it may contain secret` and the downstream job receives an empty string with no error). Resolve the secret inside the job that uses it, even if that duplicates a setup step. Cross-job outputs are fine for non-sensitive values only.
- **Pick the runner that can actually reach the target.** Before writing CI tests against a deployed service, check whether the service has a public ingress. If it doesn't, the runner must live inside the same network (self-hosted, in-cluster, VPC-attached) — the default cloud-vendor runner can't resolve cluster-internal DNS. Don't add a public route just to make CI reachable; move CI to the network instead.
- **Confirm the deployed image SHA before drawing conclusions from a post-deploy test.** When a workflow runs a load test, perf benchmark, or smoke test after a deploy job in the same pipeline, the deploy rollout may still be in progress when the test starts. The test then runs against the previous image while the artifact is labeled "post-merge" — false confidence that the change had no effect. Before any post-deploy assertion, confirm the rollout is complete: `kubectl rollout status deploy/<name> --timeout=5m`, OR assert that the deployed image tag matches the head SHA (`kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[0].image}'`), OR pull the running image from observability (Groundcover/Datadog/k8s API) and assert it matches. Either gate the test on rollout-status or fail the test if the image SHA doesn't match. Skipping the confirmation collapses half the test's value into a meaningless number.
- **Reusable workflows can't read another private repo with the default token.** When a reusable workflow (or a called workflow) needs files from its own private home repo but runs in a *different* private repo, the default `GITHUB_TOKEN` is scoped to the calling repo and the checkout fails ("Repository not found"). Fix one of two ways: (a) `actions/checkout` the home repo with an explicit PAT or GitHub App token stored as a secret, and have callers add `secrets: inherit`; or (b) ship the logic as a **composite action** — actions are auto-fetched by `uses:`, so no token is required. Prefer the composite action when the logic is self-contained; it removes the credential entirely.
</github_actions>
<health_checks>
```python
# FastAPI
@app.get("/health")
async def health():
return {"status": "ok", "version": settings.VERSION}
@app.get("/health/ready")
async def readiness():
# Check dependencies
await db.execute("SELECT 1")
return {"status": "ready"}
Kubernetes Probes
livenessProbe:
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet: { path: /health/ready, port: 8000 }
initialDelaySeconds: 5
periodSeconds: 10
Rollback checklist:
Operations:
1---2name: deployment-patterns3description: CI/CD, health checks, and rollback patterns. Use when deploying, setting up CI/CD, or writing GitHub Actions.4---56<objective>7Production deployment best practices: strategies, CI/CD pipelines, health checks, rollback, and readiness checklists. References `~/.claude/rules/infrastructure/RULE.md` for Docker and environment conventions.8</objective>910<when_to_activate>11- Setting up CI/CD pipelines12- Writing GitHub Actions workflows13- Configuring health checks14- Planning deployment strategy15- Pre-production readiness review16</when_to_activate>1718<deployment_strategies>1920| Strategy | How | Best For | Risk |21|----------|-----|----------|------|22| **Rolling** | Replace instances gradually | Most deployments | Low |23| **Blue-Green** | Two identical environments, switch traffic | Zero-downtime, easy rollback | Medium (2x resources) |24| **Canary** | Route % of traffic to new version | High-traffic, risk-sensitive | Low (gradual) |2526</deployment_strategies>2728<github_actions>29```yaml30name: CI/CD31on:32 push:33 branches: [main]34 pull_request:3536jobs:37 test:38 runs-on: ubuntu-latest39 steps:40 - uses: actions/checkout@v441 - uses: actions/setup-node@v442 with: { node-version: 20 }43 - run: npm ci44 - run: npm run lint45 - run: npm test -- --coverage46 - run: npm run build4748 deploy:49 needs: test50 if: github.ref == 'refs/heads/main'51 runs-on: ubuntu-latest52 steps:53 - uses: actions/checkout@v454 - run: ./scripts/deploy.sh55 - run: curl -f https://myapp.com/health || exit 156```5758### CI/CD anti-patterns to catch in review5960- **Don't pass secrets across job boundaries.** CI systems strip masked values when they cross jobs (GitHub Actions logs `Skip output '<X>' since it may contain secret` and the downstream job receives an empty string with no error). Resolve the secret inside the job that uses it, even if that duplicates a setup step. Cross-job outputs are fine for non-sensitive values only.61- **Pick the runner that can actually reach the target.** Before writing CI tests against a deployed service, check whether the service has a public ingress. If it doesn't, the runner must live inside the same network (self-hosted, in-cluster, VPC-attached) — the default cloud-vendor runner can't resolve cluster-internal DNS. Don't add a public route just to make CI reachable; move CI to the network instead.62- **Confirm the deployed image SHA before drawing conclusions from a post-deploy test.** When a workflow runs a load test, perf benchmark, or smoke test after a deploy job in the same pipeline, the deploy rollout may still be in progress when the test starts. The test then runs against the previous image while the artifact is labeled "post-merge" — false confidence that the change had no effect. Before any post-deploy assertion, confirm the rollout is complete: `kubectl rollout status deploy/<name> --timeout=5m`, OR assert that the deployed image tag matches the head SHA (`kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[0].image}'`), OR pull the running image from observability (Groundcover/Datadog/k8s API) and assert it matches. Either gate the test on rollout-status or fail the test if the image SHA doesn't match. Skipping the confirmation collapses half the test's value into a meaningless number.63- **Reusable workflows can't read another private repo with the default token.** When a reusable workflow (or a called workflow) needs files from its own private home repo but runs in a *different* private repo, the default `GITHUB_TOKEN` is scoped to the calling repo and the checkout fails ("Repository not found"). Fix one of two ways: (a) `actions/checkout` the home repo with an explicit PAT or GitHub App token stored as a secret, and have callers add `secrets: inherit`; or (b) ship the logic as a **composite action** — actions are auto-fetched by `uses:`, so no token is required. Prefer the composite action when the logic is self-contained; it removes the credential entirely.64</github_actions>6566<health_checks>67```python68# FastAPI69@app.get("/health")70async def health():71 return {"status": "ok", "version": settings.VERSION}7273@app.get("/health/ready")74async def readiness():75 # Check dependencies76 await db.execute("SELECT 1")77 return {"status": "ready"}78```7980### Kubernetes Probes81```yaml82livenessProbe:83 httpGet: { path: /health, port: 8000 }84 initialDelaySeconds: 1085 periodSeconds: 3086readinessProbe:87 httpGet: { path: /health/ready, port: 8000 }88 initialDelaySeconds: 589 periodSeconds: 1090```91</health_checks>9293<rollback>94```bash95# Immediate rollback strategies96git revert HEAD && git push # Revert last commit97kubectl rollout undo deployment/myapp # K8s rollback98docker compose up -d --no-deps app # Redeploy previous image99```100101**Rollback checklist:**102- [ ] Health check failing? → Rollback immediately103- [ ] Data migration involved? → Ensure backward-compatible schema104- [ ] Feature flag available? → Disable flag instead of rollback105</rollback>106107<production_readiness>108Universal application + infrastructure deployment guardrails (tests pass, migrations before app deploy, health check, rollback) live in `~/.claude/rules/infrastructure/RULE.md`. Operations items kept here because they're org-process, not infra-pattern:109110**Operations:**111- [ ] Rollback plan documented112- [ ] Monitoring dashboards set up113- [ ] Alerting configured114- [ ] On-call rotation defined115</production_readiness>116117<success_criteria>118- [ ] CI pipeline passes before any deploy119- [ ] Health checks verify deployment success120- [ ] Rollback plan tested121- [ ] Zero-downtime deployment verified122</success_criteria>