name: Deploy Supabase to Production on: workflow_dispatch: inputs: deploy_production: description: Promote the verified change to production after non-production required: false type: boolean default: false deploy_infrastructure: description: Apply Terraform as part of this manual deployment required: false type: boolean default: false push: branches: - master paths: - 'apps/supabase/**' - 'terraform/**' - '.github/workflows/deploy-supabase.yml' jobs: changes: name: Detect infrastructure changes runs-on: ubuntu-latest outputs: infrastructure: ${{ steps.infrastructure.outputs.changed }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - id: infrastructure name: Check Terraform scope env: BEFORE_SHA: ${{ github.event.before }} CURRENT_SHA: ${{ github.sha }} DEPLOY_INFRASTRUCTURE: ${{ inputs.deploy_infrastructure }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "changed=${DEPLOY_INFRASTRUCTURE:-false}" >> "$GITHUB_OUTPUT" elif git diff --quiet "$BEFORE_SHA" "$CURRENT_SHA" -- terraform/; then echo "changed=false" >> "$GITHUB_OUTPUT" else echo "changed=true" >> "$GITHUB_OUTPUT" fi quality: name: Verify deployable source runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm - run: npm ci - run: npm run verify:fast - run: npm run build deploy-database: name: Deploy Migrations & Functions runs-on: ubuntu-latest needs: quality environment: ${{ matrix.environment }} strategy: max-parallel: 1 matrix: environment: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.deploy_production && '["non-prod","prod"]' || '["non-prod"]') }} env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} SUPABASE_PROJECT_REF: ${{ matrix.environment == 'prod' && 'ckjcrkjpmhqhiucifukx' || 'itbzhrvlgvdmzbnhzhyx' }} ENVIRONMENT_NAME: ${{ matrix.environment }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Supabase CLI uses: supabase/setup-cli@v1 with: version: latest - name: Link to ${{ matrix.environment }} Project run: supabase link --project-ref $SUPABASE_PROJECT_REF --workdir apps - name: Apply Database Migrations (${{ matrix.environment }}) run: supabase db push --workdir apps --include-all - name: Sync Edge Function Secrets (${{ matrix.environment }}) env: # Add secrets from GitHub Secrets REVENUECAT_WEBHOOK_SECRET: ${{ secrets.REVENUECAT_WEBHOOK_SECRET }} REVENUECAT_API_KEY: ${{ secrets.REVENUECAT_API_KEY }} REVENUECAT_ENTITLEMENT_ID: ${{ secrets.REVENUECAT_ENTITLEMENT_ID }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} ADMIN_PRIVATE_KEY_JWK: ${{ secrets.ADMIN_PRIVATE_KEY_JWK }} ADMIN_KEYS_JSON: ${{ secrets.ADMIN_KEYS_JSON }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | # Sync secrets to Supabase (only if they're set in GitHub) secrets_to_set="" [ -n "$REVENUECAT_WEBHOOK_SECRET" ] && secrets_to_set="$secrets_to_set REVENUECAT_WEBHOOK_SECRET=$REVENUECAT_WEBHOOK_SECRET" [ -n "$REVENUECAT_API_KEY" ] && secrets_to_set="$secrets_to_set REVENUECAT_API_KEY=$REVENUECAT_API_KEY" [ -n "$REVENUECAT_ENTITLEMENT_ID" ] && secrets_to_set="$secrets_to_set REVENUECAT_ENTITLEMENT_ID=$REVENUECAT_ENTITLEMENT_ID" [ -n "$OPENROUTER_API_KEY" ] && secrets_to_set="$secrets_to_set OPENROUTER_API_KEY=$OPENROUTER_API_KEY" [ -n "$ADMIN_PRIVATE_KEY_JWK" ] && secrets_to_set="$secrets_to_set ADMIN_PRIVATE_KEY_JWK=$ADMIN_PRIVATE_KEY_JWK" [ -n "$ADMIN_KEYS_JSON" ] && secrets_to_set="$secrets_to_set ADMIN_KEYS_JSON=$ADMIN_KEYS_JSON" [ -n "$DISCORD_WEBHOOK_URL" ] && secrets_to_set="$secrets_to_set DISCORD_WEBHOOK_URL=$DISCORD_WEBHOOK_URL" if [ -n "$secrets_to_set" ]; then echo "Syncing secrets to Supabase ($ENVIRONMENT_NAME)..." supabase secrets set $secrets_to_set --project-ref $SUPABASE_PROJECT_REF else echo "No secrets to sync" fi - name: Deploy Edge Functions (${{ matrix.environment }}) run: | # Deploy all edge functions (excluding _shared directory) for dir in apps/supabase/functions/*/; do function_name=$(basename "$dir") # Skip _shared directory (it's a shared module, not a function) if [ "$function_name" = "_shared" ]; then echo "Skipping _shared (shared module)" continue fi echo "Deploying function: $function_name to $ENVIRONMENT_NAME" supabase functions deploy "$function_name" --no-verify-jwt --project-ref $SUPABASE_PROJECT_REF --workdir apps done - name: Remove Deleted Functions (${{ matrix.environment }}) run: | # Get list of functions deployed on Supabase (skip header lines, extract slug column) deployed=$(supabase functions list --project-ref $SUPABASE_PROJECT_REF | grep -E '^[a-z]' | awk '{print $1}') # Get list of functions in the repo (excluding _shared) local_functions=$(ls -d apps/supabase/functions/*/ 2>/dev/null | xargs -I {} basename {} | grep -v '^_shared$' || true) # Delete any deployed functions that no longer exist in the repo for func in $deployed; do # Skip empty or invalid function names [ -z "$func" ] && continue [[ "$func" =~ ^[^a-z] ]] && continue if ! echo "$local_functions" | grep -q "^${func}$"; then echo "Deleting removed function from $ENVIRONMENT_NAME: $func" supabase functions delete "$func" --project-ref $SUPABASE_PROJECT_REF fi done deploy-infrastructure: name: Deploy Infrastructure (Terraform) runs-on: ubuntu-latest needs: [quality, changes] if: needs.changes.outputs.infrastructure == 'true' env: TF_VAR_supabase_access_token: ${{ secrets.SUPABASE_ACCESS_TOKEN }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.6" # Non-Production Environment - name: Terraform Init (Non-Prod) working-directory: terraform/environments/non-prod run: terraform init - name: Terraform Format Check working-directory: terraform/modules/supabase run: terraform fmt -check - name: Terraform Validate (Non-Prod) working-directory: terraform/environments/non-prod run: terraform validate - name: Terraform Plan (Non-Prod) working-directory: terraform/environments/non-prod run: terraform plan -out=tfplan - name: Terraform Apply (Non-Prod) working-directory: terraform/environments/non-prod run: terraform apply -auto-approve tfplan # Production Environment - name: Terraform Init (Prod) if: github.event_name == 'workflow_dispatch' && inputs.deploy_production working-directory: terraform/environments/prod run: terraform init - name: Terraform Validate (Prod) if: github.event_name == 'workflow_dispatch' && inputs.deploy_production working-directory: terraform/environments/prod run: terraform validate - name: Terraform Plan (Prod) if: github.event_name == 'workflow_dispatch' && inputs.deploy_production working-directory: terraform/environments/prod run: terraform plan -out=tfplan - name: Terraform Apply (Prod) if: github.event_name == 'workflow_dispatch' && inputs.deploy_production working-directory: terraform/environments/prod run: terraform apply -auto-approve tfplan