Schist is a layered image editor with PSD, Affinity and camera raw support, developed by Infrawrench and packaged by its upstream author. The package re-wraps the pacman-format payloads Schist's release workflow publishes for x86_64 and aarch64, so the builder does no compiling, and both assets are pinned by SHA-256. Releases are tracked declaratively through the GitHub upstream provider, which gains a "digests": true mode here: a vendor that publishes no checksum manifest can have each asset's SHA-256 read from the digest GitHub's release API reports, so the sync never downloads the artifacts. Exactly one of "checksums" or "digests" must be set, and the provider enforces that itself because scheduled runs reach it without the metadata validator. Fresh releases wait 24 hours before the scheduled sync picks them up, as mise-bin already does. vulkan-driver is an optional dependency rather than a hard one: makepkg -s would otherwise satisfy the virtual package with nvidia-utils in the build container, and Omarchy installs a Vulkan driver per machine. Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
876 lines
34 KiB
Bash
Executable File
876 lines
34 KiB
Bash
Executable File
#!/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 <<EOF
|
|
Usage: $0 [PACKAGE...]
|
|
|
|
Update packages that track an upstream vendor release feed instead of the AUR.
|
|
|
|
Packages opt in declaratively through "upstream" in .omarchy/package.json.
|
|
Providers cover GitHub Releases with checksum manifests or API asset digests,
|
|
semver-shaped git tags whose source URLs can be hashed, and npm dist-tags. See README.md for the
|
|
schemas. Anything outside those conventions may provide
|
|
pkgbuilds/<package>/.omarchy/upstream.sh, a hook that reports JSON on stdout:
|
|
|
|
{
|
|
"pkgver": "1.2.3",
|
|
"sha256sums": { "x86_64": ["<sha256>"], "aarch64": ["<sha256>"] }
|
|
}
|
|
|
|
Architecture keys become sha256sums_<arch> 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 <package>. 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=$? ;;
|
|
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="<error>"
|
|
check "quarantine falls back past the young v2.0.0" "1.9.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
|
|
check "selected release reports its published_at" "$old2d" "$(jq -r '.published_at // "<none>"' <<<"$out")"
|
|
check "x86_64 checksum via {tag} template and ./ prefix" "$sum_x19" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
|
|
check "aarch64 checksum via {pkgver} template" "$sum_a19" "$(jq -r '.sha256sums.aarch64[0] // "<none>"' <<<"$out")"
|
|
|
|
out=$(github_upstream_release "$pkg" 0 2>/dev/null) || out="<error>"
|
|
check "no policy selects the newest stable release" "2.0.0" "$(jq -r '.pkgver // "<none>"' <<<"$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="<error>"
|
|
check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
|
|
check "x86_64 checksum via * binary-mode prefix" "$sum_x20" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
|
|
|
|
out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="<error>"
|
|
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="<error>"
|
|
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="<error>"
|
|
check "x86_64 checksum via the asset digest" "$sum_x19" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
|
|
check "aarch64 checksum via the asset digest" "$sum_a19" "$(jq -r '.sha256sums.aarch64[0] // "<none>"' <<<"$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 "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="<reject>"
|
|
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"' "<reject>"
|
|
check_age '"24hh"' "<reject>"
|
|
check_age 'false' "<reject>"
|
|
check_age '""' "<reject>"
|
|
check_age '"9999999999"' "<reject>"
|
|
|
|
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
|