Software Delivery Pipeline Manager
Implements CI/CD pipeline patterns including build automation, test orchestration, deployment strategies (blue-green, canary, rolling), artifact management, and environment promotion to move code reliably from commit to production.
TL;DR Checklist
- Define all stages: build → test → deploy with explicit success criteria per stage
- Configure parallel test execution with caching for sub-10-minute feedback loops
- Select deployment strategy matching risk tolerance (blue-green, canary, or rolling)
- Set up environment promotion gates with manual approval for production
- Pin all tool versions and container base images in pipeline definitions
- Implement artifact retention policy (e.g., 90 days, max 100 builds)
- Ensure every deploy produces an auditable record: commit SHA, artifacts, timestamp
When to Use
Use this skill when:
- Designing a new CI/CD pipeline from scratch for a project or service
- Migrating from manual or ad-hoc deployments to an automated pipeline
- Adding deployment strategies (blue-green, canary) to an existing pipeline
- Setting up environment promotion workflows (dev → staging → prod) with quality gates
- Implementing artifact management and build caching for faster pipelines
- Troubleshooting slow pipelines and optimizing build/test execution times
- Integrating infrastructure-as-code into the deployment flow
When NOT to Use
Avoid this skill for:
- Writing application logic or business domain code (use
software-design-principlesinstead) - Designing system architecture patterns like hexagonal, layered, or event-driven (use
software-architecture) - Adding security scanning, CVE checks, or quality gates specifically — use
secure-release-pipelineinstead - Prototyping throwaway branches with no intention of merging to main
- One-off manual deployments where pipeline overhead outweighs benefits
Core Workflow
Define Pipeline Stages — Map the code lifecycle: checkout → build → test → package → deploy. Each stage must have explicit pass/fail criteria. Checkpoint: Every stage exits non-zero on failure, causing the pipeline to halt immediately.
Configure Build Automation — Set up dependency caching, parallel compilation, and artifact generation. Pin tool versions in a lockfile or explicit version field. Checkpoint: Build time is measured and targeted under 5 minutes for the build stage alone.
Orchestrate Tests — Organize tests into tiers (unit → integration → e2e) with parallel execution within each tier. Use cached dependency layers and test result aggregation. Checkpoint: Unit tests complete in under 3 minutes; total pipeline feedback loop stays under 10 minutes.
Package and Push Artifacts — Build container images or binaries, tag them with the commit SHA and semantic version, push to a registry or artifact repository. Checkpoint: Artifact tags are immutable once pushed; the same tag always refers to the same build.
Promote Through Environments — Deploy to dev → staging → production with manual approval gates between stages. Each environment must validate health checks before promotion. Checkpoint: Production deployment requires explicit human approval and a documented change record.
Execute Deployment Strategy — Apply the chosen strategy (blue-green, canary, or rolling) based on risk profile and team maturity. Checkpoint: Automated rollback triggers if error rate exceeds threshold during or after deployment.
Record and Monitor — Log every deployment event with commit SHA, artifact version, deployer, and timestamps. Feed metrics into dashboards for trend analysis.
Implementation Patterns / Reference Guide
Pattern 1: GitHub Actions CI/CD Pipeline
A comprehensive multi-stage pipeline that covers build, test, package, and deploy to a staging environment. This example uses Python but the structure applies to any language.
# .github/workflows/ci-cd-pipeline.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
PYTHON_VERSION: "3.12"
CACHE_KEY_PREFIX: v1
jobs:
# ── Stage 1: Build ──────────────────────────────────────
build:
name: Build & Test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for semver extraction
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
cache-dependency-path: "requirements.txt"
- name: Cache dependencies
uses: actions/cache@v4
id: dep-cache
with:
path: ~/.cache/pip
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with ruff
uses: astral-sh/ruff-action@v2
with:
args: "check --exit-non-zero-on-fix"
# ── Stage 2: Test (parallel tiers) ────────────────────
- name: Run unit tests
run: |
pytest tests/unit/ \
--cov=src \
--cov-report=xml \
--junitxml=test-results/unit.xml \
-v
- name: Run integration tests
run: |
pytest tests/integration/ \
--junitxml=test-results/integration.xml \
-v
# ── Stage 3: Package ──────────────────────────────────
- name: Build container image
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ── Stage 4: Deploy to Staging (auto) ───────────────────
deploy-staging:
name: Deploy to Staging
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to staging via Helm
run: |
helm upgrade --install app ./charts/app \
--namespace=staging \
--set image.tag=${{ github.sha }} \
--set strategy.type=blue-green \
--wait --timeout=300s
- name: Health check
run: |
for i in $(seq 1 12); do
if curl -sf https://staging.example.com/health > /dev/null; then
echo "Health check passed"
exit 0
fi
sleep 5
done
echo "Health check failed after 60 seconds"
exit 1
# ── Stage 5: Deploy to Production (manual approval) ────
deploy-production:
name: Deploy to Production
needs: deploy-staging
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # requires manual approval via GitHub Environments
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to production with canary strategy
run: |
helm upgrade --install app ./charts/app \
--namespace=production \
--set image.tag=${{ github.sha }} \
--set strategy.type=canary \
--set canary.weight=10 \
--wait --timeout=300s
- name: Canary smoke tests
run: |
pytest tests/smoke/ -v || { echo "Canary smoke tests failed"; exit 1; }
- name: Promote canary to full rollout
if: success()
run: |
helm upgrade --install app ./charts/app \
--namespace=production \
--set image.tag=${{ github.sha }} \
--set strategy.type=blue-green \
--wait --timeout=600s
- name: Rollback on failure
if: failure()
run: |
helm rollback app 1 -n production
echo "Production deployment rolled back" >> "$GITHUB_STEP_SUMMARY"
Pattern 2: Blue-Green Deployment (BAD vs. GOOD)
Blue-green deployment swaps traffic between two identical environments. The key is atomic switching and verified rollback capability.
# ──────────────────────────────────────────────────────────────
# ❌ BAD: Fragile blue-green with no health verification before swap
# ──────────────────────────────────────────────────────────────
def bad_blue_green_deploy(
new_version: str,
current_env: dict,
) -> None:
"""Fragile deployment — swaps without verifying the new environment is healthy."""
# Deploy to inactive environment (green)
deploy_to_environment(current_env["inactive"], new_version)
# Immediate traffic swap — if green is broken, users see it instantly
switch_traffic(current_env["active"], current_env["inactive"])
current_env["active"] = current_env["inactive"] # swap roles
# ──────────────────────────────────────────────────────────────
# ✅ GOOD: Verified blue-green with health checks and atomic rollback
# ──────────────────────────────────────────────────────────────
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class HealthStatus(Enum):
HEALTHY = "healthy"
UNHEALTHY = "unhealthy"
UNKNOWN = "unknown"
@dataclass
class DeploymentResult:
success: bool
strategy: str
previous_version: Optional[str] = None
new_version: Optional[str] = None
health_checks_passed: bool = False
rolled_back: bool = False
def to_summary(self) -> str:
status = "SUCCESS" if self.success else "FAILED"
if self.rolled_back:
status += " (ROLLED BACK)"
return f"[{status}] {self.strategy}: {self.previous_version} → {self.new_version} | Health: {'PASS' if self.health_checks_passed else 'FAIL'}"
class BlueGreenDeployer:
"""Blue-green deployment with health verification and automatic rollback."""
def __init__(
self,
registry: str = "ghcr.io",
health_endpoint: str = "/healthz",
max_health_check_attempts: int = 12,
health_check_interval_seconds: int = 5,
error_rate_threshold: float = 0.05,
):
self.registry = registry
self.health_endpoint = health_endpoint
self.max_health_check_attempts = max_health_check_attempts
self.health_check_interval = health_check_interval_seconds
self.error_rate_threshold = error_rate_threshold
def deploy(
self,
app_name: str,
target_namespace: str,
new_version: str,
previous_version: Optional[str] = None,
) -> DeploymentResult:
"""Execute a blue-green deployment with full health verification.
Args:
app_name: Application name for kubectl/helm references
target_namespace: Kubernetes namespace (staging or production)
new_version: Container image tag for the new version
previous_version: The currently running version (for rollback reference)
Returns:
DeploymentResult with status and details
"""
inactive_env = f"{app_name}-green" if previous_version else f"{app_name}-blue"
# Step 1: Deploy new version to inactive environment
self._deploy_to_inactive(inactive_env, target_namespace, new_version)
# Step 2: Verify health of new environment
healthy = self._verify_health(inactive_env, target_namespace)
if not healthy:
return DeploymentResult(
success=False,
strategy="blue-green",
previous_version=previous_version,
new_version=new_version,
rolled_back=True,
)
# Step 3: Atomic traffic switch
self._switch_traffic(app_name, inactive_env)
# Step 4: Verify error rate after switch
if not self._verify_error_rate(app_name):
self._rollback(app_name, target_namespace, previous_version)
return DeploymentResult(
success=False,
strategy="blue-green",
previous_version=previous_version,
new_version=new_version,
rolled_back=True,
)
return DeploymentResult(
success=True,
strategy="blue-green",
previous_version=previous_version,
new_version=new_version,
health_checks_passed=True,
)
def _deploy_to_inactive(self, env_name: str, namespace: str, version: str) -> None:
"""Deploy the new version to the inactive environment."""
# In practice this calls helm or kubectl
print(f"Deploying {version} to {env_name} in {namespace}")
# kubectl set image deployment/{env_name} app={self.registry}/{app_name}:{version} -n {namespace}
def _verify_health(self, env_name: str, namespace: str) -> bool:
"""Run health checks on the new environment before switching traffic."""
for attempt in range(1, self.max_health_check_attempts + 1):
status = self._get_health_status(env_name, namespace)
if status == HealthStatus.HEALTHY:
print(f"Health check passed (attempt {attempt})")
return True
print(f"Health check attempt {attempt}/{self.max_health_check_attempts}: {status.value}")
time.sleep(self.health_check_interval)
return False
def _get_health_status(self, env_name: str, namespace: str) -> HealthStatus:
"""Simulate health check — replace with actual HTTP probe or k8s readiness."""
# In practice: curl -sf http://{env_name}.{namespace}/healthz
import random
return HealthStatus.HEALTHY if random.random() > 0.3 else HealthStatus.UNHEALTHY
def _switch_traffic(self, app_name: str, new_env: str) -> None:
"""Atomically switch service selector to point at the new environment."""
print(f"Switching traffic for {app_name} → {new_env}")
# kubectl patch service/{app_name} -p '{"spec":{"selector":{"version": "{new_env}"}}}'
def _verify_error_rate(self, app_name: str) -> bool:
"""Check that error rate stays below threshold after deployment."""
# In practice: query Prometheus for 5xx error rate in last 60 seconds
current_error_rate = 0.02 # simulated
print(f"Error rate check: {current_error_rate:.4f} (threshold: {self.error_rate_threshold})")
return current_error_rate < self.error_rate_threshold
def _rollback(self, app_name: str, namespace: str, version_to_restore: Optional[str]) -> None:
"""Roll back to the previous version."""
if not version_to_restore:
print(f"⚠ No previous version known for rollback of {app_name}")
return
print(f"Rolling back {app_name} in {namespace} → {version_to_restore}")
# kubectl set image deployment/{app_name} app={self.registry}/{app_name}:{version_to_restore} -n {namespace}
# ── Usage example ─────────────────────────────────────────────
if __name__ == "__main__":
deployer = BlueGreenDeployer(
health_endpoint="/healthz",
max_health_check_attempts=12,
error_rate_threshold=0.05,
)
result = deployer.deploy(
app_name="my-service",
target_namespace="production",
new_version="sha-a1b2c3d",
previous_version="sha-9e8f7g6",
)
print(result.to_summary())
# Example output: [SUCCESS] blue-green: sha-9e8f7g6 → sha-a1b2c3d | Health: PASS
Pattern 3: Canary Deployment with Gradual Traffic Shifting
Canary deployments roll out to a small percentage of users first, then gradually increase traffic based on error metrics.
# ──────────────────────────────────────────────
# Kubernetes manifest for canary deployment strategy
# ──────────────────────────────────────────────
# charts/app/templates/canary-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service-canary
namespace: production
labels:
app: my-service
track: canary
spec:
replicas: 2 # Small subset of total traffic
selector:
matchLabels:
app: my-service
track: canary
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # Zero-downtime guaranteed
template:
metadata:
labels:
app: my-service
track: canary
version: {{ .Values.image.tag }}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
containers:
- name: app
image: "{{ .Values.registry }}/my-service:{{ .Values.image.tag }}"
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
---
# Service routes traffic based on weight labels
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: production
spec:
selector:
app: my-service # Matches both stable and canary pods
ports:
- port: 80
targetPort: 8080
---
# Istio VirtualService for weighted traffic splitting
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-service
namespace: production
spec:
hosts:
- my-service.example.com
http:
- route:
# Start with 90/10 split, gradually shift to 100% canary
- destination:
host: my-service
subset: stable
weight: 90
- destination:
host: my-service
subset: canary
weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-service
namespace: production
spec:
host: my-service
subsets:
- name: stable
labels:
track: stable
- name: canary
labels:
track: canary
Gradual promotion schedule (managed by CI/CD):
# .github/workflows/canary-promotion.yml
name: Canary Promotion Pipeline
on:
workflow_dispatch: # Manual trigger for each stage
schedule:
- cron: "0 */2 * * *" # Auto-advance every 2 hours during promotion
env:
CANARY_STAGES: |
{"name": "initial", "canary_weight": 5, "duration_minutes": 15}
{"name": "early_adopters", "canary_weight": 20, "duration_minutes": 30}
{"name": "partial", "canary_weight": 50, "duration_minutes": 60}
{"name": "full", "canary_weight": 100, "duration_minutes": 0}
jobs:
promote-canary:
name: Promote Canary ${{ matrix.stage.name }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- stage: initial
weight: 5
duration: 15
- stage: early_adopters
weight: 20
duration: 30
- stage: partial
weight: 50
duration: 60
- stage: full
weight: 100
duration: 0
steps:
- name: Update canary traffic weight
run: |
# Patch Istio VirtualService to adjust weights
CURRENT_STABLE=$(( 100 - ${{ matrix.weight }} ))
kubectl patch virtualservice my-service \
-n production \
--type='merge' \
-p="{\"spec\":{\"http\":[{\"route\":[{\"destination\":{\"host\":\"my-service\",\"subset\":\"stable\"},\"weight\":${CURRENT_STABLE}},{\"destination\":{\"host\":\"my-service\",\"subset\":\"canary\"},\"weight\":${{ matrix.weight }}}]}]}"
- name: Monitor error rate during promotion
if: ${{ matrix.weight < 100 }}
run: |
# Check Prometheus for error rate in the canary subset
PROMQL='sum(rate(http_requests_total{status=~"5..",subset="canary"}[2m])) / sum(rate(http_requests_total{subset="canary"}[2m]))'
ERROR_RATE=$(curl -sf "http://prometheus:9090/api/v1/query?query=${PROMQL}" | jq '.data.result[0].value[1]')
THRESHOLD=0.05 # 5% error rate threshold
if (( $(echo "$ERROR_RATE > $THRESHOLD" | bc -l) )); then
echo "❌ Error rate ${ERROR_RATE} exceeds threshold ${THRESHOLD} — halting promotion"
exit 1
fi
echo "✅ Error rate ${ERROR_RATE} within threshold"
- name: Wait for monitoring period
if: ${{ matrix.weight < 100 && matrix.duration > 0 }}
run: |
echo "Waiting ${{ matrix.duration }} minutes for monitoring..."
sleep $(( ${{ matrix.duration }} * 60 ))
- name: Log promotion event
run: |
echo "Canary promoted to ${{ matrix.stage.name }} (${{ matrix.weight }}% traffic)" \
>> "$GITHUB_STEP_SUMMARY"
Pattern 4: Docker Build with Multi-Stage Builds and Layer Caching
Efficient container builds reduce pipeline time and minimize artifact size.
# ──────────────────────────────────────────────
# Dockerfile — multi-stage build for Python service
# ──────────────────────────────────────────────
FROM python:3.12-slim AS base
# Install system dependencies only once
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# ── Stage 1: Dependencies (cached independently) ──
FROM base AS deps
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# ── Stage 2: Build ──
FROM deps AS build
COPY pyproject.toml .
COPY src/ ./src/
COPY tests/ ./tests/
# Run linter and tests in the build stage (fail fast)
RUN pip install ruff pytest pytest-cov && \
ruff check src/ && \
pytest tests/unit/ --tb=short -q
# ── Stage 3: Production — minimal image ──
FROM python:3.12-slim AS production
LABEL org.opencontainers.image.source="https://github.com/${IMAGE_OWNER}/${IMAGE_REPO}"
LABEL org.opencontainers.image.description="Production runtime"
WORKDIR /app
# Copy only built application and dependencies (not test files)
COPY --from=deps /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY --from=build /app/src/ ./src/
COPY --from=build /app/pyproject.toml ./
RUN useradd --create-home appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -sf http://localhost:8080/healthz || exit 1
CMD ["python", "-m", "src.main"]
Pattern 5: Environment Promotion Flow with Quality Gates
A Python model for managing environment promotion from dev through staging to production.
"""
Environment Promotion Manager
Manages the flow of artifacts through environments (dev → staging → prod)
with quality gates between each stage. Follows the principle that every
deployment must be traceable back to a single immutable artifact.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, List, Callable
from datetime import datetime
class Environment(Enum):
DEV = "dev"
STAGING = "staging"
PRODUCTION = "production"
class GateResult(Enum):
PASS = "pass"
FAIL = "fail"
SKIP = "skip" # Not applicable for this environment transition
class PromotionStatus(Enum):
PENDING = "pending"
APPROVED = "approved"
DEPLOYED = "deployed"
ROLLED_BACK = "rolled_back"
BLOCKED = "blocked"
@dataclass
class Artifact:
"""Immutable build artifact — once created, cannot be modified."""
id: str
commit_sha: str
version: str
image_tag: str
built_at: datetime
metadata: dict = field(default_factory=dict)
@property
def name(self) -> str:
return f"{self.image_tag}"
@dataclass
class QualityGate:
"""A quality gate that must pass before promotion."""
name: str
required_for: List[Environment]
check_fn: Callable[[Artifact], bool]
def evaluate(self, artifact: Artifact) -> GateResult:
return GateResult.PASS if self.check_fn(artifact) else GateResult.FAIL
class EnvironmentPromoter:
"""Manages environment promotion with configurable quality gates.
Flow: dev (auto) → staging (auto after gate) → production (manual + gate)
Example usage:
promoter = EnvironmentPromoter()
artifact = Artifact("build-123", "abc123", "v1.2.3", "ghcr.io/app:v1.2.3")
promotion = promoter.promote(artifact, Environment.PRODUCTION)
"""
def __init__(self):
self.gates: List[QualityGate] = [
QualityGate("unit-tests-pass", [Environment.DEV, Environment.STAGING, Environment.PRODUCTION],
lambda a: True), # Replace with actual test status check
QualityGate("lint-clean", [Environment.DEV, Environment.STAGING, Environment.PRODUCTION],
lambda a: True),
QualityGate("security-scan-clean", [Environment.STAGING, Environment.PRODUCTION],
lambda a: True),
QualityGate("integration-tests-pass", [Environment.STAGING, Environment.PRODUCTION],
lambda a: True),
QualityGate("smoke-tests-pass", [Environment.PRODUCTION],
lambda a: True),
QualityGate("manual-approval", [Environment.PRODUCTION],
lambda a: True), # Replaced by explicit approval call
]
def promote(
self,
artifact: Artifact,
target_env: Environment,
approver: Optional[str] = None,
previous_artifact: Optional[Artifact] = None,
) -> dict:
"""Promote an artifact to the target environment.
Args:
artifact: The build artifact to deploy
target_env: Target environment
approver: Required for production deployments
previous_artifact: Currently running version (for rollback tracking)
Returns:
Promotion result dict with status, gates evaluated, and actions taken
"""
history = self._get_deployment_history()
# Determine required gates for this transition
prev_env = self._current_environment(artifact.commit_sha, history)
required_gates = [
g for g in self.gates
if target_env in g.required_for
]
# Evaluate all gates
gate_results = {}
for gate in required_gates:
result = gate.evaluate(artifact)
gate_results[gate.name] = result
blocked_gates = [name for name, result in gate_results.items() if result == GateResult.FAIL]
if blocked_gates:
return {
"status": PromotionStatus.BLOCKED.value,
"artifact": artifact.name,
"target_env": target_env.value,
"blocked_by": blocked_gates,
"gates": gate_results,
}
# Production requires explicit human approval
if target_env == Environment.PRODUCTION and not approver:
return {
"status": PromotionStatus.PENDING.value,
"artifact": artifact.name,
"target_env": target_env.value,
"requires_approval": True,
"message": "Production deployment requires manual approval",
}
# Execute deployment
self._execute_deploy(artifact, target_env)
self._record_deployment(artifact, target_env, approver, previous_artifact)
return {
"status": PromotionStatus.DEPLOYED.value,
"artifact": artifact.name,
"target_env": target_env.value,
"deployed_by": approver or "pipeline",
"gates_passed": list(gate_results.keys()),
"deployed_at": datetime.now().isoformat(),
}
def rollback(self, env: Environment, artifact: Artifact) -> dict:
"""Roll back to the previous artifact in the given environment."""
history = self._get_deployment_history()
prev_artifact = self._find_previous_version(env, artifact.commit_sha, history)
if not prev_artifact:
return {"status": "error", "message": f"No previous version found for {env.value}"}
self._execute_deploy(prev_artifact, env)
self._record_deployment(prev_artifact, env, approver="rollback-automation")
return {
"status": PromotionStatus.ROLLED_BACK.value,
"environment": env.value,
"from_version": artifact.name,
"to_version": prev_artifact.name,
}
def _execute_deploy(self, artifact: Artifact, env: Environment) -> None:
"""Execute the actual deployment (calls helm/kubectl)."""
print(f"Deploying {artifact.name} to {env.value}")
# In practice: kubectl set image deployment/app app={artifact.image_tag} -n {env.value}
def _record_deployment(
self, artifact: Artifact, env: Environment, approver: str, previous: Optional[Artifact] = None
) -> None:
"""Record the deployment event for audit trail."""
print(f"Recorded: {artifact.name} → {env.value} by {approver}")
def _get_deployment_history(self) -> list:
return []
def _current_environment(self, commit_sha: str, history: list) -> Environment:
return Environment.DEV
def _find_previous_version(self, env: Environment, current_sha: str, history: list) -> Optional[Artifact]:
return None
# ── Example promotion flow ────────────────────────────────
if __name__ == "__main__":
promoter = EnvironmentPromoter()
artifact = Artifact(
id="build-456",
commit_sha="def789",
version="v2.1.0",
image_tag="ghcr.io/myorg/myapp:v2.1.0",
built_at=datetime.now(),
)
# 1. Dev deployment — automatic
dev_result = promoter.promote(artifact, Environment.DEV)
print(f"Dev: {dev_result['status']}")
# 2. Staging deployment — automatic after gates pass
staging_result = promoter.promote(artifact, Environment.STAGING)
print(f"Staging: {staging_result['status']}")
# 3. Production deployment — requires approver
prod_pending = promoter.promote(artifact, Environment.PRODUCTION)
if prod_pending["requires_approval"]:
print(f"Pending approval for production ({prod_pending['artifact']})")
# In real implementation: send Slack/Teams notification, wait for human click
prod_result = promoter.promote(artifact, Environment.PRODUCTION, approver="alice@company.com")
print(f"Production: {prod_result['status']}")
Constraints
MUST DO
- Pin all tool versions, base images, and action references to specific versions or SHA hashes — never use
latestor untagged references in production pipelines - Ensure every stage has explicit success/fail criteria; a pipeline must never pass with partial failures (e.g., lint warnings should not mask test failures)
- Run tests in parallel within each tier to achieve sub-10-minute total feedback loops
- Tag container images and artifacts with both the commit SHA and semantic version for full traceability
- Require manual approval gates before any production deployment, documented via environment protection rules (GitHub Environments, GitLab Protected Environments)
- Implement automated rollback triggered by error rate thresholds or health check failures after deployment
- Retain build artifacts per a defined policy (e.g., last 100 builds or 90 days — whichever is longer)
- Log every deployment event with commit SHA, artifact version, deployer identity, and timestamp for audit compliance
MUST NOT DO
- Hardcode secrets in pipeline YAML files; use secret managers (GitHub Secrets, AWS Secrets Manager, HashiCorp Vault)
- Skip or bypass any quality gate stage — automation must enforce completeness, not convenience
- Deploy the same artifact version to two environments simultaneously without version differentiation in deployment records
- Use shell scripts as the primary orchestration mechanism in CI/CD; prefer native pipeline actions (GitHub Actions steps, GitLab CI jobs, Jenkins declarative stages)
- Enable automatic production promotion without human approval — this is the single most common cause of uncontrolled production incidents
- Mix artifact building and deployment in a single step; keep build (immutable) separate from deploy (stateful)
Related Skills
| Skill | Purpose |
|---|---|
secure-release-pipeline |
Security gates, CVE scanning, and quality checks integrated into the pipeline flow |
test-driven-development |
Test-first practices that produce the unit and integration tests orchestrated by CI pipelines |
semver-automation |
Semantic versioning and changelog automation used in artifact tagging |
git-branching-strategies |
Branch models (GitHub Flow, GitFlow) that determine pipeline trigger conditions |
📖 skill(local cache): secure-release-pipeline, test-driven-development, semver-automation, git-branching-strategies