本文へ移動
cccskills
無料GitHub で公開

migration-release-check

Review the EF Core migrations a release (or a PR) carries against how Voxt is deployed: a rolling update whose new pods wait for the migration Job, while old pods keep serving. Use before cutting a release branch, before merging a PR that adds a migration, or when the user says "check the migrations", "are the migrations safe to ship", "expand/contract check", "/migration-release-check". Reports risks; never blocks, edits or runs anything on prod.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md14.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

/migration-release-check — what the migrations of a release can break on rollout

A report, not a gate. For every migration in the range it answers four questions and says what would make the answer "safe". Nothing is changed: no code, no migrations, no database. Prod numbers come from SQL the skill writes for the user — nobody on the team has prod SQL access from here.

Background and the incident behind it: tmp/s/2026-10-15-expand-contract-migrations.md (if it is not in your slot, the facts below are enough), issues #5313, #5327, #5331, #5350.

Why each check exists

Deploy facts the checks rely on — verify them on every run (step 5), they drift:

  • Migrations run in a Job actual-chat-migration-<version> (Dockerfile target migrations-app, Flux HelmRelease actual-chat-migration, backoffLimit: 1). The app pods start with it, but their init container actual-chat-migration-wait (k8s-wait-for job actual-chat-migration-, NAME_SUFFIX=<version>) holds them until the Job completes — so a Job that reports success lets them in whatever happened inside (dev 2026-10-14: the Chat migration failed, the entrypoint went on, the last bundle's exit code made the Job Complete). With SKIP_WAIT_NOT_FOUND=true the wait is skipped if the Job does not exist yet, and Flux does not order the HelmRelease before the Deployment.
  • App pods also migrate on start (DbInitializer.MigrateDb), serialized by EF's migration lock: whatever the Job left pending, or all of it when the wait was skipped, runs on the app path.
  • A failed Job keeps the new pods in Init and the rollout stops. With maxUnavailable: 1 one old pod is already gone by then, so the app runs at reduced capacity until someone steps in.
  • Each migration is its own transaction (EF Core 10+; EF 9 ran all pending migrations in one, EF 10 reverted that — "Breaking changes in EF Core 9", dotnet/efcore#34439; checked on EF 11 rc.1 on 2026-10-15: one Began/Committed pair per migration). Within a migration, DropColumn, AlterColumn, CreateIndex, DropIndex hold ACCESS EXCLUSIVE on the table until it commits: every read and write of that table waits, and queued waiters can exhaust the DB's pool. Locks of consecutive migrations do not add up — queries get through between them. A failed migration rolls back alone: the ones before it stay applied, so the database is left between two releases.
  • Command timeouts: 30 s on the app path (DbInitializer.CommandTimeout), 300 s in the migration pod (Command Timeout=300 in entrypoint.sh).
  • The rollout mixes versions. Old code meets the new schema from the moment the migration commits until the last old pod is gone (dev 2026-10-15: ~2 min, 324 column ... does not exist). New code meets the old schema if the migration failed and the app started anyway (dev 2026-10-14: 3 h of null value in column "duration"). Since #5331 a failed migration fails the Job (set -e) and stops an app pod (exit code 2) — step 5 checks that this still holds.
  • EF selects every mapped column in every query, so a column the old model maps breaks all reads of that entity once it is dropped — not just the code that used the property.

The safe shape is expand/contract (parallel change): release N only adds or relaxes (nullable / default) and stops using the old thing; release N+1 drops it. With migrations generated from the model (CLAUDE.md → EF Core migrations), that means the property stays in the model, nullable, for one release before it is removed. Not a team rule — the report says where a migration departs from it and what that costs.

Arguments

  • none — the range is merge-base(latest origin/release/v*, origin/dev)..origin/dev, i.e. what the next release will carry.
  • #5339 / a PR number — the migrations that PR adds (gh pr diff <n> --name-only).
  • <from>..<to> — an explicit git range.

Steps

1. Range and the previous release

git fetch -q origin
REL=$(git branch -r --list 'origin/release/v*' --sort=-version:refname | head -1 | tr -d ' ')
BASE=$(git merge-base "$REL" origin/dev)

$REL is the code that will still be running on the old pods during the rollout. For a PR, the old code is origin/dev at the PR's base, but say in the report that prod runs $REL, which may be older still.

2. The migrations

git diff --name-only --diff-filter=A "$BASE" origin/dev -- 'src/dotnet/*/Migrations/2*.cs' \
  | grep -v '\.Designer\.cs$'

A migration that was modified rather than added (--diff-filter=M) is a finding of its own: if it already ran anywhere — dev applies dev on every merge, prod applies release branches — the edit never runs there. Check with git branch -r --contains <the commit that added it> | grep release.

For each file, list the operations of Up():

git show origin/dev:<file> | awk '/override void Up/,/override void Down/' \
  | grep -oE 'migrationBuilder\.[A-Za-z]+' | sort | uniq -c

Then read Up() itself for every migration that has anything beyond CreateTable, nullable AddColumn or CreateIndex on a new table.

Look across the migrations too, not only inside each: an index or column created by one migration and dropped by a later one in the same range is built under a lock on prod for nothing (v2.22: Add_ChatCleanup builds ix_chat_entries_chat_id_kind_is_removed_removed_at on chat_entries, RemoveUnusedChatEntryColumns drops it). Dev already ran both, so the fix is usually just to count the extra lock time — but say it.

Candidates — a name that appears in Up() of more than one migration of the same service:

for f in <the migrations>; do
  git show origin/dev:$f | awk '/override void Up/,/override void Down/' \
    | grep -oE '^\s*name: "[^"]+"' | grep -oE '"[^"]+"' | sort -u \
    | sed "s|^|$(echo $f | cut -d/ -f3) |; s|\$| $(basename $f .cs)|"
done | awk '{k=$1" "$2; n[k]++; m[k]=m[k]" "$3} END{for(k in n) if(n[k]>1) print k":"m[k]}'

Expect noise: Users.Service.Migration holds two contexts (users, oauth), so every framework column shows up twice there, and the same column name can live in two tables (pace_data). Open each candidate and keep only a real create-then-drop of the same object.

3. Classify every operation

OperationOld code on new schemaNew code on old schemaLock
CreateTable, nullable AddColumnsafesafe (new code must tolerate the table/column missing only if the migration can fail — see #5331)short
AddColumn NOT NULL with defaultsafenew code writing it fails until migratedrewrite on old PG, metadata-only on PG ≥ 11
AddColumn NOT NULL without defaultold inserts fail——
DropColumn / DropTableevery query of the entity fails unless the old model no longer maps itsafe if the new code has no property; inserts fail if the column is still NOT NULL in the old schemaACCESS EXCLUSIVE, fast itself
RenameColumn / RenameTablefailsfails—
AlterColumn → nullablesafesafeshort
AlterColumn → NOT NULL, type changeold writes of NULL / old type fail—may rewrite the table
CreateIndex on an existing tablesafesafeholds the lock while the index builds; unique: true fails on duplicates
DropIndexsafesafeshort; check the queries that used it
DropPrimaryKey / AddPrimaryKeysafe if columns unchanged—builds an index under the lock; fails on duplicates
Sql(...)read itread itDELETE / UPDATE on a big table under the same transaction

How to settle the two old-code questions instead of guessing:

  • Does the old model map the column? Look at the model snapshot on $REL: git show "$REL":src/dotnet/<Svc>.Service.Migration/Migrations/<Ctx>ModelSnapshot.cs | grep -n '"<column>"'. Mapped → old pods break on every query of that entity after the drop.
  • Is it NOT NULL in the old schema? In the same snapshot, the property's .IsRequired() or a non-nullable CLR type. NOT NULL + the new code no longer writing it = the 2026-10-14 incident shape.

Expand/contract verdict per dropped column: follows when the snapshot on $REL already has it nullable and git grep -n '<Property>' "$REL" -- 'src/dotnet/' ':!*/Migrations/*' ':!*/Db/Db*.cs' finds no use; otherwise departs, and the report names the window it opens.

Framework-owned tables (_operations, _events, __EFMigrationsHistory) count too: old and new framework versions both write them on every command.

When the migration comes with a framework upgrade (Fusion, EF — the same migration name in every service, e.g. Fusion15_DeferredInvalidation), the schema is the smaller question. Ask first whether pods on the old and the new framework can run side by side at all (RPC protocol, operation log format, invalidation). If they cannot, no expand/contract makes the rollout safe — only a rollout that never mixes versions does. The report states this as an open question for whoever made the upgrade; it does not guess the answer.

4. Data and size

For every table touched by a lock-holding operation or a Sql(...):

  • dev — run it yourself via mcp__voxt-robokitty-dev__run_sql_query (database: chat, users, media, contacts, invite, notification, flows, mlsearch, oauth — the ac_dev_ prefix is implied). If the dev schema already has the migration applied, say so: the numbers are post-migration.
  • prod — write the queries for the user to run on ac_prod_<db>; never claim prod numbers you did not get back from them.
-- size
SELECT count(*), pg_size_pretty(pg_total_relation_size('<table>')) FROM <table>;
-- a new unique index: duplicates that would fail it (after any DELETE the migration does)
SELECT <cols>, count(*) FROM <table> [WHERE <rows the migration keeps>]
GROUP BY <cols> HAVING count(*) > 1 LIMIT 10;
-- a Sql DELETE/UPDATE: how many rows, and whether anything still references them
SELECT count(*) FROM <table> WHERE <its predicate>;

Lock estimate: dev time for the same migration, if dev applied it, scaled by the size ratio — and say it is an estimate. For a measurement on prod without a copy and without a lock: CREATE UNIQUE INDEX CONCURRENTLY tmp_measure ... ; then DROP INDEX CONCURRENTLY tmp_measure; (autocommit, outside a transaction). Over 30 s → the app path can time out and roll back.

Dev migration timing, when dev applied it:

timeout 120 gcloud logging read 'resource.labels.container_name="actual-chat-migration" AND textPayload:"<MigrationName>"' \
  --project=actual-chat-app-dev --freshness=30d --format='value(timestamp,resource.labels.pod_name,textPayload)' --order=asc | head

Keep the container filter: without it the query scans every pod's log for 30 days and does not finish in two minutes. Several names can go into one query with OR.

A migration that never shows up here was applied by an app pod, not by the migration pod — that is the case for any database whose bundle the image does not build (step 5). On prod it then runs under the app's 30 s command timeout; say so next to that migration.

That gives the start. For the end, read the same pod's log from that timestamp (resource.labels.pod_name="<pod>" AND timestamp>="<start>", add resource.labels.pod_name to --format) — its next Done. line. Several Applying lines for one migration mean earlier attempts failed and rolled back.

5. Deploy guards

Report each as OK / not OK; these decide whether a slow or failed migration stays a stopped deploy or becomes an outage.

# an initializer failure must reach Program (#5331): WorkerBase.Run() swallows exceptions
grep -nE ':\s*WorkerBase|WorkerBase\[\]' src/dotnet/App.Server/AppHost.cs src/dotnet/App.Server/Initializers/*.cs  # expect nothing
grep -n 'set -e' Dockerfile                                                   # entrypoint stops on the first failure
grep -n 'CommandTimeout' src/dotnet/Db/DbInitializer.cs
# every entrypoint call has a bundle and vice versa; any output line is a mismatch
comm -3 \
  <(grep -oE 'bundle (--context [A-Za-z]+ )?--output \./artifacts/[A-Za-z.]+' Dockerfile | sed 's|.*artifacts/|./|' | sort) \
  <(grep -oE '^\./[A-Za-z.]+\.Migration\.exe' Dockerfile | sort)

The wait for the Job lives in the cluster, not in this repo. Read it from the dev cluster when kubectl is set up (read-only; prod is not reachable from here, so say the prod setup is assumed to match):

kubectl get deploy actual-chat-app -o jsonpath='{.spec.strategy}{"\n"}{range .spec.template.spec.initContainers[*]}{.name} {.args} {.env}{"\n"}{end}' \
  | grep -E 'rollingUpdate|migration-wait'

Expect actual-chat-migration-wait with job actual-chat-migration-; report SKIP_WAIT_NOT_FOUND and maxUnavailable as they are.

6. Report

One table, then the prod SQL, then the guards:

Range    release/v2.21 (merge-base 4f3c…) .. origin/dev 380cf9fd4c — 24 migrations, 3 need attention

| Migration | Operation | Old code / new schema | New code / old schema | Lock | Verdict |
|---|---|---|---|---|---|
| Chat 20261014091148_RemoveUnusedChatEntryColumns | DropColumn kind, duration, audio_entry_id… | every chat_entries query fails (mapped on v2.21) | inserts fail: duration NOT NULL | 9 DropIndex + 8 CreateIndex + DELETE, one tx | departs |

Prod SQL (run on ac_prod_chat): …
Guards   #5331 OPEN · entrypoint without set -e · Flows.Service.Migration.exe called, not bundled · app timeout 30 s

Then, per departing migration, the cheapest way to make it safe — usually "keep the property nullable for one release, drop it in the next", or "do the DELETE ahead of the release in batches". Say what it costs (a release cycle, splitting an already merged PR, re-aligning dev). The team decides.

Common mistakes

MistakeFix
Judging a drop by git grep for the property aloneEF selects every mapped column; check the $REL snapshot
Quoting dev numbers as prodProd comes only from SQL the user ran
Treating a migration dev already applied as freely editableDev will never rerun it; say what dev needs
Skipping framework tables_operations / _events are written by every command on every pod
Calling the report a verdict to block the releaseIt lists risks and costs; the team decides

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Delete the `-bak` backup branches that `/prepare-merge` leaves behind, local and on origin, once the work they back up has merged. Use when the user says "remove my bak branches", "clean up bak branches", "delete the backups", or "/clean-bak-branches".

日本語の概要は準備中です。原文の説明を表示しています。

Actual-Chat/actual-chat562026年10月16日 更新

create-pr

無料

Use when work on a branch is finished and the next step is a pull request — "create a PR", "open a PR", "push and PR this", "/create-pr" — when a draft PR should be marked ready for review — "mark the PR ready", "undraft it", "it's ready for review" — or right after either has happened and the team has not been told about it yet.

日本語の概要は準備中です。原文の説明を表示しています。

Actual-Chat/actual-chat562026年10月16日 更新

Search Google Cloud (GKE) server logs of the DEV environment (dev.voxt.ai). Use when troubleshooting anything that needs the server side of the story — stream stalls, RPC peer changes / reconnects, watchdog closures, mesh/node churn, pod restarts — or when the user says "check GCP logs", "check server logs on dev", or gives a dev.voxt.ai timeline from a client console log.

日本語の概要は準備中です。原文の説明を表示しています。

Actual-Chat/actual-chat562026年10月16日 更新

Search Google Cloud (GKE) server logs of the PROD environment. Use when troubleshooting a production incident that needs the server side of the story — stream stalls, RPC peer changes / reconnects, watchdog closures, mesh/node churn, pod restarts — or when the user says "check PROD logs", "check production server logs", or gives a production timeline from a client console log.

日本語の概要は準備中です。原文の説明を表示しています。

Actual-Chat/actual-chat562026年10月16日 更新

memories

無料

What this project has already learned the hard way, and where any new such fact belongs. Two jobs. (1) Routing: this project keeps NO per-project Claude memories — read this before writing anything down, and pick a durable home from the list here. (2) An index, by area, of non-obvious facts worth knowing before you guess: debugging rigs and measurements that lie, iOS/WebKit and the tethered iPhone, the Windows app, production and deployment, and general codebase traps. Use before setting up a browser- or server-driven debugging session, when a bug reproduces only on iPhone, in Safari or in the Windows app, before changing deployment configuration or reading store/crash data, when something behaves unexpectedly and you are about to guess why, and whenever you are tempted to record a fact for later.

日本語の概要は準備中です。原文の説明を表示しています。

Actual-Chat/actual-chat562026年10月16日 更新

CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when it creates a better product. Three modes: SCOPE EXPANSION (dream big), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials).

日本語の概要は準備中です。原文の説明を表示しています。

Actual-Chat/actual-chat562026年10月16日 更新

Actual-Chat のスキルをすべて見る

このスキルの問題を報告する