mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(ops): log pending migrations before migrate deploy and document a dump before every update
This commit is contained in:
1 parent
648bb0d985
commit
eab4d644f7
7 files changed
+259
-2
No files matched your search
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Boot-time notice before `prisma migrate deploy`: how many migrations this
|
||||
* image is about to apply, and a reminder to have a backup first.
|
||||
*
|
||||
* Run by `docker-entrypoint.sh` as `node scripts/pending-migrations.mjs`.
|
||||
* It only reports. A migration that changes data cannot be undone by pulling
|
||||
* the previous image, so an operator who reads the count in the log still has
|
||||
* a moment to stop the container and take a `pg_dump`; the entrypoint does
|
||||
* not wait for that.
|
||||
*
|
||||
* Best effort by design: any failure to count (no `_prisma_migrations` table
|
||||
* yet is not a failure, it means every migration is pending) prints one line
|
||||
* saying so and exits 0, because a notice is never a reason to keep the app
|
||||
* from starting. `migrate deploy` right after it is what decides.
|
||||
*/
|
||||
import { readdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
/** Migration directory names that carry a `migration.sql`, sorted. */
|
||||
export function shippedMigrations(migrationsDir) {
|
||||
return readdirSync(migrationsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => existsSync(join(migrationsDir, name, "migration.sql")))
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* The shipped migrations the database has not finished applying. Reads
|
||||
* `_prisma_migrations` in the schema the connection string names (Prisma's
|
||||
* `?schema=` parameter, `public` when absent); a database without that table
|
||||
* has applied nothing.
|
||||
*/
|
||||
export async function pendingMigrations({ connectionString, migrationsDir }) {
|
||||
const shipped = shippedMigrations(migrationsDir);
|
||||
const { default: pg } = await import("pg");
|
||||
const url = new URL(connectionString);
|
||||
const schema = url.searchParams.get("schema") || "public";
|
||||
url.searchParams.delete("schema");
|
||||
const client = new pg.Client({
|
||||
connectionString: url.toString(),
|
||||
connectionTimeoutMillis: 5_000,
|
||||
statement_timeout: 5_000,
|
||||
});
|
||||
await client.connect();
|
||||
try {
|
||||
const table = await client.query(
|
||||
"SELECT to_regclass(format('%I._prisma_migrations', $1::text)) AS t",
|
||||
[schema],
|
||||
);
|
||||
if (!table.rows[0]?.t) return shipped;
|
||||
const applied = await client.query(
|
||||
`SELECT migration_name FROM ${table.rows[0].t}
|
||||
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL`,
|
||||
);
|
||||
const done = new Set(applied.rows.map((row) => row.migration_name));
|
||||
return shipped.filter((name) => !done.has(name));
|
||||
} finally {
|
||||
await client.end().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/** The log line for a count, as the entrypoint prints it. */
|
||||
export function pendingNotice(count) {
|
||||
if (count === 0) return "HealthLog: Database schema is up to date.";
|
||||
const noun = count === 1 ? "migration" : "migrations";
|
||||
return (
|
||||
`HealthLog: ${count} database ${noun} pending. ` +
|
||||
"Back up the database before updating (pg_dump, see the Updates section " +
|
||||
"of docs/self-hosting/getting-started.md); a migration is not undone by " +
|
||||
"going back to the previous image."
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) return;
|
||||
try {
|
||||
const pending = await pendingMigrations({
|
||||
connectionString,
|
||||
migrationsDir: join(process.cwd(), "prisma", "migrations"),
|
||||
});
|
||||
console.log(pendingNotice(pending.length));
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
console.log(
|
||||
`HealthLog: Could not count pending migrations (${reason}); continuing.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
await main();
|
||||
}
|
||||
Reference in new issue
Block a user