437 lines
14 KiB
YAML
437 lines
14 KiB
YAML
# MotoVaultPro GitLab CI/CD Pipeline - Blue-Green Deployment
|
|
# GitLab 18.6+ with separate build and production runners
|
|
# See docs/CICD-DEPLOY.md for complete documentation
|
|
# v2.0 - Blue-Green with Auto-Rollback
|
|
|
|
stages:
|
|
- validate
|
|
- build
|
|
- deploy-prepare
|
|
- deploy-switch
|
|
- verify
|
|
- rollback
|
|
- notify
|
|
|
|
variables:
|
|
# Registry configuration
|
|
REGISTRY: registry.motovaultpro.com
|
|
REGISTRY_MIRRORS: ${REGISTRY}/mirrors
|
|
IMAGE_TAG: ${CI_COMMIT_SHORT_SHA}
|
|
BACKEND_IMAGE: ${REGISTRY}/motovaultpro/backend:${IMAGE_TAG}
|
|
FRONTEND_IMAGE: ${REGISTRY}/motovaultpro/frontend:${IMAGE_TAG}
|
|
|
|
# Deployment configuration
|
|
GIT_CLONE_PATH: ${CI_BUILDS_DIR}/motovaultpro
|
|
DEPLOY_PATH: ${CI_BUILDS_DIR}/motovaultpro
|
|
COMPOSE_FILE: docker-compose.yml
|
|
COMPOSE_BLUE_GREEN: docker-compose.blue-green.yml
|
|
|
|
# Health check configuration
|
|
HEALTH_CHECK_TIMEOUT: "60"
|
|
|
|
# Default after_script to fix permissions
|
|
default:
|
|
after_script:
|
|
- echo "Fixing file permissions..."
|
|
- sudo chown -R gitlab-runner:gitlab-runner "$DEPLOY_PATH" 2>/dev/null || true
|
|
- sudo chown -R 1001:1001 "$DEPLOY_PATH/data/backups" "$DEPLOY_PATH/data/documents" 2>/dev/null || true
|
|
|
|
# ============================================
|
|
# Stage 1: VALIDATE
|
|
# Check prerequisites before starting pipeline
|
|
# ============================================
|
|
validate:
|
|
stage: validate
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
script:
|
|
- echo "=========================================="
|
|
- echo "Validating deployment prerequisites..."
|
|
- echo "=========================================="
|
|
- echo "Checking Docker..."
|
|
- docker info > /dev/null 2>&1 || (echo "ERROR - Docker not accessible" && exit 1)
|
|
- echo "OK - Docker is accessible"
|
|
- echo "Checking Docker Compose..."
|
|
- docker compose version > /dev/null 2>&1 || (echo "ERROR - Docker Compose not available" && exit 1)
|
|
- echo "OK - Docker Compose is available"
|
|
- echo "Checking deployment path..."
|
|
- test -d "$DEPLOY_PATH" || (echo "ERROR - DEPLOY_PATH not found" && exit 1)
|
|
- echo "OK - Deployment path exists"
|
|
- echo "Checking registry access..."
|
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY" || true
|
|
- echo "OK - Registry authentication configured"
|
|
- echo "Determining target stack..."
|
|
- |
|
|
STATE_FILE="$DEPLOY_PATH/config/deployment/state.json"
|
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
ACTIVE_STACK=$(jq -r '.active_stack // "blue"' "$STATE_FILE")
|
|
if [ "$ACTIVE_STACK" = "blue" ]; then
|
|
echo "TARGET_STACK=green" >> deploy.env
|
|
else
|
|
echo "TARGET_STACK=blue" >> deploy.env
|
|
fi
|
|
else
|
|
echo "TARGET_STACK=green" >> deploy.env
|
|
fi
|
|
cat deploy.env
|
|
- echo "=========================================="
|
|
- echo "Validation complete"
|
|
- echo "=========================================="
|
|
artifacts:
|
|
reports:
|
|
dotenv: deploy.env
|
|
|
|
# ============================================
|
|
# Stage 2: BUILD
|
|
# Build and push images to GitLab Container Registry
|
|
# Runs on dedicated build server (shell executor)
|
|
# ============================================
|
|
build:
|
|
stage: build
|
|
tags:
|
|
- build
|
|
only:
|
|
- main
|
|
script:
|
|
- echo "Authenticating with GitLab Container Registry..."
|
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY"
|
|
- echo "=========================================="
|
|
- echo "Building Docker images..."
|
|
- echo "Commit - ${CI_COMMIT_SHORT_SHA}"
|
|
- echo "Backend - ${BACKEND_IMAGE}"
|
|
- echo "Frontend - ${FRONTEND_IMAGE}"
|
|
- echo "=========================================="
|
|
|
|
# Build backend
|
|
- echo "Building backend..."
|
|
- |
|
|
docker build \
|
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
|
--cache-from ${REGISTRY}/motovaultpro/backend:latest \
|
|
-t ${BACKEND_IMAGE} \
|
|
-t ${REGISTRY}/motovaultpro/backend:latest \
|
|
-f backend/Dockerfile \
|
|
.
|
|
|
|
# Build frontend
|
|
- echo "Building frontend..."
|
|
- |
|
|
docker build \
|
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
|
--build-arg VITE_AUTH0_DOMAIN=${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com} \
|
|
--build-arg VITE_AUTH0_CLIENT_ID=${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3} \
|
|
--build-arg VITE_AUTH0_AUDIENCE=${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com} \
|
|
--build-arg VITE_API_BASE_URL=/api \
|
|
--cache-from ${REGISTRY}/motovaultpro/frontend:latest \
|
|
-t ${FRONTEND_IMAGE} \
|
|
-t ${REGISTRY}/motovaultpro/frontend:latest \
|
|
-f frontend/Dockerfile \
|
|
frontend
|
|
|
|
# Push images
|
|
- echo "Pushing images to registry..."
|
|
- docker push ${BACKEND_IMAGE}
|
|
- docker push ${FRONTEND_IMAGE}
|
|
- docker push ${REGISTRY}/motovaultpro/backend:latest
|
|
- docker push ${REGISTRY}/motovaultpro/frontend:latest
|
|
|
|
- echo "=========================================="
|
|
- echo "Build complete"
|
|
- echo "=========================================="
|
|
|
|
# ============================================
|
|
# Stage 3: DEPLOY-PREPARE
|
|
# Pull images, start inactive stack, run health checks
|
|
# ============================================
|
|
deploy-prepare:
|
|
stage: deploy-prepare
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
needs:
|
|
- job: validate
|
|
artifacts: true
|
|
- job: build
|
|
environment:
|
|
name: production
|
|
url: https://motovaultpro.com
|
|
script:
|
|
- echo "=========================================="
|
|
- echo "Preparing deployment to ${TARGET_STACK} stack..."
|
|
- echo "=========================================="
|
|
- cd "$DEPLOY_PATH"
|
|
|
|
# Authenticate with registry
|
|
- echo "Authenticating with registry..."
|
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY"
|
|
|
|
# Inject secrets
|
|
- echo "Step 1/5 - Injecting secrets..."
|
|
- chmod +x scripts/inject-secrets.sh
|
|
- ./scripts/inject-secrets.sh
|
|
|
|
# Initialize data directories
|
|
- echo "Step 2/5 - Initializing data directories..."
|
|
- sudo mkdir -p data/backups data/documents
|
|
- sudo chown -R 1001:1001 data/backups data/documents
|
|
- sudo chmod 755 data/backups data/documents
|
|
|
|
# Pull new images
|
|
- echo "Step 3/5 - Pulling images..."
|
|
- docker pull ${BACKEND_IMAGE}
|
|
- docker pull ${FRONTEND_IMAGE}
|
|
|
|
# Start inactive stack
|
|
- echo "Step 4/5 - Starting ${TARGET_STACK} stack..."
|
|
- |
|
|
export BACKEND_IMAGE=${BACKEND_IMAGE}
|
|
export FRONTEND_IMAGE=${FRONTEND_IMAGE}
|
|
docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d \
|
|
mvp-frontend-${TARGET_STACK} mvp-backend-${TARGET_STACK}
|
|
|
|
# Wait for stack to be ready
|
|
- echo "Step 5/5 - Waiting for stack health..."
|
|
- sleep 10
|
|
|
|
# Run health check
|
|
- echo "Running health check on ${TARGET_STACK} stack..."
|
|
- chmod +x scripts/ci/health-check.sh
|
|
- ./scripts/ci/health-check.sh ${TARGET_STACK} ${HEALTH_CHECK_TIMEOUT}
|
|
|
|
# Update state with deployment info
|
|
- |
|
|
STATE_FILE="config/deployment/state.json"
|
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
jq --arg stack "$TARGET_STACK" \
|
|
--arg commit "$CI_COMMIT_SHORT_SHA" \
|
|
--arg ts "$TIMESTAMP" \
|
|
'.[$stack].version = $commit | .[$stack].commit = $commit | .[$stack].deployed_at = $ts | .[$stack].healthy = true' \
|
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
fi
|
|
|
|
- echo "=========================================="
|
|
- echo "Deploy preparation complete"
|
|
- echo "=========================================="
|
|
|
|
# ============================================
|
|
# Stage 4: DEPLOY-SWITCH
|
|
# Switch traffic to new stack
|
|
# ============================================
|
|
deploy-switch:
|
|
stage: deploy-switch
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
needs:
|
|
- job: validate
|
|
artifacts: true
|
|
- job: deploy-prepare
|
|
script:
|
|
- echo "=========================================="
|
|
- echo "Switching traffic to ${TARGET_STACK} stack..."
|
|
- echo "=========================================="
|
|
- cd "$DEPLOY_PATH"
|
|
|
|
# Switch traffic
|
|
- chmod +x scripts/ci/switch-traffic.sh
|
|
- ./scripts/ci/switch-traffic.sh ${TARGET_STACK} instant
|
|
|
|
# Update state
|
|
- |
|
|
STATE_FILE="config/deployment/state.json"
|
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
jq --arg commit "$CI_COMMIT_SHORT_SHA" \
|
|
--arg ts "$TIMESTAMP" \
|
|
'.last_deployment = $ts | .last_deployment_commit = $commit | .last_deployment_status = "success" | .rollback_available = true' \
|
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
fi
|
|
|
|
- echo "=========================================="
|
|
- echo "Traffic switch complete"
|
|
- echo "=========================================="
|
|
|
|
# ============================================
|
|
# Stage 5: VERIFY
|
|
# Production health verification after switch
|
|
# ============================================
|
|
verify:
|
|
stage: verify
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
needs:
|
|
- job: validate
|
|
artifacts: true
|
|
- job: deploy-switch
|
|
script:
|
|
- echo "=========================================="
|
|
- echo "Verifying production deployment..."
|
|
- echo "=========================================="
|
|
- cd "$DEPLOY_PATH"
|
|
|
|
# Wait for Traefik to propagate routing
|
|
- echo "Waiting for traffic routing to stabilize..."
|
|
- sleep 5
|
|
|
|
# Verify via external endpoint
|
|
- echo "Checking external endpoint..."
|
|
- |
|
|
for i in 1 2 3 4 5 6; do
|
|
if curl -sf https://motovaultpro.com/api/health > /dev/null 2>&1; then
|
|
echo "OK - External health check passed"
|
|
break
|
|
fi
|
|
if [ $i -eq 6 ]; then
|
|
echo "ERROR - External health check failed after 6 attempts"
|
|
exit 1
|
|
fi
|
|
echo "Attempt $i/6 - Waiting 10s..."
|
|
sleep 10
|
|
done
|
|
|
|
# Verify container status
|
|
- echo "Checking container status..."
|
|
- |
|
|
for service in mvp-frontend-${TARGET_STACK} mvp-backend-${TARGET_STACK}; do
|
|
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
|
|
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
|
|
if [ "$status" != "running" ] || [ "$health" != "healthy" ]; then
|
|
echo "ERROR - $service is not healthy (status: $status, health: $health)"
|
|
docker logs $service --tail 50 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
echo "OK - $service is running and healthy"
|
|
done
|
|
|
|
- echo "=========================================="
|
|
- echo "Deployment verified successfully!"
|
|
- echo "Version ${CI_COMMIT_SHORT_SHA} is now live"
|
|
- echo "=========================================="
|
|
|
|
# ============================================
|
|
# Stage 6: ROLLBACK (on failure)
|
|
# Automatic rollback if verify stage fails
|
|
# ============================================
|
|
rollback:
|
|
stage: rollback
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
when: on_failure
|
|
needs:
|
|
- job: validate
|
|
artifacts: true
|
|
- job: deploy-switch
|
|
- job: verify
|
|
script:
|
|
- echo "=========================================="
|
|
- echo "INITIATING AUTO-ROLLBACK"
|
|
- echo "=========================================="
|
|
- cd "$DEPLOY_PATH"
|
|
|
|
# Run rollback script
|
|
- chmod +x scripts/ci/auto-rollback.sh
|
|
- ./scripts/ci/auto-rollback.sh "Verify stage failed - automatic rollback"
|
|
|
|
# Update state
|
|
- |
|
|
STATE_FILE="config/deployment/state.json"
|
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
jq '.last_deployment_status = "rolled_back"' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
fi
|
|
|
|
- echo "=========================================="
|
|
- echo "Rollback complete"
|
|
- echo "=========================================="
|
|
|
|
# ============================================
|
|
# Stage 7: NOTIFY
|
|
# Send deployment notifications
|
|
# ============================================
|
|
notify-success:
|
|
stage: notify
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
needs:
|
|
- job: verify
|
|
script:
|
|
- echo "Sending success notification..."
|
|
- cd "$DEPLOY_PATH"
|
|
- chmod +x scripts/ci/notify.sh
|
|
- ./scripts/ci/notify.sh success "Version ${CI_COMMIT_SHORT_SHA} deployed successfully" ${CI_COMMIT_SHORT_SHA}
|
|
|
|
notify-failure:
|
|
stage: notify
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
when: on_failure
|
|
needs:
|
|
- job: build
|
|
optional: true
|
|
- job: deploy-prepare
|
|
optional: true
|
|
- job: deploy-switch
|
|
optional: true
|
|
- job: verify
|
|
optional: true
|
|
script:
|
|
- echo "Sending failure notification..."
|
|
- cd "$DEPLOY_PATH"
|
|
- chmod +x scripts/ci/notify.sh
|
|
- ./scripts/ci/notify.sh failure "Deployment of ${CI_COMMIT_SHORT_SHA} failed" ${CI_COMMIT_SHORT_SHA}
|
|
|
|
# ============================================
|
|
# Manual Jobs
|
|
# ============================================
|
|
|
|
# Manual maintenance migration job
|
|
maintenance-migration:
|
|
stage: deploy-prepare
|
|
tags:
|
|
- production
|
|
- shell
|
|
only:
|
|
- main
|
|
when: manual
|
|
script:
|
|
- echo "=========================================="
|
|
- echo "MAINTENANCE MODE MIGRATION"
|
|
- echo "=========================================="
|
|
- cd "$DEPLOY_PATH"
|
|
- chmod +x scripts/ci/maintenance-migrate.sh
|
|
- ./scripts/ci/maintenance-migrate.sh backup
|
|
|
|
# Mirror base images (scheduled or manual)
|
|
mirror-images:
|
|
stage: build
|
|
tags:
|
|
- build
|
|
only:
|
|
- schedules
|
|
- web
|
|
when: manual
|
|
script:
|
|
- echo "Mirroring base images to GitLab Container Registry..."
|
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY"
|
|
- chmod +x scripts/ci/mirror-base-images.sh
|
|
- REGISTRY=${REGISTRY}/mirrors ./scripts/ci/mirror-base-images.sh
|