From f194a340e3215ed4348e5c0209312fec1cf80621 Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 21:49:59 -0700 Subject: [PATCH 01/10] feat(ci): Add deployment workflow for staging and production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add deploy.yml workflow with SSH-based deployment - Staging: Auto-deploys on push to main (after tests pass) - Production: Deploys on GitHub releases or manual trigger - Quality gates: Requires test.yml to pass before deployment - Security: Uses ssh-agent for secure key handling - Add workflow_call trigger to test.yml for reusability - Include pre-deployment backups for production - Add health check verification post-deployment Triggers: - Push to main → staging - GitHub Release → production - Manual workflow_dispatch → either Required secrets: HOST, USER, SSH_KEY, PROJECT_PATH --- .github/workflows/deploy.yml | 251 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 1 + claudedocs/SESSION.md | 146 ++++++++++++-------- 3 files changed, 339 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..108d2f10 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,251 @@ +name: Deploy + +on: + push: + branches: [main] + release: + types: [published] + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +env: + PYTHON_VERSION: '3.11' + NODE_VERSION: '20' + +jobs: + # ============================================ + # Run Tests First (Quality Gate) + # ============================================ + test: + name: Run Test Suite + uses: ./.github/workflows/test.yml + + # ============================================ + # Deploy to Staging + # ============================================ + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: test + if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging') + environment: + name: staging + url: https://staging.codeframe.example.com + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_KEY }} + + - name: Add server to known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to staging server + run: | + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH + set -e + echo "🚀 Starting deployment to staging..." + + # Navigate to project directory + cd ${{ secrets.PROJECT_PATH }} + + # Pull latest code + echo "📥 Pulling latest code..." + git fetch origin main + git reset --hard origin/main + + # Backend setup + echo "🐍 Setting up Python backend..." + if [ -d .venv ]; then + source .venv/bin/activate + else + python3 -m venv .venv + source .venv/bin/activate + fi + pip install --quiet uv + uv sync + + # Frontend setup + echo "📦 Building frontend..." + cd web-ui + npm ci + npm run build + cd .. + + # Restart services (if systemd services exist) + echo "🔄 Restarting services..." + if systemctl is-active --quiet codeframe-backend 2>/dev/null; then + sudo systemctl restart codeframe-backend + echo "✅ Backend service restarted" + else + echo "⚠️ No codeframe-backend service found (manual restart may be needed)" + fi + + if systemctl is-active --quiet codeframe-frontend 2>/dev/null; then + sudo systemctl restart codeframe-frontend + echo "✅ Frontend service restarted" + else + echo "⚠️ No codeframe-frontend service found (manual restart may be needed)" + fi + + echo "✅ Deployment to staging complete!" + ENDSSH + + - name: Verify deployment + run: | + echo "🔍 Verifying deployment..." + sleep 10 + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:8080/health" || echo "⚠️ Health check failed or endpoint not available" + + - name: Deployment summary + run: | + echo "## Staging Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Deployed by**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "- **Time**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + + # ============================================ + # Deploy to Production + # ============================================ + deploy-production: + name: Deploy to Production + runs-on: ubuntu-latest + needs: test + if: | + (github.event_name == 'release') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production') + environment: + name: production + url: https://codeframe.example.com + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_KEY }} + + - name: Add server to known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts + + - name: Create pre-deployment backup + run: | + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH + set -e + echo "💾 Creating pre-deployment backup..." + cd ${{ secrets.PROJECT_PATH }} + + # Create backup directory + BACKUP_DIR="/tmp/codeframe-backup-\$(date +%Y%m%d-%H%M%S)" + mkdir -p \$BACKUP_DIR + + # Backup database + if [ -f .codeframe/state.db ]; then + cp .codeframe/state.db \$BACKUP_DIR/ + echo "✅ Database backed up to \$BACKUP_DIR" + fi + + # Record current commit for potential rollback + git rev-parse HEAD > \$BACKUP_DIR/previous_commit.txt + echo "✅ Current commit recorded: \$(cat \$BACKUP_DIR/previous_commit.txt)" + ENDSSH + + - name: Deploy to production server + run: | + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH + set -e + echo "🚀 Starting deployment to production..." + + # Navigate to project directory + cd ${{ secrets.PROJECT_PATH }} + + # Pull the release tag or main + echo "📥 Pulling latest code..." + git fetch origin --tags + TAG_NAME="${{ github.event.release.tag_name }}" + if [ -n "\$TAG_NAME" ]; then + git checkout \$TAG_NAME + echo "✅ Checked out tag: \$TAG_NAME" + else + git fetch origin main + git reset --hard origin/main + echo "✅ Reset to origin/main" + fi + + # Backend setup + echo "🐍 Setting up Python backend..." + if [ -d .venv ]; then + source .venv/bin/activate + else + python3 -m venv .venv + source .venv/bin/activate + fi + pip install --quiet uv + uv sync --no-dev + + # Frontend setup (production build) + echo "📦 Building frontend for production..." + cd web-ui + npm ci + npm run build + cd .. + + # Run database migrations if any + echo "🗃️ Running database migrations..." + # Add migration command here if needed + + # Restart services + echo "🔄 Restarting services..." + if systemctl is-active --quiet codeframe-backend 2>/dev/null; then + sudo systemctl restart codeframe-backend + echo "✅ Backend service restarted" + else + echo "⚠️ No codeframe-backend service found (manual restart may be needed)" + fi + + if systemctl is-active --quiet codeframe-frontend 2>/dev/null; then + sudo systemctl restart codeframe-frontend + echo "✅ Frontend service restarted" + else + echo "⚠️ No codeframe-frontend service found (manual restart may be needed)" + fi + + echo "✅ Deployment to production complete!" + ENDSSH + + - name: Verify deployment + run: | + echo "🔍 Verifying production deployment..." + sleep 15 + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:8080/health" || echo "⚠️ Health check failed or endpoint not available" + + - name: Deployment summary + run: | + echo "## Production Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${{ github.event.release.tag_name || github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Deployed by**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "- **Time**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbd26e2c..0c9c77e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: [main, develop, '0*'] pull_request: branches: [main, develop] + workflow_call: # Allow this workflow to be called by other workflows # schedule: # Run E2E tests nightly at 2 AM UTC # - cron: '0 2 * * *' diff --git a/claudedocs/SESSION.md b/claudedocs/SESSION.md index e592302e..cd61abc9 100644 --- a/claudedocs/SESSION.md +++ b/claudedocs/SESSION.md @@ -1,59 +1,87 @@ -# Session: Fix Frontend E2E Tests on CI - -**Date**: 2025-12-15 -**Branch**: `fix/ci-e2e-tests` -**PR**: https://github.com/frankbria/codeframe/pull/93 -**Status**: ✅ ALL CI CHECKS PASSING - -## Summary -Fixed 8 failing Playwright E2E tests on CI by addressing test-UI architecture mismatches. - -## Root Cause Analysis - -### The Problem -Tests had fundamental mismatches with the actual UI architecture: - -1. **Tab-based conditional rendering**: React only renders tab panels when active - - Tests expected `checkpoint-panel` in DOM, but it's only rendered when Checkpoints tab is active - -2. **Selector collision**: `[data-testid^="agent-cost-"]` matched both: - - Data rows: `agent-cost-{agent_id}` - - Empty state: `agent-cost-empty` - -3. **Non-existent UI elements**: Tests clicked `metrics-tab` which doesn't exist (metrics is in Overview tab) - -### Failed Tests (Original) -1. `should display all main dashboard sections` - expected `checkpoint-panel` in DOM -2. `should display checkpoint panel` - waited for panel before clicking tab -3. `should receive real-time updates via WebSocket` - WebSocket connected before listener -4. `should display cost breakdown by agent` - selector collision with empty state -5. `should display cost breakdown by model` - selector collision with empty state -6. `should filter metrics by date range` - clicked non-existent `metrics-tab` -7. `should display cost per task` - expected table headers when no data -8. `should display cost trend chart` - expected data when API returned empty - -## Fixes Applied - -### test_dashboard.spec.ts -- Click tabs before checking panels (React conditional rendering) -- Fixed checkpoint panel test to click Checkpoints tab first -- Removed metrics-tab navigation (metrics is in Overview tab) -- Fixed WebSocket test to reload page while listening for event - -### test_metrics_ui.spec.ts -- Removed metrics-tab navigation (panel is in Overview tab by default) -- Fixed selector collision: use `:not([data-testid="...-empty"])` to exclude empty state -- Check for empty state visibility FIRST before looking for data rows -- Made date range filter test skip-able when API errors -- Accept empty state as valid in cost breakdown tests - -## CI Results -- **Run 1**: 8 failed (original issues) -- **Run 2**: 22 passed, 1 failed (date filter issue) -- **Run 3**: 35 passed, 2 failed (selector collision) -- **Run 4**: ✅ ALL PASSED (37 tests) - -## Commits -1. `fix(e2e): Fix dashboard and metrics tests for tab-based UI rendering` -2. `docs: Update session log with fix details and PR link` -3. `fix(e2e): Handle empty state selector collision in metrics tests` +# CI/CD Deployment Workflow Implementation + +## Session Goal +Create GitHub Actions CI/CD workflow for automated deployment to staging and production environments using SSH-based deployment. + +## GitHub Secrets Available +- `HOST` - Server hostname +- `USER` - SSH username +- `SSH_KEY` - SSH private key +- `PROJECT_PATH` - Deployment path on server + +## Execution Plan + +### Phase 1: Analysis & Planning +- Understand existing test workflow structure +- Verify GitHub environments (staging, production) +- Analyze deployment mechanism + +### Phase 2: Workflow Design +- Deployment trigger strategy (main → staging, tags → production) +- Pre-deployment quality gates +- SSH connection security patterns + +### Phase 3: Implementation +- `.github/workflows/deploy.yml` - Main deployment workflow +- Environment-specific configurations +- SSH key handling with security best practices + +### Phase 4: Quality Gates Integration +- Test suite dependency (deploy only if tests pass) +- Coverage threshold enforcement (≥65%) +- Code quality checks + +### Phase 5: Security Hardening +- SSH key usage validation (no key exposure in logs) +- Least-privilege deployment permissions + +### Phase 6: Testing & Validation +- Dry-run deployment test +- Staging deployment verification + +### Phase 7: Documentation +- Deployment workflow guide +- Environment setup instructions + +## Risk Mitigations +1. SSH Key Security - Use ssh-agent, never echo secrets +2. Production Environment - Create if needed +3. Port Conflicts - Document port configuration +4. Zero-Downtime - Simple restart strategy for MVP + +--- + +## Implementation Complete + +### Files Created/Modified +- `.github/workflows/deploy.yml` - New deployment workflow +- `.github/workflows/test.yml` - Added `workflow_call` trigger for reusability + +### Deployment Triggers +| Trigger | Environment | Condition | +|---------|-------------|-----------| +| Push to `main` | Staging | Automatic after tests pass | +| GitHub Release | Production | Automatic after tests pass | +| Manual dispatch | Either | Select environment in UI | + +### Required GitHub Setup +1. **Staging environment** - Already exists with secrets (HOST, USER, SSH_KEY, PROJECT_PATH) +2. **Production environment** - Create manually when ready: + - Go to repo Settings → Environments → New environment + - Name: `production` + - Add same secrets: HOST, USER, SSH_KEY, PROJECT_PATH + - Optional: Add required reviewers for production deployments + +### Server Requirements +The deployment expects: +- Python 3.11+ with ability to create venv +- Node.js 20+ with npm +- Git installed and repo cloned at PROJECT_PATH +- Optional: systemd services `codeframe-backend` and `codeframe-frontend` + +### Manual Deployment +Use workflow_dispatch in GitHub Actions UI: +1. Go to Actions → Deploy +2. Click "Run workflow" +3. Select environment (staging/production) +4. Click "Run workflow" From c9e956e8e0e547c37550519dc5654e6da73b7bfe Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 21:55:12 -0700 Subject: [PATCH 02/10] fix(ci): Use PM2 instead of systemd for service management - Replace systemctl commands with PM2 restart/start - Update health check port from 8080 to 14200 (staging backend port) - Add logs directory creation before PM2 start - Support optional ecosystem.production.config.js for production - Add PM2 save after restart to persist process list --- .github/workflows/deploy.yml | 70 +++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 108d2f10..df607654 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -88,20 +88,24 @@ jobs: npm run build cd .. - # Restart services (if systemd services exist) - echo "🔄 Restarting services..." - if systemctl is-active --quiet codeframe-backend 2>/dev/null; then - sudo systemctl restart codeframe-backend - echo "✅ Backend service restarted" + # Ensure logs directory exists + mkdir -p logs + + # Restart PM2 services + echo "🔄 Restarting PM2 services..." + if command -v pm2 &> /dev/null; then + # Check if PM2 processes exist, if not start them + if pm2 list | grep -q "codeframe-staging"; then + pm2 restart ecosystem.config.js + echo "✅ PM2 services restarted" + else + pm2 start ecosystem.config.js + echo "✅ PM2 services started" + fi + pm2 save else - echo "⚠️ No codeframe-backend service found (manual restart may be needed)" - fi - - if systemctl is-active --quiet codeframe-frontend 2>/dev/null; then - sudo systemctl restart codeframe-frontend - echo "✅ Frontend service restarted" - else - echo "⚠️ No codeframe-frontend service found (manual restart may be needed)" + echo "❌ PM2 not found - please install PM2 globally" + exit 1 fi echo "✅ Deployment to staging complete!" @@ -111,7 +115,7 @@ jobs: run: | echo "🔍 Verifying deployment..." sleep 10 - ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:8080/health" || echo "⚠️ Health check failed or endpoint not available" + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:14200/health" || echo "⚠️ Health check failed or endpoint not available" - name: Deployment summary run: | @@ -216,20 +220,30 @@ jobs: echo "🗃️ Running database migrations..." # Add migration command here if needed - # Restart services - echo "🔄 Restarting services..." - if systemctl is-active --quiet codeframe-backend 2>/dev/null; then - sudo systemctl restart codeframe-backend - echo "✅ Backend service restarted" - else - echo "⚠️ No codeframe-backend service found (manual restart may be needed)" - fi - - if systemctl is-active --quiet codeframe-frontend 2>/dev/null; then - sudo systemctl restart codeframe-frontend - echo "✅ Frontend service restarted" + # Ensure logs directory exists + mkdir -p logs + + # Restart PM2 services + echo "🔄 Restarting PM2 services..." + if command -v pm2 &> /dev/null; then + # Production uses different ecosystem config if it exists + if [ -f ecosystem.production.config.js ]; then + CONFIG_FILE="ecosystem.production.config.js" + else + CONFIG_FILE="ecosystem.config.js" + fi + + if pm2 list | grep -q "codeframe"; then + pm2 restart \$CONFIG_FILE + echo "✅ PM2 services restarted" + else + pm2 start \$CONFIG_FILE + echo "✅ PM2 services started" + fi + pm2 save else - echo "⚠️ No codeframe-frontend service found (manual restart may be needed)" + echo "❌ PM2 not found - please install PM2 globally" + exit 1 fi echo "✅ Deployment to production complete!" @@ -239,7 +253,7 @@ jobs: run: | echo "🔍 Verifying production deployment..." sleep 15 - ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:8080/health" || echo "⚠️ Health check failed or endpoint not available" + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:14200/health" || echo "⚠️ Health check failed or endpoint not available" - name: Deployment summary run: | From b73e4cb63064d253a5c3abf2b221ffd016b86b92 Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 21:58:31 -0700 Subject: [PATCH 03/10] feat(ci): Generate .env files from GitHub secrets during deployment - Add environment file creation step for staging and production - Create .env.staging/.env.production from GitHub secrets - Set proper file permissions (600) for security - Document new required secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, CORS_ORIGINS, API_URL, WS_URL New secrets needed in staging environment: - ANTHROPIC_API_KEY - OPENAI_API_KEY (optional) - CORS_ORIGINS - API_URL - WS_URL --- .github/workflows/deploy.yml | 86 ++++++++++++++++++++++++++++++++++++ claudedocs/SESSION.md | 19 ++++++-- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index df607654..75a9d0c8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,6 +56,49 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts + - name: Create environment file + env: + REMOTE_HOST: ${{ secrets.HOST }} + REMOTE_USER: ${{ secrets.USER }} + REMOTE_PATH: ${{ secrets.PROJECT_PATH }} + ENV_ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ENV_OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }} + ENV_CORS: ${{ secrets.CORS_ORIGINS }} + ENV_API_URL: ${{ secrets.API_URL }} + ENV_WS_URL: ${{ secrets.WS_URL }} + run: | + echo "📝 Creating .env.staging file..." + ssh ${REMOTE_USER}@${REMOTE_HOST} "cat > ${REMOTE_PATH}/.env.staging && chmod 600 ${REMOTE_PATH}/.env.staging" << ENVEOF + # CodeFRAME Staging Environment Configuration + # Auto-generated by GitHub Actions deployment + + # AI Provider API Keys + ANTHROPIC_API_KEY=${ENV_ANTHROPIC_KEY} + OPENAI_API_KEY=${ENV_OPENAI_KEY} + + # Database Configuration + DATABASE_PATH=${REMOTE_PATH}/.codeframe/state.db + + # Status Server Configuration + API_HOST=127.0.0.1 + API_PORT=14200 + CORS_ALLOWED_ORIGINS=${ENV_CORS} + + # Web UI Configuration + NEXT_PUBLIC_API_URL=${ENV_API_URL} + NEXT_PUBLIC_WS_URL=${ENV_WS_URL} + + # Logging Configuration + LOG_LEVEL=INFO + LOG_FILE=${REMOTE_PATH}/.codeframe/logs/codeframe.log + + # Environment + ENVIRONMENT=staging + DEBUG=false + HOT_RELOAD=false + ENVEOF + echo "✅ .env.staging created" + - name: Deploy to staging server run: | ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH @@ -154,6 +197,49 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts + - name: Create environment file + env: + REMOTE_HOST: ${{ secrets.HOST }} + REMOTE_USER: ${{ secrets.USER }} + REMOTE_PATH: ${{ secrets.PROJECT_PATH }} + ENV_ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ENV_OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }} + ENV_CORS: ${{ secrets.CORS_ORIGINS }} + ENV_API_URL: ${{ secrets.API_URL }} + ENV_WS_URL: ${{ secrets.WS_URL }} + run: | + echo "📝 Creating .env.production file..." + ssh ${REMOTE_USER}@${REMOTE_HOST} "cat > ${REMOTE_PATH}/.env.production && chmod 600 ${REMOTE_PATH}/.env.production" << ENVEOF + # CodeFRAME Production Environment Configuration + # Auto-generated by GitHub Actions deployment + + # AI Provider API Keys + ANTHROPIC_API_KEY=${ENV_ANTHROPIC_KEY} + OPENAI_API_KEY=${ENV_OPENAI_KEY} + + # Database Configuration + DATABASE_PATH=${REMOTE_PATH}/.codeframe/state.db + + # Status Server Configuration + API_HOST=127.0.0.1 + API_PORT=14200 + CORS_ALLOWED_ORIGINS=${ENV_CORS} + + # Web UI Configuration + NEXT_PUBLIC_API_URL=${ENV_API_URL} + NEXT_PUBLIC_WS_URL=${ENV_WS_URL} + + # Logging Configuration + LOG_LEVEL=INFO + LOG_FILE=${REMOTE_PATH}/.codeframe/logs/codeframe.log + + # Environment + ENVIRONMENT=production + DEBUG=false + HOT_RELOAD=false + ENVEOF + echo "✅ .env.production created" + - name: Create pre-deployment backup run: | ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH diff --git a/claudedocs/SESSION.md b/claudedocs/SESSION.md index cd61abc9..d154dfff 100644 --- a/claudedocs/SESSION.md +++ b/claudedocs/SESSION.md @@ -3,12 +3,21 @@ ## Session Goal Create GitHub Actions CI/CD workflow for automated deployment to staging and production environments using SSH-based deployment. -## GitHub Secrets Available +## GitHub Secrets Required + +### Connection Secrets (already configured) - `HOST` - Server hostname - `USER` - SSH username - `SSH_KEY` - SSH private key - `PROJECT_PATH` - Deployment path on server +### Environment Secrets (need to add to staging environment) +- `ANTHROPIC_API_KEY` - Anthropic API key for Claude +- `OPENAI_API_KEY` - OpenAI API key (optional) +- `CORS_ORIGINS` - CORS allowed origins (e.g., `https://dev.codeframeapp.com`) +- `API_URL` - Backend API URL (e.g., `https://api.dev.codeframeapp.com`) +- `WS_URL` - WebSocket URL (e.g., `wss://api.dev.codeframeapp.com/ws`) + ## Execution Plan ### Phase 1: Analysis & Planning @@ -65,19 +74,21 @@ Create GitHub Actions CI/CD workflow for automated deployment to staging and pro | Manual dispatch | Either | Select environment in UI | ### Required GitHub Setup -1. **Staging environment** - Already exists with secrets (HOST, USER, SSH_KEY, PROJECT_PATH) +1. **Staging environment** - Already exists, needs additional secrets: + - Add: ANTHROPIC_API_KEY, OPENAI_API_KEY, CORS_ORIGINS, API_URL, WS_URL 2. **Production environment** - Create manually when ready: - Go to repo Settings → Environments → New environment - Name: `production` - - Add same secrets: HOST, USER, SSH_KEY, PROJECT_PATH + - Add all secrets from staging - Optional: Add required reviewers for production deployments ### Server Requirements The deployment expects: - Python 3.11+ with ability to create venv - Node.js 20+ with npm +- PM2 installed globally (`npm install -g pm2`) - Git installed and repo cloned at PROJECT_PATH -- Optional: systemd services `codeframe-backend` and `codeframe-frontend` +- `ecosystem.config.js` in project root (PM2 configuration) ### Manual Deployment Use workflow_dispatch in GitHub Actions UI: From 5a2d0543ff77aa26b7308a65ee11639b95eb865c Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:05:01 -0700 Subject: [PATCH 04/10] fix(ci): Enable DEBUG and HOT_RELOAD for staging environment --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 75a9d0c8..b27df2e2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -92,10 +92,10 @@ jobs: LOG_LEVEL=INFO LOG_FILE=${REMOTE_PATH}/.codeframe/logs/codeframe.log - # Environment + # Environment & Development Flags ENVIRONMENT=staging - DEBUG=false - HOT_RELOAD=false + DEBUG=true + HOT_RELOAD=true ENVEOF echo "✅ .env.staging created" @@ -233,7 +233,7 @@ jobs: LOG_LEVEL=INFO LOG_FILE=${REMOTE_PATH}/.codeframe/logs/codeframe.log - # Environment + # Environment & Development Flags ENVIRONMENT=production DEBUG=false HOT_RELOAD=false From cf977518dbe9e0bd9035e0f3cc63c54bf597803b Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:06:08 -0700 Subject: [PATCH 05/10] refactor(ci): Pull all env vars from GitHub secrets All environment variables now come from GitHub secrets so they can be configured per-environment (staging vs production) in GitHub settings. Secrets required per environment: - ANTHROPIC_API_KEY, OPENAI_API_KEY - DATABASE_PATH, API_HOST, API_PORT - CORS_ORIGINS, API_URL, WS_URL - LOG_LEVEL, LOG_FILE - ENVIRONMENT, DEBUG, HOT_RELOAD --- .github/workflows/deploy.yml | 56 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b27df2e2..dfcecbf6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -63,13 +63,21 @@ jobs: REMOTE_PATH: ${{ secrets.PROJECT_PATH }} ENV_ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ENV_OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }} + ENV_DATABASE_PATH: ${{ secrets.DATABASE_PATH }} + ENV_API_HOST: ${{ secrets.API_HOST }} + ENV_API_PORT: ${{ secrets.API_PORT }} ENV_CORS: ${{ secrets.CORS_ORIGINS }} ENV_API_URL: ${{ secrets.API_URL }} ENV_WS_URL: ${{ secrets.WS_URL }} + ENV_LOG_LEVEL: ${{ secrets.LOG_LEVEL }} + ENV_LOG_FILE: ${{ secrets.LOG_FILE }} + ENV_ENVIRONMENT: ${{ secrets.ENVIRONMENT }} + ENV_DEBUG: ${{ secrets.DEBUG }} + ENV_HOT_RELOAD: ${{ secrets.HOT_RELOAD }} run: | echo "📝 Creating .env.staging file..." ssh ${REMOTE_USER}@${REMOTE_HOST} "cat > ${REMOTE_PATH}/.env.staging && chmod 600 ${REMOTE_PATH}/.env.staging" << ENVEOF - # CodeFRAME Staging Environment Configuration + # CodeFRAME Environment Configuration # Auto-generated by GitHub Actions deployment # AI Provider API Keys @@ -77,11 +85,11 @@ jobs: OPENAI_API_KEY=${ENV_OPENAI_KEY} # Database Configuration - DATABASE_PATH=${REMOTE_PATH}/.codeframe/state.db + DATABASE_PATH=${ENV_DATABASE_PATH} # Status Server Configuration - API_HOST=127.0.0.1 - API_PORT=14200 + API_HOST=${ENV_API_HOST} + API_PORT=${ENV_API_PORT} CORS_ALLOWED_ORIGINS=${ENV_CORS} # Web UI Configuration @@ -89,13 +97,13 @@ jobs: NEXT_PUBLIC_WS_URL=${ENV_WS_URL} # Logging Configuration - LOG_LEVEL=INFO - LOG_FILE=${REMOTE_PATH}/.codeframe/logs/codeframe.log + LOG_LEVEL=${ENV_LOG_LEVEL} + LOG_FILE=${ENV_LOG_FILE} # Environment & Development Flags - ENVIRONMENT=staging - DEBUG=true - HOT_RELOAD=true + ENVIRONMENT=${ENV_ENVIRONMENT} + DEBUG=${ENV_DEBUG} + HOT_RELOAD=${ENV_HOT_RELOAD} ENVEOF echo "✅ .env.staging created" @@ -158,7 +166,7 @@ jobs: run: | echo "🔍 Verifying deployment..." sleep 10 - ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:14200/health" || echo "⚠️ Health check failed or endpoint not available" + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:${{ secrets.API_PORT }}/health" || echo "⚠️ Health check failed or endpoint not available" - name: Deployment summary run: | @@ -204,13 +212,21 @@ jobs: REMOTE_PATH: ${{ secrets.PROJECT_PATH }} ENV_ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ENV_OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }} + ENV_DATABASE_PATH: ${{ secrets.DATABASE_PATH }} + ENV_API_HOST: ${{ secrets.API_HOST }} + ENV_API_PORT: ${{ secrets.API_PORT }} ENV_CORS: ${{ secrets.CORS_ORIGINS }} ENV_API_URL: ${{ secrets.API_URL }} ENV_WS_URL: ${{ secrets.WS_URL }} + ENV_LOG_LEVEL: ${{ secrets.LOG_LEVEL }} + ENV_LOG_FILE: ${{ secrets.LOG_FILE }} + ENV_ENVIRONMENT: ${{ secrets.ENVIRONMENT }} + ENV_DEBUG: ${{ secrets.DEBUG }} + ENV_HOT_RELOAD: ${{ secrets.HOT_RELOAD }} run: | echo "📝 Creating .env.production file..." ssh ${REMOTE_USER}@${REMOTE_HOST} "cat > ${REMOTE_PATH}/.env.production && chmod 600 ${REMOTE_PATH}/.env.production" << ENVEOF - # CodeFRAME Production Environment Configuration + # CodeFRAME Environment Configuration # Auto-generated by GitHub Actions deployment # AI Provider API Keys @@ -218,11 +234,11 @@ jobs: OPENAI_API_KEY=${ENV_OPENAI_KEY} # Database Configuration - DATABASE_PATH=${REMOTE_PATH}/.codeframe/state.db + DATABASE_PATH=${ENV_DATABASE_PATH} # Status Server Configuration - API_HOST=127.0.0.1 - API_PORT=14200 + API_HOST=${ENV_API_HOST} + API_PORT=${ENV_API_PORT} CORS_ALLOWED_ORIGINS=${ENV_CORS} # Web UI Configuration @@ -230,13 +246,13 @@ jobs: NEXT_PUBLIC_WS_URL=${ENV_WS_URL} # Logging Configuration - LOG_LEVEL=INFO - LOG_FILE=${REMOTE_PATH}/.codeframe/logs/codeframe.log + LOG_LEVEL=${ENV_LOG_LEVEL} + LOG_FILE=${ENV_LOG_FILE} # Environment & Development Flags - ENVIRONMENT=production - DEBUG=false - HOT_RELOAD=false + ENVIRONMENT=${ENV_ENVIRONMENT} + DEBUG=${ENV_DEBUG} + HOT_RELOAD=${ENV_HOT_RELOAD} ENVEOF echo "✅ .env.production created" @@ -339,7 +355,7 @@ jobs: run: | echo "🔍 Verifying production deployment..." sleep 15 - ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:14200/health" || echo "⚠️ Health check failed or endpoint not available" + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:${{ secrets.API_PORT }}/health" || echo "⚠️ Health check failed or endpoint not available" - name: Deployment summary run: | From 8d27233bded10eb4288f0ef4c3e30b6e35f2bd8b Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:09:08 -0700 Subject: [PATCH 06/10] fix(ci): Use explicit PM2 process names per environment - Staging: ecosystem.staging.config.js, check for codeframe-staging-backend - Production: ecosystem.production.config.js, check for codeframe-production-backend - Use pm2 describe instead of grep for reliable process detection --- .github/workflows/deploy.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dfcecbf6..82248e49 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -145,12 +145,15 @@ jobs: # Restart PM2 services echo "🔄 Restarting PM2 services..." if command -v pm2 &> /dev/null; then + CONFIG_FILE="ecosystem.staging.config.js" + APP_NAME="codeframe-staging" + # Check if PM2 processes exist, if not start them - if pm2 list | grep -q "codeframe-staging"; then - pm2 restart ecosystem.config.js + if pm2 describe "\${APP_NAME}-backend" > /dev/null 2>&1; then + pm2 restart \${CONFIG_FILE} echo "✅ PM2 services restarted" else - pm2 start ecosystem.config.js + pm2 start \${CONFIG_FILE} echo "✅ PM2 services started" fi pm2 save @@ -328,18 +331,15 @@ jobs: # Restart PM2 services echo "🔄 Restarting PM2 services..." if command -v pm2 &> /dev/null; then - # Production uses different ecosystem config if it exists - if [ -f ecosystem.production.config.js ]; then - CONFIG_FILE="ecosystem.production.config.js" - else - CONFIG_FILE="ecosystem.config.js" - fi + CONFIG_FILE="ecosystem.production.config.js" + APP_NAME="codeframe-production" - if pm2 list | grep -q "codeframe"; then - pm2 restart \$CONFIG_FILE + # Check if PM2 processes exist, if not start them + if pm2 describe "\${APP_NAME}-backend" > /dev/null 2>&1; then + pm2 restart \${CONFIG_FILE} echo "✅ PM2 services restarted" else - pm2 start \$CONFIG_FILE + pm2 start \${CONFIG_FILE} echo "✅ PM2 services started" fi pm2 save From 4e1158b4a920fe06b38bc29f5653012047d74b41 Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:09:59 -0700 Subject: [PATCH 07/10] feat(ci): Improve production backup with retention and compression - Move backups from /tmp to PROJECT_PATH/backups for persistence - Create timestamped compressed tar.gz archives - Include: database, .codeframe configs, .env files, ecosystem configs, logs - Record git commit info in archive - Implement retention policy (keep last 10 backups) - Use atomic operations (create in /tmp, mv to final location) - Set proper permissions (700 dir, 600 archives) - Abort deployment if backup fails - Report backup size and count after completion --- .github/workflows/deploy.yml | 84 ++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 82248e49..d68d801a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -264,21 +264,87 @@ jobs: ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH set -e echo "💾 Creating pre-deployment backup..." - cd ${{ secrets.PROJECT_PATH }} - # Create backup directory - BACKUP_DIR="/tmp/codeframe-backup-\$(date +%Y%m%d-%H%M%S)" - mkdir -p \$BACKUP_DIR + PROJECT_PATH="${{ secrets.PROJECT_PATH }}" + BACKUP_BASE="\${PROJECT_PATH}/backups" + TIMESTAMP="\$(date +%Y%m%d-%H%M%S)" + BACKUP_NAME="backup-\${TIMESTAMP}" + TMP_BACKUP="/tmp/\${BACKUP_NAME}" + FINAL_ARCHIVE="\${BACKUP_BASE}/\${BACKUP_NAME}.tar.gz" + RETENTION_COUNT=10 + + cd \${PROJECT_PATH} + + # Create persistent backup directory with proper permissions + mkdir -p \${BACKUP_BASE} + chmod 700 \${BACKUP_BASE} + + # Create temporary staging directory + mkdir -p \${TMP_BACKUP} + + # Record current commit + git rev-parse HEAD > \${TMP_BACKUP}/previous_commit.txt + git log -1 --format="%H %s" >> \${TMP_BACKUP}/previous_commit.txt + echo "📝 Current commit: \$(head -1 \${TMP_BACKUP}/previous_commit.txt)" # Backup database if [ -f .codeframe/state.db ]; then - cp .codeframe/state.db \$BACKUP_DIR/ - echo "✅ Database backed up to \$BACKUP_DIR" + cp .codeframe/state.db \${TMP_BACKUP}/ + echo "✅ Database backed up" + fi + + # Backup config files + if [ -d .codeframe ]; then + cp -r .codeframe/config.* \${TMP_BACKUP}/ 2>/dev/null || true + cp -r .codeframe/*.json \${TMP_BACKUP}/ 2>/dev/null || true + fi + + # Backup environment files + cp .env* \${TMP_BACKUP}/ 2>/dev/null || true + cp ecosystem*.config.js \${TMP_BACKUP}/ 2>/dev/null || true + + # Backup recent logs (last 1000 lines each to keep size manageable) + if [ -d logs ]; then + mkdir -p \${TMP_BACKUP}/logs + for logfile in logs/*.log; do + if [ -f "\$logfile" ]; then + tail -1000 "\$logfile" > "\${TMP_BACKUP}/logs/\$(basename \$logfile)" 2>/dev/null || true + fi + done + echo "✅ Logs backed up" fi - # Record current commit for potential rollback - git rev-parse HEAD > \$BACKUP_DIR/previous_commit.txt - echo "✅ Current commit recorded: \$(cat \$BACKUP_DIR/previous_commit.txt)" + # Create compressed archive atomically + echo "📦 Creating compressed archive..." + tar -czf "\${TMP_BACKUP}.tar.gz" -C /tmp "\${BACKUP_NAME}" || { + echo "❌ Failed to create backup archive" + rm -rf \${TMP_BACKUP} + exit 1 + } + + # Move to final location atomically + mv "\${TMP_BACKUP}.tar.gz" "\${FINAL_ARCHIVE}" || { + echo "❌ Failed to move backup to final location" + rm -rf \${TMP_BACKUP} "\${TMP_BACKUP}.tar.gz" + exit 1 + } + + # Set proper permissions on archive + chmod 600 "\${FINAL_ARCHIVE}" + + # Cleanup temp directory + rm -rf \${TMP_BACKUP} + + # Retention policy: keep last N backups + echo "🗑️ Applying retention policy (keeping last \${RETENTION_COUNT} backups)..." + cd \${BACKUP_BASE} + ls -t backup-*.tar.gz 2>/dev/null | tail -n +\$((RETENTION_COUNT + 1)) | xargs -r rm -f + + # Report backup status + BACKUP_SIZE=\$(du -h "\${FINAL_ARCHIVE}" | cut -f1) + BACKUP_COUNT=\$(ls -1 backup-*.tar.gz 2>/dev/null | wc -l) + echo "✅ Backup created: \${FINAL_ARCHIVE} (\${BACKUP_SIZE})" + echo "📊 Total backups retained: \${BACKUP_COUNT}" ENDSSH - name: Deploy to production server From ec27d44b9ab885ebb78d05abe6164e8e6f04aedb Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:11:09 -0700 Subject: [PATCH 08/10] fix(ci): Add retry loop to health checks with proper failure handling - Replace one-liner health check with retry loop (12 attempts, 5s apart) - Exit 0 immediately on success - Exit 1 after all attempts fail (fails the workflow) - Show progress on each attempt - Applied to both staging and production deployments --- .github/workflows/deploy.yml | 40 +++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d68d801a..00b3fa65 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -167,9 +167,24 @@ jobs: - name: Verify deployment run: | - echo "🔍 Verifying deployment..." - sleep 10 - ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:${{ secrets.API_PORT }}/health" || echo "⚠️ Health check failed or endpoint not available" + echo "🔍 Verifying staging deployment..." + MAX_ATTEMPTS=12 + SLEEP_SECONDS=5 + + for i in $(seq 1 $MAX_ATTEMPTS); do + echo "Health check attempt $i/$MAX_ATTEMPTS..." + if ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:${{ secrets.API_PORT }}/health"; then + echo "✅ Health check passed on attempt $i" + exit 0 + fi + if [ $i -lt $MAX_ATTEMPTS ]; then + echo "⏳ Waiting ${SLEEP_SECONDS}s before retry..." + sleep $SLEEP_SECONDS + fi + done + + echo "❌ Health check failed after $MAX_ATTEMPTS attempts" + exit 1 - name: Deployment summary run: | @@ -420,8 +435,23 @@ jobs: - name: Verify deployment run: | echo "🔍 Verifying production deployment..." - sleep 15 - ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:${{ secrets.API_PORT }}/health" || echo "⚠️ Health check failed or endpoint not available" + MAX_ATTEMPTS=12 + SLEEP_SECONDS=5 + + for i in $(seq 1 $MAX_ATTEMPTS); do + echo "Health check attempt $i/$MAX_ATTEMPTS..." + if ssh ${{ secrets.USER }}@${{ secrets.HOST }} "curl -sf http://localhost:${{ secrets.API_PORT }}/health"; then + echo "✅ Health check passed on attempt $i" + exit 0 + fi + if [ $i -lt $MAX_ATTEMPTS ]; then + echo "⏳ Waiting ${SLEEP_SECONDS}s before retry..." + sleep $SLEEP_SECONDS + fi + done + + echo "❌ Health check failed after $MAX_ATTEMPTS attempts" + exit 1 - name: Deployment summary run: | From 0ea776a6022225674011ef932497aca151c9d142 Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:12:16 -0700 Subject: [PATCH 09/10] fix(ci): Target specific PM2 processes instead of entire ecosystem - Use pm2 jlist with grep for robust process existence check - Restart individual processes by name when they exist - Start with --only flag when processes don't exist - Handle backend and frontend processes separately - Applied to both staging and production deployments --- .github/workflows/deploy.yml | 90 +++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 00b3fa65..38f81594 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -144,24 +144,41 @@ jobs: # Restart PM2 services echo "🔄 Restarting PM2 services..." - if command -v pm2 &> /dev/null; then - CONFIG_FILE="ecosystem.staging.config.js" - APP_NAME="codeframe-staging" - - # Check if PM2 processes exist, if not start them - if pm2 describe "\${APP_NAME}-backend" > /dev/null 2>&1; then - pm2 restart \${CONFIG_FILE} - echo "✅ PM2 services restarted" - else - pm2 start \${CONFIG_FILE} - echo "✅ PM2 services started" - fi - pm2 save - else + if ! command -v pm2 &> /dev/null; then echo "❌ PM2 not found - please install PM2 globally" exit 1 fi + CONFIG_FILE="ecosystem.staging.config.js" + BACKEND_NAME="codeframe-staging-backend" + FRONTEND_NAME="codeframe-staging-frontend" + + # Function to check if a PM2 process exists + process_exists() { + pm2 jlist 2>/dev/null | grep -q "\"name\":\"\\$1\"" + } + + # Handle backend process + if process_exists "\${BACKEND_NAME}"; then + echo "♻️ Restarting \${BACKEND_NAME}..." + pm2 restart "\${BACKEND_NAME}" + else + echo "🚀 Starting \${BACKEND_NAME}..." + pm2 start \${CONFIG_FILE} --only "\${BACKEND_NAME}" + fi + + # Handle frontend process + if process_exists "\${FRONTEND_NAME}"; then + echo "♻️ Restarting \${FRONTEND_NAME}..." + pm2 restart "\${FRONTEND_NAME}" + else + echo "🚀 Starting \${FRONTEND_NAME}..." + pm2 start \${CONFIG_FILE} --only "\${FRONTEND_NAME}" + fi + + pm2 save + echo "✅ PM2 services updated" + echo "✅ Deployment to staging complete!" ENDSSH @@ -411,24 +428,41 @@ jobs: # Restart PM2 services echo "🔄 Restarting PM2 services..." - if command -v pm2 &> /dev/null; then - CONFIG_FILE="ecosystem.production.config.js" - APP_NAME="codeframe-production" - - # Check if PM2 processes exist, if not start them - if pm2 describe "\${APP_NAME}-backend" > /dev/null 2>&1; then - pm2 restart \${CONFIG_FILE} - echo "✅ PM2 services restarted" - else - pm2 start \${CONFIG_FILE} - echo "✅ PM2 services started" - fi - pm2 save - else + if ! command -v pm2 &> /dev/null; then echo "❌ PM2 not found - please install PM2 globally" exit 1 fi + CONFIG_FILE="ecosystem.production.config.js" + BACKEND_NAME="codeframe-production-backend" + FRONTEND_NAME="codeframe-production-frontend" + + # Function to check if a PM2 process exists + process_exists() { + pm2 jlist 2>/dev/null | grep -q "\"name\":\"\\$1\"" + } + + # Handle backend process + if process_exists "\${BACKEND_NAME}"; then + echo "♻️ Restarting \${BACKEND_NAME}..." + pm2 restart "\${BACKEND_NAME}" + else + echo "🚀 Starting \${BACKEND_NAME}..." + pm2 start \${CONFIG_FILE} --only "\${BACKEND_NAME}" + fi + + # Handle frontend process + if process_exists "\${FRONTEND_NAME}"; then + echo "♻️ Restarting \${FRONTEND_NAME}..." + pm2 restart "\${FRONTEND_NAME}" + else + echo "🚀 Starting \${FRONTEND_NAME}..." + pm2 start \${CONFIG_FILE} --only "\${FRONTEND_NAME}" + fi + + pm2 save + echo "✅ PM2 services updated" + echo "✅ Deployment to production complete!" ENDSSH From 3c6207697ec9b7f0f70b99008d6c2b6739057adb Mon Sep 17 00:00:00 2001 From: frankbria Date: Mon, 15 Dec 2025 22:13:54 -0700 Subject: [PATCH 10/10] security(ci): Fix command injection vulnerability in env file creation - Use printf with %s to build env content (no shell interpretation) - Base64 encode content before transfer to prevent injection - Decode safely on remote server - Write to .tmp file first, then atomic mv to final location - Verify file was created and is non-empty before proceeding - Exit with error if creation fails This prevents command injection if secrets contain shell metacharacters like backticks, $(), or other special characters. --- .github/workflows/deploy.yml | 154 ++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 58 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 38f81594..50f7aa54 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -76,36 +76,55 @@ jobs: ENV_HOT_RELOAD: ${{ secrets.HOT_RELOAD }} run: | echo "📝 Creating .env.staging file..." - ssh ${REMOTE_USER}@${REMOTE_HOST} "cat > ${REMOTE_PATH}/.env.staging && chmod 600 ${REMOTE_PATH}/.env.staging" << ENVEOF - # CodeFRAME Environment Configuration - # Auto-generated by GitHub Actions deployment - # AI Provider API Keys - ANTHROPIC_API_KEY=${ENV_ANTHROPIC_KEY} - OPENAI_API_KEY=${ENV_OPENAI_KEY} - - # Database Configuration - DATABASE_PATH=${ENV_DATABASE_PATH} - - # Status Server Configuration - API_HOST=${ENV_API_HOST} - API_PORT=${ENV_API_PORT} - CORS_ALLOWED_ORIGINS=${ENV_CORS} - - # Web UI Configuration - NEXT_PUBLIC_API_URL=${ENV_API_URL} - NEXT_PUBLIC_WS_URL=${ENV_WS_URL} - - # Logging Configuration - LOG_LEVEL=${ENV_LOG_LEVEL} - LOG_FILE=${ENV_LOG_FILE} + # Build env file content safely using printf (no shell interpretation) + ENV_CONTENT=$(printf '%s\n' \ + "# CodeFRAME Environment Configuration" \ + "# Auto-generated by GitHub Actions deployment" \ + "" \ + "# AI Provider API Keys" \ + "ANTHROPIC_API_KEY=${ENV_ANTHROPIC_KEY}" \ + "OPENAI_API_KEY=${ENV_OPENAI_KEY}" \ + "" \ + "# Database Configuration" \ + "DATABASE_PATH=${ENV_DATABASE_PATH}" \ + "" \ + "# Status Server Configuration" \ + "API_HOST=${ENV_API_HOST}" \ + "API_PORT=${ENV_API_PORT}" \ + "CORS_ALLOWED_ORIGINS=${ENV_CORS}" \ + "" \ + "# Web UI Configuration" \ + "NEXT_PUBLIC_API_URL=${ENV_API_URL}" \ + "NEXT_PUBLIC_WS_URL=${ENV_WS_URL}" \ + "" \ + "# Logging Configuration" \ + "LOG_LEVEL=${ENV_LOG_LEVEL}" \ + "LOG_FILE=${ENV_LOG_FILE}" \ + "" \ + "# Environment & Development Flags" \ + "ENVIRONMENT=${ENV_ENVIRONMENT}" \ + "DEBUG=${ENV_DEBUG}" \ + "HOT_RELOAD=${ENV_HOT_RELOAD}" \ + ) + + # Base64 encode to prevent any shell interpretation during transfer + ENV_BASE64=$(echo "$ENV_CONTENT" | base64 -w 0) + + # Transfer and decode safely on remote, verify creation + ssh "${REMOTE_USER}@${REMOTE_HOST}" " + set -e + echo '${ENV_BASE64}' | base64 -d > '${REMOTE_PATH}/.env.staging.tmp' + if [ ! -s '${REMOTE_PATH}/.env.staging.tmp' ]; then + echo '❌ Failed to create environment file (empty or missing)' + rm -f '${REMOTE_PATH}/.env.staging.tmp' + exit 1 + fi + mv '${REMOTE_PATH}/.env.staging.tmp' '${REMOTE_PATH}/.env.staging' + chmod 600 '${REMOTE_PATH}/.env.staging' + " - # Environment & Development Flags - ENVIRONMENT=${ENV_ENVIRONMENT} - DEBUG=${ENV_DEBUG} - HOT_RELOAD=${ENV_HOT_RELOAD} - ENVEOF - echo "✅ .env.staging created" + echo "✅ .env.staging created and verified" - name: Deploy to staging server run: | @@ -260,36 +279,55 @@ jobs: ENV_HOT_RELOAD: ${{ secrets.HOT_RELOAD }} run: | echo "📝 Creating .env.production file..." - ssh ${REMOTE_USER}@${REMOTE_HOST} "cat > ${REMOTE_PATH}/.env.production && chmod 600 ${REMOTE_PATH}/.env.production" << ENVEOF - # CodeFRAME Environment Configuration - # Auto-generated by GitHub Actions deployment - - # AI Provider API Keys - ANTHROPIC_API_KEY=${ENV_ANTHROPIC_KEY} - OPENAI_API_KEY=${ENV_OPENAI_KEY} - - # Database Configuration - DATABASE_PATH=${ENV_DATABASE_PATH} - - # Status Server Configuration - API_HOST=${ENV_API_HOST} - API_PORT=${ENV_API_PORT} - CORS_ALLOWED_ORIGINS=${ENV_CORS} - - # Web UI Configuration - NEXT_PUBLIC_API_URL=${ENV_API_URL} - NEXT_PUBLIC_WS_URL=${ENV_WS_URL} - - # Logging Configuration - LOG_LEVEL=${ENV_LOG_LEVEL} - LOG_FILE=${ENV_LOG_FILE} - - # Environment & Development Flags - ENVIRONMENT=${ENV_ENVIRONMENT} - DEBUG=${ENV_DEBUG} - HOT_RELOAD=${ENV_HOT_RELOAD} - ENVEOF - echo "✅ .env.production created" + + # Build env file content safely using printf (no shell interpretation) + ENV_CONTENT=$(printf '%s\n' \ + "# CodeFRAME Environment Configuration" \ + "# Auto-generated by GitHub Actions deployment" \ + "" \ + "# AI Provider API Keys" \ + "ANTHROPIC_API_KEY=${ENV_ANTHROPIC_KEY}" \ + "OPENAI_API_KEY=${ENV_OPENAI_KEY}" \ + "" \ + "# Database Configuration" \ + "DATABASE_PATH=${ENV_DATABASE_PATH}" \ + "" \ + "# Status Server Configuration" \ + "API_HOST=${ENV_API_HOST}" \ + "API_PORT=${ENV_API_PORT}" \ + "CORS_ALLOWED_ORIGINS=${ENV_CORS}" \ + "" \ + "# Web UI Configuration" \ + "NEXT_PUBLIC_API_URL=${ENV_API_URL}" \ + "NEXT_PUBLIC_WS_URL=${ENV_WS_URL}" \ + "" \ + "# Logging Configuration" \ + "LOG_LEVEL=${ENV_LOG_LEVEL}" \ + "LOG_FILE=${ENV_LOG_FILE}" \ + "" \ + "# Environment & Development Flags" \ + "ENVIRONMENT=${ENV_ENVIRONMENT}" \ + "DEBUG=${ENV_DEBUG}" \ + "HOT_RELOAD=${ENV_HOT_RELOAD}" \ + ) + + # Base64 encode to prevent any shell interpretation during transfer + ENV_BASE64=$(echo "$ENV_CONTENT" | base64 -w 0) + + # Transfer and decode safely on remote, verify creation + ssh "${REMOTE_USER}@${REMOTE_HOST}" " + set -e + echo '${ENV_BASE64}' | base64 -d > '${REMOTE_PATH}/.env.production.tmp' + if [ ! -s '${REMOTE_PATH}/.env.production.tmp' ]; then + echo '❌ Failed to create environment file (empty or missing)' + rm -f '${REMOTE_PATH}/.env.production.tmp' + exit 1 + fi + mv '${REMOTE_PATH}/.env.production.tmp' '${REMOTE_PATH}/.env.production' + chmod 600 '${REMOTE_PATH}/.env.production' + " + + echo "✅ .env.production created and verified" - name: Create pre-deployment backup run: |