bit's review on PR #7 (comment on the PR, appended to this reasoning):
run_test_hook() eval'd an environment variable in publish.sh, which runs
with the release signing key and S3 credentials in its environment, in a
public repo. "Never set by a real caller" was true, but it was still a
documented, discoverable path from any env-var influence to arbitrary code
execution at the worst possible moment -- unnecessary, which is the
stronger reason to remove it than any claim about likelihood.
The replacement is a feature rather than a seam: --files-only and
--index-only split the two passes into separately invokable steps. With
neither flag the default is unchanged (both passes, files first). This
makes the ordering guarantee testable by construction -- run --files-only,
assert the bucket state, run --index-only, assert again -- with no hook, no
eval, and no test-only branch in production code. It is also independently
useful: an interrupted publish can resume at the index step without
re-uploading every APK, and a caller can stage files now and publish the
index later. The APK-existence check is skipped entirely for a plain
--index-only invocation, since that pass can never upload a *.apk.
Re-tested end to end: offline 91 passed / 4 skipped / 0 failed (up from 83/
3/0, mostly new flag-construction and stub assertions); live against
fdroid-weblib-dev 104 passed / 0 skipped / 0 failed, bucket verified empty
before and after. Added a live fault-injection test that --index-only alone
against a prefix --files-only never touched really does publish an index
naming an absent file -- the exact state the ordering guarantee exists to
prevent -- contrasted with the default path, which never reaches it.
Offline, removing the run_files/run_index gating (simulating the flags
being ignored) was confirmed to fail 5 of the new assertions before being
reverted.
Left the versionCode granularity as minutes, per instruction: that is being
settled separately on weblib-viewer#15 and is a one-constant change for
whenever it lands, not something to pre-empt here.
Co-authored-by: bit <bit@das-labor.org>
Closes#5Closes#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>