# ───────────────────────────────────────────────────────────────────────────── # GitLab CI/CD Pipeline – Claude Code Agent Monitor # # Stages: test → build → deploy-staging → deploy-production # ───────────────────────────────────────────────────────────────────────────── # ── Global settings ───────────────────────────────────────────────────────── default: image: node:22-alpine interruptible: true retry: max: 1 when: - runner_system_failure - stuck_or_timeout_failure variables: NODE_VERSION: "22" REGISTRY: "${CI_REGISTRY}" IMAGE_NAME: "${CI_REGISTRY_IMAGE}/agent-monitor" MCP_IMAGE_NAME: "${CI_REGISTRY_IMAGE}/agent-monitor-mcp" HELM_CHART_PATH: "deployments/helm/agent-monitor" APP_NAME: "agent-monitor" # Kaniko cache KANIKO_CACHE_ARGS: "--cache=true --cache-repo=${CI_REGISTRY_IMAGE}/cache" stages: - test - build - deploy-staging - deploy-production - rollback # ── Cache configuration ──────────────────────────────────────────────────── .node_cache: &node_cache cache: key: files: - package-lock.json paths: - node_modules/ policy: pull-push # ── Test stage ────────────────────────────────────────────────────────────── lint: stage: test <<: *node_cache script: - npm ci --prefer-offline - npm run format:check rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH test:server: stage: test <<: *node_cache script: - npm ci --prefer-offline - npm run test:server rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH artifacts: when: on_failure paths: - server/__tests__/ expire_in: 7 days test:client: stage: test <<: *node_cache script: - npm ci --prefer-offline - cd client && npm ci --prefer-offline - npm run test:client cache: key: files: - client/package-lock.json paths: - client/node_modules/ policy: pull-push rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH test:mcp: stage: test <<: *node_cache script: - npm run mcp:install - npm run mcp:typecheck - npm run mcp:test rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # ── Build stage ───────────────────────────────────────────────────────────── .kaniko_build: &kaniko_build stage: build image: name: gcr.io/kaniko-project/executor:v1.22.0-debug entrypoint: [""] before_script: - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64)\"}}}" > /kaniko/.docker/config.json build:app: <<: *kaniko_build script: - >- /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" --destination "${IMAGE_NAME}:${CI_COMMIT_REF_SLUG}" --destination "${IMAGE_NAME}:latest" ${KANIKO_CACHE_ARGS} --label "org.opencontainers.image.revision=${CI_COMMIT_SHA}" --label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" --label "org.opencontainers.image.source=${CI_PROJECT_URL}" rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ build:mcp: <<: *kaniko_build script: - >- /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/mcp/Dockerfile" --destination "${MCP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" --destination "${MCP_IMAGE_NAME}:${CI_COMMIT_REF_SLUG}" --destination "${MCP_IMAGE_NAME}:latest" ${KANIKO_CACHE_ARGS} rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # Security scan security:scan: stage: build needs: ["build:app", "build:mcp"] image: name: aquasec/trivy:latest entrypoint: [""] script: - trivy image --exit-code 1 --severity HIGH,CRITICAL --format table "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" - trivy image --exit-code 1 --severity HIGH,CRITICAL --format table "${MCP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" - trivy image --exit-code 1 --severity CRITICAL --format json --output trivy-app-report.json "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" - trivy image --exit-code 1 --severity CRITICAL --format json --output trivy-mcp-report.json "${MCP_IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}" artifacts: paths: - trivy-app-report.json - trivy-mcp-report.json expire_in: 30 days allow_failure: false rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # ── Deploy Staging ────────────────────────────────────────────────────────── deploy:staging: stage: deploy-staging image: name: alpine/helm:3.14.0 entrypoint: [""] needs: - build:app - build:mcp environment: name: staging url: https://staging.agent-monitor.example.com on_stop: stop:staging before_script: - apk add --no-cache curl aws-cli kubectl - aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_STAGING:-agent-monitor-staging}" script: - | helm upgrade --install ${APP_NAME} ${HELM_CHART_PATH} \ --namespace agent-monitor-staging \ --create-namespace \ --set image.repository=${IMAGE_NAME} \ --set image.tag=${CI_COMMIT_SHORT_SHA} \ --set mcp.image.repository=${MCP_IMAGE_NAME} \ --set mcp.image.tag=${CI_COMMIT_SHORT_SHA} \ --set environment=staging \ --values ${HELM_CHART_PATH}/values-staging.yaml \ --wait \ --atomic \ --timeout 600s - | echo "Running health check..." kubectl wait --for=condition=ready pod \ -l app.kubernetes.io/name=${APP_NAME} \ -n agent-monitor-staging \ --timeout=300s echo "✔ Staging deployment successful" rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ stop:staging: stage: deploy-staging image: name: alpine/helm:3.14.0 entrypoint: [""] environment: name: staging action: stop before_script: - apk add --no-cache aws-cli kubectl - aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_STAGING:-agent-monitor-staging}" script: - helm uninstall ${APP_NAME} -n agent-monitor-staging --wait || true when: manual rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # ── Deploy Production ─────────────────────────────────────────────────────── deploy:production: stage: deploy-production image: name: alpine/helm:3.14.0 entrypoint: [""] needs: - deploy:staging environment: name: production url: https://agent-monitor.example.com before_script: - apk add --no-cache curl aws-cli kubectl - aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_PRODUCTION:-agent-monitor-production}" script: # Pre-deploy backup - | POD=$(kubectl get pods -n agent-monitor-production \ -l app.kubernetes.io/name=${APP_NAME} \ --field-selector=status.phase=Running \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") if [ -n "$POD" ]; then echo "Creating pre-deploy backup..." kubectl exec "$POD" -n agent-monitor-production -- \ sh -c "cp /app/data/dashboard.db /tmp/pre-deploy-backup.db" 2>/dev/null || true fi # Deploy - | helm upgrade --install ${APP_NAME} ${HELM_CHART_PATH} \ --namespace agent-monitor-production \ --create-namespace \ --set image.repository=${IMAGE_NAME} \ --set image.tag=${CI_COMMIT_SHORT_SHA} \ --set mcp.image.repository=${MCP_IMAGE_NAME} \ --set mcp.image.tag=${CI_COMMIT_SHORT_SHA} \ --set environment=production \ --values ${HELM_CHART_PATH}/values-production.yaml \ --wait \ --atomic \ --timeout 600s # Health check - | echo "Running production health check..." kubectl wait --for=condition=ready pod \ -l app.kubernetes.io/name=${APP_NAME} \ -n agent-monitor-production \ --timeout=300s echo "✔ Production deployment successful" when: manual allow_failure: false rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # ── Rollback ──────────────────────────────────────────────────────────────── rollback:staging: stage: rollback image: name: alpine/helm:3.14.0 entrypoint: [""] environment: name: staging before_script: - apk add --no-cache aws-cli kubectl - aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_STAGING:-agent-monitor-staging}" script: - echo "Rolling back staging..." - helm rollback ${APP_NAME} ${ROLLBACK_REVISION:-0} -n agent-monitor-staging --wait --timeout 300s - | kubectl wait --for=condition=ready pod \ -l app.kubernetes.io/name=${APP_NAME} \ -n agent-monitor-staging \ --timeout=300s - echo "✔ Staging rollback complete" when: manual rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH rollback:production: stage: rollback image: name: alpine/helm:3.14.0 entrypoint: [""] environment: name: production before_script: - apk add --no-cache aws-cli kubectl - aws eks update-kubeconfig --region "${AWS_REGION:-us-west-2}" --name "${EKS_CLUSTER_PRODUCTION:-agent-monitor-production}" script: - echo "⚠ Rolling back PRODUCTION..." - helm history ${APP_NAME} -n agent-monitor-production --max 5 - helm rollback ${APP_NAME} ${ROLLBACK_REVISION:-0} -n agent-monitor-production --wait --timeout 300s - | kubectl wait --for=condition=ready pod \ -l app.kubernetes.io/name=${APP_NAME} \ -n agent-monitor-production \ --timeout=300s - echo "✔ Production rollback complete" when: manual allow_failure: false rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH