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.
336 lines
12 KiB
YAML
336 lines
12 KiB
YAML
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 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
|