57dc91585d
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
324 lines
11 KiB
YAML
324 lines
11 KiB
YAML
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 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
|