Files
Marc-André Bombeck 1a4289545e ci: rebuild a lost server from its dump in a compose drill
scripts/compose-dr-drill.sh plays docs/ops/disaster-recovery.md against
real containers: it builds the image from the checkout, starts the bundled
compose stack with fixed throwaway keys, seeds the demo account and a
Guardian with a managed profile (through the app's own code), writes a
reading with an encrypted note through the API, and waits for the
scheduled database-backup job to take the dump. It then keeps only the
dump files, destroys every volume, starts a new stack, restores with
`docker compose run --rm db-restore latest`, counts every table against
the manifest apart from the restore, and boots the app: health, the demo
login, the note read back, a document opened, the Guardian still seeing
the profile.

DRILL_MUTATION breaks one step on purpose (wrong-key, skip-table,
non-empty-target), and the drill fails on each. The workflow runs it on
pull requests that touch the compose file, the image, the entrypoint or
the dump and restore code, nightly, and by hand.
2026-10-25 13:05:47 +01:00

60 lines
1.9 KiB
TypeScript

/**
* The accounts the disaster-recovery drill adds beside the demo seed: a
* Guardian with a password, and a managed profile that Guardian administers.
* Written through the app's own code (`createManagedProfile`,
* `hashPassword`), so the rows are the ones a person creating them in the app
* would leave.
*
* DATABASE_URL=… pnpm dlx tsx scripts/dr-drill-seed.ts
*
* Prints one JSON line: `{ "guardianId": …, "profileId": … }`. The drill
* (`scripts/compose-dr-drill.sh`) checks after the restore that the Guardian
* still sees that profile. Idempotent: a second run answers the same ids.
*/
import { hashPassword } from "@/lib/auth/password";
import { prisma } from "@/lib/db";
import { createManagedProfile } from "@/lib/managed-profiles/create";
const GUARDIAN = "drill-guardian";
const PASSWORD = "drill-guardian-password-1";
async function main(): Promise<void> {
let guardian = await prisma.user.findUnique({
where: { username: GUARDIAN },
});
guardian ??= await prisma.user.create({
data: {
username: GUARDIAN,
email: "drill-guardian@example.test",
passwordHash: await hashPassword(PASSWORD),
displayName: "Drill Guardian",
locale: "en",
timezone: "Europe/Berlin",
onboardingCompletedAt: new Date(),
},
});
const existing = await prisma.accountGrant.findFirst({
where: { granteeId: guardian.id, access: "MANAGE" },
select: { grantorId: true },
});
const profileId =
existing?.grantorId ??
(
await createManagedProfile({
creatorId: guardian.id,
displayName: "Drill Child",
dateOfBirth: new Date("2018-05-04T00:00:00Z"),
locale: "en",
timezone: "Europe/Berlin",
})
).profile.id;
console.log(JSON.stringify({ guardianId: guardian.id, profileId }));
}
main()
.catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());