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"