From 699261471a3e446f1e7ddbaddfcd570b79fbc94f Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 24 Aug 2026 19:30:33 -0400 Subject: [PATCH] Replace mise's upstream hook with a declarative GitHub-releases provider After the quarantine moved into the manifest, all mise-bin's hook still knew was data: the repository, the checksum manifest name, and the asset filename patterns. That now lives in .omarchy/package.json as an upstream block -- "upstream": { "github": "jdx/mise", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "mise-{tag}-linux-x64.tar.xz", ... } } -- handled by helpers/upstream-github.sh inside bin/sync-upstream. The provider walks the release feed (drafts/prereleases excluded), honors min_release_age and BYPASS_MIN_RELEASE_AGE during selection, reports published_at so the framework backstop still applies, fails closed on any unreadable tag or timestamp, and skips the checksum fetch when the newest qualifying release is already checked in. upstream.sh remains the escape hatch for feeds that fit no convention (openai-codex-desktop's Debian index, tmog's version.txt, t3code's electron-builder manifest); declaring both is an error. --- bin/sync-upstream | 29 ++++- helpers/package-metadata.sh | 22 +++- helpers/upstream-github.sh | 139 +++++++++++++++++++++++ pkgbuilds/mise-bin/.omarchy/package.json | 10 +- pkgbuilds/mise-bin/.omarchy/upstream.sh | 89 --------------- 5 files changed, 193 insertions(+), 96 deletions(-) create mode 100644 helpers/upstream-github.sh delete mode 100644 pkgbuilds/mise-bin/.omarchy/upstream.sh diff --git a/bin/sync-upstream b/bin/sync-upstream index 70567cf..206350e 100755 --- a/bin/sync-upstream +++ b/bin/sync-upstream @@ -5,6 +5,7 @@ BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/package-metadata.sh" +source "$BUILD_ROOT/helpers/upstream-github.sh" TEMP_DIR=$(mktemp -d) trap 'rm -rf "$TEMP_DIR"' EXIT @@ -17,7 +18,10 @@ Usage: $0 [PACKAGE...] Update packages that track an upstream vendor release feed instead of the AUR. -A package opts in by providing pkgbuilds//.omarchy/upstream.sh, a hook +A package whose upstream ships tagged GitHub releases with a checksum manifest +opts in declaratively, via "upstream" in .omarchy/package.json (see +helpers/upstream-github.sh for the schema); no code needed. Anything with a +bespoke feed provides pkgbuilds//.omarchy/upstream.sh instead, a hook that reports the newest upstream release as JSON on stdout: { @@ -304,12 +308,21 @@ sync_package() { return 0 fi - if [[ ! -f "$hook" ]]; then + local github_repo + github_repo=$(package_upstream_github_repo "$package_dir") + + if [[ -n "$github_repo" && -f "$hook" ]]; then + print_error "Package $package declares both upstream.github and an upstream.sh hook; keep exactly one" + ((++FAILED)) + return 0 + fi + + if [[ -z "$github_repo" && ! -f "$hook" ]]; then if [[ "$SPECIFIC_MODE" == true ]]; then - print_error "Package $package is missing .omarchy/upstream.sh" + print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh" ((++FAILED)) else - print_info "Skipping $package: no upstream hook" + print_info "Skipping $package: no upstream source" ((++SKIPPED)) fi return 0 @@ -325,7 +338,13 @@ sync_package() { print_info "Checking $package for upstream releases..." local release - if ! release=$(cd "$package_dir" && PACKAGE_NAME="$package" \ + if [[ -n "$github_repo" ]]; then + if ! release=$(github_upstream_release "$package_dir" "$min_age"); then + print_error "GitHub release provider failed for $package" + ((++FAILED)) + return 0 + fi + elif ! release=$(cd "$package_dir" && PACKAGE_NAME="$package" \ MIN_RELEASE_AGE_SECONDS="$min_age" \ BYPASS_MIN_RELEASE_AGE="${BYPASS_MIN_RELEASE_AGE:-}" \ bash .omarchy/upstream.sh); then diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh index 0d495bd..561c3de 100644 --- a/helpers/package-metadata.sh +++ b/helpers/package-metadata.sh @@ -13,6 +13,7 @@ # { "source": "aur", "rebuild_on": ["qt6-base"] } # { "source": "local" } # { "source": "local", "min_release_age": "24h" } +# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } } # # bin/sync-aur also writes upstream_commit for AUR-backed packages, and # bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on. @@ -164,9 +165,14 @@ package_has_upstream_hook() { [[ -f "$pkgdir/.omarchy/upstream.sh" ]] } +package_has_upstream_provider() { + local pkgdir="$1" + [[ -n "$(package_metadata_value "$pkgdir" '.upstream.github' "")" ]] +} + packages_for_upstream_sync() { package_dirs | while IFS= read -r pkgdir; do - if package_has_upstream_hook "$pkgdir"; then + if package_has_upstream_hook "$pkgdir" || package_has_upstream_provider "$pkgdir"; then basename "$pkgdir" fi done @@ -340,6 +346,20 @@ validate_package_metadata() { return 1 fi + if ! jq -e ' + (.upstream // {}) | type == "object" + and (if . == {} then true else + ((.github // "") | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z")) + and ((.checksums // "") | type == "string" and length > 0) + and ((.assets // {}) | type == "object" and length > 0 and (to_entries | all( + (.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0) + ))) + end) + ' "$metadata" >/dev/null; then + echo "invalid upstream for $(basename "$pkgdir"): needs github owner/repo, checksums asset name, and an assets arch->name map" + return 1 + fi + pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata") case "$pkgrel_type" in object|missing) ;; diff --git a/helpers/upstream-github.sh b/helpers/upstream-github.sh new file mode 100644 index 0000000..adf2b4b --- /dev/null +++ b/helpers/upstream-github.sh @@ -0,0 +1,139 @@ +# GitHub-releases upstream provider for bin/sync-upstream. +# +# A package whose upstream ships tagged GitHub releases with a checksum +# manifest asset needs no upstream.sh hook: the whole feed is data, declared +# in .omarchy/package.json -- +# +# "upstream": { +# "github": "jdx/mise", +# "checksums": "SHASUMS256.txt", +# "assets": { +# "x86_64": "mise-{tag}-linux-x64.tar.xz", +# "aarch64": "mise-{tag}-linux-arm64.tar.xz" +# } +# } +# +# {tag} and {pkgver} interpolate into asset names; tags may carry a leading +# "v", which is stripped for pkgver. Drafts and prereleases are ignored. The +# provider emits the same JSON contract as an upstream.sh hook, so +# bin/sync-upstream's validation and min_release_age backstop apply +# unchanged; a feed that fits no convention keeps a bespoke upstream.sh. + +package_upstream_github_repo() { + local pkgdir="$1" + package_metadata_value "$pkgdir" '.upstream.github' "" +} + +# Emits the newest qualifying release as hook-contract JSON. min_release_age +# is honored during selection (newest release older than the window wins, +# even when a younger one exists) and BYPASS_MIN_RELEASE_AGE=1 lifts it. +# Unusable tags or timestamps anywhere in the feed fail the sync rather than +# being skipped: a feed this provider cannot fully read is a feed it should +# not silently choose from. +github_upstream_release() { + local package_dir="$1" min_age="${2:-0}" + local metadata repo checksums_name + metadata=$(metadata_file_for_dir "$package_dir") + + repo=$(jq -r '.upstream.github // ""' "$metadata") + if [[ ! "$repo" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "invalid upstream.github repository: '${repo:-}'" >&2 + return 1 + fi + checksums_name=$(jq -r '.upstream.checksums // ""' "$metadata") + if [[ -z "$checksums_name" ]]; then + echo "upstream.checksums names the checksum manifest asset and is required" >&2 + return 1 + fi + local arches + mapfile -t arches < <(jq -r '.upstream.assets // {} | keys[]' "$metadata") + if [[ ${#arches[@]} -eq 0 ]]; then + echo "upstream.assets must map at least one architecture to an asset name" >&2 + return 1 + fi + + local releases now + now=$(date +%s) + if ! releases=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=20"); then + echo "could not fetch the release feed for $repo" >&2 + return 1 + fi + + local candidates=0 best_tag="" best_pkgver="" best_published_at="" + local tag published_at pkgver published_epoch + while IFS=$'\t' read -r tag published_at; do + if [[ ! "$tag" =~ ^v?([A-Za-z0-9._+]+)$ ]]; then + echo "$repo release has an unusable tag: ${tag:-}" >&2 + return 1 + fi + pkgver=${BASH_REMATCH[1]} + + if [[ -z "$published_at" ]] || ! published_epoch=$(date --date="$published_at" +%s 2>/dev/null); then + echo "$repo release $tag has an invalid published_at: ${published_at:-}" >&2 + return 1 + fi + candidates=$((candidates + 1)) + + if (( now - published_epoch < min_age )); then + if [[ "${BYPASS_MIN_RELEASE_AGE:-}" == "1" ]]; then + echo "Bypassing release-age gate for $repo $tag" >&2 + else + continue + fi + fi + + if [[ -z "$best_pkgver" ]] || [[ "$(vercmp "$pkgver" "$best_pkgver")" -gt 0 ]]; then + best_tag=$tag + best_pkgver=$pkgver + best_published_at=$published_at + fi + done < <(jq -r '.[] | select((.draft or .prerelease) | not) | [.tag_name // empty, .published_at // empty] | @tsv' <<<"$releases") + + if (( candidates == 0 )); then + echo "no stable releases found in the feed for $repo" >&2 + return 1 + fi + if [[ -z "$best_tag" ]]; then + echo "every recent $repo release is still inside the release-age quarantine; skipping" >&2 + echo '{}' + return 0 + fi + + # Already checked in: report no update instead of re-fetching checksums. + local current_pkgver + current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'") + if [[ "$best_pkgver" == "$current_pkgver" ]]; then + echo '{}' + return 0 + fi + + local checksums + if ! checksums=$(curl -fsSL "https://github.com/$repo/releases/download/$best_tag/$checksums_name"); then + echo "could not fetch $checksums_name for $repo $best_tag" >&2 + return 1 + fi + + local jq_args=(--arg pkgver "$best_pkgver" --arg published_at "$best_published_at") + local jq_filter='{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}' + local arch template filename checksum + for arch in "${arches[@]}"; do + if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then + echo "invalid architecture key in upstream.assets: '$arch'" >&2 + return 1 + fi + template=$(jq -r --arg arch "$arch" '.upstream.assets[$arch]' "$metadata") + filename=${template//\{pkgver\}/$best_pkgver} + filename=${filename//\{tag\}/$best_tag} + # Manifest lines are " ", with the name sometimes prefixed + # "./" (sha256sum of a local path) or "*" (binary-mode marker). + checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums") + if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then + echo "no valid checksum for $filename in $repo $best_tag $checksums_name" >&2 + return 1 + fi + jq_args+=(--arg "sum_$arch" "$checksum") + jq_filter+=" | .sha256sums[\"$arch\"] = [\$sum_$arch]" + done + + jq -n "${jq_args[@]}" "$jq_filter" +} diff --git a/pkgbuilds/mise-bin/.omarchy/package.json b/pkgbuilds/mise-bin/.omarchy/package.json index bbe9ae1..f90090e 100644 --- a/pkgbuilds/mise-bin/.omarchy/package.json +++ b/pkgbuilds/mise-bin/.omarchy/package.json @@ -1,5 +1,13 @@ { "source": "local", "release_ring": "fast", - "min_release_age": "24h" + "min_release_age": "24h", + "upstream": { + "github": "jdx/mise", + "checksums": "SHASUMS256.txt", + "assets": { + "x86_64": "mise-{tag}-linux-x64.tar.xz", + "aarch64": "mise-{tag}-linux-arm64.tar.xz" + } + } } diff --git a/pkgbuilds/mise-bin/.omarchy/upstream.sh b/pkgbuilds/mise-bin/.omarchy/upstream.sh deleted file mode 100644 index 8db9297..0000000 --- a/pkgbuilds/mise-bin/.omarchy/upstream.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -set -euo pipefail - -repo="jdx/mise" - -# Keep a compromised mise release from reaching Omarchy before there has been -# time for maintainers and the community to notice and pull it. The window -# comes from min_release_age in .omarchy/package.json, exported by -# bin/sync-upstream as MIN_RELEASE_AGE_SECONDS. Walking the release list -# instead of gating on /releases/latest alone means mise's near-daily cadence -# cannot starve updates: the newest release that has finished its quarantine -# ships even while an even newer one is still inside it. Nothing younger than -# the window ever ships without the explicit BYPASS_MIN_RELEASE_AGE=1 bypass, -# which bin/sync-upstream honors too. -minimum_release_age_seconds=${MIN_RELEASE_AGE_SECONDS:-0} -now=$(date +%s) - -releases=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=20") - -candidates=0 -best_tag="" -best_pkgver="" -best_published_at="" -while IFS=$'\t' read -r tag published_at; do - if [[ ! "$tag" =~ ^v([A-Za-z0-9._+]+)$ ]]; then - echo "mise release has an invalid tag: ${tag:-}" >&2 - exit 1 - fi - pkgver=${BASH_REMATCH[1]} - - if [[ -z "$published_at" ]] || ! published_epoch=$(date --date="$published_at" +%s); then - echo "mise release $tag has an invalid published_at: ${published_at:-}" >&2 - exit 1 - fi - candidates=$((candidates + 1)) - - if (( now - published_epoch < minimum_release_age_seconds )); then - if [[ "${BYPASS_MIN_RELEASE_AGE:-}" == "1" ]]; then - echo "Bypassing mise release-age gate for $tag" >&2 - else - continue - fi - fi - - if [[ -z "$best_pkgver" ]] || [[ "$(vercmp "$pkgver" "$best_pkgver")" -gt 0 ]]; then - best_tag=$tag - best_pkgver=$pkgver - best_published_at=$published_at - fi -done < <(jq -r '.[] | select((.draft or .prerelease) | not) | [.tag_name // empty, .published_at // empty] | @tsv' <<<"$releases") - -if (( candidates == 0 )); then - echo "No stable mise releases found in the release feed" >&2 - exit 1 -fi - -if [[ -z "$best_tag" ]]; then - echo "Every recent mise release is still inside the release-age quarantine; skipping" >&2 - echo '{}' - exit 0 -fi - -tag=$best_tag -pkgver=$best_pkgver -checksums=$(curl -fsSL \ - "https://github.com/$repo/releases/download/$tag/SHASUMS256.txt") - -checksum_for() { - local filename=$1 - local checksum - checksum=$(awk -v filename="./$filename" '$2 == filename { print $1 }' <<<"$checksums") - - if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then - echo "No valid checksum found for $filename in $tag" >&2 - exit 1 - fi - - echo "$checksum" -} - -x86_64=$(checksum_for "mise-v${pkgver}-linux-x64.tar.xz") -aarch64=$(checksum_for "mise-v${pkgver}-linux-arm64.tar.xz") - -jq -n \ - --arg pkgver "$pkgver" \ - --arg published_at "$best_published_at" \ - --arg x86_64 "$x86_64" \ - --arg aarch64 "$aarch64" \ - '{pkgver: $pkgver, published_at: $published_at, sha256sums: {x86_64: [$x86_64], aarch64: [$aarch64]}}'