#!/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 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. 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("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 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 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..." 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" 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 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