#!/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 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 # , and check that every entry sits under / when one is given. # usage: fetch_tarball [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_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 [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 at ; false on 404; dies on # anything else, so a broken query is never mistaken for "not published yet". # usage: registry_has 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 with PUT; Gitea answers 201 Created. # usage: registry_upload 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 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 }