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.
This commit is contained in:
+24
-5
@@ -5,6 +5,7 @@ BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
|||||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||||
source "$BUILD_ROOT/helpers/paths.sh"
|
source "$BUILD_ROOT/helpers/paths.sh"
|
||||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||||
|
source "$BUILD_ROOT/helpers/upstream-github.sh"
|
||||||
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
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.
|
Update packages that track an upstream vendor release feed instead of the AUR.
|
||||||
|
|
||||||
A package opts in by providing pkgbuilds/<package>/.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/<package>/.omarchy/upstream.sh instead, a hook
|
||||||
that reports the newest upstream release as JSON on stdout:
|
that reports the newest upstream release as JSON on stdout:
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -304,12 +308,21 @@ sync_package() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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
|
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))
|
((++FAILED))
|
||||||
else
|
else
|
||||||
print_info "Skipping $package: no upstream hook"
|
print_info "Skipping $package: no upstream source"
|
||||||
((++SKIPPED))
|
((++SKIPPED))
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
@@ -325,7 +338,13 @@ sync_package() {
|
|||||||
print_info "Checking $package for upstream releases..."
|
print_info "Checking $package for upstream releases..."
|
||||||
|
|
||||||
local release
|
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" \
|
MIN_RELEASE_AGE_SECONDS="$min_age" \
|
||||||
BYPASS_MIN_RELEASE_AGE="${BYPASS_MIN_RELEASE_AGE:-}" \
|
BYPASS_MIN_RELEASE_AGE="${BYPASS_MIN_RELEASE_AGE:-}" \
|
||||||
bash .omarchy/upstream.sh); then
|
bash .omarchy/upstream.sh); then
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
# { "source": "aur", "rebuild_on": ["qt6-base"] }
|
# { "source": "aur", "rebuild_on": ["qt6-base"] }
|
||||||
# { "source": "local" }
|
# { "source": "local" }
|
||||||
# { "source": "local", "min_release_age": "24h" }
|
# { "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-aur also writes upstream_commit for AUR-backed packages, and
|
||||||
# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on.
|
# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on.
|
||||||
@@ -164,9 +165,14 @@ package_has_upstream_hook() {
|
|||||||
[[ -f "$pkgdir/.omarchy/upstream.sh" ]]
|
[[ -f "$pkgdir/.omarchy/upstream.sh" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
package_has_upstream_provider() {
|
||||||
|
local pkgdir="$1"
|
||||||
|
[[ -n "$(package_metadata_value "$pkgdir" '.upstream.github' "")" ]]
|
||||||
|
}
|
||||||
|
|
||||||
packages_for_upstream_sync() {
|
packages_for_upstream_sync() {
|
||||||
package_dirs | while IFS= read -r pkgdir; do
|
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"
|
basename "$pkgdir"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -340,6 +346,20 @@ validate_package_metadata() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
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")
|
pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
|
||||||
case "$pkgrel_type" in
|
case "$pkgrel_type" in
|
||||||
object|missing) ;;
|
object|missing) ;;
|
||||||
|
|||||||
@@ -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:-<empty>}'" >&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:-<empty>}" >&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:-<empty>}" >&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 "<sha256> <name>", 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"
|
||||||
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
{
|
{
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"release_ring": "fast",
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:-<empty>}" >&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:-<empty>}" >&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]}}'
|
|
||||||
Reference in New Issue
Block a user