v0.4.5 - releases + update-check
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m2s
Deploy server-up (prod) / deploy (push) Successful in 41s
Release / release (push) Successful in 5s

This commit is contained in:
bes-r 2026-06-06 22:04:04 +02:00
parent 7dfb6a0abb
commit 7dd83f0a88
9 changed files with 290 additions and 6 deletions

View file

@ -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

View file

@ -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

View file

@ -1 +1 @@
0.4.4
0.4.5

View file

@ -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())

View file

@ -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"))

69
server-up/core/updater.py Normal file
View file

@ -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

View file

@ -126,6 +126,11 @@ tailwind.config = {
<span class="w-px h-4 bg-bd dark:bg-bd-dark mx-1"></span>
<span class="mdi mdi-layers-outline text-bl"></span><span x-text="docker.images ?? ''"></span>
</div>
<a x-show="update.update_available" x-cloak :href="update.url" target="_blank" rel="noopener"
:title="t('update_available')"
class="flex items-center gap-1.5 px-3 h-10 rounded-xl bg-ac/10 text-ac dark:text-ac-dark text-xs font-bold hover:bg-ac/20 transition">
<span class="mdi mdi-arrow-up-bold-circle text-base"></span><span x-text="'v'+update.latest"></span>
</a>
<button @click="toggleTheme()" :title="t('theme')" class="btn-icon">
<span class="mdi text-xl" :class="theme==='dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'"></span>
</button>
@ -360,6 +365,44 @@ tailwind.config = {
</div>
</div>
<!-- Updates -->
<div class="card">
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
<span class="mdi mdi-update text-ac dark:text-ac-dark text-lg"></span>
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('updates')">Updates</span>
<template x-if="update.update_available"><span class="badge badge-ok" x-text="t('update_available')">update beschikbaar</span></template>
<template x-if="update.enabled && !update.update_available && !update.error"><span class="badge badge-dim" x-text="t('up_to_date')">up-to-date</span></template>
<button class="btn-sm ml-auto" @click="loadUpdate()"><span class="mdi mdi-refresh"></span> <span x-text="t('check_now')">Controleren</span></button>
</div>
<div class="p-5 space-y-4">
<div class="flex flex-wrap gap-x-8 gap-y-2 text-sm">
<div><div class="form-label" x-text="t('current_version')">Huidige versie</div><div class="font-mono font-semibold text-wh dark:text-wh-dark" x-text="'v'+(update.current||version)"></div></div>
<div x-show="update.latest"><div class="form-label" x-text="t('latest_version')">Laatste release</div><div class="font-mono font-semibold" :class="update.update_available ? 'text-ac dark:text-ac-dark' : 'text-wh dark:text-wh-dark'" x-text="'v'+update.latest"></div></div>
</div>
<template x-if="update.update_available && update.notes">
<details class="rounded-xl border border-bd dark:border-bd-dark overflow-hidden">
<summary class="px-3.5 py-2.5 cursor-pointer font-bold text-xs text-wh dark:text-wh-dark bg-s2/60 dark:bg-s2-dark/60 flex items-center gap-2"><span class="mdi mdi-text-box-outline"></span> <span x-text="t('release_notes')">Release notes</span></summary>
<pre class="p-3.5 text-xs whitespace-pre-wrap break-words text-t2 dark:text-t2-dark max-h-64 overflow-auto" x-text="update.notes"></pre>
</details>
</template>
<template x-if="update.update_available">
<a :href="update.url" target="_blank" rel="noopener" class="btn btn-primary w-max"><span class="mdi mdi-open-in-new"></span> <span x-text="t('view_release')">Bekijk release</span></a>
</template>
<p x-show="update.error" class="text-xs text-er" x-text="update.error"></p>
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
<div>
<label class="form-label" x-text="t('update_api_url')">Releases-API URL</label>
<input class="form-input font-mono" x-model="settings.UPDATE_API_URL" @change="autoSave(); setTimeout(()=>loadUpdate(), 600)" placeholder="http://git.example.com/api/v1/repos/bes-r/server-up/releases">
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('update_api_hint')">Forgejo/GitHub Releases-API. Leeg = uit.</p>
</div>
<label class="inline-flex items-center gap-2.5 cursor-pointer">
<input type="checkbox" :checked="!!settings.UPDATE_INCLUDE_PRERELEASE" @change="settings.UPDATE_INCLUDE_PRERELEASE=$event.target.checked; autoSave(); setTimeout(()=>loadUpdate(), 600)" class="w-5 h-5 rounded border-bd dark:border-bd-dark text-ac">
<span class="text-sm text-tx dark:text-tx-dark" x-text="t('include_prerelease')">Pre-releases (beta) meenemen</span>
</label>
</div>
</div>
</div>
<!-- Taalmodule (add-on) -->
<div class="card">
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
@ -728,7 +771,7 @@ tailwind.config = {
<script>
function app() {
return {
version: '0.4.4',
version: '0.4.5',
page: location.hash.slice(1) || 'dashboard',
isMobile: window.innerWidth < 768,
sidebarOpen: window.innerWidth >= 768 && localStorage.getItem('sidebar') !== 'closed',
@ -750,6 +793,7 @@ function app() {
settings: {},
languages: [],
modules: [],
update: { enabled:false, update_available:false, current:'', latest:'', url:'', notes:'', prerelease:false },
T: {},
job: { id: null, lines: [], status: 'idle', timer: null },
@ -794,6 +838,7 @@ function app() {
await this.loadDocker();
await this.loadModules();
await this.loadDashboard();
this.loadUpdate();
setInterval(() => { if (this.page==='dashboard') this.loadDocker(); }, 10000);
this.$watch('page', async (p) => {
if (p === 'stacks') this.loadStacks();
@ -1080,6 +1125,8 @@ function app() {
async loadModules() { const d = await this.rpc('/api/modules'); this.modules = Array.isArray(d) ? d : []; },
async loadUpdate() { const d = await this.rpc('/api/update'); if (d && typeof d === 'object') this.update = d; },
async checkWizard() {
const d = await this.rpc('/api/wizard');
if (!d.done) {

View file

@ -104,5 +104,16 @@
"lang_name": "Name",
"lang_flag": "Flag",
"search_keys": "Search key or text…",
"delete_language_confirm": "Are you sure you want to delete this language?"
"delete_language_confirm": "Are you sure you want to delete this language?",
"updates": "Updates",
"update_available": "Update available",
"up_to_date": "Up to date",
"check_now": "Check now",
"current_version": "Current version",
"latest_version": "Latest release",
"release_notes": "Release notes",
"view_release": "View release",
"update_api_url": "Releases API URL",
"update_api_hint": "Forgejo/GitHub Releases API returning the latest release. Empty = disabled.",
"include_prerelease": "Include pre-releases (beta)"
}

View file

@ -104,5 +104,16 @@
"lang_name": "Naam",
"lang_flag": "Vlag",
"search_keys": "Zoek vertaalsleutel of tekst…",
"delete_language_confirm": "Weet je zeker dat je deze taal wilt verwijderen?"
"delete_language_confirm": "Weet je zeker dat je deze taal wilt verwijderen?",
"updates": "Updates",
"update_available": "Update beschikbaar",
"up_to_date": "Up-to-date",
"check_now": "Controleren",
"current_version": "Huidige versie",
"latest_version": "Laatste release",
"release_notes": "Release notes",
"view_release": "Bekijk release",
"update_api_url": "Releases-API URL",
"update_api_hint": "Forgejo/GitHub Releases-API die de laatste release teruggeeft. Leeg = uitgeschakeld.",
"include_prerelease": "Pre-releases (beta) meenemen"
}