Workflow: automatic testing of patches (#191)

* First migrate

* weekly patch test

* fetch proxmox files

* add test mods

---------

Co-authored-by: Meliox <na>
This commit is contained in:
MelioxandMeliox authored and GitHub committed 2026-06-10 20:08:04 +02:00
1 parent 0c25030a21
commit f91125ec9f
4 files changed
+415

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# src/Scripts/test/fetch-proxmox-files.sh
#
# Downloads the upstream Proxmox VE files that pve-mod patches and installs them
# at their real system paths, so the patch engine can be exercised on a plain
# (non-Proxmox) CI runner.
#
# Pulls pve-manager and proxmox-widget-toolkit from the Proxmox no-subscription
# repository (default: PVE 9 / Debian trixie) and extracts the target files.
#
# CI-only helper. The repo is added with [trusted=yes]: we are only fetching
# public, unmodified UI files to patch against, not installing Proxmox, so
# managing the signing key would add nothing.
set -euo pipefail
SUITE="${PVE_SUITE:-trixie}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
# Files provided by the two packages, relative to the filesystem root. These are
# exactly the paths the .patch files (and the nag_screen post-apply hook) touch.
FILES=(
usr/share/perl5/PVE/API2/Nodes.pm
usr/share/pve-manager/js/pvemanagerlib.js
usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js
)
echo "[fetch] Configuring Proxmox no-subscription repo ($SUITE)..."
echo "deb [trusted=yes] http://download.proxmox.com/debian/pve $SUITE pve-no-subscription" \
| sudo tee /etc/apt/sources.list.d/pve-test.list >/dev/null
sudo apt-get update -qq
echo "[fetch] Downloading pve-manager and proxmox-widget-toolkit..."
cd "$WORKDIR"
apt-get download pve-manager proxmox-widget-toolkit
for deb in *.deb; do
echo "[fetch] Extracting $deb"
dpkg-deb -x "$deb" extract
done
for rel in "${FILES[@]}"; do
src="extract/$rel"
dst="/$rel"
if [[ ! -f "$src" ]]; then
echo "[fetch] ERROR: expected file not found in packages: $rel" >&2
exit 1
fi
sudo install -Dm644 "$src" "$dst"
echo "[fetch] Installed $dst"
done
echo "[fetch] Done."
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# src/Scripts/test/test-mods.sh
#
# Exercises pve-mod's patch engine against the installed Proxmox files.
#
# Usage: test-mods.sh <mod> | all
#
# Mods are auto-discovered from the [modules] section of the main config, so
# adding a new mod (a new [modules] key plus a patches/<mod>/ directory) is
# picked up automatically - no edits to this script are required.
#
# For each target mod it:
# 1. enables only that mod in the main config
# 2. turns on every conditional flag referenced by the mod's patches.list,
# so all of the mod's patches are exercised (e.g. node_info's gpu_history)
# 3. runs apply-patches.sh and asserts it exits 0
# 4. runs revert-patches.sh and asserts it reports no unclean reversions
# 5. restores the pristine Proxmox files before moving to the next mod
#
# Failure detection relies on the apply exit code: the patch engine performs an
# atomic preflight dry-run and exits non-zero if any mod cannot apply cleanly.
#
# Must run as root (it edits /etc/pve-mod and the patched system files).
set -u
MAIN_CONF="${PVE_MOD_MAIN_CONF:-/etc/pve-mod/pve-mod.conf}"
CONFD_DIR="${PVE_MOD_CONFD_DIR:-/etc/pve-mod/conf.d}"
PATCHES_DIR="${PVE_MOD_PATCHES_DIR:-/usr/lib/pve-mod/patches}"
APPLY="${PVE_MOD_APPLY:-/usr/lib/pve-mod/apply-patches.sh}"
REVERT="${PVE_MOD_REVERT:-/usr/lib/pve-mod/revert-patches.sh}"
# Proxmox files touched by the patches; snapshotted before and restored after
# each mod so every mod is tested from a pristine baseline.
PROXMOX_FILES=(
/usr/share/perl5/PVE/API2/Nodes.pm
/usr/share/pve-manager/js/pvemanagerlib.js
/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js
)
SNAP_DIR="$(mktemp -d)"
trap 'rm -rf "$SNAP_DIR"' EXIT
info() { echo "[test] $*"; }
warn() { echo "[test] WARNING: $*" >&2; }
# List the keys of the [modules] section in the main config, one per line.
list_modules() {
[[ -f "$MAIN_CONF" ]] || return 0
awk -F= '
/^\[/ { in_sec = ($0 == "[modules]") }
in_sec && /^[^#=]+=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
print $1
}
' "$MAIN_CONF"
}
# set_conf <file> <section> <key> <value>: set an existing key within a section.
set_conf() {
local file="$1" section="$2" key="$3" value="$4"
[[ -f "$file" ]] || { warn "config not found: $file"; return 1; }
awk -v sec="[$section]" -v k="$key" -v v="$value" '
/^\[/ { in_sec = ($0 == sec) }
{
if (in_sec && $0 ~ "^[[:space:]]*"k"[[:space:]]*=") {
print k"="v
} else {
print
}
}
' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
}
# Disable every discovered mod in the main config.
disable_all_modules() {
local m
for m in $(list_modules); do
set_conf "$MAIN_CONF" modules "$m" 0
done
}
# Turn on every conditional flag a mod's patches.list references, so all of its
# patches become active. Conditions look like: <patch-file> section.key=value
enable_conditions() {
local mod="$1"
local manifest="$PATCHES_DIR/$mod/patches.list"
local mod_conf="$CONFD_DIR/$mod.conf"
[[ -f "$manifest" ]] || return 0
local line cond local_key want sect ckey
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
[[ -z "$line" ]] && continue
# Skip lines without a condition (no whitespace after the patch name).
[[ "$line" == *[[:space:]]* ]] || continue
cond="$(echo "${line#*[[:space:]]}" | sed -E 's/^[[:space:]]+//')"
[[ "$cond" == *=* ]] || continue
local_key="${cond%%=*}"
want="${cond#*=}"
sect="${local_key%%.*}"
ckey="${local_key#*.}"
set_conf "$mod_conf" "$sect" "$ckey" "$want" \
&& info " enabled condition $local_key=$want in $(basename "$mod_conf")"
done < "$manifest"
}
snapshot_files() {
rm -rf "$SNAP_DIR"/*; mkdir -p "$SNAP_DIR"
local i=0 f
for f in "${PROXMOX_FILES[@]}"; do
if [[ -e "$f" || -L "$f" ]]; then
cp -a "$f" "$SNAP_DIR/$i"
fi
i=$((i+1))
done
}
restore_files() {
local i=0 f
for f in "${PROXMOX_FILES[@]}"; do
if [[ -e "$SNAP_DIR/$i" || -L "$SNAP_DIR/$i" ]]; then
rm -f "$f"
cp -a "$SNAP_DIR/$i" "$f"
fi
i=$((i+1))
done
}
# Test a single mod end to end. Returns 0 on success, 1 on failure.
test_one_mod() {
local mod="$1"
echo "::group::Testing mod: $mod"
local ok=0
if [[ ! -f "$PATCHES_DIR/$mod/patches.list" ]]; then
warn "no patch manifest for mod '$mod' ($PATCHES_DIR/$mod/patches.list)"
echo "::endgroup::"
return 1
fi
disable_all_modules
set_conf "$MAIN_CONF" modules "$mod" 1
enable_conditions "$mod"
snapshot_files
info "Applying patches for '$mod'..."
local apply_log apply_rc
apply_log="$("$APPLY" 2>&1)"; apply_rc=$?
echo "$apply_log"
if [[ $apply_rc -ne 0 ]]; then
warn "apply-patches.sh failed for mod '$mod' (exit $apply_rc)"
ok=1
fi
if [[ $ok -eq 0 ]]; then
info "Reverting patches for '$mod'..."
local revert_log
revert_log="$("$REVERT" 2>&1)"
echo "$revert_log"
if echo "$revert_log" | grep -qE "could not be reverted cleanly|reported an error"; then
warn "revert-patches.sh reported an unclean revert for mod '$mod'"
ok=1
fi
fi
restore_files
if [[ $ok -eq 0 ]]; then
info "PASS: $mod"
else
warn "FAIL: $mod"
fi
echo "::endgroup::"
return $ok
}
# ── main ──────────────────────────────────────────────────────────────────────
[[ $# -eq 1 ]] || { echo "Usage: $0 <mod>|all" >&2; exit 2; }
if ! command -v patch >/dev/null 2>&1; then
warn "'patch' command not found; install the 'patch' package."
exit 1
fi
targets=()
if [[ "$1" == "all" ]]; then
mapfile -t targets < <(list_modules)
[[ ${#targets[@]} -gt 0 ]] || { warn "no mods found in [modules] of $MAIN_CONF"; exit 1; }
else
targets=("$1")
fi
failed=()
for mod in "${targets[@]}"; do
test_one_mod "$mod" || failed+=("$mod")
done
echo ""
if [[ ${#failed[@]} -gt 0 ]]; then
warn "Failed mods: ${failed[*]}"
exit 1
fi
info "All tested mods passed: ${targets[*]}"
exit 0