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
21 changes: 8 additions & 13 deletions skills/refresh-drizzle-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ description: "Refresh a branch's Drizzle migration after its base gained newer o

# Refresh a Drizzle migration

A migration is **stale** when the base branch gained migrations after the head branch generated its own, so the head's journal entry, snapshot chain, or folder timestamp no longer follows the base's latest. Every refresh drops the head's migration and regenerates it on top of the base.
A migration is **stale** when the base branch gained migrations after the head branch generated its own, so the head's journal entry, snapshot chain, or folder timestamp no longer follows the base's latest.

`drizzle-kit generate` is the only drizzle-kit command this skill runs. The user's refresh request is the per-run permission for that one generate step. `migrate`, `push`, `pull`, `up`, `studio`, and any script that wraps them stay out of this run, because they touch a database or rewrite the migrations layout.

## 1. Record the starting point

Resolve the base branch the way `resolving-merge-conflicts` step 1 does. Fetch it, and use the remote-tracking ref `refs/remotes/<remote>/<base>` as `<base-ref>` in every command below; a local branch of the same name can lag the remote. When the base comes from the PR, require `<base-ref>` to equal the PR's `baseRefOid`. Require a clean worktree with the head branch checked out, as that skill does.
Resolve the base branch the way `resolving-merge-conflicts` step 1 does. Fetch it, and use the remote-tracking ref `refs/remotes/<remote>/<base>` as `<base-ref>` in every command below. When the base comes from the PR, require `<base-ref>` to equal the PR's `baseRefOid`. Require a clean worktree with the head branch checked out, as that skill does.

Find every Drizzle config: `git ls-files '*drizzle.config.*'`, plus every path a `package.json` script passes to `drizzle-kit` through `--config`. Read each config's `out` folder (default `./drizzle`) and resolve it against the config's own directory, so it works as a repo-root pathspec. Identify the layout per `out` folder:
Find every Drizzle config: `git ls-files '*drizzle.config.*'`, plus every path a `package.json` script passes to `drizzle-kit` through `--config`. Record each config's **generate script**, the `package.json` script that runs `drizzle-kit generate` for it, and its **generate directory**, where drizzle-kit runs when that script runs. That is the script's `package.json` directory unless the script changes directory first (`cd`, `--filter`, `--prefix`, `--cwd`, `-C`). With no script, it is the nearest directory, from the config's own up to the repo root, where `out` resolves to a tracked folder; with none, the config is untouched. Resolve a script's `--config` path against the generate directory too. When scripts give one config different generate directories or different commands, stop and ask which one the project uses. drizzle-kit resolves `out`, `schema`, and `.env` from the directory it runs in, not from the config's. Read each config's `out` folder (default `./drizzle`) and resolve it against the generate directory, so it works as a repo-root pathspec. Identify the layout per `out` folder:

- **Journal layout** (drizzle-kit 0.x): `meta/_journal.json`, `<idx>_<name>.sql`, `meta/<idx>_snapshot.json`.
- **Folder layout** (drizzle-kit v1): one `<timestamp>_<name>/` folder per migration holding `migration.sql` and `snapshot.json`, and no journal.
Expand All @@ -28,7 +28,7 @@ The step is done when every config is classified as untouched or as holding exac

## 2. Merge the base

Skip this step when `git merge-base --is-ancestor <base-ref> HEAD` exits 0. Otherwise merge the base into the head branch through `resolving-merge-conflicts`, whose step 1 owns how a conflicted Drizzle `meta/` file resolves. That skill ends the merge with its own commit and readiness card, and leaves the push to step 6 here.
Skip this step when `git merge-base --is-ancestor <base-ref> HEAD` exits 0. Otherwise merge the base into the head branch through `resolving-merge-conflicts`, whose step 1 owns how a conflicted Drizzle `meta/` file resolves. That skill ends the merge with its own commit, skips its own `done` run when the merge dropped the head's migration because step 6 here runs it after regeneration, and leaves the push to step 6 here.

The step is done when `git merge-base --is-ancestor <base-ref> HEAD` exits 0 and `git status --porcelain` is empty.

Expand All @@ -43,14 +43,9 @@ The step is done when `git diff --name-only <base-ref> -- <out>`, which compares

## 4. Regenerate

Read the `package.json` of the config's package and find the script that runs `drizzle-kit generate`. Run that script through the project's package manager only when it runs nothing but `drizzle-kit generate` and targets this config, through a `--config` that resolves to it or, with no `--config`, because this config is the package's default `drizzle.config.*`. Otherwise run the installed binary directly from the config's own directory, since drizzle-kit resolves `schema`, `out`, and `.env` from where it runs: `<bin> generate --config <absolute config path>`, where `<bin>` is the nearest `node_modules/.bin/drizzle-kit` from the package directory up to the repo root. When no such binary exists, stop and tell the user to install dependencies. Fetching drizzle-kit on the fly, through `npx`, `bunx`, or `npm exec`, pulls unreviewed code.
Take the generate script step 1 recorded for this config, if any. Run that script through the project's package manager only when all three hold. It runs nothing but `drizzle-kit generate`. It targets this config, through a `--config` that resolves to it or, with no `--config`, because this config is the first of `drizzle.config.ts`, `.js`, and `.json` in the generate directory, the only names drizzle-kit reads without one. Its `package.json` has no `pre<script>` or `post<script>` entry for it, since npm runs those hooks with the script. Otherwise run the installed binary directly from the config's generate directory, recorded in step 1: `<bin> generate --config <absolute config path>`, where `<bin>` is the nearest `node_modules/.bin/drizzle-kit` from the generate directory up to the repo root. When no such binary exists, stop and tell the user to install dependencies; `npx`, `bunx`, and `npm exec` fetch unreviewed code.

Run generate with stdin from `/dev/null` and a two-minute limit. drizzle-kit asks "created or renamed?" whenever a diff drops one column or table and adds another, and an agent shell has no TTY to answer it. When generate errors on a prompt or TTY, or hits the limit, stop and hand the user:

- the exact command, to run in their own terminal;
- the answer for each prompt, read from the stale SQL. A `RENAME` statement there means "renamed", and a matching `DROP` with `ADD` means "created".

Resume at step 5 once the user reports the new files exist.
Run generate with stdin from `/dev/null` and a two-minute limit. When it errors on a prompt or TTY, or hits the limit, read `${CLAUDE_SKILL_DIR}/references/rename-prompts.md` and follow it.

The step is done when the `out` folder holds exactly one migration the base lacks, or generate reported no schema changes. With no changes, step 5 compares the stale SQL against an empty set.

Expand All @@ -60,11 +55,11 @@ Both checks must pass before anything is committed. A failure stops the run and

**Order.** In the journal layout, the new entry's `idx` is one past the base's highest, its `when` is greater than every base entry's, and its snapshot's `prevId` equals the `id` of the base's latest snapshot. In the folder layout, the new folder's timestamp sorts after every base folder, and its snapshot's `prevIds` names the base's latest snapshot `id`.

**Same change.** Compare the stale SQL (`git show <stale commit>:<path>`) with the regenerated SQL as statement sets. Normalize both: split on `--> statement-breakpoint`, collapse whitespace, trim each trailing `;`. Order does not matter. A statement on only one side fails the check. A statement only in the stale file is hand-written SQL, a backfill, or a `--custom` migration that generate cannot reproduce. A statement only in the new file means the merged schema drifted from what the PR intended.
**Same change.** Compare the stale SQL (`git show <stale commit>:<path>`) with the regenerated SQL as statement sets. Normalize both: split on `--> statement-breakpoint`, drop `--` comments outside quoted spans, collapse whitespace outside quoted spans while leaving quoted text byte-exact, then trim each statement and strip one trailing `;`. A quoted span is single-quoted, double-quoted, or dollar-quoted (`$$` or `$tag$`, closed only by the same tag, with quote characters inside it opening nothing). Order does not matter. A statement on only one side fails the check. A statement only in the stale file is hand-written SQL, a backfill, or a `--custom` migration that generate cannot reproduce. A statement only in the new file means the merged schema drifted from what the PR intended.
Comment thread
bhagyamudgal marked this conversation as resolved.
Resolved
Comment thread
bhagyamudgal marked this conversation as resolved.
Resolved

## 6. Verify, commit, push

Hand off to `done`, then commit the regenerated files through `git-commit`, separate from the merge commit.
Hand off to `done`. When it changes any file, probe each touched config with step 4's generate command alone: stdin from `/dev/null`, the two-minute limit, and no prompt handoff. A probe passes when it exits 0 without writing a file. One that writes a migration, stops on a prompt, or hits the limit means `done` changed that config's schema. Then delete every migration this run generated for it (journal layout: each SQL file and its `meta/<idx>_snapshot.json`; folder layout: each folder), restore its journal from `<base-ref>`, require `git diff --name-only <base-ref> -- <out>` and `git ls-files --others --exclude-standard -- <out>` to list nothing, and rerun steps 4 and 5 and `done`. When a probe after such a rerun again writes a migration, stops on a prompt, or hits the limit, stop and show the user its SQL or output. Any other probe failure, such as an error loading the schema or config, compared nothing, so stop and show the user its output too. Then commit through `git-commit`, separate from the merge commit: first `done`'s fixes for merge damage outside the migrations folder, then the regenerated files.

Pushing updates a shared ref, and the branch may belong to a teammate. Push to `<head-remote>`, the remote that resolves to the PR's `headRepository`, or the branch's upstream when there is no PR; on a fork PR it differs from the base's remote. When no configured remote resolves to it, stop and ask. Invoke `preflight-mutations` immediately before the push, with the branch, `<head-remote>`, the local and remote SHAs, and the branch owner (the PR author, or the head repository's owner) in its ownership field. Apply its result. Push without force, then read the remote ref back with `git ls-remote <head-remote> refs/heads/<branch>` and confirm it equals local `HEAD`.

Expand Down
8 changes: 8 additions & 0 deletions skills/refresh-drizzle-migration/references/rename-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# When generate stops on a prompt

drizzle-kit asks "created or renamed?" whenever a diff drops one column or table and adds another, and an agent shell has no TTY to answer it. When generate errors on a prompt or TTY, or hits the two-minute limit, stop and hand the user:

- the generate command without the `/dev/null` redirect or the time limit, so they can answer, to run in their own terminal from the config's generate directory;
- the answer for each prompt, read from the stale SQL. A `RENAME` statement there means "renamed", and a matching `DROP` with `ADD` means "created".

Resume at step 5 once the user reports the new files exist.
2 changes: 1 addition & 1 deletion skills/resolving-merge-conflicts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Resolve an in-progress git conflict without a silent drop. Use whe

1. Orient before you touch anything. Establish from `git status` whether you are in a merge, a rebase, a cherry-pick, a revert, a `git am`, or a stash pop. Unmerged paths with none of the other five in progress mean a conflicting stash pop; confirm it with `git stash list` and the request, and ask the user when their origin is unclear. When none is in progress and nothing is unmerged, start a merge only from a clean worktree. When `git status --porcelain` lists anything, stop and ask the user to commit it, stash it, or move it to a separate worktree; never stash it yourself. For a PR, the base repository is the PR URL's `<owner>/<repo>`; read the base ref, its tip, the head branch, and the head repository with `gh pr view <url> --json baseRefName,baseRefOid,headRefName,headRepository`. The base branch is the one the request names, else the PR's `baseRefName`, else the default branch from `gh repo view --json defaultBranchRef`. When the head branch, `headRefName` for a PR, is not checked out, stop and ask. Fetch from the remote that resolves to the base repository, require the fetched base ref to equal `baseRefOid` when the base is the PR's, merge that remote's base ref into the head branch, or rebase onto it when the user asked for a rebase, and orient. Rebase only when the user asked for one, because rebasing a published branch rewrites its history; step 5's `preflight-mutations` gate covers its push. Under a rebase `ours` is the upstream you are replaying onto and `theirs` is your own commit, the inverse of a merge, and getting this backwards is the most common wrong resolution.

Then split the conflicted files. Regenerate generated artifacts and lockfiles from source, never hand-merge them. That means `bun.lock`, `pnpm-lock.yaml`, `_generated/`, and snapshots. Only hand-written files go through steps 2-3. Under a rebase, stop and ask before touching a conflicted Drizzle migration file, because deleting the head's SQL there rewrites the commit that added it. Under a merge, a conflicted Drizzle `meta/_journal.json` or `meta/<idx>_snapshot.json` takes the base side. Delete the head's own migration SQL in the same merge, or take the base side when the base added the same path, and record the head's SQL as dropped in the ledger either way. When `refresh-drizzle-migration` did not start this merge, invoke it after step 5 and before any push, so the branch never ships a schema change without its migration.
Then split the conflicted files. Regenerate generated artifacts and lockfiles from source, never hand-merge them. That means `bun.lock`, `pnpm-lock.yaml`, `_generated/`, and snapshots. Only hand-written files go through steps 2-3. Under a rebase, stop and ask before touching a conflicted Drizzle migration file, because deleting the head's SQL there rewrites the commit that added it. Under a merge, a conflicted Drizzle `meta/_journal.json` or `meta/<idx>_snapshot.json` takes the base side. Delete the head's own migration SQL in the same merge, or take the base side when the base added the same path, and record the head's SQL as dropped in the ledger either way. When `refresh-drizzle-migration` did not start this merge, first run its step 1 config discovery, head-migration listing, and base-owned check with `MERGE_HEAD` as `<base-ref>`, before resolving any Drizzle file, and stop on any path it flags, because taking the base side there discards the head's edit to a migration the base already has. When this merge drops the head's migration, skip step 4, since checks run against a schema with no migration fail until it is regenerated and `refresh-drizzle-migration` step 6 runs `done` after regeneration. Finish at step 5. When `refresh-drizzle-migration` did not start this merge, run its step 1 head-migration listing against the remote-tracking branch (`<remote>/<branch>`) this merge brought in, which must contain `HEAD^2`. When that listing finds a head migration in any config, including one this merge or an earlier one dropped, invoke `refresh-drizzle-migration` before any push with that branch as its base, so the branch never ships a schema change without its migration.

2. Find the primary sources for each conflict. Learn why each side made its change. Read the commit messages, check the PRs, check original issues and tickets.

Expand Down
Loading