feat: add the nightly deb reconciler

This commit is contained in:
bryanpedini committed 2026-10-10 11:09:04 +02:00
1 parent 4f730b914e
commit f340028aae
5 files changed
+786 -1

No files matched your search

Executable
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# Shared helpers for the packaging autoupdaters (rpm/.autoupdate, deb/.autoupdate).
# Sourced by autoupdate.sh and latest-version.sh; never executed directly.
#
# latest-version.sh is a verbatim copy of aur/.autoupdate's, so the names it
# relies on (die, validate_version) keep the exact behaviour they have there.
set -euo pipefail
log() { printf '==> %s\n' "$*"; }
info() { printf ' -> %s\n' "$*"; }
warn() { printf 'WARNING: %s\n' "$*" >&2; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
# Fail up front on a missing tool rather than midway through, where an absent
# binary tends to be read as "condition false" and silently takes the wrong
# branch (aur/.autoupdate learned this from diff going missing).
#
# usage: require_tools [extra-tool ...]
require_tools() {
local t missing=()
for t in git curl jq sha256sum awk sed grep tar gzip "$@"; do
command -v "$t" >/dev/null 2>&1 || missing+=("$t")
done
[ "${#missing[@]}" -eq 0 ] || die "missing required tool(s): ${missing[*]}"
return 0
}
require_noninteractive() {
[ -t 0 ] && die "refusing to prompt: this script must never depend on interactive input"
return 0
}
# A version we are willing to write into a spec or a changelog.
VERSION_RE='^[0-9][0-9a-zA-Z._+]*$'
validate_version() {
local ver="${1-}" what="${2-version}"
[ -n "$ver" ] || die "${what} is empty"
[[ "$ver" =~ $VERSION_RE ]] || die "${what} '${ver}' does not look like a version"
return 0
}
# True if $1 is strictly newer than $2. Same ordering as latest-version.sh
# (sort -V), so "newest tag" and "newer than packaged" can never disagree.
version_gt() {
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]
}
# Clone the package repo, or bring an existing clone back to origin's state.
# usage: clone_or_reset <url> <dir>
clone_or_reset() {
local url="$1" dir="$2"
if [ -d "$dir/.git" ]; then
git -C "$dir" fetch --quiet origin
git -C "$dir" reset --quiet --hard origin/HEAD
else
rm -rf "$dir"
git clone --quiet "$url" "$dir" || die "could not clone ${url}"
fi
}
# Identity for the changelog entry and the bump commit: GIT_NAME/GIT_EMAIL
# (the same variables the workflow uses), else the git config the workflow
# wrote. Never guessed. Sets MAINT_NAME and MAINT_EMAIL in the calling shell
# rather than printing them, because a failing $(...) inside a larger string
# does not trip `set -e`.
resolve_identity() {
MAINT_NAME="${GIT_NAME:-}"
MAINT_EMAIL="${GIT_EMAIL:-}"
[ -n "$MAINT_NAME" ] || MAINT_NAME="$(git config user.name || true)"
[ -n "$MAINT_EMAIL" ] || MAINT_EMAIL="$(git config user.email || true)"
[ -n "$MAINT_NAME" ] || die "no maintainer name: set GIT_NAME or git config user.name"
[ -n "$MAINT_EMAIL" ] || die "no maintainer email: set GIT_EMAIL or git config user.email"
return 0
}
git_commit() {
resolve_identity
git -c user.name="$MAINT_NAME" -c user.email="$MAINT_EMAIL" commit --quiet -m "$1"
}
# Download the upstream release tarball (or copy the local test tarball) to
# <out>, and check that every entry sits under <topdir>/ when one is given.
# usage: fetch_tarball <url> <out> [topdir]
fetch_tarball() {
local url="$1" out="$2" topdir="${3:-}"
mkdir -p "$(dirname "$out")"
if [ -n "${AUTOUPDATE_TEST_TARBALL:-}" ]; then
[ -f "$AUTOUPDATE_TEST_TARBALL" ] || die "AUTOUPDATE_TEST_TARBALL '${AUTOUPDATE_TEST_TARBALL}' not found"
info "test mode: using ${AUTOUPDATE_TEST_TARBALL} instead of ${url}"
cp "$AUTOUPDATE_TEST_TARBALL" "$out"
else
info "fetching ${url}"
curl --silent --show-error --location --fail --retry 3 --retry-delay 2 \
--max-time 900 -o "${out}.part" "$url" \
|| die "download failed: ${url}"
mv "${out}.part" "$out"
fi
gzip -t "$out" 2>/dev/null || die "${out} is not a gzip tarball"
if [ -n "$topdir" ]; then
# awk reads the whole listing, so tar never dies of SIGPIPE under pipefail.
tar -tzf "$out" | awk -F/ -v d="$topdir" '$1 != d { bad = 1 } END { exit bad }' \
|| die "tarball entries are not all under ${topdir}/"
fi
info "sha256 $(sha256sum "$out" | awk '{print $1}')"
}
# --- Gitea package registry ---------------------------------------------------
#
# Basic auth with the same credential the workflow stores for git
# (https://<GIT_EMAIL>:<GIT_TOKEN>@<GIT_HOST>): GIT_TOKEN from the environment
# if set, otherwise `git credential fill`. Gitea ignores the username when the
# password is a token. The credential is handed to curl as a config file on
# stdin, so it never appears on a command line, in `ps`, or in a log.
registry_curl_config() {
local user="${GIT_EMAIL:-}" pass="${GIT_TOKEN:-}" cred
if [ -z "$pass" ]; then
cred="$(printf 'protocol=https\nhost=%s\n\n' "$GIT_HOST" \
| GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=true git credential fill 2>/dev/null)" \
|| die "no registry credentials: set GIT_TOKEN, or store a token for https://${GIT_HOST} in a git credential helper"
user="$(printf '%s\n' "$cred" | sed -n 's/^username=//p')"
pass="$(printf '%s\n' "$cred" | sed -n 's/^password=//p')"
fi
[ -n "$pass" ] || die "no registry token for https://${GIT_HOST}"
printf 'user = "%s:%s"\n' "${user:-autoupdate}" "$pass"
}
# registry_request <method> <url> [curl args...]
# Prints the HTTP status; the response body lands in $REGISTRY_BODY.
registry_request() {
local method="$1" url="$2"; shift 2
local cfg code
cfg="$(registry_curl_config)" || exit 1
code="$(printf '%s' "$cfg" | curl --silent --show-error --max-time 600 \
--config - --request "$method" --output "$REGISTRY_BODY" \
--write-out '%{http_code}' "$@" "$url")" \
|| die "${method} ${url} failed"
printf '%s' "$code"
}
# True if the registry already holds <name> at <version>; false on 404; dies on
# anything else, so a broken query is never mistaken for "not published yet".
# usage: registry_has <owner> <type> <name> <version>
registry_has() {
local url="https://${GIT_HOST}/api/v1/packages/$1/$2/$3/$4" code
code="$(registry_request GET "$url" --retry 3 --retry-delay 2)"
case "$code" in
200) return 0 ;;
404) return 1 ;;
*) cat "$REGISTRY_BODY" >&2 2>/dev/null || true
die "registry lookup ${url} failed${code:+ (HTTP ${code})}" ;;
esac
}
# Upload <file> with PUT; Gitea answers 201 Created.
# usage: registry_upload <url> <file>
registry_upload() {
local url="$1" file="$2" code
code="$(registry_request PUT "$url" --upload-file "$file")"
case "$code" in
201) log "uploaded $(basename "$file")" ;;
409) die "the registry already has $(basename "$file") (HTTP 409); refusing to overwrite without FORCE_REBUILD" ;;
*) cat "$REGISTRY_BODY" >&2 2>/dev/null || true
die "upload of $(basename "$file") to ${url} answered HTTP ${code}" ;;
esac
}
# Delete one published file (FORCE_REBUILD replacing an existing build).
# usage: registry_delete <url>
registry_delete() {
local url="$1" code
code="$(registry_request DELETE "$url")"
case "$code" in
204|200) info "deleted ${url}" ;;
404) info "nothing to delete at ${url}" ;;
*) cat "$REGISTRY_BODY" >&2 2>/dev/null || true
die "DELETE ${url} answered HTTP ${code}" ;;
esac
}