fdroid-repo/bin/register-webhooks.sh

74 lines
2.9 KiB
Bash
Raw Normal View History

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>
2026-10-14 11:54:26 +03:00
#!/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.
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>
2026-10-14 11:54:26 +03:00
#
# Both are taken from the environment rather than arguments, so pipe them in
# from wherever they are kept — an argv is visible to every other process on
# the box, and a pasted secret stays in the shell history.
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>
2026-10-14 11:54:26 +03:00
#
# Usage:
# FORGEJO_TOKEN=<token with repo:write on each app repo> \
# FDROID_WEBHOOK_SECRET=<the shared webhook secret> \
# register-webhooks.sh [base-url]
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>
2026-10-14 11:54:26 +03:00
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."