erp-core/docs/07-devops/CI-CD-PIPELINE.md

26 KiB

CI/CD PIPELINE - ERP Generic

Última actualización: 2025-11-24 Responsable: DevOps Team / Tech Lead Estado: Production-Ready


TABLE OF CONTENTS

  1. Overview
  2. CI/CD Architecture
  3. GitHub Actions Workflows
  4. Automated Testing Integration
  5. Code Quality Gates
  6. Security Scanning
  7. Docker Build & Push
  8. Deployment Automation
  9. Rollback Automation
  10. Notifications

1. OVERVIEW

1.1 CI/CD Pipeline Architecture

┌─────────────────────────────────────────────────────────────┐
│                   Developer Workflow                        │
│  1. git push origin feature/new-feature                     │
│  2. Create Pull Request                                     │
└────────────┬────────────────────────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────────────────────────┐
│              CI Pipeline (GitHub Actions)                   │
│  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐           │
│  │  Lint  │→ │  Build │→ │  Test  │→ │Security│           │
│  │        │  │        │  │ (1.5k) │  │  Scan  │           │
│  └────────┘  └────────┘  └────────┘  └────────┘           │
│       ↓          ↓           ↓           ↓                  │
│  ┌─────────────────────────────────────────────┐           │
│  │  Quality Gate (SonarQube)                   │           │
│  │  - Coverage >80%                            │           │
│  │  - No critical bugs                         │           │
│  └─────────────────────────────────────────────┘           │
└────────────┬────────────────────────────────────────────────┘
             │ (If all checks pass)
             ▼
┌─────────────────────────────────────────────────────────────┐
│            CD Pipeline (Deployment)                         │
│                                                             │
│  develop branch → QA (Auto)                                │
│  main branch    → Staging (Manual approval)                │
│  release tags   → Production (Manual approval)             │
│                                                             │
│  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐          │
│  │ Build  │→ │  Push  │→ │ Deploy │→ │ Verify │          │
│  │ Docker │  │   to   │  │   to   │  │ Health │          │
│  │ Image  │  │Registry│  │ Server │  │ Checks │          │
│  └────────┘  └────────┘  └────────┘  └────────┘          │
└────────────┬────────────────────────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────────────────────────┐
│              Post-Deployment                                │
│  - Health checks                                            │
│  - Smoke tests                                              │
│  - Notifications (Slack/Discord)                           │
│  - Rollback if failure                                      │
└─────────────────────────────────────────────────────────────┘

1.2 Pipeline Stages

Stage Duration Trigger Blocking
Lint 30s Every push Yes
Build 2-3 min Every push Yes
Unit Tests 1-2 min Every push Yes
Integration Tests 5-10 min Every push Yes
E2E Tests 15-30 min Push to main/develop No (parallel)
Security Scan 2-5 min Every push Yes (critical only)
Quality Gate 1 min Every push Yes
Docker Build 5-10 min Merge to main/develop No
Deploy QA 3-5 min Merge to develop No
Deploy Production 10-15 min Release tag + approval No

Total CI time: ~15-25 minutes (blocking) Total CD time: 3-15 minutes (per environment)


2. CI/CD ARCHITECTURE

2.1 Branching Strategy (GitFlow)

main (production)
  │
  ├── release/v1.0.0 (staging)
  │   │
  │   └── hotfix/critical-bug
  │
  └── develop (QA)
      │
      ├── feature/user-authentication
      ├── feature/sales-orders
      └── bugfix/fix-inventory-calc

Branch Protection Rules:

# main branch
required_reviews: 2
require_code_owner_review: true
dismiss_stale_reviews: true
require_linear_history: true
required_status_checks:
  - ci/lint
  - ci/build
  - ci/test
  - ci/security
  - ci/quality-gate

# develop branch
required_reviews: 1
require_code_owner_review: false
required_status_checks:
  - ci/lint
  - ci/test

2.2 Deployment Environments

Environment Branch Trigger Approval Required
Development feature/* Manual No
QA develop Automatic No
Staging main Manual Yes (Tech Lead)
Production tags/v* Manual Yes (CTO + PO)

3. GITHUB ACTIONS WORKFLOWS

3.1 CI Workflow (Main Pipeline)

File: .github/workflows/ci.yml

name: CI Pipeline

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

env:
  NODE_VERSION: '20'
  PNPM_VERSION: '8'

jobs:
  lint:
    name: Lint Code
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Run Prettier check
        run: npm run format:check

  build:
    name: Build Application
    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: lint
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build backend
        run: npm run build --workspace=backend

      - name: Build frontend
        run: npm run build --workspace=frontend

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: |
            backend/dist
            frontend/dist            
          retention-days: 7

  test-unit:
    name: Unit Tests
    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: build
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests (Backend)
        run: npm run test:unit --workspace=backend -- --coverage

      - name: Run unit tests (Frontend)
        run: npm run test:unit --workspace=frontend -- --coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./backend/coverage/coverage-final.json,./frontend/coverage/coverage-final.json
          flags: unit-tests
          name: unit-tests-coverage

  test-integration:
    name: Integration Tests
    runs-on: ubuntu-latest
    timeout-minutes: 20
    needs: build
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_password
        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

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run Prisma migrations
        run: npx prisma migrate deploy --workspace=backend
        env:
          DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db

      - name: Seed test data
        run: npm run seed:test --workspace=backend
        env:
          DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db

      - name: Run integration tests
        run: npm run test:integration --workspace=backend
        env:
          DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
          REDIS_URL: redis://localhost:6379

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: integration-test-results
          path: backend/test-results/
          retention-days: 7

  test-e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30
    needs: build
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps

      - name: Start services (Docker Compose)
        run: docker-compose -f docker-compose.test.yml up -d

      - name: Wait for services to be ready
        run: |
          timeout 60 bash -c 'until curl -f http://localhost:3000/health; do sleep 2; done'          

      - name: Run E2E tests
        run: npm run test:e2e
        env:
          BASE_URL: http://localhost:5173

      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: Stop services
        if: always()
        run: docker-compose -f docker-compose.test.yml down

  security-scan:
    name: Security Scanning
    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: build
    steps:
      - uses: actions/checkout@v4

      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high --fail-on=all

      - name: Run OWASP Dependency Check
        uses: dependency-check/Dependency-Check_Action@main
        with:
          project: 'erp-generic'
          path: '.'
          format: 'HTML'
          args: >
            --failOnCVSS 7
            --enableRetired            

      - name: Upload dependency check report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: dependency-check-report
          path: reports/
          retention-days: 30

  quality-gate:
    name: Quality Gate (SonarQube)
    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: [test-unit, test-integration]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Full history for SonarQube

      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
        with:
          args: >
            -Dsonar.projectKey=erp-generic
            -Dsonar.qualitygate.wait=true            

      - name: Check Quality Gate status
        run: |
          STATUS=$(curl -s -u ${{ secrets.SONAR_TOKEN }}: \
            "${{ secrets.SONAR_HOST_URL }}/api/qualitygates/project_status?projectKey=erp-generic" \
            | jq -r '.projectStatus.status')

          if [ "$STATUS" != "OK" ]; then
            echo "Quality Gate failed: $STATUS"
            exit 1
          fi          

  notify-success:
    name: Notify Success
    runs-on: ubuntu-latest
    needs: [lint, build, test-unit, test-integration, test-e2e, security-scan, quality-gate]
    if: success()
    steps:
      - name: Send Slack notification
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "✅ CI Pipeline passed for ${{ github.repository }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*CI Pipeline Success* ✅\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Author:* ${{ github.actor }}"
                  }
                }
              ]
            }            

  notify-failure:
    name: Notify Failure
    runs-on: ubuntu-latest
    needs: [lint, build, test-unit, test-integration, test-e2e, security-scan, quality-gate]
    if: failure()
    steps:
      - name: Send Slack notification
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "❌ CI Pipeline failed for ${{ github.repository }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*CI Pipeline Failed* ❌\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Author:* ${{ github.actor }}\n*Logs:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>"
                  }
                }
              ]
            }            

3.2 CD Workflow (Deployment to QA)

File: .github/workflows/cd-qa.yml

name: CD - Deploy to QA

on:
  push:
    branches: [develop]
  workflow_dispatch: # Manual trigger

env:
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  deploy-qa:
    name: Deploy to QA Environment
    runs-on: ubuntu-latest
    timeout-minutes: 15
    environment:
      name: qa
      url: https://qa.erp-generic.local
    steps:
      - uses: actions/checkout@v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.DOCKER_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Backend Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          file: ./backend/Dockerfile
          push: true
          tags: |
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:qa
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:qa-${{ github.sha }}            
          cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:qa
          cache-to: type=inline

      - name: Build and push Frontend Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./frontend
          file: ./frontend/Dockerfile
          push: true
          tags: |
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:qa
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:qa-${{ github.sha }}            
          cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:qa
          cache-to: type=inline

      - name: Deploy to QA server
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.QA_SERVER_HOST }}
          username: ${{ secrets.QA_SERVER_USER }}
          key: ${{ secrets.QA_SERVER_SSH_KEY }}
          script: |
            cd /opt/erp-generic
            docker-compose pull
            docker-compose up -d --no-deps backend frontend
            docker-compose exec -T backend npx prisma migrate deploy
            docker image prune -f            

      - name: Wait for services to be ready
        run: |
          timeout 120 bash -c 'until curl -f https://qa.erp-generic.local/health; do sleep 5; done'          

      - name: Run smoke tests
        run: |
          curl -f https://qa.erp-generic.local/api/health || exit 1
          # Add more smoke tests here          

      - name: Notify deployment success
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "✅ Deployed to QA: ${{ github.sha }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Deployment to QA* ✅\n*Environment:* QA\n*Version:* ${{ github.sha }}\n*URL:* https://qa.erp-generic.local\n*Deployed by:* ${{ github.actor }}"
                  }
                }
              ]
            }            

3.3 CD Workflow (Deployment to Production)

File: .github/workflows/cd-production.yml

name: CD - Deploy to Production

on:
  push:
    tags:
      - 'v*.*.*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to deploy (e.g., v1.0.0)'
        required: true

env:
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment:
      name: production
      url: https://erp-generic.com
    steps:
      - uses: actions/checkout@v4

      - name: Extract version
        id: version
        run: |
          if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
            echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
          else
            echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
          fi          

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.DOCKER_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Backend Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          file: ./backend/Dockerfile
          push: true
          tags: |
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:${{ steps.version.outputs.VERSION }}
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:latest            
          cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:latest
          cache-to: type=inline

      - name: Build and push Frontend Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./frontend
          file: ./frontend/Dockerfile
          push: true
          tags: |
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:${{ steps.version.outputs.VERSION }}
            ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:latest            
          cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:latest
          cache-to: type=inline

      - name: Create database backup
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SERVER_SSH_KEY }}
          script: |
            /opt/erp-generic/scripts/backup-postgres.sh            

      - name: Deploy to Production server (Blue-Green)
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.PROD_SERVER_HOST }}
          username: ${{ secrets.PROD_SERVER_USER }}
          key: ${{ secrets.PROD_SERVER_SSH_KEY }}
          script: |
            cd /opt/erp-generic
            ./scripts/deploy-blue-green.sh production ${{ steps.version.outputs.VERSION }}            

      - name: Verify deployment
        run: |
          timeout 300 bash -c 'until curl -f https://erp-generic.com/health; do sleep 10; done'          

      - name: Run post-deployment tests
        run: |
          npm run test:smoke:production          

      - name: Notify deployment success
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "✅ Deployed to Production: ${{ steps.version.outputs.VERSION }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Deployment to Production* ✅\n*Environment:* Production\n*Version:* ${{ steps.version.outputs.VERSION }}\n*URL:* https://erp-generic.com\n*Deployed by:* ${{ github.actor }}"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Rollback command:*\n```./scripts/rollback.sh ${{ steps.version.outputs.VERSION }}```"
                  }
                }
              ]
            }            

      - name: Create GitHub Release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ steps.version.outputs.VERSION }}
          release_name: Release ${{ steps.version.outputs.VERSION }}
          body: |
            ## Changes in this release
            - Deployed to production on $(date)
            - Docker images: ${{ steps.version.outputs.VERSION }}

            ## Rollback
            If issues arise, run: `./scripts/rollback.sh ${{ steps.version.outputs.VERSION }}`            
          draft: false
          prerelease: false

4. AUTOMATED TESTING INTEGRATION

Test Execution Summary:

  • Unit Tests: 876 tests (backend: 526, frontend: 350) - ~2 minutes
  • Integration Tests: 438 tests (API + DB + Components) - ~10 minutes
  • E2E Tests: 147 tests (Critical flows) - ~25 minutes
  • Total: 1,461 automated tests

Coverage Requirements:

  • Line coverage: >80%
  • Branch coverage: >75%
  • Function coverage: >85%

5. CODE QUALITY GATES

SonarQube Quality Gate:

  • Coverage: >80%
  • Duplicated Lines: <3%
  • Code Smells: <10 per 1000 LOC
  • Bugs: 0 critical, 0 major
  • Vulnerabilities: 0 critical, 0 major
  • Security Hotspots: Reviewed
  • Maintainability Rating: A or B
  • Reliability Rating: A
  • Security Rating: A

6. SECURITY SCANNING

Tools:

  • Snyk: Dependency vulnerability scanning
  • OWASP Dependency Check: Known CVE scanning
  • GitGuardian: Secret scanning
  • Trivy: Container image scanning

Fail Criteria:

  • Critical vulnerabilities: Block merge
  • High vulnerabilities: Warning (fail after 7 days)
  • Medium/Low vulnerabilities: Warning only

7. DOCKER BUILD & PUSH

Image Naming Convention:

ghcr.io/your-org/erp-generic-backend:latest
ghcr.io/your-org/erp-generic-backend:v1.0.0
ghcr.io/your-org/erp-generic-backend:qa
ghcr.io/your-org/erp-generic-backend:qa-abc123

Image Scanning:

- name: Scan Docker image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ghcr.io/${{ github.repository }}-backend:latest
    format: 'sarif'
    output: 'trivy-results.sarif'

8. DEPLOYMENT AUTOMATION

Deployment Strategies:

  1. QA: Rolling update (zero-downtime)
  2. Staging: Blue-green deployment
  3. Production: Blue-green with canary (10% → 50% → 100%)

Pre-Deployment Checklist:

  • All tests passing
  • Security scans clean
  • Database backup created
  • Migration scripts reviewed
  • Rollback plan documented
  • On-call team notified

9. ROLLBACK AUTOMATION

Automated Rollback Triggers:

  • Health check failures (>5 consecutive)
  • Error rate >5% for >5 minutes
  • p95 latency >2s for >5 minutes
  • Manual trigger via GitHub Actions

10. NOTIFICATIONS

Notification Channels:

  • Slack: #erp-deployments (all deployments), #erp-alerts (failures)
  • Email: devops@erp-generic.com (production deployments)
  • PagerDuty: On-call engineer (production failures)

REFERENCES


Documento: CI-CD-PIPELINE.md Versión: 1.0 Total Páginas: ~16 Última Actualización: 2025-11-24