Files
homelab-upgrade/docs/PORT.md
T
homelab-updaterandClaude Opus 5 e9cb72a59c
ci / check (push) Successful in 37s
ci / publish (push) Successful in 19s
image_version: name the version of a digest-only pin from the container's labels
Port of image_version.sh with a contract test (16 cases computed by the old script's Python). For
apps with real updates, status sends a manifest that pins a digest only or a floating tag (dockge,
grafana) to image_version, which takes the version from the OCI or LinuxServer label and hands it to
image_tags; no usable label is STOP version_unknown. inspect exposes only the version labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:15:18 +02:00

8.2 KiB
Raw Blame History

Port skryptów ze starego repo (etap 2)

Zasada z DESIGN.md: po jednym skrypcie, z testem kontraktowym (wynik TS zgodny ze starym skryptem na zapisanym wejściu). Bash służy tylko jako wzorzec; niczego mutującego ze starego repo nie uruchamiamy.

Skrypt Stan Gdzie Test kontraktowy
status.sh + compose_ps_json.py gotowe domain/compose.ts, app/ops/status.ts fixtures/compose-ps/cases.json: 15 wejść (2 prawdziwe wyjścia compose ps z VM103 + przypadki brzegowe); oczekiwania to wynik starego parsera
status na sucho (bez plików projektu) gotowe domain/compose.ts (parseDockerPs, healthFromStatus), app/ops/containers.ts Nowe, bez odpowiednika w starym repo: docker ps -a z filtrami etykiet compose zamiast docker compose ps, bo compose.yaml/.env są root:root 600. Testy na wierszach nagranych z VM103 (Docker 28). manifest_sha256 = null; status.sh zostaje dla kroków zmieniających
image_tags.sh gotowe domain/tags.ts, domain/registry.ts, infra/http-registry.ts, app/ops/image-tags.ts fixtures/image-tags/cases.json: 25 przypadków (16 syntetycznych + 9 nagranych z prawdziwych rejestrów); oczekiwania liczy czysta część starego skryptu (generate.py). Dodatkowo raz sprawdzone na żywo: stary skrypt dał ten sam JSON co port dla wszystkich 9 repozytoriów
image_resolve.sh gotowe (przez HTTP, bez Dockera) domain/image-ref.ts, infra/http-registry.ts (resolveImage), app/ops/image-resolve.ts Bez testu kontraktowego względem starego skryptu: docker buildx nie ma na hoście dev. Zamiast tego testy jednostkowe na fałszywym rejestrze i sprawdzenie na żywo: digest z portu równy Docker-Content-Digest z curl dla nextcloud:31.0.12-apache i ghcr.io/linuxserver/radarr:latest
preflight.sh, verify.sh reguły gotowe; obserwacje w executorze/driverze do zrobienia domain/preflight.ts (preflightVerdict), domain/verify.ts (verifyChecks, nextVerifyAction) Testy jednostkowe reguł. Dryf manifestu: obraz działającego kontenera = obraz w manifeście, mounty zadeklarowane = zaobserwowane. Nie porównujemy docker compose config --hash z etykietą config-hash: na VM103 (compose 2.37.3) różnią się dla każdej usługi z env_file zaraz po up (sprawdzone 2026-09-19 na jackett i ombi; orcaslicer bez env_file się zgadza)
backup_check (nowe, z DESIGN.md) reguła gotowa domain/backup-check.ts (HostBackupRecord, backupVerdict) Testy jednostkowe: kopia VM103 młodsza niż 48 h, rekord innej VM, brak kopii, data z przyszłości (tolerancja 5 min)
apply_support.py (bramki apply.sh) gotowe domain/compose-edit.ts (validateImage, rewriteImage, diffExpanded) fixtures/apply-gates/cases.json: 55 przypadków (obraz, podmiana linii image:, diff rozwiniętego compose); oczekiwania liczą czyste funkcje starego modułu importowane przez generate.py
image_version.sh gotowe domain/image-version.ts (imageVersion), krok image_version w app/dry-run/handlers.ts fixtures/image-version/cases.json: 16 przypadków; oczekiwania liczy Python starego skryptu z dwoma wywołaniami Dockera podmienionymi na dane (generate.py). Krok działa tylko dla aplikacji z apply = on: obraz z manifestu daje executor (/inspect), etykiety wersji docker inspect przez proxy
backup.sh, apply.sh, rollback.sh do zrobienia jako ostatnie (mutacje)

Infrastruktura pod porty: infra/process-runner.ts (prawdziwy CommandRunner: bez shella, czyste środowisko, AbortSignal, limit wyjścia).

Regeneracja fixture'ów

node fixtures/image-tags/record.ts fixtures/image-tags/recorded            # nagranie z rejestrów (sieć)
python3 fixtures/image-tags/generate.py <ścieżka do starego image_tags.sh> fixtures/image-tags/recorded

cases.json zawiera hash skryptu, z którego pochodzą oczekiwania (provenance). Nagrania są zredukowane: wszystkie tagi pasujące do wzorca skilla plus 50 innych.

Zachowania starego kodu przeniesione świadomie (i pułapki)

  • candidate to najwyższy tag, nie kolejny krok. steps to ścieżka (najwyższy tag każdej nowszej linii major.minor, rosnąco). DESIGN.md mówi „bramka: version_change wyższy niż auto → STOP” i jednocześnie „wersje krok po kroku”. Trzeba rozstrzygnąć, czy bramka patrzy na candidate czy na steps[0] (demo używa steps[0]).

  • Starszy „kandydat” jest raportowany jak zmiana. Gdy current jest wyższy niż każdy tag pasujący do wzorca, skrypt i tak zwraca candidate i niezerowe version_change (np. major). Przypięte testem w tags.test.ts. Handler musi to odrzucić, inaczej auto: major mógłby „zaktualizować” w dół.

  • Nowszy build tej samej wersji (-ls364 → -ls365) daje version_change: none. To celowe: zmienia się obraz, nie wersja aplikacji.

  • Wzorzec ^[0-9.]+$ jest zbyt luźny. W prawdziwym rejestrze grafana/grafana pasuje do niego tag 9799770991 (to nie wersja), który stary skrypt uznaje za candidate (major). To samo dotyczy każdej aplikacji z takim wzorcem. Wzorce ze skilli trzeba zawęzić (np. ^[0-9]+\.[0-9]+\.[0-9]+$) przy imporcie skilli.

  • Wersje to liczby całkowite dowolnej długości (BigInt), bo tagi mogą nieść znaczniki czasu.

  • Wyrażenia regularne: wszystkie 11 wzorców ze skilli działa tak samo w Pythonie i JS. Wzorzec spoza tego zbioru (np. (?P<n>…) lub (?i)) nie skompiluje się w JS i zostanie zgłoszony jako invalid tag pattern.

  • image_resolve: brak etykiety wersji to null, nie błąd. nextcloud i grafana/grafana nie mają org.opencontainers.image.version. Decyzja, czy to STOP (no_version_label przy repin), należy do handlera kroku, nie do portu.

  • platforms powtarza wpisy (linux/arm dwa razy: warianty v6 i v7), tak jak stary skrypt. Lista nie jest zbiorem.

  • Skrypt czytał przez docker buildx na VM, port czyta rejestr po HTTP. Ten krok nie potrzebuje więc docker.sock. Port dodatkowo sprawdza, że bajty manifestu i configu zgadzają się z digestem, i odrzuca odpowiedzi większe niż 4 MiB.

  • Bramki apply przepisane wiernie, z dziwactwami. a/b:c:d@sha256:… przechodzi jako obraz a/b:c z tagiem d; tag latest-1.0 nie jest „pływający”. Ścisły format referencji sprawdza dodatkowo executor na wejściu (zod) i porównuje ją z pinned z image_resolve.

  • Tam, gdzie Python i JS się różnią, port odmawia. Digest z końcowym \n (Pythonowe $ go przepuszczało) to malformed_digest. Manifest z BOM, samotnym \r, \v, \f, \x1c–\x1f, \x85 albo U+2028/9 daje unsupported_characters, bo splitlines/strip w Pythonie dzielą i obcinają inaczej niż JS. W diffie 1 i 1.0 są w JS równe (Python widział zmianę typu); true i 1 różnią się w obu. Rozwinięty compose, który nie jest obiektem, to zmiana korzenia (ścieżka ""), a nie wyjątek.

  • rewriteImage gubi cudzysłowy wokół starej wartości, a resztę linii (np. komentarz) zostawia: image: "nginx:1.0" # x staje się image: <nowy> # x.

  • image_version: wersja musi dać się użyć jako tag. Stary skrypt z etykiety " 1.0 " proponował org/app: 1.0 @sha256:…; port uznaje ją za brak wersji (no_version_label → STOP version_unknown). Z etykiet kontenera czytamy tylko trzy etykiety wersji (inne potrafią nieść hasze haseł, np. basic auth traefika).

  • Repin bez nowej wersji nie jest osobnym apply. DESIGN.md opisuje łańcuch repin → backup → apply. Tu image_version tylko nazywa wersję działającego obrazu i przekazuje ją do image_tags; najbliższa aktualizacja i tak wpisze do manifestu repo:tag@digest.

Co port dodaje względem skryptów

  • RegistryClient: typowane błędy (rate_limited, unauthorized → auth_failed, …), walidacja odpowiedzi zod, kolejne strony tylko z domeny rejestru (token nigdy nie wychodzi poza nią), tylko https dla adresu tokenu, limit stron i czasu.
  • CommandRunner: polecenie widzi tylko PATH/LANG/TZ (i to, co przekaże wywołujący), więc tokeny drivera nie wyciekają do skryptów.
  • Błędy poleceń mają postać wymaganą przez DESIGN.md: polecenie, kod wyjścia, pierwsza linia stderr (maskowanie robi driver).