Feed the F-Droid repo from Forgejo releases automatically
The scaffold here was a manual pipeline: copy an APK into repo/, run
`fdroid update`, rsync the result somewhere. Every app already builds and
signs its own APK in CI and publishes it as a Forgejo release, so all of
that copying was re-doing by hand what the forge already had.
Both halves of a listing now come from the same release: the APK from the
release asset, and the title, summary, description, changelog, icon and
screenshots from the fastlane/ tree at the same tag. A listing can no longer
describe a build other than the one it ships, because there is no second copy
of the text to drift.
fdroid-sync.py is the whole reconcile. It is a pure function of what the forge
currently holds — work out what should be published, make it so, re-sign the
index only if something moved — which is what makes it safe to drive from a
webhook that has no delivery guarantee and no retry. A missed delivery costs
latency, not correctness.
Three things were only found by running the real thing with an empty
environment, and all three fail late and confusingly:
- fdroidserver bundles apksigner but no JDK, and shells out to jar, keytool
and jarsigner by bare name. The PATH is now set outright rather than
inherited, because the bundled apksigner is a Gradle-style launcher that
also needs xargs and sed — and without xargs fdroidserver reads every APK
as having an invalid signature and quarantines the lot into archive/.
- index-v2 only carries a changelog for a version matching the last Builds
entry, and a repo of prebuilt APKs has no build recipes. Without a declared
entry every release shipped an empty "What's New".
- fdroidserver does not fall back to the APK's launcher icon, so an app whose
fastlane tree has no images/icon.png was listed with no icon at all, which
is two of these four. The sync now recovers one from the APK.
module.nix keeps the keystore password out of the Nix store via
fdroidserver's {env: ...} indirection, and serves the repo rooted at repo/
rather than at the working directory, which would also have handed out
config.yml and metadata/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
81319cee4f
commit
4f1a67bed3
9 changed files with 982 additions and 69 deletions
74
bin/register-webhooks.sh
Executable file
74
bin/register-webhooks.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
# Point every app repo's release webhook at the F-Droid rebuild endpoint.
|
||||
#
|
||||
# Forgejo webhooks are instance state, not repo content, so they cannot be
|
||||
# declared in a flake and have to be registered once per repo. This does that
|
||||
# from the single source of truth — apps.nix — so adding an app means editing
|
||||
# that file and re-running this, not remembering four web forms.
|
||||
#
|
||||
# Idempotent: a hook already pointing at the same URL is updated in place
|
||||
# rather than duplicated, so re-running after a secret rotation is the
|
||||
# supported way to roll the secret.
|
||||
#
|
||||
# The secret must be the same value the serving host reads from
|
||||
# webhookSecretFile; a mismatch makes every delivery fail the HMAC check and
|
||||
# the repo silently stops updating. Pipe it straight out of sops so it reaches
|
||||
# neither the terminal nor the shell history:
|
||||
#
|
||||
# <redacted>
|
||||
# FORGEJO_TOKEN=<token with repo:write on each app repo> \
|
||||
# FDROID_WEBHOOK_SECRET=$(<redacted>=<redacted> \
|
||||
# nix run nixpkgs#sops -- -d --extract '["fdroid_webhook_secret"]' \
|
||||
# <redacted>) \
|
||||
# ../fdroid-repo/bin/register-webhooks.sh
|
||||
#
|
||||
# Usage: register-webhooks.sh [base-url]
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
: "${FORGEJO_TOKEN:?set FORGEJO_TOKEN to a Forgejo access token with repo write access}"
|
||||
# Env, not an argument: an argv is visible to every other process on the box
|
||||
# for as long as the script runs.
|
||||
SECRET="${FDROID_WEBHOOK_SECRET:?set FDROID_WEBHOOK_SECRET to the shared webhook secret}"
|
||||
BASE="${1:-https://apps.randogoth.com}"
|
||||
FORGE="${FORGE:-https://code.randogoth.com}"
|
||||
URL="$BASE/hooks/rebuild-fdroid"
|
||||
|
||||
api() {
|
||||
local method=$1 path=$2
|
||||
shift 2
|
||||
curl -sS -X "$method" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$FORGE/api/v1$path" "$@"
|
||||
}
|
||||
|
||||
for repo in $(nix eval --json --file ./apps.nix | jq -r '.[].repo'); do
|
||||
# Forgejo returns the full hook list; match on the target URL because the
|
||||
# hook id is assigned by the server and is not knowable in advance.
|
||||
existing=$(api GET "/repos/$repo/hooks" | jq -r --arg u "$URL" \
|
||||
'.[] | select(.config.url == $u) | .id' | head -1)
|
||||
|
||||
payload=$(jq -n --arg url "$URL" --arg secret "$SECRET" '{
|
||||
type: "gitea",
|
||||
active: true,
|
||||
# "release" alone: a tag push fires before CI has built anything, so a
|
||||
# push-triggered rebuild would always find the old release and do nothing.
|
||||
events: ["release"],
|
||||
config: { url: $url, content_type: "json", secret: $secret }
|
||||
}')
|
||||
|
||||
if [ -n "$existing" ]; then
|
||||
api PATCH "/repos/$repo/hooks/$existing" -d "$payload" >/dev/null
|
||||
echo "updated $repo (hook $existing) -> $URL"
|
||||
else
|
||||
api POST "/repos/$repo/hooks" -d "$payload" >/dev/null
|
||||
echo "created $repo -> $URL"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Note: the release webhook fires when the release object is created,"
|
||||
echo "which in these workflows is one API call before the APK is attached."
|
||||
echo "fdroid-sync waits for the upload to land, so that race is handled."
|
||||
Loading…
Add table
Add a link
Reference in a new issue