Files
marfrit-packages/.gitea/scripts/check-already-published.sh
T
claude-noether f8d1257d35 ci: add check-already-published helper script
CI currently rebuilds every recipe on every push. distcc-avahi hasn't
changed in weeks but still burns runner-time. Add a small bash helper
that takes a recipe dir (arch/<n> or debian/<n>), resolves the expected
pool URL on packages.reauktion.de, and prints `skip=1` or `skip=0`.

Live-tested against all 14 recipes. Sources PKGBUILDs in a sandboxed
subshell so epoch=, ${_pkgver/-/}, and pkgname=() arrays resolve
correctly. For debian/*, extracts top-level PKGVER/PKGREL lines with
an awk guard that rejects command-subst and embedded-command
assignments (avoids false-positive matches against HEREDOC-quoted
dkms.conf content).

Wiring into build.yml lands in the next commit.
2026-05-20 11:47:24 +02:00

211 lines
6.9 KiB
Bash
Executable File

#!/bin/bash
# check-already-published.sh <recipe-dir>
#
# Decide whether a given recipe (arch/<name> or debian/<name>) is already
# present in https://packages.reauktion.de/. Emits exactly one line to
# stdout:
#
# skip=1 — package with this version-pkgrel-arch tuple already lives in
# the pool; CI should short-circuit.
# skip=0 — file is missing or HEAD failed; CI should build + publish.
#
# Design notes:
# * For Arch recipes we source the PKGBUILD in a clean subshell so
# shell expansions (epoch=, ${_pkgver/-/}, pkgname=() arrays) resolve
# naturally. Only the first element of pkgname[] is checked — split
# packages share one source tarball / one build, so any-one-missing
# forces the full rebuild anyway.
# * For Debian recipes we extract the bare top-level PKGVER= /
# PKGREL= assignments (plus any other top-level VAR=value lines they
# reference) via grep and re-evaluate them in an isolated subshell —
# sourcing the entire build-deb.sh would run curl/tar/dpkg-deb
# against a tempdir we don't want to materialise here.
# * Epoch handling differs by ecosystem: Arch keeps `<epoch>:` in the
# pool filename, Debian/reprepro strips it.
# * curl --head with -f maps non-2xx to non-zero exit, which is what we
# want — 404 means "build it". -L follows mirrors. --max-time caps
# the worst-case latency per HEAD.
set -euo pipefail
REPO_BASE="${REPO_BASE:-https://packages.reauktion.de}"
HEAD_TIMEOUT="${HEAD_TIMEOUT:-15}"
RECIPE_DIR="${1:?usage: $0 <recipe-dir> (e.g. arch/distcc-avahi or debian/lmcp)}"
# Resolve relative to repo root if a leading path is passed; allow
# both `arch/foo` and absolute paths.
if [ ! -d "$RECIPE_DIR" ]; then
echo "error: recipe dir not found: $RECIPE_DIR" >&2
exit 2
fi
ecosystem="${RECIPE_DIR%%/*}"
http_head() {
local url="$1"
curl -sS -L --max-time "$HEAD_TIMEOUT" -o /dev/null \
-w '%{http_code}' --head "$url" || echo "000"
}
emit() {
# one-line GITHUB_OUTPUT-compatible kv
echo "skip=$1"
exit 0
}
case "$ecosystem" in
arch)
pkgbuild="$RECIPE_DIR/PKGBUILD"
[ -f "$pkgbuild" ] || { echo "error: $pkgbuild missing" >&2; exit 2; }
# Source in a fresh bash to capture variables. Some PKGBUILDs run
# functions or call commands at top level — keep this fast by
# restricting PATH and trapping side effects.
eval "$(
bash --noprofile --norc -c "
set +e
# Stub out anything that might shell out; we only need variable
# assignments to land.
cd '$RECIPE_DIR'
source ./PKGBUILD >/dev/null 2>&1 || true
# pkgname may be array; print first element.
if declare -p pkgname 2>/dev/null | grep -q 'declare -a'; then
first_name=\"\${pkgname[0]}\"
else
first_name=\"\$pkgname\"
fi
if declare -p arch 2>/dev/null | grep -q 'declare -a'; then
first_arch=\"\${arch[0]}\"
else
first_arch=\"\$arch\"
fi
printf 'PB_NAME=%q\n' \"\$first_name\"
printf 'PB_VER=%q\n' \"\$pkgver\"
printf 'PB_REL=%q\n' \"\$pkgrel\"
printf 'PB_EPOCH=%q\n' \"\${epoch:-}\"
printf 'PB_ARCH=%q\n' \"\$first_arch\"
"
)"
if [ -z "${PB_NAME:-}" ] || [ -z "${PB_VER:-}" ] || [ -z "${PB_REL:-}" ]; then
echo "error: failed to parse PKGBUILD ($RECIPE_DIR)" >&2
emit 0
fi
# Pool arch:
# arch=('any') → any
# arch=('aarch64' 'x86_64') → aarch64 (we publish for both, but the
# aarch64 artifact is the canonical CI build)
# arch=('aarch64') → aarch64
case "$PB_ARCH" in
any) pool_arch=any ;;
*) pool_arch=aarch64 ;;
esac
# Version string with optional epoch (epoch:pkgver-pkgrel).
if [ -n "${PB_EPOCH:-}" ]; then
ver_full="${PB_EPOCH}:${PB_VER}-${PB_REL}"
else
ver_full="${PB_VER}-${PB_REL}"
fi
# Pool URL path (arch keeps any/aarch64 split; 'any' lands in the
# aarch64 dir per current marfrit layout — both arches share the
# blob via the publish-to-both-arches step in build.yml).
pool_dir="arch/aarch64"
base_url="${REPO_BASE}/${pool_dir}/${PB_NAME}-${ver_full}-${pool_arch}.pkg.tar"
for ext in zst xz gz; do
code=$(http_head "${base_url}.${ext}")
if [ "$code" = "200" ]; then
emit 1
fi
done
emit 0
;;
debian)
bd="$RECIPE_DIR/build-deb.sh"
ctrl="$RECIPE_DIR/control"
[ -f "$bd" ] || { echo "error: $bd missing" >&2; exit 2; }
# Pull top-level `VAR=value` lines until we've passed PKGREL, and
# only those whose RHS is safe to re-evaluate (no command
# substitution `$(...)`, no escaped `\$`, no embedded commands like
# `DESTDIR=... meson ...`). This deliberately undershoots: we just
# need PKGVER/PKGREL plus any version vars they reference. Anything
# else (HERE=$(readlink ...), KERNELVER=\$(uname -r) inside a
# HEREDOC, etc.) gets dropped.
assigns=$(awk '
/^[A-Z_][A-Z0-9_]*=/ {
# split into LHS and RHS
eq = index($0, "=")
lhs = substr($0, 1, eq - 1)
rhs = substr($0, eq + 1)
# strip inline `# comment`
hash = index(rhs, "#")
if (hash > 1 && substr(rhs, hash-1, 1) == " ") rhs = substr(rhs, 1, hash - 2)
# reject lines with command-subst or escaped-dollar or naked commands
if (rhs ~ /\$\(/) next
if (rhs ~ /\\\$/) next
if (rhs ~ / [a-z]/) next # e.g. `DESTDIR="$ROOT" meson ...`
print lhs "=" rhs
if (lhs == "PKGREL") exit
}
' "$bd")
eval "$(
bash --noprofile --norc -c "
set +e
$assigns
printf 'PKGVER=%q\n' \"\${PKGVER:-}\"
printf 'PKGREL=%q\n' \"\${PKGREL:-}\"
"
)"
if [ -z "${PKGVER:-}" ] || [ -z "${PKGREL:-}" ]; then
echo "error: failed to parse PKGVER/PKGREL from $bd" >&2
emit 0
fi
# Strip epoch (`N:` prefix) — debian pool filenames omit it.
ver_no_epoch="${PKGVER#*:}"
# If PKGVER had no colon, ${PKGVER#*:} returns PKGVER unchanged (bash quirk:
# the pattern must match for the prefix to be stripped). Guard explicitly.
case "$PKGVER" in
*:*) : ;;
*) ver_no_epoch="$PKGVER" ;;
esac
ver_full="${ver_no_epoch}-${PKGREL}"
# Architecture: parse control's `Architecture:` field.
if [ ! -f "$ctrl" ]; then
# Some recipes ship debian/control instead of ./control
ctrl="$RECIPE_DIR/debian/control"
fi
ctrl_arch=$(grep -m1 '^Architecture:' "$ctrl" 2>/dev/null | awk '{print $2}')
case "$ctrl_arch" in
all) file_arch=all ;;
arm64|any) file_arch=arm64 ;;
amd64) file_arch=amd64 ;;
*) file_arch=arm64 ;; # conservative default
esac
pkg_name=$(basename "$RECIPE_DIR")
first_letter="${pkg_name:0:1}"
url="${REPO_BASE}/debian/pool/main/${first_letter}/${pkg_name}/${pkg_name}_${ver_full}_${file_arch}.deb"
code=$(http_head "$url")
if [ "$code" = "200" ]; then
emit 1
fi
emit 0
;;
*)
echo "error: unsupported ecosystem '$ecosystem' (recipe-dir=$RECIPE_DIR)" >&2
emit 0
;;
esac