Merge pull request #234 from ejay-dev/feat/enterprise-defense-flow #805
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: 'Deployment Quality Gates' | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: 'Target environment' | |
| required: true | |
| default: 'production' | |
| type: choice | |
| options: | |
| - staging | |
| - production | |
| skip_extended_gates: | |
| description: 'Skip extended (non-core) quality gates for hotfix' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| NODE_VERSION: '20' | |
| DEPLOYMENT_TIMEOUT: '600' # 10 minutes | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || vars.NEXT_PUBLIC_SUPABASE_URL || '' }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || vars.NEXT_PUBLIC_SUPABASE_ANON_KEY || '' }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY || vars.SUPABASE_SERVICE_ROLE_KEY || '' }} | |
| NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL || vars.NEXT_PUBLIC_APP_URL || 'https://app.formaos.com.au' }} | |
| NEXT_PUBLIC_SITE_URL: ${{ secrets.NEXT_PUBLIC_SITE_URL || vars.NEXT_PUBLIC_SITE_URL || 'https://www.formaos.com.au' }} | |
| FOUNDER_EMAILS: ${{ secrets.FOUNDER_EMAILS || vars.FOUNDER_EMAILS || '' }} | |
| HEALTH_DETAILED_FOUNDER_TOKEN: ${{ secrets.HEALTH_DETAILED_FOUNDER_TOKEN || '' }} | |
| STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY || '' }} | |
| STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET || '' }} | |
| STRIPE_PRICE_FOUNDATION: ${{ secrets.STRIPE_PRICE_FOUNDATION || vars.STRIPE_PRICE_FOUNDATION || '' }} | |
| STRIPE_PRICE_GROWTH: ${{ secrets.STRIPE_PRICE_GROWTH || vars.STRIPE_PRICE_GROWTH || '' }} | |
| # audit M9: required by scripts/check-env.js productionRequiredKeys but was | |
| # absent here, so the production-config gate could never pass clean. Set the | |
| # STRIPE_PRICE_SCALE secret/var in the repo for this to resolve. | |
| STRIPE_PRICE_SCALE: ${{ secrets.STRIPE_PRICE_SCALE || vars.STRIPE_PRICE_SCALE || '' }} | |
| RESEND_API_KEY: ${{ secrets.RESEND_API_KEY || '' }} | |
| RESEND_FROM_EMAIL: ${{ secrets.RESEND_FROM_EMAIL || vars.RESEND_FROM_EMAIL || '' }} | |
| UPSTASH_REDIS_REST_URL: ${{ secrets.UPSTASH_REDIS_REST_URL || vars.UPSTASH_REDIS_REST_URL || '' }} | |
| UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN || '' }} | |
| CRON_SECRET: ${{ secrets.CRON_SECRET || '' }} | |
| NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN || vars.NEXT_PUBLIC_SENTRY_DSN || '' }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN || '' }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG || vars.SENTRY_ORG || '' }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT || vars.SENTRY_PROJECT || '' }} | |
| jobs: | |
| # Pre-deployment validation | |
| pre_deployment_check: | |
| name: 'Pre-Deployment Validation' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| deploy_approved: ${{ steps.quality_gate.outputs.approved }} | |
| skip_extended: ${{ github.event.inputs.skip_extended_gates }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run quality gate checks | |
| run: | | |
| echo "Running pre-deployment quality checks..." | |
| echo "✅ Core quality gates ALWAYS run — cannot be bypassed" | |
| if [ "${{ github.event.inputs.skip_extended_gates }}" == "true" ]; then | |
| echo "⚠️ Extended gates will be skipped (hotfix mode)" | |
| fi | |
| echo "approved=conditional" >> $GITHUB_OUTPUT | |
| id: quality_gate | |
| # Core quality validation (cannot be skipped) | |
| core_quality_validation: | |
| name: 'Core Quality Validation' | |
| runs-on: ubuntu-latest | |
| needs: pre_deployment_check | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Production configuration validation (advisory) | |
| # Audit 2026-05-28: this check has been failing on every main | |
| # push for months because it tries to verify production secrets | |
| # (FOUNDER_EMAILS, STRIPE_*, RESEND_*, UPSTASH_*, CRON_SECRET, | |
| # SENTRY_*) against process.env in CI, but those secrets live | |
| # only in Vercel — they were never mirrored to GitHub Actions | |
| # secrets. The workflow can't actually block production deploys | |
| # either, because Vercel's git integration deploys main on its | |
| # own schedule independent of this gate. | |
| # | |
| # Demoted to advisory so the summary still surfaces what's | |
| # missing in CI (operators can mirror as needed), but a red gate | |
| # doesn't hide real failures from the rest of the pipeline. | |
| # Re-promote to blocking once GitHub Actions secrets mirror | |
| # Vercel prod and this step passes clean for a week. | |
| continue-on-error: true | |
| run: npm run check:production-config | |
| - name: TypeScript compilation (critical) | |
| run: npx tsc --noEmit | |
| - name: ESLint validation (critical) | |
| # Ceiling is intentionally tight: lint-warning-ratchet.yml runs weekly | |
| # and opens a PR to lower it when actuals fall further. Bumping this | |
| # number to land code is a code-review-blocking event. | |
| run: npm run lint -- --max-warnings 25 | |
| - name: Security baseline (critical) | |
| run: SECURITY_BASELINE_STRICT=1 npm run check:security-baseline | |
| - name: Build verification (critical) | |
| run: npm run build | |
| - name: Critical security tests (cannot be skipped) | |
| run: | | |
| npx playwright install --with-deps | |
| npx playwright test e2e/admin-security-verification.spec.ts --reporter=list | |
| # Extended quality validation (can be skipped in emergency hotfix only) | |
| extended_quality_validation: | |
| name: 'Extended Quality Validation' | |
| runs-on: ubuntu-latest | |
| needs: [pre_deployment_check, core_quality_validation] | |
| if: needs.pre_deployment_check.outputs.skip_extended != 'true' | |
| # Extended gates are now blocking. The escape hatch is the explicit | |
| # `skip_extended_gates` workflow_dispatch input above — silently | |
| # `continue-on-error: true` was hiding regressions (Blocker 4). | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Full test suite | |
| run: npm test -- --coverage --watchAll=false | |
| env: | |
| CI: true | |
| - name: Full E2E test suite | |
| # Failing E2E specs that should not gate the release belong in | |
| # e2e/quarantine/ with a TODO referencing their remediation | |
| # ticket. Adding `continue-on-error: true` here is not the fix. | |
| run: | | |
| npx playwright install --with-deps | |
| npm run build | |
| npx playwright test --reporter=list | |
| - name: Performance baseline check (Lighthouse) | |
| # v4-028: previously echoed "Performance check would run here" | |
| # — pure theatre. Now runs a real Lighthouse CI against the | |
| # built app's preview URL. Score thresholds enforced via | |
| # .lighthouserc.json; missing thresholds fail the job. | |
| run: | | |
| npm install -g @lhci/cli@0.13.x | |
| npx lhci autorun --config=./lighthouserc.json --upload.target=temporary-public-storage | |
| continue-on-error: false | |
| # Deployment approval gate | |
| deployment_approval: | |
| name: 'Deployment Approval' | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| pre_deployment_check, | |
| core_quality_validation, | |
| extended_quality_validation, | |
| ] | |
| if: always() | |
| environment: | |
| name: ${{ github.event.inputs.environment || 'production' }} | |
| url: https://formaos.com.au | |
| steps: | |
| - name: Deployment readiness check | |
| run: | | |
| echo "## Deployment Readiness Report" >> $GITHUB_STEP_SUMMARY | |
| echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Core Quality | ${{ needs.core_quality_validation.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Extended Quality | ${{ needs.extended_quality_validation.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Emergency Override | ${{ needs.pre_deployment_check.outputs.skip_extended }} |" >> $GITHUB_STEP_SUMMARY | |
| - name: Block deployment on core failures | |
| if: needs.core_quality_validation.result == 'failure' | |
| run: | | |
| echo "❌ DEPLOYMENT BLOCKED: Core quality validation failed" | |
| echo "Critical issues must be resolved before deployment" | |
| exit 1 | |
| - name: Approve deployment | |
| run: | | |
| echo "✅ DEPLOYMENT APPROVED" | |
| if [ "${{ needs.pre_deployment_check.outputs.skip_extended }}" == "true" ]; then | |
| echo "⚠️ Hotfix mode — extended gates skipped, core gates passed" | |
| else | |
| echo "📋 All quality gates satisfied" | |
| fi | |
| # Deployment execution | |
| deploy_to_vercel: | |
| name: 'Deploy to Vercel' | |
| runs-on: ubuntu-latest | |
| needs: [deployment_approval] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build application | |
| run: npm run build | |
| - name: Deploy to Vercel | |
| # v4-028: the prior implementation literally echoed | |
| # "npx vercel --prod --yes" without invoking it. Production | |
| # deployment is owned by Vercel's git integration (pushes to | |
| # main trigger a build out-of-band). This step now either | |
| # actually runs the Vercel CLI when VERCEL_TOKEN is provided | |
| # (manual deploy path) or no-ops with a clear message when | |
| # relying on git integration. | |
| env: | |
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| DEPLOY_ENV: ${{ github.event.inputs.environment || 'production' }} | |
| run: | | |
| if [ -z "$VERCEL_TOKEN" ]; then | |
| echo "ℹ️ VERCEL_TOKEN not set — production deployment is owned by" | |
| echo " the Vercel git integration. This job has no work to do." | |
| echo " To enable workflow-driven deploys, add VERCEL_TOKEN +" | |
| echo " VERCEL_ORG_ID + VERCEL_PROJECT_ID repository secrets." | |
| exit 0 | |
| fi | |
| echo "🚀 Deploying via Vercel CLI to $DEPLOY_ENV..." | |
| npm install -g vercel@latest | |
| if [ "$DEPLOY_ENV" = "production" ]; then | |
| npx vercel --prod --yes --token="$VERCEL_TOKEN" | |
| else | |
| npx vercel --yes --token="$VERCEL_TOKEN" | |
| fi | |
| echo "✅ Vercel CLI deploy completed" | |
| # Post-deployment validation | |
| post_deployment_validation: | |
| name: 'Post-Deployment Validation' | |
| runs-on: ubuntu-latest | |
| needs: [deploy_to_vercel] | |
| steps: | |
| - name: Health check | |
| # audit M10: these checks used `curl -f … || echo "⚠️"`, so a real | |
| # outage only printed a warning and the step still passed. Now they | |
| # fail the job. `-L` follows the apex→www 308 redirect | |
| # (next.config.ts), `--retry` rides out cold-start latency. | |
| run: | | |
| set -euo pipefail | |
| echo "🏥 Running post-deployment health checks..." | |
| # Site root (follows apex → www redirect) | |
| curl -fsSL --retry 3 --retry-delay 5 --retry-connrefused \ | |
| -o /dev/null https://formaos.com.au | |
| # Pricing page | |
| curl -fsSL --retry 3 --retry-delay 5 --retry-connrefused \ | |
| -o /dev/null https://formaos.com.au/pricing | |
| echo "✅ Basic health checks passed" | |
| - name: Security verification | |
| run: | | |
| set -euo pipefail | |
| echo "🔒 Verifying security after deployment..." | |
| # Admin routes must redirect/deny unauthenticated traffic. A 200 OR | |
| # a curl failure (000) both fail the gate — a publicly-reachable | |
| # admin route or an unreachable site are both deployment failures. | |
| RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -L --max-redirs 0 https://formaos.com.au/admin || echo "000") | |
| case "$RESPONSE" in | |
| 301|302|303|307|308|401|403) | |
| echo "✅ Admin routes properly protected (HTTP $RESPONSE)" ;; | |
| *) | |
| echo "❌ Admin route protection check failed (HTTP $RESPONSE)" | |
| exit 1 ;; | |
| esac | |
| - name: Deployment summary | |
| run: | | |
| echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "🚀 **Environment:** ${{ github.event.inputs.environment || 'production' }}" >> $GITHUB_STEP_SUMMARY | |
| echo "📅 **Deployed at:** $(date)" >> $GITHUB_STEP_SUMMARY | |
| echo "🔗 **URL:** https://formaos.com.au" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ **Status:** Deployment completed successfully" >> $GITHUB_STEP_SUMMARY | |
| # Rollback trigger (manual) | |
| rollback_trigger: | |
| name: 'Rollback Preparation' | |
| runs-on: ubuntu-latest | |
| needs: [post_deployment_validation] | |
| if: failure() | |
| steps: | |
| - name: Prepare rollback information | |
| run: | | |
| echo "## Rollback Information" >> $GITHUB_STEP_SUMMARY | |
| echo "❌ **Deployment Status:** Failed validation" >> $GITHUB_STEP_SUMMARY | |
| echo "🔄 **Rollback Required:** Yes" >> $GITHUB_STEP_SUMMARY | |
| echo "📋 **Next Steps:**" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Investigate deployment failure" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Consider manual rollback to previous version" >> $GITHUB_STEP_SUMMARY | |
| echo "3. Review quality gate failures" >> $GITHUB_STEP_SUMMARY |