From 5bc499cf3a80d5fdb8e755121714c998d2903f89 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 18 Sep 2026 11:57:25 -0700 Subject: [PATCH] Add f-droid to the release pipeline --- README.md | 8 +++++- scripts/release.mjs | 59 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 18df84ee..34de82bf 100644 --- a/README.md +++ b/README.md @@ -178,17 +178,23 @@ run and prints the command to pick up from there. | --- | --- | | `web` | `scripts/build.sh`: web bundle, `cap sync`, generated icons and splash screens | | `apk` | `assembleRelease` signed with the distribution key, renamed to the path in `zapstore.yaml` | +| `fdroid` | reruns F-Droid's own preparation and build against the tag in a throwaway worktree | | `play` | `bundleRelease` signed with the upload key, uploaded to a Play track as a draft | | `ios` | `cap build ios` to an archive and IPA, uploaded with `altool` | | `desktop` | `package:desktop:*` for this OS | | `gitea` | creates the release for the tag from the changelog, attaches the APK and any desktop packages | | `zapstore` | `zsp publish zapstore.yaml` | -| `fdroid` | nothing to upload; F-Droid builds from the tag, see [fdroid/README.md](fdroid/README.md) | Release notes come from the `CHANGELOG.md` section matching `package.json`'s version, so every store shows the same text. The APK and zapstore share one artifact, whose path lives in `zapstore.yaml`. +F-Droid has nothing to upload — their servers build from the tag themselves — so the `fdroid` step +is a gate instead: it runs [their preparation and build](fdroid/README.md) against the tag in a +throwaway git worktree, and fails the release before anything is published if that build no longer +works. Preparation patches source with exact-match replacements, so it breaks quietly when the +files it rewrites change. Expect it to take a while; it installs and builds from scratch. + ### Credentials These go in `.env.local`, which is gitignored. `pnpm release --check` lists whichever are missing diff --git a/scripts/release.mjs b/scripts/release.mjs index 358ecc8c..2133ce90 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import {existsSync} from "node:fs" -import {readFile, readdir, rename} from "node:fs/promises" +import {mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises" +import {tmpdir} from "node:os" import {dirname, join, resolve} from "node:path" import {fileURLToPath} from "node:url" import {parseArgs} from "node:util" @@ -143,6 +144,52 @@ const steps = [ await rename(join(dirname(apk), outputFile), apk) }, }, + { + name: "fdroid", + title: "Rebuild the tag the way F-Droid will", + missing: () => + git("cat-file", "-e", `${version}:scripts/prepare-fdroid-source.sh`) === undefined + ? [`F-Droid support in the ${version} tag`] + : [], + setup: [ + `The ${version} tag is older than fdroid/, so there is nothing for F-Droid to build from it.`, + "Name the steps you do want, or release a tag that has it.", + ], + run: async () => { + const parent = await mkdtemp(join(tmpdir(), "flotilla-fdroid-")) + const checkout = join(parent, "flotilla") + + // Preparation rewrites source and dependencies in place, so it only runs against a checkout + // that can be thrown away + try { + await run("git", ["worktree", "add", "--detach", checkout, version], {cwd: root}) + await run("./scripts/prepare-fdroid-source.sh", [], {cwd: checkout}) + await run("./scripts/build-fdroid-assets.sh", [], {cwd: checkout}) + + // F-Droid signs its own builds, so keep the distribution key out of gradle's environment + const env = {...process.env} + + delete env.ANDROID_KEYSTORE_PATH + + await run("./gradlew", ["--no-daemon", "assembleFdroidRelease"], { + cwd: join(checkout, "android"), + env, + }) + + const built = join( + checkout, + "android/app/build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk", + ) + + if (!existsSync(built)) { + throw new Error(`the F-Droid build produced no apk at ${built}`) + } + } finally { + await rm(parent, {recursive: true, force: true}) + await run("git", ["worktree", "prune"], {cwd: root}) + } + }, + }, { name: "play", title: "Build the AAB and upload it to Google Play", @@ -289,16 +336,6 @@ const steps = [ ], run: () => run("zsp", ["publish", "zapstore.yaml"], {cwd: root}), }, - { - name: "fdroid", - title: "F-Droid", - optional: true, - manual: [ - "F-Droid builds from source on their own servers, so a release has nothing to upload: the", - "metadata tracks version tags, which makes pushing the tag the whole story. The fdroiddata", - "submission is still open — see fdroid/README.md.", - ], - }, ] const unknownStep = chosen.find(step => !steps.some(({name}) => name === step))