From af32b7ca12564291a4a941b8634ff4429a115868 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 10 Sep 2026 02:00:30 +0000 Subject: [PATCH] Document and hint at the APK-filename/versionCode naming contract publish.sh's overwrite guard (fdroid-repo-ci#6) is correct but implies a contract nothing states: a colliding filename with no versionCode in it means only the first publish into a prefix can ever succeed. Flutter's constant app--release.apk hit this in weblib-viewer (#29), and the failure only surfaces on the second publish with an error that reads like a bucket-state problem. Add the naming contract to the README, and append a hint to the refusal message when the colliding object's basename has no digits at all -- a pure text addition, never changing which publishes are refused. Closes fdroid-repo-ci#13. Co-authored-by: bit --- README.md | 22 ++++++++++++++++++++++ publish.sh | 12 ++++++++++++ test_publish.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/README.md b/README.md index a7a820b..3f87fc1 100644 --- a/README.md +++ b/README.md @@ -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 `_.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--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 `_.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: diff --git a/publish.sh b/publish.sh index e4d866e..c05212a 100755 --- a/publish.sh +++ b/publish.sh @@ -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--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 _.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 diff --git a/test_publish.sh b/test_publish.sh index a2b7d6f..970a62b 100755 --- a/test_publish.sh +++ b/test_publish.sh @@ -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" \ + "_.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 -- 2.54.0