diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..8117a75 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,112 @@ +# Forgejo Actions β€” maak automatisch een Release bij een v*-tag. +# +# Bouwt de release-notes uit: +# 1) de bijbehorende sectie in CHANGELOG.md +# 2) een commit-overzicht sinds de vorige tag +# en publiceert die als Forgejo-Release via de API. Pre-release tags +# (bv. v0.8.4-beta1) worden als pre-release gemarkeerd. +# +# Token: gebruikt de automatische GITHUB_TOKEN van Forgejo Actions. Werkt dat +# niet (onvoldoende rechten), zet dan een secret RELEASE_TOKEN met repo-write. + +name: Release + +on: + push: + tags: + - "v*" + +jobs: + release: + runs-on: [self-hosted, prod] + timeout-minutes: 10 + + steps: + - name: Checkout (volledige historie + tags) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Release-notes opbouwen + id: notes + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#v}" + echo "TAG=${TAG}" >> "$GITHUB_ENV" + + # Pre-release? (tag bevat een suffix met streepje, bv. -beta1 / -rc.1) + if [[ "$VERSION" == *-* ]]; then + echo "PRERELEASE=true" >> "$GITHUB_ENV" + else + echo "PRERELEASE=false" >> "$GITHUB_ENV" + fi + + NOTES="$(mktemp)" + # 1) Sectie uit CHANGELOG.md (van "# vX..." tot de volgende "# v...") + awk -v ver="$VERSION" ' + $0 ~ ("^# v?" ver "([ \t).β€”-]|$)") {grab=1; print; next} + grab && /^# v/ {exit} + grab {print} + ' CHANGELOG.md > "$NOTES" || true + + # 2) Commit-overzicht sinds de vorige tag + PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")" + { + echo "" + echo "## Commits" + if [ -n "$PREV" ]; then + git log --no-merges --pretty="- %s (%h)" "${PREV}..${TAG}" + echo "" + echo "**Volledige changelog:** ${PREV}...${TAG}" + else + git log --no-merges --pretty="- %s (%h)" "${TAG}" + fi + } >> "$NOTES" + + echo "NOTES_FILE=${NOTES}" >> "$GITHUB_ENV" + echo "----- release notes -----"; cat "$NOTES" + + - name: Release aanmaken via Forgejo API + run: | + set -euo pipefail + TOKEN="${{ secrets.RELEASE_TOKEN }}" + if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITHUB_TOKEN }}"; fi + + # Bouw JSON-payload (body veilig ge-escaped via python) + python3 - "$TAG" "$NOTES_FILE" "$PRERELEASE" > payload.json <<'PY' + import json, sys + tag, notes_file, pre = sys.argv[1], sys.argv[2], sys.argv[3] + body = open(notes_file, encoding="utf-8").read() + print(json.dumps({ + "tag_name": tag, + "name": tag, + "body": body, + "draft": False, + "prerelease": pre == "true", + })) + PY + + API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases" + code=$(curl -sS -o resp.json -w "%{http_code}" -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d @payload.json "$API") + echo "HTTP ${code}"; cat resp.json || true + + if [ "$code" = "201" ]; then + echo "Release ${TAG} aangemaakt." + elif [ "$code" = "409" ]; then + # Bestaat al: update de bestaande release + ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))") + if [ -n "$ID" ]; then + curl -fsS -X PATCH \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d @payload.json "${API}/${ID}" >/dev/null + echo "Bestaande release ${TAG} bijgewerkt." + fi + else + echo "Release aanmaken faalde (HTTP ${code})"; exit 1 + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ddc00..9b65f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +# v0.4.5 β€” Releases & in-app update-check + +## Nieuw in v0.4.5 + +### 🏷️ Automatische releases bij een tag +Push je een `v*`-tag, dan maakt de nieuwe `release.yml`-workflow automatisch een +Forgejo-**Release** aan met notes uit de bijbehorende `CHANGELOG.md`-sectie plus +een commit-overzicht sinds de vorige tag. Pre-release tags (bv. `v0.8.4-beta1`) +worden als pre-release gemarkeerd. Zo krijg je een Releases-pagina met duidelijke +changelog per versie, vergelijkbaar met GitHub Releases. + +### πŸ”” In-app "update beschikbaar" +Server Up vergelijkt de draaiende versie met de laatste release via de Forgejo/ +GitHub Releases-API (semver-vergelijking, pre-releases optioneel). Is er een +nieuwere versie, dan verschijnt een melding in de topbar en een blok in +Instellingen β†’ Updates met versie, release-notes en een link. Instelbaar via +`UPDATE_API_URL` (of env `SU_UPDATE_API`) en de optie "pre-releases meenemen". + +### πŸ”– Versiebeleid +Bump bij elke wijziging het centrale `VERSION`-bestand; tags zijn de bron voor +release-versies (`vX.Y.Z`, of `-beta`/`-rc` voor pre-releases). + +--- + # v0.4.4 β€” Nette versienummers op alle builds ## Nieuw in v0.4.4 diff --git a/VERSION b/VERSION index 6f2743d..0bfccb0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.4 +0.4.5 diff --git a/server-up/app.py b/server-up/app.py index f5f5b6c..f9092d3 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -13,12 +13,12 @@ from flask import Flask, Response, jsonify, render_template, request import core as cfg from core import audit, jobs, i18n -from core import docker, git, boilerplates +from core import docker, git, boilerplates, updater from core.modules import Module, CORE, discover app = Flask(__name__, static_folder="static", template_folder="templates") -VERSION = os.environ.get("SU_VERSION", "0.4.4") +VERSION = os.environ.get("SU_VERSION", "0.4.5") MODULES: list[Module] = [] USER_MOD = APP / "modules" @@ -133,6 +133,11 @@ def api_version(): return jsonify(version=VERSION) +@app.route("/api/update") +def api_update(): + return jsonify(updater.check(VERSION)) + + @app.route("/api/docker/images") def api_images(): return jsonify(images=docker.images()) diff --git a/server-up/core/__init__.py b/server-up/core/__init__.py index 62c829a..c43d585 100644 --- a/server-up/core/__init__.py +++ b/server-up/core/__init__.py @@ -23,6 +23,11 @@ DEFAULTS = { "LANGUAGE": "nl", "THEME": "dark", "WIZARD_DONE": False, + # Update-check: Forgejo/GitHub Releases-API die de laatste release teruggeeft. + # Leeg = uitgeschakeld. Voorbeeld (Forgejo): + # http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases + "UPDATE_API_URL": os.environ.get("SU_UPDATE_API", ""), + "UPDATE_INCLUDE_PRERELEASE": False, } _path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json")) diff --git a/server-up/core/updater.py b/server-up/core/updater.py new file mode 100644 index 0000000..35f195f --- /dev/null +++ b/server-up/core/updater.py @@ -0,0 +1,69 @@ +"""Update-check tegen een Forgejo/GitHub Releases-API (stdlib only).""" +from __future__ import annotations +import json +import re +import urllib.request + +import core as cfg + +_SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+.](.+))?$") + + +def _pre_key(pre: str): + parts = [p for p in re.split(r"[.+-]", pre) if p] if pre else [] + return tuple((0, int(p)) if p.isdigit() else (1, p) for p in parts) + + +def _parse(v: str): + """Naar vergelijkbare tuple. Release sorteert hoger dan pre-release.""" + if not v: + return None + m = _SEMVER.match(v.strip()) + if not m: + return None + pre = m.group(4) or "" + return (int(m.group(1)), int(m.group(2)), int(m.group(3)), + 1 if not pre else 0, _pre_key(pre)) + + +def check(current: str) -> dict: + c = cfg.load() + url = (c.get("UPDATE_API_URL") or "").strip() + include_pre = bool(c.get("UPDATE_INCLUDE_PRERELEASE")) + res = {"enabled": bool(url), "current": current, "latest": None, + "update_available": False, "url": "", "notes": "", + "prerelease": False, "error": ""} + if not url: + return res + try: + req = urllib.request.Request( + url, headers={"Accept": "application/json", "User-Agent": "server-up"}) + with urllib.request.urlopen(req, timeout=8) as r: + data = json.loads(r.read().decode("utf-8")) + except Exception as e: + res["error"] = f"{type(e).__name__}: {e}" + return res + + rels = data if isinstance(data, list) else [data] + best, best_t = None, None + for rel in rels: + if not isinstance(rel, dict) or rel.get("draft"): + continue + if rel.get("prerelease") and not include_pre: + continue + t = _parse(rel.get("tag_name") or rel.get("name") or "") + if not t: + continue + if best_t is None or t > best_t: + best, best_t = rel, t + if not best: + return res + + res["latest"] = (best.get("tag_name") or best.get("name") or "").lstrip("v") + res["url"] = best.get("html_url") or best.get("url") or "" + res["notes"] = best.get("body") or "" + res["prerelease"] = bool(best.get("prerelease")) + cur_t = _parse(current) + if cur_t and best_t and best_t > cur_t: + res["update_available"] = True + return res diff --git a/server-up/templates/index.html b/server-up/templates/index.html index 2ac764f..fb15c19 100644 --- a/server-up/templates/index.html +++ b/server-up/templates/index.html @@ -126,6 +126,11 @@ tailwind.config = { + + + @@ -360,6 +365,44 @@ tailwind.config = { + +
+
+ + Updates + + + +
+
+
+
Huidige versie
+
Laatste release
+
+ + +

+
+
+ + +

Forgejo/GitHub Releases-API. Leeg = uit.

+
+ +
+
+
+
@@ -728,7 +771,7 @@ tailwind.config = {