# ───────────────────────────────────────────────────────────────────────────── # 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