mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 15:54:03 +00:00
fix(db): add locale repair migration hygiene guard
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
15
packages/db/drizzle-checksums.json
Normal file
15
packages/db/drizzle-checksums.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/db/drizzle/0009_quiet_wallflower.sql
Normal file
2
packages/db/drizzle/0009_quiet_wallflower.sql
Normal 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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
56
scripts/check-migration-hygiene.ts
Normal file
56
scripts/check-migration-hygiene.ts
Normal 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()
|
||||||
27
scripts/update-migration-checksums.ts
Normal file
27
scripts/update-migration-checksums.ts
Normal 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`)
|
||||||
Reference in New Issue
Block a user