Feature/claude helps with cicd #64
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Merge To Main | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| env: | |
| TEST_GCP_PROJECT: ${{ vars.IM_TEST_PROJECT_ID }} | |
| TEST_FIREBASE_PROJECT: ${{ vars.IM_TEST_PROJECT_ID }} | |
| TEST_DB_INSTANCE: ${{ vars.IM_TEST_DB_INSTANCE }} | |
| NODE_VERSION: 22.13.0 | |
| CLOUD_SQL_PROXY_VERSION: v2.18.0 | |
| CLOUD_SQL_PROXY_CHECKSUM: e48f49397a69a1b509a74ce025dcf76019da0dcaae17f744c524dd19e9824800 | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ACTIONS_STEP_DEBUG: true | |
| IM_DB_INSTANCE_NAME: ${{ vars.IM_DB_INSTANCE_NAME }} | |
| jobs: | |
| # ===== MIGRATION PIPELINE (Pre-Deployment) ===== | |
| check-pending-migrations: | |
| name: 🔍 Check for Pending Migrations | |
| runs-on: ubuntu-latest | |
| environment: Test-Database-NoApproval | |
| env: | |
| DB_HOST: ${{ secrets.DB_HOST }} | |
| DB_PORT: ${{ secrets.DB_PORT }} | |
| DB_USER: ${{ secrets.DB_USER }} | |
| DB_PASSWORD: ${{ secrets.DB_PASSWORD }} | |
| DB_NAME: ${{ secrets.DB_NAME }} | |
| outputs: | |
| pending-migrations: ${{ steps.check-pending.outputs.pending-migrations }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2.1.0 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Setup Cloud SQL Proxy | |
| run: | | |
| # Download Cloud SQL Proxy with checksum verification | |
| curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/${{ env.CLOUD_SQL_PROXY_VERSION }}/cloud-sql-proxy.linux.amd64 | |
| echo "${{ env.CLOUD_SQL_PROXY_CHECKSUM }} cloud-sql-proxy" | sha256sum --check | |
| chmod +x cloud-sql-proxy | |
| # Start proxy in background | |
| ./cloud-sql-proxy --port 5432 ${{ env.TEST_DB_INSTANCE }} & | |
| PROXY_PID=$! | |
| # Wait for proxy to be ready with retry logic | |
| echo "⏳ Waiting for Cloud SQL Proxy to be ready..." | |
| for i in {1..30}; do | |
| if nc -z 127.0.0.1 5432; then | |
| echo "✅ Cloud SQL Proxy is ready (attempt $i)" | |
| break | |
| fi | |
| if [ $i -eq 30 ]; then | |
| echo "❌ Cloud SQL Proxy failed to start after 30 attempts" | |
| echo "📋 Proxy logs:" | |
| ps aux | grep cloud-sql-proxy || echo "No proxy process found" | |
| kill $PROXY_PID 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "⏳ Attempt $i: Proxy not ready, waiting 2 seconds..." | |
| sleep 2 | |
| done | |
| - name: Check for pending migrations | |
| id: check-pending | |
| run: | | |
| npx ts-node --project ./util/tsconfig.json ./util/check_for_pending_migrations.ts > ./migration_check.log | |
| if grep -q 'MIGRATION_PENDING=true' migration_check.log; then | |
| echo "pending-migrations=true" >> $GITHUB_OUTPUT | |
| echo "⚠️ Pending migrations detected" | |
| else | |
| echo "pending-migrations=false" >> $GITHUB_OUTPUT | |
| echo "✅ No pending migrations" | |
| fi | |
| cat migration_check.log | |
| - name: Show pending migrations | |
| if: steps.check-pending.outputs.pending-migrations == 'true' | |
| id: show-migrations | |
| run: | | |
| echo "📋 PENDING MIGRATIONS PREVIEW:" | |
| echo "" | |
| npx ts-node --project ./util/tsconfig.json ./node_modules/typeorm/cli.js migration:show -f util/ormconfig.js || echo "Unable to show migrations - will be visible during application" | |
| echo "" | |
| echo "📁 Migration files to be applied:" | |
| ls -la libs/migration/*.ts 2>/dev/null || echo "No migration files found in libs/migration/" | |
| apply-pending-migrations: | |
| name: 🔍 Apply Pending Migrations (View Check Pending Migrations job to see migrations before running) | |
| runs-on: ubuntu-latest | |
| needs: [check-pending-migrations] | |
| environment: Test-Database-Approval | |
| env: | |
| DB_HOST: ${{ secrets.DB_HOST }} | |
| DB_PORT: ${{ secrets.DB_PORT }} | |
| DB_USER: ${{ secrets.DB_USER }} | |
| DB_PASSWORD: ${{ secrets.DB_PASSWORD }} | |
| DB_NAME: ${{ secrets.DB_NAME }} | |
| outputs: | |
| backup-id: ${{ steps.create-backup-pending.outputs.backup-id }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@v4 | |
| if: needs.check-pending-migrations.outputs.pending-migrations == 'true' | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install dependencies | |
| if: needs.check-pending-migrations.outputs.pending-migrations == 'true' | |
| run: npm ci | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2.1.0 | |
| if: needs.check-pending-migrations.outputs.pending-migrations == 'true' | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Setup Cloud SQL Proxy | |
| if: needs.check-pending-migrations.outputs.pending-migrations == 'true' | |
| run: | | |
| # Download Cloud SQL Proxy with checksum verification | |
| curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/${{ env.CLOUD_SQL_PROXY_VERSION }}/cloud-sql-proxy.linux.amd64 | |
| echo "${{ env.CLOUD_SQL_PROXY_CHECKSUM }} cloud-sql-proxy" | sha256sum --check | |
| chmod +x cloud-sql-proxy | |
| # Start proxy in background | |
| ./cloud-sql-proxy --port 5432 ${{ env.TEST_DB_INSTANCE }} & | |
| PROXY_PID=$! | |
| # Wait for proxy to be ready with retry logic | |
| echo "⏳ Waiting for Cloud SQL Proxy to be ready..." | |
| for i in {1..30}; do | |
| if nc -z 127.0.0.1 5432; then | |
| echo "✅ Cloud SQL Proxy is ready (attempt $i)" | |
| break | |
| fi | |
| if [ $i -eq 30 ]; then | |
| echo "❌ Cloud SQL Proxy failed to start after 30 attempts" | |
| echo "📋 Proxy logs:" | |
| ps aux | grep cloud-sql-proxy || echo "No proxy process found" | |
| kill $PROXY_PID 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "⏳ Attempt $i: Proxy not ready, waiting 2 seconds..." | |
| sleep 2 | |
| done | |
| - name: Create database backup for pending migrations | |
| id: create-backup-pending | |
| if: needs.check-pending-migrations.outputs.pending-migrations == 'true' | |
| run: | | |
| echo "📦 Creating on-demand database backup for pending migrations..." | |
| echo "🔧 Using instance: ${{ env.IM_DB_INSTANCE_NAME }}" | |
| # Create backup and capture the full output for debugging | |
| echo "🔄 Creating backup for instance: ${{ env.IM_DB_INSTANCE_NAME }}" | |
| # Create backup with quiet flag to reduce noise, capture JSON output separately | |
| if gcloud sql backups create --instance=${{ env.IM_DB_INSTANCE_NAME }} --description="Pending migration backup $(date +%Y-%m-%d_%H:%M:%S)" --quiet; then | |
| echo "✅ Backup creation command completed" | |
| # Get the most recent backup (the one we just created) | |
| BACKUP_OUTPUT=$(gcloud sql backups list --instance=${{ env.IM_DB_INSTANCE_NAME }} --limit=1 --format="json" 2>/dev/null) | |
| echo "🔍 Recent backup info: $BACKUP_OUTPUT" | |
| else | |
| echo "❌ Backup creation failed" | |
| exit 1 | |
| fi | |
| # Extract backup ID from JSON response (handle both object and array formats) | |
| BACKUP_ID=$(echo "$BACKUP_OUTPUT" | jq -r 'if type == "array" then .[0].name // .[0].id else .name // .id end // empty') | |
| echo "🆔 Extracted backup ID: '$BACKUP_ID'" | |
| if [ -z "$BACKUP_ID" ] || [ "$BACKUP_ID" = "null" ]; then | |
| echo "❌ Failed to extract backup ID from response" | |
| echo "🔍 Attempting alternative backup creation method..." | |
| # Try alternative method: create backup without JSON format and get ID separately | |
| TIMESTAMP=$(date +%Y%m%d%H%M%S) | |
| BACKUP_DESC="PendingMigrationBackup-$TIMESTAMP" | |
| echo "🔄 Creating backup with description: $BACKUP_DESC" | |
| if gcloud sql backups create --instance=${{ env.IM_DB_INSTANCE_NAME }} --description="$BACKUP_DESC" --quiet; then | |
| echo "✅ Backup creation command succeeded" | |
| # Wait a moment for backup to appear in list | |
| sleep 5 | |
| # Find the backup we just created | |
| BACKUP_ID=$(gcloud sql backups list --instance=${{ env.IM_DB_INSTANCE_NAME }} --filter="description:$BACKUP_DESC" --format="value(name)" --limit=1) | |
| if [ -n "$BACKUP_ID" ]; then | |
| echo "✅ Found backup with ID: $BACKUP_ID" | |
| else | |
| echo "⏳ Backup not yet visible, checking recent backups..." | |
| BACKUP_ID=$(gcloud sql backups list --instance=${{ env.IM_DB_INSTANCE_NAME }} --limit=1 --format="value(name)") | |
| if [ -n "$BACKUP_ID" ]; then | |
| echo "✅ Using most recent backup: $BACKUP_ID" | |
| else | |
| echo "❌ No backup found after creation" | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| echo "❌ Alternative backup creation also failed" | |
| exit 1 | |
| fi | |
| fi | |
| echo "backup-id=$BACKUP_ID" >> $GITHUB_OUTPUT | |
| echo "✅ On-demand backup created: $BACKUP_ID" | |
| - name: Apply pending migrations (if any) | |
| if: needs.check-pending-migrations.outputs.pending-migrations == 'true' | |
| run: | | |
| echo "⚙️ Applying pending migrations..." | |
| npx ts-node --project ./util/tsconfig.json ./node_modules/typeorm/cli.js migration:run -f util/ormconfig.js | |
| echo "✅ Pending migrations applied successfully" | |
| check-schema-drift: | |
| name: 🔍 Check for Schema Drift | |
| runs-on: ubuntu-latest | |
| needs: [apply-pending-migrations] | |
| environment: Test-Database-NoApproval | |
| env: | |
| DB_HOST: ${{ secrets.DB_HOST }} | |
| DB_PORT: ${{ secrets.DB_PORT }} | |
| DB_USER: ${{ secrets.DB_USER }} | |
| DB_PASSWORD: ${{ secrets.DB_PASSWORD }} | |
| DB_NAME: ${{ secrets.DB_NAME }} | |
| outputs: | |
| migration-needed: ${{ steps.check-drift.outputs.migration-needed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2.1.0 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Setup Cloud SQL Proxy | |
| run: | | |
| # Download Cloud SQL Proxy with checksum verification | |
| curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/${{ env.CLOUD_SQL_PROXY_VERSION }}/cloud-sql-proxy.linux.amd64 | |
| echo "${{ env.CLOUD_SQL_PROXY_CHECKSUM }} cloud-sql-proxy" | sha256sum --check | |
| chmod +x cloud-sql-proxy | |
| # Start proxy in background | |
| ./cloud-sql-proxy --port 5432 ${{ env.TEST_DB_INSTANCE }} & | |
| PROXY_PID=$! | |
| # Wait for proxy to be ready with retry logic | |
| echo "⏳ Waiting for Cloud SQL Proxy to be ready..." | |
| for i in {1..30}; do | |
| if nc -z 127.0.0.1 5432; then | |
| echo "✅ Cloud SQL Proxy is ready (attempt $i)" | |
| break | |
| fi | |
| if [ $i -eq 30 ]; then | |
| echo "❌ Cloud SQL Proxy failed to start after 30 attempts" | |
| echo "📋 Proxy logs:" | |
| ps aux | grep cloud-sql-proxy || echo "No proxy process found" | |
| kill $PROXY_PID 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "⏳ Attempt $i: Proxy not ready, waiting 2 seconds..." | |
| sleep 2 | |
| done | |
| - name: Check for schema drift | |
| id: check-drift | |
| run: | | |
| npx ts-node --project ./util/tsconfig.json ./util/detect_db_entity_drift.ts > ./drift.log | |
| if grep -q 'MIGRATION_NEEDED=true' drift.log; then | |
| echo "migration-needed=true" >> $GITHUB_OUTPUT | |
| echo "📈 Schema drift detected - new migration needed" | |
| else | |
| echo "migration-needed=false" >> $GITHUB_OUTPUT | |
| echo "✅ No schema drift detected" | |
| fi | |
| - name: Preview new migration | |
| id: preview-migration | |
| if: steps.check-drift.outputs.migration-needed == 'true' | |
| run: | | |
| echo "📋 NEW MIGRATION PREVIEW:" | |
| echo "" | |
| echo "🔄 Generating migration preview (dry-run)..." | |
| npx ts-node --project ./util/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -n PreviewMigration-$(date +%Y%m%d%H%M%S) -f util/ormconfig.js --dryrun 2>/dev/null || { | |
| echo "⚠️ TypeORM dry-run not supported, generating actual migration for preview..." | |
| npx ts-node --project ./util/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -n PreviewMigration-$(date +%Y%m%d%H%M%S) -f util/ormconfig.js | |
| echo "" | |
| echo "📁 Generated migration preview:" | |
| ls -la libs/migration/*PreviewMigration*.ts 2>/dev/null | tail -1 | |
| echo "" | |
| echo "📄 Migration content:" | |
| cat $(ls -t libs/migration/*PreviewMigration*.ts 2>/dev/null | head -1) || echo "Unable to display migration content" | |
| echo "" | |
| echo "🗑️ Cleaning up preview migration..." | |
| rm -f libs/migration/*PreviewMigration*.ts | |
| } | |
| - name: New migration review required | |
| if: steps.check-drift.outputs.migration-needed == 'true' | |
| run: | | |
| echo "🔍 NEW MIGRATION REQUIRED" | |
| echo "" | |
| echo "📈 Schema changes detected that require a new migration." | |
| echo "" | |
| echo "Please verify:" | |
| echo "- Review the migration preview shown above" | |
| echo "- Ensure the generated SQL is safe to apply" | |
| echo "- Confirm no data loss will occur" | |
| echo "- Check that indexes and constraints are properly handled" | |
| echo "" | |
| echo "After approval, the system will:" | |
| echo "- Generate a new migration file" | |
| echo "- Commit it to this PR" | |
| echo "- Apply the migration to the database" | |
| echo "" | |
| echo "✅ Approve to proceed with generating and applying the new migration." | |
| echo "" | |
| generate-and-apply-migration: | |
| name: ⚙️ Generate & Apply New Migration | |
| runs-on: ubuntu-latest | |
| environment: Test-Database-Approval | |
| needs: [check-schema-drift, apply-pending-migrations] | |
| outputs: | |
| backup-id: ${{ steps.create-backup-new.outputs.backup-id || steps.use-existing-backup.outputs.backup-id }} | |
| env: | |
| DB_HOST: ${{ secrets.DB_HOST }} | |
| DB_PORT: ${{ secrets.DB_PORT }} | |
| DB_USER: ${{ secrets.DB_USER }} | |
| DB_PASSWORD: ${{ secrets.DB_PASSWORD }} | |
| DB_NAME: ${{ secrets.DB_NAME }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install dependencies | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' | |
| run: npm ci | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2.1.0 | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Setup Cloud SQL Proxy | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' | |
| run: | | |
| # Download Cloud SQL Proxy with checksum verification | |
| curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/${{ env.CLOUD_SQL_PROXY_VERSION }}/cloud-sql-proxy.linux.amd64 | |
| echo "${{ env.CLOUD_SQL_PROXY_CHECKSUM }} cloud-sql-proxy" | sha256sum --check | |
| chmod +x cloud-sql-proxy | |
| # Start proxy in background | |
| ./cloud-sql-proxy --port 5432 ${{ env.TEST_DB_INSTANCE }} & | |
| PROXY_PID=$! | |
| # Wait for proxy to be ready with retry logic | |
| echo "⏳ Waiting for Cloud SQL Proxy to be ready..." | |
| for i in {1..30}; do | |
| if nc -z 127.0.0.1 5432; then | |
| echo "✅ Cloud SQL Proxy is ready (attempt $i)" | |
| break | |
| fi | |
| if [ $i -eq 30 ]; then | |
| echo "❌ Cloud SQL Proxy failed to start after 30 attempts" | |
| echo "📋 Proxy logs:" | |
| ps aux | grep cloud-sql-proxy || echo "No proxy process found" | |
| kill $PROXY_PID 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "⏳ Attempt $i: Proxy not ready, waiting 2 seconds..." | |
| sleep 2 | |
| done | |
| - name: Configure git | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} | |
| # Checkout the actual branch instead of detached HEAD | |
| git fetch origin ${{ github.head_ref }} | |
| git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }} | |
| - name: Create database backup (if not already done) | |
| id: create-backup-new | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' && (needs.apply-pending-migrations.outputs.backup-id == '' || needs.apply-pending-migrations.outputs.backup-id == null) | |
| run: | | |
| echo "📦 Creating on-demand database backup for new migration..." | |
| echo "🔧 Using instance: ${{ env.IM_DB_INSTANCE_NAME }}" | |
| # Create backup and capture the full output for debugging | |
| echo "🔄 Creating backup for instance: ${{ env.IM_DB_INSTANCE_NAME }}" | |
| # Create backup with quiet flag to reduce noise, capture JSON output separately | |
| if gcloud sql backups create --instance=${{ env.IM_DB_INSTANCE_NAME }} --description="New migration backup $(date +%Y-%m-%d_%H:%M:%S)" --quiet; then | |
| echo "✅ Backup creation command completed" | |
| # Get the most recent backup (the one we just created) | |
| BACKUP_OUTPUT=$(gcloud sql backups list --instance=${{ env.IM_DB_INSTANCE_NAME }} --limit=1 --format="json" 2>/dev/null) | |
| echo "🔍 Recent backup info: $BACKUP_OUTPUT" | |
| else | |
| echo "❌ Backup creation failed" | |
| exit 1 | |
| fi | |
| # Extract backup ID from JSON response (handle both object and array formats) | |
| BACKUP_ID=$(echo "$BACKUP_OUTPUT" | jq -r 'if type == "array" then .[0].name // .[0].id else .name // .id end // empty') | |
| echo "🆔 Extracted backup ID: '$BACKUP_ID'" | |
| if [ -z "$BACKUP_ID" ] || [ "$BACKUP_ID" = "null" ]; then | |
| echo "❌ Failed to extract backup ID from response" | |
| echo "🔍 Attempting alternative backup creation method..." | |
| # Try alternative method: create backup without JSON format and get ID separately | |
| TIMESTAMP=$(date +%Y%m%d%H%M%S) | |
| BACKUP_DESC="PendingMigrationBackup-$TIMESTAMP" | |
| echo "🔄 Creating backup with description: $BACKUP_DESC" | |
| if gcloud sql backups create --instance=${{ env.IM_DB_INSTANCE_NAME }} --description="$BACKUP_DESC" --quiet; then | |
| echo "✅ Backup creation command succeeded" | |
| # Wait a moment for backup to appear in list | |
| sleep 5 | |
| # Find the backup we just created | |
| BACKUP_ID=$(gcloud sql backups list --instance=${{ env.IM_DB_INSTANCE_NAME }} --filter="description:$BACKUP_DESC" --format="value(name)" --limit=1) | |
| if [ -n "$BACKUP_ID" ]; then | |
| echo "✅ Found backup with ID: $BACKUP_ID" | |
| else | |
| echo "⏳ Backup not yet visible, checking recent backups..." | |
| BACKUP_ID=$(gcloud sql backups list --instance=${{ env.IM_DB_INSTANCE_NAME }} --limit=1 --format="value(name)") | |
| if [ -n "$BACKUP_ID" ]; then | |
| echo "✅ Using most recent backup: $BACKUP_ID" | |
| else | |
| echo "❌ No backup found after creation" | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| echo "❌ Alternative backup creation also failed" | |
| exit 1 | |
| fi | |
| fi | |
| echo "backup-id=$BACKUP_ID" >> $GITHUB_OUTPUT | |
| echo "✅ On-demand backup created: $BACKUP_ID" | |
| - name: Use existing backup if available | |
| id: use-existing-backup | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' && needs.apply-pending-migrations.outputs.backup-id != '' && needs.apply-pending-migrations.outputs.backup-id != null | |
| run: | | |
| echo "♾️ Using existing backup from pending migrations: ${{ needs.apply-pending-migrations.outputs.backup-id }}" | |
| echo "backup-id=${{ needs.apply-pending-migrations.outputs.backup-id }}" >> $GITHUB_OUTPUT | |
| - name: Generate and apply migration | |
| if: needs.check-schema-drift.outputs.migration-needed == 'true' | |
| run: | | |
| set -e # Exit on any error | |
| echo "🔄 Generating new migration..." | |
| MIGRATION_NAME="AutoMigration-$(date +%Y%m%d%H%M%S)" | |
| if ! npx ts-node --project ./util/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -n "$MIGRATION_NAME" -f util/ormconfig.js; then | |
| echo "❌ Failed to generate migration" | |
| exit 1 | |
| fi | |
| # Verify migration file was created | |
| MIGRATION_FILE=$(ls libs/migration/*"$MIGRATION_NAME"*.ts 2>/dev/null | head -1) | |
| if [ -z "$MIGRATION_FILE" ]; then | |
| echo "❌ Migration file not found after generation" | |
| exit 1 | |
| fi | |
| echo "✅ Generated migration: $MIGRATION_FILE" | |
| echo "💾 Committing generated migration..." | |
| if ! git add libs/migration/*.ts; then | |
| echo "❌ Failed to stage migration files" | |
| exit 1 | |
| fi | |
| if ! git commit -m "chore: auto-generated migration - approved 🤖 This migration was automatically generated and approved. Generated and applied at: $(date -u)"; then | |
| echo "❌ Failed to commit migration" | |
| exit 1 | |
| fi | |
| # Pull any remote changes before pushing | |
| git pull origin ${{ github.head_ref }} --rebase || echo "No remote changes to pull" | |
| if ! git push origin ${{ github.head_ref }}; then | |
| echo "❌ Failed to push migration commit" | |
| exit 1 | |
| fi | |
| echo "✅ Migration committed and pushed" | |
| echo "⚙️ Applying new migration..." | |
| if ! npx ts-node --project ./util/tsconfig.json ./node_modules/typeorm/cli.js migration:run -f util/ormconfig.js; then | |
| echo "❌ Failed to apply migration" | |
| echo "⚠️ Migration was committed but failed to apply - manual intervention required" | |
| exit 1 | |
| fi | |
| echo "✅ New migration applied successfully" | |
| deploy-test: | |
| name: ▶️ Deploy → Test (MANUAL) | |
| environment: Test | |
| runs-on: ubuntu-latest | |
| needs: [generate-and-apply-migration] | |
| if: needs.generate-and-apply-migration.result == 'success' | |
| concurrency: | |
| group: test-environment | |
| # allow only one "waiting for approval" + "running" job in that group | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Determine new version name | |
| id: get-version | |
| run: | | |
| TAG=${GITHUB_REF#refs/} | |
| TAG=${TAG#merge} | |
| SHORT_SHA=${GITHUB_SHA:0:7} | |
| V="${TAG,,}-${SHORT_SHA}" | |
| V=${V//[^a-z0-9-]/-} | |
| echo $V | |
| echo "NEW_VERSION=${V}" >> $GITHUB_OUTPUT | |
| shell: bash | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install nx@12.3.6 | |
| run: npm install -g nx@12.3.6 | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2.1.0 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Generate API Environment (test) | |
| run: npx ts-node ./util/generate_environment.ts generate test | |
| env: | |
| INVOLVEMINT_SETTINGS: ${{ secrets.INVOLVEMINT_TEST_SETTINGS_SERVER }} | |
| - name: Build API (test) | |
| run: | | |
| export NODE_OPTIONS=--openssl-legacy-provider | |
| nx run-many --target=build --projects=api,involvemint --configuration=test --skip-nx-cache | |
| - name: Store current versions for rollback | |
| id: store-versions | |
| run: | | |
| # Store current API version | |
| CURRENT_API_VERSION=$(gcloud app versions list \ | |
| --service=default \ | |
| --format="value(version.id)" \ | |
| --sort-by="~version.createTime" \ | |
| --limit=1 2>/dev/null || echo "none") | |
| echo "current-api-version=$CURRENT_API_VERSION" >> $GITHUB_OUTPUT | |
| echo "📋 Current API version: $CURRENT_API_VERSION" | |
| - name: 🚀 Deploy API to App Engine (Test) - No Traffic Initially | |
| id: deploy-api | |
| run: | | |
| set -e | |
| gcloud config set project ${{ env.TEST_GCP_PROJECT }} | |
| # Deploy without stopping previous version and without traffic | |
| echo "🚀 Deploying new API version without traffic..." | |
| gcloud app deploy app.yaml \ | |
| --no-stop-previous-version \ | |
| --no-promote \ | |
| --quiet \ | |
| --version=${{ steps.get-version.outputs.NEW_VERSION }} | |
| echo "✅ API deployed successfully (no traffic yet)" | |
| - name: Delete API Environment (test) | |
| if: steps.deploy-api.outcome == 'success' | |
| run: npx ts-node ./util/generate_environment.ts delete test | |
| - name: Generate Client Environment (test) | |
| if: steps.deploy-api.outcome == 'success' | |
| run: npx ts-node ./util/generate_environment.ts generate test | |
| env: | |
| INVOLVEMINT_SETTINGS: ${{ secrets.INVOLVEMINT_TEST_SETTINGS_CLIENT }} | |
| - name: Build Client (test) | |
| if: steps.deploy-api.outcome == 'success' | |
| run: | | |
| export NODE_OPTIONS=--openssl-legacy-provider | |
| nx run-many --target=build --projects=involvemint --configuration=test --skip-nx-cache | |
| - name: Install firebase-tools@13.29.0 | |
| if: steps.deploy-api.outcome == 'success' | |
| run: npm install -g firebase-tools@13.29.0 | |
| - name: 🚀 Deploy Client to Firebase | |
| id: deploy-client | |
| if: steps.deploy-api.outcome == 'success' | |
| run: | | |
| set -e | |
| firebase use ${{ env.TEST_FIREBASE_PROJECT }} | |
| firebase deploy --message "${{ steps.get-version.outputs.NEW_VERSION }}" | |
| echo "✅ Client deployed successfully" | |
| - name: Promote API version (switch traffic) | |
| id: promote-api | |
| if: steps.deploy-client.outcome == 'success' | |
| run: | | |
| set -e | |
| echo "🔄 Switching traffic to new API version..." | |
| gcloud app services set-traffic default \ | |
| --splits="${{ steps.get-version.outputs.NEW_VERSION }}=1" \ | |
| --quiet | |
| echo "🧹 Cleaning up old API versions (keeping last 3)..." | |
| # Keep the last 3 versions for rollback capability, delete older ones | |
| gcloud app versions list --service=default --format="value(version.id)" --sort-by="~version.createTime" --limit=10 | tail -n +4 | xargs -r gcloud app versions delete --quiet || echo "⚠️ Could not delete old versions" | |
| echo "✅ Atomic deployment completed successfully" | |
| - name: Comprehensive rollback on failure | |
| if: ${{ failure() }} | |
| run: | | |
| set -e | |
| echo "🚨 Deployment failed - initiating rollback procedures..." | |
| # Rollback API if it was deployed but not yet promoted | |
| if [ "${{ steps.deploy-api.outcome }}" == "success" ] && [ "${{ steps.promote-api.outcome }}" != "success" ]; then | |
| echo "🔄 Cleaning up failed API deployment..." | |
| gcloud app versions delete ${{ steps.get-version.outputs.NEW_VERSION }} --quiet || echo "⚠️ Could not delete failed API version" | |
| fi | |
| # Rollback API traffic if promotion happened but client deployment failed | |
| if [ "${{ steps.promote-api.outcome }}" == "success" ] && [ "${{ steps.deploy-client.outcome }}" != "success" ]; then | |
| echo "🔄 Rolling back API traffic to previous version..." | |
| PREV_VERSION="${{ steps.store-versions.outputs.current-api-version }}" | |
| if [ "$PREV_VERSION" != "none" ] && [ -n "$PREV_VERSION" ]; then | |
| gcloud app services set-traffic default --splits="$PREV_VERSION=1" --quiet | |
| gcloud app versions delete ${{ steps.get-version.outputs.NEW_VERSION }} --quiet || echo "⚠️ Could not delete failed API version" | |
| echo "✅ API traffic rolled back to $PREV_VERSION" | |
| else | |
| echo "❌ No previous API version found for rollback" | |
| fi | |
| fi | |
| # Database rollback if migrations were applied | |
| BACKUP_ID="${{ needs.generate-and-apply-migration.outputs.backup-id }}" | |
| if [ -n "$BACKUP_ID" ] && [ "$BACKUP_ID" != "" ]; then | |
| echo "🔄 Rolling back database to backup: $BACKUP_ID" | |
| echo "⚠️ Note: Database rollback from backup requires manual approval due to data loss risk" | |
| echo "📋 Backup ID available for manual restore: $BACKUP_ID" | |
| echo "🔧 To restore: gcloud sql backups restore $BACKUP_ID --restore-instance=${{ env.TEST_DB_INSTANCE }}" | |
| else | |
| echo "ℹ️ No database backup available for rollback" | |
| fi | |
| # TODO: Add Firebase client rollback when available | |
| # Note: Firebase doesn't have built-in version rollback, would need custom implementation | |
| echo "🔄 Rollback procedures completed" | |
| shell: bash | |
| test-env-verified: | |
| name: ✅ Approve Test Env Validation (MANUAL) | |
| environment: Test | |
| runs-on: ubuntu-latest | |
| needs: deploy-test | |
| steps: | |
| - run: echo "Testing in TEST environment has been completed, approved by." | |