Skip to content

build: support unsigned release builds for F-Droid - #371

Open
garfiec wants to merge 3 commits into
developfrom
chore/unsigned-release-builds
Open

build: support unsigned release builds for F-Droid#371
garfiec wants to merge 3 commits into
developfrom
chore/unsigned-release-builds

Conversation

@garfiec

@garfiec garfiec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

F-Droid builds from source on its own infrastructure and needs an APK with no signature, which this repo could not produce — a credential-less assembleRelease fell through to the committed debug key and emitted an app-release.apk that looked signed. This adds that build mode, removes a defect that made a published release impossible to rebuild from its own tag, and adds the store-listing metadata F-Droid and IzzyOnDroid read.

Changes

Build

  • -PunsignedRelease drops the release signing config, so AGP emits app-release-unsigned.apk. It is read through providers.gradleProperty to stay a tracked configuration-cache input, and outranks credentials on purpose so the path is testable without moving keystore.properties aside.
  • The flag itself carries no F-Droid-specific concept and adds no product flavor — it means "build unsigned" and is equally usable by any downstream packager. F-Droid requests it from its own recipe via gradleprops.

Release workflow

  • The version-bump commit and tag are created before the build and pushed only after every check passes. BuildConfig.GIT_SHA is stamped from git rev-parse HEAD, so building first shipped an APK carrying the SHA of the commit preceding its own tag. A failed run pushes no commit, tag or release.
  • Asserts the built APK carries the signing certificate published in the README. A swapped or rotated keystore secret is otherwise undetectable here, and Android refuses in-place updates across a key change.
  • Rebuilds with -PunsignedRelease and asserts the result is genuinely unsigned. This runs at release cuts rather than in ordinary CI: it is the only time the unsigned build matters, and that job already pays for one R8 run.

Versioning

  • version.properties spells out versionCode alongside versionName. bump-version.sh writes it and refuses a bump if the line is missing; the plugin still derives the code and fails the build if the literal disagrees. F-Droid's update detection is regex-only and cannot evaluate the packing.

Store metadata

  • fastlane/metadata/android/en-US/ — descriptions, a changelog keyed by versionCode, and phone/tablet screenshots, with a fastlane/README.md covering the naming rules.

Release process

  • docs/RELEASING.md documents the unsigned build, the tag-before-build ordering and the certificate assertion, and gains one new manual step: a changelog file must be committed before dispatching a release, because F-Droid reads it from the tagged commit and the release commit touches only version.properties.

Verification

detekt, detektMetadataCommonMain, test, :app:assembleDebug and vendor-web-assets.py --check pass. assembleRelease was run with and without the flag, confirming the output filename and apksigner verify result in each case, and Gradle was confirmed to invalidate the configuration cache citing unsignedRelease. Both new workflow guards were executed against real APKs, and the expected certificate was checked against the fingerprint in README.md. Every feature and privacy claim in the store listing was checked against the source.

Not device-tested. The workflow changes are unexercised until the next release cut.

Notes

  • An rc and its finalized stable share a versionCode, so F-Droid cannot offer one as an update to the other.
  • Nothing writes the fastlane changelog automatically; it is a documented pre-flight step.
  • This removes the known blocker to reproducing a release, but reproducibility has not been demonstrated end to end — a rebuild inside fdroidserver is the remaining gate before an RFP, since the developer-signed model has no fallback.

Without credentials, `assembleRelease` fell through to the committed debug
key and emitted an `app-release.apk` that looks signed. Packagers that apply
their own signature need no signature at all, and the failure was silent.

`-PunsignedRelease` drops the signing config so AGP emits
`app-release-unsigned.apk`. It outranks credentials on purpose, so the path
is testable without moving `keystore.properties` aside, and the differing
filename means the two builds cannot be confused.

The release workflow now asserts the shipped APK carries the published
certificate — a swapped keystore secret was otherwise undetectable, and a key
change strands every existing install — and re-verifies the unsigned path,
which nothing else exercises.
BuildConfig.GIT_SHA is stamped from `git rev-parse HEAD` at build time, but
the workflow built the APK before committing the version bump and tagging.
The published v2026.08.4 APK therefore carries fcbaf61 in its dex — the
parent of tag v2026.08.4 (c09ec96). Anyone rebuilding from the tag gets a
different string and can never match the release byte-for-byte, which rules
out F-Droid publishing our developer-signed APK instead of re-signing it.

Commit and tag locally before the build, and push only once every check has
passed. A failed build still leaves nothing behind: the commit and tag are
local to the runner.
F-Droid's update detection is regex-only and cannot evaluate our packing, so
a versionCode derived inside the Gradle plugin is invisible to it — every
release would need a hand-filed metadata merge request. Spell the code out in
version.properties, write it from bump-version.sh, and have the plugin keep
re-deriving it and fail the build when the two disagree, so the literal is a
cross-check rather than a second source of truth.

Add fastlane/metadata/android/en-US/, the listing layout F-Droid and
IzzyOnDroid both read: descriptions, a changelog keyed by versionCode, and
phone/tablet screenshots.
@github-actions

Copy link
Copy Markdown
Contributor

Android debug APK

Artifact: switchboard-android-debug-371
Download: switchboard-android-debug-371.zip
Retention: 90 days
Commit: 365e463efa8b005ba7cdfd22ea0201446b891183

Download requires a GitHub login. Installs over previous debug builds without uninstalling (stable signing key).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant