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>
127 lines
6.3 KiB
Bash
Executable File
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
|