Files
incudal/scripts/watch-migrations.mjs
T
2026-06-20 14:26:29 +08:00

123 lines
3.3 KiB
JavaScript

import { createHash } from 'node:crypto'
import path from 'node:path'
import { spawn } from 'node:child_process'
import { readdir, stat } from 'node:fs/promises'
const watchDir = process.env.MIGRATIONS_DIR || '/workspace/server/prisma/migrations'
const intervalMs = Number(process.env.MIGRATION_WATCH_INTERVAL_MS || 3000)
let initialized = false
let lastFingerprint = ''
let isRunning = false
let rerunRequested = false
async function collectFiles(dir, files = []) {
const entries = await readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
await collectFiles(fullPath, files)
continue
}
if (!entry.isFile()) continue
const info = await stat(fullPath)
files.push({
path: fullPath,
size: info.size,
mtimeMs: Math.trunc(info.mtimeMs)
})
}
return files
}
async function buildFingerprint() {
try {
const files = await collectFiles(watchDir)
files.sort((a, b) => a.path.localeCompare(b.path))
const hash = createHash('sha1')
for (const file of files) {
hash.update(file.path)
hash.update(':')
hash.update(String(file.size))
hash.update(':')
hash.update(String(file.mtimeMs))
hash.update('\n')
}
return hash.digest('hex')
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return `missing:${message}`
}
}
function runCommand(command, args) {
return new Promise((resolve) => {
const child = spawn(command, args, { stdio: 'inherit' })
child.on('error', () => resolve(1))
child.on('close', (code) => resolve(typeof code === 'number' ? code : 1))
})
}
async function applyMigrations(reason) {
if (isRunning) {
rerunRequested = true
return
}
isRunning = true
console.log(`[MigrationWatcher] Change detected (${reason}). Running migrate deploy...`)
const deployCode = await runCommand('pnpm', ['--filter', 'server', 'exec', 'prisma', 'migrate', 'deploy'])
let generateCode = 0
if (deployCode === 0) {
generateCode = await runCommand('pnpm', ['--filter', 'server', 'exec', 'prisma', 'generate'])
}
if (deployCode === 0 && generateCode === 0) {
console.log('[MigrationWatcher] Migration sync finished.')
} else {
console.error(
`[MigrationWatcher] Migration sync failed (deploy=${deployCode}, generate=${generateCode}).`
)
}
isRunning = false
if (rerunRequested) {
rerunRequested = false
await applyMigrations('queued rerun')
}
}
async function tick() {
const current = await buildFingerprint()
if (!initialized) {
initialized = true
lastFingerprint = current
console.log(`[MigrationWatcher] Watching ${watchDir} (interval: ${intervalMs}ms).`)
return
}
if (current === lastFingerprint) return
const previous = lastFingerprint
lastFingerprint = current
await applyMigrations(`${previous} -> ${current}`)
}
setInterval(() => {
tick().catch((error) => {
const message = error instanceof Error ? error.message : String(error)
console.error(`[MigrationWatcher] Tick failed: ${message}`)
})
}, intervalMs)
tick().catch((error) => {
const message = error instanceof Error ? error.message : String(error)
console.error(`[MigrationWatcher] Initial tick failed: ${message}`)
})