From 48ad6b9d7b376ff13266471290bf5693ae8946cd Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 24 Aug 2026 19:17:22 -0400 Subject: [PATCH] Generalize the release-age quarantine into a manifest policy Move the hold from a mise-only hardcode to min_release_age in .omarchy/package.json ("24h", "2d", or bare seconds), alongside source and release_ring where package policy already lives. bin/sync-upstream exports the window to every hook as MIN_RELEASE_AGE_SECONDS so a hook that can walk its release feed selects the newest release that has cleared it, and enforces it as a backstop: with a policy set, the hook must report published_at, and a release younger than the window is treated as no update. A hook that cannot prove the age fails the sync rather than shipping unverified. BYPASS_MIN_RELEASE_AGE=1 replaces the package-specific bypass for deliberate emergency updates; scheduled automation never sets it. The mise hook keeps its release-list walk but reads the window from the environment and reports published_at; the other upstream hooks are untouched and unaffected until they opt in. --- bin/sync-upstream | 41 +++++++++++++++++++++++- helpers/package-metadata.sh | 27 ++++++++++++++++ pkgbuilds/mise-bin/.omarchy/package.json | 3 +- pkgbuilds/mise-bin/.omarchy/upstream.sh | 22 ++++++++----- 4 files changed, 83 insertions(+), 10 deletions(-) diff --git a/bin/sync-upstream b/bin/sync-upstream index 93e6f0d..70567cf 100755 --- a/bin/sync-upstream +++ b/bin/sync-upstream @@ -31,6 +31,16 @@ the unsuffixed sha256sums array. An empty object ({}) reports no update. When the reported version is newer than the checked-in one, pkgver and the listed checksum arrays are rewritten and pkgrel is reset to 1. +A package may declare "min_release_age" in .omarchy/package.json ("24h", "2d", +or bare seconds) to quarantine fresh releases until maintainers have had time +to pull a bad or compromised one. The window is exported to the hook as +MIN_RELEASE_AGE_SECONDS so it can select the newest release that has already +cleared it, and enforced here as a backstop: the hook must then report +"published_at" (ISO 8601), and a release younger than the window is treated +as no update. A maintainer shipping an emergency update inside the window +runs: BYPASS_MIN_RELEASE_AGE=1 $0 . Scheduled automation never sets +the bypass, so the resulting change still goes through a reviewed PR. + Arguments: PACKAGE One or more package names to update (optional) @@ -170,6 +180,7 @@ validate_release() { and (.sha256sums | to_entries | all( .value | type == "array" and length > 0 and all(test("\\A[0-9a-f]{64}\\z")) )) + and (if has("published_at") then (.published_at | type == "string" and length > 0) else true end) ' <<<"$release" >/dev/null } @@ -304,10 +315,20 @@ sync_package() { return 0 fi + local min_age + if ! min_age=$(package_min_release_age_seconds "$package_dir"); then + print_error "Invalid min_release_age in $package_dir/.omarchy/package.json" + ((++FAILED)) + return 0 + fi + print_info "Checking $package for upstream releases..." local release - if ! release=$(cd "$package_dir" && PACKAGE_NAME="$package" bash .omarchy/upstream.sh); then + if ! 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 print_error "Upstream hook failed for $package" ((++FAILED)) return 0 @@ -331,6 +352,24 @@ sync_package() { return 0 fi + # Backstop for min_release_age: the hook already selects within the window, + # but a hook bug must not be able to ship a release younger than the policy. + if (( min_age > 0 )) && [[ "${BYPASS_MIN_RELEASE_AGE:-}" != "1" ]]; then + local published_at published_epoch age + published_at=$(jq -r '.published_at // empty' <<<"$release") + if [[ -z "$published_at" ]] || ! published_epoch=$(date --date="$published_at" +%s 2>/dev/null); then + print_error "min_release_age is set for $package but its hook reported no usable published_at; refusing an unverifiable release" + ((++FAILED)) + return 0 + fi + age=$(( $(date +%s) - published_epoch )) + if (( age < min_age )); then + print_warning " Hook reported a release only $((age / 3600))h old, inside the ${min_age}s minimum age; leaving it alone" + ((++SKIPPED)) + return 0 + fi + fi + local pkgver current_pkgver pkgver=$(jq -r '.pkgver' <<<"$release") current_pkgver=$(get_pkgver "$package_dir") diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh index 83ed37a..0d495bd 100644 --- a/helpers/package-metadata.sh +++ b/helpers/package-metadata.sh @@ -12,6 +12,7 @@ # { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } } # { "source": "aur", "rebuild_on": ["qt6-base"] } # { "source": "local" } +# { "source": "local", "min_release_age": "24h" } # # bin/sync-aur also writes upstream_commit for AUR-backed packages, and # bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on. @@ -78,6 +79,27 @@ package_is_fast_ring() { [[ "$(package_release_ring "$pkgdir")" == "fast" ]] } +# Quarantine window for upstream releases, in seconds. Accepts a bare number +# of seconds or a number suffixed s/m/h/d ("24h", "2d"). Unset means 0 (no +# hold); an unparseable value returns 1 so callers fail closed instead of +# silently dropping the hold. +package_min_release_age_seconds() { + local pkgdir="$1" raw + raw=$(package_metadata_value "$pkgdir" '.min_release_age' "") + if [[ -z "$raw" ]]; then + echo 0 + return 0 + fi + [[ "$raw" =~ ^([0-9]+)([smhd]?)$ ]] || return 1 + local n=${BASH_REMATCH[1]} + case "${BASH_REMATCH[2]}" in + ""|s) echo "$n" ;; + m) echo $((n * 60)) ;; + h) echo $((n * 3600)) ;; + d) echo $((n * 86400)) ;; + esac +} + package_build_skipped() { local pkgdir="$1" local metadata skip_build @@ -313,6 +335,11 @@ validate_package_metadata() { *) echo "invalid release_ring for $(basename "$pkgdir"): $ring"; return 1 ;; esac + if ! package_min_release_age_seconds "$pkgdir" >/dev/null; then + echo "invalid min_release_age for $(basename "$pkgdir"): must be a number with optional s/m/h/d suffix" + 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/pkgbuilds/mise-bin/.omarchy/package.json b/pkgbuilds/mise-bin/.omarchy/package.json index db153c3..bbe9ae1 100644 --- a/pkgbuilds/mise-bin/.omarchy/package.json +++ b/pkgbuilds/mise-bin/.omarchy/package.json @@ -1,4 +1,5 @@ { "source": "local", - "release_ring": "fast" + "release_ring": "fast", + "min_release_age": "24h" } diff --git a/pkgbuilds/mise-bin/.omarchy/upstream.sh b/pkgbuilds/mise-bin/.omarchy/upstream.sh index cad158a..8db9297 100644 --- a/pkgbuilds/mise-bin/.omarchy/upstream.sh +++ b/pkgbuilds/mise-bin/.omarchy/upstream.sh @@ -4,12 +4,15 @@ set -euo pipefail repo="jdx/mise" # Keep a compromised mise release from reaching Omarchy before there has been -# a full day for maintainers and the community to notice and pull it. 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. -minimum_release_age_seconds=$((24 * 60 * 60)) +# 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") @@ -17,6 +20,7 @@ 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 @@ -31,7 +35,7 @@ while IFS=$'\t' read -r tag published_at; do candidates=$((candidates + 1)) if (( now - published_epoch < minimum_release_age_seconds )); then - if [[ "${MISE_BIN_BYPASS_RELEASE_AGE:-}" == "1" ]]; then + if [[ "${BYPASS_MIN_RELEASE_AGE:-}" == "1" ]]; then echo "Bypassing mise release-age gate for $tag" >&2 else continue @@ -41,6 +45,7 @@ while IFS=$'\t' read -r tag published_at; do 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") @@ -78,6 +83,7 @@ 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, sha256sums: {x86_64: [$x86_64], aarch64: [$aarch64]}}' + '{pkgver: $pkgver, published_at: $published_at, sha256sums: {x86_64: [$x86_64], aarch64: [$aarch64]}}'