# 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