From e964933af15d5fa893c6b781d066fcc75b76558a Mon Sep 17 00:00:00 2001 From: ilkermanap Date: Fri, 4 Sep 2026 22:35:30 +0200 Subject: [PATCH] Add a Gitea Actions pipeline that publishes the image as a release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the image on a self-hosted runner and publishes it, with the checksum and a corresponding-source offer, as a Gitea release. The schedule is daily but the build is conditional. Proxmox does not ship daily, so an unconditional daily build would produce roughly 45 GB a month of near-identical artifacts; instead the job compares the newest pve-manager in the configured repository against the last published release and stops early when they match. A manual trigger with `force` rebuilds anyway. Releases are tagged after the version they contain (`pve-9.2.11`) rather than the date, so the tag says something useful, and older ones are pruned to keep three. The logic lives in scripts/ci/ rather than inline in the workflow. Shell inside a YAML block scalar cannot carry an indented heredoc terminator, and the release body needs several; scripts also mean the pieces can be run and tested by hand. A `print-var` target exposes single Makefile variables to them. Two constraints shaped this: * The runner is registered in host mode, so steps run directly on the build machine as root. The build needs /dev/kvm, qemu-nbd, FUSE and root, which a container would have to be given anyway. The consequence — anything able to dispatch a workflow gets root on that machine — is stated in the workflow header rather than left implicit. * Checkout is a plain git clone. actions/checkout is a JavaScript action and the host-mode runner has no Node.js runtime. The artifact is named maas-image-pve--amd64.tar.gz, not proxmox-ve-*.tar.gz, and the release body says the build is unofficial and unaffiliated. Proxmox permits redistribution under the AGPLv3 but asks that the trademark not be used in product names. SOURCES.md is generated per release: the image is an unmodified installation of Debian and Proxmox packages, so it points at those archives for the corresponding source, and records that the firmware licence texts ship inside the image at /usr/share/doc/pve-firmware/licenses/ and must not be stripped. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-image.yml | 124 +++++++++++++++++++++++++++++++ Makefile | 7 +- scripts/ci/assemble-artifacts.sh | 65 ++++++++++++++++ scripts/ci/prune-releases.sh | 29 ++++++++ scripts/ci/publish-release.sh | 75 +++++++++++++++++++ scripts/ci/upstream-version.sh | 21 ++++++ 6 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/build-image.yml create mode 100755 scripts/ci/assemble-artifacts.sh create mode 100755 scripts/ci/prune-releases.sh create mode 100755 scripts/ci/publish-release.sh create mode 100755 scripts/ci/upstream-version.sh diff --git a/.gitea/workflows/build-image.yml b/.gitea/workflows/build-image.yml new file mode 100644 index 0000000..800afd2 --- /dev/null +++ b/.gitea/workflows/build-image.yml @@ -0,0 +1,124 @@ +# Copyright (C) 2026 Ilker Manap +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Builds the MAAS image and publishes it as a Gitea release. +# +# Runs daily, but only *builds* when the Proxmox repository actually carries a +# newer pve-manager than the last published release. Proxmox does not ship daily, +# so an unconditional daily build would produce ~45 GB a month of near-identical +# artifacts for nothing. Trigger manually with `force` to rebuild anyway. +# +# 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. +# Anything that can dispatch a workflow here therefore has root on that machine. + +name: build-image + +on: + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + inputs: + force: + description: 'Build even if the version has not changed' + type: boolean + default: false + +concurrency: + group: build-image + cancel-in-progress: false + +jobs: + build: + runs-on: maas-builder + timeout-minutes: 120 + + env: + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + KEEP_RELEASES: '3' + + steps: + - name: Check out + # A plain clone rather than actions/checkout: the runner is in host mode + # and has no Node.js runtime for JavaScript actions. + run: | + set -eux + rm -rf "$GITHUB_WORKSPACE" + git clone --depth 1 --branch "$GITHUB_REF_NAME" \ + "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$GITHUB_WORKSPACE" + cd "$GITHUB_WORKSPACE" && git log --oneline -1 + + - name: Decide whether to build + id: decide + run: | + set -eu + cd "$GITHUB_WORKSPACE" + + 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:-}" + + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${UPSTREAM}" >> "$GITHUB_OUTPUT" + + if [ "${{ inputs.force }}" = "true" ]; then + echo "forced by manual trigger" + echo "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 + + - name: Build + if: steps.decide.outputs.build == 'yes' + run: | + set -eux + cd "$GITHUB_WORKSPACE" + make image + + - name: Verify + if: steps.decide.outputs.build == 'yes' + run: | + set -eux + cd "$GITHUB_WORKSPACE" + make verify + + - name: Assemble release artifacts + if: steps.decide.outputs.build == 'yes' + run: | + set -eu + cd "$GITHUB_WORKSPACE" + ./scripts/ci/assemble-artifacts.sh "${{ steps.decide.outputs.version }}" + + - name: Publish + if: steps.decide.outputs.build == 'yes' + run: | + set -eu + cd "$GITHUB_WORKSPACE" + ./scripts/ci/publish-release.sh \ + "${{ steps.decide.outputs.tag }}" \ + "${{ steps.decide.outputs.version }}" \ + dist + + - name: Prune old releases + if: steps.decide.outputs.build == 'yes' + run: | + set -eu + cd "$GITHUB_WORKSPACE" + ./scripts/ci/prune-releases.sh "$KEEP_RELEASES" + + - name: Clean up + if: always() + run: | + # A 1.5 GB artifact per run would fill the builder otherwise. + rm -rf "$GITHUB_WORKSPACE/dist" "$GITHUB_WORKSPACE/build" || true + df -h / | tail -1 diff --git a/Makefile b/Makefile index 826ca77..95b711e 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ OVMF_DIR ?= /usr/share/OVMF OVMF_SFX ?= $(shell test -f $(OVMF_DIR)/OVMF_CODE.fd && echo "" || echo "_4M") # ---------------------------------------------------------------- hedefler -.PHONY: help deps deps-cache check-upstream checkout overlay customize image verify preseed install-preseed upload clean distclean lint +.PHONY: help deps deps-cache check-upstream print-var checkout overlay customize image verify preseed install-preseed upload clean distclean lint help: @echo "maas-proxmox - Proxmox VE $(PVE_VERSION) MAAS imaji" @@ -239,6 +239,11 @@ deps-cache: @echo " sudo make image APT_PROXY=http://10.0.2.2:3142" @echo "(10.0.2.2 = packer user-mode aginda build host'un adresi)" +# Tek bir degiskenin degerini bas - CI script'leri bunu kullanir. +# make -s print-var VAR=OUTPUT +print-var: + @echo "$($(VAR))" + lint: @bash -n scripts/customize-proxmox.sh.in && echo "customize-proxmox.sh.in: OK" @bash -n overlay/usr/local/sbin/pve-maas-init && echo "pve-maas-init: OK" diff --git a/scripts/ci/assemble-artifacts.sh b/scripts/ci/assemble-artifacts.sh new file mode 100755 index 0000000..f165996 --- /dev/null +++ b/scripts/ci/assemble-artifacts.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Copyright (C) 2026 Ilker Manap +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Moves the built image into dist/ under a release-friendly name and writes the +# checksum plus the corresponding-source offer that has to accompany a binary +# distribution of GPL/AGPL software. +# +# assemble-artifacts.sh +set -euo pipefail +cd "$(dirname "$0")/../.." + +VERSION="${1:?pve-manager version required}" +OUT=$(make -s print-var VAR=OUTPUT) +ARCH=$(make -s print-var VAR=ARCH) +[ -f "$OUT" ] || { echo "built image not found: $OUT" >&2; exit 1; } + +# Deliberately not named "proxmox-ve-*": that would look like an official +# Proxmox artifact. Proxmox asks that their trademark not be used in product +# names, so the image is named after what it is - a MAAS image. +NAME="maas-image-pve-${VERSION}-${ARCH}.tar.gz" + +rm -rf dist && mkdir -p dist +mv "$OUT" "dist/${NAME}" +( cd dist && sha256sum "$NAME" > "${NAME}.sha256" ) + +tar xzf "dist/${NAME}" -O ./etc/pve-maas/image-info > dist/image-info.txt 2>/dev/null \ + || echo "(this image predates /etc/pve-maas/image-info)" > dist/image-info.txt + +DEB_VER=$(make -s print-var VAR=DEBIAN_VERSION) +DEB_SUITE=$(make -s print-var VAR=DEBIAN_SERIES) +PVE_REPO=$(make -s print-var VAR=PVE_REPO) +PVE_URI=$(make -s print-var VAR=PVE_REPO_URI) +REPO_URL="${GITHUB_SERVER_URL:-}/${GITHUB_REPOSITORY:-}" + +{ + echo "# Corresponding source" + echo + echo "This image is an unmodified installation of packages from the archives listed" + echo "below. No package was patched. What is ours is the selection and the" + echo "configuration, and that is the entire content of the repository linked below." + echo + echo "Written offer, per GPLv2 section 3 and GPLv3 section 6: the complete" + echo "corresponding source for every package in this image is available from the" + echo "archive it was installed from, at the versions recorded in the image's own" + echo "package database (\`/var/lib/dpkg/status\`)." + echo + echo "| Component | Source |" + echo "|---|---|" + echo "| Debian ${DEB_VER} \"${DEB_SUITE}\" | — \`apt-get source \` |" + echo "| Proxmox VE (${PVE_REPO}) | and \`deb-src ${PVE_URI}\` |" + echo "| Build recipe | <${REPO_URL}> |" + echo + echo "The firmware blobs under \`/usr/lib/firmware\` are redistributed verbatim;" + echo "their licence texts ship inside the image at" + echo "\`/usr/share/doc/pve-firmware/licenses/\` and must not be stripped." + echo + echo "## Image metadata" + echo + echo '```' + cat dist/image-info.txt + echo '```' +} > dist/SOURCES.md + +ls -lh dist/ diff --git a/scripts/ci/prune-releases.sh b/scripts/ci/prune-releases.sh new file mode 100755 index 0000000..8f41d27 --- /dev/null +++ b/scripts/ci/prune-releases.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (C) 2026 Ilker Manap +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Keeps the newest N releases and deletes the rest, tags included. +# Each image is ~1.5 GB, so unbounded retention fills the server. +# +# prune-releases.sh [keep] (default 3) +# +# Requires GITEA_API and GITEA_TOKEN in the environment. +set -euo pipefail + +KEEP="${1:-3}" +: "${GITEA_API:?GITEA_API required}" +: "${GITEA_TOKEN:?GITEA_TOKEN required}" + +curl -fsS -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API}/releases?limit=50" \ + | KEEP="$KEEP" python3 -c ' +import json, os, sys +keep = int(os.environ["KEEP"]) +for r in json.load(sys.stdin)[keep:]: + print(r["id"], r["tag_name"]) +' | while read -r id tag; do + echo "removing old release ${tag}" + curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_API}/releases/${id}" || true + curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_API}/tags/${tag}" || true +done diff --git a/scripts/ci/publish-release.sh b/scripts/ci/publish-release.sh new file mode 100755 index 0000000..857281a --- /dev/null +++ b/scripts/ci/publish-release.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# Copyright (C) 2026 Ilker Manap +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Creates a Gitea release and uploads the image, its checksum and the +# corresponding-source offer. +# +# publish-release.sh +# +# Requires GITEA_API and GITEA_TOKEN in the environment. +set -euo pipefail + +TAG="${1:?tag required}" +VERSION="${2:?version required}" +DIST="${3:?dist directory required}" +: "${GITEA_API:?GITEA_API required}" +: "${GITEA_TOKEN:?GITEA_TOKEN required}" + +REPO_URL="${GITHUB_SERVER_URL:-}/${GITHUB_REPOSITORY:-}" +RUN="${GITHUB_RUN_NUMBER:-manual}" +SHA="${GITHUB_SHA:-}" +IMAGE=$(basename "$(ls "$DIST"/maas-image-*.tar.gz)") + +BODY=$(cat </dev/null +done + +echo "published: ${REPO_URL}/releases/tag/${TAG}" diff --git a/scripts/ci/upstream-version.sh b/scripts/ci/upstream-version.sh new file mode 100755 index 0000000..dfbcfdc --- /dev/null +++ b/scripts/ci/upstream-version.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Copyright (C) 2026 Ilker Manap +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Prints the newest pve-manager version in the configured Proxmox repository. +# Used by CI to decide whether a rebuild is worth doing. +set -euo pipefail +cd "$(dirname "$0")/../.." + +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) + +VER=$(curl -fsS "${URI}/dists/${SUITE}/${COMP}/binary-${ARCH}/Packages.gz" \ + | gunzip \ + | awk '/^Package: pve-manager$/{p=1; next} p && /^Version: /{print $2; p=0}' \ + | sort -V | tail -1) + +[ -n "$VER" ] || { echo "pve-manager version not found in ${URI} ${SUITE}/${COMP}" >&2; exit 1; } +printf '%s\n' "$VER"