-
Notifications
You must be signed in to change notification settings - Fork 1.6k
ci(den-db): automated Drizzle migrations for PlanetScale #2152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
benjaminshafii
wants to merge
2
commits into
dev
Choose a base branch
from
ci/den-db-auto-migrations
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+349
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| name: Den DB Check | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - "ee/packages/den-db/**" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| migrations-in-sync: | ||
| name: Schema and migrations in sync | ||
| runs-on: blacksmith-4vcpu-ubuntu-2204 | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 24 | ||
|
|
||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 11.4.0 | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --filter "@openwork-ee/den-db..." | ||
|
|
||
| - name: Generate migrations from schema | ||
| run: pnpm --filter @openwork-ee/den-db db:generate | ||
|
|
||
| - name: Fail if schema changed without a committed migration | ||
| run: | | ||
| set -euo pipefail | ||
| changes="$(git status --porcelain -- ee/packages/den-db/drizzle)" | ||
| if [ -n "$changes" ]; then | ||
| echo "$changes" | ||
| echo "::error::Schema changed without a committed migration. Run 'pnpm --filter @openwork-ee/den-db db:generate' locally and commit the generated files in ee/packages/den-db/drizzle." | ||
| exit 1 | ||
| fi | ||
| echo "Migrations are in sync with the schema." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| name: Den DB Migrate | ||
|
|
||
| # Applies Drizzle migrations to the production PlanetScale database. | ||
| # | ||
| # Required GitHub Actions secrets (Settings -> Secrets and variables -> Actions): | ||
| # DATABASE_HOST PlanetScale host (e.g. aws.connect.psdb.cloud) | ||
| # DATABASE_USERNAME PlanetScale branch password username | ||
| # DATABASE_PASSWORD PlanetScale branch password | ||
| # | ||
| # Runs automatically when migration files land on dev, and manually via | ||
| # workflow_dispatch. For a database previously managed with db:push (no | ||
| # __drizzle_migrations table yet), run once manually with baseline=true | ||
| # to record existing migrations as applied without executing them. | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - dev | ||
| paths: | ||
| - "ee/packages/den-db/drizzle/**" | ||
| workflow_dispatch: | ||
| inputs: | ||
| baseline: | ||
| description: "One-time: mark existing migrations as applied without executing them (for a db previously managed via db:push)" | ||
| type: boolean | ||
| default: false | ||
| baseline_through: | ||
| description: "Baseline through this migration tag (default: latest, e.g. 0020_breezy_siren)" | ||
| type: string | ||
| required: false | ||
| dry_run: | ||
| description: "Print what would happen without applying migrations" | ||
| type: boolean | ||
| default: false | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: den-db-migrate | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| migrate: | ||
| name: Apply migrations | ||
| if: github.repository == 'different-ai/openwork' | ||
| runs-on: blacksmith-4vcpu-ubuntu-2204 | ||
|
|
||
| env: | ||
| DATABASE_HOST: ${{ secrets.DATABASE_HOST }} | ||
| DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }} | ||
| DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} | ||
|
|
||
| steps: | ||
| - name: Verify database secrets are configured | ||
| run: | | ||
| set -euo pipefail | ||
| missing="" | ||
| [ -z "$DATABASE_HOST" ] && missing="$missing DATABASE_HOST" | ||
| [ -z "$DATABASE_USERNAME" ] && missing="$missing DATABASE_USERNAME" | ||
| [ -z "$DATABASE_PASSWORD" ] && missing="$missing DATABASE_PASSWORD" | ||
| if [ -n "$missing" ]; then | ||
| echo "::error::Missing GitHub Actions secrets:$missing. Add them under Settings -> Secrets and variables -> Actions." | ||
| exit 1 | ||
| fi | ||
| echo "Database secrets present." | ||
|
|
||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 24 | ||
|
|
||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 11.4.0 | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --filter "@openwork-ee/den-db..." | ||
|
|
||
| - name: Baseline existing migrations (one-time) | ||
| if: github.event_name == 'workflow_dispatch' && inputs.baseline | ||
| run: | | ||
| set -euo pipefail | ||
| extra="" | ||
| if [ -n "${{ inputs.baseline_through }}" ]; then | ||
| extra="--through ${{ inputs.baseline_through }}" | ||
| fi | ||
| if [ "${{ inputs.dry_run }}" = "true" ]; then | ||
| pnpm --filter @openwork-ee/den-db db:baseline -- $extra | ||
| else | ||
| pnpm --filter @openwork-ee/den-db db:baseline -- --yes $extra | ||
| fi | ||
|
|
||
| - name: Apply migrations | ||
| if: github.event_name != 'workflow_dispatch' || !inputs.dry_run | ||
| run: pnpm --filter @openwork-ee/den-db db:migrate | ||
|
|
||
| - name: Dry run note | ||
| if: github.event_name == 'workflow_dispatch' && inputs.dry_run | ||
| run: echo "Dry run requested -- skipped 'db:migrate'. Baseline output above (if enabled) shows the plan." | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| /** | ||
| * One-time baseline for databases that were previously managed with | ||
| * `db:push` (state-based) and have no `__drizzle_migrations` table. | ||
| * | ||
| * Marks existing migrations as applied WITHOUT executing them, so a | ||
| * subsequent `db:migrate` only runs migrations newer than the baseline. | ||
| * | ||
| * Usage: | ||
| * pnpm --filter @openwork-ee/den-db db:baseline # dry run | ||
| * pnpm --filter @openwork-ee/den-db db:baseline -- --yes # apply | ||
| * pnpm --filter @openwork-ee/den-db db:baseline -- --yes --through 0020_breezy_siren | ||
| * | ||
| * Connects with DATABASE_URL (mysql2) or DATABASE_HOST/DATABASE_USERNAME/ | ||
| * DATABASE_PASSWORD (PlanetScale HTTP driver) -- same as the rest of den-db. | ||
| */ | ||
| import "../src/load-env.ts" | ||
| import crypto from "node:crypto" | ||
| import { readFileSync } from "node:fs" | ||
| import path from "node:path" | ||
| import { fileURLToPath } from "node:url" | ||
|
|
||
| const MIGRATIONS_TABLE = "__drizzle_migrations" | ||
|
|
||
| const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") | ||
| const drizzleDir = path.join(packageDir, "drizzle") | ||
|
|
||
| type JournalEntry = { | ||
| idx: number | ||
| when: number | ||
| tag: string | ||
| } | ||
|
|
||
| type Executor = { | ||
| query: (sql: string, args?: (string | number)[]) => Promise<Record<string, unknown>[]> | ||
| close: () => Promise<void> | ||
| } | ||
|
|
||
| function parseArgs() { | ||
| const args = process.argv.slice(2) | ||
| const apply = args.includes("--yes") | ||
| const throughIndex = args.indexOf("--through") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||
| const through = throughIndex >= 0 ? args[throughIndex + 1] : undefined | ||
| return { apply, through } | ||
| } | ||
|
|
||
| async function createExecutor(): Promise<Executor> { | ||
| const databaseUrl = process.env.DATABASE_URL?.trim() | ||
|
|
||
| if (databaseUrl) { | ||
| const mysql = await import("mysql2/promise") | ||
| const connection = await mysql.createConnection(databaseUrl) | ||
| return { | ||
| query: async (sql, args = []) => { | ||
| const [rows] = await connection.query(sql, args) | ||
| return Array.isArray(rows) ? (rows as Record<string, unknown>[]) : [] | ||
| }, | ||
| close: () => connection.end(), | ||
| } | ||
| } | ||
|
|
||
| const host = process.env.DATABASE_HOST?.trim() | ||
| const username = process.env.DATABASE_USERNAME?.trim() | ||
| const password = process.env.DATABASE_PASSWORD ?? "" | ||
|
|
||
| if (!host || !username) { | ||
| throw new Error("Provide DATABASE_URL, or DATABASE_HOST/DATABASE_USERNAME/DATABASE_PASSWORD (see .env.example)") | ||
| } | ||
|
|
||
| const { Client } = await import("@planetscale/database") | ||
| const client = new Client({ host, username, password }) | ||
| return { | ||
| query: async (sql, args = []) => { | ||
| const result = await client.execute(sql, args) | ||
| return result.rows as Record<string, unknown>[] | ||
| }, | ||
| close: async () => {}, | ||
| } | ||
| } | ||
|
|
||
| async function main() { | ||
| const { apply, through } = parseArgs() | ||
|
|
||
| const journal = JSON.parse(readFileSync(path.join(drizzleDir, "meta", "_journal.json"), "utf8")) as { | ||
| entries: JournalEntry[] | ||
| } | ||
| const entries = [...journal.entries].sort((a, b) => a.when - b.when) | ||
|
|
||
| if (entries.length === 0) { | ||
| console.log("No migrations in journal; nothing to baseline.") | ||
| return | ||
| } | ||
|
|
||
| const throughEntry = through ? entries.find((e) => e.tag === through) : entries[entries.length - 1] | ||
| if (!throughEntry) { | ||
| throw new Error(`--through tag "${through}" not found in drizzle/meta/_journal.json`) | ||
| } | ||
|
|
||
| const executor = await createExecutor() | ||
| try { | ||
| await executor.query( | ||
| `create table if not exists \`${MIGRATIONS_TABLE}\` (id serial primary key, hash text not null, created_at bigint)`, | ||
| ) | ||
|
|
||
| const rows = await executor.query(`select max(created_at) as latest from \`${MIGRATIONS_TABLE}\``) | ||
| const latestRaw = rows[0]?.latest | ||
| const latest = latestRaw == null ? 0 : Number(latestRaw) | ||
|
|
||
| const pending = entries.filter((e) => e.when > latest && e.when <= throughEntry.when) | ||
|
|
||
| console.log(`Baseline target: ${throughEntry.tag} (when=${throughEntry.when})`) | ||
| console.log(`Already recorded through: created_at=${latest || "none"}`) | ||
| console.log(`Entries to mark as applied (without executing): ${pending.length}`) | ||
| for (const entry of pending) { | ||
| console.log(` - ${entry.tag}`) | ||
| } | ||
|
|
||
| if (pending.length === 0) { | ||
| console.log("Nothing to do.") | ||
| return | ||
| } | ||
|
|
||
| if (!apply) { | ||
| console.log("\nDry run. Re-run with --yes to record the baseline.") | ||
| return | ||
| } | ||
|
|
||
| for (const entry of pending) { | ||
| const sqlContents = readFileSync(path.join(drizzleDir, `${entry.tag}.sql`), "utf8") | ||
| const hash = crypto.createHash("sha256").update(sqlContents).digest("hex") | ||
| await executor.query(`insert into \`${MIGRATIONS_TABLE}\` (hash, created_at) values (?, ?)`, [hash, entry.when]) | ||
| console.log(`Recorded ${entry.tag}`) | ||
| } | ||
|
|
||
| console.log(`\nBaseline complete. 'db:migrate' will now only apply migrations newer than ${throughEntry.tag}.`) | ||
| } finally { | ||
| await executor.close() | ||
| } | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error instanceof Error ? error.message : error) | ||
| process.exit(1) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
workflow_dispatchinput is directly interpolated in arunscript, creating a shell-injection path forbaseline_through.Prompt for AI agents