Skip to content

Merge pull request #248 from ejay-dev/feat/app-ui-enterprise-elevation #819

Merge pull request #248 from ejay-dev/feat/app-ui-enterprise-elevation

Merge pull request #248 from ejay-dev/feat/app-ui-enterprise-elevation #819

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