Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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.

**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/<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.

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:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 18 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand All @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions app/version.properties
Original file line number Diff line number Diff line change
@@ -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
150 changes: 150 additions & 0 deletions docs/plans/2026-07-06-fdroid-submission.md
Original file line number Diff line number Diff line change
@@ -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/<ver>:$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/<your-gitlab-user>/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.
1 change: 0 additions & 1 deletion fastlane/metadata/android/de-DE/changelogs/100010.txt
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions fastlane/metadata/android/de-DE/changelogs/101000.txt
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions fastlane/metadata/android/de-DE/changelogs/101010.txt
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion fastlane/metadata/android/en-US/changelogs/100010.txt
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/101000.txt
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/101010.txt
Original file line number Diff line number Diff line change
@@ -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.
Loading