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.
This commit is contained in:
+40
-1
@@ -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
|
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.
|
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 <package>. Scheduled automation never sets
|
||||||
|
the bypass, so the resulting change still goes through a reviewed PR.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
PACKAGE One or more package names to update (optional)
|
PACKAGE One or more package names to update (optional)
|
||||||
|
|
||||||
@@ -170,6 +180,7 @@ validate_release() {
|
|||||||
and (.sha256sums | to_entries | all(
|
and (.sha256sums | to_entries | all(
|
||||||
.value | type == "array" and length > 0 and all(test("\\A[0-9a-f]{64}\\z"))
|
.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
|
' <<<"$release" >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,10 +315,20 @@ sync_package() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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..."
|
print_info "Checking $package for upstream releases..."
|
||||||
|
|
||||||
local release
|
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"
|
print_error "Upstream hook failed for $package"
|
||||||
((++FAILED))
|
((++FAILED))
|
||||||
return 0
|
return 0
|
||||||
@@ -331,6 +352,24 @@ sync_package() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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
|
local pkgver current_pkgver
|
||||||
pkgver=$(jq -r '.pkgver' <<<"$release")
|
pkgver=$(jq -r '.pkgver' <<<"$release")
|
||||||
current_pkgver=$(get_pkgver "$package_dir")
|
current_pkgver=$(get_pkgver "$package_dir")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
|
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
|
||||||
# { "source": "aur", "rebuild_on": ["qt6-base"] }
|
# { "source": "aur", "rebuild_on": ["qt6-base"] }
|
||||||
# { "source": "local" }
|
# { "source": "local" }
|
||||||
|
# { "source": "local", "min_release_age": "24h" }
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
@@ -78,6 +79,27 @@ package_is_fast_ring() {
|
|||||||
[[ "$(package_release_ring "$pkgdir")" == "fast" ]]
|
[[ "$(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() {
|
package_build_skipped() {
|
||||||
local pkgdir="$1"
|
local pkgdir="$1"
|
||||||
local metadata skip_build
|
local metadata skip_build
|
||||||
@@ -313,6 +335,11 @@ validate_package_metadata() {
|
|||||||
*) echo "invalid release_ring for $(basename "$pkgdir"): $ring"; return 1 ;;
|
*) echo "invalid release_ring for $(basename "$pkgdir"): $ring"; return 1 ;;
|
||||||
esac
|
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")
|
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) ;;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"release_ring": "fast"
|
"release_ring": "fast",
|
||||||
|
"min_release_age": "24h"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ set -euo pipefail
|
|||||||
repo="jdx/mise"
|
repo="jdx/mise"
|
||||||
|
|
||||||
# Keep a compromised mise release from reaching Omarchy before there has been
|
# 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
|
# time for maintainers and the community to notice and pull it. The window
|
||||||
# the release list instead of gating on /releases/latest alone means mise's
|
# comes from min_release_age in .omarchy/package.json, exported by
|
||||||
# near-daily cadence cannot starve updates: the newest release that has
|
# bin/sync-upstream as MIN_RELEASE_AGE_SECONDS. Walking the release list
|
||||||
# finished its quarantine ships even while an even newer one is still inside
|
# instead of gating on /releases/latest alone means mise's near-daily cadence
|
||||||
# it. Nothing younger than the window ever ships without the explicit bypass.
|
# cannot starve updates: the newest release that has finished its quarantine
|
||||||
minimum_release_age_seconds=$((24 * 60 * 60))
|
# 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)
|
now=$(date +%s)
|
||||||
|
|
||||||
releases=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=20")
|
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
|
candidates=0
|
||||||
best_tag=""
|
best_tag=""
|
||||||
best_pkgver=""
|
best_pkgver=""
|
||||||
|
best_published_at=""
|
||||||
while IFS=$'\t' read -r tag published_at; do
|
while IFS=$'\t' read -r tag published_at; do
|
||||||
if [[ ! "$tag" =~ ^v([A-Za-z0-9._+]+)$ ]]; then
|
if [[ ! "$tag" =~ ^v([A-Za-z0-9._+]+)$ ]]; then
|
||||||
echo "mise release has an invalid tag: ${tag:-<empty>}" >&2
|
echo "mise release has an invalid tag: ${tag:-<empty>}" >&2
|
||||||
@@ -31,7 +35,7 @@ while IFS=$'\t' read -r tag published_at; do
|
|||||||
candidates=$((candidates + 1))
|
candidates=$((candidates + 1))
|
||||||
|
|
||||||
if (( now - published_epoch < minimum_release_age_seconds )); then
|
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
|
echo "Bypassing mise release-age gate for $tag" >&2
|
||||||
else
|
else
|
||||||
continue
|
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
|
if [[ -z "$best_pkgver" ]] || [[ "$(vercmp "$pkgver" "$best_pkgver")" -gt 0 ]]; then
|
||||||
best_tag=$tag
|
best_tag=$tag
|
||||||
best_pkgver=$pkgver
|
best_pkgver=$pkgver
|
||||||
|
best_published_at=$published_at
|
||||||
fi
|
fi
|
||||||
done < <(jq -r '.[] | select((.draft or .prerelease) | not) | [.tag_name // empty, .published_at // empty] | @tsv' <<<"$releases")
|
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 \
|
jq -n \
|
||||||
--arg pkgver "$pkgver" \
|
--arg pkgver "$pkgver" \
|
||||||
|
--arg published_at "$best_published_at" \
|
||||||
--arg x86_64 "$x86_64" \
|
--arg x86_64 "$x86_64" \
|
||||||
--arg aarch64 "$aarch64" \
|
--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]}}'
|
||||||
|
|||||||
Reference in New Issue
Block a user