Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,40 @@ jobs:
cache: npm
- run: npm ci --include=dev
- run: npm run typecheck

migrations:
name: migrations
runs-on: ubuntu-latest
needs: build
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: astroid
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d astroid"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/astroid?schema=public
SHADOW_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/astroid_shadow?schema=public
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci --include=dev
# The shadow database is an empty scratch PostgreSQL database that Prisma
# uses to rebuild the schema purely from the committed migrations.
- name: Create shadow database
run: |
sudo apt-get update && sudo apt-get install -y postgresql-client
PGPASSWORD=postgres createdb -h localhost -p 5432 -U postgres astroid_shadow
- name: Verify database migrations
run: npm run db:verify
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:seed": "ts-node prisma/seed.ts",
"db:seed": "ts-node prisma/seed.ts"
"db:seed": "ts-node prisma/seed.ts",
"db:verify": "scripts/verify-migrations.sh"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
Expand Down
93 changes: 93 additions & 0 deletions prisma/migrations/20260830174000_sync_schema/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
-- Sync schema to schema.prisma
CREATE TYPE "WebhookDeliveryStatus" AS ENUM ('PENDING', 'RETRYING', 'FAILED', 'DELIVERED');

-- AlterTable
ALTER TABLE "api_keys" ADD COLUMN "allowedIps" TEXT[] DEFAULT ARRAY[]::TEXT[];

-- AlterTable
ALTER TABLE "passkey_credentials" ADD COLUMN "userAgent" TEXT;

-- AlterTable
ALTER TABLE "policies" ADD COLUMN "originalLimit" DECIMAL(30,7),
ADD COLUMN "overrideLimit" DECIMAL(30,7),
ADD COLUMN "overrideUntil" TIMESTAMP(3);

-- CreateTable
CREATE TABLE "webhook_deliveries" (
"id" TEXT NOT NULL,
"webhookId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"eventName" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"payload" JSONB NOT NULL DEFAULT '{}',
"status" "WebhookDeliveryStatus" NOT NULL DEFAULT 'PENDING',
"attempts" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"responseStatus" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "webhook_deliveries_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "passkey_challenges" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"challenge" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "passkey_challenges_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "outbox_events" (
"id" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"retryCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"processedAt" TIMESTAMP(3),
"error" TEXT,

CONSTRAINT "outbox_events_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "webhook_deliveries_webhookId_idx" ON "webhook_deliveries"("webhookId");

-- CreateIndex
CREATE INDEX "webhook_deliveries_organizationId_idx" ON "webhook_deliveries"("organizationId");

-- CreateIndex
CREATE INDEX "webhook_deliveries_status_idx" ON "webhook_deliveries"("status");

-- CreateIndex
CREATE INDEX "webhook_deliveries_eventId_idx" ON "webhook_deliveries"("eventId");

-- CreateIndex
CREATE INDEX "webhook_deliveries_createdAt_idx" ON "webhook_deliveries"("createdAt");

-- CreateIndex
CREATE INDEX "passkey_challenges_userId_idx" ON "passkey_challenges"("userId");

-- CreateIndex
CREATE INDEX "passkey_challenges_expiresAt_idx" ON "passkey_challenges"("expiresAt");

-- CreateIndex
CREATE INDEX "outbox_events_status_createdAt_idx" ON "outbox_events"("status", "createdAt");

-- CreateIndex
CREATE INDEX "policies_overrideUntil_idx" ON "policies"("overrideUntil");

-- AddForeignKey
ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "passkey_challenges" ADD CONSTRAINT "passkey_challenges_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add integrity hash columns to audit_logs
ALTER TABLE "audit_logs" ADD COLUMN "hash" TEXT,
ADD COLUMN "previousHash" TEXT;
59 changes: 59 additions & 0 deletions scripts/verify-migrations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
#
# Verifies the Prisma database migrations for the Astroid API.
#
# What it does, in order:
# 1. Generates the Prisma client.
# 2. Applies every pending migration to the target database (idempotent).
# 3. Drift check: rebuilds the schema purely from the committed migrations
# (in an ephemeral shadow database) and fails if it does not match
# prisma/schema.prisma. This catches schema edits that were never
# captured in a migration.
#
# Env:
# DATABASE_URL (required) Target PostgreSQL the migrations are
# applied to — e.g. a fresh ephemeral CI database.
# SHADOW_DATABASE_URL (optional but recommended) An empty scratch database
# used for the drift check. When unset the drift check
# is skipped and only apply + status are verified.
#
# Exit code 0 when migrations apply cleanly and stay in sync with the schema.
set -euo pipefail

: "${DATABASE_URL:?DATABASE_URL is required}"

echo "==> Generating Prisma client"
npx prisma generate

echo "==> Applying migrations to ${DATABASE_URL}"
npx prisma migrate deploy

echo "==> Checking migration status"
npx prisma migrate status

if [[ -n "${SHADOW_DATABASE_URL:-}" ]]; then
echo "==> Drift check: rebuilding schema from migrations only"
echo " shadow database: ${SHADOW_DATABASE_URL}"
# `--script` prints the SQL that would reconcile the migrations-built schema
# with schema.prisma: nothing when in sync, the full delta when out of sync.
# Strip blank lines and SQL comment markers so an "empty migration" counts as
# in sync.
drift="$(npx prisma migrate diff \
--from-migrations prisma/migrations \
--to-schema-datamodel prisma/schema.prisma \
--script \
--shadow-database-url "${SHADOW_DATABASE_URL}" \
| grep -Ev '^[[:space:]]*$|^--' || true)"

if [[ -n "${drift//[[:space:]]/}" ]]; then
echo "!! Schema drift detected — schema.prisma differs from the applied migrations." >&2
echo "$drift" >&2
exit 1
fi

echo "==> Migrations are in sync with the schema"
else
echo "!! SHADOW_DATABASE_URL unset — skipping drift check" >&2
fi

echo "==> Migration verification passed"
Loading