# CI/CD Pipelines — Pipelines Completos de Referencia

Ejemplos completos de pipelines para GitHub Actions y GitLab CI.

---

## GitHub Actions — Pipeline Python/FastAPI Completo

```yaml
# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  PYTHON_VERSION: "3.12"
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  lint:
    name: Lint y Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: pip
      - name: Instalar herramientas de lint
        run: pip install ruff mypy
      - name: Ruff — linting
        run: ruff check . --output-format=github
      - name: Ruff — formato
        run: ruff format --check .
      - name: MyPy — type checking
        run: mypy app/ --ignore-missing-imports

  test:
    name: Tests (${{ matrix.python-version }})
    runs-on: ubuntu-latest
    needs: lint
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]
      fail-fast: false
    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    env:
      DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test_db
      REDIS_URL: redis://localhost:6379/0
      SECRET_KEY: test-secret-key-no-usar-en-produccion
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - name: Instalar dependencias
        run: pip install -r requirements-dev.txt
      - name: Ejecutar migraciones
        run: alembic upgrade head
      - name: Pytest con cobertura
        run: |
          pytest \
            --cov=app \
            --cov-report=xml \
            --cov-report=term-missing \
            --cov-fail-under=80 \
            --junit-xml=test-results.xml \
            -v
      - name: Subir cobertura a Codecov
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml
          flags: python-${{ matrix.python-version }}
      - name: Publicar resultados de tests
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Tests Python ${{ matrix.python-version }}
          path: test-results.xml
          reporter: java-junit

  security:
    name: Security Scan
    runs-on: ubuntu-latest
    needs: lint
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - name: Bandit — vulnerabilidades en código Python
        uses: PyCQA/bandit-action@v1
        with:
          targets: app/
          level: medium
      - name: pip-audit — vulnerabilidades en dependencias
        run: |
          pip install pip-audit
          pip-audit -r requirements.txt --format=json > audit.json
          cat audit.json
      - name: Trivy — escaneo del repositorio
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          format: sarif
          output: trivy.sarif
          severity: CRITICAL,HIGH
      - name: Subir resultados de seguridad
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy.sarif

  build:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: [test, security]
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
    permissions:
      contents: read
      packages: write
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=sha,prefix=sha-
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login a GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build y Push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

  deploy-staging:
    name: Deploy a Staging
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment:
      name: staging
      url: https://staging-api.empresa.com.mx
    steps:
      - name: Deploy a Kubernetes (staging)
        uses: azure/k8s-deploy@v4
        with:
          namespace: staging
          images: ${{ needs.build.outputs.image-tag }}
          manifests: k8s/staging/
      - name: Smoke tests en staging
        run: |
          sleep 30
          curl -f https://staging-api.empresa.com.mx/health

  deploy-production:
    name: Deploy a Producción
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://api.empresa.com.mx
    steps:
      - name: Deploy a Kubernetes (producción)
        uses: azure/k8s-deploy@v4
        with:
          namespace: produccion
          images: ${{ needs.build.outputs.image-tag }}
          manifests: k8s/production/
          strategy: canary
          percentage: 20
```

---

## GitLab CI — Equivalente

```yaml
# .gitlab-ci.yml
stages:
  - lint
  - test
  - security
  - build
  - deploy-staging
  - deploy-production

variables:
  PYTHON_VERSION: "3.12"
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  DOCKER_BUILDKIT: "1"

.python-cache: &python-cache
  cache:
    key:
      files:
        - requirements*.txt
    paths:
      - .cache/pip
      - venv/
    policy: pull-push

lint:ruff:
  stage: lint
  image: python:3.12-slim
  <<: *python-cache
  script:
    - pip install ruff
    - ruff check . --output-format=gitlab
    - ruff format --check .
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

test:unit:
  stage: test
  image: python:3.12-slim
  <<: *python-cache
  services:
    - name: postgres:15-alpine
      alias: postgres
      variables:
        POSTGRES_DB: test_db
        POSTGRES_USER: test
        POSTGRES_PASSWORD: test
  variables:
    DATABASE_URL: "postgresql+asyncpg://test:test@postgres:5432/test_db"
  script:
    - pip install -r requirements-dev.txt
    - pytest --cov=app --cov-report=xml --cov-fail-under=80
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
      junit: test-results.xml
    expire_in: 1 week

build:image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - |
      docker build \
        --cache-from $CI_REGISTRY_IMAGE:latest \
        --build-arg BUILDKIT_INLINE_CACHE=1 \
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        --tag $CI_REGISTRY_IMAGE:latest \
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```

---

## Caching Efectivo

```yaml
# GitHub Actions — caché de dependencias Python
- uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: pip

# Caché manual para casos especiales
- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/pip
      .venv
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

# Docker layer cache con GitHub Actions
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

---

## Matrix Builds — Múltiples Configuraciones

```yaml
test:
  strategy:
    matrix:
      os: [ubuntu-latest, macos-latest, windows-latest]
      python: ["3.11", "3.12"]
      exclude:
        - os: windows-latest
          python: "3.11"
      include:
        - os: ubuntu-latest
          python: "3.12"
          coverage: true
    fail-fast: false
  runs-on: ${{ matrix.os }}
  steps:
    - name: Cobertura (solo en ubuntu+3.12)
      if: matrix.coverage == true
      run: pytest --cov=app
```

---

## Gestión de Secrets

```yaml
# GitHub Actions — secrets del repositorio
- name: Deploy
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    API_KEY: ${{ secrets.PROD_API_KEY }}
  run: ./scripts/deploy.sh

# Environment-scoped secrets
jobs:
  deploy:
    environment: production
    steps:
      - run: echo ${{ secrets.DEPLOY_KEY }}
```

```bash
# Agregar secrets via CLI de GitHub
gh secret set DATABASE_URL --body "postgresql://..." --env production
gh secret set DATABASE_URL --body "postgresql://..." --env staging
```

---

## Pipeline para Monorepo

```yaml
name: Monorepo CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      api: ${{ steps.filter.outputs.api }}
      frontend: ${{ steps.filter.outputs.frontend }}
      infra: ${{ steps.filter.outputs.infra }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            api:
              - 'apps/api/**'
              - 'packages/shared/**'
            frontend:
              - 'apps/frontend/**'
              - 'packages/shared/**'
            infra:
              - 'infra/**'
              - 'k8s/**'

  test-api:
    needs: changes
    if: ${{ needs.changes.outputs.api == 'true' }}
    uses: ./.github/workflows/api-test.yml

  test-frontend:
    needs: changes
    if: ${{ needs.changes.outputs.frontend == 'true' }}
    uses: ./.github/workflows/frontend-test.yml

  deploy-infra:
    needs: changes
    if: ${{ needs.changes.outputs.infra == 'true' && github.ref == 'refs/heads/main' }}
    uses: ./.github/workflows/infra-deploy.yml
```

---

## Notificaciones y Reportes

```yaml
- name: Notificar fallo en Slack
  if: failure() && github.ref == 'refs/heads/main'
  uses: slackapi/slack-github-action@v1.26.0
  with:
    channel-id: alertas-deploy
    slack-message: |
      *Pipeline fallido en main* :red_circle:
      Repositorio: ${{ github.repository }}
      Commit: ${{ github.sha }}
      Autor: ${{ github.actor }}
      <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Ver pipeline>
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
```

---

## Uso en CI del Sistema SWL

```yaml
# .github/workflows/validate-system.yml
name: Validar integridad del sistema SWL

on:
  push:
    branches: [main]
  pull_request:

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run validate:catalog
      - uses: actions/upload-artifact@v4
        with:
          name: catalogo-sistema
          path: .claude/catalogo.json
```
