182 lines
7.1 KiB
Bash
Executable File
182 lines
7.1 KiB
Bash
Executable File
#!/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
|
|
}
|