# Ersten Release veröffentlichen und das System testen Zwei Teile: [Release veröffentlichen](#teil-1--release-veröffentlichen) und [System testen](#teil-2--das-system-testen). Wer beides zum ersten Mal macht, sollte Teil 2 zuerst lesen — ein Release, den man nicht ausprobiert hat, ist eine Behauptung. --- # Teil 1 — Release veröffentlichen ## 1. Vorbedingungen prüfen ```bash cd ~/Documents/development/syncova-backup # Alles eingecheckt? git status --porcelain # muss leer sein # Alle Prüfungen grün? set -a; . ./.env; set +a export SYNCOVA_TEST_DATABASE_URL="postgres://syncova:${SYNCOVA_DB_PASSWORD}@127.0.0.1:5432/syncova?sslmode=prefer" make check ``` **Ohne `SYNCOVA_TEST_DATABASE_URL` überspringen neun Testdateien ihre Datenbanktests still** — darunter Upgrade und Rollback. Ein übersprungener Test sieht in der Zusammenfassung aus wie ein bestandener. Und die CI muss grün sein: ```text https://git.jfritzsche.de/jf/syncova-backup/actions ``` ## 2. Version festlegen Die Version kommt aus dem Git-Tag. Ohne Tag heißt das Paket `0.1.0-dev` — für einen Release nicht gut genug: Man kann eine Fassung dann nicht auf einen Stand zurückführen. ```bash git tag -a v1.0.0 -m "Syncova Backups V1" git push https://jf:@git.jfritzsche.de/jf/syncova-backup.git v1.0.0 ``` Bei `v1.0.0` bleiben oder `v1.0.0-rc1` nehmen? **Nehmen Sie `-rc1`**, solange Windows-Dienst, systemd-Einheit und der Proxmox-Boot ungeprüft sind. Ein `1.0.0` ist eine Zusage, die diese drei Punkte noch nicht decken. ## 3. Paket bauen ```bash make release ``` Ergebnis in `dist/`: ```text syncova-v1.0.0-rc1-linux-amd64.tar.gz ~26 MB syncova-v1.0.0-rc1-linux-arm64.tar.gz ~24 MB syncova-v1.0.0-rc1-windows-amd64.zip ~4,4 MB SHA256SUMS ``` Die Version steckt danach in den Programmen: ```bash tar -xzf dist/syncova-v1.0.0-rc1-linux-amd64.tar.gz -C /tmp /tmp/syncova-v1.0.0-rc1-linux-amd64/bin/syncova-api --version ``` ## 4. Das Paket ausprobieren, bevor es jemand bekommt ```bash # Läuft es auf einem System, das nichts installiert hat? docker run --rm -v "$PWD/dist/syncova-v1.0.0-rc1-linux-amd64:/paket:ro" \ debian:12-slim /paket/bin/syncova-api --version # Sind die Prüfsummen im Paket stimmig? cd dist/syncova-v1.0.0-rc1-linux-amd64 && shasum -a 256 -c SHA256SUMS | grep -c OK ``` Der erste Aufruf ist kein Formalismus. Ohne statische Bindung scheitert der Start auf einer älteren Distribution mit einer GLIBC-Meldung, die niemand einem Backupprogramm zuordnet. ## 5. Release in Gitea anlegen **Über die Oberfläche:** Repository → *Releases* → *New Release* → Tag `v1.0.0-rc1` wählen, Text aus `CHANGELOG.md` übernehmen, die drei Archive und `SHA256SUMS` anhängen. Solange Hardware-Punkte offen sind: **„This is a pre-release"** ankreuzen. **Über die API:** ```bash export GIT_TOKEN='' export RELEASE_TAG='v1.0.0-rc1' # Release anlegen RELEASE_ID=$(curl -s -X POST \ -H "Authorization: token $GIT_TOKEN" -H 'Content-Type: application/json' \ "https://git.jfritzsche.de/api/v1/repos/jf/syncova-backup/releases" \ -d "{\"tag_name\":\"$RELEASE_TAG\",\"name\":\"Syncova Backups V1 (RC1)\", \"body\":\"Siehe CHANGELOG.md. Nicht auf echter Hardware geprüft: Windows-Dienst, systemd-Einheit, Proxmox-Boot.\", \"prerelease\":true}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") # Archive anhängen for ARCHIVE in dist/*.tar.gz dist/*.zip dist/SHA256SUMS; do curl -s -X POST -H "Authorization: token $GIT_TOKEN" \ "https://git.jfritzsche.de/api/v1/repos/jf/syncova-backup/releases/$RELEASE_ID/assets?name=$(basename "$ARCHIVE")" \ -F "attachment=@$ARCHIVE" -o /dev/null -w " $(basename "$ARCHIVE"): %{http_code}\n" done ``` Das Token braucht den Bereich `write:repository`. Reicht er nicht, meldet Gitea `token does not have at least one of required scope(s)`. ## 6. Danach - **`SHA256SUMS` gehört mit veröffentlicht**, sonst kann niemand prüfen, ob er das Richtige heruntergeladen hat. - **Sichtbarkeit prüfen:** Das Repository ist derzeit **öffentlich**. Es enthält `PROMPT.md`, `CLAUDE.md` und den vollständigen Quellbestand. Falls das nicht beabsichtigt ist: Settings → *Visibility*. - **Den Verschlüsselungsschlüssel niemals mit ausliefern** — auch nicht als Beispiel, das jemand übernimmt. --- # Teil 2 — Das System testen Vier Stufen, jede baut auf der vorigen auf. Steigen Sie nicht ein, wo es interessant aussieht — steigen Sie unten ein. | Stufe | Aufwand | Was danach belegt ist | | --- | --- | --- | | [A — Rauchtest](#a--rauchtest-15-minuten) | 15 min | Die Anlage läuft | | [B — Der Rundlauf](#b--der-rundlauf-30-minuten) | 30 min | Sicherung und Wiederherstellung stimmen bitgenau | | [C — Agent](#c--ein-zweites-system-über-den-agenten) | 1 h | Ein anderes System lässt sich sichern | | [D — Proxmox](#d--proxmox-der-offene-meilenstein) | 2 h | Der offene Meilenstein der Phase 7 | ## A — Rauchtest (15 Minuten) Auf Ihrem Dev-Server, mit dem entpackten Paket. ```bash # 1. PostgreSQL bereitstellen (falls noch nicht vorhanden) docker run -d --name syncova-db -p 5432:5432 \ -e POSTGRES_DB=syncova -e POSTGRES_USER=syncova \ -e POSTGRES_PASSWORD= postgres:17-alpine # 2. Schlüssel erzeugen — und aufheben ./bin/syncova-admin generate-key # 3. Umgebung cat > /tmp/syncova.env <<'ENV' SYNCOVA_ENV=production SYNCOVA_DB_HOST=127.0.0.1 SYNCOVA_DB_PORT=5432 SYNCOVA_DB_NAME=syncova SYNCOVA_DB_USER=syncova SYNCOVA_DB_PASSWORD= SYNCOVA_DB_SSLMODE=disable SYNCOVA_ENCRYPTION_KEYS=v1: SYNCOVA_ENCRYPTION_CURRENT_KEY=v1 SYNCOVA_HTTP_LISTEN_ADDRESS=127.0.0.1:8080 ENV set -a; . /tmp/syncova.env; set +a # 4. Schema und erster Administrator ./bin/syncova-migrate up ./bin/syncova-migrate status ./bin/syncova-admin create-admin --username admin # fragt das Passwort ab # 5. Starten ./bin/syncova-api & sleep 3 curl -s http://127.0.0.1:8080/health/ready | python3 -m json.tool | head -5 ``` **Erwartet:** `"status": "healthy"`. Bei `503` sagt die Antwort, welche Komponente fehlt — die Hülle trägt den vollständigen Bericht, auch im Fehlerfall. ```bash # 6. Anmelden TOKEN=$(curl -s -X POST http://127.0.0.1:8080/api/v1/auth/login \ -H 'Content-Type: application/json' -H "X-Correlation-ID: $(uuidgen)" \ -d '{"username":"admin","password":""}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['tokens']['access_token'])") echo "${TOKEN:0:12}…" ``` **Damit ist belegt:** Dienst, Datenbank, Schema, Verschlüsselung und Anmeldung arbeiten. Mehr nicht — es wurde noch kein Byte gesichert. ## B — Der Rundlauf (30 Minuten) Der Test, auf den es ankommt. ```bash api() { curl -s -H "Authorization: Bearer $TOKEN" -H "X-Correlation-ID: $(uuidgen)" \ -H 'Content-Type: application/json' "$@"; } # 1. Testdaten — inkompressibel, sonst misst man Kompression statt Durchsatz mkdir -p /srv/test/quelle python3 - <<'PY' import os, pathlib root = pathlib.Path("/srv/test/quelle") for index in range(60): (root / f"ordner-{index % 6}").mkdir(exist_ok=True) (root / f"ordner-{index % 6}" / f"datei-{index}.bin").write_bytes(os.urandom(512 * 1024)) (root / "notiz.txt").write_text("Testlauf\n") os.symlink("notiz.txt", root / "verweis.txt") PY # 2. Repository — gehärtet ./bin/syncova-repo create --path /srv/test/repository --name "Testziel" --hardened # 3. Eintragen REPO=$(api -X POST http://127.0.0.1:8080/api/v1/repositories \ -d '{"name":"Testziel","location":"/srv/test/repository"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])") # 4. Löschschutz MESSEN — nicht glauben api -X POST "http://127.0.0.1:8080/api/v1/repositories/$REPO/enforcement/measure" \ | python3 -m json.tool | head -8 ``` **Erwartet:** `"level": "filesystem"`. Steht dort `advisory`, setzt Ihr Dateisystem den Löschschutz **nicht** durch — auf einem gewöhnlichen ext4/xfs mit root sollte `filesystem` herauskommen; in einem Container auf overlayfs nicht. Das ist eine Auskunft über Ihre Anlage, kein Fehler der Software. ```bash # 5. Auftrag anlegen und starten JOB=$(api -X POST http://127.0.0.1:8080/api/v1/jobs -H "Idempotency-Key: $(uuidgen)" -d "{ \"name\":\"Testlauf\",\"repository_id\":\"$REPO\", \"schedule\":{\"type\":\"manual\"}, \"sources\":[{\"type\":\"filesystem\",\"id\":\"/srv/test/quelle\",\"name\":\"Testquelle\"}]}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])") api -X POST "http://127.0.0.1:8080/api/v1/jobs/$JOB/run" -H "Idempotency-Key: $(uuidgen)" \ -o /dev/null -w "HTTP %{http_code}\n" # 202, nicht 201: der Lauf ist eingereiht sleep 15 api "http://127.0.0.1:8080/api/v1/jobs/$JOB/runs" | python3 -c " import sys,json r=json.load(sys.stdin)['data'][0] print(f\" {r['status']} {r.get('bytes_processed',0)/1048576:.1f} MiB gelesen, {r.get('files_processed',0)} Objekte\")" ``` **Erwartet:** `succeeded`, rund 30 MiB, 68 Objekte. Steht dort `partial_failure`, nennt `skip_reasons` den Grund — ein Teilfehler ist **kein** Erfolg. ```bash # 6. Integrität prüfen api -X POST "http://127.0.0.1:8080/api/v1/repositories/$REPO/integrity-scan" \ | python3 -c "import sys,json; print(' ', json.load(sys.stdin)['data']['details']['summary'])" # 7. Wiederherstellbarkeit NACHWEISEN BACKUP=$(api "http://127.0.0.1:8080/api/v1/backups" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data'][0]['id'])") api -X POST http://127.0.0.1:8080/api/v1/verification -H "Idempotency-Key: $(uuidgen)" \ -d "{\"backup_id\":\"$BACKUP\",\"verification_type\":\"restore_test\"}" >/dev/null sleep 10 api "http://127.0.0.1:8080/api/v1/backups/$BACKUP/assurance" | python3 -c " import sys,json d=json.load(sys.stdin)['data'] print(f\" {d['classification']} — {d['percentage']} %\") print(f\" fehlende Messungen: {d['missing_measurements']}\")" ``` **Erwartet:** `recoverable — 70 %`. Vorher stand dort `unverified`. Die 70 % sind richtig: RTO und RPO sind noch nicht gemessen, und Unbekanntes zählt niemals als gut. ```bash # 8. Der Ernstfall: Prüfsummen merken, Quelle vernichten find /srv/test/quelle -type f -print0 | sort -z | xargs -0 shasum -a 256 \ | sed 's|/srv/test/quelle/||' | sort -k2 > /tmp/vorher.sha rm -rf /srv/test/quelle # 9. Vorabprüfung — schreibt nichts api -X POST http://127.0.0.1:8080/api/v1/restores/validate \ -d "{\"backup_id\":\"$BACKUP\",\"target_type\":\"filesystem\",\"target_path\":\"/srv/test/ziel\"}" \ | python3 -c "import sys,json; print(' ', json.load(sys.stdin)['data']['summary'])" # 10. Wiederherstellen RESTORE=$(api -X POST http://127.0.0.1:8080/api/v1/restores -H "Idempotency-Key: $(uuidgen)" \ -d "{\"backup_id\":\"$BACKUP\",\"target_type\":\"filesystem\",\"target_path\":\"/srv/test/ziel\"}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])") sleep 15 # 11. DER NACHWEIS cd /srv/test/ziel && find . -type f -print0 | sort -z | xargs -0 shasum -a 256 \ | sed 's|\./||' | sort -k2 > /tmp/nachher.sha diff /tmp/vorher.sha /tmp/nachher.sha && echo " BITGENAU IDENTISCH" ls -l /srv/test/ziel/verweis.txt # Symlink muss ein Symlink sein ``` **Damit ist belegt:** Die Kette trägt. Alles davor waren Indizien. ### Und jetzt die Gegenprobe Ein Test, der nur den Erfolgsfall zeigt, belegt wenig. Diese drei **müssen** scheitern: ```bash # Ziel nicht leer → Ablehnung api -X POST http://127.0.0.1:8080/api/v1/restores -H "Idempotency-Key: $(uuidgen)" \ -d "{\"backup_id\":\"$BACKUP\",\"target_type\":\"filesystem\",\"target_path\":\"/srv/test/ziel\"}" \ | python3 -c "import sys,json; print(' ', json.load(sys.stdin)['error']['code'])" # Gehärtetes Repository löschen → verweigert rm -rf /srv/test/repository 2>&1 | head -3 ls /srv/test/repository/format/repository.json && echo " Repository hat den Angriff überlebt" # Beschädigter Block → wird erkannt CHUNK=$(find /srv/test/repository/chunks -type f | head -1) chattr -i "$CHUNK" 2>/dev/null; printf 'X' | dd of="$CHUNK" bs=1 seek=0 conv=notrunc 2>/dev/null api -X POST "http://127.0.0.1:8080/api/v1/repositories/$REPO/integrity-scan" \ | python3 -c "import sys,json; d=json.load(sys.stdin)['data']['details']; print(' ', d['summary'])" ``` **Erwartet der Reihe nach:** `CONFLICT` oder `VALIDATION_FAILED`; das Repository steht noch; der Scan meldet einen beschädigten Block und nennt das betroffene Backup. > Nach dem dritten Punkt ist Ihr Testrepository absichtlich kaputt. Für weitere > Versuche neu anlegen. ## C — Ein zweites System über den Agenten ```bash # Am Server: Aufnahme-Token erzeugen api -X POST http://127.0.0.1:8080/api/v1/agents/enrollment-tokens \ -H "Idempotency-Key: $(uuidgen)" -d '{"agent_name":"SRV-02"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])" ``` Auf dem zu sichernden System: ```bash ./syncova-agent register --server http://:8080 \ --token --state /etc/syncova/agent.json ./syncova-agent run --state /etc/syncova/agent.json ``` Am Server einen Auftrag mit `agent_id` in der Quelle anlegen. **Der Agent schreibt selbst ins Repository** — es muss auf seinem System eingehängt und beschreibbar sein. Fehlt das, meldet er `REPOSITORY_UNREACHABLE` und liefert **keine** leere Sicherung ab. Der Agent baut ausschließlich **ausgehende** Verbindungen auf. Eine eingehende Freigabe ist nie nötig. ### Windows — der offene Punkt ```powershell .\syncova-agent.exe register --server http://:8080 ` --token --state C:\ProgramData\Syncova\agent.json sc.exe create SyncovaAgent ` binPath= "C:\Program Files\Syncova\syncova-agent.exe run --state C:\ProgramData\Syncova\agent.json" ` start= auto sc.exe start SyncovaAgent ``` **Der Dienstname muss `SyncovaAgent` lauten** — er steht im Programm. Weicht er ab, meldet sich der Prozess nicht beim Dienstverwalter an, und Windows beendet ihn nach der Startfrist mit einer Meldung, die wie ein Absturz aussieht. Zu prüfen: Anmeldung am Dienstverwalter, Verhalten bei *Dienst beenden* und Systemneustart, Rechte des Dienstkontos beim Lesen. **Das ist der Punkt, der die Zeile „Windows file backup" der Akzeptanzmatrix offen hält.** ## D — Proxmox: der offene Meilenstein Vier Schritte. Der erste ist rein lesend. ```bash # 1. Nur nachsehen export SYNCOVA_PROXMOX_TOKEN_SECRET='' ./bin/syncova-proxmox discover --url https://pve.example:8006 \ --token 'syncova@pve!backup' --fingerprint --disks # 2. Verbund eintragen api -X POST http://127.0.0.1:8080/api/v1/proxmox/clusters -d '{ "name":"pve-labor","api_endpoint":"https://pve.example:8006", "api_token_id":"syncova@pve!backup","api_token_secret":"", "tls_fingerprint":"","backup_storage_id":"local", "archive_transport":"local","archive_mount_roots":{"local":"/var/lib/vz"}}' # 3. Bestand aufnehmen, Auftrag mit proxmox_vm-Quelle anlegen, sichern # 4. Test-VM löschen, wiederherstellen — NEBEN das Original ./bin/syncova-proxmox restore-guest --cluster \ --repository /srv/test/repository --backup \ --target-guest qemu/900 --node pve-01 ``` Dann in Proxmox: **VM 900 starten und hineinsehen.** Bootet sie? Stimmen die Daten? Ist die MAC-Adresse erhalten? **Genau das ist der verpflichtende Meilenstein der Phase 7 und der einzige Punkt, der ihn offen hält.** Alles davor läuft gegen einen Nachbau der API durch; ob eine wiederhergestellte Maschine startet, kann nur echte Hardware beantworten. Rechnen Sie mit Nacharbeit — die Plattenzuordnung wird bewusst nicht aus der gesicherten Konfiguration gesetzt (sie verwiese auf den alten Ort) und erscheint als Warnung. --- ## Was danach in der Akzeptanzmatrix stehen darf Nach Stufe B: `Linux file backup`, `file restore`, `folder restore`, `restore validation`, `local repository`, `integrity scan`, `corruption detection`. Nach C: `agent restart` — und mit Windows die Zeile `Windows file backup`. Nach D: `Proxmox VM backup` und `Proxmox restore`. **Haken Sie nur ab, was Sie gefahren haben.** Eine Zeile, die man abhakt, weil der Code vorhanden ist, ist die teuerste Zeile des ganzen Dokuments — sie kostet genau dann, wenn man sich auf sie verlässt. ## Wenn etwas nicht geht [docs/troubleshooting.md](troubleshooting.md) — nach Symptom geordnet. Für eine Rückfrage nützlich: Version (`--version`), Schemastand (`syncova-migrate status`), die `request_id` der fehlgeschlagenen Anfrage und die Protokollzeilen dazu.