A release is a signed APK in dist/, built from a clean tree, with its SHA-256
next to it.
The key lives outside the repository and its password in the system keyring. Losing the key means nobody who installed Tern can ever be updated, so back it up before the first release.
keytool -genkeypair -alias tern-upload -keystore ~/keys/tern-upload.jks \
-keyalg RSA -keysize 4096 -validity 10950 -dname "CN=Munzzyy"
secret-tool store --label "Tern upload keystore" service tern-keystore key upload- Set
versionCodeandversionNameinapp/build.gradle.kts, add the entry toCHANGELOG.md, and addfastlane/metadata/android/en-US/changelogs/<versionCode>.txt. - Run the device tests on every image:
./gradlew :app:assembleDebug :app:assembleDebugAndroidTest, thenbash tools/device-suite.sh <serial>for each. Gradle's own connected task is not used, because it drops the install permission that old Android only takes from the host, and the tests that install something then step aside without a word. The script names every test that was skipped. CI runs only the engine's tests, and only on Android 16 (tools/ci-emulator.sh), so the screen tests and the other images are run here by hand. bash tools/check-reproducible.sh ~/.cache/tern-reproducibleon the commit to release.bash tools/release.sh. It runs what the build job of CI runs, in the same order: the wrapper, translation and network checks, the JVM tests ofcore, the handoff page and site checks, the JVM tests ofapp, the release build, lint,tools/check-java-api.shandtools/check-apk.sh. Thentools/check-release-notes.shwants aCHANGELOG.mdheading for the version and its fastlane changelog of at most 500 characters. Only then does it sign the APK with apksigner 34.0.0 and writedist/tern-<version>.apk,dist/tern.apkand their.sha256files.bash tools/release.sh --gates-onlyruns the same gates without the key.- Install
dist/tern-<version>.apkon an emulator over the previous release and walk through adding, installing and updating an app. - Tag, push, and create the release with both APK files and both checksum files.
- F-Droid takes the release from the tag. Its recipe,
metadata/io.github.munzzyy.tern.ymlin fdroiddata, looks at the tags (UpdateCheckMode: Tags), readsversionNameandversionCodefromapp/build.gradle.ktsat the new one and adds the build itself (AutoUpdateMode: Version), so there is no merge request to bump. Its build has to reproduce the APK signed in step 4: it fetchesreleases/download/v%v/tern-%v.apk(Binaries) and takes the signature from it only when the two match. So the tag isv<versionName>on the released commit, the file is uploaded as exactlytern-<versionName>.apk, and both version values stay plain literals inapp/build.gradle.kts. Never push a tag that is not a release, since F-Droid reads every tag on GitHub as one.
apksigner comes from build-tools 34.0.0 on purpose. F-Droid copies the developer's signature onto its own reproducible build, and its tool rejects signatures made by newer build-tools.
Two builds of one commit give the same unsigned file, bit for bit.
tools/check-reproducible.sh proves it by building two fresh copies in two
folders of different names without the build cache, and CI runs it on every
push. On 2026-09-29 both builds of commit be0688ad had the SHA-256
b1c55ab01309b974f756798b9ff4109a401e024268796bc03d3f45625f301e56.
What this does not prove yet: that a build on another machine, with another copy of the JDK and the Android build tools, gives the same file. F-Droid's build server is the place where that is found out.
- The Gradle wrapper jar and the Gradle distribution are held to the hashes
Gradle publishes (
tools/check-wrapper.sh,distributionSha256Sum). - Every dependency and every build plugin is held to a SHA-256 in
gradle/verification-metadata.xml. A file with another hash stops the build. The hashes were taken from this machine's downloads on 2026-09-29, over HTTPS from Google's and Maven Central's servers. Signatures of the publishers are not checked. - The CI actions are named by commit, not by tag.
To change a dependency: change the version, run
./gradlew --write-verification-metadata sha256 :core:test :app:testDebugUnitTest :app:assembleDebug :app:assembleDebugAndroidTest :app:assembleRelease :app:lintDebug :core:lint,
and read the difference in gradle/verification-metadata.xml before committing
it. The entries for aapt2 on macOS and Windows are not written by a build on
Linux and have to be added by hand, as they were the first time.