feat(betrieb): Docker Compose und LXC-Installer

Ein Laufzeit-Paket (scripts/paket-bauen.sh) für beide Wege: Next standalone, worker als Bundle mit nur den nötigen Paketen, Migrationen ohne Prisma-CLI. Compose lokal komplett hochgefahren; der Installer auf pve-demo in einem echten Debian-13-LXC durchgelaufen.

Closes #019

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 17:35:41 +02:00
co-authored by Claude Opus 5.5
parent 7f07efe05d
commit 533a821087
17 changed files with 767 additions and 7 deletions
+64
View File
@@ -0,0 +1,64 @@
import { execFileSync } from "node:child_process";
import pg from "pg";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { migrieren } from "@/betrieb/migrieren";
import { datenbankVerfuegbar } from "./helpers/db";
const NAME = "proxdeploy_migtest";
describe.runIf(datenbankVerfuegbar)("Migrationen im Betrieb (ohne Prisma-CLI)", () => {
const basis = new URL(process.env["TEST_DATABASE_URL"]!);
const ziel = new URL(basis);
ziel.pathname = `/${NAME}`;
const verwaltung = async (sql: string) => {
const c = new pg.Client({ connectionString: basis.href });
await c.connect();
try {
await c.query(sql);
} finally {
await c.end();
}
};
beforeAll(async () => {
await verwaltung(`DROP DATABASE IF EXISTS ${NAME} WITH (FORCE)`);
await verwaltung(`CREATE DATABASE ${NAME}`);
});
afterAll(async () => {
await verwaltung(`DROP DATABASE IF EXISTS ${NAME} WITH (FORCE)`);
});
it("spielt alle ein, beim zweiten Mal nichts – und Prisma hält den Stand für aktuell", async () => {
const erstes = await migrieren(ziel.href, "prisma/migrations");
expect(erstes.eingespielt.length).toBeGreaterThan(5);
expect(erstes.eingespielt[0]).toMatch(/_grundgeruest$/);
expect(await migrieren(ziel.href, "prisma/migrations")).toEqual({
eingespielt: [],
vorhanden: erstes.eingespielt.length,
});
const status = execFileSync("pnpm", ["exec", "prisma", "migrate", "status"], {
env: { ...process.env, DATABASE_URL: ziel.href },
encoding: "utf8",
});
expect(status).toMatch(/Database schema is up to date/);
// Auch die Regeln aus den Migrationen (Trigger) sind da: das Audit-Log ist nur anhängbar.
const c = new pg.Client({ connectionString: ziel.href });
await c.connect();
try {
await c.query(`INSERT INTO "Protokoll" (quelle, aktion) VALUES ('SYSTEM', 'Test')`);
await expect(c.query(`DELETE FROM "Protokoll"`)).rejects.toThrow();
} finally {
await c.end();
}
});
it("verweigert, wenn eine eingespielte Migration verändert wurde", async () => {
const c = new pg.Client({ connectionString: ziel.href });
await c.connect();
await c.query(`UPDATE "_prisma_migrations" SET checksum = 'x' WHERE migration_name LIKE '%_server'`);
await c.end();
await expect(migrieren(ziel.href, "prisma/migrations")).rejects.toThrow(/verändert/);
});
});