v0.8.13-beta - updates voor beta en releases herstellen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 7m43s

This commit is contained in:
Ramon 2026-08-04 16:10:59 +02:00
parent 331475415a
commit 6cc078e7b9
15 changed files with 268 additions and 63 deletions

View file

@ -23,6 +23,10 @@ PORT=5000
# hand aanpassen kan om terug te rollen naar een eerdere versie. # hand aanpassen kan om terug te rollen naar een eerdere versie.
#SU_TAG=0.5.40-beta #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 # 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 # 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. # als de server zelf bouwt na een push naar Git — dan is Git de update-route.

View file

@ -40,6 +40,7 @@ jobs:
if [ -z "$VERSION" ]; then if [ -z "$VERSION" ]; then
VERSION="$(tr -d '[:space:]' < VERSION)" VERSION="$(tr -d '[:space:]' < VERSION)"
fi fi
VERSION="${VERSION#v}"
IMAGE="${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up" IMAGE="${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up"
echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
@ -131,6 +132,7 @@ jobs:
--pull \ --pull \
--build-arg "SU_VERSION=${VERSION}" \ --build-arg "SU_VERSION=${VERSION}" \
-t "${IMAGE}:${VERSION}" \ -t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:v${VERSION}" \
-t "${IMAGE}:${EXTRA_TAG}" \ -t "${IMAGE}:${EXTRA_TAG}" \
. .
@ -138,8 +140,11 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
docker push "${IMAGE}:${VERSION}" 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}" docker push "${IMAGE}:${EXTRA_TAG}"
echo "Gepusht: ${IMAGE}:${VERSION} en ${IMAGE}:${EXTRA_TAG}" echo "Gepusht: ${IMAGE}:${VERSION}, :v${VERSION} en :${EXTRA_TAG}"
- name: Uitloggen - name: Uitloggen
if: always() if: always()

View file

@ -146,8 +146,8 @@ jobs:
if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then
touch .env touch .env
sed -i '/^SU_TAG=/d' .env sed -i '/^SU_TAG=/d; /^SU_VERSION=/d' .env
echo "SU_TAG=${VERSION}" >> .env printf 'SU_TAG=%s\nSU_VERSION=%s\n' "${VERSION}" "${VERSION}" >> .env
echo "Inhoud van $(pwd)/.env:" echo "Inhoud van $(pwd)/.env:"
grep -vE '^\s*(#|$)' .env | sed 's/^/ /' grep -vE '^\s*(#|$)' .env | sed 's/^/ /'
else else
@ -172,6 +172,11 @@ jobs:
| docker login "${{ vars.REGISTRY }}" -u "${{ vars.OWNER }}" --password-stdin | docker login "${{ vars.REGISTRY }}" -u "${{ vars.OWNER }}" --password-stdin
docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:${CHANNEL}" || true docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:${CHANNEL}" || true
docker push "${SU_IMAGE}:${VERSION}" 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. # Pre-releases krijgen geen 'latest': dat hoort bij stable.
case "$VERSION" in case "$VERSION" in
*-*) docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:beta" *-*) docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:beta"

View file

@ -149,8 +149,8 @@ jobs:
if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then
touch .env touch .env
sed -i '/^SU_TAG=/d' .env sed -i '/^SU_TAG=/d; /^SU_VERSION=/d' .env
echo "SU_TAG=${VERSION}" >> .env printf 'SU_TAG=%s\nSU_VERSION=%s\n' "${VERSION}" "${VERSION}" >> .env
echo "Inhoud van $(pwd)/.env:" echo "Inhoud van $(pwd)/.env:"
grep -vE '^\s*(#|$)' .env | sed 's/^/ /' grep -vE '^\s*(#|$)' .env | sed 's/^/ /'
else else

View file

@ -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<versie>` 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 # 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, De controle of elk image nog bestaat kostte elf à twaalf minuten, bij élke push,

View file

@ -1 +1 @@
0.8.12-beta 0.8.13-beta

View file

@ -3,8 +3,8 @@ services:
build: build:
context: . context: .
args: args:
# Bakt de versie in het image. De deploy-workflow zet SU_VERSION # Bakt de versie in het image. De deploy-workflow zet SU_VERSION uit
# (git-tag of main-<sha>); zonder waarde valt het terug op 'dev'. # VERSION of de release-tag; zonder waarde valt het terug op 'dev'.
SU_VERSION: ${SU_VERSION:-dev} SU_VERSION: ${SU_VERSION:-dev}
# Image + tag staan los, zodat bijwerken vanuit de interface werkt: die # Image + tag staan los, zodat bijwerken vanuit de interface werkt: die
# schrijft SU_TAG in .env en laat compose de container hercreëren. # schrijft SU_TAG in .env en laat compose de container hercreëren.

View file

@ -56,15 +56,17 @@ aangeboden.
### Wat er gebeurt als je op "Nu bijwerken" klikt ### Wat er gebeurt als je op "Nu bijwerken" klikt
``` ```
1. docker pull git.example.com/bes-r/server-up:0.5.10 1. Forgejo-tag `v0.5.10` vertalen naar registry-tag `0.5.10`
2. SU_TAG=0.5.10 wegschrijven in het .env van de deploy-map 2. docker pull git.example.com/bes-r/server-up:0.5.10
3. helper-container starten: sleep 5 && docker compose up -d server-up 3. SU_TAG=0.5.10 en SU_VERSION=0.5.10 in de deploy-.env vastleggen
4. Server Up wordt vervangen; de browser pollt /healthz tot de nieuwe versie leeft 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 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. 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 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 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 # .env
SU_IMAGE=10.0.20.22:3000/bes-r/server-up SU_IMAGE=10.0.20.22:3000/bes-r/server-up
SU_TAG=0.5.10 SU_TAG=0.5.10
SU_VERSION=0.5.10
BIND=127.0.0.1 BIND=127.0.0.1
PORT=5000 PORT=5000
``` ```
@ -178,10 +181,16 @@ PORT=5000
registry en werkt de updateknop. Staat het er niet, dan bouwt de server lokaal 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. 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 De Git-release heet bijvoorbeeld `v0.5.10`, maar de canonieke image-tag heet
> installatiemap is nergens in de container gemount. De helper-container die de `:0.5.10`. Server Up verwijdert die ene voorloop-`v` automatisch voor zowel
> hercreatie doet, mount hem wél en zet de tag daar. Tot v0.7.95 probeerde stable/main- als beta-releases. De build publiceert daarnaast `:v0.5.10` als
> Server Up het zelf, en faalde dat dus altijd. 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: Eerste keer overstappen van lokaal bouwen naar de registry:

View file

@ -407,10 +407,11 @@ def api_update_apply():
Kort na afloop verdwijnt deze container; de front-end pollt daarna /healthz. Kort na afloop verdwijnt deze container; de front-end pollt daarna /healthz.
""" """
d = request.json or {} d = request.json or {}
tag = (d.get("tag") or "").strip() tag = updater.image_tag(d.get("tag") or "")
if not tag: if not tag:
info = updater.check(VERSION) 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: if not tag:
return jsonify(ok=False, msg="Geen versie opgegeven en geen release gevonden"), 400 return jsonify(ok=False, msg="Geen versie opgegeven en geen release gevonden"), 400
st = selfupdate.status() st = selfupdate.status()

View file

@ -6,8 +6,8 @@ Up alleen het voorwerk (image ophalen, tag wegschrijven) en laat het de
daadwerkelijke hercreatie over aan een korte **helper-container**: daadwerkelijke hercreatie over aan een korte **helper-container**:
1. `docker pull <registry>/<repo>:<tag>` nieuw image binnenhalen 1. `docker pull <registry>/<repo>:<tag>` nieuw image binnenhalen
2. `SU_TAG=<tag>` in het .env van de deploy-map schrijven 2. `SU_TAG=<tag>` en `SU_VERSION=<tag>` in de deploy-.env schrijven
3. helper starten: sleep 5 && docker compose up -d server-up 3. helper starten met exact hetzelfde Compose-project en -configuratie
4. helper stopt zichzelf (`--rm`) 4. helper stopt zichzelf (`--rm`)
De helper draait op het zojuist gepulde image: dat bevat de docker- en De helper draait op het zojuist gepulde image: dat bevat de docker- en
@ -27,7 +27,7 @@ import time
from pathlib import Path from pathlib import Path
import core as cfg import core as cfg
from core import docker from core import docker, updater
# Labels die `docker compose` op elke container zet. # Labels die `docker compose` op elke container zet.
_LBL_PROJECT = "com.docker.compose.project" _LBL_PROJECT = "com.docker.compose.project"
@ -70,6 +70,7 @@ def inspect_self() -> dict:
"working_dir": labels.get(_LBL_WORKDIR, ""), "working_dir": labels.get(_LBL_WORKDIR, ""),
"config_files": labels.get(_LBL_CONFIGS, ""), "config_files": labels.get(_LBL_CONFIGS, ""),
"service": labels.get(_LBL_SERVICE, "") or "server-up", "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]: def pull(tag: str, log_fn=None) -> tuple[bool, str]:
"""Haal `<repo>:<tag>` binnen. Retourneert (ok, image of foutmelding).""" """Haal `<repo>:<tag>` 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 ""): if not _TAG_RE.match(tag or ""):
return False, f"Ongeldige tag: {tag!r}" return False, f"Ongeldige tag: {tag!r}"
repo = image_repo() repo = image_repo()
@ -180,22 +185,19 @@ def pull(tag: str, log_fn=None) -> tuple[bool, str]:
# ── Toepassen ──────────────────────────────────────────────────────────────── # ── Toepassen ────────────────────────────────────────────────────────────────
def _tag_script(tag: str) -> str: 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 Dit gebeurt in de helper-container: de installatiemap is niet in de
installatiemap (`/opt/server-up`) is nérgens in deze container gemount. Er draaiende Server Up-container gemount, maar wordt wel expliciet aan de
zijn maar drie mounts de docker-socket, het `su-data`-volume en `BASE_DIR` helper gekoppeld. SU_VERSION is nodig voor oudere compose-bestanden die de
en de installatiemap zit daar niet bij. Zelf schrijven belandde dus in de ingebakken imageversie met een runtimewaarde overschrijven.
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.
""" """
q = shlex.quote(f"SU_TAG={tag}") tag_regel = shlex.quote(f"SU_TAG={tag}")
versie_regel = shlex.quote(f"SU_VERSION={tag}")
return ( return (
"touch .env; " "touch .env; "
"sed -i '/^SU_TAG=/d' .env; " "sed -i '/^SU_TAG=/d; /^SU_VERSION=/d' .env; "
f"printf '%s\\n' {q} >> .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 Bij succes keert deze functie terug terwijl de helper al gestart is; enkele
seconden later verdwijnt dit proces omdat de container hercreëerd wordt. seconden later verdwijnt dit proces omdat de container hercreëerd wordt.
""" """
tag = updater.image_tag(tag)
st = status() st = status()
if not st["can_apply"]: if not st["can_apply"]:
return False, st["reason"] return False, st["reason"]
@ -229,39 +232,66 @@ def apply(tag: str, log_fn=None) -> tuple[bool, str]:
return False, image_of_fout return False, image_of_fout
image = image_of_fout image = image_of_fout
# Onthoud waar we vandaan komen, zodat terugrollen mogelijk blijft als het # Een alias als latest of beta verschuift bij iedere release en is dus geen
# nieuwe image niet start. # 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", "") 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, cfg.patch({"UPDATE_PREVIOUS_TAG": vorige_tag,
"UPDATE_LAST_APPLIED": {"tag": tag, "ts": time.time(), "UPDATE_LAST_APPLIED": {"tag": tag, "ts": time.time(),
"from": vorige_tag}}) "from": vorige_tag}})
if log_fn: if log_fn:
log_fn(f"Vorige versie: {vorige_tag} (terugrollen kan vanuit Instellingen)") 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…") 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: if not ok:
return False, melding return False, melding
return True, (f"Bijwerken naar {tag} gestart. Server Up herstart nu; " return True, (f"Bijwerken naar {tag} gestart. Server Up herstart nu; "
"de interface komt over enkele seconden terug.") "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, def _spawn_helper(image: str, working_dir: str, service: str, tag: str,
project: str = "", config_files: str = "",
log_fn=None) -> tuple[bool, str]: log_fn=None) -> tuple[bool, str]:
"""Start een korte container die `docker compose up -d <service>` 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. Projectnaam en configuratiebestanden komen uit de labels van de huidige
`sleep` geeft dit proces de tijd om zijn HTTP-antwoord af te maken voordat container. Daardoor werkt dit ook bij `docker compose -p ... -f ...`, en
het onder zijn eigen voeten wordt weggehaald. wordt niet per ongeluk een tweede project gestart. `--no-build` garandeert
dat Compose het zojuist gepullde release-image gebruikt.
De helper zet ook SU_TAG: hij mount de werkmap, deze container niet.
""" """
script = (f"sleep 5; " compose_up = _compose_up(working_dir, project, config_files, service)
f"{_tag_script(tag)}" script = f"sleep 5; {_tag_script(tag)}{compose_up}"
f"docker compose up -d --remove-orphans {shlex.quote(service)}")
cmd = [docker._bin(), "run", "--rm", "--detach", cmd = [docker._bin(), "run", "--rm", "--detach",
"--name", "server-up-updater", "--name", "server-up-updater",
"-v", "/var/run/docker.sock:/var/run/docker.sock", "-v", "/var/run/docker.sock:/var/run/docker.sock",

View file

@ -67,6 +67,17 @@ def is_prerelease(version: str) -> bool:
return bool(t and t[3] == 0) 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]: def _releases(url: str) -> list[dict]:
req = urllib.request.Request( req = urllib.request.Request(
url, headers={"Accept": "application/json", "User-Agent": "server-up"}) url, headers={"Accept": "application/json", "User-Agent": "server-up"})
@ -91,7 +102,8 @@ def check(current: str, force: bool = False) -> dict:
ch = channel() ch = channel()
res = {"enabled": bool(url), "current": current, "channel": ch, res = {"enabled": bool(url), "current": current, "channel": ch,
"current_is_prerelease": is_prerelease(current), "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": "", "url": "", "notes": "", "prerelease": False, "error": "",
"checked_at": time.time()} "checked_at": time.time()}
if not url: 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 "") tag = (best.get("tag_name") or best.get("name") or "")
res["latest_tag"] = tag 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["url"] = best.get("html_url") or best.get("url") or ""
res["notes"] = best.get("body") or "" res["notes"] = best.get("body") or ""
res["prerelease"] = bool(best.get("prerelease")) res["prerelease"] = bool(best.get("prerelease"))

View file

@ -4078,7 +4078,7 @@ function app() {
// Bijwerken: de container vervangt zichzelf, dus na het startsein is deze // Bijwerken: de container vervangt zichzelf, dus na het startsein is deze
// pagina zijn server kwijt. We pollen /healthz tot de nieuwe versie leeft. // pagina zijn server kwijt. We pollen /healthz tot de nieuwe versie leeft.
async applyUpdate() { 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; if (!confirm(this.t('update_confirm', {tag: naar}))) return;
this.updating = true; this.updating = true;
const d = await this.rpc('/api/update/apply', { tag: naar }); const d = await this.rpc('/api/update/apply', { tag: naar });

View file

@ -349,6 +349,18 @@ check('balk staat halverwege', c.job.progress.pct === 50);
// De poortcontrole praat met de backend, dus die checks zijn asynchroon. // De poortcontrole praat met de backend, dus die checks zijn asynchroon.
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting. // Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
(async () => { (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 ─────────────────────────────────────────────────────────── // ── De hoofdmap ───────────────────────────────────────────────────────────
let gevraagdPad = null; let gevraagdPad = null;
c.rpc = async (url) => { c.rpc = async (url) => {

View file

@ -106,11 +106,25 @@ def test_pull_weigert_ongeldige_tag(su, monkeypatch, tag):
assert "Ongeldige tag" in msg 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 ────────────────────────────────────────────────────── # ── SU_TAG wegschrijven ──────────────────────────────────────────────────────
# Dit gebeurde eerder in Python, hier in de container. Maar de installatiemap is # De draaiende container kan de deploy-.env niet schrijven: de installatiemap
# hier nergens gemount — er zijn maar drie mounts: de docker-socket, su-data en # is daar niet gemount. De helper mount de werkmap wel en houdt tag en versie
# BASE_DIR. Bijwerken vanuit de interface kon daardoor nooit werken. Het gebeurt # bij elkaar, dus toetsen we het shellfragment rechtstreeks.
# nu in de helper, die de werkmap wél mount, dus toetsen we het shellfragment.
def _draai_tagscript(su, map_: Path, tag: str): def _draai_tagscript(su, map_: Path, tag: str):
import subprocess import subprocess
@ -121,14 +135,19 @@ def _draai_tagscript(su, map_: Path, tag: str):
def test_tagscript_maakt_env_aan(su, tmp_path): 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): 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() 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_TAG=")) == 1
assert sum(1 for r in regels if r.startswith("SU_VERSION=")) == 1
def test_tagscript_behoudt_andere_instellingen(su, tmp_path): 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") inhoud = _draai_tagscript(su, tmp_path, "0.5.10")
assert "SU_IMAGE=git.example.com/bes-r/server-up" in inhoud assert "SU_IMAGE=git.example.com/bes-r/server-up" in inhoud
assert "SU_TAG=0.5.10" 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): 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 '") 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 not Path("/tmp/su-inbraak").exists(), "de tag brak uit het commando"
assert inhoud.count("SU_TAG=") == 1 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): 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 """De helper gebruikt dezelfde projectnaam en bestanden als de container."""
tag-fragment start compose gewoon het oude image opnieuw."""
aanroepen = [] aanroepen = []
def nep_run(cmd, **kw): 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": ""})() return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})()
monkeypatch.setattr(su.docker, "_run", nep_run) monkeypatch.setattr(su.docker, "_run", nep_run)
su._spawn_helper("git.example.com/bes-r/server-up:0.5.10", su._spawn_helper(
"/opt/server-up", "server-up", "0.5.10") "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. # 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) cmd = next(c for c in aanroepen if "run" in c)
assert "-v" in cmd and "/opt/server-up:/opt/server-up" in cmd assert "-v" in cmd and "/opt/server-up:/opt/server-up" in cmd
script = cmd[-1] script = cmd[-1]
assert "SU_TAG=0.5.10" in script, "de helper zet de tag niet" 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): def test_rollback_zonder_vorige_versie(su):

View file

@ -27,6 +27,8 @@ def test_stable_negeert_prereleases(upd):
res = upd.check("0.4.60", force=True) res = upd.check("0.4.60", force=True)
assert res["channel"] == "stable" assert res["channel"] == "stable"
assert res["latest"] == "0.5.00" 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 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) res = upd.check("0.5.00", force=True)
assert res["channel"] == "beta" assert res["channel"] == "beta"
assert res["latest"] == "0.5.10-beta1" assert res["latest"] == "0.5.10-beta1"
assert res["image_tag"] == "0.5.10-beta1"
assert res["update_available"] is True assert res["update_available"] is True
assert res["prerelease"] is True assert res["prerelease"] is True
@ -107,6 +110,18 @@ def test_is_prerelease():
assert updater.is_prerelease("0.5.00") is False 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): def test_ongeldig_kanaal_wordt_geweigerd(client):
from conftest import login from conftest import login
csrf = login(client) csrf = login(client)