diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e2cc03..8c78ff2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,6 +34,19 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 + - name: Verify release metadata matches tag + run: | + VER="${GITHUB_REF_NAME#v}" + IFS=. read -r MA MI PA <<< "${VER%%-*}" # strip any -suffix, matching versionCodeFrom + CODE=$((MA * 100000 + MI * 1000 + PA * 10)) + grep -qx "versionName=$VER" app/version.properties + grep -qx "versionCode=$CODE" app/version.properties + for LOCALE in en-US de-DE; do + FILE="fastlane/metadata/android/$LOCALE/changelogs/$CODE.txt" + test -f "$FILE" || { echo "Missing F-Droid changelog: $FILE"; exit 1; } + test "$(wc -c < "$FILE")" -le 500 || { echo "Changelog over 500 chars: $FILE"; exit 1; } + done + - name: Decode release keystore if: ${{ env.SIGNING_KEYSTORE_BASE64 != '' }} env: diff --git a/AGENTS.md b/AGENTS.md index 74de201..3d30450 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,13 @@ Toolchain: JDK 17, Android SDK platform 36 (`compileSdk`/`targetSdk`), `minSdk = 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` 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. +**Release checklist (do this in the commit you will tag, before pushing the tag):** + +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. +2. Hand-write the changelog for the new `versionCode` in **both** locales: `fastlane/metadata/android/en-US/changelogs/.txt` and `.../de-DE/changelogs/.txt`, each ≤500 chars, user-facing tone. F-Droid displays only the current version's file and reads it from the tagged tree. + +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. + ## Module architecture Four Gradle modules with a strict one-way dependency flow: diff --git a/README.md b/README.md index 72b932e..3ad126e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Signed APKs are published on the [Releases page](https://github.com/Xitee1/sleep Requirements: - JDK 17 -- Android SDK with platform 35 (`compileSdk`) +- Android SDK with platform 36 (`compileSdk`) - `minSdk` is 26 (Android 8.0) ```sh diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31abde5..8d85e2e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,12 +49,14 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) - // Use release signing when SIGNING_KEYSTORE_PATH env var is provided (CI with secrets); - // otherwise fall back to debug signing so local/CI builds without secrets still succeed. - signingConfig = if (!System.getenv("SIGNING_KEYSTORE_PATH").isNullOrBlank()) { - signingConfigs.getByName("release") - } else { - signingConfigs.getByName("debug") + // F-Droid builds pass -PdisableSigning to emit an unsigned release APK it signs itself. + // Otherwise use release signing when SIGNING_KEYSTORE_PATH is provided (CI with secrets), + // and fall back to debug signing so local/CI builds without secrets still succeed. + signingConfig = when { + project.hasProperty("disableSigning") -> null + !System.getenv("SIGNING_KEYSTORE_PATH").isNullOrBlank() -> + signingConfigs.getByName("release") + else -> signingConfigs.getByName("debug") } } } @@ -64,6 +66,16 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + packaging { + jniLibs { + // Package the DataStore native libs as shipped in the dependency instead of + // letting AGP strip them — stripping is host-dependent (build machine's NDK), + // which breaks F-Droid's reproducible-build match against the release APK. + // The unstripped bytes come straight from the AAR, so every build agrees. + keepDebugSymbols += "**/*.so" + } + } + buildFeatures { compose = true } diff --git a/app/version.properties b/app/version.properties new file mode 100644 index 0000000..2a270ea --- /dev/null +++ b/app/version.properties @@ -0,0 +1,5 @@ +# Read by F-Droid checkupdates (UpdateCheckData in fdroiddata's metadata/dev.xitee.sleeptimer.yml). +# NOT used by the Gradle build — axion-release derives the real values from the git tag. +# Bump in the release commit so these values match the tag about to be created. +versionName=1.1.1 +versionCode=101010 diff --git a/docs/plans/2026-07-06-fdroid-submission.md b/docs/plans/2026-07-06-fdroid-submission.md new file mode 100644 index 0000000..48af90b --- /dev/null +++ b/docs/plans/2026-07-06-fdroid-submission.md @@ -0,0 +1,150 @@ +# F-Droid submission + +Reference notes for publishing SleepTimer (`dev.xitee.sleeptimer`) on F-Droid. +Verified against live F-Droid docs and precedent recipes on 2026-07-06. + +## What lives where + +- **This repo** carries everything F-Droid reads from source: `LICENSE` (GPL-3.0), + `fastlane/metadata/android/{en-US,de-DE}/` (title, descriptions, icon, screenshots, + per-`versionCode` changelogs), `app/version.properties` (version literals for the + update checker), and the `-PdisableSigning` build path in `app/build.gradle.kts`. +- **The fdroiddata fork** carries the build recipe below. It is **not** committed to + this repo — F-Droid lint rejects a recipe that duplicates the fastlane + `Summary`/`Description`, and the recipe references paths inside this repo by URL. + +## The recipe — `metadata/dev.xitee.sleeptimer.yml` + +Add this file in a fork of https://gitlab.com/fdroid/fdroiddata (not here): + +```yaml +Categories: + - Timer + - Multimedia +License: GPL-3.0-or-later +AuthorName: Xitee +SourceCode: https://github.com/Xitee1/sleep-timer +IssueTracker: https://github.com/Xitee1/sleep-timer/issues +Changelog: https://github.com/Xitee1/sleep-timer/releases +Donate: https://ko-fi.com/xitee165479 + +AutoName: SleepTimer + +RepoType: git +Repo: https://github.com/Xitee1/sleep-timer.git + +Builds: + - versionName: 1.1.1 + versionCode: 101010 + commit: v1.1.1 + subdir: app + gradle: + - yes + gradleprops: + - disableSigning=true + binary: https://github.com/Xitee1/sleep-timer/releases/download/v%v/SleepTimer-v%v.apk + +AllowedAPKSigningKeys: 87e5fe65c58d5b8406d239d68e7a9d6d7f40245ee6c3a1f458e67094b0a67fb0 + +AutoUpdateMode: Version +UpdateCheckMode: Tags ^v\d+\.\d+\.\d+$ +UpdateCheckData: app/version.properties|versionCode=(\d+)|.|versionName=(.+) +CurrentVersion: 1.1.1 +CurrentVersionCode: 101010 +``` + +This is the **reproducible-builds (your-key)** form: `binary:` points F-Droid at your +signed GitHub release APK as the reference, and `AllowedAPKSigningKeys` is the SHA-256 of +your release signing certificate (`CN=Xitee`, extracted from the v1.1.0 release APK — valid +as long as v1.1.1 is signed with the same keystore). F-Droid rebuilds from source, confirms +its build is byte-identical to your APK, and ships your APK's signature. **Only submit this +form if the probe below passes for v1.1.1.** If it does not, drop `binary:` and +`AllowedAPKSigningKeys:` and F-Droid signs with its own key (a one-way door — you cannot +switch to your signature later). + +Notes: +- No `Summary:`/`Description:` — the fastlane metadata in this repo supplies them. +- No `AntiFeatures:` — Shizuku is an *optional* Apache-2.0 dependency from Maven Central + (no `NonFreeDep`; precedent: the Hail recipe, which also has optional Shizuku), and the + app has no `INTERNET` permission and no trackers. +- `UpdateCheckData` reads `app/version.properties` because the Gradle build derives + `versionCode`/`versionName` dynamically from the git tag (axion-release), which the + static update checker cannot parse. The `.` reuses the same file for the versionName + regex. The `Tags` regex excludes the old `v0.0.1-beta` tag. +- After the first release, the checkupdates bot appends new build entries automatically + (copying `gradleprops`), so future releases need no recipe edits. + +## Reproducible-builds probe — preliminary result (already run against v1.1.0) + +Reproducible builds are a one-way door: if the F-Droid build is bit-identical to the +signed GitHub release APK, F-Droid ships *your* signature (users cross-update between +GitHub and F-Droid); if not enabled at inclusion time, F-Droid signs with its own key and +you cannot switch later. + +**A preliminary probe was run on 2026-07-06 against the existing v1.1.0 release** (build the +`v1.1.0` tag from a clean clone, unsigned, and compare its zip entries by CRC against +`SleepTimer-v1.1.0.apk`). Result: **121 of 122 entries were already byte-identical** — +including all DEX, resources, and `resources.arsc`. The only mismatch was +`libdatastore_shared_counter.so` (the AndroidX DataStore native lib, 4 ABIs): GitHub Actions +**strips** it (host-dependent, via the runner's NDK), while a plain build leaves it as the +pristine bytes shipped in the `datastore-core` AAR. The unstripped bytes are fixed by the +dependency version, so they are identical on every machine. + +**Fix applied in this repo** (`app/build.gradle.kts`): a `packaging { jniLibs { +keepDebugSymbols += "**/*.so" } }` block, which tells AGP not to strip native libs, so every +environment (GitHub Actions, F-Droid, local) packages the pristine AAR bytes. Verified: with +the block, the four `.so` CRCs equal the pristine AAR CRCs. This **must ship in the v1.1.1 +tag** so the reference GitHub APK is also unstripped — with it in place, all 122 entries +match and the build is reproducible. + +**Confirm on v1.1.1 before submitting** (definitive check, once v1.1.1 is tagged & released): + +```sh +# at the v1.1.1 tag, JDK 17, ANDROID_HOME set: +./gradlew :app:assembleRelease -PdisableSigning +export PATH="$ANDROID_HOME/build-tools/:$PATH" # for apksigner +pip install apksigcopier +apksigcopier compare SleepTimer-v1.1.1.apk \ + --unsigned app/build/outputs/apk/release/app-release-unsigned.apk +# apksigcopier 1.1.1 may error parsing the signing block; the robust fallback is a +# per-entry CRC diff of `unzip -v` on both APKs (ignoring META-INF signature files). +``` + +The definitive check is F-Droid's own CI on the submission MR (or a local +`fdroid build` in a fdroiddata checkout), which builds in F-Droid's controlled environment. +Given the preliminary result (only the now-fixed `.so` differed), it is very likely to pass. +If it unexpectedly fails, drop `binary:` + `AllowedAPKSigningKeys:` from the recipe and write +"No, I don't want this." in the MR reproducibility question. + +## Local validation + +```sh +python3 -m venv ~/venvs/fdroid && ~/venvs/fdroid/bin/pip install fdroidserver +git clone https://gitlab.com//fdroiddata.git && cd fdroiddata +git switch -c dev.xitee.sleeptimer # branch name = applicationId (convention) +# add metadata/dev.xitee.sleeptimer.yml, then: +fdroid readmeta && fdroid rewritemeta dev.xitee.sleeptimer +fdroid lint dev.xitee.sleeptimer # must be clean +fdroid checkupdates dev.xitee.sleeptimer # only after v1.1.1 is tagged +fdroid build -v -l dev.xitee.sleeptimer # optional locally (needs ANDROID_HOME); fork CI runs it too +``` + +## Submission MR + +1. Fork fdroiddata (public fork, unprotected branch `dev.xitee.sleeptimer`), push. +2. Wait for the fork's GitLab pipelines to go green (they run lint + a full `fdroid build`). +3. Open an MR against fdroiddata `master` with the **App inclusion** template, title + `New app: SleepTimer`, squash enabled. +4. In the description: state you are the upstream author (RFP issue not needed), that there + are no anti-features (optional FLOSS Shizuku, no `INTERNET` permission, no trackers), and + your reproducible-builds decision. + +Review typically takes days to a few weeks; after merge the app appears on f-droid.org +within about a week. + +## After publication + +- Replace the `_Submission pending._` line in `README.md` with the F-Droid badge linking + `https://f-droid.org/packages/dev.xitee.sleeptimer`. +- Optional: add `fastlane/metadata/android/en-US/images/featureGraphic.png` (1024×500) for + a nicer "Latest" listing card. diff --git a/fastlane/metadata/android/de-DE/changelogs/100010.txt b/fastlane/metadata/android/de-DE/changelogs/100010.txt index 43c15ef..f20fa6d 100644 --- a/fastlane/metadata/android/de-DE/changelogs/100010.txt +++ b/fastlane/metadata/android/de-DE/changelogs/100010.txt @@ -1,4 +1,3 @@ * Neu: Fehlende Berechtigungen für die Bildschirm-aus-Funktion werden beim App-Start abgefragt, mit Wiederherstellungs-Dialog beim Verwerfen. -* Behoben: Shizuku-Startup-Race, das Shizuku kurz als „nicht laufend“ melden konnte. * Politur: Material-3-Stil für Dialoge, klarere Titel beim Geräteadministrator-Dialog. * Intern: Upgrade auf AGP 9, Kotlin 2.3, Compose BOM 2026.03.01. diff --git a/fastlane/metadata/android/de-DE/changelogs/101000.txt b/fastlane/metadata/android/de-DE/changelogs/101000.txt new file mode 100644 index 0000000..85a5bbe --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/101000.txt @@ -0,0 +1,5 @@ +* Neu: Neues App-Symbol im Nachthimmel-Design mit Monduhr. +* Neu: Einstellung für den Bildschirm-Rotationsmodus (automatisch, Hoch- oder Querformat). +* Behoben: Korrektheit der Timer-Laufzeit und Darstellungsprobleme. +* Behoben: Shizuku-Startup-Race, das Shizuku kurz als „nicht laufend“ melden konnte. +* Intern: Shizuku-Shell-Zugriff auf einen gebundenen UserService umgestellt. diff --git a/fastlane/metadata/android/de-DE/changelogs/101010.txt b/fastlane/metadata/android/de-DE/changelogs/101010.txt new file mode 100644 index 0000000..9ee11e9 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/101010.txt @@ -0,0 +1 @@ +* Intern: Vorbereitung für F-Droid – Versions-Metadatendatei, Unterstützung für reproduzierbare unsignierte Builds und Änderungsprotokolle pro Version. Keine sichtbaren Änderungen. diff --git a/fastlane/metadata/android/en-US/changelogs/100010.txt b/fastlane/metadata/android/en-US/changelogs/100010.txt index 7f541a9..91e8d3b 100644 --- a/fastlane/metadata/android/en-US/changelogs/100010.txt +++ b/fastlane/metadata/android/en-US/changelogs/100010.txt @@ -1,4 +1,3 @@ * New: missing permissions for the screen-off feature are now prompted on app startup, with a recovery dialog if the prompt is dismissed. -* Fixed: Shizuku startup race that could briefly report Shizuku as not running. * Polish: Material 3 dialog styling, clearer device-admin prompt titles. * Internal: upgraded to AGP 9, Kotlin 2.3, Compose BOM 2026.03.01. diff --git a/fastlane/metadata/android/en-US/changelogs/101000.txt b/fastlane/metadata/android/en-US/changelogs/101000.txt new file mode 100644 index 0000000..fe1ed46 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/101000.txt @@ -0,0 +1,5 @@ +* New: night-sky moon-clock launcher icon. +* New: screen rotation mode setting (auto, portrait, or landscape). +* Fixed: timer runtime correctness and theming issues. +* Fixed: Shizuku startup race that could briefly report Shizuku as not running. +* Internal: migrated Shizuku shell access to a bound UserService. diff --git a/fastlane/metadata/android/en-US/changelogs/101010.txt b/fastlane/metadata/android/en-US/changelogs/101010.txt new file mode 100644 index 0000000..ce0a719 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/101010.txt @@ -0,0 +1 @@ +* Internal: prepared the app for F-Droid — added a version metadata file, support for reproducible unsigned builds, and per-release changelogs. No user-facing changes.