#!/bin/bash set -euo pipefail BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/package-metadata.sh" source "$BUILD_ROOT/helpers/upstream-github.sh" TEMP_DIR=$(mktemp -d) trap 'rm -rf "$TEMP_DIR"' EXIT export UPSTREAM_CACHE_DIR="$TEMP_DIR/watch-cache" SPECIFIC_PACKAGES=() usage() { cat </.omarchy/upstream.sh, a hook that reports JSON on stdout: { "pkgver": "1.2.3", "sha256sums": { "x86_64": [""], "aarch64": [""] } } Architecture keys become sha256sums_ in the PKGBUILD; the key "any" means the unsuffixed sha256sums array. An empty object ({}) reports no update. Optional "variables" maps existing underscore-prefixed release scalars to values containing only letters, digits, periods, underscores, plus, colon and hyphen. They are verified and written in the same atomic replacement as pkgver and checksums. 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) Commands: self-test Run the offline fixture tests for release selection, the quarantine backstop, and metadata parsing Examples: $0 # Update every package with an upstream source $0 openai-codex-desktop # Update specific packages EOF } while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; --*) print_error "Unknown option: $1" exit 1 ;; *) SPECIFIC_PACKAGES+=("$1") shift ;; esac done if ! command -v vercmp >/dev/null 2>&1; then print_error "vercmp not found: this needs pacman to decide whether a release is an upgrade" exit 1 fi print_header "Upstream Package Sync" UPDATED=0 SKIPPED=0 FAILED=0 SPECIFIC_MODE=false get_pkgver() { local package_dir="$1" grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'" } assert_single_assignment() { local pkgbuild="$1" local pattern="$2" local label="$3" if [[ $(grep -c "$pattern" "$pkgbuild") -ne 1 ]]; then print_error "Expected exactly one $label assignment in $pkgbuild" return 1 fi } set_pkgbuild_scalar() { local pkgbuild="$1" local field="$2" local value="$3" assert_single_assignment "$pkgbuild" "^${field}=" "$field" || return 1 sed -i "s/^${field}=.*/${field}=${value}/" "$pkgbuild" } # Replace an array assignment, however many lines the original spans. set_pkgbuild_array() { local pkgbuild="$1" local name="$2" shift 2 local values=("$@") assert_single_assignment "$pkgbuild" "^${name}=(" "$name" || return 1 if [[ ${#values[@]} -eq 0 ]]; then print_error "No values to write for ${name}" return 1 fi local block="$TEMP_DIR/array-block" if [[ ${#values[@]} -eq 1 ]]; then printf "%s=('%s')\n" "$name" "${values[0]}" > "$block" || return 1 else { printf '%s=(\n' "$name" printf " '%s'\n" "${values[@]}" printf ')\n' } > "$block" || return 1 fi local rewritten="$TEMP_DIR/pkgbuild-rewritten" if ! awk -v prefix="${name}=(" -v block="$block" ' !replaced && index($0, prefix) == 1 { while ((getline line < block) > 0) print line close(block) replaced = 1 # A ")" anywhere past the opening closes the array; testing for one at end # of line instead would treat a trailing comment as a continuation and eat # every line up to the next ")". if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1 next } skipping { if ($0 ~ /\)/) skipping = 0; next } { print } ' "$pkgbuild" > "$rewritten"; then print_error "Failed to rewrite ${name} in $pkgbuild" rm -f "$rewritten" return 1 fi mv "$rewritten" "$pkgbuild" } # Backstop verdict for a reported release against min_release_age. Returns 0 # when old enough (or no policy is set, or the bypass is deliberate), 1 when # the release is younger than the window, 2 when the report carries no usable # published_at and the age cannot be established at all. release_age_status() { local release="$1" min_age="$2" (( min_age > 0 )) || return 0 [[ "${BYPASS_MIN_RELEASE_AGE:-}" != "1" ]] || return 0 local published_at published_epoch published_at=$(jq -r '.published_at // empty' <<<"$release") # 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 } # pacman's own comparator, because nothing else agrees with it at the corners: # sort -V calls 1.0a newer than 1.0, vercmp calls it older, and pacman is what # decides whether a published package is an upgrade. version_is_newer() { local candidate="$1" local current="$2" [[ "$candidate" != "$current" ]] || return 1 [[ "$(vercmp "$candidate" "$current")" -gt 0 ]] } validate_release() { local release="$1" # pkgver is written into the PKGBUILD, which makepkg sources as shell, so it # is held to pacman's own character set rather than merely being non-empty. # The anchors are \A and \z, not ^ and $: jq's $ also matches before a # trailing newline, which would let "1.0\n" through and break the rewrite. jq -e ' (.pkgver | type == "string" and test("\\A[A-Za-z0-9._+]+\\z")) and (.sha256sums | type == "object" and length > 0) and (.sha256sums | to_entries | all( .key | test("\\A[a-z0-9_]+\\z") )) and (.sha256sums | to_entries | all( .value | type == "array" and length > 0 and all(test("\\A[0-9a-f]{64}\\z")) )) and (if has("variables") then (.variables | type == "object" and (to_entries | all( (.key | test("\\A_[a-z][a-z0-9_]*\\z")) and (.value | type == "string" and test("\\A[A-Za-z0-9._+:-]+\\z")) ))) else true end) and (if has("published_at") then (.published_at | type == "string" and length > 0) else true end) ' <<<"$release" >/dev/null } # Confirm the rewritten PKGBUILD parses and actually holds what we meant to put # in it. Editing shell with awk and sed can go wrong in ways no amount of # pattern-matching anticipates -- an array element carrying a ")" in a comment, # say -- so the result is checked rather than trusted. verify_pkgbuild() { local pkgbuild="$1" local release="$2" local pkgver="$3" shift 3 local arrays=("$@") if ! bash -n "$pkgbuild" 2>/dev/null; then print_error "Rewritten PKGBUILD is not valid shell" return 1 fi local dump if ! dump=$(CARCH=x86_64 bash -c ' source "$1" >/dev/null 2>&1 || exit 1 printf "pkgver\t%s\n" "$pkgver" printf "pkgrel\t%s\n" "$pkgrel" for name in "${@:2}"; do declare -n array="$name" printf "%s\t%s\n" "$name" "${array[*]}" done ' _ "$pkgbuild" "${arrays[@]}" 2>/dev/null); then print_error "Rewritten PKGBUILD could not be read back" return 1 fi local expected expected=$( printf 'pkgver\t%s\n' "$pkgver" printf 'pkgrel\t1\n' local array arch for array in "${arrays[@]}"; do arch="${array#sha256sums}" arch="${arch#_}" [[ -n "$arch" ]] || arch="any" printf '%s\t%s\n' "$array" \ "$(jq -r --arg arch "$arch" '.sha256sums[$arch] | join(" ")' <<<"$release")" done ) if [[ "$dump" != "$expected" ]]; then print_error "Rewritten PKGBUILD does not hold the reported release" diff <(echo "$expected") <(echo "$dump") | sed 's/^/ /' >&2 || true return 1 fi } apply_release() { local package_dir="$1" local release="$2" local pkgver="$3" local pkgbuild="$package_dir/PKGBUILD" local arch array values local arrays=() while IFS= read -r arch; do if [[ "$arch" == "any" ]]; then array="sha256sums" else array="sha256sums_$arch" fi arrays+=("$array") done < <(jq -r '.sha256sums | keys[]' <<<"$release") # validate_release guarantees at least one entry, so an empty list here means # jq died inside the process substitution rather than that there is nothing # to do. if [[ ${#arrays[@]} -eq 0 ]]; then print_error "Could not read the checksum architectures from the reported release" return 1 fi # Every edit lands on a scratch copy that replaces the PKGBUILD in one rename # at the end, so a failure part way through leaves the original untouched # rather than half updated. local scratch="$pkgbuild.sync-upstream" cp "$pkgbuild" "$scratch" || return 1 if ! ( assert_single_assignment "$scratch" '^pkgver=' pkgver || exit 1 assert_single_assignment "$scratch" '^pkgrel=' pkgrel || exit 1 for array in "${arrays[@]}"; do arch="${array#sha256sums}" arch="${arch#_}" [[ -n "$arch" ]] || arch="any" mapfile -t values < <(jq -r --arg arch "$arch" '.sha256sums[$arch][]' <<<"$release") set_pkgbuild_array "$scratch" "$array" "${values[@]}" || exit 1 done set_pkgbuild_scalar "$scratch" pkgver "$pkgver" || exit 1 set_pkgbuild_scalar "$scratch" pkgrel 1 || exit 1 local field value while IFS=$'\t' read -r field value; do [[ -n "$field" ]] || continue set_pkgbuild_scalar "$scratch" "$field" "$value" || exit 1 [[ $(bash -c 'source "$1"; printf "%s" "${!2}"' _ "$scratch" "$field") == "$value" ]] || exit 1 done < <(jq -r '(.variables // {}) | to_entries[] | [.key, .value] | @tsv' <<<"$release") verify_pkgbuild "$scratch" "$release" "$pkgver" "${arrays[@]}" || exit 1 ); then rm -f "$scratch" return 1 fi chmod --reference="$pkgbuild" "$scratch" mv "$scratch" "$pkgbuild" } sync_package() { local package="$1" local package_dir="$PKGBUILDS_DIR/$package" local hook="$package_dir/.omarchy/upstream.sh" if [[ ! -f "$package_dir/PKGBUILD" ]]; then print_error "Package $package has no PKGBUILD" ((++FAILED)) return 0 fi if jq -e '.sync == false' "$package_dir/.omarchy/package.json" >/dev/null 2>&1; then print_info "Skipping $package: upstream updates held by sync=false" ((++SKIPPED)) return 0 fi local provider has_upstream=false provider=$(package_upstream_provider "$package_dir") if package_has_upstream_provider "$package_dir"; then has_upstream=true fi # A present-but-unusable declaration fails loudly; treating it like "no # upstream source" would silently drop the package from scheduled runs. if [[ "$has_upstream" == true && -z "$provider" ]]; then print_error "Package $package has an unusable or ambiguous upstream declaration" ((++FAILED)) return 0 fi if [[ -n "$provider" && -f "$hook" ]]; then print_error "Package $package declares both an upstream provider and an upstream.sh hook; keep exactly one" ((++FAILED)) return 0 fi if [[ -z "$provider" && ! -f "$hook" ]]; then if [[ "$SPECIFIC_MODE" == true ]]; then print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh" ((++FAILED)) else print_info "Skipping $package: no upstream source" ((++SKIPPED)) fi 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..." if [[ "$provider" == watch ]]; then local result if ! result=$(python3 "$BUILD_ROOT/helpers/upstream-watch.py" sync "$package_dir" --min-age "$min_age"); then print_error "Upstream watch failed for $package" ((++FAILED)) elif [[ $(jq -r .status <<<"$result") == updated ]]; then print_success " $(jq -r '.before + " -> " + .after' <<<"$result")" ((++UPDATED)) else print_info " $(jq -r '.reason' <<<"$result")" ((++SKIPPED)) fi return 0 fi local release release_status=0 if [[ -n "$provider" ]]; then case "$provider" in github) release=$(github_upstream_release "$package_dir" "$min_age") || release_status=$? ;; git_tags) release=$(git_tags_upstream_release "$package_dir") || release_status=$? ;; npm) release=$(npm_upstream_release "$package_dir") || release_status=$? ;; debian) release=$(debian_upstream_release "$package_dir") || release_status=$? ;; esac if [[ ${release_status:-0} -ne 0 ]]; then print_error "$provider upstream provider failed for $package" ((++FAILED)) return 0 fi elif ! 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 fi if ! jq -e . >/dev/null 2>&1 <<<"$release"; then print_error "Upstream hook for $package did not report valid JSON" ((++FAILED)) return 0 fi if [[ "$(jq -r 'has("pkgver")' <<<"$release")" == "false" ]]; then print_info " No upstream update reported" ((++SKIPPED)) return 0 fi if ! validate_release "$release"; then print_error "Upstream hook for $package reported a malformed release" ((++FAILED)) return 0 fi # Backstop for min_release_age: the selection already honors the window, # but a provider or hook bug must not be able to ship a release younger # than the policy. local age_status=0 release_age_status "$release" "$min_age" || age_status=$? case "$age_status" in 1) print_warning " Reported release is inside the ${min_age}s minimum release age; leaving it alone" ((++SKIPPED)) return 0 ;; 2) print_error "min_release_age is set for $package but its source reported no usable published_at; refusing an unverifiable release" ((++FAILED)) return 0 ;; esac local pkgver current_pkgver pkgver=$(jq -r '.pkgver' <<<"$release") current_pkgver=$(get_pkgver "$package_dir") if [[ -z "$current_pkgver" ]]; then print_error "Could not read pkgver from $package_dir/PKGBUILD" ((++FAILED)) return 0 fi if [[ "$pkgver" == "$current_pkgver" ]]; then print_info " Already at $current_pkgver" ((++SKIPPED)) return 0 fi if ! version_is_newer "$pkgver" "$current_pkgver"; then print_warning " Upstream reports $pkgver, older than the checked-in $current_pkgver; leaving it alone" ((++SKIPPED)) return 0 fi if ! apply_release "$package_dir" "$release" "$pkgver"; then print_error "Failed to update $package" ((++FAILED)) return 0 fi print_success " $current_pkgver -> $pkgver" ((++UPDATED)) } # Offline fixture tests: the network fetches in helpers/upstream-github.sh # are swapped for fixture readers, everything else runs the production code # paths. Covers release selection (fallback past quarantined releases, # draft/prerelease filtering, bypass, unchanged version), failure paths # (unusable tags/timestamps, missing checksums), checksum template mapping # for both architectures, release API digests, the min_release_age backstop, # the duration parser, and manifest validation. cmd_self_test() { local failures=0 check() { local desc="$1" expected="$2" got="$3" if [[ "$expected" == "$got" ]]; then echo " ok: $desc" else echo " FAIL: $desc (expected '$expected', got '$got')" failures=$((failures + 1)) fi } local pkg="$TEMP_DIR/selftest-pkg" mkdir -p "$pkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=1\n' > "$pkg/PKGBUILD" cat > "$pkg/.omarchy/package.json" <<'EOF' { "source": "local", "min_release_age": "24h", "upstream": { "github": "example/tool", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "tool-{tag}-x64.tar.xz", "aarch64": "tool-v{pkgver}-arm64.tar.xz" } } } EOF local young old2d old3d young=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) old2d=$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%SZ) old3d=$(date -u -d '3 days ago' +%Y-%m-%dT%H:%M:%SZ) # v2.0.0 is inside the 24h window; v1.9.9/v1.9.8 are a prerelease and a # draft that would outrank v1.9.0 if the filters failed. local sum_x19 sum_a19 sum_x20 sum_a20 sum_x19=$(printf 'a%.0s' {1..64}) sum_a19=$(printf 'b%.0s' {1..64}) sum_x20=$(printf 'c%.0s' {1..64}) sum_a20=$(printf 'd%.0s' {1..64}) FIXTURE_RELEASES=$(jq -n --arg young "$young" --arg old2 "$old2d" --arg old3 "$old3d" '[ {tag_name: "v2.0.0", published_at: $young, draft: false, prerelease: false}, {tag_name: "v1.9.9", published_at: $old2, draft: false, prerelease: true}, {tag_name: "v1.9.8", published_at: $old2, draft: true, prerelease: false}, {tag_name: "v1.9.0", published_at: $old2, draft: false, prerelease: false}, {tag_name: "v1.8.0", published_at: $old3, draft: false, prerelease: false} ]') FIXTURE_CHECKSUMS=$(printf '%s\n' \ "$sum_x19 ./tool-v1.9.0-x64.tar.xz" \ "$sum_a19 tool-v1.9.0-arm64.tar.xz" \ "$sum_x20 *tool-v2.0.0-x64.tar.xz" \ "$sum_a20 tool-v2.0.0-arm64.tar.xz") github_fetch_releases() { printf '%s' "$FIXTURE_RELEASES"; } github_fetch_checksums() { printf '%s\n' "$FIXTURE_CHECKSUMS"; } echo "Release selection:" local out out=$(github_upstream_release "$pkg" 86400 2>/dev/null) || out="" check "quarantine falls back past the young v2.0.0" "1.9.0" "$(jq -r '.pkgver // ""' <<<"$out")" check "selected release reports its published_at" "$old2d" "$(jq -r '.published_at // ""' <<<"$out")" check "x86_64 checksum via {tag} template and ./ prefix" "$sum_x19" "$(jq -r '.sha256sums.x86_64[0] // ""' <<<"$out")" check "aarch64 checksum via {pkgver} template" "$sum_a19" "$(jq -r '.sha256sums.aarch64[0] // ""' <<<"$out")" out=$(github_upstream_release "$pkg" 0 2>/dev/null) || out="" check "no policy selects the newest stable release" "2.0.0" "$(jq -r '.pkgver // ""' <<<"$out")" check "prerelease v1.9.9 and draft v1.9.8 are never selected" "" "$(jq -r 'select(.pkgver == "1.9.9" or .pkgver == "1.9.8") | .pkgver' <<<"$out")" out=$(BYPASS_MIN_RELEASE_AGE=1 github_upstream_release "$pkg" 86400 2>/dev/null) || out="" check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // ""' <<<"$out")" check "x86_64 checksum via * binary-mode prefix" "$sum_x20" "$(jq -r '.sha256sums.x86_64[0] // ""' <<<"$out")" cp "$pkg/.omarchy/package.json" "$pkg/normal.json" jq '.upstream.latest_only = true' "$pkg/normal.json" > "$pkg/.omarchy/package.json" FIXTURE_RELEASES=$(jq -n --arg old "$old2d" '[ {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false}, {tag_name: "v0.4.1-8", published_at: $old, draft: false, prerelease: false} ]') FIXTURE_CHECKSUMS=$(printf '%s\n' \ "$sum_x19 ./tool-v1.9.0-x64.tar.xz" \ "$sum_a19 ./tool-v1.9.0-arm64.tar.xz") out=$(github_upstream_release "$pkg" 0 2>/dev/null) || out="" check "latest_only ignores incompatible historical tags" "1.9.0" "$(jq -r '.pkgver // ""' <<<"$out")" cp "$pkg/normal.json" "$pkg/.omarchy/package.json" FIXTURE_RELEASES=$(jq -n --arg young "$young" --arg old2 "$old2d" --arg old3 "$old3d" '[ {tag_name: "v2.0.0", published_at: $young, draft: false, prerelease: false}, {tag_name: "v1.9.9", published_at: $old2, draft: false, prerelease: true}, {tag_name: "v1.9.8", published_at: $old2, draft: true, prerelease: false}, {tag_name: "v1.9.0", published_at: $old2, draft: false, prerelease: false}, {tag_name: "v1.8.0", published_at: $old3, draft: false, prerelease: false} ]') FIXTURE_CHECKSUMS=$(printf '%s\n' \ "$sum_x19 ./tool-v1.9.0-x64.tar.xz" \ "$sum_a19 tool-v1.9.0-arm64.tar.xz" \ "$sum_x20 *tool-v2.0.0-x64.tar.xz" \ "$sum_a20 tool-v2.0.0-arm64.tar.xz") out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="" check "everything quarantined reports no update" "{}" "$(jq -c . <<<"$out")" printf 'pkgver=1.9.0\npkgrel=1\n' > "$pkg/PKGBUILD" out=$(github_upstream_release "$pkg" 86400 2>/dev/null) || out="" check "already checked in reports no update" "{}" "$(jq -c . <<<"$out")" printf 'pkgver=1.0.0\npkgrel=1\n' > "$pkg/PKGBUILD" echo "Failure paths:" local rc FIXTURE_RELEASES=$(jq -n '[{tag_name: "v1.9.0", published_at: "not-a-date", draft: false, prerelease: false}]') rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$? check "invalid published_at fails the sync" "1" "$rc" FIXTURE_RELEASES=$(jq -n --arg old "$old2d" '[{tag_name: "release 1.9!", published_at: $old, draft: false, prerelease: false}]') rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$? check "unusable tag fails the sync" "1" "$rc" FIXTURE_RELEASES=$(jq -n --arg old "$old2d" '[{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false}]') FIXTURE_CHECKSUMS="$sum_x19 ./tool-v1.9.0-x64.tar.xz" rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$? check "missing aarch64 checksum fails the sync" "1" "$rc" # A vendor publishing no manifest: checksums come from the digests the # release API reports per asset, with nothing fetched beyond the feed. echo "Release API digests:" local digpkg="$TEMP_DIR/selftest-digests" mkdir -p "$digpkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=1\n' > "$digpkg/PKGBUILD" cat > "$digpkg/.omarchy/package.json" <<'EOF' { "source": "local", "upstream": { "github": "example/tool", "digests": true, "assets": { "x86_64": "tool-{pkgver}-1-x86_64.pkg.tar.zst", "aarch64": "tool-{pkgver}-1-aarch64.pkg.tar.zst" } } } EOF FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg a "$sum_a19" '[ {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [ {name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)}, {name: "tool-1.9.0-1-aarch64.pkg.tar.zst", digest: ("sha256:" + $a)}, {name: "tool-1.9.0-1-x86_64.rpm", digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"} ]} ]') FIXTURE_CHECKSUMS="manifest must not be consulted" out=$(github_upstream_release "$digpkg" 0 2>/dev/null) || out="" check "x86_64 checksum via the asset digest" "$sum_x19" "$(jq -r '.sha256sums.x86_64[0] // ""' <<<"$out")" check "aarch64 checksum via the asset digest" "$sum_a19" "$(jq -r '.sha256sums.aarch64[0] // ""' <<<"$out")" FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" '[ {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [ {name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)}, {name: "tool-1.9.0-1-aarch64.pkg.tar.zst"} ]} ]') rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$? check "asset without a digest fails the sync" "1" "$rc" FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" '[ {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [ {name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)}, {name: "tool-1.9.0-2-aarch64.pkg.tar.zst", digest: ("sha256:" + $x)} ]} ]') rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$? check "asset re-cut under another release number fails the sync" "1" "$rc" FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg a "$sum_a19" '[ {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [ {name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: $x}, {name: "tool-1.9.0-1-aarch64.pkg.tar.zst", digest: ("sha256:" + $a)} ]} ]') rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$? check "digest without the sha256: prefix fails the sync" "1" "$rc" # The provider enforces the declaration shape itself: scheduled runs reach # it without validate_package_metadata. jq '.upstream.checksums = "SHASUMS256.txt"' "$digpkg/.omarchy/package.json" > "$digpkg/both.json" cp "$digpkg/.omarchy/package.json" "$digpkg/good.json" cp "$digpkg/both.json" "$digpkg/.omarchy/package.json" rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$? check "provider rejects checksums and digests together" "1" "$rc" jq '.upstream.digests = "true"' "$digpkg/good.json" > "$digpkg/.omarchy/package.json" rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$? check "provider rejects a non-boolean digests" "1" "$rc" cp "$digpkg/good.json" "$digpkg/.omarchy/package.json" echo "Ordered GitHub assets with supplemental sources:" local multipkg="$TEMP_DIR/selftest-multi-assets" multi_sum support_sum multi_vst mkdir -p "$multipkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=1\n' > "$multipkg/PKGBUILD" cat > "$multipkg/.omarchy/package.json" <<'EOF' { "source": "local", "upstream": { "github": "example/tool", "digests": true, "assets": { "x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"], "aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"] }, "sources": { "any": ["https://example.test/tool/{tag}/support.txt"] } } } EOF local sum_x19_sig sum_a19_sig sum_x19_sig=$(printf '1%.0s' {1..64}) sum_a19_sig=$(printf '2%.0s' {1..64}) FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg xs "$sum_x19_sig" \ --arg a "$sum_a19" --arg as "$sum_a19_sig" '[ {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [ {name: "tool-1.9.0-x86_64", digest: ("sha256:" + $x)}, {name: "tool-1.9.0-x86_64.asc", digest: ("sha256:" + $xs)}, {name: "tool-1.9.0-aarch64", digest: ("sha256:" + $a)}, {name: "tool-1.9.0-aarch64.asc", digest: ("sha256:" + $as)} ]} ]') upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; } multi_sum=$(github_upstream_release "$multipkg" 0 2>/dev/null) || multi_sum="" support_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/v1.9.0/support.txt' | sha256sum | cut -d' ' -f1) check "ordered GitHub assets produce an ordered checksum array" "$sum_x19 $sum_x19_sig" \ "$(jq -r '.sha256sums.x86_64 | join(" ")' <<<"$multi_sum")" check "GitHub supplemental source is downloaded and hashed" "$support_sum" \ "$(jq -r '.sha256sums.any[0]' <<<"$multi_sum")" multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$? check "GitHub asset lists and disjoint sources validate" "0" "$multi_vst" jq '.upstream.sources.x86_64 = ["https://example.test/duplicate"]' \ "$multipkg/.omarchy/package.json" > "$multipkg/overlap.json" cp "$multipkg/.omarchy/package.json" "$multipkg/good.json" cp "$multipkg/overlap.json" "$multipkg/.omarchy/package.json" multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$? check "GitHub assets and sources cannot target the same checksum array" "1" "$multi_vst" cp "$multipkg/good.json" "$multipkg/.omarchy/package.json" echo "Debian Packages provider:" local debpkg="$TEMP_DIR/selftest-debian" deb_sum deb_x_sum deb_a_sum deb_vst mkdir -p "$debpkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD" cat > "$debpkg/.omarchy/package.json" <<'EOF' { "source": "local", "upstream": { "debian": "https://packages.example.test/dists/stable/main/binary-amd64/Packages", "package": "example-app", "sources": { "x86_64": ["https://downloads.example.test/app-{pkgver}-x64.tar.gz"], "aarch64": ["https://downloads.example.test/app-{pkgver}-arm64.tar.gz"] } } } EOF debian_fetch_packages() { cat <<'EOF' Package: unrelated Version: 99.0.0 Package: example-app Version: 1.9.0 Package: example-app Version: 1.10.0 EOF } deb_sum=$(debian_upstream_release "$debpkg") deb_x_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-x64.tar.gz' | sha256sum | cut -d' ' -f1) deb_a_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-arm64.tar.gz' | sha256sum | cut -d' ' -f1) check "Debian provider selects the newest exact package stanza" "1.10.0" "$(jq -r '.pkgver' <<<"$deb_sum")" check "Debian x86_64 source is hashed" "$deb_x_sum" "$(jq -r '.sha256sums.x86_64[0]' <<<"$deb_sum")" check "Debian aarch64 source is hashed" "$deb_a_sum" "$(jq -r '.sha256sums.aarch64[0]' <<<"$deb_sum")" deb_vst=0; validate_package_metadata "$debpkg" >/dev/null || deb_vst=$? check "Debian declaration validates" "0" "$deb_vst" printf 'pkgver=1.10.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD" check "checked-in Debian version avoids source downloads" "{}" "$(debian_upstream_release "$debpkg" | jq -c .)" echo "End-to-end Debian sync with the checked-in 1password recipe:" local one_root="$TEMP_DIR/e2e-1password" one_dir one_version=8.12.35 mkdir -p "$one_root" cp -a "$BUILD_ROOT/pkgbuilds/1password" "$one_root/1password" one_dir="$one_root/1password" # Keep the real recipe shape, but make the fixture's starting version stable. # Otherwise a routine package update can outrun the mocked release below. sed -i -e 's/^pkgver=.*/pkgver=8.12.34/' -e 's/^pkgrel=.*/pkgrel=2/' "$one_dir/PKGBUILD" debian_fetch_packages() { printf 'Package: 1password\nVersion: %s\n' "$one_version" } upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; } local one_prev_updated=$UPDATED one_prev_failed=$FAILED PKGBUILDS_DIR="$one_root" sync_package 1password >/dev/null 2>&1 || true check "1password sync updates without failures" "updated=1 failed=0" \ "updated=$((UPDATED - one_prev_updated)) failed=$((FAILED - one_prev_failed))" check "1password pkgver is the single download version source" "$one_version" \ "$(grep -m1 '^pkgver=' "$one_dir/PKGBUILD" | cut -d= -f2-)" check "1password pkgrel resets to 1" "1" \ "$(grep -m1 '^pkgrel=' "$one_dir/PKGBUILD" | cut -d= -f2-)" check "1password x86 URL follows the rewritten pkgver" "1password-${one_version}.x64.tar.gz" \ "$(CARCH=x86_64 bash -c 'source "$1"; basename "${source_x86_64[0]}"' _ "$one_dir/PKGBUILD")" check "1password ARM URL follows the rewritten pkgver" "1password-${one_version}.arm64.tar.gz" \ "$(CARCH=aarch64 bash -c 'source "$1"; basename "${source_aarch64[0]}"' _ "$one_dir/PKGBUILD")" echo "Quarantine backstop:" local rel st rel=$(jq -n --arg p "$old2d" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}') st=0; release_age_status "$rel" 86400 || st=$? check "old enough passes" "0" "$st" rel=$(jq -n --arg p "$young" '{pkgver: "2.0.0", published_at: $p, sha256sums: {}}') st=0; release_age_status "$rel" 86400 || st=$? check "too young is held" "1" "$st" st=0; release_age_status "$rel" 0 || st=$? check "no policy passes anything" "0" "$st" st=0; BYPASS_MIN_RELEASE_AGE=1 release_age_status "$rel" 86400 || st=$? check "deliberate bypass passes" "0" "$st" 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" mkdir -p "$agepkg/.omarchy" check_age() { local json_value="$1" expected="$2" got jq -n "{source: \"local\", min_release_age: $json_value}" > "$agepkg/.omarchy/package.json" got=$(package_min_release_age_seconds "$agepkg") || got="" check "min_release_age $json_value" "$expected" "$got" } check_age '"24h"' 86400 check_age '"90m"' 5400 check_age '"2d"' 172800 check_age '3600' 3600 check_age '"600s"' 600 check_age '"010h"' 36000 check_age '"abc"' "" check_age '"24hh"' "" check_age 'false' "" check_age '""' "" check_age '"9999999999"' "" echo "Manifest validation:" printf 'pkgver=1.0.0\n' > "$agepkg/PKGBUILD" local vst echo '{"source": "local", "upstream": false}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "upstream: false is rejected" "1" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool"}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "upstream without checksums/assets is rejected" "1" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SHASUMS256.txt", "digests": true, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "upstream with both checksums and digests is rejected" "1" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool", "digests": "yes", "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "non-boolean digests is rejected" "1" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": false, "digests": true, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "checksums: false alongside digests is rejected" "1" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SUMS", "digests": null, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "digests: null is rejected" "1" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SUMS", "digests": false, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "digests: false beside a checksums manifest is accepted" "0" "$vst" echo '{"source": "local", "upstream": {"github": "example/tool", "digests": false, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "digests: false alone is rejected" "1" "$vst" cp "$digpkg/.omarchy/package.json" "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "the digests declaration shape is accepted" "0" "$vst" echo '{"source":"local","upstream":{"github":"example/tool","git_tags":"https://example/tool.git","checksums":"sums","assets":{"any":"tool"}}}' > "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "multiple provider types are rejected" "1" "$vst" cp "$pkg/.omarchy/package.json" "$agepkg/.omarchy/package.json" vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? check "the real declaration shape is accepted" "0" "$vst" echo "Git-tag provider:" local tagpkg="$TEMP_DIR/selftest-tags" tag_sum remote_sum local_sum mkdir -p "$tagpkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=4\nsha256sums=("old" "old")\n' > "$tagpkg/PKGBUILD" printf 'local fixture\n' > "$tagpkg/local.patch" cat > "$tagpkg/.omarchy/package.json" <<'EOF' { "source": "local", "upstream": { "git_tags": "https://example.test/tool.git", "tag_pattern": "release/{pkgver}", "sources": { "any": ["https://downloads.example.test/tool-{pkgver}.tar.gz", "file:local.patch"] } } } EOF git_tags_fetch_refs() { printf '%s\n' \ 'aaaa refs/tags/release/1.9.0' \ 'bbbb refs/tags/release/1.10.0' \ 'cccc refs/tags/not-a-release' } upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; } tag_sum=$(git_tags_upstream_release "$tagpkg") remote_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/tool-1.10.0.tar.gz' | sha256sum | cut -d' ' -f1) local_sum=$(sha256sum "$tagpkg/local.patch" | cut -d' ' -f1) check "pacman ordering selects 1.10.0 over 1.9.0" "1.10.0" "$(jq -r '.pkgver' <<<"$tag_sum")" check "remote source template is downloaded and hashed" "$remote_sum" "$(jq -r '.sha256sums.any[0]' <<<"$tag_sum")" check "local source entry is hashed" "$local_sum" "$(jq -r '.sha256sums.any[1]' <<<"$tag_sum")" vst=0; validate_package_metadata "$tagpkg" >/dev/null || vst=$? check "git-tags declaration validates" "0" "$vst" echo "npm provider:" local npmpkg="$TEMP_DIR/selftest-npm" npm_sum npm_tar_sum npm_notes_sum mkdir -p "$npmpkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=1\nsha256sums=("old" "old")\n' > "$npmpkg/PKGBUILD" cat > "$npmpkg/.omarchy/package.json" <<'EOF' { "source": "local", "upstream": { "npm": "@example/tool", "dist_tag": "latest", "sources": { "any": ["{npm_tarball}", "https://example.test/tool/{pkgver}/notes"] } } } EOF npm_fetch_metadata() { jq -n '{ "dist-tags": {latest: "2.0.0"}, versions: {"2.0.0": {dist: {tarball: "https://registry.npmjs.org/@example/tool/-/tool-2.0.0.tgz"}}}, time: {"2.0.0": "2024-01-02T03:04:05.000Z"} }' } npm_sum=$(npm_upstream_release "$npmpkg") npm_tar_sum=$(printf 'remote fixture for %s\n' 'https://registry.npmjs.org/@example/tool/-/tool-2.0.0.tgz' | sha256sum | cut -d' ' -f1) npm_notes_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/2.0.0/notes' | sha256sum | cut -d' ' -f1) check "npm dist-tag selects its version" "2.0.0" "$(jq -r '.pkgver' <<<"$npm_sum")" check "npm tarball placeholder is hashed" "$npm_tar_sum" "$(jq -r '.sha256sums.any[0]' <<<"$npm_sum")" check "npm pkgver template is hashed" "$npm_notes_sum" "$(jq -r '.sha256sums.any[1]' <<<"$npm_sum")" check "npm publication time is preserved" "2024-01-02T03:04:05.000Z" "$(jq -r '.published_at' <<<"$npm_sum")" vst=0; validate_package_metadata "$npmpkg" >/dev/null || vst=$? check "npm declaration validates" "0" "$vst" # End to end over the real mise-bin package: its checked-in metadata and # PKGBUILD, the full sync_package path (selection, validation, backstop, # rewrite, read-back verification), with only the two network fetches # replaced by mise-shaped fixtures. echo "End-to-end sync_package with the checked-in mise-bin metadata:" local e2e_root="$TEMP_DIR/e2e-pkgbuilds" mkdir -p "$e2e_root" cp -a "$BUILD_ROOT/pkgbuilds/mise-bin" "$e2e_root/mise-bin" # Fixture versions extend the checked-in pkgver so they stay newer no # matter what version the real package is at when the test runs. local mise_current mise_aged mise_fresh mise_x64 mise_a64 mise_current=$(grep -m1 '^pkgver=' "$e2e_root/mise-bin/PKGBUILD" | cut -d= -f2- | tr -d "\"'") mise_aged="${mise_current}.90" mise_fresh="${mise_current}.91" mise_x64=$(printf 'e%.0s' {1..64}) mise_a64=$(printf 'f%.0s' {1..64}) FIXTURE_RELEASES=$(jq -n --arg young "$young" --arg old2 "$old2d" \ --arg aged "v$mise_aged" --arg fresh "v$mise_fresh" '[ {tag_name: $fresh, published_at: $young, draft: false, prerelease: false}, {tag_name: $aged, published_at: $old2, draft: false, prerelease: false} ]') FIXTURE_CHECKSUMS=$(printf '%s\n' \ "$mise_x64 ./mise-v$mise_aged-linux-x64.tar.xz" \ "$mise_a64 ./mise-v$mise_aged-linux-arm64.tar.xz") local prev_updated=$UPDATED prev_failed=$FAILED PKGBUILDS_DIR="$e2e_root" sync_package mise-bin >/dev/null 2>&1 || true check "sync_package updates without failures" "updated=1 failed=0" \ "updated=$((UPDATED - prev_updated)) failed=$((FAILED - prev_failed))" check "the 24h manifest policy holds v$mise_fresh and ships v$mise_aged" "$mise_aged" \ "$(grep -m1 '^pkgver=' "$e2e_root/mise-bin/PKGBUILD" | cut -d= -f2-)" check "pkgrel resets to 1" "1" \ "$(grep -m1 '^pkgrel=' "$e2e_root/mise-bin/PKGBUILD" | cut -d= -f2-)" check "x86_64 checksum lands in the PKGBUILD" "sha256sums_x86_64=('$mise_x64')" \ "$(grep -m1 '^sha256sums_x86_64=' "$e2e_root/mise-bin/PKGBUILD")" check "aarch64 checksum lands in the PKGBUILD" "sha256sums_aarch64=('$mise_a64')" \ "$(grep -m1 '^sha256sums_aarch64=' "$e2e_root/mise-bin/PKGBUILD")" # A malformed declaration must fail the run loudly, and still be discovered. local badpkg="$e2e_root/selftest-broken" mkdir -p "$badpkg/.omarchy" printf 'pkgver=1.0.0\npkgrel=1\n' > "$badpkg/PKGBUILD" echo '{"source": "local", "upstream": false}' > "$badpkg/.omarchy/package.json" check "malformed upstream stays discoverable for scheduled runs" "yes" \ "$(PKGBUILDS_DIR="$e2e_root" packages_for_upstream_sync | grep -qx selftest-broken && echo yes || echo no)" prev_failed=$FAILED PKGBUILDS_DIR="$e2e_root" sync_package selftest-broken >/dev/null 2>&1 || true check "malformed upstream fails the sync instead of skipping" "1" "$((FAILED - prev_failed))" FAILED=0 if ! bash "$BUILD_ROOT/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh"; then failures=$((failures + 1)) fi echo "" if [[ "$failures" -eq 0 ]]; then print_success "Self-test passed" else print_error "$failures self-test failure(s)" exit 1 fi } if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 && "${SPECIFIC_PACKAGES[0]}" == "self-test" ]]; then cmd_self_test exit 0 fi if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then SPECIFIC_MODE=true for package in "${SPECIFIC_PACKAGES[@]}"; do sync_package "$package" done else while IFS= read -r package; do sync_package "$package" done < <(packages_for_upstream_sync) fi echo "" if [[ $FAILED -gt 0 ]]; then print_error "Upstream sync completed with failures" else print_success "Upstream sync complete!" fi echo " Target: $PKGBUILDS_DIR" echo " Updated: $UPDATED" echo " Skipped: $SKIPPED" echo " Failed: $FAILED" if [[ $FAILED -gt 0 ]]; then exit 1 fi