Compare commits

..
3 Commits
Author SHA1 Message Date
bryanpediniandClaude Opus 5 25492ed3ee ci: prebuild the Arch environment as a container image
Build Autoupdater Image / build (push) Failing after 6s
All six nightly jobs ran the same pacman transaction before doing any work.
The toolchain now lives in an image published to the Gitea registry as
git.bjphoster.com/aur/autoupdate:latest, rebuilt monthly since Arch is rolling
and a month-old image is a month behind.

The build job itself runs on plain ubuntu-latest -- it only drives docker. Only
the image is Arch.

Based on archlinux:base rather than base-devel: the autoupdater parses
PKGBUILDs and never compiles, so the toolchain is dead weight, 822 MB against
1.48 GB. nodejs is included because actions/checkout is a JavaScript action the
runner executes with node from inside the image.

Slimming to base initially dropped diffutils, and apply_pkgrel_rule used
diff -q. A missing binary makes the `if` fail, which reads as "changed", so
pkgrel ratcheted upward on every run -- the same silent-wrong-default class as
the empty .SRCINFO this rewrite was about, and invisible to the host test run.
The comparison is now a shell string equality with no external dependency, and
is verified against an image with no diff at all. require_tools() additionally
fails on startup for any missing binary rather than midway through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 23:17:37 +02:00
bryanpediniandClaude Opus 5 028b3a5691 ci: install nodejs before checkout
actions/checkout@v4 is a JavaScript action and the archlinux image ships no
node, so it failed with `exec: "node": executable file not found in $PATH`.
A `run:` step does not require a checkout, so the toolchain is installed
first and the standard action then works unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 22:59:43 +02:00
bryanpediniandClaude Opus 5 986c212b82 Replace per-package push scripts with a self-healing AUR reconciler
The six nightly workflows each cloned a package repo and ran that repo's own
push.sh. Two problems compounded into months of silent breakage.

push.sh pushed to origin before the AUR. When the AUR push failed, origin
already carried the new version, so every later run hit "same (old) version
specified", bare-exit 0'd, and the workflow went green while the AUR rotted.
reflex-appimage sat at 1.0.11 locally against 1.0.10 published; nethlink-appimage
was never submitted at all.

The AUR pushes were failing because .SRCINFO was empty -- 0 bytes in
reflex-appimage, 1 byte in nethlink-appimage and pman-helper, against 500-2000
in the healthy ones. The AUR rejects an invalid .SRCINFO. Both PKGBUILDs verify
clean under a real Arch makepkg, so this was the CI environment, and with no
set -e the script committed and pushed the truncated file regardless.

pman-helper failed independently: the workflows queried the Gitea endpoint
/repos/{owner}/{repo}/repo/tags, which 404s -- the correct path is /tags. The
empty result fell through to an interactive read that got EOF, and it committed
pkgver="" plus the sha256 of a zero-byte download.

The replacement is a reconciler rather than a version-bump script. A new
upstream release, a manual push to origin, and a push that failed last night
all take the same path, and the AUR reconcile runs on every pass -- so a failed
push simply retries. Sync is decided by comparing tree content against the AUR,
not by whether this run happened to find something new.

pkgrel is now derived from the AUR rather than from local history, which is
what makes it idempotent: a version differing from the published one resets it
to 1, a packaging change increments it, and once published the next run sees no
difference and does nothing. Editing a PKGBUILD no longer needs a manual bump.

Checksums are computed directly instead of via makepkg -g / updpkgsums. Those
generate every arch array in one pass into a single directory, so when two
arches rename to the same target the second finds the first one's file and
emits an identical, silently wrong sum -- verified against freetube-appimage.
Downloads go to content-addressed cache paths instead. Multi-arch works for
when it is needed; single-arch is the degenerate case of the same loop.

Guards for each observed failure: a real Arch makepkg in an archlinux container
as non-root, set -euo pipefail throughout, .SRCINFO validated against the
PKGBUILD before any commit, and a failed version lookup exiting 1 so it can
never again be mistaken for a quiet "no new version" night.

Package-specific detail moves out of this repo entirely, into an .autoupdate
manifest committed to each package repo. sha256 only; sha1sums and md5sums are
removed. deskflow-bin and freetube-appimage are dropped from automation, having
been deleted from the AUR.

Covered by test.sh: 37 assertions against local bare repos and a local HTTP
server, no network and never touching the real AUR. The retry, pkgrel
idempotence and multi-arch collision tests were each verified by mutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 22:50:07 +02:00
5 changed files with 35 additions and 295 deletions

No files matched your search

+13 -96
View File
@@ -31,86 +31,7 @@ defaults:
shell: bash shell: bash
jobs: jobs:
# Resolves the package list for the update matrix below, so adding or
# dropping a package is a variable edit on the org, not a commit here.
# See README "Package list".
plan:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.resolve.outputs.packages }}
steps:
- name: Resolve the package list
id: resolve
# Both go through the environment, never pasted into the script: the
# variable is free-form multi-line text.
env:
UPDATE_PACKAGES: ${{ vars.UPDATE_PACKAGES }}
PACKAGE_INPUT: ${{ inputs.package }}
run: |
set -euo pipefail
export LC_ALL=C
: "${UPDATE_PACKAGES:=}" "${PACKAGE_INPUT:=}"
re='^[A-Za-z0-9][A-Za-z0-9._+-]*$'
pkgs=()
declare -A seen=()
# add NAME: validate, then append unless already listed.
add() {
if [[ ! $1 =~ $re ]]; then
echo "::error::${from}: invalid package name '$1' (letters, digits and . _ + - only, starting with a letter or digit)" >&2
exit 1
fi
if [ -z "${seen[$1]:-}" ]; then
seen[$1]=1
pkgs+=("$1")
fi
}
if [ -n "${PACKAGE_INPUT//[[:space:]]/}" ]; then
# workflow_dispatch with a package name runs only that one,
# whatever UPDATE_PACKAGES says. Surrounding whitespace is
# trimmed; the rest must be a single valid name.
from="workflow_dispatch input 'package'"
want=${PACKAGE_INPUT#"${PACKAGE_INPUT%%[![:space:]]*}"}
want=${want%"${want##*[![:space:]]}"}
add "${want}"
else
# "- a\n- b", "a\nb", "- a - b" and "a b" are the same list:
# comments go first, then every whitespace run (CR included, in
# case the value was saved with CRLF line ends) becomes a line
# break. Each line is then a name, a lone "-" list marker, or a
# "-" glued to a name.
from="UPDATE_PACKAGES"
while IFS= read -r tok; do
case ${tok} in
"" | -) continue ;;
-*) tok=${tok#-} ;;
esac
add "${tok}"
done <<< "$(printf '%s\n' "${UPDATE_PACKAGES}" | sed -e 's/#.*$//' | tr -s '[:space:]' '\n')"
fi
# Fail rather than run nothing: an empty matrix would pass and look
# exactly like a night with nothing to update.
if [ "${#pkgs[@]}" -eq 0 ]; then
echo '::error::UPDATE_PACKAGES is not set: add an org or repo variable with one package per line (leading "- " optional)' >&2
exit 1
fi
# Names are validated above, so none needs JSON escaping.
json=
for p in "${pkgs[@]}"; do
json+="${json:+,}\"${p}\""
done
json="[${json}]"
echo "${#pkgs[@]} package(s) from ${from}:"
printf ' - %s\n' "${pkgs[@]}"
echo "matrix: ${json}"
echo "packages=${json}" >> "${GITHUB_OUTPUT}"
update: update:
needs: plan
runs-on: ubuntu-latest runs-on: ubuntu-latest
# A real Arch makepkg is required. Ubuntu's build environment silently # A real Arch makepkg is required. Ubuntu's build environment silently
# emits an empty .SRCINFO, which the AUR then rejects -- that is what broke # emits an empty .SRCINFO, which the AUR then rejects -- that is what broke
@@ -129,9 +50,13 @@ jobs:
# One broken package must not cancel the rest of the matrix. # One broken package must not cancel the rest of the matrix.
fail-fast: false fail-fast: false
matrix: matrix:
# One entry per package, resolved by the plan job above from package:
# vars.UPDATE_PACKAGES or the dispatch input. - nethlink-appimage
package: ${{ fromJSON(needs.plan.outputs.packages) }} - open-video-downloader-appimage
- open-video-downloader-bin
- pman-helper
- reflex-appimage
- tsparams
steps: steps:
# No bootstrap step: node, git, openssh, jq, curl and the makepkg # No bootstrap step: node, git, openssh, jq, curl and the makepkg
@@ -156,22 +81,8 @@ jobs:
chmod 600 ~/.git-credentials chmod 600 ~/.git-credentials
mkdir -p ~/.ssh && chmod 700 ~/.ssh 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 printf '%s\n' "${AUR_SSH_KEY}" > ~/.ssh/aur
chmod 600 ~/.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 ssh-keyscan -t rsa,ecdsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
cat >> ~/.ssh/config <<'EOF' cat >> ~/.ssh/config <<'EOF'
Host aur.archlinux.org Host aur.archlinux.org
@@ -202,6 +113,12 @@ jobs:
NO_GIT_PUSH: ${{ (inputs.no_git_push == true || inputs.no_git_push == 'true') && '1' || '' }} NO_GIT_PUSH: ${{ (inputs.no_git_push == true || inputs.no_git_push == 'true') && '1' || '' }}
run: | run: |
set -euo pipefail set -euo pipefail
# workflow_dispatch with a package name runs only that one.
want="${{ inputs.package }}"
if [ -n "${want}" ] && [ "${want}" != "${{ matrix.package }}" ]; then
echo "skipping ${{ matrix.package }} (dispatch asked for ${want})"
exit 0
fi
runuser -u builder -- env \ runuser -u builder -- env \
GIT_HOST="${GIT_HOST}" \ GIT_HOST="${GIT_HOST}" \
FORCE_REBUILD="${FORCE_REBUILD}" \ FORCE_REBUILD="${FORCE_REBUILD}" \
+6 -45
View File
@@ -18,7 +18,7 @@ three take the same path, and the AUR reconcile runs on every pass.
| `sync-aur.sh` | reconciles the repo against the AUR | | `sync-aur.sh` | reconciles the repo against the AUR |
| `common.sh` | logging, guards, PKGBUILD parsing and array splicing | | `common.sh` | logging, guards, PKGBUILD parsing and array splicing |
| `test.sh` | hermetic test suite | | `test.sh` | hermetic test suite |
| `.gitea/workflows/update.yaml` | nightly job: `plan` reads `UPDATE_PACKAGES`, `update` runs one matrix entry per package | | `.gitea/workflows/update.yaml` | nightly matrix job |
Nothing here is package-specific. Everything that differs between packages Nothing here is package-specific. Everything that differs between packages
lives in a `.autoupdate` manifest committed to the package repo itself. lives in a `.autoupdate` manifest committed to the package repo itself.
@@ -40,42 +40,6 @@ icon="reflex.png"
It rides along to the AUR on push, as `README.md` and `LICENSE` already do. It rides along to the AUR on push, as `README.md` and `LICENSE` already do.
Harmless: the AUR reads only `PKGBUILD` and `.SRCINFO`. Harmless: the AUR reads only `PKGBUILD` and `.SRCINFO`.
## Package list
The nightly job updates the packages named in the Gitea Actions variable
`UPDATE_PACKAGES`; nothing in this repo lists them. Set it on the `aur` org
(Settings -> Actions -> Variables), which is the recommended place; a variable
of the same name on `aur/.autoupdate` itself overrides the org one.
One package per line. A leading `- ` is optional, so YAML-list lines and bare
names both work and can be mixed; blank lines and `#` comments (whole-line or
trailing) are ignored, and duplicates are dropped. A name must match
`^[A-Za-z0-9][A-Za-z0-9._+-]*$`, otherwise the run fails naming it. The
current value:
```
- nethlink-appimage
- open-video-downloader-appimage
- open-video-downloader-bin
- pman-helper
- reflex-appimage
- tsparams
```
Add `- skeptical-updater` once `source/skeptical-updater` has its first tag.
Until then its version lookup finds no tag, which is a failed lookup (see
[Exit codes](#exit-codes)) and would turn that matrix job red every night.
The workflow's `plan` job turns the variable into the `update` matrix, one job
per package. A manual run's `package` input (Actions -> Update AUR Packages ->
Run workflow) overrides the variable and runs that one package only, listed or
not. An unset or empty variable fails the run on purpose: an empty matrix
would pass and look exactly like a night with nothing to update.
This needs Gitea 28.0 or newer, the first release that expands a matrix from
another job's outputs. Older servers expand it before `plan` has run, start a
single `update` job with an empty package name, and that job fails.
## What a run does ## What a run does
1. Clone the package repo, read its manifest. 1. Clone the package repo, read its manifest.
@@ -121,15 +85,12 @@ its `sha256sums` entry is regenerated first. A README-only edit does not.
| `NO_GIT_PUSH=1` | dry run: do everything except push | | `NO_GIT_PUSH=1` | dry run: do everything except push |
| `FORCE_REBUILD=1` | bump `pkgrel` even with no detected change | | `FORCE_REBUILD=1` | bump `pkgrel` even with no detected change |
| `FORCE_REFRESH=1` | re-download remote sources without a version 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 | | `GIT_HOST` | git host holding the package repos |
| `GITHUB_TOKEN` | optional; avoids GitHub's 60/hour anonymous rate limit | | `GITHUB_TOKEN` | optional; avoids GitHub's 60/hour anonymous rate limit |
There is deliberately no force-push escape hatch. The AUR accepts fast-forwards A rejected AUR push usually means someone pushed there directly. The run fails
only and refuses force pushes from anyone who is not an AUR administrator, so loudly rather than overwriting it; inspect, then re-run with `AUR_FORCE=1`.
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 ## Exit codes
@@ -152,8 +113,8 @@ immediately instead of taking a silently wrong branch further in.
## Container image ## Container image
`dockerfile` builds the environment the nightly job runs in, published to the `dockerfile` builds the environment the nightly job runs in, published to the
Gitea registry as `git.bjphoster.com/aur/autoupdate:latest`. Without it, every Gitea registry as `git.bjphoster.com/aur/autoupdate:latest`. Without it, all six
matrix job would run a full `pacman` transaction every night. matrix jobs would run a full `pacman` transaction every night.
`.gitea/workflows/build-image.yaml` rebuilds and overwrites `:latest` monthly `.gitea/workflows/build-image.yaml` rebuilds and overwrites `:latest` monthly
(Arch is rolling, so a month-old image is a month behind), on any push touching (Arch is rolling, so a month-old image is a month behind), on any push touching
+3 -26
View File
@@ -9,6 +9,7 @@
# NO_GIT_PUSH=1 do everything except push (dry run) # NO_GIT_PUSH=1 do everything except push (dry run)
# FORCE_REBUILD=1 bump pkgrel even with no detected change # FORCE_REBUILD=1 bump pkgrel even with no detected change
# FORCE_REFRESH=1 re-download remote sources even without a version 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 # 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 # only one of the things that can make a package diverge from the AUR; a manual
@@ -92,11 +93,7 @@ update_one() {
# A failure in here exits non-zero and is NOT mistaken for "no new version". # A failure in here exits non-zero and is NOT mistaken for "no new version".
local oldver newver local oldver newver
oldver="$(pkgbuild_get pkgver)" oldver="$(pkgbuild_get pkgver)"
# Checked explicitly rather than leaning on `set -e`, so the failure is newver="$("$LATEST_VERSION_CMD" .)"
# 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>}" info "upstream ${newver}, local ${oldver:-<empty>}"
local version_changed=0 local version_changed=0
@@ -270,28 +267,8 @@ extract_appimage_assets() {
rm -rf "$(readlink -f "${tmp}/squashfs-root" 2>/dev/null || true)" "$tmp" 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 rc=0
for pkg in "$@"; do for pkg in "$@"; do
AUTOUPDATE_SINGLE=1 "$0" "$pkg" || { rc=1; warn "${pkg} FAILED"; } ( update_one "$pkg" ) || { rc=1; warn "${pkg} FAILED"; }
done done
exit "$rc" exit "$rc"
+12 -30
View File
@@ -50,16 +50,6 @@ if git fetch --quiet aur-ro master 2>/dev/null; then
log "AUR already in sync (${pkgname})" log "AUR already in sync (${pkgname})"
exit 0 exit 0
fi 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})" log "AUR differs from origin; pushing (${pkgname})"
else else
log "no AUR repo for ${pkgname} yet; the first push will create it" log "no AUR repo for ${pkgname} yet; the first push will create it"
@@ -70,28 +60,20 @@ if [ -n "${NO_GIT_PUSH:-}" ]; then
exit 0 exit 0
fi fi
if push_output="$(git push aur HEAD:master 2>&1)"; then if git push aur HEAD:master; then
printf '%s\n' "$push_output"
log "pushed ${pkgname} to the AUR" log "pushed ${pkgname} to the AUR"
exit 0 exit 0
fi fi
# Report what git actually said, then name a remedy that can work. The previous # A rejected push here usually means the AUR has commits origin does not,
# version blamed every failure on a diverged history and recommended a force # i.e. someone pushed directly. Overwriting that silently would destroy it.
# push -- wrong on both counts for what turned out to be an unregistered SSH key. if [ -n "${AUR_FORCE:-}" ]; then
printf '%s\n' "$push_output" >&2 warn "AUR_FORCE set: force-pushing over the AUR history for ${pkgname}"
git push --force aur HEAD:master
exit 0
fi
case "$push_output" in die "push to the AUR was rejected for ${pkgname}.
*"Permission denied"*|*"publickey"*|*"Could not read from remote repository"*) The AUR history has probably diverged from origin (someone pushed there
die "the AUR rejected the SSH key for ${pkgname}. directly). Inspect it, then re-run with AUR_FORCE=1 to overwrite.
Check that AUR_SSH_KEY holds a valid private key and that its public half is This run leaves origin untouched and will be retried on the next pass."
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
+1 -98
View File
@@ -59,21 +59,6 @@ EOS
done done
[ -n "${PORT:-}" ] || { echo "could not start test http server" >&2; exit 1; } [ -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 PKG_REPO_BASE="file://$SANDBOX/origin"
export AUR_HTTPS_BASE="file://$SANDBOX/aur" export AUR_HTTPS_BASE="file://$SANDBOX/aur"
export AUR_SSH_BASE="file://$SANDBOX/aur" export AUR_SSH_BASE="file://$SANDBOX/aur"
@@ -348,18 +333,8 @@ test_lookup_failure_is_loud() {
make_pkg p9 make_pkg p9
local before; before="$(aur_head p9)" local before; before="$(aur_head p9)"
FAKE_VERSION_FAIL=1 run_update p9 FAKE_VERSION_FAIL=1 run_update p9
local rc=$? assert_eq "1" "$?" "broken upstream lookup: exits 1, not mistaken for 'no new version'"
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_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() { test_empty_version_aborts_before_commit() {
@@ -422,78 +397,6 @@ test_multiarch_colliding_downloads() {
assert_ne "$x" "$a" "colliding downloads: sums differ despite the shared :: target" 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() { test_srcinfo_guard() {
make_pkg p13 make_pkg p13
local clone="$SANDBOX/manual-p13" local clone="$SANDBOX/manual-p13"