From 6cc078e7b921e7984fddee90acd8a473a8ff4ad9 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 4 Aug 2026 16:10:59 +0200 Subject: [PATCH] v0.8.13-beta - updates voor beta en releases herstellen --- .env.example | 4 ++ .forgejo/workflows/build.yml | 7 +- .forgejo/workflows/deploy-prod.yml | 9 ++- .forgejo/workflows/deploy.yml | 4 +- CHANGELOG.md | 23 ++++++ VERSION | 2 +- docker-compose.yml | 4 +- docs/updates.md | 27 ++++--- server-up/app.py | 5 +- server-up/core/selfupdate.py | 88 +++++++++++++++-------- server-up/core/updater.py | 17 ++++- server-up/templates/index.html | 2 +- tests/js/menu.test.js | 12 ++++ tests/test_selfupdate.py | 112 +++++++++++++++++++++++++---- tests/test_update_channels.py | 15 ++++ 15 files changed, 268 insertions(+), 63 deletions(-) diff --git a/.env.example b/.env.example index b0df40b..fefe11c 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,10 @@ PORT=5000 # hand aanpassen kan om terug te rollen naar een eerdere versie. #SU_TAG=0.5.40-beta +# De versie die bij die image-tag hoort. De deploy-workflow en updatehelper +# houden SU_TAG en SU_VERSION samen bij; normaal hoef je dit niet zelf te zetten. +#SU_VERSION=0.5.40-beta + # Registry-image, zónder tag. Staat dit ingevuld, dan draait deze server vanaf # een registry en werkt de knop "Nu bijwerken" in de interface. Laat het leeg # als de server zelf bouwt na een push naar Git — dan is Git de update-route. diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index d221bbc..3666041 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -40,6 +40,7 @@ jobs: if [ -z "$VERSION" ]; then VERSION="$(tr -d '[:space:]' < VERSION)" fi + VERSION="${VERSION#v}" IMAGE="${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up" echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" @@ -131,6 +132,7 @@ jobs: --pull \ --build-arg "SU_VERSION=${VERSION}" \ -t "${IMAGE}:${VERSION}" \ + -t "${IMAGE}:v${VERSION}" \ -t "${IMAGE}:${EXTRA_TAG}" \ . @@ -138,8 +140,11 @@ jobs: run: | set -euo pipefail docker push "${IMAGE}:${VERSION}" + # Compatibiliteit met Server Up <= 0.8.12: die vroeg ten onrechte + # om de Forgejo-tag mét v. Nieuwe versies gebruiken de kale tag. + docker push "${IMAGE}:v${VERSION}" docker push "${IMAGE}:${EXTRA_TAG}" - echo "Gepusht: ${IMAGE}:${VERSION} en ${IMAGE}:${EXTRA_TAG}" + echo "Gepusht: ${IMAGE}:${VERSION}, :v${VERSION} en :${EXTRA_TAG}" - name: Uitloggen if: always() diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index a288daa..5e23f56 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -146,8 +146,8 @@ jobs: if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then touch .env - sed -i '/^SU_TAG=/d' .env - echo "SU_TAG=${VERSION}" >> .env + sed -i '/^SU_TAG=/d; /^SU_VERSION=/d' .env + printf 'SU_TAG=%s\nSU_VERSION=%s\n' "${VERSION}" "${VERSION}" >> .env echo "Inhoud van $(pwd)/.env:" grep -vE '^\s*(#|$)' .env | sed 's/^/ /' else @@ -172,6 +172,11 @@ jobs: | docker login "${{ vars.REGISTRY }}" -u "${{ vars.OWNER }}" --password-stdin docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:${CHANNEL}" || true docker push "${SU_IMAGE}:${VERSION}" + # Compatibiliteit met Server Up <= 0.8.12: de oude updater vroeg + # om de Forgejo-tag mét v. Hiermee kan die zichzelf één keer naar + # de gerepareerde updater brengen; nieuwe versies gebruiken de kale tag. + docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:v${VERSION}" + docker push "${SU_IMAGE}:v${VERSION}" # Pre-releases krijgen geen 'latest': dat hoort bij stable. case "$VERSION" in *-*) docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:beta" diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 1a73071..93d6add 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -149,8 +149,8 @@ jobs: if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then touch .env - sed -i '/^SU_TAG=/d' .env - echo "SU_TAG=${VERSION}" >> .env + sed -i '/^SU_TAG=/d; /^SU_VERSION=/d' .env + printf 'SU_TAG=%s\nSU_VERSION=%s\n' "${VERSION}" "${VERSION}" >> .env echo "Inhoud van $(pwd)/.env:" grep -vE '^\s*(#|$)' .env | sed 's/^/ /' else diff --git a/CHANGELOG.md b/CHANGELOG.md index 127f4d6..d39c27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# v0.8.13-beta — De update haalt nu de juiste release op + +**Bijwerken vanuit de interface vroeg Docker om een image dat niet bestond.** +Forgejo noemt een release `v0.8.13`, terwijl de workflows bewust image +`:0.8.13` publiceren. De interface stuurde de Git-tag mét `v` door en dus +mislukte iedere beta- en stable/main-update bij `docker pull`. + +- **Eén expliciete registry-tag.** De updatecheck geeft naast de Git-tag nu + `image_tag` terug. Frontend, endpoint en pull-laag normaliseren allemaal de + ene voorloop-`v`. De workflows publiceren ook `:v` als tijdelijke + compatibiliteitstag, zodat de al geïnstalleerde defecte updater zichzelf naar + deze reparatie kan brengen. +- **De herstart gebruikt exact het bestaande Compose-project.** Projectnaam, + projectmap en alle configuratiebestanden komen uit de containerlabels. + `--no-build --pull never` voorkomt dat Compose tijdens de update oude + broncode bouwt of onverwacht een andere tag ophaalt. +- **Tag en versie blijven bij elkaar.** De helper en beide deploy-workflows + schrijven `SU_TAG` en `SU_VERSION` samen. Ook na een handmatige Compose- + herstart toont en draait Server Up daardoor dezelfde beta- of releaseversie. +- **Terugrollen blijft naar een echte versie wijzen.** Als de container via de + beweeglijke alias `beta` of `latest` draaide, wordt voortaan het ingebakken + OCI-versielabel als terugrolpunt onthouden. + # v0.8.12-beta — De image-controle van twaalf minuten naar seconden De controle of elk image nog bestaat kostte elf à twaalf minuten, bij élke push, diff --git a/VERSION b/VERSION index 4dc5aa8..71d2038 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.12-beta +0.8.13-beta diff --git a/docker-compose.yml b/docker-compose.yml index 7b82ea5..a6da7f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,8 @@ services: build: context: . args: - # Bakt de versie in het image. De deploy-workflow zet SU_VERSION - # (git-tag of main-); zonder waarde valt het terug op 'dev'. + # Bakt de versie in het image. De deploy-workflow zet SU_VERSION uit + # VERSION of de release-tag; zonder waarde valt het terug op 'dev'. SU_VERSION: ${SU_VERSION:-dev} # Image + tag staan los, zodat bijwerken vanuit de interface werkt: die # schrijft SU_TAG in .env en laat compose de container hercreëren. diff --git a/docs/updates.md b/docs/updates.md index e21544f..839ef20 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -56,15 +56,17 @@ aangeboden. ### Wat er gebeurt als je op "Nu bijwerken" klikt ``` -1. docker pull git.example.com/bes-r/server-up:0.5.10 -2. SU_TAG=0.5.10 wegschrijven in het .env van de deploy-map -3. helper-container starten: sleep 5 && docker compose up -d server-up -4. Server Up wordt vervangen; de browser pollt /healthz tot de nieuwe versie leeft +1. Forgejo-tag `v0.5.10` vertalen naar registry-tag `0.5.10` +2. docker pull git.example.com/bes-r/server-up:0.5.10 +3. SU_TAG=0.5.10 en SU_VERSION=0.5.10 in de deploy-.env vastleggen +4. helper starten met exact dezelfde Compose-projectnaam en configuratiebestanden +5. Server Up vervangen; de browser pollt /healthz tot de nieuwe versie leeft ``` -Stap 3 is nodig omdat een container zichzelf niet kan hercreëren: het commando +Stap 4 is nodig omdat een container zichzelf niet kan hercreëren: het commando zou halverwege zijn eigen proces afbreken. De helper draait op het zojuist gehaalde image — dat bevat de docker- en compose-CLI al — en ruimt zichzelf op. +Met `--no-build --pull never` gebruikt Compose gegarandeerd precies dat image. De deploy-map wordt niet geraden maar uitgelezen uit de compose-labels op de eigen container (`com.docker.compose.project.working_dir`). Draait Server Up niet @@ -170,6 +172,7 @@ Op de server waar Server Up draait, in de deploy-map (`/opt/server-up`): # .env SU_IMAGE=10.0.20.22:3000/bes-r/server-up SU_TAG=0.5.10 +SU_VERSION=0.5.10 BIND=127.0.0.1 PORT=5000 ``` @@ -178,10 +181,16 @@ PORT=5000 registry en werkt de updateknop. Staat het er niet, dan bouwt de server lokaal zoals voorheen en meldt de interface netjes waarom bijwerken niet kan. -> Server Up schrijft `SU_TAG` niet zelf in je `.env`, en dat is met opzet: de -> installatiemap is nergens in de container gemount. De helper-container die de -> hercreatie doet, mount hem wél en zet de tag daar. Tot v0.7.95 probeerde -> Server Up het zelf, en faalde dat dus altijd. +De Git-release heet bijvoorbeeld `v0.5.10`, maar de canonieke image-tag heet +`:0.5.10`. Server Up verwijdert die ene voorloop-`v` automatisch voor zowel +stable/main- als beta-releases. De build publiceert daarnaast `:v0.5.10` als +compatibiliteitstag, zodat installaties met de oude updater één keer zonder +handmatige ingreep naar de gerepareerde versie kunnen overstappen. + +> De draaiende Server Up-container schrijft niet rechtstreeks in je deploy- +> `.env`: de installatiemap is daar niet gemount. De updatehelper mount die map +> wel en werkt `SU_TAG` en `SU_VERSION` samen bij. Zo blijft ook een latere +> handmatige `docker compose up -d` op dezelfde versie. Eerste keer overstappen van lokaal bouwen naar de registry: diff --git a/server-up/app.py b/server-up/app.py index 4dee607..534ace6 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -407,10 +407,11 @@ def api_update_apply(): Kort na afloop verdwijnt deze container; de front-end pollt daarna /healthz. """ d = request.json or {} - tag = (d.get("tag") or "").strip() + tag = updater.image_tag(d.get("tag") or "") if not tag: info = updater.check(VERSION) - tag = info.get("latest_tag") or "" + tag = (info.get("image_tag") + or updater.image_tag(info.get("latest_tag") or "")) if not tag: return jsonify(ok=False, msg="Geen versie opgegeven en geen release gevonden"), 400 st = selfupdate.status() diff --git a/server-up/core/selfupdate.py b/server-up/core/selfupdate.py index c4ac9a3..501849a 100644 --- a/server-up/core/selfupdate.py +++ b/server-up/core/selfupdate.py @@ -6,8 +6,8 @@ Up alleen het voorwerk (image ophalen, tag wegschrijven) en laat het de daadwerkelijke hercreatie over aan een korte **helper-container**: 1. `docker pull /:` ← nieuw image binnenhalen - 2. `SU_TAG=` in het .env van de deploy-map schrijven - 3. helper starten: sleep 5 && docker compose up -d server-up + 2. `SU_TAG=` en `SU_VERSION=` in de deploy-.env schrijven + 3. helper starten met exact hetzelfde Compose-project en -configuratie 4. helper stopt zichzelf (`--rm`) De helper draait op het zojuist gepulde image: dat bevat de docker- en @@ -27,7 +27,7 @@ import time from pathlib import Path import core as cfg -from core import docker +from core import docker, updater # Labels die `docker compose` op elke container zet. _LBL_PROJECT = "com.docker.compose.project" @@ -70,6 +70,7 @@ def inspect_self() -> dict: "working_dir": labels.get(_LBL_WORKDIR, ""), "config_files": labels.get(_LBL_CONFIGS, ""), "service": labels.get(_LBL_SERVICE, "") or "server-up", + "version": labels.get("org.opencontainers.image.version", ""), } @@ -159,6 +160,10 @@ def _login(log_fn=None) -> tuple[bool, str]: def pull(tag: str, log_fn=None) -> tuple[bool, str]: """Haal `:` binnen. Retourneert (ok, image of foutmelding).""" + # Forgejo noemt de Git-tag v0.8.13, maar de workflows pushen :0.8.13. + # Normaliseer hier óók, zodat oude clients die nog latest_tag versturen + # nooit een niet-bestaande :v0.8.13 proberen op te halen. + tag = updater.image_tag(tag) if not _TAG_RE.match(tag or ""): return False, f"Ongeldige tag: {tag!r}" repo = image_repo() @@ -180,22 +185,19 @@ def pull(tag: str, log_fn=None) -> tuple[bool, str]: # ── Toepassen ──────────────────────────────────────────────────────────────── def _tag_script(tag: str) -> str: - """Sh-regels die SU_TAG in het .env van de werkmap zetten. + """Sh-regels die de image-tag én versie in de deploy-.env vastleggen. - Dit gebeurt in de helper-container en niet hier, en dat is geen detail: de - installatiemap (`/opt/server-up`) is nérgens in deze container gemount. Er - zijn maar drie mounts — de docker-socket, het `su-data`-volume en `BASE_DIR` - — en de installatiemap zit daar niet bij. Zelf schrijven belandde dus in de - overlay van de container of, waarschijnlijker, mislukte omdat de map hier - niet bestaat. Bijwerken vanuit de interface kon daardoor nooit werken. - - De helper mount `working_dir` wél, op hetzelfde pad, en draait als root. + Dit gebeurt in de helper-container: de installatiemap is niet in de + draaiende Server Up-container gemount, maar wordt wel expliciet aan de + helper gekoppeld. SU_VERSION is nodig voor oudere compose-bestanden die de + ingebakken imageversie met een runtimewaarde overschrijven. """ - q = shlex.quote(f"SU_TAG={tag}") + tag_regel = shlex.quote(f"SU_TAG={tag}") + versie_regel = shlex.quote(f"SU_VERSION={tag}") return ( "touch .env; " - "sed -i '/^SU_TAG=/d' .env; " - f"printf '%s\\n' {q} >> .env; " + "sed -i '/^SU_TAG=/d; /^SU_VERSION=/d' .env; " + f"printf '%s\\n' {tag_regel} {versie_regel} >> .env; " ) @@ -216,6 +218,7 @@ def apply(tag: str, log_fn=None) -> tuple[bool, str]: Bij succes keert deze functie terug terwijl de helper al gestart is; enkele seconden later verdwijnt dit proces omdat de container hercreëerd wordt. """ + tag = updater.image_tag(tag) st = status() if not st["can_apply"]: return False, st["reason"] @@ -229,39 +232,66 @@ def apply(tag: str, log_fn=None) -> tuple[bool, str]: return False, image_of_fout image = image_of_fout - # Onthoud waar we vandaan komen, zodat terugrollen mogelijk blijft als het - # nieuwe image niet start. + # Een alias als latest of beta verschuift bij iedere release en is dus geen + # bruikbaar terugrolpunt. Het OCI-versielabel verwijst naar de echte, + # onveranderlijke versietag; val terug op de gebruikte imagetag voor oude + # images zonder label. + versie = updater.image_tag(info.get("version", "")) huidig = info.get("image", "") - vorige_tag = huidig.rsplit(":", 1)[-1] if _has_tag(huidig) else "latest" + if _TAG_RE.match(versie or "") and versie != "dev": + vorige_tag = versie + else: + vorige_tag = huidig.rsplit(":", 1)[-1] if _has_tag(huidig) else "latest" + cfg.patch({"UPDATE_PREVIOUS_TAG": vorige_tag, "UPDATE_LAST_APPLIED": {"tag": tag, "ts": time.time(), "from": vorige_tag}}) if log_fn: log_fn(f"Vorige versie: {vorige_tag} (terugrollen kan vanuit Instellingen)") - log_fn(f"SU_TAG={tag} wordt door de helper in {werkmap}/.env gezet") + log_fn(f"SU_TAG en SU_VERSION worden in {werkmap}/.env bijgewerkt") log_fn("Helper starten die de container hercreëert…") - ok, melding = _spawn_helper(image, werkmap, service, tag, log_fn) + ok, melding = _spawn_helper( + image, werkmap, service, tag, + project=info.get("project", ""), + config_files=info.get("config_files", ""), + log_fn=log_fn, + ) if not ok: return False, melding return True, (f"Bijwerken naar {tag} gestart. Server Up herstart nu; " "de interface komt over enkele seconden terug.") +def _compose_up(working_dir: str, project: str, config_files: str, + service: str) -> str: + """Bouw het commando voor exact het huidige Compose-project.""" + cmd = ["docker", "compose", "--project-directory", working_dir] + if project: + cmd.extend(["--project-name", project]) + for config in (p.strip() for p in (config_files or "").split(",")): + if config: + cmd.extend(["--file", config]) + # Het doelimage is vlak hiervoor expliciet gepulld. Bouw nooit stilletjes + # oude broncode uit de deploy-map en haal tijdens de herstart niets anders. + cmd.extend(["up", "-d", "--remove-orphans", "--no-build", "--pull", "never", + service]) + return shlex.join(cmd) + + def _spawn_helper(image: str, working_dir: str, service: str, tag: str, + project: str = "", config_files: str = "", log_fn=None) -> tuple[bool, str]: - """Start een korte container die `docker compose up -d ` uitvoert. + """Start een helper die het bestaande Compose-project exact hercreëert. - Draait op hetzelfde (nieuwe) image, dat de docker- en compose-CLI al bevat. - `sleep` geeft dit proces de tijd om zijn HTTP-antwoord af te maken voordat - het onder zijn eigen voeten wordt weggehaald. - - De helper zet ook SU_TAG: hij mount de werkmap, deze container niet. + Projectnaam en configuratiebestanden komen uit de labels van de huidige + container. Daardoor werkt dit ook bij `docker compose -p ... -f ...`, en + wordt niet per ongeluk een tweede project gestart. `--no-build` garandeert + dat Compose het zojuist gepullde release-image gebruikt. """ - script = (f"sleep 5; " - f"{_tag_script(tag)}" - f"docker compose up -d --remove-orphans {shlex.quote(service)}") + compose_up = _compose_up(working_dir, project, config_files, service) + script = f"sleep 5; {_tag_script(tag)}{compose_up}" cmd = [docker._bin(), "run", "--rm", "--detach", "--name", "server-up-updater", "-v", "/var/run/docker.sock:/var/run/docker.sock", diff --git a/server-up/core/updater.py b/server-up/core/updater.py index 4ac6c93..09b378e 100644 --- a/server-up/core/updater.py +++ b/server-up/core/updater.py @@ -67,6 +67,17 @@ def is_prerelease(version: str) -> bool: return bool(t and t[3] == 0) +def image_tag(release_tag: str) -> str: + """Vertaal een Forgejo-release-tag naar de tag uit onze registry. + + Git-tags heten volgens afspraak `v0.8.13`; de build-workflows verwijderen + die voorloop-v en pushen `:0.8.13`. Alleen een geldige semver wordt + aangepast, zodat een handmatige registry-tag zoals `beta` intact blijft. + """ + tag = (release_tag or "").strip() + return tag[1:] if tag.startswith("v") and _parse(tag) else tag + + def _releases(url: str) -> list[dict]: req = urllib.request.Request( url, headers={"Accept": "application/json", "User-Agent": "server-up"}) @@ -91,7 +102,8 @@ def check(current: str, force: bool = False) -> dict: ch = channel() res = {"enabled": bool(url), "current": current, "channel": ch, "current_is_prerelease": is_prerelease(current), - "latest": None, "latest_tag": "", "update_available": False, + "latest": None, "latest_tag": "", "image_tag": "", + "update_available": False, "url": "", "notes": "", "prerelease": False, "error": "", "checked_at": time.time()} if not url: @@ -121,7 +133,8 @@ def check(current: str, force: bool = False) -> dict: tag = (best.get("tag_name") or best.get("name") or "") res["latest_tag"] = tag - res["latest"] = tag.lstrip("v") + res["image_tag"] = image_tag(tag) + res["latest"] = res["image_tag"] res["url"] = best.get("html_url") or best.get("url") or "" res["notes"] = best.get("body") or "" res["prerelease"] = bool(best.get("prerelease")) diff --git a/server-up/templates/index.html b/server-up/templates/index.html index f9c93ae..1b6b08b 100644 --- a/server-up/templates/index.html +++ b/server-up/templates/index.html @@ -4078,7 +4078,7 @@ function app() { // Bijwerken: de container vervangt zichzelf, dus na het startsein is deze // pagina zijn server kwijt. We pollen /healthz tot de nieuwe versie leeft. async applyUpdate() { - const naar = this.update.latest_tag || this.update.latest; + const naar = this.update.image_tag || this.update.latest || this.update.latest_tag; if (!confirm(this.t('update_confirm', {tag: naar}))) return; this.updating = true; const d = await this.rpc('/api/update/apply', { tag: naar }); diff --git a/tests/js/menu.test.js b/tests/js/menu.test.js index 195672e..8a2a0f4 100644 --- a/tests/js/menu.test.js +++ b/tests/js/menu.test.js @@ -349,6 +349,18 @@ check('balk staat halverwege', c.job.progress.pct === 50); // De poortcontrole praat met de backend, dus die checks zijn asynchroon. // Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting. (async () => { + // Forgejo noemt de release v0.8.13-beta, maar de registry-workflow pusht de + // image-tag zonder v. De updateknop moet de expliciete image-tag gebruiken. + let updateBody = null; + global.confirm = () => true; + c.update = { current: '0.8.12-beta', latest: '0.8.13-beta', + latest_tag: 'v0.8.13-beta', image_tag: '0.8.13-beta' }; + c.rpc = async (url, body) => { updateBody = body; return { job_id: 'upd1' }; }; + c.pollJob = () => {}; + await c.applyUpdate(); + check('update gebruikt registry-tag zonder v', + updateBody && updateBody.tag === '0.8.13-beta'); + // ── De hoofdmap ─────────────────────────────────────────────────────────── let gevraagdPad = null; c.rpc = async (url) => { diff --git a/tests/test_selfupdate.py b/tests/test_selfupdate.py index 7123a20..3b42cbc 100644 --- a/tests/test_selfupdate.py +++ b/tests/test_selfupdate.py @@ -106,11 +106,25 @@ def test_pull_weigert_ongeldige_tag(su, monkeypatch, tag): assert "Ongeldige tag" in msg +def test_pull_stript_de_v_van_een_forgejo_release(su, monkeypatch): + """De workflow pusht :0.8.13-beta, niet :v0.8.13-beta.""" + _fake_inspect(su, monkeypatch) + getrokken = [] + monkeypatch.setattr(su, "_login", lambda log_fn=None: (True, "")) + monkeypatch.setattr( + su.docker, "_stream", + lambda cmd, **kw: getrokken.append(cmd[-1]) or 0, + ) + ok, image = su.pull("v0.8.13-beta") + assert ok is True + assert image == "git.example.com/bes-r/server-up:0.8.13-beta" + assert getrokken == [image] + + # ── SU_TAG wegschrijven ────────────────────────────────────────────────────── -# Dit gebeurde eerder in Python, hier in de container. Maar de installatiemap is -# hier nergens gemount — er zijn maar drie mounts: de docker-socket, su-data en -# BASE_DIR. Bijwerken vanuit de interface kon daardoor nooit werken. Het gebeurt -# nu in de helper, die de werkmap wél mount, dus toetsen we het shellfragment. +# De draaiende container kan de deploy-.env niet schrijven: de installatiemap +# is daar niet gemount. De helper mount de werkmap wel en houdt tag en versie +# bij elkaar, dus toetsen we het shellfragment rechtstreeks. def _draai_tagscript(su, map_: Path, tag: str): import subprocess @@ -121,14 +135,19 @@ def _draai_tagscript(su, map_: Path, tag: str): def test_tagscript_maakt_env_aan(su, tmp_path): - assert _draai_tagscript(su, tmp_path, "0.5.10").strip() == "SU_TAG=0.5.10" + assert _draai_tagscript(su, tmp_path, "0.5.10").splitlines() == [ + "SU_TAG=0.5.10", "SU_VERSION=0.5.10", + ] def test_tagscript_vervangt_bestaande_regel(su, tmp_path): - (tmp_path / ".env").write_text("BIND=0.0.0.0\nSU_TAG=0.5.00\nPORT=5000\n") + (tmp_path / ".env").write_text( + "BIND=0.0.0.0\nSU_TAG=0.5.00\nSU_VERSION=0.5.00\nPORT=5000\n") regels = _draai_tagscript(su, tmp_path, "0.5.10").splitlines() - assert regels == ["BIND=0.0.0.0", "PORT=5000", "SU_TAG=0.5.10"] + assert regels == ["BIND=0.0.0.0", "PORT=5000", "SU_TAG=0.5.10", + "SU_VERSION=0.5.10"] assert sum(1 for r in regels if r.startswith("SU_TAG=")) == 1 + assert sum(1 for r in regels if r.startswith("SU_VERSION=")) == 1 def test_tagscript_behoudt_andere_instellingen(su, tmp_path): @@ -136,6 +155,7 @@ def test_tagscript_behoudt_andere_instellingen(su, tmp_path): inhoud = _draai_tagscript(su, tmp_path, "0.5.10") assert "SU_IMAGE=git.example.com/bes-r/server-up" in inhoud assert "SU_TAG=0.5.10" in inhoud + assert "SU_VERSION=0.5.10" in inhoud def test_tagscript_laat_zich_niet_uitbreiden(su, tmp_path): @@ -144,11 +164,11 @@ def test_tagscript_laat_zich_niet_uitbreiden(su, tmp_path): inhoud = _draai_tagscript(su, tmp_path, "1.0.0'; touch /tmp/su-inbraak; echo '") assert not Path("/tmp/su-inbraak").exists(), "de tag brak uit het commando" assert inhoud.count("SU_TAG=") == 1 + assert inhoud.count("SU_VERSION=") == 1 def test_de_helper_zet_de_tag_en_mount_de_werkmap(su, monkeypatch): - """Zonder de mount schrijft de helper in zijn eigen overlay, en zonder het - tag-fragment start compose gewoon het oude image opnieuw.""" + """De helper gebruikt dezelfde projectnaam en bestanden als de container.""" aanroepen = [] def nep_run(cmd, **kw): @@ -156,14 +176,82 @@ def test_de_helper_zet_de_tag_en_mount_de_werkmap(su, monkeypatch): return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})() monkeypatch.setattr(su.docker, "_run", nep_run) - su._spawn_helper("git.example.com/bes-r/server-up:0.5.10", - "/opt/server-up", "server-up", "0.5.10") + su._spawn_helper( + "git.example.com/bes-r/server-up:0.5.10", + "/opt/server-up", "server-up", "0.5.10", + project="eigen-project", + config_files="/opt/server-up/compose.yml,/opt/server-up/extra.yml", + ) # De eerste aanroep ruimt een blijven hangen helper op; de tweede is de start. cmd = next(c for c in aanroepen if "run" in c) assert "-v" in cmd and "/opt/server-up:/opt/server-up" in cmd script = cmd[-1] assert "SU_TAG=0.5.10" in script, "de helper zet de tag niet" - assert "docker compose up -d" in script + assert "SU_VERSION=0.5.10" in script, "de helper zet de versie niet" + assert "--project-directory /opt/server-up" in script + assert "--project-name eigen-project" in script + assert "--file /opt/server-up/compose.yml" in script + assert "--file /opt/server-up/extra.yml" in script + assert "up -d --remove-orphans --no-build --pull never server-up" in script + + +def test_apply_endpoint_normaliseert_release_tag(client, monkeypatch): + """Ook een oudere UI mag :v0.8.13 nooit aan Docker doorgeven.""" + from core import jobs, selfupdate + csrf = login(client) + gezien = [] + monkeypatch.setattr(selfupdate, "status", lambda: { + "can_apply": True, "reason": "", "previous_tag": "", + }) + monkeypatch.setattr( + selfupdate, "apply", + lambda tag, log_fn=None: (gezien.append(tag) or True, "gestart"), + ) + monkeypatch.setattr(jobs, "run", lambda fn, *args: fn(*args)) + + r = client.post("/api/update/apply", json={"tag": "v0.8.13-beta"}, + headers={"X-CSRF-Token": csrf}) + assert r.status_code == 200 + assert r.get_json()["tag"] == "0.8.13-beta" + assert gezien == ["0.8.13-beta"] + + +def test_apply_onthoudt_versie_in_plaats_van_beweeglijke_alias( + su, env, monkeypatch): + _fake_inspect(su, monkeypatch, image="git.example.com/bes-r/server-up:beta", + version="0.8.12-beta") + monkeypatch.setattr( + su, "pull", + lambda tag, log_fn=None: ( + True, "git.example.com/bes-r/server-up:0.8.13-beta"), + ) + monkeypatch.setattr(su, "_spawn_helper", lambda *args, **kw: (True, "ok")) + + ok, _ = su.apply("v0.8.13-beta") + assert ok is True + opgeslagen = env["core"].load() + assert opgeslagen["UPDATE_PREVIOUS_TAG"] == "0.8.12-beta" + assert opgeslagen["UPDATE_LAST_APPLIED"]["tag"] == "0.8.13-beta" + + +@pytest.mark.parametrize("workflow", [ + ".forgejo/workflows/deploy.yml", + ".forgejo/workflows/deploy-prod.yml", +]) +def test_deploy_schrijft_tag_en_versie_samen(workflow): + tekst = (Path(__file__).resolve().parent.parent / workflow).read_text() + assert "'/^SU_TAG=/d; /^SU_VERSION=/d'" in tekst + assert "SU_TAG=%s\\nSU_VERSION=%s\\n" in tekst + + +@pytest.mark.parametrize("workflow,pushregel", [ + (".forgejo/workflows/build.yml", 'docker push "${IMAGE}:v${VERSION}"'), + (".forgejo/workflows/deploy-prod.yml", + 'docker push "${SU_IMAGE}:v${VERSION}"'), +]) +def test_release_publiceert_tag_voor_oude_updater(workflow, pushregel): + tekst = (Path(__file__).resolve().parent.parent / workflow).read_text() + assert pushregel in tekst def test_rollback_zonder_vorige_versie(su): diff --git a/tests/test_update_channels.py b/tests/test_update_channels.py index d3badc2..8b7a255 100644 --- a/tests/test_update_channels.py +++ b/tests/test_update_channels.py @@ -27,6 +27,8 @@ def test_stable_negeert_prereleases(upd): res = upd.check("0.4.60", force=True) assert res["channel"] == "stable" assert res["latest"] == "0.5.00" + assert res["latest_tag"] == "v0.5.00" + assert res["image_tag"] == "0.5.00" assert res["update_available"] is True @@ -35,6 +37,7 @@ def test_beta_ziet_de_prerelease(upd, env): res = upd.check("0.5.00", force=True) assert res["channel"] == "beta" assert res["latest"] == "0.5.10-beta1" + assert res["image_tag"] == "0.5.10-beta1" assert res["update_available"] is True assert res["prerelease"] is True @@ -107,6 +110,18 @@ def test_is_prerelease(): assert updater.is_prerelease("0.5.00") is False +@pytest.mark.parametrize("release_tag,image_tag", [ + ("v0.8.13", "0.8.13"), + ("v0.8.13-beta", "0.8.13-beta"), + ("0.8.13", "0.8.13"), + ("beta", "beta"), +]) +def test_registry_tag_past_bij_de_workflow(release_tag, image_tag): + """Forgejo-tags beginnen met v; de registry-workflow stript die v.""" + from core import updater + assert updater.image_tag(release_tag) == image_tag + + def test_ongeldig_kanaal_wordt_geweigerd(client): from conftest import login csrf = login(client)