Harden the provider per Momus review and prove it with offline fixtures
bin/sync-upstream self-test swaps the two network fetches in
helpers/upstream-github.sh for fixture readers and runs the production code
paths: fallback past a quarantined release, draft/prerelease filtering, the
deliberate bypass, unchanged-version and all-quarantined no-update paths,
unusable tags/timestamps and missing checksums failing the sync, {tag} and
{pkgver} asset templates with ./ and * manifest prefixes across both
architectures, the min_release_age backstop verdicts (now a testable
release_age_status function), the duration parser, and manifest validation.
Also fixes from the review: the duration parser forces base-10 arithmetic
(leading zeros no longer parse as octal) and bounds values to nine digits so
no suffix can overflow; jq // treating false as absent can no longer let
"min_release_age": false or "upstream": false slip through as unset; the
release feed page grew to the API maximum of 100 with the bounded search
documented; and the README package-metadata section documents the upstream
block, min_release_age, the bypass, and provider-versus-hook exclusivity.
This commit is contained in:
+26
-10
@@ -82,17 +82,29 @@ package_is_fast_ring() {
|
||||
|
||||
# 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.
|
||||
# hold); an unparseable value -- including a non-string/non-number JSON type
|
||||
# like false -- returns 1 so callers fail closed instead of silently dropping
|
||||
# the hold. At most 9 digits: enough for three decades in seconds, and small
|
||||
# enough that no suffix multiplication can overflow 64-bit arithmetic.
|
||||
package_min_release_age_seconds() {
|
||||
local pkgdir="$1" raw
|
||||
raw=$(package_metadata_value "$pkgdir" '.min_release_age' "")
|
||||
local pkgdir="$1" metadata raw
|
||||
metadata=$(metadata_file_for_dir "$pkgdir")
|
||||
if [[ ! -f "$metadata" ]]; then
|
||||
echo 0
|
||||
return 0
|
||||
fi
|
||||
raw=$(jq -r '
|
||||
if has("min_release_age") then
|
||||
.min_release_age | if type == "string" or type == "number" then tostring else "unparseable" end
|
||||
else "" end
|
||||
' "$metadata")
|
||||
if [[ -z "$raw" ]]; then
|
||||
echo 0
|
||||
return 0
|
||||
fi
|
||||
[[ "$raw" =~ ^([0-9]+)([smhd]?)$ ]] || return 1
|
||||
local n=${BASH_REMATCH[1]}
|
||||
[[ "$raw" =~ ^([0-9]{1,9})([smhd]?)$ ]] || return 1
|
||||
# Forced base 10: bash arithmetic would otherwise read "010" as octal.
|
||||
local n=$((10#${BASH_REMATCH[1]}))
|
||||
case "${BASH_REMATCH[2]}" in
|
||||
""|s) echo "$n" ;;
|
||||
m) echo $((n * 60)) ;;
|
||||
@@ -167,7 +179,8 @@ package_has_upstream_hook() {
|
||||
|
||||
package_has_upstream_provider() {
|
||||
local pkgdir="$1"
|
||||
[[ -n "$(package_metadata_value "$pkgdir" '.upstream.github' "")" ]]
|
||||
# `objects` drops a non-object upstream value instead of erroring jq.
|
||||
[[ -n "$(package_metadata_value "$pkgdir" '(.upstream? | objects | .github)' "")" ]]
|
||||
}
|
||||
|
||||
packages_for_upstream_sync() {
|
||||
@@ -346,15 +359,18 @@ validate_package_metadata() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# `has` rather than `// {}`: jq's // treats false as absent, which would
|
||||
# let "upstream": false slip through as an empty declaration.
|
||||
if ! jq -e '
|
||||
(.upstream // {}) | type == "object"
|
||||
and (if . == {} then true else
|
||||
if has("upstream") | not then true
|
||||
elif (.upstream | type) != "object" then false
|
||||
else .upstream |
|
||||
((.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)
|
||||
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
|
||||
|
||||
@@ -21,7 +21,24 @@
|
||||
|
||||
package_upstream_github_repo() {
|
||||
local pkgdir="$1"
|
||||
package_metadata_value "$pkgdir" '.upstream.github' ""
|
||||
# `objects` drops a non-object upstream value (validation rejects those
|
||||
# separately) instead of erroring the jq pipeline.
|
||||
package_metadata_value "$pkgdir" '(.upstream? | objects | .github)' ""
|
||||
}
|
||||
|
||||
# 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.
|
||||
github_fetch_releases() {
|
||||
local repo="$1"
|
||||
curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=100"
|
||||
}
|
||||
|
||||
github_fetch_checksums() {
|
||||
local repo="$1" tag="$2" asset="$3"
|
||||
curl -fsSL "https://github.com/$repo/releases/download/$tag/$asset"
|
||||
}
|
||||
|
||||
# Emits the newest qualifying release as hook-contract JSON. min_release_age
|
||||
@@ -35,18 +52,18 @@ github_upstream_release() {
|
||||
local metadata repo checksums_name
|
||||
metadata=$(metadata_file_for_dir "$package_dir")
|
||||
|
||||
repo=$(jq -r '.upstream.github // ""' "$metadata")
|
||||
repo=$(jq -r '(.upstream? | objects | .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")
|
||||
checksums_name=$(jq -r '(.upstream? | objects | .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")
|
||||
mapfile -t arches < <(jq -r '(.upstream? | objects | .assets) // {} | keys[]' "$metadata")
|
||||
if [[ ${#arches[@]} -eq 0 ]]; then
|
||||
echo "upstream.assets must map at least one architecture to an asset name" >&2
|
||||
return 1
|
||||
@@ -54,7 +71,7 @@ github_upstream_release() {
|
||||
|
||||
local releases now
|
||||
now=$(date +%s)
|
||||
if ! releases=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=20"); then
|
||||
if ! releases=$(github_fetch_releases "$repo"); then
|
||||
echo "could not fetch the release feed for $repo" >&2
|
||||
return 1
|
||||
fi
|
||||
@@ -108,7 +125,7 @@ github_upstream_release() {
|
||||
fi
|
||||
|
||||
local checksums
|
||||
if ! checksums=$(curl -fsSL "https://github.com/$repo/releases/download/$best_tag/$checksums_name"); then
|
||||
if ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
|
||||
echo "could not fetch $checksums_name for $repo $best_tag" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user