A daily job keeps the F-Droid tools current and rebuilds the repository when they change
Build / Desktop, server and tools (push) Successful in 51s

- Every day the F-Droid repository's tools are checked for a newer version that has been out for at least two
  weeks. When there is one, or when the app's listing changed, the repository is rebuilt with it, and releases
  use it only once that worked, so a faulty update of the tools can never hold up a release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9uhaCTSycTh1r8At679Pz
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-31 11:03:52 +01:00
1 parent cafa40decf
commit 10a85e6bc5
3 files changed
+100 -36

No files matched your search

+62 -25
View File
@@ -1,35 +1,32 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
version=$1 version=${1:-}
here=$(pwd) here=$(pwd)
root=${PACKAGES_ROOT:-/var/www/packages.arasaka.software} root=${PACKAGES_ROOT:-/var/www/packages.arasaka.software}
keep=${FDROID_KEEP:-3} keep=${FDROID_KEEP:-3}
url=https://packages.arasaka.software/fdroid/repo url=https://packages.arasaka.software/fdroid/repo
fingerprint=5F52CFF4F52F4E0B6DA6CD044BFCEAF78D1D42DB24F2470602DB7F19CBFDF846 fingerprint=5F52CFF4F52F4E0B6DA6CD044BFCEAF78D1D42DB24F2470602DB7F19CBFDF846
id=software.arasaka.meweb id=software.arasaka.meweb
apk=$here/releases/$version/MeWeb-$version-android.apk
test -f "$apk"
if [ -z "${MEWEB_KEYSTORE_B64:-}" ] || [ -z "${MEWEB_KEYSTORE_PASS:-}" ]; then if [ -z "${MEWEB_KEYSTORE_B64:-}" ] || [ -z "${MEWEB_KEYSTORE_PASS:-}" ]; then
echo "::error::the repository secrets MEWEB_KEYSTORE_B64 and MEWEB_KEYSTORE_PASS are not set" echo "::error::the repository secrets MEWEB_KEYSTORE_B64 and MEWEB_KEYSTORE_PASS are not set"
exit 1 exit 1
fi fi
major=$(echo "$version" | cut -d. -f1) if [ -n "$version" ]; then
minor=$(echo "$version" | cut -d. -f2) apk=$here/releases/$version/MeWeb-$version-android.apk
patch=$(echo "$version" | cut -d. -f3) test -f "$apk"
code=$((major * 10000 + minor * 100 + patch)) major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
patch=$(echo "$version" | cut -d. -f3)
code=$((major * 10000 + minor * 100 + patch))
elif [ ! -e "$root/fdroid/repo" ]; then
echo "no repository at $root/fdroid/repo yet: the first release makes it"
exit 0
fi
tools/setup.sh --group fdroid tools/setup.sh --group fdroid
tools=$here/tools/fdroid/linux-x64 tools=$(cd "$here/tools/fdroid/linux-x64" && pwd -P)
uv=$tools/uv-x86_64-unknown-linux-gnu/uv uv=$tools/uv-x86_64-unknown-linux-gnu/uv
work=$(mktemp -d) python=$(command -v python3)
trap 'rm -rf "$work"' EXIT
venv=$work/venv
settled=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)
env -u MEWEB_KEYSTORE_B64 -u MEWEB_KEYSTORE_PASS UV_CACHE_DIR="$tools/cache" UV_PYTHON_INSTALL_DIR="$tools/python" \
"$uv" venv -q --python 3 "$venv"
env -u MEWEB_KEYSTORE_B64 -u MEWEB_KEYSTORE_PASS UV_CACHE_DIR="$tools/cache" \
"$uv" pip install -q --python "$venv/bin/python" --exclude-newer "$settled" fdroidserver
echo "fdroidserver $("$venv/bin/fdroid" --version), the newest released before $settled"
JAVA_HOME=$(ls -d "$here"/tools/jdk/linux-x64/*/ | head -n 1) JAVA_HOME=$(ls -d "$here"/tools/jdk/linux-x64/*/ | head -n 1)
ANDROID_HOME=$here/tools/android-sdk ANDROID_HOME=$here/tools/android-sdk
PATH=$JAVA_HOME/bin:$PATH PATH=$JAVA_HOME/bin:$PATH
@@ -37,6 +34,33 @@ export JAVA_HOME ANDROID_HOME PATH
exec 9>>"$root/.lock" exec 9>>"$root/.lock"
flock -w 2700 9 flock -w 2700 9
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
keyless() {
env -u MEWEB_KEYSTORE_B64 -u MEWEB_KEYSTORE_PASS UV_CACHE_DIR="$tools/cache" "$@"
}
if [ -n "$version" ] && "$tools/current/bin/fdroid" --version > /dev/null 2>&1; then
install=$(readlink -f "$tools/current")
else
settled=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)
echo fdroidserver | keyless "$uv" pip compile -q --python "$python" --exclude-newer "$settled" --generate-hashes \
--no-header --no-annotate -o "$work/lock.txt" -
install=$tools/fdroidserver-$(sha256sum < "$work/lock.txt" | cut -c1-12)
if ! "$install/bin/fdroid" --version > /dev/null 2>&1; then
rm -rf "$install"
keyless "$uv" venv -q --python "$python" "$install"
keyless "$uv" pip sync -q --python "$install/bin/python" "$work/lock.txt"
cp "$work/lock.txt" "$install/lock.txt"
fi
fi
tool=$("$install/bin/fdroid" --version)
stamp=$( { cat "$install/lock.txt"; cd packaging/fdroid && find config.yml icon.png metadata -type f | LC_ALL=C sort | xargs sha256sum; } | sha256sum | cut -d' ' -f1)
if [ -z "$version" ] && [ "$(cat "$(readlink -f "$root/fdroid/repo")/../built-from" 2>/dev/null)" = "$stamp" ]; then
echo "nothing new: the repository is already built with fdroidserver $tool and this listing"
exit 0
fi
echo "fdroidserver $tool, $(basename "$install")"
(umask 077 && printf '%s' "$MEWEB_KEYSTORE_B64" | base64 -d > "$work/meweb.keystore") (umask 077 && printf '%s' "$MEWEB_KEYSTORE_B64" | base64 -d > "$work/meweb.keystore")
MEWEB_KEYSTORE=$work/meweb.keystore MEWEB_KEYSTORE=$work/meweb.keystore
@@ -46,13 +70,16 @@ held=$(keytool -J-Duser.language=en -list -v -keystore "$MEWEB_KEYSTORE" -storep
test "$held" = "$fingerprint" test "$held" = "$fingerprint"
state=$root/.fdroid state=$root/.fdroid
snap=$state/$(date -u +%Y%m%d%H%M%S)-$version snap=$state/$(date -u +%Y%m%d%H%M%S)-${version:-tools}
mkdir -p "$snap/repo" "$snap/metadata" "$snap/tmp" mkdir -p "$snap/repo" "$snap/metadata" "$snap/tmp"
if [ -e "$root/fdroid/repo" ]; then if [ -e "$root/fdroid/repo" ]; then
prior=$(readlink -f "$root/fdroid/repo")/.. prior=$(readlink -f "$root/fdroid/repo")/..
for file in "$prior"/repo/*.apk; do for file in "$prior"/repo/*.apk; do
[ -e "$file" ] && ln "$file" "$snap/repo/" [ -e "$file" ] && ln "$file" "$snap/repo/"
done done
for icons in "$prior"/repo/icons*; do
[ -d "$icons" ] && cp -a "$icons" "$snap/repo/"
done
cp -a "$prior/metadata/." "$snap/metadata/" cp -a "$prior/metadata/." "$snap/metadata/"
cp "$prior/repo/index-v2.json" "$snap/repo/" cp "$prior/repo/index-v2.json" "$snap/repo/"
if [ -f "$prior/tmp/apkcache.json" ]; then cp "$prior/tmp/apkcache.json" "$snap/tmp/"; fi if [ -f "$prior/tmp/apkcache.json" ]; then cp "$prior/tmp/apkcache.json" "$snap/tmp/"; fi
@@ -61,15 +88,19 @@ cp packaging/fdroid/config.yml "$snap/config.yml"
chmod 600 "$snap/config.yml" chmod 600 "$snap/config.yml"
cp packaging/fdroid/icon.png "$snap/icon.png" cp packaging/fdroid/icon.png "$snap/icon.png"
cp -r packaging/fdroid/metadata/. "$snap/metadata/" cp -r packaging/fdroid/metadata/. "$snap/metadata/"
rm -f "$snap/repo/${id}_$code.apk" if [ -n "$version" ]; then
cp "$apk" "$snap/repo/${id}_$code.apk" rm -f "$snap/repo/${id}_$code.apk"
cp "$apk" "$snap/repo/${id}_$code.apk"
fi
(cd "$snap/repo" && ls "$id"_*.apk | sed 's/.*_\([0-9]*\)\.apk$/\1/' | sort -n | head -n "-$keep" | while read -r old; do (cd "$snap/repo" && ls "$id"_*.apk | sed 's/.*_\([0-9]*\)\.apk$/\1/' | sort -n | head -n "-$keep" | while read -r old; do
rm -f "${id}_$old.apk" rm -f "${id}_$old.apk"
done) done)
newest=$(cd "$snap/repo" && ls "$id"_*.apk | sed 's/.*_\([0-9]*\)\.apk$/\1/' | sort -n | tail -n 1)
(cd "$snap" && "$venv/bin/fdroid" update) (cd "$snap" && "$install/bin/fdroid" update)
rm -rf "$snap/repo/status" rm -rf "$snap/repo/status"
chmod -R u=rwX,go=rX "$snap/repo" chmod -R u=rwX,go=rX "$snap/repo"
echo "$stamp" > "$snap/built-from"
mkdir -p "$root/fdroid" mkdir -p "$root/fdroid"
ln -sfn "../.fdroid/$(basename "$snap")/repo" "$root/fdroid/repo.new" ln -sfn "../.fdroid/$(basename "$snap")/repo" "$root/fdroid/repo.new"
@@ -77,13 +108,19 @@ mv -T -f "$root/fdroid/repo.new" "$root/fdroid/repo"
ls -1d "$state"/*/ | sort | head -n -3 | while read -r old; do ls -1d "$state"/*/ | sort | head -n -3 | while read -r old; do
find "$old" -maxdepth 0 -mmin +1440 -exec rm -rf {} + find "$old" -maxdepth 0 -mmin +1440 -exec rm -rf {} +
done done
echo "published ${id}_$code.apk in $root/fdroid/repo" echo "published $root/fdroid/repo, newest ${id}_$newest.apk, with fdroidserver $tool"
if [ "$root" = /var/www/packages.arasaka.software ]; then if [ "$root" = /var/www/packages.arasaka.software ]; then
curl -fsS "$url/entry.jar" -o "$work/entry.jar" curl -fsS "$url/entry.jar" -o "$work/entry.jar"
signed=$(keytool -J-Duser.language=en -printcert -jarfile "$work/entry.jar" | sed -n 's/^.*SHA256: *//p' | head -n 1 | tr -d ':') signed=$(keytool -J-Duser.language=en -printcert -jarfile "$work/entry.jar" | sed -n 's/^.*SHA256: *//p' | head -n 1 | tr -d ':')
test "$signed" = "$fingerprint" test "$signed" = "$fingerprint"
curl -fsS "$url/index-v2.json" | grep -Eq "\"versionCode\": ?$code\b" curl -fsS "$url/index-v2.json" | grep -Eq "\"versionCode\": ?$newest\b"
curl -fsSI "$url/${id}_$code.apk" > /dev/null curl -fsSI "$url/${id}_$newest.apk" > /dev/null
echo "live at $url: $id $version ($code)" echo "live at $url: newest $id $newest"
fi fi
ln -sfn "$(basename "$install")" "$tools/current.new"
mv -T -f "$tools/current.new" "$tools/current"
for old in "$tools"/fdroidserver-*; do
[ "$old" = "$install" ] || rm -rf "$old"
done
+22
View File
@@ -0,0 +1,22 @@
name: F-Droid tools
on:
schedule:
- cron: '23 5 * * *'
workflow_dispatch:
jobs:
refresh:
name: The newest F-Droid tools, tried on the repository
runs-on: nuvola
steps:
- uses: actions/checkout@v4
- name: Prepare
run: .gitea/scripts/prepare.sh
- name: Rebuild the repository on packages.arasaka.software when the tools or the listing changed
env:
MEWEB_KEYSTORE_B64: ${{ secrets.MEWEB_KEYSTORE_B64 }}
MEWEB_KEYSTORE_PASS: ${{ secrets.MEWEB_KEYSTORE_PASS }}
run: .gitea/scripts/fdroid.sh
+16 -11
View File
@@ -44,7 +44,8 @@ is root-owned, so CI uses the runner user's own `~/.dotnet` (passolo's `tools/re
| `MEWEB_KEYSTORE` | release step | a temporary file in `$RUNNER_TEMP` decoded by `.gitea/scripts/android.sh`, removed on exit | | `MEWEB_KEYSTORE` | release step | a temporary file in `$RUNNER_TEMP` decoded by `.gitea/scripts/android.sh`, removed on exit |
`build.yml` runs on every push to `master`; `release.yml` on a `v*` tag (publishes the Gitea release) or by hand `build.yml` runs on every push to `master`; `release.yml` on a `v*` tag (publishes the Gitea release) or by hand
(`publish: false` builds every platform and deploys the server only). Both run on the Arasaka org runner `nuvola`, (`publish: false` builds every platform and deploys the server only); `fdroid.yml` daily at 05:23 UTC or by hand
(the F-Droid tools, below). All three run on the Arasaka org runner `nuvola`,
user `gitea-runner`, home `/var/lib/gitea-runner`: `tools/sdk.sh` installs the SDK from `global.json` into its user `gitea-runner`, home `/var/lib/gitea-runner`: `tools/sdk.sh` installs the SDK from `global.json` into its
`~/.dotnet` (the system SDK in `/opt/dotnet` is older and root-owned), and the JDK, nfpm and the Android SDK are `~/.dotnet` (the system SDK in `/opt/dotnet` is older and root-owned), and the JDK, nfpm and the Android SDK are
cached in `~/.cache/meweb`. There is deliberately no `pull_request` trigger: the runner is the production box. cached in `~/.cache/meweb`. There is deliberately no `pull_request` trigger: the runner is the production box.
@@ -176,20 +177,24 @@ The Android release key: alias `meweb`, certificate SHA-256
- **The APK reaches an F-Droid repository on packages.arasaka.software from the same job** (31 Oct 2026), after the - **The APK reaches an F-Droid repository on packages.arasaka.software from the same job** (31 Oct 2026), after the
release is published, so the repository can lag the release page but never lead it. `.gitea/scripts/fdroid.sh` release is published, so the repository can lag the release page but never lead it. `.gitea/scripts/fdroid.sh`
installs fdroidserver with uv from `setup.json` (its download cache in `tools/fdroid`, cached like the JDK), because installs fdroidserver with uv from `setup.json` (its download cache in `tools/fdroid`, cached like the JDK), because
the box has no `ensurepip` and the apt package would bring 242 others. **Nothing pins fdroidserver** (owner, 31 Oct the box has no `ensurepip` and the apt package would bring 242 others. **Nothing pins fdroidserver; a daily job
2026: no patching it from time to time): each run installs the newest fdroidserver, and the newest of everything it keeps it current** (owner, 31 Oct 2026: a daily service, not patching it from time to time). `fdroid.yml` runs the
needs, published at least 14 days before the run, so a bad upload has usually been pulled before the job sees it, same script with no version at 05:23 UTC: it resolves the newest fdroidserver, and the newest of everything it
and prints the version it got. It installs into a fresh environment in a temporary folder with the signing key taken needs, published at least 14 days earlier (a bad upload has usually been pulled by then), against the box's own
out of its environment; nothing is kept between runs, because an environment's scripts carry the absolute path they `python3`, into a hash-locked list; a new list gets its own install, `fdroidserver-<hash of the list>` in the cache's
were made at, and 0.15.1's step failed with `fdroid: not found` on a kept one. A new fdroidserver that breaks real path (an install's scripts carry the absolute path they were made at; 0.15.1 failed with `fdroid: not found` on
`fdroid update` fails this step after the release is published: the repository then lags, and the log names the one made from a deleted folder). When that list or the listing in `packaging/fdroid/` differs from what the live
version to look at. repository was built from (`built-from` beside each snapshot), it rebuilds the repository with it, and only once
that is live and verified does the install become `current` and the older ones go. A release uses `current` and
downloads nothing, so a new fdroidserver that breaks fails the daily job, never a release; the job's log names the
version. Every install runs with the signing key out of its environment.
It signs the repository with the APK's own key (`MEWEB_KEYSTORE_B64` and `MEWEB_KEYSTORE_PASS`, alias `meweb`), so It signs the repository with the APK's own key (`MEWEB_KEYSTORE_B64` and `MEWEB_KEYSTORE_PASS`, alias `meweb`), so
the F-Droid fingerprint is the certificate's `5F52CFF4…FDF846` and there is no second key to lose; it refuses any the F-Droid fingerprint is the certificate's `5F52CFF4…FDF846` and there is no second key to lose; it refuses any
other. It builds a new snapshot in other. It builds a new snapshot in
`/var/www/packages.arasaka.software/.fdroid/` from the live one (the APKs as hard links, so `rm` then `cp` as above; `/var/www/packages.arasaka.software/.fdroid/` from the live one (the APKs as hard links, so `rm` then `cp` as above;
the metadata, `apkcache.json` and the previous `index-v2.json`, which is where fdroidserver reads each APK's first the metadata, the icons, `apkcache.json` and the previous `index-v2.json`, which is where fdroidserver reads each
date), adds this version, keeps the newest three, runs `fdroid update`, deletes `repo/status/` (it names the APK's first date; without the icons a rebuild with no new APK fails, because the cache skips extracting them), adds
this version (a release) or nothing (the daily job), keeps the newest three, runs `fdroid update`, deletes `repo/status/` (it names the
build machine's paths and `/etc/issue.net`), swaps the `fdroid/repo` link and checks the live signature. The key build machine's paths and `/etc/issue.net`), swaps the `fdroid/repo` link and checks the live signature. The key
was already never to be regenerated; now everyone who added the repository trusts only its fingerprint too. `packaging/fdroid/` holds the was already never to be regenerated; now everyone who added the repository trusts only its fingerprint too. `packaging/fdroid/` holds the
repository's settings, its icon (the Arasaka emblem) and the app's listing in English and Italian; the download repository's settings, its icon (the Arasaka emblem) and the app's listing in English and Italian; the download