ci: Pruefung bei jedem Push und Konsistenzpruefung der Doku (#019)

Gitea-Actions-Workflow mit Lint, Typen, Tests gegen PostgreSQL, Build,
ShellCheck und einem Skript, das Version, Issue-Uebersicht und Handbuch-Bilder
abgleicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 23:47:38 +02:00
co-authored by Claude Opus 5
parent af38a4efce
commit 0d16dfd379
8 changed files with 210 additions and 18 deletions
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* Prüft, ob Dokumentation und Version zusammenpassen.
*
* npm run pruefe:doku
*
* Geprüft wird:
* 1. Die Version aus package.json hat einen Abschnitt im Changelog.
* 2. Jede Issue-Datei steht in der Übersicht – und umgekehrt.
* 3. Status und tatsächliche Zeit stimmen zwischen Issue-Datei und Übersicht überein.
* 4. Alle im Handbuch verlinkten Bilder sind vorhanden.
*/
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
const WURZEL = process.cwd();
const fehler = [];
function pruefe(bedingung, meldung) {
if (!bedingung) fehler.push(meldung);
}
async function versionImChangelog() {
const { version } = JSON.parse(await readFile(path.join(WURZEL, "package.json"), "utf8"));
const changelog = await readFile(path.join(WURZEL, "CHANGELOG.md"), "utf8");
pruefe(
changelog.includes(`## [${version}]`),
`Version ${version} aus package.json fehlt im CHANGELOG.md.`,
);
}
async function issuesUndUebersicht() {
const verzeichnis = path.join(WURZEL, "docs", "issues");
const dateien = (await readdir(verzeichnis)).filter((name) => /^\d{3}-.*\.md$/.test(name)).sort();
const uebersicht = await readFile(path.join(verzeichnis, "README.md"), "utf8");
for (const datei of dateien) {
pruefe(uebersicht.includes(`(${datei})`), `Issue ${datei} fehlt in docs/issues/README.md.`);
const inhalt = await readFile(path.join(verzeichnis, datei), "utf8");
const status = /^\*\*Status:\*\*\s*(.+)$/m.exec(inhalt)?.[1]?.trim();
const zeit = /^\*\*Geschätzt:\*\*.*\*\*Tatsächlich:\*\*\s*(.+)$/m.exec(inhalt)?.[1]?.trim();
const zeile = uebersicht.split("\n").find((z) => z.includes(`(${datei})`));
if (!zeile) continue;
pruefe(
zeile.includes(`| ${status} |`),
`Status von ${datei} ist „${status}“, die Übersicht sagt etwas anderes: ${zeile.trim()}`,
);
if (zeit && zeit !== "–") {
pruefe(
zeile.includes(zeit),
`Zeit von ${datei} ist „${zeit}“, steht so aber nicht in der Übersicht.`,
);
}
}
// Umgekehrt: Zeilen in der Übersicht ohne zugehörige Datei.
for (const treffer of uebersicht.matchAll(/\((\d{3}-[^)]+\.md)\)/g)) {
pruefe(dateien.includes(treffer[1]), `Übersicht verweist auf ${treffer[1]}, die Datei fehlt.`);
}
}
async function bilderImHandbuch() {
const handbuch = path.join(WURZEL, "docs", "anleitung");
const text = await readFile(path.join(handbuch, "README.md"), "utf8");
const bilder = [...text.matchAll(/!\[[^\]]*\]\((bilder\/[^)]+)\)/g)].map((t) => t[1]);
const vorhanden = new Set(await readdir(path.join(handbuch, "bilder")));
for (const bild of bilder) {
pruefe(vorhanden.has(path.basename(bild)), `Handbuch verweist auf ${bild}, die Datei fehlt.`);
}
}
await versionImChangelog();
await issuesUndUebersicht();
await bilderImHandbuch();
if (fehler.length > 0) {
console.error("Die Dokumentation passt nicht zusammen:\n");
for (const meldung of fehler) console.error(` · ${meldung}`);
process.exit(1);
}
console.log("Dokumentation, Versionen und Bilder passen zusammen.");