Skip to content

Commit e9c3268

Browse files
authored
Merge pull request #42 from Xitee1/chore/fdroid-prep
chore(fdroid): prepare repo for F-Droid submission
2 parents 545cc40 + d39795e commit e9c3268

12 files changed

Lines changed: 206 additions & 9 deletions

File tree

.github/workflows/release.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ jobs:
3434
- name: Set up Gradle
3535
uses: gradle/actions/setup-gradle@v4
3636

37+
- name: Verify release metadata matches tag
38+
run: |
39+
VER="${GITHUB_REF_NAME#v}"
40+
IFS=. read -r MA MI PA <<< "${VER%%-*}" # strip any -suffix, matching versionCodeFrom
41+
CODE=$((MA * 100000 + MI * 1000 + PA * 10))
42+
grep -qx "versionName=$VER" app/version.properties
43+
grep -qx "versionCode=$CODE" app/version.properties
44+
for LOCALE in en-US de-DE; do
45+
FILE="fastlane/metadata/android/$LOCALE/changelogs/$CODE.txt"
46+
test -f "$FILE" || { echo "Missing F-Droid changelog: $FILE"; exit 1; }
47+
test "$(wc -c < "$FILE")" -le 500 || { echo "Changelog over 500 chars: $FILE"; exit 1; }
48+
done
49+
3750
- name: Decode release keystore
3851
if: ${{ env.SIGNING_KEYSTORE_BASE64 != '' }}
3952
env:

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ Toolchain: JDK 17, Android SDK platform 36 (`compileSdk`/`targetSdk`), `minSdk =
1414

1515
Versioning is dynamic via the [axion-release](https://github.com/allegro/axion-release-plugin) plugin (applied at the root). `versionName` is derived from the latest `v*` git tag (`project.version`); `versionCode` is computed from that same tag version with the schema `major*100_000 + minor*1_000 + patch*10` (tag `v1.0.1``100010`), keeping it monotonic with releases published before dynamic versioning. Tags must be plain SemVer after the `v` prefix — axion-release fails the build on anything else (e.g. `v1.0.1.1`), so a hotfix is just the next patch tag. Don't hand-edit either field in `app/build.gradle.kts`. To cut a release, push a new `v<x.y.z>` tag — `.github/workflows/release.yml` builds a signed APK and publishes the GitHub Release. CI checkouts must use `fetch-depth: 0` so tags and full history are visible.
1616

17+
**Release checklist (do this in the commit you will tag, before pushing the tag):**
18+
19+
1. Bump `app/version.properties` to the new `versionName`/`versionCode`. This file is **not** read by Gradle — axion-release still derives the real build values from the tag — it exists only so F-Droid's `checkupdates` can read the version statically (see `UpdateCheckData` in the fdroiddata recipe). It must match the tag or the release build fails.
20+
2. Hand-write the changelog for the new `versionCode` in **both** locales: `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` and `.../de-DE/changelogs/<versionCode>.txt`, each ≤500 chars, user-facing tone. F-Droid displays only the current version's file and reads it from the tagged tree.
21+
22+
The `release.yml` workflow enforces both (a `Verify release metadata matches tag` step) and fails the tag build if `version.properties` disagrees with the tag or a changelog is missing/oversized. If it fails, fix on `main`, then delete and re-push the tag. F-Droid builds the app itself from source (unsigned via `-PdisableSigning`); the GitHub release APK is unaffected.
23+
1724
## Module architecture
1825

1926
Four Gradle modules with a strict one-way dependency flow:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Signed APKs are published on the [Releases page](https://github.com/Xitee1/sleep
7373
Requirements:
7474

7575
- JDK 17
76-
- Android SDK with platform 35 (`compileSdk`)
76+
- Android SDK with platform 36 (`compileSdk`)
7777
- `minSdk` is 26 (Android 8.0)
7878

7979
```sh

app/build.gradle.kts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ android {
4949
getDefaultProguardFile("proguard-android-optimize.txt"),
5050
"proguard-rules.pro",
5151
)
52-
// Use release signing when SIGNING_KEYSTORE_PATH env var is provided (CI with secrets);
53-
// otherwise fall back to debug signing so local/CI builds without secrets still succeed.
54-
signingConfig = if (!System.getenv("SIGNING_KEYSTORE_PATH").isNullOrBlank()) {
55-
signingConfigs.getByName("release")
56-
} else {
57-
signingConfigs.getByName("debug")
52+
// F-Droid builds pass -PdisableSigning to emit an unsigned release APK it signs itself.
53+
// Otherwise use release signing when SIGNING_KEYSTORE_PATH is provided (CI with secrets),
54+
// and fall back to debug signing so local/CI builds without secrets still succeed.
55+
signingConfig = when {
56+
project.hasProperty("disableSigning") -> null
57+
!System.getenv("SIGNING_KEYSTORE_PATH").isNullOrBlank() ->
58+
signingConfigs.getByName("release")
59+
else -> signingConfigs.getByName("debug")
5860
}
5961
}
6062
}
@@ -64,6 +66,16 @@ android {
6466
targetCompatibility = JavaVersion.VERSION_17
6567
}
6668

69+
packaging {
70+
jniLibs {
71+
// Package the DataStore native libs as shipped in the dependency instead of
72+
// letting AGP strip them — stripping is host-dependent (build machine's NDK),
73+
// which breaks F-Droid's reproducible-build match against the release APK.
74+
// The unstripped bytes come straight from the AAR, so every build agrees.
75+
keepDebugSymbols += "**/*.so"
76+
}
77+
}
78+
6779
buildFeatures {
6880
compose = true
6981
}

app/version.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Read by F-Droid checkupdates (UpdateCheckData in fdroiddata's metadata/dev.xitee.sleeptimer.yml).
2+
# NOT used by the Gradle build — axion-release derives the real values from the git tag.
3+
# Bump in the release commit so these values match the tag about to be created.
4+
versionName=1.1.1
5+
versionCode=101010
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# F-Droid submission
2+
3+
Reference notes for publishing SleepTimer (`dev.xitee.sleeptimer`) on F-Droid.
4+
Verified against live F-Droid docs and precedent recipes on 2026-07-06.
5+
6+
## What lives where
7+
8+
- **This repo** carries everything F-Droid reads from source: `LICENSE` (GPL-3.0),
9+
`fastlane/metadata/android/{en-US,de-DE}/` (title, descriptions, icon, screenshots,
10+
per-`versionCode` changelogs), `app/version.properties` (version literals for the
11+
update checker), and the `-PdisableSigning` build path in `app/build.gradle.kts`.
12+
- **The fdroiddata fork** carries the build recipe below. It is **not** committed to
13+
this repo — F-Droid lint rejects a recipe that duplicates the fastlane
14+
`Summary`/`Description`, and the recipe references paths inside this repo by URL.
15+
16+
## The recipe — `metadata/dev.xitee.sleeptimer.yml`
17+
18+
Add this file in a fork of https://gitlab.com/fdroid/fdroiddata (not here):
19+
20+
```yaml
21+
Categories:
22+
- Timer
23+
- Multimedia
24+
License: GPL-3.0-or-later
25+
AuthorName: Xitee
26+
SourceCode: https://github.com/Xitee1/sleep-timer
27+
IssueTracker: https://github.com/Xitee1/sleep-timer/issues
28+
Changelog: https://github.com/Xitee1/sleep-timer/releases
29+
Donate: https://ko-fi.com/xitee165479
30+
31+
AutoName: SleepTimer
32+
33+
RepoType: git
34+
Repo: https://github.com/Xitee1/sleep-timer.git
35+
36+
Builds:
37+
- versionName: 1.1.1
38+
versionCode: 101010
39+
commit: v1.1.1
40+
subdir: app
41+
gradle:
42+
- yes
43+
gradleprops:
44+
- disableSigning=true
45+
binary: https://github.com/Xitee1/sleep-timer/releases/download/v%v/SleepTimer-v%v.apk
46+
47+
AllowedAPKSigningKeys: 87e5fe65c58d5b8406d239d68e7a9d6d7f40245ee6c3a1f458e67094b0a67fb0
48+
49+
AutoUpdateMode: Version
50+
UpdateCheckMode: Tags ^v\d+\.\d+\.\d+$
51+
UpdateCheckData: app/version.properties|versionCode=(\d+)|.|versionName=(.+)
52+
CurrentVersion: 1.1.1
53+
CurrentVersionCode: 101010
54+
```
55+
56+
This is the **reproducible-builds (your-key)** form: `binary:` points F-Droid at your
57+
signed GitHub release APK as the reference, and `AllowedAPKSigningKeys` is the SHA-256 of
58+
your release signing certificate (`CN=Xitee`, extracted from the v1.1.0 release APK — valid
59+
as long as v1.1.1 is signed with the same keystore). F-Droid rebuilds from source, confirms
60+
its build is byte-identical to your APK, and ships your APK's signature. **Only submit this
61+
form if the probe below passes for v1.1.1.** If it does not, drop `binary:` and
62+
`AllowedAPKSigningKeys:` and F-Droid signs with its own key (a one-way door — you cannot
63+
switch to your signature later).
64+
65+
Notes:
66+
- No `Summary:`/`Description:` — the fastlane metadata in this repo supplies them.
67+
- No `AntiFeatures:` — Shizuku is an *optional* Apache-2.0 dependency from Maven Central
68+
(no `NonFreeDep`; precedent: the Hail recipe, which also has optional Shizuku), and the
69+
app has no `INTERNET` permission and no trackers.
70+
- `UpdateCheckData` reads `app/version.properties` because the Gradle build derives
71+
`versionCode`/`versionName` dynamically from the git tag (axion-release), which the
72+
static update checker cannot parse. The `.` reuses the same file for the versionName
73+
regex. The `Tags` regex excludes the old `v0.0.1-beta` tag.
74+
- After the first release, the checkupdates bot appends new build entries automatically
75+
(copying `gradleprops`), so future releases need no recipe edits.
76+
77+
## Reproducible-builds probe — preliminary result (already run against v1.1.0)
78+
79+
Reproducible builds are a one-way door: if the F-Droid build is bit-identical to the
80+
signed GitHub release APK, F-Droid ships *your* signature (users cross-update between
81+
GitHub and F-Droid); if not enabled at inclusion time, F-Droid signs with its own key and
82+
you cannot switch later.
83+
84+
**A preliminary probe was run on 2026-07-06 against the existing v1.1.0 release** (build the
85+
`v1.1.0` tag from a clean clone, unsigned, and compare its zip entries by CRC against
86+
`SleepTimer-v1.1.0.apk`). Result: **121 of 122 entries were already byte-identical** —
87+
including all DEX, resources, and `resources.arsc`. The only mismatch was
88+
`libdatastore_shared_counter.so` (the AndroidX DataStore native lib, 4 ABIs): GitHub Actions
89+
**strips** it (host-dependent, via the runner's NDK), while a plain build leaves it as the
90+
pristine bytes shipped in the `datastore-core` AAR. The unstripped bytes are fixed by the
91+
dependency version, so they are identical on every machine.
92+
93+
**Fix applied in this repo** (`app/build.gradle.kts`): a `packaging { jniLibs {
94+
keepDebugSymbols += "**/*.so" } }` block, which tells AGP not to strip native libs, so every
95+
environment (GitHub Actions, F-Droid, local) packages the pristine AAR bytes. Verified: with
96+
the block, the four `.so` CRCs equal the pristine AAR CRCs. This **must ship in the v1.1.1
97+
tag** so the reference GitHub APK is also unstripped — with it in place, all 122 entries
98+
match and the build is reproducible.
99+
100+
**Confirm on v1.1.1 before submitting** (definitive check, once v1.1.1 is tagged & released):
101+
102+
```sh
103+
# at the v1.1.1 tag, JDK 17, ANDROID_HOME set:
104+
./gradlew :app:assembleRelease -PdisableSigning
105+
export PATH="$ANDROID_HOME/build-tools/<ver>:$PATH" # for apksigner
106+
pip install apksigcopier
107+
apksigcopier compare SleepTimer-v1.1.1.apk \
108+
--unsigned app/build/outputs/apk/release/app-release-unsigned.apk
109+
# apksigcopier 1.1.1 may error parsing the signing block; the robust fallback is a
110+
# per-entry CRC diff of `unzip -v` on both APKs (ignoring META-INF signature files).
111+
```
112+
113+
The definitive check is F-Droid's own CI on the submission MR (or a local
114+
`fdroid build` in a fdroiddata checkout), which builds in F-Droid's controlled environment.
115+
Given the preliminary result (only the now-fixed `.so` differed), it is very likely to pass.
116+
If it unexpectedly fails, drop `binary:` + `AllowedAPKSigningKeys:` from the recipe and write
117+
"No, I don't want this." in the MR reproducibility question.
118+
119+
## Local validation
120+
121+
```sh
122+
python3 -m venv ~/venvs/fdroid && ~/venvs/fdroid/bin/pip install fdroidserver
123+
git clone https://gitlab.com/<your-gitlab-user>/fdroiddata.git && cd fdroiddata
124+
git switch -c dev.xitee.sleeptimer # branch name = applicationId (convention)
125+
# add metadata/dev.xitee.sleeptimer.yml, then:
126+
fdroid readmeta && fdroid rewritemeta dev.xitee.sleeptimer
127+
fdroid lint dev.xitee.sleeptimer # must be clean
128+
fdroid checkupdates dev.xitee.sleeptimer # only after v1.1.1 is tagged
129+
fdroid build -v -l dev.xitee.sleeptimer # optional locally (needs ANDROID_HOME); fork CI runs it too
130+
```
131+
132+
## Submission MR
133+
134+
1. Fork fdroiddata (public fork, unprotected branch `dev.xitee.sleeptimer`), push.
135+
2. Wait for the fork's GitLab pipelines to go green (they run lint + a full `fdroid build`).
136+
3. Open an MR against fdroiddata `master` with the **App inclusion** template, title
137+
`New app: SleepTimer`, squash enabled.
138+
4. In the description: state you are the upstream author (RFP issue not needed), that there
139+
are no anti-features (optional FLOSS Shizuku, no `INTERNET` permission, no trackers), and
140+
your reproducible-builds decision.
141+
142+
Review typically takes days to a few weeks; after merge the app appears on f-droid.org
143+
within about a week.
144+
145+
## After publication
146+
147+
- Replace the `_Submission pending._` line in `README.md` with the F-Droid badge linking
148+
`https://f-droid.org/packages/dev.xitee.sleeptimer`.
149+
- Optional: add `fastlane/metadata/android/en-US/images/featureGraphic.png` (1024×500) for
150+
a nicer "Latest" listing card.
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
* Neu: Fehlende Berechtigungen für die Bildschirm-aus-Funktion werden beim App-Start abgefragt, mit Wiederherstellungs-Dialog beim Verwerfen.
2-
* Behoben: Shizuku-Startup-Race, das Shizuku kurz als „nicht laufend“ melden konnte.
32
* Politur: Material-3-Stil für Dialoge, klarere Titel beim Geräteadministrator-Dialog.
43
* Intern: Upgrade auf AGP 9, Kotlin 2.3, Compose BOM 2026.03.01.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
* Neu: Neues App-Symbol im Nachthimmel-Design mit Monduhr.
2+
* Neu: Einstellung für den Bildschirm-Rotationsmodus (automatisch, Hoch- oder Querformat).
3+
* Behoben: Korrektheit der Timer-Laufzeit und Darstellungsprobleme.
4+
* Behoben: Shizuku-Startup-Race, das Shizuku kurz als „nicht laufend“ melden konnte.
5+
* Intern: Shizuku-Shell-Zugriff auf einen gebundenen UserService umgestellt.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Intern: Vorbereitung für F-Droid – Versions-Metadatendatei, Unterstützung für reproduzierbare unsignierte Builds und Änderungsprotokolle pro Version. Keine sichtbaren Änderungen.
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
* New: missing permissions for the screen-off feature are now prompted on app startup, with a recovery dialog if the prompt is dismissed.
2-
* Fixed: Shizuku startup race that could briefly report Shizuku as not running.
32
* Polish: Material 3 dialog styling, clearer device-admin prompt titles.
43
* Internal: upgraded to AGP 9, Kotlin 2.3, Compose BOM 2026.03.01.

0 commit comments

Comments
 (0)