fix(db): add locale repair migration hygiene guard

This commit is contained in:
2026-03-09 15:54:50 +04:00
parent 2d8e0491cc
commit 63a2677601
7 changed files with 113 additions and 0 deletions

View File

@@ -30,6 +30,7 @@ jobs:
- test - test
- build - build
- db-check - db-check
- db-migrations
steps: steps:
- name: Checkout - name: Checkout
@@ -74,6 +75,9 @@ jobs:
db-check) db-check)
bun run db:check bun run db:check
;; ;;
db-migrations)
bun run db:migrations:check
;;
*) *)
echo "Unknown task: ${{ matrix.task }}" echo "Unknown task: ${{ matrix.task }}"
exit 1 exit 1

View File

@@ -19,6 +19,8 @@
"db:generate": "bunx drizzle-kit generate --config packages/db/drizzle.config.ts", "db:generate": "bunx drizzle-kit generate --config packages/db/drizzle.config.ts",
"db:check": "bunx drizzle-kit check --config packages/db/drizzle.config.ts", "db:check": "bunx drizzle-kit check --config packages/db/drizzle.config.ts",
"db:migrate": "bunx drizzle-kit migrate --config packages/db/drizzle.config.ts", "db:migrate": "bunx drizzle-kit migrate --config packages/db/drizzle.config.ts",
"db:migrations:check": "bun run scripts/check-migration-hygiene.ts",
"db:migrations:manifest": "bun run scripts/update-migration-checksums.ts",
"db:push": "bunx drizzle-kit push --config packages/db/drizzle.config.ts", "db:push": "bunx drizzle-kit push --config packages/db/drizzle.config.ts",
"db:studio": "bunx drizzle-kit studio --config packages/db/drizzle.config.ts", "db:studio": "bunx drizzle-kit studio --config packages/db/drizzle.config.ts",
"db:seed": "set -a; [ -f .env ] && . ./.env; set +a; bun run --filter @household/db seed", "db:seed": "set -a; [ -f .env ] && . ./.env; set +a; bun run --filter @household/db seed",

View File

@@ -0,0 +1,15 @@
{
"algorithm": "sha256",
"files": {
"0000_modern_centennial.sql": "3529b04edcdbe2537466fe532f50779e3175d60c384eb2f77818a37231b65fac",
"0001_spicy_sersi.sql": "148421f99147fbac0c740d67c1a33f085540dd75375916e33821cc92eba9e35b",
"0002_tough_sandman.sql": "842e3e153760224e2d6710dd43f72a64cc0f4845f6467ad0747de29c273441d1",
"0003_mature_roulette.sql": "ea3eeba17984fedf862d05027013addfe8dc5997fad6236c41484b4fed2c0389",
"0004_big_ultimatum.sql": "d01fda1cc11720417bc5ef4d09eef0fc9ed82c0ca2ea4f34f7c06959751007e4",
"0005_free_kang.sql": "e749a58fadc9332ed7980ba498897e7094e583d95bead473ad88f162221b0e34",
"0006_marvelous_nehzno.sql": "369a862f68dede5568116e29865aa3c8a7a0ff494487af1b202b62932ffe83f4",
"0007_sudden_murmur.sql": "eedf265c46705c20be4ddb3e2bd7d9670108756915b911e580e19a00f8118104",
"0008_lowly_spiral.sql": "4f016332d60f7ef1fef0311210a0fa1a0bfc1d9b6da1da4380a60c14d54a9681",
"0009_quiet_wallflower.sql": "c5bcb6a01b6f22a9e64866ac0d11468105aad8b2afb248296370f15b462e3087"
}
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE "households" ADD COLUMN IF NOT EXISTS "default_locale" text DEFAULT 'ru' NOT NULL;--> statement-breakpoint
ALTER TABLE "members" ADD COLUMN IF NOT EXISTS "preferred_locale" text;--> statement-breakpoint

View File

@@ -64,6 +64,13 @@
"when": 1773047624171, "when": 1773047624171,
"tag": "0008_lowly_spiral", "tag": "0008_lowly_spiral",
"breakpoints": true "breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1773055200000,
"tag": "0009_quiet_wallflower",
"breakpoints": true
} }
] ]
} }

View File

@@ -0,0 +1,56 @@
import { createHash } from 'node:crypto'
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
interface ChecksumManifest {
algorithm: string
files: Record<string, string>
}
const rootDir = process.cwd()
const migrationDir = path.join(rootDir, 'packages', 'db', 'drizzle')
const manifestPath = path.join(rootDir, 'packages', 'db', 'drizzle-checksums.json')
function sha256(input: string): string {
return createHash('sha256').update(input).digest('hex')
}
async function main() {
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as ChecksumManifest
if (manifest.algorithm !== 'sha256') {
throw new Error(`Unsupported migration checksum algorithm: ${manifest.algorithm}`)
}
const files = (await readdir(migrationDir)).filter((entry) => entry.endsWith('.sql')).sort()
const missingFromDisk = Object.keys(manifest.files).filter((file) => !files.includes(file))
if (missingFromDisk.length > 0) {
throw new Error(`Missing committed migration files: ${missingFromDisk.join(', ')}`)
}
const unexpectedFiles = files.filter((file) => !(file in manifest.files))
if (unexpectedFiles.length > 0) {
throw new Error(
`New migrations must update packages/db/drizzle-checksums.json: ${unexpectedFiles.join(', ')}`
)
}
const changedFiles: string[] = []
for (const file of files) {
const sql = await readFile(path.join(migrationDir, file), 'utf8')
const actual = sha256(sql)
if (actual !== manifest.files[file]) {
changedFiles.push(file)
}
}
if (changedFiles.length > 0) {
throw new Error(`Historical migration files changed: ${changedFiles.join(', ')}`)
}
console.log(`Verified ${files.length} migration checksums`)
}
await main()

View File

@@ -0,0 +1,27 @@
import { createHash } from 'node:crypto'
import { readdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const rootDir = process.cwd()
const migrationDir = path.join(rootDir, 'packages', 'db', 'drizzle')
const manifestPath = path.join(rootDir, 'packages', 'db', 'drizzle-checksums.json')
function sha256(input: string): string {
return createHash('sha256').update(input).digest('hex')
}
const files = (await readdir(migrationDir)).filter((entry) => entry.endsWith('.sql')).sort()
const manifest = {
algorithm: 'sha256',
files: {} as Record<string, string>
}
for (const file of files) {
const sql = await readFile(path.join(migrationDir, file), 'utf8')
manifest.files[file] = sha256(sql)
}
await writeFile(`${manifestPath}`, `${JSON.stringify(manifest, null, 2)}\n`)
console.log(`Wrote checksums for ${files.length} migrations`)