From 4b7f8a5c5372c4c7ba9f1ce5c938bbdfdd05b811 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 23:20:22 +0000 Subject: [PATCH 1/2] Add a Supabase migrations workflow Pull requests that touch supabase/ apply every migration to a fresh database with supabase db start, so broken SQL fails before merge. Merges to main apply the migrations they added to production with supabase db push, behind the production environment's approval. The job only applies files the push added: if production is missing anything else, or has a version the repo doesn't, it stops instead of replaying older migrations. Manual runs only report what's pending. The production job stays off until the SUPABASE_PROJECT_ID repository variable is set. Co-authored-by: Eric Zakariasson --- .github/workflows/supabase-migrations.yml | 109 ++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/supabase-migrations.yml diff --git a/.github/workflows/supabase-migrations.yml b/.github/workflows/supabase-migrations.yml new file mode 100644 index 00000000..23320920 --- /dev/null +++ b/.github/workflows/supabase-migrations.yml @@ -0,0 +1,109 @@ +name: Supabase migrations + +on: + pull_request: + paths: + - "supabase/**" + - ".github/workflows/supabase-migrations.yml" + push: + branches: [main] + paths: + - "supabase/migrations/**" + # Report-only: lists what production is missing without applying anything. + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + name: Migrations apply to a fresh database + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: supabase/setup-cli@v1 + with: + version: 2.121.0 + + - name: Apply all migrations + run: supabase db start + + production: + name: Apply to production + # Stays off until a maintainer sets the SUPABASE_PROJECT_ID repository + # variable; see "Database migrations" in the README. + if: github.event_name != 'pull_request' && vars.SUPABASE_PROJECT_ID != '' + runs-on: ubuntu-latest + environment: production + concurrency: + group: supabase-production + cancel-in-progress: false + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }} + SUPABASE_PROJECT_ID: ${{ vars.SUPABASE_PROJECT_ID }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: supabase/setup-cli@v1 + with: + version: 2.121.0 + + - run: supabase link --project-ref "$SUPABASE_PROJECT_ID" + + - name: Migration history (repo vs production) + run: supabase migration list + + - name: Check pending migrations + id: pending + env: + BEFORE: ${{ github.event.before }} + run: | + set -euo pipefail + + # The CLI already refuses when production's history and the repo + # disagree (a version missing from the repo, or a new file older than + # the last applied one) and prints how to repair it. + if ! result=$(supabase db push --dry-run --output-format json); then + echo "$result" + exit 1 + fi + pending=$(jq -r '.migrations[]' <<<"$result") + + if [ -z "$pending" ]; then + echo "Production is up to date." + echo "apply=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Pending migrations:" + echo "$pending" + + if [ "$GITHUB_EVENT_NAME" != "push" ]; then + echo "Manual runs only report; nothing was applied." + echo "apply=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Only apply files this push added, so a history that's out of sync + # with the repo can never replay older migrations against production. + added=$(git diff --name-only --diff-filter=A "$BEFORE" "$GITHUB_SHA" -- supabase/migrations | xargs -r -n1 basename) + unexpected=$(comm -23 <(sort <<<"$pending") <(sort <<<"$added")) + if [ -n "$unexpected" ]; then + echo "::error::Production is missing migrations this push didn't add: $(tr '\n' ' ' <<<"$unexpected"). Reconcile the migration history (README, Database migrations) before applying." + exit 1 + fi + + edited=$(git diff --name-only --diff-filter=M "$BEFORE" "$GITHUB_SHA" -- supabase/migrations) + if [ -n "$edited" ]; then + echo "::warning::Edits to already-applied migrations don't run again: $(tr '\n' ' ' <<<"$edited")" + fi + + echo "apply=true" >> "$GITHUB_OUTPUT" + + - name: Apply + if: steps.pending.outputs.apply == 'true' + run: supabase db push From 89239e667f839a57e97ad705c4fb98d348e8088c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 23:20:22 +0000 Subject: [PATCH 2/2] Document the migrations workflow and its one-time setup Cover the access token, environment secrets and reviewers, lining up production's hand-applied migration history with the repo, and why migrations have to work with both the old and new code. Co-authored-by: Eric Zakariasson --- AGENTS.md | 1 + README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b2d1a7b2..923d9afb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Lint from repo root: `bunx biome ci .` (matches CI in `.github/workflows/ci.yml` - `bun` is installed via npm into `~/.local/bin` (on `PATH` via `~/.bashrc`); the standalone bun.sh installer is blocked by the network egress policy. - The repo's dated migrations assume a pre-existing hosted base schema. `supabase/migrations/00000000000000_local_base_schema.sql` reconstructs it (tables, signup trigger, slug triggers, `pgmq_public` wrappers, storage policies) so a fresh `supabase start` / `supabase db reset` works locally. It is guarded to be a no-op where objects already exist. +- Merging a migration to `main` applies it to production through `.github/workflows/supabase-migrations.yml` (after an environment approval), in parallel with the Vercel deploy, so migrations must work with both the old and new code. Never run `supabase db push` against production by hand; see README → Database migrations. - App env lives in `apps/cursor/.env` (gitignored). For local dev it uses the Supabase CLI's well-known local default keys (`supabase status` prints them) with `NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321`. `CRON_SECRET` guards `/api/queue/plugin-scans/drain` and `/api/cron/*`. - The login page only offers GitHub/Google OAuth, which are not configured locally. Create a confirmed user via the GoTrue admin API (`POST {SUPABASE_URL}/auth/v1/admin/users` with the secret key, `"email_confirm": true`); a DB trigger provisions the `public.users` profile row. To get a browser session, add a temporary route that calls `supabase.auth.signInWithPassword(...)` with the server client (do not commit it). Put the user's id in `ADMIN_USER_IDS`/`NEXT_PUBLIC_ADMIN_USER_IDS` to use `/admin/plugins`. - Cache Components (`cacheComponents: true`) makes pages with `generateStaticParams` throw 500 on an empty database — seed at least one active plugin. Use `bun run seed:extract` / `seed:insert`, but note: the insert script must be run as `bun run --conditions=react-server --env-file=apps/cursor/.env apps/cursor/src/scripts/insert-from-jsonl.ts` or the `server-only` import throws under plain `bun run`. GitHub Code Search (used by `seed:extract` discovery) is heavily rate-limited; hand-writing `apps/cursor/.seed/candidates.json` with `{"candidates":[{"owner":...,"repo":...,"source":"seed:topic","matchedQuery":"manual"}]}` skips discovery. diff --git a/README.md b/README.md index 25bd9c7f..327d01f5 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,50 @@ See the [Open Plugins specification](https://open-plugins.com/plugin-builders/sp - **URL State**: [nuqs](https://nuqs.47ng.com) - **Linting**: [Biome](https://biomejs.dev) +## Database migrations + +Create migrations with `supabase migration new `. The **Supabase +migrations** workflow (`.github/workflows/supabase-migrations.yml`) then: + +- On pull requests that touch `supabase/`, applies every migration to a fresh + database, so broken SQL fails before merge. +- On merges to `main`, applies the migrations that merge added to production, + once someone approves the `production` environment. It refuses if production + is missing any other migration or has one the repo doesn't, so a history + that's out of sync can't replay old migrations. +- When run by hand (Actions → Supabase migrations → Run workflow), lists what + production is missing without applying anything. + +Vercel deploys the same merge in parallel, so a migration has to work with both +the old and the new code: add tables and columns first, and remove them in a +later PR. If new code can't run without its migration, merge the migration in +its own PR first. + +### One-time setup + +The production job stays off until a repo admin does this: + +1. In Supabase, create a + [personal access token](https://supabase.com/docs/guides/platform/personal-access-tokens) + scoped to the Cursor Directory project, with read access to Project + Settings, API Keys, and API Key Secrets. Note the database password + (Project Settings → Database). The app only uses API keys, so resetting the + password doesn't affect it. +2. In GitHub → Settings → Environments, create `production`. Add required + reviewers, limit deployment branches to `main`, and add the secrets + `SUPABASE_ACCESS_TOKEN` and `SUPABASE_DB_PASSWORD`. +3. Line up production's migration history with the repo. Earlier migrations + were applied by hand, so the history may be empty or use other version + numbers. With the CLI, run `supabase link --project-ref ` and + `supabase migration list`. Mark every migration that's already in + production as applied with + `supabase migration repair --status applied ...`, and remove + versions the repo doesn't have with `--status reverted`. Repeat until + nothing is missing on either side. +4. In GitHub → Settings → Variables, add the repository variable + `SUPABASE_PROJECT_ID` (the project ref). This turns the production job on. + Run the workflow by hand once to confirm it reports nothing pending. + ## Plugin security scan Submitted plugins are auto-reviewed by a Cursor SDK agent (`composer-2`) running