Compare commits

..

1 Commits

Author SHA1 Message Date
2565c193e6 fix: fail safely when AUR synchronization fails
All checks were successful
Update AUR Packages / update (nethlink-appimage) (push) Successful in 13s
Update AUR Packages / update (open-video-downloader-appimage) (push) Successful in 13s
Update AUR Packages / update (open-video-downloader-bin) (push) Successful in 14s
Update AUR Packages / update (pman-helper) (push) Successful in 13s
Update AUR Packages / update (reflex-appimage) (push) Successful in 13s
Update AUR Packages / update (tsparams) (push) Successful in 12s
2026-07-27 00:17:01 +02:00
5 changed files with 174 additions and 19 deletions

View File

@@ -81,8 +81,22 @@ jobs:
chmod 600 ~/.git-credentials
mkdir -p ~/.ssh && chmod 700 ~/.ssh
# Validate the key HERE rather than letting it surface as an opaque
# `Permission denied (publickey)` at push time, several steps later.
if [ -z "${AUR_SSH_KEY//[[:space:]]/}" ]; then
echo "::error::AUR_SSH_KEY secret is empty or unset" >&2
exit 1
fi
printf '%s\n' "${AUR_SSH_KEY}" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
if ! ssh-keygen -y -P "" -f ~/.ssh/aur >/dev/null 2>&1; then
echo "::error::AUR_SSH_KEY is not a usable unencrypted private key." >&2
echo "Paste the whole file including the BEGIN/END lines; a passphrase-protected key cannot be used unattended." >&2
exit 1
fi
echo "AUR key fingerprint: $(ssh-keygen -lf ~/.ssh/aur | awk '{print $2}')"
ssh-keyscan -t rsa,ecdsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
cat >> ~/.ssh/config <<'EOF'
Host aur.archlinux.org

View File

@@ -85,12 +85,15 @@ its `sha256sums` entry is regenerated first. A README-only edit does not.
| `NO_GIT_PUSH=1` | dry run: do everything except push |
| `FORCE_REBUILD=1` | bump `pkgrel` even with no detected change |
| `FORCE_REFRESH=1` | re-download remote sources without a version change |
| `AUR_FORCE=1` | force-push over diverged AUR history (see below) |
| `GIT_HOST` | git host holding the package repos |
| `GITHUB_TOKEN` | optional; avoids GitHub's 60/hour anonymous rate limit |
A rejected AUR push usually means someone pushed there directly. The run fails
loudly rather than overwriting it; inspect, then re-run with `AUR_FORCE=1`.
There is deliberately no force-push escape hatch. The AUR accepts fast-forwards
only and refuses force pushes from anyone who is not an AUR administrator, so
such a flag could never do anything except recommend a remedy that always fails.
`sync-aur.sh` checks fast-forward status *before* pushing and, on failure, prints
git's actual error and names a cause that fits it — a rejected SSH key, an
unreachable host, or a genuine divergence — rather than assuming.
## Exit codes

View File

@@ -9,7 +9,6 @@
# NO_GIT_PUSH=1 do everything except push (dry run)
# FORCE_REBUILD=1 bump pkgrel even with no detected change
# FORCE_REFRESH=1 re-download remote sources even without a version change
# AUR_FORCE=1 force-push over diverged AUR history
#
# This is a RECONCILER, not a version-bump script. A new upstream release is
# only one of the things that can make a package diverge from the AUR; a manual
@@ -93,7 +92,11 @@ update_one() {
# A failure in here exits non-zero and is NOT mistaken for "no new version".
local oldver newver
oldver="$(pkgbuild_get pkgver)"
newver="$("$LATEST_VERSION_CMD" .)"
# Checked explicitly rather than leaning on `set -e`, so the failure is
# attributed to the lookup instead of surfacing several steps later as an
# unrelated .SRCINFO complaint.
newver="$("$LATEST_VERSION_CMD" .)" || die "upstream version lookup failed for ${pkg}"
validate_version "$newver" "upstream version for ${pkg}"
info "upstream ${newver}, local ${oldver:-<empty>}"
local version_changed=0
@@ -267,8 +270,28 @@ extract_appimage_assets() {
rm -rf "$(readlink -f "${tmp}/squashfs-root" 2>/dev/null || true)" "$tmp"
}
# One package per process, deliberately.
#
# The obvious `( update_one "$pkg" ) || { rc=1; ... }` is broken: putting a
# subshell in an && / || list disables `set -e` for its ENTIRE body, not just
# for the subshell as a unit. update_one then sails past a failed sync-aur.sh
# straight into `git push origin`, which is precisely the "origin ahead of the
# AUR" state this whole tool exists to avoid -- and it happened in production.
#
# inner() { echo start; false; echo "REACHED CODE AFTER FAILURE"; }
# ( inner ) || echo caught # prints REACHED CODE AFTER FAILURE
# ( inner ) # correctly aborts
#
# Re-exec'ing gives each package a fresh shell whose `set -e` is intact; the
# parent's AND-OR context cannot reach across a process boundary. The CI matrix
# already passes exactly one package, so the loop only affects local runs.
if [ -n "${AUTOUPDATE_SINGLE:-}" ]; then
update_one "$1"
exit 0
fi
rc=0
for pkg in "$@"; do
( update_one "$pkg" ) || { rc=1; warn "${pkg} FAILED"; }
AUTOUPDATE_SINGLE=1 "$0" "$pkg" || { rc=1; warn "${pkg} FAILED"; }
done
exit "$rc"

View File

@@ -50,6 +50,16 @@ if git fetch --quiet aur-ro master 2>/dev/null; then
log "AUR already in sync (${pkgname})"
exit 0
fi
# Established BEFORE pushing, rather than inferred from a failure afterwards.
# The AUR only ever accepts fast-forwards, so this is the whole decision --
# there is no force to fall back on.
if ! git merge-base --is-ancestor FETCH_HEAD HEAD; then
die "the AUR has commits origin does not, for ${pkgname}.
The AUR accepts fast-forwards only and refuses force pushes from anyone who
is not an AUR administrator, so this cannot be overwritten. Rebase or replay
the local changes onto the AUR's HEAD, then re-run."
fi
log "AUR differs from origin; pushing (${pkgname})"
else
log "no AUR repo for ${pkgname} yet; the first push will create it"
@@ -60,20 +70,28 @@ if [ -n "${NO_GIT_PUSH:-}" ]; then
exit 0
fi
if git push aur HEAD:master; then
if push_output="$(git push aur HEAD:master 2>&1)"; then
printf '%s\n' "$push_output"
log "pushed ${pkgname} to the AUR"
exit 0
fi
# A rejected push here usually means the AUR has commits origin does not,
# i.e. someone pushed directly. Overwriting that silently would destroy it.
if [ -n "${AUR_FORCE:-}" ]; then
warn "AUR_FORCE set: force-pushing over the AUR history for ${pkgname}"
git push --force aur HEAD:master
exit 0
fi
# Report what git actually said, then name a remedy that can work. The previous
# version blamed every failure on a diverged history and recommended a force
# push -- wrong on both counts for what turned out to be an unregistered SSH key.
printf '%s\n' "$push_output" >&2
die "push to the AUR was rejected for ${pkgname}.
The AUR history has probably diverged from origin (someone pushed there
directly). Inspect it, then re-run with AUR_FORCE=1 to overwrite.
This run leaves origin untouched and will be retried on the next pass."
case "$push_output" in
*"Permission denied"*|*"publickey"*|*"Could not read from remote repository"*)
die "the AUR rejected the SSH key for ${pkgname}.
Check that AUR_SSH_KEY holds a valid private key and that its public half is
saved on your AUR account (My Account -> SSH Public Key; the form needs your
account password before it will save)." ;;
*"non-fast-forward"*|*"fetch first"*)
die "the AUR moved between the pre-check and the push for ${pkgname}.
Nothing to do: the next run re-reads the AUR and reconciles." ;;
*"Host key verification failed"*|*"Could not resolve hostname"*)
die "could not reach aur.archlinux.org for ${pkgname}; see the error above." ;;
*)
die "push to the AUR failed for ${pkgname}; see the git error above." ;;
esac

99
test.sh
View File

@@ -59,6 +59,21 @@ EOS
done
[ -n "${PORT:-}" ] || { echo "could not start test http server" >&2; exit 1; }
# A git shim used by one test to simulate an SSH auth rejection on push.
# Everything else passes straight through to the real git.
REAL_GIT="$(command -v git)"
mkdir -p "$SANDBOX/bin"
cat > "$SANDBOX/bin/git" <<EOS
#!/usr/bin/env bash
if [ "\${FAKE_PUSH_AUTH_FAIL:-}" = "1" ] && [ "\$1" = "push" ] && [ "\$2" = "aur" ]; then
echo "aur@aur.archlinux.org: Permission denied (publickey)." >&2
echo "fatal: Could not read from remote repository." >&2
exit 128
fi
exec "$REAL_GIT" "\$@"
EOS
chmod +x "$SANDBOX/bin/git"
export PKG_REPO_BASE="file://$SANDBOX/origin"
export AUR_HTTPS_BASE="file://$SANDBOX/aur"
export AUR_SSH_BASE="file://$SANDBOX/aur"
@@ -333,8 +348,18 @@ test_lookup_failure_is_loud() {
make_pkg p9
local before; before="$(aur_head p9)"
FAKE_VERSION_FAIL=1 run_update p9
assert_eq "1" "$?" "broken upstream lookup: exits 1, not mistaken for 'no new version'"
local rc=$?
assert_eq "1" "$rc" "broken upstream lookup: exits 1, not mistaken for 'no new version'"
assert_eq "$before" "$(aur_head p9)" "broken upstream lookup: nothing pushed"
# Assert the REASON, not just the exit code. This previously passed even with
# the lookup failure being swallowed: the run carried on with an empty version
# and exited 1 several steps later when .SRCINFO generation rejected it, which
# is the pman-helper pkgver="" bug wearing a different hat.
grep -q "upstream version lookup failed" "$SANDBOX/last.log" \
&& pass "broken upstream lookup: fails AT the lookup, not later" \
|| fail "broken upstream lookup: fails AT the lookup, not later" \
"expected the lookup-failure message; got: $(tail -3 "$SANDBOX/last.log")"
}
test_empty_version_aborts_before_commit() {
@@ -397,6 +422,78 @@ test_multiarch_colliding_downloads() {
assert_ne "$x" "$a" "colliding downloads: sums differ despite the shared :: target"
}
# The ordering guarantee: if the AUR push fails, origin must NOT be pushed.
#
# This regressed in production. `( update_one ) || ...` disables set -e for the
# whole subshell body, so a failed sync-aur.sh did not stop the run and origin
# ended up ahead of the AUR -- the exact state the tool exists to prevent.
test_aur_failure_leaves_origin_unpushed() {
make_pkg p16
local clone="$SANDBOX/manual-p16"
git clone --quiet "file://$SANDBOX/origin/p16" "$clone"
( cd "$clone" && sed -i 's/^pkgdesc=.*/pkgdesc="needs publishing"/' PKGBUILD \
&& makepkg --printsrcinfo > .SRCINFO \
&& git commit --quiet -am "change to publish" && git push --quiet origin main )
local origin_before aur_before
origin_before="$(git -C "$SANDBOX/origin/p16" rev-parse main)"
aur_before="$(aur_head p16)"
# Push target does not exist -> the AUR push fails, the fetch still works.
AUR_SSH_BASE="file://$SANDBOX/nonexistent" FAKE_VERSION=1.0.0 run_update p16
local rc=$?
assert_eq "1" "$rc" "aur failure: run exits 1"
assert_eq "$aur_before" "$(aur_head p16)" "aur failure: AUR unchanged"
assert_eq "$origin_before" "$(git -C "$SANDBOX/origin/p16" rev-parse main)" \
"aur failure: origin NOT pushed (the ordering guarantee)"
# It must not blame divergence for what is a plain push failure.
grep -q "has commits origin does not" "$SANDBOX/last.log" \
&& fail "aur failure: not misreported as divergence" "blamed divergence for a non-divergence failure" \
|| pass "aur failure: not misreported as divergence"
}
# A real non-fast-forward must be named as such -- and must NOT suggest forcing,
# since the AUR refuses force pushes from non-administrators.
test_genuine_divergence_is_reported() {
make_pkg p17
local clone="$SANDBOX/manual-p17"
git clone --quiet "file://$SANDBOX/aur/p17.git" "$clone"
( cd "$clone" && echo "edited directly on the AUR" >> README.md \
&& git commit --quiet -am "out-of-band AUR commit" \
&& git push --quiet origin HEAD:master )
FAKE_VERSION=1.0.0 run_update p17
local rc=$?
assert_eq "1" "$rc" "divergence: exits 1"
grep -q "has commits origin does not" "$SANDBOX/last.log" \
&& pass "divergence: reported as divergence" \
|| fail "divergence: reported as divergence" "$(tail -4 "$SANDBOX/last.log")"
grep -qi "AUR_FORCE" "$SANDBOX/last.log" \
&& fail "divergence: does not suggest forcing" "AUR_FORCE is gone; nothing should mention it" \
|| pass "divergence: does not suggest forcing"
}
test_auth_failure_is_reported_as_auth() {
make_pkg p18
local clone="$SANDBOX/manual-p18"
git clone --quiet "file://$SANDBOX/origin/p18" "$clone"
( cd "$clone" && sed -i 's/^pkgdesc=.*/pkgdesc="publish me"/' PKGBUILD \
&& makepkg --printsrcinfo > .SRCINFO \
&& git commit --quiet -am "change" && git push --quiet origin main )
PATH="$SANDBOX/bin:$PATH" FAKE_PUSH_AUTH_FAIL=1 FAKE_VERSION=1.0.0 run_update p18
local rc=$?
assert_eq "1" "$rc" "auth failure: exits 1"
grep -q "rejected the SSH key" "$SANDBOX/last.log" \
&& pass "auth failure: reported as an SSH key problem" \
|| fail "auth failure: reported as an SSH key problem" "$(tail -4 "$SANDBOX/last.log")"
grep -q "has commits origin does not" "$SANDBOX/last.log" \
&& fail "auth failure: not misreported as divergence" "this is the production bug" \
|| pass "auth failure: not misreported as divergence"
}
test_srcinfo_guard() {
make_pkg p13
local clone="$SANDBOX/manual-p13"