syncova-backup/scripts/release_test.go
Jerrit Fritzsche c4f0b3e665
Some checks failed
CI / Backend (Go) (push) Failing after 1m46s
CI / Frontend (React/TypeScript) (push) Successful in 33s
CI / Sicherheitsprüfungen (push) Successful in 23s
Anleitung zum Veroeffentlichen und Testen des Systems
Zwei Teile: Release bauen und in Gitea veroeffentlichen, und das System in vier
Stufen durchtesten — Rauchtest, Rundlauf, Agent, Proxmox.

Jede Stufe endet mit dem, was danach belegt ist, und der Rundlauf hat eine
Gegenprobe: nicht leeres Ziel, rm -rf gegen ein gehaertetes Repository und ein
gekippter Block muessen scheitern beziehungsweise erkannt werden. Ein Test, der
nur den Erfolgsfall zeigt, belegt wenig.

Der Vorschlag fuer den ersten Tag ist v1.0.0-rc1, nicht v1.0.0: Solange
Windows-Dienst, systemd-Einheit und der Proxmox-Boot ungeprueft sind, waere
1.0.0 eine Zusage, die diese drei Punkte nicht deckt.

Jeder genannte Endpunkt wurde gegen den eingefrorenen API-Vertrag gehalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:47:30 +02:00

141 lines
5.3 KiB
Go

// Package scripts prüft die Vollständigkeit der Auslieferung (Phase 23).
//
// Der Test läuft im gewöhnlichen Testlauf mit und braucht kein gebautes Paket:
// Er prüft die **Bestandteile**, aus denen es entsteht. Ein Paket zu bauen,
// nur um es zu prüfen, dauerte Minuten und liefe deshalb bald nicht mehr mit.
package scripts
import (
"os"
"path/filepath"
"strings"
"testing"
)
// repositoryRoot ist der Pfad zum Wurzelverzeichnis.
const repositoryRoot = ".."
// TestReleaseDocumentationIsComplete prueft die Liste aus §25.
//
// Der Umsetzungsplan nennt zwoelf Bestandteile, die ein Auslieferungspaket
// enthalten muss. Fuenf davon sind Dokumente, und die schreibt man einmal —
// danach faellt es niemandem auf, wenn eines beim Umbenennen verlorengeht.
// Genau deshalb steht die Liste hier und nicht nur in einer Aufzaehlung.
func TestReleaseDocumentationIsComplete(testInstance *testing.T) {
requiredDocuments := map[string]string{
"docs/installation.md": "Installationsanleitung",
"docs/recovery-runbook.md": "Wiederherstellungsdokumentation",
"docs/security-guide.md": "Sicherheitsleitfaden",
"docs/api.md": "API-Dokumentation",
"docs/troubleshooting.md": "Stoerungsleitfaden",
"docs/release-howto.md": "Anleitung zum Veroeffentlichen und Testen",
"docs/agent-installation.md": "Installationsanleitung der Agenten",
"CHANGELOG.md": "Aenderungsliste",
"README.md": "Ueberblick",
}
for documentPath, documentPurpose := range requiredDocuments {
fileInformation, statError := os.Stat(filepath.Join(repositoryRoot, documentPath))
if statError != nil {
testInstance.Errorf("%s fehlt (%s): %v", documentPath, documentPurpose, statError)
continue
}
// Eine leere Datei erfüllt die Liste formal und hilft niemandem. Die
// Grenze ist bewusst niedrig — sie fängt die versehentlich angelegte
// Hülle, nicht ein knappes Dokument.
if fileInformation.Size() < 500 {
testInstance.Errorf("%s ist mit %d Byte zu klein für %s",
documentPath, fileInformation.Size(), documentPurpose)
}
}
}
// TestReleaseScriptCoversEveryCommand prueft, dass kein Programm fehlt.
//
// Ein Programm, das gebaut, aber nicht ausgeliefert wird, faellt erst beim
// Kunden auf — und dann fehlt genau das Werkzeug, das er gerade braucht.
func TestReleaseScriptCoversEveryCommand(testInstance *testing.T) {
buildScript := readReleaseScript(testInstance)
commandDirectories := []string{"apps/api/cmd", "apps/agent/cmd"}
for _, commandDirectory := range commandDirectories {
directoryEntries, readError := os.ReadDir(filepath.Join(repositoryRoot, commandDirectory))
if readError != nil {
testInstance.Fatalf("%s ließ sich nicht lesen: %v", commandDirectory, readError)
}
for _, singleEntry := range directoryEntries {
if !singleEntry.IsDir() {
continue
}
commandPath := commandDirectory + "/" + singleEntry.Name()
if !strings.Contains(buildScript, commandPath) {
testInstance.Errorf("das Programm %s wird gebaut, aber nicht ausgeliefert.\n"+
" Entweder in scripts/build-release.sh aufnehmen oder begründen, "+
"warum es nicht ins Paket gehört.", singleEntry.Name())
}
}
}
}
// TestReleaseScriptShipsEveryTargetPlatform prueft die Zielplattformen.
//
// Dieselben drei, die `make cross-build` bei jedem Lauf uebersetzt. Eine
// Plattform, die uebersetzt, aber nicht ausgeliefert wird, ist eine Zusage ohne
// Gegenstueck.
func TestReleaseScriptShipsEveryTargetPlatform(testInstance *testing.T) {
buildScript := readReleaseScript(testInstance)
requiredPlatforms := []string{"linux/amd64", "linux/arm64", "windows/amd64"}
for _, platformName := range requiredPlatforms {
if !strings.Contains(buildScript, platformName) {
testInstance.Errorf("die Zielplattform %s wird nicht ausgeliefert", platformName)
}
}
}
// TestReleaseScriptIncludesMigrationsAndInterface prueft die uebrigen Bestandteile.
func TestReleaseScriptIncludesMigrationsAndInterface(testInstance *testing.T) {
buildScript := readReleaseScript(testInstance)
requiredParts := map[string]string{
"migrations/*.sql": "Migrationsdateien",
"migrations/checksums.txt": "Prüfsummen der Migrationen",
"apps/web/dist": "Weboberfläche",
"docs/": "Dokumentation",
"CHANGELOG.md": "Änderungsliste",
"SHA256SUMS": "Prüfsummen des Pakets",
}
for partPattern, partPurpose := range requiredParts {
if !strings.Contains(buildScript, partPattern) {
testInstance.Errorf("im Auslieferungsskript fehlt %s (%s)", partPattern, partPurpose)
}
}
// Ohne diesen Schalter bindet Go gegen die libc des Bausystems. Der Start
// scheitert dann auf einer älteren Distribution mit einer Meldung über
// GLIBC — die niemand einem Backupprogramm zuordnet.
if !strings.Contains(buildScript, "CGO_ENABLED=0") {
testInstance.Error("die Programme werden nicht statisch gebunden (CGO_ENABLED=0 fehlt)")
}
}
// readReleaseScript liest das Auslieferungsskript.
func readReleaseScript(testInstance *testing.T) string {
testInstance.Helper()
scriptContent, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts/build-release.sh"))
if readError != nil {
testInstance.Fatalf("das Auslieferungsskript ließ sich nicht lesen: %v", readError)
}
return string(scriptContent)
}