Files
fdroid-repo-ci/test_next_version_code.sh
T
claudeandbit e5d1f9a5bf next-version-code.sh: derive the candidate from the commit, not the build clock
fdroid-repo-ci#6's "Superseded" sections change the rule: the candidate is
now the committer date of the commit being built, clamped on both sides,
rather than whatever minute the runner happened to pick the job up in.

Backwards: rebase, amend and cherry-pick can produce a commit older than
one already published, so the clamp keys off the committer date, not the
author date -- author date survives a rebase unchanged and would replay a
stale, already-superseded number.

Forwards: a commit's timestamp is attacker- and typo-controlled and the
number only ever rises, so a bad one can never be walked back. A value
placed just below the int32 ceiling would still publish and then no later
build could ever exceed it -- the app becomes permanently un-updatable.
TOLERANCE absorbs honest clock skew and the clamp warns loudly rather than
rejecting, since rejecting would block a legitimate build over a few
minutes of skew while buying nothing the clamp doesn't already cover.

next_version_code.py now takes the committer timestamp as a third
argument (Unix seconds, what `git log -1 --format=%ct` prints) instead of
reading the system clock, keeping it pure and testable without a git
checkout. next-version-code.sh, README.md and action.yml updated to match.

Granularity stays minutes: concurrency is handled separately (a
workflow-level concurrency group, plus publish.sh's refuse-on-existing),
so it is now a pure headroom choice.

Tests (test_next_version_code.py, test_next_version_code.sh): a normal
commit yields its own minute, a commit older than the published maximum
still yields published+1, a future-dated commit within tolerance passes
through untouched while one beyond it is clamped and warns, a commit
dated near the int32 ceiling does not produce a near-int32 result, and a
dedicated property test checks the result always strictly exceeds every
versionCode in the fixture across all of the above. Each was verified by
injecting the corresponding fault (removing the backwards clamp, removing
the forwards clamp, and ignoring the commit's date entirely) and
confirming the relevant test failed, then reverting.

98 assertions offline (0 failed, 4 skipped -- pre-existing, live-bucket
only), up from 91.

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

127 lines
6.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"
# "now" is pinned for the whole script, the same reasoning
# test_next_version_code.py uses NEXT_VERSION_CODE_NOW for: every expected
# result below is then a fixed number, not one that depends on the real
# wall clock (and, unlike the Python suite, this file drives the real CLI
# through with-nixpkgs.sh, so it is also slower -- one more reason not to
# let it depend on when it happens to run).
export NEXT_VERSION_CODE_NOW="2026-06-01T00:00:00+00:00"
# The committer timestamp for most cases: the *same* instant as "now" above,
# as Unix seconds (`git log -1 --format=%ct`'s own unit) -- comfortably
# inside tolerance, so a normal invocation is not accidentally
# forward-clamped. Verified equal to NEXT_VERSION_CODE_NOW by construction:
# both come from 2026-06-01T00:00:00Z.
COMMITTER_TS=1780272000
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"
assert_failure "two arguments" bash "$here/next-version-code.sh" "$work/index.json" "$APP_ID"
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" "$COMMITTER_TS")
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" "$COMMITTER_TS")
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 commit dated far in the future is clamped through the real CLI, and warns ---"
# "now" is still pinned at 2026-06-01T00:00:00Z (exported above), so a
# committer date one century later (2126-01-01T00:00:00Z, hardcoded rather
# than computed with `date`, whose -d/-j flags differ between GNU and BSD)
# is unambiguously beyond tolerance.
future_ts=4922899200
out6=$(bash "$here/next-version-code.sh" "$work/other-app.json" "$APP_ID" "$future_ts" \
2>/tmp/nvc_stderr4.$$) || true
err6=$(cat /tmp/nvc_stderr4.$$); rm -f /tmp/nvc_stderr4.$$
if [[ "$out6" =~ ^[0-9]+$ ]]; then
pass "a far-future commit date still exits 0 (clamp, not rejection)"
else
fail "a far-future commit date still exits 0 (clamp, not rejection) (got '$out6')"
fi
assert_contains "a far-future commit date warns on stderr" "$err6" "warning"
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" "$COMMITTER_TS" 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" "$COMMITTER_TS" 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" "$COMMITTER_TS" 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"
echo "--- a non-numeric committer timestamp is refused, not silently coerced ---"
assert_failure "non-numeric committer timestamp" \
bash "$here/next-version-code.sh" "$work/index.json" "$APP_ID" "not-a-timestamp"
report_and_exit