publish clears and loads in one transaction
pg_dump warns that `service_nodes` carries a circular foreign key and that a --data-only dump of it may not restore. Today the rows come out parents-first and it loads either way, but the previous shape made that a load-bearing accident: truncate in one psql call, load in the next, so the day the ordering changes the live site is left EMPTY until somebody notices and restores by hand. Both statements now run as one transaction — TRUNCATE is transactional in Postgres — with foreign-key checks off for the session. A failed load rolls the truncate back with it. Verified against a scratch copy of production rather than reasoned about: a `create database ... template fofos_website`, a deliberately failing load, and all nine rows still there afterwards. The same copy confirmed what the real run will do — gallery and its nine items promoted, hero pointing at the new file, and `users` and `contact_submissions` untouched at 2 and 8 rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+38
-19
@@ -1,4 +1,4 @@
|
||||
import { mkdir, rm, stat } from 'node:fs/promises';
|
||||
import { mkdir, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { db } from 'officerdb/db';
|
||||
import { fofosAdminSettings } from '../db/schema';
|
||||
@@ -148,35 +148,54 @@ export async function publish(secrets: Parameters<typeof buildSite>[0]): Promise
|
||||
const size = (await stat(staged).catch(() => null))?.size ?? 0;
|
||||
lines.push(`read ${CONTENT_TABLES.length} tables from ${development} (${size} bytes)`);
|
||||
|
||||
/*
|
||||
── 3. Clear and load, as ONE transaction ──
|
||||
|
||||
Written as a wrapper around the dump rather than as two psql calls, and that shape is the whole
|
||||
point of this step:
|
||||
|
||||
`-1` puts the truncate and the load in one transaction, and TRUNCATE is transactional in Postgres.
|
||||
So a load that fails takes the truncate down with it and production keeps the content it had.
|
||||
Two separate calls — which is what this was first — leave the live site EMPTY when the second one
|
||||
fails, for however long it takes somebody to notice and restore. Verified against a scratch copy of
|
||||
production: a deliberately failing load left all nine rows in place.
|
||||
|
||||
`session_replication_role = replica` turns off foreign-key checks for the session. `pg_dump` warns
|
||||
that `service_nodes` carries a circular foreign key and that a --data-only dump of it may not
|
||||
restore; today the rows happen to come out parents-first and it loads either way, but "happens to"
|
||||
is not a property to depend on for the live site. It needs superuser, and if it is refused the
|
||||
transaction rolls back like any other failure — production intact, the reason in the output.
|
||||
|
||||
`RESTART IDENTITY`, so the sequence values the dump then sets are not merely ahead of the rows it
|
||||
inserts. A copy whose sequences still start at 1 does not fail here; it fails on the client's next
|
||||
save, hours later, somewhere else entirely.
|
||||
*/
|
||||
const wrapper = join(dir, `.publish-${stamp}.psql`);
|
||||
await writeFile(
|
||||
wrapper,
|
||||
[
|
||||
'SET session_replication_role = replica;',
|
||||
`TRUNCATE ${CONTENT_TABLES.join(', ')} RESTART IDENTITY CASCADE;`,
|
||||
`\\i ${staged}`,
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
try {
|
||||
/*
|
||||
── 3. Clear, in one statement ──
|
||||
|
||||
All of them together, because foreign keys between them make any single truncate fail on its own.
|
||||
`RESTART IDENTITY` so the sequence values the dump then sets are not merely ahead of the rows it
|
||||
inserts — a copy whose sequences still start at 1 collides on the client's next save, hours later,
|
||||
somewhere else entirely.
|
||||
*/
|
||||
const cleared = await run(
|
||||
['psql', '-v', 'ON_ERROR_STOP=1', '-c', `TRUNCATE ${CONTENT_TABLES.join(', ')} RESTART IDENTITY CASCADE;`, production],
|
||||
env,
|
||||
);
|
||||
if (!cleared.ok) return { ok: false, output: `${lines.join('\n')}\nCould not clear production: ${cleared.output}` };
|
||||
|
||||
// ── 4. Load ──
|
||||
const loaded = await run(['psql', '-v', 'ON_ERROR_STOP=1', '-f', staged, production], env);
|
||||
const loaded = await run(['psql', '-1', '-q', '-v', 'ON_ERROR_STOP=1', '-f', wrapper, production], env);
|
||||
if (!loaded.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
output: `${lines.join('\n')}\nThe copy failed and production content is now EMPTY.\nRestore it with:\n psql ${production} -f ${safety}\n\n${loaded.output}`,
|
||||
output: `${lines.join('\n')}\nThe copy failed and was rolled back — production still holds the content it had.\n\n${loaded.output}`,
|
||||
};
|
||||
}
|
||||
lines.push('copied development content into production');
|
||||
} finally {
|
||||
await rm(staged, { force: true });
|
||||
await rm(wrapper, { force: true });
|
||||
}
|
||||
|
||||
// ── 5. Render it ──
|
||||
// ── 4. Render it ──
|
||||
const built = await buildSite(secrets);
|
||||
lines.push(built.output);
|
||||
if (!built.ok) return { ok: false, output: lines.join('\n') };
|
||||
|
||||
Reference in New Issue
Block a user