Document the APK filename/versionCode naming contract #14

Merged
bit merged 1 commits from apk-versioncode-naming-contract into main 2026-09-13 08:01:21 +00:00
3 changed files with 74 additions and 0 deletions
+22
View File
@@ -272,6 +272,28 @@ collision; it prevents the collision from silently corrupting the repo.
Skipped entirely for a plain `--index-only` invocation (below), since that
pass can never upload a `*.apk` in the first place.
### APK filenames must carry the versionCode (fdroid-repo-ci#13)
That guard is correct -- a published versionCode's artifact has to stay
immutable -- but it has a consequence nothing else here states: **the
filename has to carry the versionCode**, or the guard permits exactly one
publish into a prefix, ever. `publish.sh` checks the destination *key*, not
the artifact's contents, so a build that always emits the same filename
collides with itself on its second run regardless of what actually changed
inside the APK.
Name the APK `<applicationId>_<versionCode>.apk` -- F-Droid's own
convention, and what `fdroid update` expects -- before handing it to
`fdroid-update.sh`/`publish.sh`. Concretely: Flutter's
`flutter build apk` always emits `app-<flavor>-release.apk`, independent of
`--build-number`, so a consumer that publishes that path unchanged gets a
successful first publish and then a permanent refusal on every publish
after it -- exactly what happened to weblib-viewer (weblib-viewer#29) before
it started renaming the artifact to `<applicationId>_<versionCode>.apk`
ahead of this step. The failure only shows up on the *second* publish, long
after the integration looks finished, and reads like a bucket-state problem
(see the error text below) rather than the naming contract it actually is.
### `--files-only` and `--index-only`
The two passes can also be run as two separate invocations:
+12
View File
@@ -267,6 +267,18 @@ if [ "$run_files" = true ]; then
fi
if head_out=$("${aws_base[@]}" s3api head-object --bucket "$bucket" --key "$key" 2>&1); then
echo "publish.sh: refusing to publish -- $key already exists in s3://$bucket (would overwrite an existing APK, fdroid-repo-ci#6)" >&2
# Hint only, never a control-flow change (fdroid-repo-ci#13): a
# colliding filename with no digits at all cannot be carrying a
# versionCode, which is the single most common way to trip this
# guard on every publish after the first -- Flutter's
# app-<flavor>-release.apk being the concrete case that motivated
# this. A filename that does contain digits still gets refused with
# just the message above, exactly as before; it may yet be a genuine
# collision (fdroid-repo-ci#6) and this heuristic cannot tell.
base=$(basename "$key")
if [[ $base != *[0-9]* ]]; then
echo "publish.sh: $base does not appear to carry a versionCode -- F-Droid's convention is <applicationId>_<versionCode>.apk; a constant filename plus this guard means only the first publish into a prefix can ever succeed" >&2
fi
exit 1
elif echo "$head_out" | grep -qi '404\|not found\|NoSuchKey'; then
: # does not exist yet -- fine, this is the common case
+40
View File
@@ -213,6 +213,45 @@ if [ "$have_python" = true ]; then
else
pass "publish.sh never starts pass 1 after refusing a colliding APK"
fi
# de.example.app_1.apk carries a digit (the versionCode "1"), so the
# no-digits hint below (fdroid-repo-ci#13) must not fire for it -- this is
# the negative control for that hint, proving it is not just always on.
if echo "$out" | grep -q "does not appear to carry a versionCode"; then
fail "publish.sh must not hint about a missing versionCode when the colliding filename has digits (it did!)"
else
pass "publish.sh does not add the no-versionCode hint when the colliding filename has digits"
fi
echo "--- a colliding filename with no digits gets the versionCode hint (fdroid-repo-ci#13) ---"
# The Flutter case the issue is about: a build that always emits the same
# filename regardless of version, e.g. app-dev-release.apk. The guard
# still refuses exactly as above; only the message grows a line pointing
# at the naming contract, since a bucket-state-shaped error is what took a
# full CI cycle to diagnose in weblib-viewer#29.
nodigits_dir="$work/nodigits"
mkdir -p "$nodigits_dir"
echo '{}' > "$nodigits_dir/index-v2.json"
echo 'unsigned bytes, same name every build' > "$nodigits_dir/app-dev-release.apk"
start_stub mybucket "myprefix/app-dev-release.apk"
out=$(bash "$here/publish.sh" "$nodigits_dir" mybucket "http://127.0.0.1:$stub_port" myprefix 2>&1) && rc=0 || rc=$?
kill "$stub_pid" 2>/dev/null || true
wait "$stub_pid" 2>/dev/null || true
if [ "${rc:-0}" -ne 0 ]; then
pass "publish.sh refuses to publish a colliding APK filename with no digits"
else
fail "publish.sh refuses to publish a colliding APK filename with no digits (got exit 0)"
fi
assert_contains "the refusal names the colliding key" "$out" "myprefix/app-dev-release.apk"
assert_contains "the refusal explains why" "$out" "already exists"
assert_contains "the refusal hints that the filename carries no versionCode" "$out" \
"does not appear to carry a versionCode"
assert_contains "the hint states F-Droid's naming convention" "$out" \
"<applicationId>_<versionCode>.apk"
if echo "$out" | grep -q "pass 1/2"; then
fail "publish.sh must not start pass 1 after refusing a colliding no-digits APK (it did!)"
else
pass "publish.sh never starts pass 1 after refusing a colliding no-digits APK"
fi
echo "--- negative control: a non-colliding APK does not trip the same check ---"
start_stub mybucket # nothing pre-exists
@@ -244,6 +283,7 @@ if [ "$have_python" = true ]; then
fi
else
skip "credential-check-is-real / APK-collision-refusal (no python3 available, even via with-nixpkgs.sh)"
skip "APK-collision refusal includes the no-versionCode hint for a digit-free filename"
skip "negative control: non-colliding APK does not trip the check"
skip "--index-only never runs the APK-collision check"
fi