Zum Inhalt

CI/CD Integration

Overview

Continuous Integration and Continuous Deployment setup for Solar-Log Enterprise using GitHub Actions, GitLab CI, and Jenkins.

GitHub Actions

Main Workflow

# .github/workflows/ci.yml
name: CI/CD Pipeline

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

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

jobs:
  test-backend:
    name: Backend Tests
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: solarlog_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install dependencies
        run: |
          cd backend
          pip install -r requirements.txt
          pip install pytest pytest-cov pytest-asyncio

      - name: Run tests
        env:
          DATABASE_URL: postgresql://postgres:test@localhost:5432/solarlog_test
        run: |
          cd backend
          pytest tests/ -v --cov=app --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./backend/coverage.xml
          flags: backend

  test-frontend:
    name: Frontend Tests
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend-web/package-lock.json

      - name: Install dependencies
        run: |
          cd frontend-web
          npm ci

      - name: Run tests
        run: |
          cd frontend-web
          npm test -- --coverage --watchAll=false

      - name: Build
        run: |
          cd frontend-web
          npm run build

  build-and-push:
    name: Build Docker Images
    runs-on: ubuntu-latest
    needs: [test-backend, test-frontend]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    permissions:
      contents: read
      packages: write

    strategy:
      matrix:
        service: [backend, frontend]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile.${{ matrix.service }}
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/solarlog
            git pull origin main
            docker compose pull
            docker compose up -d
            docker compose exec -T backend alembic upgrade head

Security Scanning

# .github/workflows/security.yml
name: Security Scan

on:
  push:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * 1'  # Weekly on Monday 2 AM

jobs:
  trivy-scan:
    name: Trivy Security Scan
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  dependabot:
    name: Dependency Updates
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Check for outdated dependencies
        run: |
          cd backend
          pip list --outdated
          cd ../frontend-web
          npm outdated || true

GitLab CI

.gitlab-ci.yml

stages:
  - test
  - build
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

before_script:
  - docker info

test:backend:
  stage: test
  image: python:3.11
  services:
    - postgres:16
  variables:
    POSTGRES_DB: solarlog_test
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: test
    DATABASE_URL: postgresql://postgres:test@postgres:5432/solarlog_test
  script:
    - cd backend
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
    - pytest tests/ -v --cov=app
  coverage: '/TOTAL.*\s+(\d+%)$/'

test:frontend:
  stage: test
  image: node:20
  cache:
    paths:
      - frontend-web/node_modules/
  script:
    - cd frontend-web
    - npm ci
    - npm test -- --coverage --watchAll=false
    - npm run build
  artifacts:
    paths:
      - frontend-web/build/
    expire_in: 1 day

build:backend:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  only:
    - main
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA -f Dockerfile.backend .
    - docker push $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
    - docker tag $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE/backend:latest
    - docker push $CI_REGISTRY_IMAGE/backend:latest

build:frontend:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  only:
    - main
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA -f Dockerfile.frontend .
    - docker push $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA
    - docker tag $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE/frontend:latest
    - docker push $CI_REGISTRY_IMAGE/frontend:latest

deploy:production:
  stage: deploy
  image: alpine:latest
  only:
    - main
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts
  script:
    - |
      ssh $DEPLOY_USER@$DEPLOY_HOST << 'EOF'
        cd /opt/solarlog
        git pull origin main
        docker compose pull
        docker compose up -d
        docker compose exec -T backend alembic upgrade head
      EOF

Jenkins Pipeline

Jenkinsfile

pipeline {
    agent any

    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        IMAGE_TAG = "${BUILD_NUMBER}"
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Test Backend') {
            agent {
                docker {
                    image 'python:3.11'
                }
            }
            steps {
                sh '''
                    cd backend
                    pip install -r requirements.txt
                    pip install pytest pytest-cov
                    pytest tests/ -v --cov=app --cov-report=xml
                '''
            }
            post {
                always {
                    junit 'backend/test-results.xml'
                    cobertura coberturaReportFile: 'backend/coverage.xml'
                }
            }
        }

        stage('Test Frontend') {
            agent {
                docker {
                    image 'node:20'
                }
            }
            steps {
                sh '''
                    cd frontend-web
                    npm ci
                    npm test -- --coverage --watchAll=false
                    npm run build
                '''
            }
        }

        stage('Build Images') {
            parallel {
                stage('Build Backend') {
                    steps {
                        script {
                            docker.build("${DOCKER_REGISTRY}/solarlog-backend:${IMAGE_TAG}", "-f Dockerfile.backend .")
                        }
                    }
                }
                stage('Build Frontend') {
                    steps {
                        script {
                            docker.build("${DOCKER_REGISTRY}/solarlog-frontend:${IMAGE_TAG}", "-f Dockerfile.frontend .")
                        }
                    }
                }
            }
        }

        stage('Push Images') {
            steps {
                script {
                    docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
                        docker.image("${DOCKER_REGISTRY}/solarlog-backend:${IMAGE_TAG}").push()
                        docker.image("${DOCKER_REGISTRY}/solarlog-backend:${IMAGE_TAG}").push('latest')

                        docker.image("${DOCKER_REGISTRY}/solarlog-frontend:${IMAGE_TAG}").push()
                        docker.image("${DOCKER_REGISTRY}/solarlog-frontend:${IMAGE_TAG}").push('latest')
                    }
                }
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sshagent(['ssh-credentials']) {
                    sh '''
                        ssh user@deploy-server << 'EOF'
                            cd /opt/solarlog
                            docker compose pull
                            docker compose up -d
                            docker compose exec -T backend alembic upgrade head
                        EOF
                    '''
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        failure {
            mail to: 'team@example.com',
                 subject: "Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                 body: "Check console output at ${env.BUILD_URL}"
        }
    }
}

NixOS CI/CD

With Nix Flakes

# .github/workflows/nix.yml
name: Nix Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v23
        with:
          nix_path: nixpkgs=channel:nixos-unstable

      - name: Setup Cachix
        uses: cachix/cachix-action@v12
        with:
          name: solarlog
          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

      - name: Build backend image
        run: |
          nix build .#dockerImages.backend
          docker load < result

      - name: Build frontend image
        run: |
          nix build .#dockerImages.frontend
          docker load < result

      - name: Run tests
        run: |
          nix flake check

Deployment Strategies

Blue-Green Deployment

#!/bin/bash
# deploy-blue-green.sh

# Current active
ACTIVE=$(docker compose ps -q backend | head -1)

# Deploy new version (blue)
docker compose up -d --no-deps --scale backend=2 backend

# Wait for health check
sleep 10

# Get new container
NEW=$(docker compose ps -q backend | tail -1)

# Test new container
if curl -f http://localhost:8000/health; then
    # Success - remove old container
    docker stop $ACTIVE
    docker rm $ACTIVE
    echo "Deployment successful"
else
    # Failure - rollback
    docker stop $NEW
    docker rm $NEW
    echo "Deployment failed - rolled back"
    exit 1
fi

Canary Deployment

# docker-compose.canary.yml
services:
  backend-stable:
    image: solarlog-backend:stable
    deploy:
      replicas: 9

  backend-canary:
    image: solarlog-backend:latest
    deploy:
      replicas: 1

  nginx:
    # Nginx will load balance 90/10
    depends_on:
      - backend-stable
      - backend-canary

Database Migrations

Automated Migration in CI

#!/bin/bash
# migrate.sh

set -e

echo "πŸ”„ Running database migrations..."

docker compose exec -T backend alembic upgrade head

echo "βœ… Migrations complete"

# Verify
docker compose exec -T backend alembic current

Rollback Script

#!/bin/bash
# rollback.sh

echo "⚠️  Rolling back to previous version..."

# Downgrade database
docker compose exec -T backend alembic downgrade -1

# Deploy previous image
docker compose pull backend:previous
docker compose up -d backend

echo "βœ… Rollback complete"

Environment-Specific Configs

.env.production

# Production environment
ENVIRONMENT=production
DEBUG=false
LOG_LEVEL=INFO

# Database
DATABASE_URL=postgresql://solarlog:${DB_PASSWORD}@postgres:5432/solarlog

# Security
SECRET_KEY=${SECRET_KEY}
ALLOWED_HOSTS=solarlog.karma.organic,solarlog-api.karma.organic

# Cloudflare
CLOUDFLARE_TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}

.env.staging

# Staging environment
ENVIRONMENT=staging
DEBUG=true
LOG_LEVEL=DEBUG

DATABASE_URL=postgresql://solarlog:test@postgres-staging:5432/solarlog_staging

Secrets Management

GitHub Secrets

# Set secrets via CLI
gh secret set DEPLOY_HOST --body "production.server.com"
gh secret set DEPLOY_USER --body "deploy"
gh secret set DEPLOY_SSH_KEY < ~/.ssh/deploy_key
gh secret set DB_PASSWORD --body "$(openssl rand -hex 16)"

HashiCorp Vault

# vault.hcl
path "secret/solarlog/*" {
  capabilities = ["read"]
}

# Retrieve secrets
vault kv get secret/solarlog/production

Monitoring Deployments

Deployment Metrics

# backend/app/middleware/metrics.py
from prometheus_client import Counter

deployment_counter = Counter(
    'deployment_total',
    'Total number of deployments',
    ['service', 'status']
)

@app.on_event("startup")
async def record_deployment():
    deployment_counter.labels(service='backend', status='success').inc()

Troubleshooting CI/CD

# Check GitHub Actions logs
gh run view --log

# Re-run failed job
gh run rerun <run-id>

# Test Docker build locally
docker build -t test-backend -f Dockerfile.backend .

# Test Docker Compose
docker compose -f docker-compose.yml config

# Validate Kubernetes manifests
kubectl apply --dry-run=client -f k8s/

Next Steps