From c51eba4f0f42117e784fc5917364cda44228d158 Mon Sep 17 00:00:00 2001 From: Jonathan Flatt Date: Fri, 23 May 2025 23:39:06 +0000 Subject: [PATCH] Add deployment workflow and scripts for self-hosted runner - Add GitHub Actions deployment workflow for staging and production - Add deployment scripts for automated deployments - Add GitHub runner management scripts - Add staging docker-compose configuration - Enable automatic deployments on push to main (staging) and version tags (production) --- .github/workflows/deploy.yml | 326 +++++++++++++++++++++----- docker-compose.staging.yml | 74 ++++++ scripts/deploy/deploy-production.sh | 202 ++++++++++++++++ scripts/deploy/deploy-staging.sh | 121 ++++++++++ scripts/manage-runner.sh | 336 +++++++++++++++++++++++++++ scripts/setup/setup-github-runner.sh | 91 ++++++++ 6 files changed, 1087 insertions(+), 63 deletions(-) create mode 100644 docker-compose.staging.yml create mode 100755 scripts/deploy/deploy-production.sh create mode 100755 scripts/deploy/deploy-staging.sh create mode 100755 scripts/manage-runner.sh create mode 100755 scripts/setup/setup-github-runner.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3a9d760..f0b54f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,81 +1,281 @@ -name: Deploy +name: CI/CD Pipeline on: push: - branches: [ main ] + branches: + - main + - develop + tags: + - 'v*.*.*' # Semantic versioning tags (v1.0.0, v2.1.3, etc.) + pull_request: + types: [opened, synchronize, reopened] env: - NODE_VERSION: '20' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: - build-and-push: - name: Build & Push Images + # ============================================ + # CI Jobs - Run on GitHub-hosted runners + # ============================================ + + test: + name: Run Tests runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x] - permissions: - contents: read - packages: write - steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test + + - name: Run type checking + run: npm run typecheck + + - name: Upload coverage + if: matrix.node-version == '20.x' + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push main image - uses: docker/build-push-action@v6 - 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 - - - name: Build and push Claude Code image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile.claudecode - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-claudecode:latest - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy: - name: Deploy to Staging + build: + name: Build Docker Image runs-on: ubuntu-latest - needs: [build-and-push] + needs: test + + outputs: + image-tag: ${{ steps.meta.outputs.tags }} + image-digest: ${{ steps.build.outputs.digest }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}}- + type=raw,value=staging,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v5 + with: + context: . + 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 + + security-scan: + name: Security Scanning + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ needs.build.outputs.image-tag }} + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + # ============================================ + # CD Jobs - Run on self-hosted runners + # ============================================ + + deploy-staging: + name: Deploy to Staging + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + needs: [build, security-scan] + runs-on: [self-hosted, linux, x64, deployment, webhook-cd] environment: staging steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - name: Create .env file for staging + run: | + cat > .env.staging << EOF + GITHUB_APP_ID_STAGING=${{ secrets.GITHUB_APP_ID_STAGING }} + GITHUB_PRIVATE_KEY_STAGING=${{ secrets.GITHUB_PRIVATE_KEY_STAGING }} + GITHUB_WEBHOOK_SECRET_STAGING=${{ secrets.GITHUB_WEBHOOK_SECRET_STAGING }} + ANTHROPIC_API_KEY_STAGING=${{ secrets.ANTHROPIC_API_KEY_STAGING }} + MCP_SERVER_URL_STAGING=${{ vars.MCP_SERVER_URL_STAGING }} + ALLOWED_ORGS_STAGING=${{ vars.ALLOWED_ORGS_STAGING }} + ALLOWED_REPOS_STAGING=${{ vars.ALLOWED_REPOS_STAGING }} + EOF + + - name: Deploy to staging + run: | + export $(cat .env.staging | xargs) + ./scripts/deploy/deploy-staging.sh + + - name: Clean up + if: always() + run: rm -f .env.staging + + - name: Create deployment record + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.sha, + environment: 'staging', + required_contexts: [], + auto_merge: false, + description: 'Staging deployment from main branch' + }); + + - name: Notify deployment status + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + text: 'Staging deployment ${{ job.status }}' + webhook_url: ${{ secrets.SLACK_WEBHOOK }} - - name: Deploy notification - run: | - echo "🚀 Deployment to staging would happen here" - echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" - # Add actual deployment logic here (e.g., update Kubernetes, docker-compose, etc.) \ No newline at end of file + deploy-production: + name: Deploy to Production + if: startsWith(github.ref, 'refs/tags/v') + needs: [build, security-scan] + runs-on: [self-hosted, linux, x64, deployment, webhook-cd] + environment: + name: production + url: https://webhook.yourdomain.com + + steps: + - uses: actions/checkout@v4 + + - name: Validate tag is on main branch + run: | + # Get the commit SHA that the tag points to + TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }}) + + # Check if this commit exists on main branch + if ! git branch -r --contains $TAG_COMMIT | grep -q "origin/main"; then + echo "Error: Tag ${{ github.ref_name }} is not on the main branch!" + echo "Production deployments must be tagged from the main branch." + exit 1 + fi + + echo "✓ Tag ${{ github.ref_name }} is on main branch" + + - name: Extract version info + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Deploying version: $VERSION" + + - name: Create .env file for production + run: | + cat > .env << EOF + GITHUB_APP_ID=${{ secrets.GITHUB_APP_ID }} + GITHUB_PRIVATE_KEY=${{ secrets.GITHUB_PRIVATE_KEY }} + GITHUB_WEBHOOK_SECRET=${{ secrets.GITHUB_WEBHOOK_SECRET }} + ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} + MCP_SERVER_URL=${{ vars.MCP_SERVER_URL }} + ALLOWED_ORGS=${{ vars.ALLOWED_ORGS }} + ALLOWED_REPOS=${{ vars.ALLOWED_REPOS }} + DEPLOYMENT_VERSION=${{ steps.version.outputs.version }} + EOF + + - name: Deploy to production + run: | + export $(cat .env | xargs) + ./scripts/deploy/deploy-production.sh + + - name: Clean up + if: always() + run: rm -f .env + + - name: Create deployment record + uses: actions/github-script@v7 + with: + script: | + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.ref, + environment: 'production', + required_contexts: [], + auto_merge: false, + description: `Production deployment ${context.ref.replace('refs/tags/', '')}` + }); + + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.data.id, + state: 'success', + environment_url: 'https://webhook.yourdomain.com', + description: `Deployed version ${context.ref.replace('refs/tags/', '')}` + }); + + - name: Create GitHub Release + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: context.ref.replace('refs/tags/', ''), + name: `Release ${context.ref.replace('refs/tags/', '')}`, + body: `Production deployment of ${context.ref.replace('refs/tags/', '')}`, + draft: false, + prerelease: false + }); + + - name: Notify deployment status + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + text: 'Production deployment ${{ steps.version.outputs.version }} ${{ job.status }}' + webhook_url: ${{ secrets.SLACK_WEBHOOK }} + fields: repo,message,commit,author,action,eventName,ref,workflow \ No newline at end of file diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..faec7eb --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,74 @@ +version: '3.8' + +services: + webhook-staging: + image: ghcr.io/YOUR_ORG/claude-github-webhook:staging + container_name: claude-webhook-staging + restart: unless-stopped + ports: + - "8083:3003" # External:Internal port mapping + environment: + # Node environment + NODE_ENV: staging + PORT: 3003 + + # GitHub App Configuration (Staging Bot: @MCPClaude-Staging) + GITHUB_APP_ID: ${GITHUB_APP_ID_STAGING} + GITHUB_PRIVATE_KEY: ${GITHUB_PRIVATE_KEY_STAGING} + GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET_STAGING} + GITHUB_BOT_NAME: MCPClaude-Staging + + # Anthropic Configuration + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY_STAGING} + + # MCP Server Configuration (if applicable) + MCP_SERVER_URL: ${MCP_SERVER_URL_STAGING:-} + + # Security & Access Control + ALLOWED_ORGS: ${ALLOWED_ORGS_STAGING:-} + ALLOWED_REPOS: ${ALLOWED_REPOS_STAGING:-} + + # Logging + LOG_LEVEL: ${LOG_LEVEL_STAGING:-info} + LOG_FORMAT: json + + # Feature Flags + ENABLE_METRICS: ${ENABLE_METRICS_STAGING:-false} + ENABLE_RATE_LIMITING: ${ENABLE_RATE_LIMITING_STAGING:-true} + + volumes: + # Persist logs + - ./logs/staging:/app/logs + + # Configuration files (if needed) + - ./config/staging:/app/config:ro + + # GitHub App private key (alternative to env var) + # - ./keys/staging/github-app.pem:/app/github-app.pem:ro + + networks: + - webhook-network + + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3003/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + labels: + - "com.example.environment=staging" + - "com.example.service=claude-webhook" + - "com.example.bot=MCPClaude-Staging" + + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + labels: "environment,service,bot" + +networks: + webhook-network: + driver: bridge + name: claude-webhook-staging-net \ No newline at end of file diff --git a/scripts/deploy/deploy-production.sh b/scripts/deploy/deploy-production.sh new file mode 100755 index 0000000..e109d73 --- /dev/null +++ b/scripts/deploy/deploy-production.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +# Claude Webhook Production Deployment Script +# Deploys the production environment on port 8082 + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +NC='\033[0m' + +# Configuration +COMPOSE_FILE="docker-compose.yml" +SERVICE_NAME="webhook" +HEALTH_CHECK_URL="http://localhost:8082/health" +MAX_HEALTH_RETRIES=30 +PRODUCTION_BOT="MCPClaude" +BACKUP_DIR="/home/jonflatt/backups/webhook" + +echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${RED} Claude Webhook - PRODUCTION Deployment${NC}" +echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "Bot: ${GREEN}@${PRODUCTION_BOT}${NC}" +echo -e "Port: ${GREEN}8082${NC}" +echo -e "Time: ${GREEN}$(date)${NC}" +echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" + +# Function to check if service is healthy +check_health() { + local retries=0 + echo -e "${YELLOW}Checking service health...${NC}" + + while [ $retries -lt $MAX_HEALTH_RETRIES ]; do + if curl -f -s "$HEALTH_CHECK_URL" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Service is healthy!${NC}" + return 0 + fi + retries=$((retries + 1)) + echo -e " Waiting for service to start... ($retries/$MAX_HEALTH_RETRIES)" + sleep 2 + done + + echo -e "${RED}✗ Health check failed after $MAX_HEALTH_RETRIES attempts${NC}" + return 1 +} + +# Function to create backup +create_backup() { + echo -e "${YELLOW}Creating pre-deployment backup...${NC}" + mkdir -p "$BACKUP_DIR" + + # Get current container info + CONTAINER_ID=$(docker-compose -f "$COMPOSE_FILE" ps -q 2>/dev/null || echo "") + if [ -n "$CONTAINER_ID" ]; then + BACKUP_FILE="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz" + + # Export container + docker export "$CONTAINER_ID" | gzip > "$BACKUP_FILE" + echo -e "${GREEN}✓ Backup created: $BACKUP_FILE${NC}" + + # Keep only last 5 backups + ls -t "$BACKUP_DIR"/backup-*.tar.gz 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true + else + echo -e "${YELLOW}No running container to backup${NC}" + fi +} + +# Step 1: Production safety confirmation +echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${RED}⚠️ PRODUCTION DEPLOYMENT CONFIRMATION ⚠️${NC}" +echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${YELLOW}You are about to deploy to PRODUCTION.${NC}" +echo -e "${YELLOW}This will affect the live @${PRODUCTION_BOT} bot.${NC}" +echo -e "" +read -p "Type 'DEPLOY PRODUCTION' to continue: " confirmation + +if [ "$confirmation" != "DEPLOY PRODUCTION" ]; then + echo -e "${RED}Deployment cancelled.${NC}" + exit 1 +fi + +# Step 2: Pre-deployment checks +echo -e "\n${YELLOW}[1/8] Running pre-deployment checks...${NC}" + +# Check if docker-compose file exists +if [ ! -f "$COMPOSE_FILE" ]; then + echo -e "${RED}Error: $COMPOSE_FILE not found${NC}" + echo -e "${YELLOW}Please ensure you're running from the project root directory${NC}" + exit 1 +fi + +# Check Docker daemon +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}Error: Docker daemon is not running${NC}" + exit 1 +fi + +# Check disk space +DISK_USAGE=$(df -h /var/lib/docker | awk 'NR==2 {print $5}' | sed 's/%//') +if [ "$DISK_USAGE" -gt 85 ]; then + echo -e "${RED}Warning: Disk usage is at ${DISK_USAGE}%${NC}" + echo -e "${YELLOW}Consider cleaning up Docker images/containers${NC}" +fi + +echo -e "${GREEN}✓ Pre-deployment checks passed${NC}\n" + +# Step 3: Create backup +echo -e "${YELLOW}[2/8] Creating backup...${NC}" +create_backup +echo -e "${GREEN}✓ Backup complete${NC}\n" + +# Step 4: Pull latest images +echo -e "${YELLOW}[3/8] Pulling latest production images...${NC}" +docker-compose -f "$COMPOSE_FILE" pull +echo -e "${GREEN}✓ Images updated${NC}\n" + +# Step 5: Graceful shutdown +echo -e "${YELLOW}[4/8] Gracefully shutting down existing container...${NC}" +docker-compose -f "$COMPOSE_FILE" stop || true +sleep 5 # Allow time for graceful shutdown +docker-compose -f "$COMPOSE_FILE" down --remove-orphans || true +echo -e "${GREEN}✓ Existing container stopped${NC}\n" + +# Step 6: Start new container +echo -e "${YELLOW}[5/8] Starting new production container...${NC}" +docker-compose -f "$COMPOSE_FILE" up -d +echo -e "${GREEN}✓ Container started${NC}\n" + +# Step 7: Health check +echo -e "${YELLOW}[6/8] Running health checks...${NC}" +sleep 5 # Give container time to initialize + +if check_health; then + echo -e "${GREEN}✓ Health check passed${NC}\n" +else + echo -e "${RED}✗ Health check failed${NC}" + echo -e "${YELLOW}Rolling back deployment...${NC}" + + # Attempt rollback + docker-compose -f "$COMPOSE_FILE" down + + # Check if we have a backup to restore + LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/backup-*.tar.gz 2>/dev/null | head -1) + if [ -n "$LATEST_BACKUP" ]; then + echo -e "${YELLOW}Rollback instructions:${NC}" + echo -e "1. Load backup: ${YELLOW}docker load < $LATEST_BACKUP${NC}" + echo -e "2. Start previous version manually" + fi + + exit 1 +fi + +# Step 8: Post-deployment verification +echo -e "${YELLOW}[7/8] Post-deployment verification...${NC}" + +# Check container status +CONTAINER_STATUS=$(docker-compose -f "$COMPOSE_FILE" ps -q | xargs docker inspect -f '{{.State.Status}}' 2>/dev/null || echo "not found") +if [ "$CONTAINER_STATUS" = "running" ]; then + echo -e "${GREEN}✓ Container status: Running${NC}" +else + echo -e "${RED}✗ Container status: $CONTAINER_STATUS${NC}" + exit 1 +fi + +# Test webhook endpoint +echo -e "${YELLOW}Testing webhook endpoint...${NC}" +WEBHOOK_TEST=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$HEALTH_CHECK_URL" -H "Content-Type: application/json" -d '{"test": true}' || echo "000") +if [[ "$WEBHOOK_TEST" =~ ^(200|400|401)$ ]]; then + echo -e "${GREEN}✓ Webhook endpoint responding (HTTP $WEBHOOK_TEST)${NC}" +else + echo -e "${RED}⚠️ Webhook endpoint returned unexpected status: HTTP $WEBHOOK_TEST${NC}" +fi + +# Step 9: Final status +echo -e "\n${YELLOW}[8/8] Deployment summary...${NC}" + +# Show resource usage +echo -e "\n${BLUE}Container Resource Usage:${NC}" +docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" $(docker-compose -f "$COMPOSE_FILE" ps -q) + +# Show recent logs +echo -e "\n${BLUE}Recent logs:${NC}" +docker-compose -f "$COMPOSE_FILE" logs --tail=20 + +# Clean up old images +echo -e "\n${YELLOW}Cleaning up old images...${NC}" +docker image prune -f + +echo -e "\n${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${GREEN}✓ PRODUCTION deployment completed successfully!${NC}" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "Service URL: ${GREEN}http://localhost:8082${NC}" +echo -e "GitHub Bot: ${GREEN}@${PRODUCTION_BOT}${NC}" +echo -e "Backup: ${GREEN}${LATEST_BACKUP:-No backup created}${NC}" +echo -e "Logs: ${YELLOW}docker-compose -f $COMPOSE_FILE logs -f${NC}" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC} + +${PURPLE}Remember to monitor the service for the next few minutes!${NC}" \ No newline at end of file diff --git a/scripts/deploy/deploy-staging.sh b/scripts/deploy/deploy-staging.sh new file mode 100755 index 0000000..2e252bd --- /dev/null +++ b/scripts/deploy/deploy-staging.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Claude Webhook Staging Deployment Script +# Deploys the staging environment on port 8083 + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Configuration +COMPOSE_FILE="docker-compose.staging.yml" +SERVICE_NAME="webhook-staging" +HEALTH_CHECK_URL="http://localhost:8083/health" +MAX_HEALTH_RETRIES=30 +STAGING_BOT="MCPClaude-Staging" + +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE} Claude Webhook - Staging Deployment${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "Bot: ${GREEN}@${STAGING_BOT}${NC}" +echo -e "Port: ${GREEN}8083${NC}" +echo -e "Time: ${GREEN}$(date)${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" + +# Function to check if service is healthy +check_health() { + local retries=0 + echo -e "${YELLOW}Checking service health...${NC}" + + while [ $retries -lt $MAX_HEALTH_RETRIES ]; do + if curl -f -s "$HEALTH_CHECK_URL" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Service is healthy!${NC}" + return 0 + fi + retries=$((retries + 1)) + echo -e " Waiting for service to start... ($retries/$MAX_HEALTH_RETRIES)" + sleep 2 + done + + echo -e "${RED}✗ Health check failed after $MAX_HEALTH_RETRIES attempts${NC}" + return 1 +} + +# Step 1: Pre-deployment checks +echo -e "${YELLOW}[1/6] Running pre-deployment checks...${NC}" + +# Check if docker-compose file exists +if [ ! -f "$COMPOSE_FILE" ]; then + echo -e "${RED}Error: $COMPOSE_FILE not found${NC}" + echo -e "${YELLOW}Please ensure you're running from the project root directory${NC}" + exit 1 +fi + +# Check Docker daemon +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}Error: Docker daemon is not running${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Pre-deployment checks passed${NC}\n" + +# Step 2: Pull latest images +echo -e "${YELLOW}[2/6] Pulling latest staging images...${NC}" +docker-compose -f "$COMPOSE_FILE" pull +echo -e "${GREEN}✓ Images updated${NC}\n" + +# Step 3: Stop existing container +echo -e "${YELLOW}[3/6] Stopping existing staging container...${NC}" +docker-compose -f "$COMPOSE_FILE" down --remove-orphans || true +echo -e "${GREEN}✓ Existing container stopped${NC}\n" + +# Step 4: Start new container +echo -e "${YELLOW}[4/6] Starting new staging container...${NC}" +docker-compose -f "$COMPOSE_FILE" up -d +echo -e "${GREEN}✓ Container started${NC}\n" + +# Step 5: Health check +echo -e "${YELLOW}[5/6] Running health checks...${NC}" +sleep 5 # Give container time to initialize + +if check_health; then + echo -e "${GREEN}✓ Health check passed${NC}\n" +else + echo -e "${RED}✗ Health check failed${NC}" + echo -e "${YELLOW}Checking container logs...${NC}" + docker-compose -f "$COMPOSE_FILE" logs --tail=50 + exit 1 +fi + +# Step 6: Post-deployment verification +echo -e "${YELLOW}[6/6] Post-deployment verification...${NC}" + +# Check container status +CONTAINER_STATUS=$(docker-compose -f "$COMPOSE_FILE" ps -q | xargs docker inspect -f '{{.State.Status}}' 2>/dev/null || echo "not found") +if [ "$CONTAINER_STATUS" = "running" ]; then + echo -e "${GREEN}✓ Container status: Running${NC}" +else + echo -e "${RED}✗ Container status: $CONTAINER_STATUS${NC}" + exit 1 +fi + +# Show resource usage +echo -e "\n${BLUE}Container Resource Usage:${NC}" +docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" $(docker-compose -f "$COMPOSE_FILE" ps -q) + +# Show recent logs +echo -e "\n${BLUE}Recent logs:${NC}" +docker-compose -f "$COMPOSE_FILE" logs --tail=10 + +echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${GREEN}✓ Staging deployment completed successfully!${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "Service URL: ${GREEN}http://localhost:8083${NC}" +echo -e "GitHub Bot: ${GREEN}@${STAGING_BOT}${NC}" +echo -e "Logs: ${YELLOW}docker-compose -f $COMPOSE_FILE logs -f${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" \ No newline at end of file diff --git a/scripts/manage-runner.sh b/scripts/manage-runner.sh new file mode 100755 index 0000000..cfc623c --- /dev/null +++ b/scripts/manage-runner.sh @@ -0,0 +1,336 @@ +#!/bin/bash + +# GitHub Actions Runner Management Script +# Manage the webhook deployment runner service + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Configuration +SERVICE_NAME="webhook-deployment-runner" +RUNNER_DIR="/home/jonflatt/github-actions-runner" +RUNNER_USER="jonflatt" + +# Function to print usage +usage() { + echo -e "${BLUE}GitHub Actions Runner Management Tool${NC}" + echo -e "${BLUE}=====================================${NC}" + echo -e "\nUsage: $0 [command]" + echo -e "\nCommands:" + echo -e " ${GREEN}start${NC} - Start the runner service" + echo -e " ${GREEN}stop${NC} - Stop the runner service" + echo -e " ${GREEN}restart${NC} - Restart the runner service" + echo -e " ${GREEN}status${NC} - Check runner service status" + echo -e " ${GREEN}logs${NC} - View runner logs (live)" + echo -e " ${GREEN}logs-tail${NC} - View last 50 lines of logs" + echo -e " ${GREEN}update${NC} - Update runner to latest version" + echo -e " ${GREEN}config${NC} - Show runner configuration" + echo -e " ${GREEN}health${NC} - Check runner health" + echo -e " ${GREEN}jobs${NC} - Show recent job history" + echo -e " ${GREEN}cleanup${NC} - Clean up work directory" + echo -e " ${GREEN}info${NC} - Show runner information" + exit 1 +} + +# Check if running with correct permissions +check_permissions() { + if [[ $EUID -ne 0 ]] && [[ "$1" =~ ^(start|stop|restart|update)$ ]]; then + echo -e "${RED}Error: This command requires sudo privileges${NC}" + echo -e "${YELLOW}Run: sudo $0 $1${NC}" + exit 1 + fi +} + +# Start the runner +start_runner() { + echo -e "${YELLOW}Starting runner service...${NC}" + systemctl start $SERVICE_NAME + sleep 2 + if systemctl is-active --quiet $SERVICE_NAME; then + echo -e "${GREEN}✓ Runner started successfully${NC}" + systemctl status $SERVICE_NAME --no-pager | head -n 10 + else + echo -e "${RED}✗ Failed to start runner${NC}" + systemctl status $SERVICE_NAME --no-pager + exit 1 + fi +} + +# Stop the runner +stop_runner() { + echo -e "${YELLOW}Stopping runner service...${NC}" + systemctl stop $SERVICE_NAME + echo -e "${GREEN}✓ Runner stopped${NC}" +} + +# Restart the runner +restart_runner() { + echo -e "${YELLOW}Restarting runner service...${NC}" + systemctl restart $SERVICE_NAME + sleep 2 + if systemctl is-active --quiet $SERVICE_NAME; then + echo -e "${GREEN}✓ Runner restarted successfully${NC}" + systemctl status $SERVICE_NAME --no-pager | head -n 10 + else + echo -e "${RED}✗ Failed to restart runner${NC}" + systemctl status $SERVICE_NAME --no-pager + exit 1 + fi +} + +# Check runner status +check_status() { + echo -e "${BLUE}Runner Service Status${NC}" + echo -e "${BLUE}===================${NC}" + systemctl status $SERVICE_NAME --no-pager + + echo -e "\n${BLUE}Runner Process Info${NC}" + echo -e "${BLUE}===================${NC}" + ps aux | grep -E "(Runner.Listener|run.sh)" | grep -v grep || echo "No runner processes found" +} + +# View logs +view_logs() { + echo -e "${YELLOW}Viewing live logs (Ctrl+C to exit)...${NC}" + journalctl -u $SERVICE_NAME -f +} + +# View last 50 lines of logs +view_logs_tail() { + echo -e "${BLUE}Last 50 lines of runner logs${NC}" + echo -e "${BLUE}===========================${NC}" + journalctl -u $SERVICE_NAME -n 50 --no-pager +} + +# Update runner +update_runner() { + echo -e "${YELLOW}Updating GitHub Actions Runner...${NC}" + + # Stop the service + systemctl stop $SERVICE_NAME + + # Get current version + CURRENT_VERSION=$($RUNNER_DIR/bin/Runner.Listener --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown") + echo -e "Current version: ${YELLOW}$CURRENT_VERSION${NC}" + + # Get latest version + LATEST_VERSION=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') + echo -e "Latest version: ${GREEN}$LATEST_VERSION${NC}" + + if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then + echo -e "${GREEN}✓ Runner is already up to date${NC}" + systemctl start $SERVICE_NAME + return + fi + + # Backup current runner + echo -e "${YELLOW}Backing up current runner...${NC}" + cd $RUNNER_DIR + tar -czf runner-backup-$(date +%Y%m%d-%H%M%S).tar.gz bin externals + + # Download and extract new version + echo -e "${YELLOW}Downloading new version...${NC}" + curl -o actions-runner-linux-x64.tar.gz -L "https://github.com/actions/runner/releases/download/v${LATEST_VERSION}/actions-runner-linux-x64-${LATEST_VERSION}.tar.gz" + tar xzf ./actions-runner-linux-x64.tar.gz + rm actions-runner-linux-x64.tar.gz + + # Start the service + systemctl start $SERVICE_NAME + echo -e "${GREEN}✓ Runner updated to version $LATEST_VERSION${NC}" +} + +# Show configuration +show_config() { + echo -e "${BLUE}Runner Configuration${NC}" + echo -e "${BLUE}===================${NC}" + + if [ -f "$RUNNER_DIR/.runner" ]; then + echo -e "\n${GREEN}Runner Settings:${NC}" + cat "$RUNNER_DIR/.runner" | jq '.' 2>/dev/null || cat "$RUNNER_DIR/.runner" + fi + + if [ -f "$RUNNER_DIR/.credentials" ]; then + echo -e "\n${GREEN}Runner Registration:${NC}" + echo "Runner is registered (credentials file exists)" + else + echo -e "\n${RED}Runner is not configured${NC}" + fi + + echo -e "\n${GREEN}Service Configuration:${NC}" + systemctl show $SERVICE_NAME | grep -E "(LoadState|ActiveState|SubState|MainPID|Environment)" +} + +# Check health +check_health() { + echo -e "${BLUE}Runner Health Check${NC}" + echo -e "${BLUE}==================${NC}" + + # Check service status + if systemctl is-active --quiet $SERVICE_NAME; then + echo -e "${GREEN}✓ Service is running${NC}" + else + echo -e "${RED}✗ Service is not running${NC}" + fi + + # Check disk space + DISK_USAGE=$(df -h $RUNNER_DIR | awk 'NR==2 {print $5}' | sed 's/%//') + if [ "$DISK_USAGE" -lt 80 ]; then + echo -e "${GREEN}✓ Disk usage: ${DISK_USAGE}%${NC}" + else + echo -e "${RED}✗ Disk usage: ${DISK_USAGE}% (High)${NC}" + fi + + # Check work directory size + if [ -d "$RUNNER_DIR/_work" ]; then + WORK_SIZE=$(du -sh "$RUNNER_DIR/_work" 2>/dev/null | cut -f1) + echo -e "${BLUE}Work directory size: $WORK_SIZE${NC}" + fi + + # Check runner connectivity + if [ -f "$RUNNER_DIR/.runner" ]; then + GITHUB_URL=$(cat "$RUNNER_DIR/.runner" | jq -r '.gitHubUrl' 2>/dev/null || echo "") + if [ -n "$GITHUB_URL" ] && curl -s -o /dev/null -w "%{http_code}" "$GITHUB_URL" | grep -q "200"; then + echo -e "${GREEN}✓ GitHub connectivity OK${NC}" + else + echo -e "${YELLOW}⚠ Cannot verify GitHub connectivity${NC}" + fi + fi +} + +# Show recent jobs +show_jobs() { + echo -e "${BLUE}Recent Runner Jobs${NC}" + echo -e "${BLUE}=================${NC}" + + # Check for job history in work directory + if [ -d "$RUNNER_DIR/_work" ]; then + echo -e "\n${GREEN}Recent job directories:${NC}" + ls -la "$RUNNER_DIR/_work" 2>/dev/null | tail -n 10 || echo "No job directories found" + fi + + # Show recent log entries + echo -e "\n${GREEN}Recent job activity:${NC}" + journalctl -u $SERVICE_NAME --since "1 hour ago" | grep -E "(Running job|Job .* completed|Completed request)" | tail -n 20 || echo "No recent job activity" +} + +# Cleanup work directory +cleanup_work() { + echo -e "${YELLOW}Cleaning up work directory...${NC}" + + if [ ! -d "$RUNNER_DIR/_work" ]; then + echo -e "${GREEN}Work directory doesn't exist${NC}" + return + fi + + # Show current size + BEFORE_SIZE=$(du -sh "$RUNNER_DIR/_work" 2>/dev/null | cut -f1) + echo -e "Current size: ${YELLOW}$BEFORE_SIZE${NC}" + + # Confirm + read -p "Are you sure you want to clean the work directory? (y/N): " confirm + if [ "$confirm" != "y" ]; then + echo -e "${YELLOW}Cleanup cancelled${NC}" + return + fi + + # Stop runner + systemctl stop $SERVICE_NAME + + # Clean work directory + rm -rf "$RUNNER_DIR/_work"/* + + # Start runner + systemctl start $SERVICE_NAME + + echo -e "${GREEN}✓ Work directory cleaned${NC}" +} + +# Show runner info +show_info() { + echo -e "${BLUE}GitHub Actions Runner Information${NC}" + echo -e "${BLUE}=================================${NC}" + + echo -e "\n${GREEN}Basic Info:${NC}" + echo -e "Service Name: ${YELLOW}$SERVICE_NAME${NC}" + echo -e "Runner Directory: ${YELLOW}$RUNNER_DIR${NC}" + echo -e "Runner User: ${YELLOW}$RUNNER_USER${NC}" + + if [ -f "$RUNNER_DIR/bin/Runner.Listener" ]; then + VERSION=$($RUNNER_DIR/bin/Runner.Listener --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown") + echo -e "Runner Version: ${YELLOW}$VERSION${NC}" + fi + + echo -e "\n${GREEN}System Info:${NC}" + echo -e "Hostname: ${YELLOW}$(hostname)${NC}" + echo -e "OS: ${YELLOW}$(lsb_release -d | cut -f2)${NC}" + echo -e "Kernel: ${YELLOW}$(uname -r)${NC}" + echo -e "Architecture: ${YELLOW}$(uname -m)${NC}" + + echo -e "\n${GREEN}Docker Info:${NC}" + if command -v docker &> /dev/null; then + DOCKER_VERSION=$(docker --version | awk '{print $3}' | sed 's/,$//') + echo -e "Docker Version: ${YELLOW}$DOCKER_VERSION${NC}" + + if groups $RUNNER_USER | grep -q docker; then + echo -e "Docker Access: ${GREEN}✓ User in docker group${NC}" + else + echo -e "Docker Access: ${RED}✗ User not in docker group${NC}" + fi + else + echo -e "${RED}Docker not installed${NC}" + fi + + echo -e "\n${GREEN}Labels:${NC}" + echo -e "${YELLOW}self-hosted,linux,x64,deployment,webhook-cd${NC}" +} + +# Main logic +check_permissions "$1" + +case "$1" in + start) + start_runner + ;; + stop) + stop_runner + ;; + restart) + restart_runner + ;; + status) + check_status + ;; + logs) + view_logs + ;; + logs-tail) + view_logs_tail + ;; + update) + update_runner + ;; + config) + show_config + ;; + health) + check_health + ;; + jobs) + show_jobs + ;; + cleanup) + cleanup_work + ;; + info) + show_info + ;; + *) + usage + ;; +esac \ No newline at end of file diff --git a/scripts/setup/setup-github-runner.sh b/scripts/setup/setup-github-runner.sh new file mode 100755 index 0000000..133251f --- /dev/null +++ b/scripts/setup/setup-github-runner.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Setup GitHub Actions self-hosted runner for claude-github-webhook + +set -e + +# Configuration +RUNNER_DIR="/home/jonflatt/github-actions-runner" +RUNNER_VERSION="2.324.0" +REPO_URL="https://github.com/intelligence-assist/claude-github-webhook" +RUNNER_NAME="claude-webhook-runner" +RUNNER_LABELS="self-hosted,linux,x64,claude-webhook" + +echo "🚀 Setting up GitHub Actions self-hosted runner..." + +# Create runner directory +mkdir -p "$RUNNER_DIR" +cd "$RUNNER_DIR" + +# Download runner if not exists +if [ ! -f "actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" ]; then + echo "📦 Downloading runner v${RUNNER_VERSION}..." + curl -o "actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" -L \ + "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" +fi + +# Extract runner +echo "📂 Extracting runner..." +tar xzf "./actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" + +# Install dependencies if needed +echo "🔧 Installing dependencies..." +sudo ./bin/installdependencies.sh || true + +echo "" +echo "⚠️ IMPORTANT: You need to get a runner registration token from GitHub!" +echo "" +echo "1. Go to: https://github.com/intelligence-assist/claude-github-webhook/settings/actions/runners/new" +echo "2. Copy the registration token" +echo "3. Run the configuration command below with your token:" +echo "" +echo "cd $RUNNER_DIR" +echo "./config.sh --url $REPO_URL --token YOUR_TOKEN_HERE --name $RUNNER_NAME --labels $RUNNER_LABELS --unattended --replace" +echo "" +echo "4. After configuration, install as a service:" +echo "sudo ./svc.sh install" +echo "sudo ./svc.sh start" +echo "" +echo "5. Check status:" +echo "sudo ./svc.sh status" +echo "" + +# Create systemd service file for the runner +cat > "$RUNNER_DIR/actions.runner.service" << 'EOF' +[Unit] +Description=GitHub Actions Runner (claude-webhook-runner) +After=network-online.target + +[Service] +Type=simple +User=jonflatt +WorkingDirectory=/home/jonflatt/github-actions-runner +ExecStart=/home/jonflatt/github-actions-runner/run.sh +Restart=on-failure +RestartSec=5 +KillMode=process +KillSignal=SIGTERM +StandardOutput=journal +StandardError=journal +SyslogIdentifier=github-runner + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=/home/jonflatt/github-actions-runner +ReadWritePaths=/home/jonflatt/n8n/claude-repo +ReadWritePaths=/var/run/docker.sock + +[Install] +WantedBy=multi-user.target +EOF + +echo "📄 Systemd service file created at: $RUNNER_DIR/actions.runner.service" +echo "" +echo "Alternative: Use systemd directly instead of ./svc.sh:" +echo "sudo cp $RUNNER_DIR/actions.runner.service /etc/systemd/system/github-runner-claude.service" +echo "sudo systemctl daemon-reload" +echo "sudo systemctl enable github-runner-claude" +echo "sudo systemctl start github-runner-claude" \ No newline at end of file