# Software Delivery Pipelines

> Implements CI/CD pipelines with build automation, test orchestration, blue-green/canary deployments, artifact management, and environment promotion for reliable software delivery.

- Skill: `paulpas/software-delivery-pipelines` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/software-delivery-pipelines`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/software-delivery-pipelines/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/software-delivery-pipelines

---





# 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-principles` instead)
- 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-pipeline` instead
- Prototyping throwaway branches with no intention of merging to main
- One-off manual deployments where pipeline overhead outweighs benefits

---

## Core Workflow

1. **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.

2. **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.

3. **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.

4. **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.

5. **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.

6. **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.

7. **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.

```yaml
# .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.

```python
# ──────────────────────────────────────────────────────────────
# ❌ 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.

```yaml
# ──────────────────────────────────────────────
# 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):**

```yaml
# .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
# ──────────────────────────────────────────────
# 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.

```python
"""
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 `latest` or 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

