feat(update): Testing/Stable-Update-Kanäle wie enconf (Suite=trixie, Komponente=Kanal)

Installer (EDGEGUARD_CHANNEL), Kanal-Lesen/-Schreiben ohne DB-State
(sources.list ist Quelle der Wahrheit), Cluster-Endpoints für Kanalwechsel
mit mTLS-Peer-Propagation + Drift-Erkennung, --allow-downgrades für
testing→stable-Downgrades über den bestehenden sicheren Rolling-Update-
Flow, Settings-UI mit Bestätigung, neues scripts/release.sh (Testing-Push
datumsbasiert YYYY.MM.DD.NN, Stable-Promotion mit Verify-Gate + Git-Tag),
publish.sh/cleanup-old.sh kanalfähig mit Stable-Tag-Schutz.

Migriert Bestandsnodes automatisch von der alten "main"-Komponente auf
"stable" (postinst, idempotent) — ohne das würden vor diesem Release
installierte Nodes stillschweigend keine Updates mehr sehen, sobald
main nicht mehr bespielt wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-02 16:31:52 +02:00
parent 7ff6575790
commit bab82f8d5b
13 changed files with 571 additions and 33 deletions

View File

@@ -1,27 +1,49 @@
#!/bin/bash
# Cleanup-old: löscht alle EdgeGuard-Versionen im Gitea Package
# Registry außer den letzten N (default 10).
# Registry außer den letzten N (default 10) — pro Kanal (Komponente).
#
# Wird vom Makefile direkt nach erfolgreichem Upload aufgerufen. Reihen-
# folge ist wichtig: ERST der neue Build hochladen, DANN die ältesten
# wegschmeißen — sonst riskieren wir bei Cleanup-vor-Upload eine Lücke.
# Wird vom Makefile/release.sh direkt nach erfolgreichem Upload
# aufgerufen. Reihenfolge ist wichtig: ERST der neue Build hochladen,
# DANN die ältesten wegschmeißen — sonst riskieren wir bei Cleanup-vor-
# Upload eine Lücke.
#
# Stable-Schutz: Versionen, die als Git-Tag `v<version>` im Repo stehen,
# werden im stable-Kanal NIE gelöscht (analog enconf STABLE_PROTECT) —
# ein Kunde, der genau diese Version installiert hat, muss sie über
# apt-get weiterhin ziehen können.
#
# Voraussetzungen:
# - ~/.gitea-token mit write-Package-Scope
# - jq + curl
#
# Aufruf:
# ./cleanup-old.sh # KEEP=10 (default)
# KEEP=20 ./cleanup-old.sh # mehr behalten
# ./cleanup-old.sh # Kanal stable, KEEP=10 (default)
# ./cleanup-old.sh testing # Kanal testing
# KEEP=20 ./cleanup-old.sh stable # mehr behalten
set -euo pipefail
KEEP="${KEEP:-10}"
OWNER="projekte"
DIST="trixie"
COMPONENT="main"
COMPONENT="${1:-stable}"
case "$COMPONENT" in stable|testing) ;; *) echo "cleanup-old: unknown channel '$COMPONENT' (expected stable or testing)" >&2; exit 2 ;; esac
BASE="https://git.netcell-it.de"
# Geschützte Versionen (nur relevant im stable-Kanal): jeder vorhandene
# Git-Tag `v*` im Repo-Root. Leer wenn nicht im Repo ausgeführt oder im
# testing-Kanal (dort gibt es keine Tags/keinen Schutz).
PROTECTED_VERSIONS=""
if [ "$COMPONENT" = "stable" ]; then
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PROTECTED_VERSIONS="$(git -C "$REPO_ROOT" tag --list 'v*' 2>/dev/null | sed 's/^v//' || true)"
fi
is_protected() {
local v="$1"
[ -z "$PROTECTED_VERSIONS" ] && return 1
printf '%s\n' "$PROTECTED_VERSIONS" | grep -qx "$v"
}
if [ -z "${GITEA_TOKEN:-}" ]; then
if [ -r "$HOME/.gitea-token" ]; then
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
@@ -37,6 +59,7 @@ TOK="$GITEA_TOKEN"
PKGS=(
"edgeguard:all"
"edgeguard-api:amd64"
"edgeguard-api:arm64"
"edgeguard-ui:all"
)
@@ -44,27 +67,37 @@ cleanup_pkg() {
local pkg="$1"
local arch="$2"
# Versionen sammeln. Gitea Package-API liefert flache Liste:
# /api/v1/packages/{owner}?type=debian&q={name}
# Versionen sammeln. Gitea Package-API liefert flache Liste über ALLE
# Kanäle hinweg: /api/v1/packages/{owner}?type=debian&q={name}
# Wir filtern client-seitig auf name (exact-match — Gitea-Query ist
# leider substring-fuzzy) und sort -V (semver) absteigend.
# leider substring-fuzzy) UND auf das Versionsformat des aktuellen
# Kanals — stable=Semver (X.Y.Z), testing=datumsbasiert (YYYY.MM.DD.NN).
# Die beiden Formate überlappen nie, das hält testing- und stable-
# Versionen sauber getrennt, obwohl Gitea (Name,Version) global dedupliziert.
local raw
raw="$(curl -fsS -H "Authorization: token $TOK" \
"$BASE/api/v1/packages/$OWNER?type=debian&q=$pkg&limit=1000")"
local ver_pattern
if [ "$COMPONENT" = "stable" ]; then
ver_pattern='^[0-9]+\.[0-9]+\.[0-9]+$'
else
ver_pattern='^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$'
fi
local versions
versions="$(printf '%s' "$raw" | jq -r --arg n "$pkg" \
'.[] | select(.name==$n) | .version' | sort -V -r | awk '!seen[$0]++')"
'.[] | select(.name==$n) | .version' | grep -E "$ver_pattern" | sort -V -r | awk '!seen[$0]++')"
if [ -z "$versions" ]; then
echo " $pkg ($arch): no versions found, skip"
echo " $pkg ($arch): no $COMPONENT versions found, skip"
return 0
fi
local count
count="$(printf '%s\n' "$versions" | wc -l)"
if [ "$count" -le "$KEEP" ]; then
echo " $pkg ($arch): $count versions, ≤ keep=$KEEP, nothing to delete"
echo " $pkg ($arch): $count $COMPONENT versions, ≤ keep=$KEEP, nothing to delete"
return 0
fi
@@ -72,10 +105,14 @@ cleanup_pkg() {
to_delete="$(printf '%s\n' "$versions" | tail -n +$((KEEP+1)))"
local kept_count=$((count - $(printf '%s\n' "$to_delete" | wc -l)))
echo " $pkg ($arch): $count versions total, keeping $kept_count newest, deleting $(printf '%s\n' "$to_delete" | wc -l)"
echo " $pkg ($arch): $count $COMPONENT versions total, keeping $kept_count newest, considering $(printf '%s\n' "$to_delete" | wc -l) for deletion"
while IFS= read -r v; do
[ -z "$v" ] && continue
if is_protected "$v"; then
echo " skip $pkg $v $arch (protected — git tag v$v exists)"
continue
fi
# DELETE-Endpoint für Debian-Pakete:
# /api/packages/{owner}/debian/pool/{dist}/{comp}/{name}/{version}/{arch}
local url="$BASE/api/packages/$OWNER/debian/pool/$DIST/$COMPONENT/$pkg/$v/$arch"

View File

@@ -11,16 +11,22 @@
# Befund 2026-05-17 nach Gitea-Disk-Full-Vorfall.
#
# Aufruf:
# ./publish.sh <version> <arch>
# arch = amd64 | arm64. arm64 publiziert NUR edgeguard-api (das einzige
# arch-spezifische Paket); amd64 publiziert alle drei.
# ./publish.sh <version> <arch> [channel]
# arch = amd64 | arm64. arm64 publiziert NUR edgeguard-api (das einzige
# arch-spezifische Paket); amd64 publiziert alle drei.
# channel = stable | testing (Default stable). Kanal-Modell wie enconf:
# Suite=trixie fest, Komponente=Kanal. Wird von scripts/release.sh
# gesetzt — direkter Aufruf (z.B. aus altem `make publish`) bleibt
# ohne 3. Arg unverändert auf stable.
set -euo pipefail
VERSION="${1:?usage: publish.sh <version> <arch>}"
ARCH="${2:?usage: publish.sh <version> <arch>}"
VERSION="${1:?usage: publish.sh <version> <arch> [channel]}"
ARCH="${2:?usage: publish.sh <version> <arch> [channel]}"
CHANNEL="${3:-stable}"
case "$CHANNEL" in stable|testing) ;; *) echo "publish: unknown channel '$CHANNEL' (expected stable or testing)" >&2; exit 2 ;; esac
BASE="https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/main/upload"
BASE="https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/${CHANNEL}/upload"
if [ -z "${GITEA_TOKEN:-}" ]; then
if [ -r "$HOME/.gitea-token" ]; then
@@ -88,4 +94,4 @@ case "$ARCH" in
;;
esac
echo "publish: ok ($VERSION/$ARCH)"
echo "publish: ok ($VERSION/$ARCH/$CHANNEL)"

View File

@@ -4,6 +4,9 @@
#
# curl -fsSL https://get.netcell-edgeguard.de | sudo bash
#
# Kanal wählen (Default: stable):
# curl -fsSL https://get.netcell-edgeguard.de | EDGEGUARD_CHANNEL=testing sudo -E bash
#
# Supported: Debian 13 (Trixie), amd64 + arm64.
# ══════════════════════════════════════════════════════════════════════════════
@@ -111,12 +114,14 @@ setup_repo() {
curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \
-o /etc/apt/keyrings/nmg.asc
fi
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie main" \
# Kanal-Modell (wie enconf): Suite = OS-Codename, Komponente = Kanal.
# Default stable; EDGEGUARD_CHANNEL=testing für Testing-Kanal.
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie ${EDGEGUARD_CHANNEL:-stable}" \
> /etc/apt/sources.list.d/edgeguard.list
apt-get update -qq
}
step "Set up EdgeGuard apt repository" setup_repo
step "Set up EdgeGuard apt repository (${EDGEGUARD_CHANNEL:-stable})" setup_repo
AVAILABLE=$(LC_ALL=C apt-cache policy edgeguard 2>/dev/null | awk '/Candidate:/ {print $2; exit}' || true)
if [ -n "$AVAILABLE" ] && [ "$AVAILABLE" != "(none)" ]; then

181
scripts/release.sh Executable file
View File

@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# EdgeGuard — Release erstellen (Testing-Push oder Stable-Promotion)
#
# Verwendung:
# ./scripts/release.sh # Testing-Push: baut den aktuellen
# # Stand, Version = datumsbasiert
# # YYYY.MM.DD.NN, Upload → testing
# ./scripts/release.sh stable # Stable-Promotion: baut den
# # aktuellen Stand unter der
# # nächsten Patch-Version (VERSION-
# # Datei +1), Upload → stable,
# # VERSION-Commit + Git-Tag v<version>
# ./scripts/release.sh stable 1.4.0 # Stable mit expliziter Version
#
# Kanal-Modell 1:1 von enconf (netcell-webpanel) übernommen — Suite=trixie
# fest, Komponente=Kanal (stable/testing). An EdgeGuard angepasst:
# - Gates laufen über die bestehende Go-Quality-Baseline (`make deb` ruft
# `release-check` + `management-ui`-tsc automatisch auf) statt eigener
# Preflight-Schritte.
# - Kein Docs-/Changelog-/Checksum-/Marketing-Site-Deploy — dafür
# existiert bei EdgeGuard keine Infrastruktur (siehe CLAUDE.md).
# - Testing-Versionen sind datumsbasiert (wie enconf) DAMIT sie (a) bei
# apt immer über jeder Stable-Semver-Version sortieren und (b) nie mit
# einer Stable-Versionsnummer kollidieren — Gitea dedupliziert
# (Name,Version) global über alle Kanäle hinweg.
#
# Voraussetzung: GITEA_TOKEN (env oder ~/.gitea-token) mit Package-
# Upload/Delete/Query-Scope.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_DIR"
OWNER="projekte"
GITEA_URL="https://git.netcell-it.de"
KEEP="${KEEP:-10}"
log() { echo "[release] $*"; }
warn() { echo "$*" >&2; }
abort() { echo "🛑 ABBRUCH: $*" >&2; exit 1; }
if [ -z "${GITEA_TOKEN:-}" ]; then
if [ -r "$HOME/.gitea-token" ]; then
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
else
abort "GITEA_TOKEN fehlt (env oder ~/.gitea-token)"
fi
fi
export GITEA_TOKEN
# ─── Testing-Version: datumsbasiert YYYY.MM.DD.NN ──────────────────────
# NN = laufende Nummer für den Tag, ermittelt aus den bereits im Registry
# vorhandenen Versionen des Meta-Pakets (edgeguard) mit dem heutigen
# Datumspräfix. Erster Release des Tages -> .01.
next_testing_version() {
local today
today="$(date -u +%Y.%m.%d)"
local raw
raw="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/packages/$OWNER?type=debian&q=edgeguard&limit=1000" 2>/dev/null || echo '[]')"
local last_n
last_n="$(printf '%s' "$raw" | jq -r --arg n "edgeguard" --arg pfx "${today}." \
'.[] | select(.name==$n) | .version | select(startswith($pfx))' \
| sed "s/^${today}\.//" | sort -n | tail -1)"
printf '%s.%02d' "$today" "$(( ${last_n:-0} + 1 ))"
}
# ─── Stable-Version: Patch-Bump der VERSION-Datei (Default) ────────────
bump_patch() {
local major minor patch
IFS='.' read -r major minor patch <<< "$1"
echo "${major}.${minor}.$((patch + 1))"
}
# ─── Alte Version im Ziel-Kanal ersetzen — Upload derselben Nummer würde
# sonst mit 409 den alten Inhalt behalten (Gitea dedupliziert (Name,
# Version,Arch,Kanal-Pool) — best-effort, 404 ist der Normalfall). ──────
delete_before_upload() {
local channel="$1" version="$2" name="$3" arch="$4"
local url="$GITEA_URL/api/packages/$OWNER/debian/pool/trixie/$channel/$name/$version/$arch"
curl -sS -o /dev/null -w '' -X DELETE -H "Authorization: token $GITEA_TOKEN" "$url" || true
}
replace_in_channel() {
local channel="$1" version="$2"
delete_before_upload "$channel" "$version" edgeguard-api amd64
delete_before_upload "$channel" "$version" edgeguard-api arm64
delete_before_upload "$channel" "$version" edgeguard-ui all
delete_before_upload "$channel" "$version" edgeguard all
}
# ─── verify_channel_debs — Health-Gate wie enconf verify_stable_debs ───
# Holt den Packages-Index eines Kanals und prüft für JEDE dort
# referenzierte .deb, ob sie unter ihrer Pool-URL wirklich mit HTTP 200
# abrufbar ist (nicht nur ob der Index existiert — Gitea regeneriert den
# Index nicht immer zuverlässig nach einem Delete).
verify_channel_debs() {
local channel="$1"
log "verify_channel_debs($channel): jede indexierte .deb muss abrufbar sein..."
local files f code bad=0 checked=0
files="$(curl -sk -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/packages/$OWNER/debian/dists/trixie/$channel/binary-amd64/Packages" 2>/dev/null \
| awk '/^Filename:/{print $2}')"
if [ -z "$files" ]; then
warn "$channel: Packages-Index leer/nicht erreichbar — übersprungen"
return 0
fi
while IFS= read -r f; do
[ -z "$f" ] && continue
checked=$((checked + 1))
code="$(curl -sk -o /dev/null -w '%{http_code}' \
-H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/packages/$OWNER/debian/$f")"
if [ "$code" != "200" ]; then
warn "$channel: $f -> HTTP $code"
bad=$((bad + 1))
fi
done <<< "$files"
[ "$bad" -eq 0 ] || abort "$channel: $bad von $checked .deb(s) im Index nicht abrufbar — Registry inkonsistent."
log "$channel: $checked .deb(s) verifiziert, alle abrufbar."
}
# ─── Testing-Push ───────────────────────────────────────────────────────
testing_push() {
local new_version
new_version="$(next_testing_version)"
log "[testing 1/3] Gates + Build $new_version (make deb — release-check, amd64+arm64+ui)..."
make VERSION="$new_version" deb
log "[testing 2/3] Alte Version im testing-Kanal ersetzen + hochladen..."
replace_in_channel testing "$new_version"
./scripts/apt-repo/publish.sh "$new_version" amd64 testing
./scripts/apt-repo/publish.sh "$new_version" arm64 testing
log "[testing 3/3] Cleanup (keep last $KEEP) + Verify..."
KEEP="$KEEP" ./scripts/apt-repo/cleanup-old.sh testing
verify_channel_debs testing
log "✅ Testing-Release $new_version veröffentlicht."
}
# ─── Stable-Promotion ───────────────────────────────────────────────────
promote_stable() {
local explicit_version="${1:-}"
local current_version new_version
current_version="$(cat "$REPO_DIR/VERSION")"
new_version="${explicit_version:-$(bump_patch "$current_version")}"
git -C "$REPO_DIR" rev-parse "v$new_version" >/dev/null 2>&1 && \
abort "Git-Tag v$new_version existiert bereits."
[ -n "$(git -C "$REPO_DIR" status --porcelain)" ] && \
abort "Working Tree nicht sauber — commit/stash erst, dann Stable-Release."
log "[stable 1/4] Gates + Build $new_version (make deb — release-check, amd64+arm64+ui)..."
make VERSION="$new_version" deb
log "[stable 2/4] Alte Version im stable-Kanal ersetzen + hochladen..."
replace_in_channel stable "$new_version"
./scripts/apt-repo/publish.sh "$new_version" amd64 stable
./scripts/apt-repo/publish.sh "$new_version" arm64 stable
log "[stable 3/4] VERSION-Datei setzen + Commit + Git-Tag..."
printf '%s' "$new_version" > "$REPO_DIR/VERSION"
git -C "$REPO_DIR" add VERSION
git -C "$REPO_DIR" commit -m "chore(release): v$new_version stable"
git -C "$REPO_DIR" tag "v$new_version"
log "[stable 4/4] Cleanup (keep last $KEEP) + Verify..."
KEEP="$KEEP" ./scripts/apt-repo/cleanup-old.sh stable
verify_channel_debs stable
log "✅ Stable-Release v$new_version fertig."
log " Push nicht vergessen: git push origin main --tags"
}
case "${1:-}" in
stable) promote_stable "${2:-}" ;;
"") testing_push ;;
*) abort "unbekanntes Argument '$1' (erwartet: 'stable' oder kein Argument)" ;;
esac