Files
fdroid-repo-ci/test_next_version_code.sh
T
claudeandbit 19e55c4a53 publish.sh: verify credentials and upload the index last; add next-version-code.sh
Closes #5
Closes #6

An F-Droid repo is a signed index plus the files it names, and a partial
upload could previously leave them disagreeing: aws s3 sync walks the tree
in its own order, so an interrupted publish could land the index before the
APKs it names, breaking the repo for every client until the upload finished
or was rolled back. publish.sh now uploads in two passes, unconditionally --
every non-index file first, the index (index-v1.json(.asc), index-v2.json,
entry.json, entry.jar) last -- so an interruption anywhere in pass 1 simply
leaves the repo describing the old state a little longer, never a mixture.
Credentials are also verified with one real authenticated call
(head-bucket) before either pass, rather than merely checking the
environment variables are non-empty, since an unusable-but-present
credential failing halfway through is exactly how a partial upload happens.

next-version-code.sh is the one implementation of the versionCode rule
(candidate = minutes since 2026-01-01Z, clamped to strictly exceed whatever
is already published for that applicationId), shared by CI and manual
builds so there is exactly one place this can drift. bit pointed out
(appended to #6) that the clamp is monotonic against *observed* state, not
monotonic by construction: two jobs starting in the same minute can both
read the same published value and race to publish the same versionCode on
different APKs. Nothing can prevent that wasted build, so publish.sh now
also refuses to overwrite an already-existing APK object before pass 1 --
turning the collision from a silent, permanent wrong state into a loud
failure at the last moment the information is reliable.

Tested offline (83 assertions: pure-stdlib suites for both scripts, the
real CLIs through with-nixpkgs.sh, and a small local HEAD-only S3 stub
proving the credential check is a real authenticated call and the
APK-collision refusal fires only when it should) and live against
fdroid-weblib-dev (7 more: a real publish observed mid-flight via a
same-process synchronization hook to prove the index is genuinely absent
while the file is already present, both objects reading back
byte-identical, and a second publish refused against a real
already-published APK with its bytes left unchanged).

Co-authored-by: bit <bit@das-labor.org>
2026-09-09 08:00:45 +00:00

92 lines
4.3 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Shell-level integration test for next-version-code.sh (fdroid-repo-ci#6) --
# the actual CLI entry point, run the same way sign-apk.sh/fdroid-update.sh
# are tested: through with-nixpkgs.sh, against the pinned interpreter,
# exercising real argv/exit-code/stdout behaviour rather than importing a
# function. The arithmetic itself, and every clamp case, is covered offline
# and without nix by test_next_version_code.py; this file only has to prove
# the wrapper script wires that up correctly, plus the two things only the
# wrapper (not the Python module directly) can be asked about: a local file
# path as the index source, and an unreachable URL as one.
#
# bash test_next_version_code.sh
set -euo pipefail
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "$here/test_lib.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
APP_ID="de.example.app"
echo "--- wrong argument count is refused ---"
assert_failure "no arguments" bash "$here/next-version-code.sh"
assert_failure "one argument" bash "$here/next-version-code.sh" "$work/index.json"
echo "--- a local file path works end to end, through the real CLI ---"
cat > "$work/index.json" <<JSON
{"packages": {"$APP_ID": {"versions": {"h": {"manifest": {"versionCode": 41}}}}}}
JSON
out=$(bash "$here/next-version-code.sh" "$work/index.json" "$APP_ID")
if [[ "$out" =~ ^[0-9]+$ ]] && [ "$out" -gt 41 ]; then
pass "the CLI prints an integer strictly greater than the published version code (got $out)"
else
fail "the CLI prints an integer strictly greater than the published version code (got '$out')"
fi
echo "--- stdout is only the integer, nothing else, from the real CLI ---"
lines=$(echo "$out" | wc -l)
if [ "$lines" -eq 1 ]; then
pass "exactly one line of stdout"
else
fail "exactly one line of stdout (got $lines)"
fi
echo "--- a first-ever publish for this application id yields the bare candidate, no error ---"
cat > "$work/other-app.json" <<'JSON'
{"packages": {"some.other.app": {"versions": {"h": {"manifest": {"versionCode": 5}}}}}}
JSON
out2=$(bash "$here/next-version-code.sh" "$work/other-app.json" "$APP_ID")
if [[ "$out2" =~ ^[0-9]+$ ]]; then
pass "an application id absent from the index is not an error"
else
fail "an application id absent from the index is not an error (got '$out2')"
fi
echo "--- a missing local file fails loudly, prints nothing on stdout ---"
out3=$(bash "$here/next-version-code.sh" "$work/does-not-exist.json" "$APP_ID" 2>/tmp/nvc_stderr.$$) && rc=0 || rc=$?
err3=$(cat /tmp/nvc_stderr.$$); rm -f /tmp/nvc_stderr.$$
if [ "${rc:-0}" -ne 0 ]; then pass "a missing index file exits non-zero"; else
fail "a missing index file exits non-zero (got 0)"
fi
if [ -z "$out3" ]; then pass "a missing index file prints nothing on stdout"; else
fail "a missing index file prints nothing on stdout (got '$out3')"
fi
assert_contains "the error explains the index could not be read" "$err3" "could not read the index"
echo "--- an unreachable URL fails loudly too, the same way, through the real network path ---"
# port 1 is a reserved, unassigned TCP port -- nothing listens there, so
# this never leaves the loopback interface and needs no real network access,
# the same trick test_publish.sh uses.
out4=$(bash "$here/next-version-code.sh" "http://127.0.0.1:1/index-v2.json" "$APP_ID" 2>/tmp/nvc_stderr2.$$) && rc4=0 || rc4=$?
err4=$(cat /tmp/nvc_stderr2.$$); rm -f /tmp/nvc_stderr2.$$
if [ "${rc4:-0}" -ne 0 ]; then pass "an unreachable index URL exits non-zero"; else
fail "an unreachable index URL exits non-zero (got 0)"
fi
if [ -z "$out4" ]; then pass "an unreachable index URL prints nothing on stdout"; else
fail "an unreachable index URL prints nothing on stdout (got '$out4')"
fi
assert_contains "the error explains the index could not be read (URL case)" "$err4" "could not read the index"
echo "--- malformed JSON fails loudly ---"
echo "{not valid json" > "$work/malformed.json"
out5=$(bash "$here/next-version-code.sh" "$work/malformed.json" "$APP_ID" 2>/tmp/nvc_stderr3.$$) && rc5=0 || rc5=$?
err5=$(cat /tmp/nvc_stderr3.$$); rm -f /tmp/nvc_stderr3.$$
if [ "${rc5:-0}" -ne 0 ]; then pass "malformed JSON exits non-zero"; else
fail "malformed JSON exits non-zero (got 0)"
fi
assert_contains "the error explains the index could not be read (malformed JSON)" "$err5" "could not read the index"
report_and_exit