feat: Claude Code Monitor — lanes, pipelines and a merged workspace
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.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GitHub Actions CI Pipeline – Claude Code Agent Monitor
|
||||
#
|
||||
# Triggers on push to main and PRs. Runs linting, tests, builds Docker
|
||||
# images, and scans for security vulnerabilities.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: "22"
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}/agent-monitor
|
||||
MCP_IMAGE_NAME: ${{ github.repository }}/agent-monitor-mcp
|
||||
|
||||
jobs:
|
||||
# ── Lint & Format Check ─────────────────────────────────────────────────
|
||||
lint:
|
||||
name: Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check
|
||||
|
||||
# ── Server Tests ────────────────────────────────────────────────────────
|
||||
test-server:
|
||||
name: Server Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run server tests
|
||||
run: npm run test:server
|
||||
|
||||
# ── Client Tests ────────────────────────────────────────────────────────
|
||||
test-client:
|
||||
name: Client Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install client dependencies
|
||||
run: cd client && npm ci
|
||||
|
||||
- name: Run client tests
|
||||
run: npm run test:client
|
||||
|
||||
# ── MCP Tests ───────────────────────────────────────────────────────────
|
||||
test-mcp:
|
||||
name: MCP Sidecar Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install MCP dependencies
|
||||
run: npm run mcp:install
|
||||
|
||||
- name: Type check MCP
|
||||
run: npm run mcp:typecheck
|
||||
|
||||
- name: Run MCP tests
|
||||
run: npm run mcp:test
|
||||
|
||||
# ── Build Docker Images ────────────────────────────────────────────────
|
||||
build-image:
|
||||
name: Build Docker Images
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-server, test-client, test-mcp]
|
||||
# Only push images on main branch
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.version }}
|
||||
image-digest: ${{ steps.build-app.outputs.digest }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (app)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
- name: Build & push app image
|
||||
id: build-app
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Extract metadata (MCP)
|
||||
id: meta-mcp
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build & push MCP image
|
||||
id: build-mcp
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./mcp/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta-mcp.outputs.tags }}
|
||||
labels: ${{ steps.meta-mcp.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# ── Security Scan ──────────────────────────────────────────────────────
|
||||
security-scan:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-image
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run Trivy vulnerability scanner (app)
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-image.outputs.image-tag }}"
|
||||
format: "sarif"
|
||||
output: "trivy-app-results.sarif"
|
||||
severity: "CRITICAL,HIGH"
|
||||
exit-code: "1"
|
||||
|
||||
- name: Run Trivy vulnerability scanner (MCP)
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: "${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}:${{ needs.build-image.outputs.image-tag }}"
|
||||
format: "sarif"
|
||||
output: "trivy-mcp-results.sarif"
|
||||
severity: "CRITICAL,HIGH"
|
||||
exit-code: "1"
|
||||
|
||||
- name: Upload Trivy SARIF (app)
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: "trivy-app-results.sarif"
|
||||
category: "trivy-app"
|
||||
|
||||
- name: Upload Trivy SARIF (MCP)
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: "trivy-mcp-results.sarif"
|
||||
category: "trivy-mcp"
|
||||
|
||||
- name: Run npm audit
|
||||
run: npm audit --production --audit-level=high
|
||||
|
||||
- name: Trivy filesystem scan (IaC)
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
scan-type: "fs"
|
||||
scan-ref: "./deployments"
|
||||
format: "table"
|
||||
severity: "CRITICAL,HIGH"
|
||||
exit-code: "1"
|
||||
@@ -0,0 +1,335 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GitHub Actions Deploy Pipeline – Claude Code Agent Monitor
|
||||
#
|
||||
# Triggers on version tags and manual dispatch. Deploys to staging
|
||||
# automatically and to production after manual approval.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Target environment"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
image_tag:
|
||||
description: "Image tag to deploy (default: latest from main)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write # For OIDC cloud auth
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.event.inputs.environment || 'staging' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}/agent-monitor
|
||||
MCP_IMAGE_NAME: ${{ github.repository }}/agent-monitor-mcp
|
||||
HELM_CHART_PATH: deployments/helm/agent-monitor
|
||||
|
||||
jobs:
|
||||
# ── Resolve image tag ──────────────────────────────────────────────────
|
||||
prepare:
|
||||
name: Prepare Deployment
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image-tag: ${{ steps.resolve.outputs.tag }}
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve image tag
|
||||
id: resolve
|
||||
run: |
|
||||
if [[ -n "${{ github.event.inputs.image_tag }}" ]]; then
|
||||
TAG="${{ github.event.inputs.image_tag }}"
|
||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
TAG="${{ github.ref_name }}"
|
||||
else
|
||||
TAG="sha-$(git rev-parse --short HEAD)"
|
||||
fi
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved image tag: ${TAG}"
|
||||
|
||||
# ── Build (if triggered by tag) ────────────────────────────────────────
|
||||
build:
|
||||
name: Build Images
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare
|
||||
if: github.ref_type == 'tag'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & push app image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.image-tag }}
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build & push MCP image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./mcp/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}:${{ needs.prepare.outputs.image-tag }}
|
||||
${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# ── Deploy to Staging ──────────────────────────────────────────────────
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, build]
|
||||
if: |
|
||||
always() &&
|
||||
needs.prepare.result == 'success' &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
|
||||
(github.event.inputs.environment == 'staging' || github.event.inputs.environment == '' || github.ref_type == 'tag')
|
||||
environment:
|
||||
name: staging
|
||||
url: https://staging.agent-monitor.example.com
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE_ARN_STAGING }}
|
||||
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: "v1.29.0"
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: "v3.14.0"
|
||||
|
||||
- name: Update kubeconfig
|
||||
run: |
|
||||
aws eks update-kubeconfig \
|
||||
--region ${{ vars.AWS_REGION || 'us-west-2' }} \
|
||||
--name ${{ vars.EKS_CLUSTER_STAGING || 'agent-monitor-staging' }}
|
||||
|
||||
- name: Deploy to staging via Helm
|
||||
run: |
|
||||
helm upgrade --install agent-monitor ${{ env.HELM_CHART_PATH }} \
|
||||
--namespace agent-monitor-staging \
|
||||
--create-namespace \
|
||||
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
|
||||
--set image.tag=${{ needs.prepare.outputs.image-tag }} \
|
||||
--set mcp.image.repository=${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }} \
|
||||
--set mcp.image.tag=${{ needs.prepare.outputs.image-tag }} \
|
||||
--set environment=staging \
|
||||
--set ingress.host=staging.agent-monitor.example.com \
|
||||
--values ${{ env.HELM_CHART_PATH }}/values-staging.yaml \
|
||||
--wait \
|
||||
--atomic \
|
||||
--timeout 600s
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
echo "Waiting for pods to be ready..."
|
||||
kubectl wait --for=condition=ready pod \
|
||||
-l app.kubernetes.io/name=agent-monitor \
|
||||
-n agent-monitor-staging \
|
||||
--timeout=300s
|
||||
|
||||
# Port forward and check health
|
||||
kubectl port-forward svc/agent-monitor 14820:4820 -n agent-monitor-staging &
|
||||
PF_PID=$!
|
||||
sleep 5
|
||||
|
||||
for i in $(seq 1 10); do
|
||||
if curl -sf http://localhost:14820/api/health | grep -q '"status":"ok"'; then
|
||||
echo "✔ Health check passed"
|
||||
kill $PF_PID 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/10..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
kill $PF_PID 2>/dev/null || true
|
||||
echo "✖ Health check failed"
|
||||
exit 1
|
||||
|
||||
- name: Notify Slack (staging)
|
||||
if: always()
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Staging deployment ${{ job.status }}: `${{ needs.prepare.outputs.image-tag }}`",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "${{ job.status == 'success' && ':white_check_mark:' || ':x:' }} *Staging Deployment ${{ job.status }}*\n*Image:* `${{ needs.prepare.outputs.image-tag }}`\n*Commit:* `${{ github.sha }}`\n*Actor:* ${{ github.actor }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
# ── Deploy to Production ───────────────────────────────────────────────
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, deploy-staging]
|
||||
if: |
|
||||
always() &&
|
||||
needs.prepare.result == 'success' &&
|
||||
needs.deploy-staging.result == 'success' &&
|
||||
(github.event.inputs.environment == 'production' || github.ref_type == 'tag')
|
||||
environment:
|
||||
name: production
|
||||
url: https://agent-monitor.example.com
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE_ARN_PRODUCTION }}
|
||||
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: "v1.29.0"
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: "v3.14.0"
|
||||
|
||||
- name: Update kubeconfig
|
||||
run: |
|
||||
aws eks update-kubeconfig \
|
||||
--region ${{ vars.AWS_REGION || 'us-west-2' }} \
|
||||
--name ${{ vars.EKS_CLUSTER_PRODUCTION || 'agent-monitor-production' }}
|
||||
|
||||
- name: Create database backup
|
||||
run: |
|
||||
chmod +x deployments/scripts/db-backup.sh
|
||||
# Find a running pod to backup from
|
||||
POD=$(kubectl get pods -n agent-monitor-production \
|
||||
-l app.kubernetes.io/name=agent-monitor \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$POD" ]]; then
|
||||
echo "Backing up database from pod: $POD"
|
||||
kubectl exec "$POD" -n agent-monitor-production -- \
|
||||
sh -c "cp /app/data/dashboard.db /tmp/pre-deploy-backup.db 2>/dev/null || true"
|
||||
echo "Pre-deploy backup created"
|
||||
else
|
||||
echo "⚠ No running pods found – skipping backup"
|
||||
fi
|
||||
|
||||
- name: Deploy to production via Helm
|
||||
run: |
|
||||
helm upgrade --install agent-monitor ${{ env.HELM_CHART_PATH }} \
|
||||
--namespace agent-monitor-production \
|
||||
--create-namespace \
|
||||
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
|
||||
--set image.tag=${{ needs.prepare.outputs.image-tag }} \
|
||||
--set mcp.image.repository=${{ env.REGISTRY }}/${{ env.MCP_IMAGE_NAME }} \
|
||||
--set mcp.image.tag=${{ needs.prepare.outputs.image-tag }} \
|
||||
--set environment=production \
|
||||
--set ingress.host=agent-monitor.example.com \
|
||||
--values ${{ env.HELM_CHART_PATH }}/values-production.yaml \
|
||||
--wait \
|
||||
--atomic \
|
||||
--timeout 600s
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
echo "Waiting for pods to be ready..."
|
||||
kubectl wait --for=condition=ready pod \
|
||||
-l app.kubernetes.io/name=agent-monitor \
|
||||
-n agent-monitor-production \
|
||||
--timeout=300s
|
||||
|
||||
kubectl port-forward svc/agent-monitor 14820:4820 -n agent-monitor-production &
|
||||
PF_PID=$!
|
||||
sleep 5
|
||||
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:14820/api/health | grep -q '"status":"ok"'; then
|
||||
echo "✔ Production health check passed"
|
||||
kill $PF_PID 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/15..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
kill $PF_PID 2>/dev/null || true
|
||||
echo "✖ Production health check failed!"
|
||||
exit 1
|
||||
|
||||
- name: Notify Slack (production)
|
||||
if: always()
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status == 'success' && '🚀' || '🚨' }} Production deployment ${{ job.status }}: `${{ needs.prepare.outputs.image-tag }}`",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "${{ job.status == 'success' && ':rocket:' || ':rotating_light:' }} *Production Deployment ${{ job.status }}*\n*Image:* `${{ needs.prepare.outputs.image-tag }}`\n*Version:* `${{ needs.prepare.outputs.version }}`\n*Commit:* `${{ github.sha }}`\n*Actor:* ${{ github.actor }}\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
@@ -0,0 +1,160 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GitHub Actions Rollback Pipeline – Claude Code Agent Monitor
|
||||
#
|
||||
# Manual workflow to roll back a Helm deployment to a previous revision.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: Rollback
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Target environment to rollback"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
revision:
|
||||
description: "Helm revision number (leave empty for previous)"
|
||||
required: false
|
||||
type: string
|
||||
reason:
|
||||
description: "Reason for rollback"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.event.inputs.environment }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
rollback:
|
||||
name: Rollback ${{ github.event.inputs.environment }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ github.event.inputs.environment }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ github.event.inputs.environment == 'production' && secrets.AWS_ROLE_ARN_PRODUCTION || secrets.AWS_ROLE_ARN_STAGING }}
|
||||
aws-region: ${{ vars.AWS_REGION || 'us-west-2' }}
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: "v1.29.0"
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: "v3.14.0"
|
||||
|
||||
- name: Update kubeconfig
|
||||
run: |
|
||||
CLUSTER_NAME="${{ github.event.inputs.environment == 'production' && vars.EKS_CLUSTER_PRODUCTION || vars.EKS_CLUSTER_STAGING }}"
|
||||
CLUSTER_NAME="${CLUSTER_NAME:-agent-monitor-${{ github.event.inputs.environment }}}"
|
||||
aws eks update-kubeconfig \
|
||||
--region ${{ vars.AWS_REGION || 'us-west-2' }} \
|
||||
--name "${CLUSTER_NAME}"
|
||||
|
||||
- name: Show Helm history
|
||||
run: |
|
||||
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
|
||||
echo "## Current Helm History"
|
||||
helm history agent-monitor -n "${NAMESPACE}" --max 10 || echo "No history found"
|
||||
|
||||
- name: Execute rollback
|
||||
run: |
|
||||
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
|
||||
REVISION="${{ github.event.inputs.revision }}"
|
||||
|
||||
echo "Rolling back in namespace: ${NAMESPACE}"
|
||||
|
||||
ROLLBACK_ARGS="helm rollback agent-monitor"
|
||||
if [[ -n "${REVISION}" ]]; then
|
||||
ROLLBACK_ARGS="${ROLLBACK_ARGS} ${REVISION}"
|
||||
echo "Target revision: ${REVISION}"
|
||||
else
|
||||
echo "Target revision: previous"
|
||||
fi
|
||||
|
||||
${ROLLBACK_ARGS} -n "${NAMESPACE}" --wait --timeout 300s
|
||||
|
||||
echo "✔ Rollback command succeeded"
|
||||
|
||||
- name: Health check after rollback
|
||||
run: |
|
||||
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
|
||||
|
||||
echo "Waiting for pods to be ready..."
|
||||
kubectl wait --for=condition=ready pod \
|
||||
-l app.kubernetes.io/name=agent-monitor \
|
||||
-n "${NAMESPACE}" \
|
||||
--timeout=300s
|
||||
|
||||
kubectl port-forward svc/agent-monitor 14820:4820 -n "${NAMESPACE}" &
|
||||
PF_PID=$!
|
||||
sleep 5
|
||||
|
||||
HEALTHY=false
|
||||
for i in $(seq 1 10); do
|
||||
if curl -sf http://localhost:14820/api/health | grep -q '"status":"ok"'; then
|
||||
echo "✔ Health check passed after rollback"
|
||||
HEALTHY=true
|
||||
break
|
||||
fi
|
||||
echo "Attempt $i/10..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
kill $PF_PID 2>/dev/null || true
|
||||
|
||||
if [[ "$HEALTHY" != true ]]; then
|
||||
echo "✖ Health check failed after rollback!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Show post-rollback status
|
||||
if: always()
|
||||
run: |
|
||||
NAMESPACE="agent-monitor-${{ github.event.inputs.environment }}"
|
||||
echo "## Post-Rollback Status"
|
||||
echo ""
|
||||
echo "### Helm Status"
|
||||
helm status agent-monitor -n "${NAMESPACE}" || true
|
||||
echo ""
|
||||
echo "### Pod Status"
|
||||
kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=agent-monitor || true
|
||||
echo ""
|
||||
echo "### Recent Events"
|
||||
kubectl get events -n "${NAMESPACE}" --sort-by='.lastTimestamp' | tail -20 || true
|
||||
|
||||
- name: Notify Slack
|
||||
if: always()
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status == 'success' && '⏪' || '🚨' }} Rollback ${{ job.status }} on ${{ github.event.inputs.environment }}",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "${{ job.status == 'success' && ':rewind:' || ':rotating_light:' }} *Rollback ${{ job.status }}*\n*Environment:* `${{ github.event.inputs.environment }}`\n*Revision:* `${{ github.event.inputs.revision || 'previous' }}`\n*Reason:* ${{ github.event.inputs.reason }}\n*Actor:* ${{ github.actor }}\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
@@ -0,0 +1,323 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
Reference in New Issue
Block a user