ci(deploy): establish staged Dokploy promotion

This commit is contained in:
jjbrunton committed 2026-08-28 14:54:12 +01:00
1 parent 2dad7aa352
commit a455325147
14 files changed
+199 -310

No files matched your search

-215
View File
@@ -1,215 +0,0 @@
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
+50 -45
View File
@@ -3,13 +3,35 @@ name: Verify
on:
pull_request:
push:
branches: [master]
branches: [staging, main]
concurrency:
group: verify-${{ github.ref }}
cancel-in-progress: true
jobs:
branch-policy:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Enforce promotion flow
env:
BASE_BRANCH: ${{ github.base_ref }}
HEAD_BRANCH: ${{ github.head_ref }}
run: |
if [ "$BASE_BRANCH" = "master" ]; then
echo "master is frozen; target staging or main" >&2
exit 1
fi
if [ "$BASE_BRANCH" = "main" ] && [ "$HEAD_BRANCH" != "staging" ]; then
echo "Only staging may be promoted to main" >&2
exit 1
fi
if [ "$BASE_BRANCH" != "staging" ] && [ "$BASE_BRANCH" != "main" ]; then
echo "Feature pull requests target staging; promotions target main" >&2
exit 1
fi
static:
runs-on: ubuntu-latest
steps:
@@ -23,56 +45,39 @@ jobs:
- run: npm run verify:fast
- run: npm run build
full-stack:
backend-integration:
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 15
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: sauci_test
POSTGRES_USER: sauci
POSTGRES_PASSWORD: sauci_ci_only
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U sauci -d sauci_test"
--health-interval 5s
--health-timeout 3s
--health-retries 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Start local Supabase
- name: Run standalone backend integration tests
env:
GOOGLE_CLIENT_ID: local
GOOGLE_CLIENT_SECRET: local
APPLE_CLIENT_ID: local
APPLE_CLIENT_SECRET: local
run: npx supabase start --workdir apps
- name: Reset and seed
run: |
npx supabase db reset --workdir apps
scripts/with-local-supabase-env.sh tests node scripts/seed-e2e.mjs
- name: Start application services
run: |
npx supabase functions serve --workdir apps > /tmp/sauci-functions.log 2>&1 &
scripts/with-local-supabase-env.sh web npm run dev -w @sauci/web -- --port 3000 > /tmp/sauci-web.log 2>&1 &
scripts/with-local-supabase-env.sh admin npm run dev -w @sauci/admin -- --host 127.0.0.1 --port 3001 > /tmp/sauci-admin.log 2>&1 &
scripts/with-local-supabase-env.sh mcp npm run dev -w sauci-mcp-server > /tmp/sauci-mcp.log 2>&1 &
- name: Wait for services
run: |
for url in http://127.0.0.1:3000 http://127.0.0.1:3001/login http://127.0.0.1:3002/health; do
for attempt in $(seq 1 60); do
curl --fail --silent "$url" >/dev/null && break
sleep 2
done
curl --fail --silent "$url" >/dev/null
done
- run: npm run test:supabase
- run: npm run verify:e2e
- name: Upload E2E proof
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-proof
path: |
e2e/test-results
e2e/playwright-report
/tmp/sauci-*.log
if-no-files-found: ignore
retention-days: 14
DATABASE_URL: postgresql://sauci:sauci_ci_only@127.0.0.1:5432/sauci_test
run: npm run test:api:integration
container-contract:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Verify production Compose contract
run: infra/backend/verify-production.sh
+10 -8
View File
@@ -1,18 +1,19 @@
# Sauci agent map
Sauci is a couples connection product: an Expo mobile app, React admin, Next.js
web app, Supabase backend, and an internal MCP service in one npm/Turborepo
workspace.
web app, standalone Node/PostgreSQL backend, hosted Supabase Auth, and an internal
MCP service in one npm/Turborepo workspace.
## Golden rules
1. Read the nearest `AGENTS.md` before changing a scoped area.
2. Preserve tenant and couple isolation. Never bypass RLS, role checks, or auth.
3. All schema changes are local files in `apps/supabase/migrations/` created with
`npm run db:migration:new -- <name>`. Never use remote `apply_migration`, never
execute DDL through MCP SQL tools, and use those SQL tools for `SELECT` only.
4. Never target production from local development or E2E. Local tests must fail
closed when a configured Supabase URL is not localhost.
3. Product-data schema changes are committed SQL files in `apps/api/drizzle/`.
Hosted Supabase owns Auth only; do not deploy product-data migrations or Edge
Functions there. Never execute remote DDL through MCP or provider tools.
4. Never target production from local development or E2E. Database integration
tests must fail closed unless `DATABASE_URL` resolves to loopback. Authenticated
native acceptance may use only the designated non-production Auth project.
5. Read `apps/mobile/DESIGN.md` before mobile UI work.
6. Keep shared package exports type-only and route them through `src/index.ts`.
7. Do not commit secrets, generated evidence, local logs, or runtime state.
@@ -26,7 +27,8 @@ workspace.
| `apps/mobile` | Expo React Native product | `apps/mobile/AGENTS.md` |
| `apps/admin` | Vite administration UI | `apps/admin/AGENTS.md` |
| `apps/web` | Next.js marketing/redemption UI | `apps/web/AGENTS.md` |
| `apps/supabase` | migrations, functions, local backend | `apps/supabase/AGENTS.md` |
| `apps/api` | standalone API, worker, PostgreSQL migrations | repository rules |
| `apps/supabase` | legacy data-plane history; hosted Auth configuration only | `apps/supabase/AGENTS.md` |
| `apps/mcp` | internal administrative MCP service | `apps/mcp/AGENTS.md` |
| `packages/shared` | shared TypeScript contracts | `packages/shared/AGENTS.md` |
| `e2e` | cross-application Playwright flows | `e2e/README.md` |
+5 -1
View File
@@ -11,12 +11,16 @@
},
"preview": {
"node": "20.10.0",
"distribution": "internal"
"distribution": "internal",
"env": {
"EXPO_PUBLIC_API_URL": "https://api.preprod.sauci.app"
}
},
"production": {
"node": "20.10.0",
"autoIncrement": true,
"env": {
"EXPO_PUBLIC_API_URL": "https://api.sauci.app",
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNramNya2pwbWhxaGl1Y2lmdWt4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcxMDc1ODEsImV4cCI6MjA4MjY4MzU4MX0.7FA_RmFXUjlmZF96O9xlAUfxHrQrSIZcJclNReLL4lI",
"EXPO_PUBLIC_REVENUECAT_IOS_API_KEY": "appl_lfPyYonFQtVytftPAfXIrGsMNFu",
+9 -9
View File
@@ -1,16 +1,16 @@
# Supabase backend
# Legacy Supabase data plane
This directory owns local configuration, migrations, Edge Functions, seeds, and
integration tests.
This directory preserves the former product-data schema, Edge Functions, seeds,
and migration source needed for audited cutover. Hosted Supabase now owns Auth
only. The active application schema and server code live under `apps/api`.
## Schema safety
- Create migrations only with `npm run db:migration:new -- <name>`.
- Edit only `apps/supabase/migrations/`; this is the authoritative migration tree.
- Do not add product-data migrations or functions here.
- Do not deploy this migration tree or these Edge Functions to hosted Supabase.
- Never use any remote `apply_migration` tool.
- Never execute DDL through MCP SQL tools; those tools are `SELECT`-only.
- Never execute DDL through MCP or provider SQL tools.
- Never link or reset a remote project during local verification.
- Cron/function URLs must resolve per environment, never hardcode production.
## Function boundaries
@@ -21,5 +21,5 @@ integration tests.
- Preserve CORS handling and return non-sensitive errors.
- Test RLS, authorization, and important side effects, not only HTTP status.
Verify with local reset, `npm run test:supabase`, invariant lint, and affected
client tests. See `docs/operations/database-migrations.md`.
Legacy tests may be run during cutover archaeology, but they are not the active
deployment gate. See `docs/operations/database-migrations.md`.
+1
View File
@@ -9,6 +9,7 @@ historical notes. Agents should load only the section relevant to their task.
- [Local full-stack development](development/local-stack.md)
- [Testing strategy](testing/strategy.md)
- [Database migration safety](operations/database-migrations.md)
- [Backend deployment and branching](operations/backend-deployments.md)
- [Agent routing and effort](agents/routing.md)
- [Learning promotion](agents/learnings.md)
- [Authentication](authentication.md)
+44
View File
@@ -0,0 +1,44 @@
# Backend deployment and branching
Dokploy deploys the standalone backend directly from GitHub. GitHub Actions is
the verification gate; it does not hold a broad Dokploy API credential and does
not deploy the retired Supabase data plane.
## Branch contract
| Branch | Purpose | Dokploy target | Public API |
|---|---|---|---|
| feature branches | isolated reviewed work | none | none |
| `staging` | integrated release candidate | `backend-staging` | `https://api.preprod.sauci.app` |
| `main` | production promotion | `backend-production` | `https://api.sauci.app` |
| `master` | frozen legacy default | none | none |
Normal feature pull requests target `staging`. After CI and authenticated staging
acceptance pass, open a promotion pull request from `staging` to `main`. Do not
add production-only commits to `main`: production should run the exact commit
already exercised in staging. Direct pushes to either deployment branch are for
explicit incident recovery only.
Each Dokploy Compose application is connected to `jjbrunton/Sauci`, watches its
single branch with push-triggered automatic deployment, and uses
`infra/backend/compose.production.yaml`. The Compose file deliberately has no
top-level project name so Dokploy assigns different project names, networks, and
named volumes to staging and production. Never point both applications at the
same PostgreSQL or media volume.
## Promotion checks
1. GitHub `Verify / static` and `Verify / backend-integration` succeed for the
candidate commit.
2. The Dokploy deployment for `staging` reports success; `/health/live` and
`/health/ready` return 200 over valid TLS.
3. An unauthenticated protected request returns 401, proving the public route is
not bypassing authentication.
4. The mobile app signs in through non-production Supabase Auth and reads real
migrated content from `https://api.preprod.sauci.app`.
5. Promote the exact `staging` commit to `main` and verify the production health,
TLS, and authentication boundary independently.
Health proves infrastructure, not customer-data cutover. The App Store build
must not be released against production until the controlled final sync and the
checks in [backend-cutover.md](backend-cutover.md) are complete.
+27 -18
View File
@@ -1,26 +1,35 @@
# Database migration safety
All schema changes are versioned local files under `apps/supabase/migrations/`.
The standalone PostgreSQL schema is owned by ordered SQL files under
`apps/api/drizzle/`. Hosted Supabase owns Auth only; its legacy product-data
migrations and Edge Functions are retained as cutover history and are not a
deployment target.
## Required workflow
1. `npm run db:migration:new -- <descriptive_name>`
2. Edit the generated local migration.
3. Prefer idempotent SQL where semantics permit it.
4. `npm run db:reset` against local Supabase.
5. Run `npm run test:supabase` and affected application checks.
6. Commit the migration with dependent code and types.
1. Add the next ordered SQL migration under `apps/api/drizzle/`.
2. Keep application, repository, shared-contract, and client changes in the same
reviewed change where the schema contract requires them.
3. Run `npm run verify:full` against the dedicated loopback PostgreSQL service.
4. Run `infra/backend/verify-production.sh` for container and migration startup.
5. Merge to `staging`; the Dokploy migration service applies the committed files
under an advisory lock before the API receives traffic.
6. Prove staging health and affected authenticated behavior before promoting the
exact commit from `staging` to `main`.
## Forbidden paths
## Safety invariants
- Never call a remote `apply_migration` MCP tool.
- Never execute DDL through MCP SQL execution.
- MCP SQL execution is read-only `SELECT` use only.
- Never insert migration history manually except an explicitly approved repair.
- Never hardcode a production URL in a migration or cron definition.
- Never point local integration tests, reset commands, or disposable verification
at a remote database.
- Never deploy `apps/supabase/migrations` or `apps/supabase/functions` as part of
the standalone backend release.
- Never execute DDL through MCP or provider SQL tools.
- Never edit migration history already applied to a persistent environment; add
a forward migration.
- Back up PostgreSQL and private media together before a destructive production
migration or data import, and prove restore separately.
- A failed migration must block API startup. Do not bypass the one-shot `migrate`
service to force new code against an old schema.
CI deploys committed migrations and Edge Functions to non-production after relevant
pushes. Production promotion is a manual workflow dispatch. Terraform is applied
only when files under `terraform/` change, or when `deploy_infrastructure` is
explicitly selected during a manual deployment. Local verification must not link a
remote project.
Production data cutover has additional stop-source, final-sync, parity, legacy
media, and authenticated-mobile checks in [backend-cutover.md](backend-cutover.md).
+11 -9
View File
@@ -6,23 +6,25 @@ is proved through the real stack.
## Gates
- `verify:fast`: invariant lint, workspace lint/typecheck, and unit tests.
- `verify:full`: fast gate plus builds and local Supabase function tests when the
required local tools are present.
- `verify:full`: fast gate plus builds and standalone API/PostgreSQL integration
tests against a loopback database.
- `verify:e2e`: Playwright browser flows and native Maestro flows against an
already-running local stack.
- `verify`: full plus E2E; this is the pre-PR target.
CI runs deterministic checks on every pull request. Running-stack tests use a
separate job because they require Docker/Supabase and produce traces,
screenshots, and videos as artifacts.
CI runs deterministic checks on every pull request and on pushes to `staging`
and `main`. The backend integration job provisions disposable PostgreSQL and
refuses non-loopback database URLs. User-facing browser/native acceptance remains
a pre-promotion check because it requires running services and test identities.
## Test ownership
- Unit/component tests live with their workspace.
- Supabase function integration tests live in `apps/supabase/tests`.
- Standalone backend integration tests live in `apps/api/test`.
- `apps/supabase/tests` cover the retired data plane and are not a deployment gate.
- Cross-application browser tests live in `e2e`.
- Native black-box flows live in `e2e/maestro`.
E2E must use local or sandbox endpoints. Tests refuse non-local Supabase URLs.
Authentication is proved once through the public flow; reusable fixture sessions
avoid repeating auth in unrelated scenarios.
E2E product-data endpoints must be local or staging, never production. Local
database tests refuse non-loopback URLs. Authenticated staging acceptance uses
the designated non-production hosted Auth project and disposable fixture users.
+7
View File
@@ -21,6 +21,13 @@ API and worker, PostgreSQL, and a one-shot migration service. Configure Dokploy
Compose file from the repository root and route the public API hostname to the
`api` service on port `3003`. Do not publish PostgreSQL.
Create separate Dokploy Compose applications for `staging` and `main`, both using
`infra/backend/compose.production.yaml`; enable push-triggered auto-deploy for
their respective branch. The file intentionally omits a top-level Compose project
name so Dokploy isolates networks and named volumes per application. See
[`docs/operations/backend-deployments.md`](../../docs/operations/backend-deployments.md)
for the promotion contract.
Copy the variable names from `.env.production.example` into Dokploy's secret
configuration. `POSTGRES_PASSWORD` and the password embedded (URL encoded) in
`DATABASE_URL` must match. Generate them for the deployment; never reuse the
-2
View File
@@ -1,5 +1,3 @@
name: sauci-backend
x-common-environment: &common-environment
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL:?set DATABASE_URL}
+2 -1
View File
@@ -20,9 +20,10 @@
"lint:harness": "node scripts/lint-harness.mjs",
"test": "turbo test --filter=!@sauci/e2e",
"test:supabase": "scripts/test-supabase.sh",
"test:api:integration": "scripts/test-api-integration.sh",
"test:mobile:e2e": "maestro test e2e/maestro",
"verify:fast": "npm run lint && npm run typecheck && npm test",
"verify:full": "npm run verify:fast && npm run build && npm run test:supabase",
"verify:full": "npm run verify:fast && npm run build && npm run test:api:integration",
"verify:e2e": "scripts/with-local-supabase-env.sh tests npm run test -w @sauci/e2e",
"verify": "npm run verify:full && npm run verify:e2e",
"db:start": "npx supabase start --workdir apps",
+17 -2
View File
@@ -31,7 +31,7 @@ for (const file of allFiles) {
if (!/\.(md|sql)$/.test(file)) continue;
const source = await readFile(file, 'utf8');
if (/^\s*[-*]\s*Use\s+.*apply_migration/im.test(source)) {
fail(file, 'instruction authorizes untracked remote migrations', 'create a local apps/supabase/migrations file instead');
fail(file, 'instruction authorizes untracked remote migrations', 'create a committed apps/api/drizzle migration instead');
}
}
@@ -53,7 +53,7 @@ if (!scoped) {
const duplicateMigrations = path.join(root, 'supabase', 'migrations');
if (await exists(duplicateMigrations)) {
const entries = await readdir(duplicateMigrations);
if (entries.length) fail(duplicateMigrations, 'second migration tree can bypass deployment', 'move migrations to apps/supabase/migrations and remove the duplicate');
if (entries.length) fail(duplicateMigrations, 'second migration tree can bypass deployment', 'move active migrations to apps/api/drizzle and remove the duplicate');
}
const cutoff = '20260201100806';
@@ -77,6 +77,21 @@ if (!scoped) {
if (/allowing all requests|query\(['"]apiKey/i.test(mcpAuth)) {
fail(path.join(root, 'apps', 'mcp', 'src', 'lib', 'auth.ts'), 'MCP authentication can leak or fail open', 'require a configured Bearer token and fail closed');
}
for (const directory of [
path.join(root, 'apps', 'mobile'),
path.join(root, 'apps', 'admin', 'src'),
path.join(root, 'apps', 'web'),
path.join(root, 'apps', 'mcp', 'src'),
]) {
for (const file of await walk(directory, [])) {
if (!/\.(?:ts|tsx|js|jsx)$/.test(file) || /(?:\.test\.|__tests__)/.test(file)) continue;
const source = await readFile(file, 'utf8');
if (/\/functions\/v1\/|\.functions\.invoke\s*\(/.test(source)) {
fail(file, 'runtime code calls the retired Supabase Edge Function data plane', 'call the standalone versioned API instead');
}
}
}
}
if (errors.length) {
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DATABASE_URL="${DATABASE_URL:-postgresql://sauci:sauci_local_only@127.0.0.1:54320/sauci}"
case "$DATABASE_URL" in
postgresql://*@127.0.0.1:*/*|postgresql://*@localhost:*/*|postgresql://*@\[::1\]:*/*) ;;
*)
echo "Refusing API integration tests against non-local DATABASE_URL" >&2
exit 1
;;
esac
cd "$ROOT"
env DATABASE_URL="$DATABASE_URL" npm run test:integration -w @sauci/api