diff --git a/bin/sync-upstream b/bin/sync-upstream index a454f67..a0da5f3 100755 --- a/bin/sync-upstream +++ b/bin/sync-upstream @@ -171,7 +171,11 @@ release_age_status() { [[ "${BYPASS_MIN_RELEASE_AGE:-}" != "1" ]] || return 0 local published_at published_epoch published_at=$(jq -r '.published_at // empty' <<<"$release") - if [[ -z "$published_at" ]] || ! published_epoch=$(date --date="$published_at" +%s 2>/dev/null); then + # Strict ISO 8601 before GNU date sees it: date also accepts relative + # expressions like "2 days ago", which would let a buggy hook fabricate an + # age instead of failing closed. + if [[ ! "$published_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:?[0-9]{2})$ ]] \ + || ! published_epoch=$(date --date="$published_at" +%s 2>/dev/null); then return 2 fi (( $(date +%s) - published_epoch >= min_age )) || return 1 @@ -573,6 +577,12 @@ EOF rel=$(jq -n '{pkgver: "2.0.0", sha256sums: {}}') st=0; release_age_status "$rel" 86400 || st=$? check "missing published_at is unprovable" "2" "$st" + rel=$(jq -n '{pkgver: "2.0.0", published_at: "2 days ago", sha256sums: {}}') + st=0; release_age_status "$rel" 86400 || st=$? + check "relative-date expression is unprovable, not an age" "2" "$st" + rel=$(jq -n --arg p "$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%S+00:00)" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}') + st=0; release_age_status "$rel" 86400 || st=$? + check "numeric-offset ISO timestamp passes" "0" "$st" echo "Duration parser:" local agepkg="$TEMP_DIR/selftest-age" diff --git a/helpers/upstream-github.sh b/helpers/upstream-github.sh index a0e3ebc..e37e6ab 100644 --- a/helpers/upstream-github.sh +++ b/helpers/upstream-github.sh @@ -29,8 +29,10 @@ package_upstream_github_repo() { # Fetches sit behind functions so the self-test can replace them with fixture # readers; everything below the fetch is deterministic and testable offline. # Only the 100 most recent releases are considered -- a bounded search, not -# pagination. A feed whose entire first page is drafts, prereleases, or -# quarantined releases reports no update and waits for the next run. +# pagination. Quarantined releases report no update and wait for the next +# run; a page with no stable release at all (drafts and prereleases only) +# fails the sync instead, because a provider-tracked feed suddenly shipping +# nothing stable is an anomaly worth a loud error, not a silent skip. github_fetch_releases() { local repo="$1" curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=100"