Rebuild on kernel changes and on age, not just on pve-manager
The build condition only compared pve-manager, which left two gaps. Kernel security fixes ship in proxmox-default-kernel and do not bump pve-manager, so the updates that matter most would not have triggered a rebuild. Debian base security updates bump neither, so an image could have sat unchanged indefinitely while its openssl and glibc went stale. scripts/ci/decide-build.sh now rebuilds when pve-manager changes, when proxmox-default-kernel changes, or when the newest release passes MAX_AGE_DAYS (30). It compares against image-info.txt from the last release rather than inferring from tag names, so the comparison reflects what is actually inside the published image, and it can be run by hand to see the decision without triggering anything. The daily schedule stays. Measured from the trixie repository, Proxmox publishes about weekly — 56 pve-manager and 28 proxmox-kernel versions since 9.0 — so daily checking costs about 30 seconds on the days nothing changed and cuts worst-case staleness from a week to a day. Tags carry the date now (pve-<version>-<date>) because an age-triggered rebuild can repeat a version. The README gains a "Prebuilt images" section linking the releases page, with the checksum and MAAS upload commands, and a "Release automation" section explaining the trigger table and stating plainly that the host-mode runner gives root on the build machine to anything that can dispatch a workflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,9 @@ jobs:
|
|||||||
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
KEEP_RELEASES: '3'
|
KEEP_RELEASES: '3'
|
||||||
|
# Rebuild even without a Proxmox version change once an image reaches this
|
||||||
|
# age, so Debian base security updates make it into the image.
|
||||||
|
MAX_AGE_DAYS: '30'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
@@ -58,29 +61,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
cd "$GITHUB_WORKSPACE"
|
cd "$GITHUB_WORKSPACE"
|
||||||
|
./scripts/ci/decide-build.sh "$MAX_AGE_DAYS" > "$GITHUB_OUTPUT"
|
||||||
UPSTREAM=$(./scripts/ci/upstream-version.sh)
|
|
||||||
TAG="pve-${UPSTREAM}"
|
|
||||||
echo "upstream pve-manager: ${UPSTREAM}"
|
|
||||||
|
|
||||||
PUBLISHED=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${GITEA_API}/releases?limit=1" \
|
|
||||||
| python3 -c 'import json,sys; r=json.load(sys.stdin); print(r[0]["tag_name"] if r else "")')
|
|
||||||
echo "latest release: ${PUBLISHED:-<none>}"
|
|
||||||
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "version=${UPSTREAM}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
if [ "${{ inputs.force }}" = "true" ]; then
|
if [ "${{ inputs.force }}" = "true" ]; then
|
||||||
echo "forced by manual trigger"
|
echo "forced by manual trigger"
|
||||||
echo "build=yes" >> "$GITHUB_OUTPUT"
|
sed -i 's/^build=no$/build=yes/' "$GITHUB_OUTPUT"
|
||||||
elif [ "$PUBLISHED" = "$TAG" ]; then
|
|
||||||
echo "${TAG} is already published — nothing to do"
|
|
||||||
echo "build=no" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "new version — building"
|
|
||||||
echo "build=yes" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
fi
|
||||||
|
cat "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: steps.decide.outputs.build == 'yes'
|
if: steps.decide.outputs.build == 'yes'
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -14,6 +14,7 @@ variable change away (see [Moving to a new Proxmox release](#moving-to-a-new-pro
|
|||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- [What this does](#what-this-does)
|
- [What this does](#what-this-does)
|
||||||
|
- [Prebuilt images](#prebuilt-images)
|
||||||
- [Why Debian + the Proxmox repository, and not the Proxmox ISO](#why-debian--the-proxmox-repository-and-not-the-proxmox-iso)
|
- [Why Debian + the Proxmox repository, and not the Proxmox ISO](#why-debian--the-proxmox-repository-and-not-the-proxmox-iso)
|
||||||
- [How it works](#how-it-works)
|
- [How it works](#how-it-works)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
@@ -27,6 +28,7 @@ variable change away (see [Moving to a new Proxmox release](#moving-to-a-new-pro
|
|||||||
- [Four traps this image works around](#four-traps-this-image-works-around)
|
- [Four traps this image works around](#four-traps-this-image-works-around)
|
||||||
- [Moving to a new Proxmox release](#moving-to-a-new-proxmox-release)
|
- [Moving to a new Proxmox release](#moving-to-a-new-proxmox-release)
|
||||||
- [Build performance](#build-performance)
|
- [Build performance](#build-performance)
|
||||||
|
- [Release automation](#release-automation)
|
||||||
- [Repository layout](#repository-layout)
|
- [Repository layout](#repository-layout)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Verified status](#verified-status)
|
- [Verified status](#verified-status)
|
||||||
@@ -163,6 +165,33 @@ fails is retried on the next boot rather than leaving the node half-configured.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Prebuilt images
|
||||||
|
|
||||||
|
Images are built and published automatically:
|
||||||
|
|
||||||
|
**→ [Download the latest image](https://gitea.mynodes.xyz/ilker/maas-proxmox/releases)**
|
||||||
|
|
||||||
|
Each release carries the image, its SHA-256 checksum, the build's metadata
|
||||||
|
(`image-info.txt`) and `SOURCES.md`, the corresponding-source offer required by the
|
||||||
|
licences of the packages inside it. The builds are **unofficial** and not affiliated
|
||||||
|
with Proxmox Server Solutions GmbH.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -LO https://gitea.mynodes.xyz/ilker/maas-proxmox/releases/download/<tag>/maas-image-pve-<version>-amd64.tar.gz
|
||||||
|
curl -LO https://gitea.mynodes.xyz/ilker/maas-proxmox/releases/download/<tag>/maas-image-pve-<version>-amd64.tar.gz.sha256
|
||||||
|
sha256sum -c maas-image-pve-<version>-amd64.tar.gz.sha256
|
||||||
|
|
||||||
|
maas $PROFILE boot-resources create name='custom/proxmox-ve-9' \
|
||||||
|
title='Proxmox VE 9' architecture='amd64/generic' \
|
||||||
|
filetype='tgz' content@=maas-image-pve-<version>-amd64.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
You still need the curtin preseed on your MAAS region controller — see
|
||||||
|
[Quick start](#quick-start) step 5. Downloading an image skips only steps 1-3.
|
||||||
|
|
||||||
|
Prefer building it yourself if you would rather not trust someone else's binary;
|
||||||
|
that is what the rest of this document is about.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -722,6 +751,45 @@ serve them; package signatures are still verified.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Release automation
|
||||||
|
|
||||||
|
[`.gitea/workflows/build-image.yml`](.gitea/workflows/build-image.yml) builds the image
|
||||||
|
on a self-hosted runner and publishes it as a release.
|
||||||
|
|
||||||
|
It runs **daily**, but rebuilds only when there is a reason to. Proxmox publishes
|
||||||
|
roughly weekly — the trixie repository currently holds 56 `pve-manager` and 28
|
||||||
|
`proxmox-kernel` versions — so an unconditional daily build would produce about
|
||||||
|
45 GB a month of near-identical artifacts. A check that finds nothing costs about
|
||||||
|
30 seconds.
|
||||||
|
|
||||||
|
A rebuild is triggered when any of these holds:
|
||||||
|
|
||||||
|
| Trigger | Why |
|
||||||
|
|---|---|
|
||||||
|
| `pve-manager` differs from the published image | The obvious one |
|
||||||
|
| `proxmox-default-kernel` differs | Kernel security fixes do not bump `pve-manager`, and those matter most |
|
||||||
|
| The newest release is older than `MAX_AGE_DAYS` (30) | Debian base security updates bump neither of the above; without a floor an image could sit unchanged for months |
|
||||||
|
| Manual dispatch with `force` | Escape hatch |
|
||||||
|
|
||||||
|
The comparison reads `image-info.txt` from the last release rather than guessing from
|
||||||
|
tag names, so it reflects what is actually inside the published image.
|
||||||
|
[`scripts/ci/decide-build.sh`](scripts/ci/decide-build.sh) can be run by hand to see the
|
||||||
|
decision without triggering anything.
|
||||||
|
|
||||||
|
Releases are tagged `pve-<version>-<date>` and pruned to the newest
|
||||||
|
`KEEP_RELEASES` (3) — at ~1.5 GB each, unbounded retention fills the server.
|
||||||
|
|
||||||
|
### Runner
|
||||||
|
|
||||||
|
The runner is registered in **host mode**: steps run directly on the build machine as
|
||||||
|
root, because the build needs `/dev/kvm`, `qemu-nbd`, FUSE and root privileges, all of
|
||||||
|
which a container would have to be granted anyway. The consequence is worth stating
|
||||||
|
plainly: anything able to dispatch a workflow in this repository gets root on the build
|
||||||
|
machine. Do not attach this runner to a repository that accepts outside contributions.
|
||||||
|
|
||||||
|
Checkout is a plain `git clone`, not `actions/checkout` — the latter is a JavaScript
|
||||||
|
action and a host-mode runner has no Node.js runtime.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -730,6 +798,8 @@ scripts/install-deps.sh build host dependencies
|
|||||||
scripts/customize-proxmox.sh.in template for the script that runs inside the build VM
|
scripts/customize-proxmox.sh.in template for the script that runs inside the build VM
|
||||||
scripts/deploy-cluster.sh deploy a whole cluster through MAAS
|
scripts/deploy-cluster.sh deploy a whole cluster through MAAS
|
||||||
scripts/verify-image.sh check a built image's contents
|
scripts/verify-image.sh check a built image's contents
|
||||||
|
scripts/ci/ release pipeline helpers (decision, artifacts, publish, prune)
|
||||||
|
.gitea/workflows/ Gitea Actions pipeline
|
||||||
overlay/ files baked into the image
|
overlay/ files baked into the image
|
||||||
usr/local/sbin/pve-maas-init first-boot state machine
|
usr/local/sbin/pve-maas-init first-boot state machine
|
||||||
etc/pve-maas/pve-maas.conf defaults, with every option documented
|
etc/pve-maas/pve-maas.conf defaults, with every option documented
|
||||||
|
|||||||
96
scripts/ci/decide-build.sh
Executable file
96
scripts/ci/decide-build.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Copyright (C) 2026 Ilker Manap
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Decides whether a rebuild is worth doing, and prints the decision as
|
||||||
|
# key=value lines suitable for $GITHUB_OUTPUT.
|
||||||
|
#
|
||||||
|
# decide-build.sh [max-age-days] (default 30)
|
||||||
|
#
|
||||||
|
# A rebuild happens when any of these is true:
|
||||||
|
#
|
||||||
|
# 1. pve-manager in the repository differs from the published image
|
||||||
|
# 2. proxmox-default-kernel differs — kernel security fixes do not bump
|
||||||
|
# pve-manager, and those are the ones that matter most
|
||||||
|
# 3. the newest release is older than max-age-days — Debian base security
|
||||||
|
# updates bump neither of the above, so without a floor an image could
|
||||||
|
# sit unchanged for months
|
||||||
|
#
|
||||||
|
# Requires GITEA_API and GITEA_TOKEN in the environment.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
MAX_AGE_DAYS="${1:-30}"
|
||||||
|
: "${GITEA_API:?GITEA_API required}"
|
||||||
|
: "${GITEA_TOKEN:?GITEA_TOKEN required}"
|
||||||
|
|
||||||
|
SUITE=$(make -s print-var VAR=DEBIAN_SERIES)
|
||||||
|
COMP=$(make -s print-var VAR=PVE_REPO)
|
||||||
|
URI=$(make -s print-var VAR=PVE_REPO_URI)
|
||||||
|
ARCH=$(make -s print-var VAR=ARCH)
|
||||||
|
|
||||||
|
# Newest version of each package we care about, in one pass over the index.
|
||||||
|
PKGS=$(curl -fsS "${URI}/dists/${SUITE}/${COMP}/binary-${ARCH}/Packages.gz" | gunzip)
|
||||||
|
newest() {
|
||||||
|
printf '%s\n' "$PKGS" \
|
||||||
|
| awk -v want="$1" '$1=="Package:" && $2==want {p=1; next} p && $1=="Version:" {print $2; p=0}' \
|
||||||
|
| sort -V | tail -1
|
||||||
|
}
|
||||||
|
UP_PVE=$(newest pve-manager)
|
||||||
|
UP_KERNEL=$(newest proxmox-default-kernel)
|
||||||
|
[ -n "$UP_PVE" ] || { echo "pve-manager not found in ${URI} ${SUITE}/${COMP}" >&2; exit 1; }
|
||||||
|
[ -n "$UP_KERNEL" ] || { echo "proxmox-default-kernel not found" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "upstream: pve-manager=${UP_PVE} proxmox-default-kernel=${UP_KERNEL}" >&2
|
||||||
|
|
||||||
|
# What the newest published release actually contains. image-info.txt is a few
|
||||||
|
# hundred bytes and is published with every release, so this needs no guessing
|
||||||
|
# from tag names.
|
||||||
|
LATEST=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API}/releases?limit=1")
|
||||||
|
INFO_URL=$(printf '%s' "$LATEST" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
r = json.load(sys.stdin)
|
||||||
|
if r:
|
||||||
|
for a in r[0].get("assets", []):
|
||||||
|
if a["name"] == "image-info.txt":
|
||||||
|
print(a["browser_download_url"]); break
|
||||||
|
')
|
||||||
|
CREATED=$(printf '%s' "$LATEST" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
r = json.load(sys.stdin); print(r[0]["created_at"] if r else "")')
|
||||||
|
|
||||||
|
PREV_PVE="" PREV_KERNEL=""
|
||||||
|
if [ -n "$INFO_URL" ]; then
|
||||||
|
INFO=$(curl -fsSL "$INFO_URL" || true)
|
||||||
|
PREV_PVE=$(printf '%s' "$INFO" | awk -F= '$1=="pve-manager"{print $2}')
|
||||||
|
PREV_KERNEL=$(printf '%s' "$INFO" | awk -F= '$1=="proxmox-default-kernel"{print $2}')
|
||||||
|
fi
|
||||||
|
echo "published: pve-manager=${PREV_PVE:-<none>} proxmox-default-kernel=${PREV_KERNEL:-<none>}" >&2
|
||||||
|
|
||||||
|
AGE_DAYS=99999
|
||||||
|
if [ -n "$CREATED" ]; then
|
||||||
|
AGE_DAYS=$(CREATED="$CREATED" python3 -c '
|
||||||
|
import datetime, os
|
||||||
|
c = datetime.datetime.fromisoformat(os.environ["CREATED"].replace("Z", "+00:00"))
|
||||||
|
print((datetime.datetime.now(datetime.timezone.utc) - c).days)')
|
||||||
|
echo "newest release is ${AGE_DAYS} day(s) old" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUILD=no
|
||||||
|
REASON="up to date"
|
||||||
|
if [ -z "$PREV_PVE" ]; then
|
||||||
|
BUILD=yes; REASON="no published image yet"
|
||||||
|
elif [ "$UP_PVE" != "$PREV_PVE" ]; then
|
||||||
|
BUILD=yes; REASON="pve-manager ${PREV_PVE} -> ${UP_PVE}"
|
||||||
|
elif [ "$UP_KERNEL" != "$PREV_KERNEL" ]; then
|
||||||
|
BUILD=yes; REASON="kernel ${PREV_KERNEL} -> ${UP_KERNEL}"
|
||||||
|
elif [ "$AGE_DAYS" -ge "$MAX_AGE_DAYS" ]; then
|
||||||
|
BUILD=yes; REASON="image is ${AGE_DAYS} days old (limit ${MAX_AGE_DAYS}) — picking up Debian updates"
|
||||||
|
fi
|
||||||
|
echo "decision: ${BUILD} (${REASON})" >&2
|
||||||
|
|
||||||
|
printf 'build=%s\n' "$BUILD"
|
||||||
|
printf 'tag=%s\n' "pve-${UP_PVE}-$(date -u +%Y%m%d)"
|
||||||
|
printf 'version=%s\n' "$UP_PVE"
|
||||||
|
printf 'kernel=%s\n' "$UP_KERNEL"
|
||||||
|
printf 'reason=%s\n' "$REASON"
|
||||||
Reference in New Issue
Block a user