diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..50f7aa54 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,535 @@ +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: 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_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..." + + # 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' + " + + echo "✅ .env.staging created and verified" + + - 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 .. + + # Ensure logs directory exists + mkdir -p logs + + # Restart PM2 services + echo "🔄 Restarting PM2 services..." + 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 + + - name: Verify deployment + run: | + 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: | + 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 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_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..." + + # 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: | + ssh ${{ secrets.USER }}@${{ secrets.HOST }} "bash -s" << ENDSSH + set -e + echo "💾 Creating pre-deployment backup..." + + 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 \${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 + + # 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 + 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 + + # Ensure logs directory exists + mkdir -p logs + + # Restart PM2 services + echo "🔄 Restarting PM2 services..." + 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 + + - name: Verify deployment + run: | + echo "🔍 Verifying production 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: | + 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..d154dfff 100644 --- a/claudedocs/SESSION.md +++ b/claudedocs/SESSION.md @@ -1,59 +1,98 @@ -# 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 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 +- 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, 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 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 +- `ecosystem.config.js` in project root (PM2 configuration) + +### 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"