v0.4.5 - releases + update-check
This commit is contained in:
parent
7dfb6a0abb
commit
7dd83f0a88
9 changed files with 290 additions and 6 deletions
112
.forgejo/workflows/release.yml
Normal file
112
.forgejo/workflows/release.yml
Normal 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
|
||||
24
CHANGELOG.md
24
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
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.4
|
||||
0.4.5
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
69
server-up/core/updater.py
Normal 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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue