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 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