From f4d66f8a2dd61238c0e193139b07b35a19068658 Mon Sep 17 00:00:00 2001 From: Tom Blaymire Date: Mon, 16 Mar 2026 10:21:19 +0000 Subject: [PATCH 1/3] fix: sync drizzle migration state and add ci schema check Add missing 0002_snapshot.json and journal entry for the manually-created api_key_hashing migration so drizzle-kit generate works correctly. Add schema-check CI job that detects schema drift in PRs. --- .github/workflows/ci.yml | 53 +- packages/db/drizzle/meta/0002_snapshot.json | 1126 +++++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + 3 files changed, 1184 insertions(+), 2 deletions(-) create mode 100644 packages/db/drizzle/meta/0002_snapshot.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6843ab..af2c473 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,6 +217,54 @@ jobs: fail_ci_if_error: false verbose: true + schema-check: + name: Schema Check + runs-on: ubuntu-latest + needs: setup + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + id: pnpm-cache + + - name: Restore dependencies + uses: actions/cache@v4 + with: + path: | + ${{ steps.pnpm-cache.outputs.STORE_PATH }} + node_modules + apps/*/node_modules + packages/*/node_modules + key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install dependencies (if cache miss) + run: pnpm install --frozen-lockfile + + - name: Check for schema drift + working-directory: packages/db + run: | + timeout 30 npx drizzle-kit generate 2>&1 || true + if [ -n "$(git status --porcelain drizzle/)" ]; then + echo "::error::Schema drift detected! The Drizzle schema has changes that are not reflected in migrations." + echo "Run: pnpm --filter @switchflag/db db:generate" + echo "Then commit the generated migration file." + git diff drizzle/ + exit 1 + fi + echo "Schema and migrations are in sync." + build: name: Build runs-on: ubuntu-latest @@ -277,7 +325,7 @@ jobs: ci-success: name: CI Success runs-on: ubuntu-latest - needs: [lint, typecheck, test, build] + needs: [lint, typecheck, test, build, schema-check] if: always() steps: - name: Check all jobs passed @@ -285,7 +333,8 @@ jobs: if [[ "${{ needs.lint.result }}" != "success" ]] || \ [[ "${{ needs.typecheck.result }}" != "success" ]] || \ [[ "${{ needs.test.result }}" != "success" ]] || \ - [[ "${{ needs.build.result }}" != "success" ]]; then + [[ "${{ needs.build.result }}" != "success" ]] || \ + [[ "${{ needs.schema-check.result }}" != "success" ]]; then echo "One or more jobs failed" exit 1 fi diff --git a/packages/db/drizzle/meta/0002_snapshot.json b/packages/db/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..98ffec7 --- /dev/null +++ b/packages/db/drizzle/meta/0002_snapshot.json @@ -0,0 +1,1126 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "08a455df-5bf4-46fd-b560-e59adbd71eed", + "prevId": "39664440-da66-4009-b0b6-dca6c3e0976f", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "changes": { + "name": "changes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "auditLogs_organizationId_idx": { + "name": "auditLogs_organizationId_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "auditLogs_userId_idx": { + "name": "auditLogs_userId_idx", + "columns": ["user_id"], + "isUnique": false + }, + "auditLogs_entityType_entityId_idx": { + "name": "auditLogs_entityType_entityId_idx", + "columns": ["entity_type", "entity_id"], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_organization_id_organization_id_fk": { + "name": "audit_logs_organization_id_organization_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_logs_user_id_user_id_fk": { + "name": "audit_logs_user_id_user_id_fk", + "tableFrom": "audit_logs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key_hash": { + "name": "api_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environments_api_key_hash_unique": { + "name": "environments_api_key_hash_unique", + "columns": ["api_key_hash"], + "isUnique": true + }, + "environments_projectId_idx": { + "name": "environments_projectId_idx", + "columns": ["project_id"], + "isUnique": false + }, + "environments_apiKeyHash_idx": { + "name": "environments_apiKeyHash_idx", + "columns": ["api_key_hash"], + "isUnique": false + }, + "environments_project_name_uidx": { + "name": "environments_project_name_uidx", + "columns": ["project_id", "name"], + "isUnique": true + } + }, + "foreignKeys": { + "environments_project_id_projects_id_fk": { + "name": "environments_project_id_projects_id_fk", + "tableFrom": "environments", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evaluations": { + "name": "evaluations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "flag_id": { + "name": "flag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "evaluations_environmentId_idx": { + "name": "evaluations_environmentId_idx", + "columns": ["environment_id"], + "isUnique": false + }, + "evaluations_flagId_idx": { + "name": "evaluations_flagId_idx", + "columns": ["flag_id"], + "isUnique": false + }, + "evaluations_env_flag_date_uidx": { + "name": "evaluations_env_flag_date_uidx", + "columns": ["environment_id", "flag_id", "date"], + "isUnique": true + } + }, + "foreignKeys": { + "evaluations_environment_id_environments_id_fk": { + "name": "evaluations_environment_id_environments_id_fk", + "tableFrom": "evaluations", + "tableTo": "environments", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "evaluations_flag_id_flags_id_fk": { + "name": "evaluations_flag_id_flags_id_fk", + "tableFrom": "evaluations", + "tableTo": "flags", + "columnsFrom": ["flag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "flag_environments": { + "name": "flag_environments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "flag_id": { + "name": "flag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rules": { + "name": "rules", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "flagEnvs_flagId_idx": { + "name": "flagEnvs_flagId_idx", + "columns": ["flag_id"], + "isUnique": false + }, + "flagEnvs_environmentId_idx": { + "name": "flagEnvs_environmentId_idx", + "columns": ["environment_id"], + "isUnique": false + }, + "flagEnvs_flag_env_uidx": { + "name": "flagEnvs_flag_env_uidx", + "columns": ["flag_id", "environment_id"], + "isUnique": true + } + }, + "foreignKeys": { + "flag_environments_flag_id_flags_id_fk": { + "name": "flag_environments_flag_id_flags_id_fk", + "tableFrom": "flag_environments", + "tableTo": "flags", + "columnsFrom": ["flag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "flag_environments_environment_id_environments_id_fk": { + "name": "flag_environments_environment_id_environments_id_fk", + "tableFrom": "flag_environments", + "tableTo": "environments", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "flags": { + "name": "flags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_value": { + "name": "default_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "flags_projectId_idx": { + "name": "flags_projectId_idx", + "columns": ["project_id"], + "isUnique": false + }, + "flags_project_key_uidx": { + "name": "flags_project_key_uidx", + "columns": ["project_id", "key"], + "isUnique": true + } + }, + "foreignKeys": { + "flags_project_id_projects_id_fk": { + "name": "flags_project_id_projects_id_fk", + "tableFrom": "flags", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": ["email"], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": ["slug"], + "isUnique": true + }, + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": ["slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_organizationId_idx": { + "name": "projects_organizationId_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "projects_org_slug_uidx": { + "name": "projects_org_slug_uidx", + "columns": ["organization_id", "slug"], + "isUnique": true + } + }, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": ["token"], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index f7ddca8..501980a 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1770148834873, "tag": "0001_clammy_wolverine", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1770200000000, + "tag": "0002_api_key_hashing", + "breakpoints": true } ] } From 469d4b70cc02c7eb277c7d01dece7b3ade04351a Mon Sep 17 00:00:00 2001 From: Tom Blaymire Date: Mon, 16 Mar 2026 10:59:44 +0000 Subject: [PATCH 2/3] fix: add dummy better auth secret to ci build step Suppresses the BetterAuthError warning during Next.js static page generation in CI. No real auth happens during build. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af2c473..4d2ef99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,6 +310,9 @@ jobs: - name: Build run: pnpm turbo build + env: + SKIP_ENV_VALIDATION: true + BETTER_AUTH_SECRET: ci-build-dummy-secret-that-is-long-enough - name: Upload build artifacts uses: actions/upload-artifact@v4 From cba83f5d5fbc30eaae44951d6dc3846f49be7ea0 Mon Sep 17 00:00:00 2001 From: Tom Blaymire Date: Mon, 16 Mar 2026 20:41:44 +0000 Subject: [PATCH 3/3] fix: seed default project to prevent onboarding wizard in e2e The onboarding wizard triggers for owners with zero projects, covering the entire viewport with z-[100] and blocking button clicks. Seed a default project + 3 environments so the wizard never appears. --- apps/dashboard/e2e/helpers/seed.constants.ts | 6 ++++ apps/dashboard/e2e/seed-db.ts | 37 ++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/e2e/helpers/seed.constants.ts b/apps/dashboard/e2e/helpers/seed.constants.ts index 8da2261..c19f87b 100644 --- a/apps/dashboard/e2e/helpers/seed.constants.ts +++ b/apps/dashboard/e2e/helpers/seed.constants.ts @@ -8,6 +8,12 @@ export const TEST_ORG = { slug: 'e2e-test-team', } +export const TEST_PROJECT = { + id: 'e2e-project-000000000001', + name: 'E2E Test Project', + slug: 'e2e-test-project', +} + export type Persona = { id: string email: string diff --git a/apps/dashboard/e2e/seed-db.ts b/apps/dashboard/e2e/seed-db.ts index 9a4cd24..5797e04 100644 --- a/apps/dashboard/e2e/seed-db.ts +++ b/apps/dashboard/e2e/seed-db.ts @@ -1,6 +1,11 @@ +import crypto from 'node:crypto' import { createClient } from '@libsql/client' import { hashPassword } from 'better-auth/crypto' -import { PERSONAS, TEST_ORG } from './helpers/seed.constants' +import { PERSONAS, TEST_ORG, TEST_PROJECT } from './helpers/seed.constants' + +function hashApiKey(apiKey: string): string { + return crypto.createHash('sha256').update(apiKey).digest('hex') +} const personaIds = Object.values(PERSONAS) .map((p) => `'${p.id}'`) @@ -12,6 +17,8 @@ export async function seedDatabase(dbUrl: string, authToken?: string) { // Clean existing seed data (idempotent) await client.executeMultiple(` + DELETE FROM environments WHERE project_id = '${TEST_PROJECT.id}'; + DELETE FROM projects WHERE id = '${TEST_PROJECT.id}'; DELETE FROM member WHERE organization_id = '${TEST_ORG.id}'; DELETE FROM organization WHERE id = '${TEST_ORG.id}'; DELETE FROM account WHERE user_id IN (${personaIds}); @@ -45,8 +52,34 @@ export async function seedDatabase(dbUrl: string, authToken?: string) { }) } + // Create a default project so the onboarding wizard doesn't trigger for the owner + await client.execute({ + sql: 'INSERT INTO projects (id, organization_id, name, slug, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', + args: [TEST_PROJECT.id, TEST_ORG.id, TEST_PROJECT.name, TEST_PROJECT.slug, now, now], + }) + + // Create default environments with hashed API keys + const envs = ['development', 'staging', 'production'] + for (const envName of envs) { + const apiKey = `sf_${envName.slice(0, 3)}_e2e${envName}key000000000` + const apiKeyHashValue = hashApiKey(apiKey) + const apiKeyPrefixValue = `sf_${envName.slice(0, 3)}_****` + + await client.execute({ + sql: 'INSERT INTO environments (id, project_id, name, api_key_hash, api_key_prefix, created_at) VALUES (?, ?, ?, ?, ?, ?)', + args: [ + `e2e-env-${envName}`, + TEST_PROJECT.id, + envName, + apiKeyHashValue, + apiKeyPrefixValue, + now, + ], + }) + } + client.close() - console.log('Seed complete — created org + %d personas', Object.keys(PERSONAS).length) + console.log('Seed complete — created org + project + %d personas', Object.keys(PERSONAS).length) } // CLI entry point: tsx e2e/seed-db.ts