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/