mirror of
https://github.com/chmouel/liseur.git
synced 2026-10-16 12:50:22 +00:00
feat(release): publish to the Google Play internal track (#36)
Liseur ships as an APK on GitHub releases and through F-Droid. Play is a third channel, and the point of this change is that it is the least load-bearing of the three: the tag that publishes a release now also builds an app bundle and pushes it to internal testing, and every part of that is arranged so it cannot take the other two down with it. Nothing about the APK path moves. F-Droid's recipe builds assembleRelease and never sees fastlane/Fastfile, no Gradle publishing plugin is applied, and bundleRelease is an additional output rather than a replacement. The workflow step skips itself when the service account secret is absent, so forks still cut releases, and is continue-on-error so a rejected upload is not what fails one. Only the changelog is pushed from here. The store listing stays in the console, because Play holds declarations that no file in this repository describes -- data safety, content rating, target audience -- and an accidental metadata push is a listing change under review. Play also requires a privacy policy at a public URL, so docs/PRIVACY.md says what the app actually does: no collection, no trackers, network only to the book server you enter and to a dictionary lookup that is off until you turn it on. GitHub Pages serves it from main /docs, which makes it a document that changes in a commit rather than in a console text box. The upload credential is the existing account-level service account, already in pass; hack/release --sync-secrets carries it into the release environment alongside the signing key.
This commit is contained in:
parent
744fca3594
commit
36050777f9
10 changed files with 273 additions and 6 deletions
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
|
|
@ -13,7 +13,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
release:
|
||||
name: Build signed APK
|
||||
name: Build and publish the release
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 30
|
||||
|
|
@ -106,3 +106,45 @@ jobs:
|
|||
--title "Liseur $RELEASE_TAG" \
|
||||
--notes-file "$RELEASE_NOTES" \
|
||||
"$RELEASE_APK"
|
||||
|
||||
# Everything below is Google Play, and only Google Play. It runs
|
||||
# after the release above on purpose: the GitHub artefact and the
|
||||
# F-Droid submission are the primary channels, so a rejected upload
|
||||
# here must never be what makes a release fail.
|
||||
- name: Check for the Play credential
|
||||
id: play
|
||||
env:
|
||||
SERVICE_ACCOUNT: ${{ secrets.LISEUR_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "$SERVICE_ACCOUNT" ]]; then
|
||||
printf 'enabled=true\n' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
printf 'enabled=false\n' >> "$GITHUB_OUTPUT"
|
||||
printf 'No Play service account configured; skipping.\n'
|
||||
fi
|
||||
|
||||
- name: Build the release bundle
|
||||
if: steps.play.outputs.enabled == 'true'
|
||||
run: ./gradlew bundleRelease --console=plain
|
||||
|
||||
- name: Set up Ruby
|
||||
if: steps.play.outputs.enabled == 'true'
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
with:
|
||||
ruby-version: "3.3"
|
||||
|
||||
- name: Upload to the Play internal track
|
||||
if: steps.play.outputs.enabled == 'true'
|
||||
continue-on-error: true
|
||||
env:
|
||||
SERVICE_ACCOUNT: ${{ secrets.LISEUR_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
key="$RUNNER_TEMP/play-service-account.json"
|
||||
# The trap covers the failure paths too: the credential must not
|
||||
# outlive this step whatever fastlane makes of it.
|
||||
trap 'rm -f "$key"' EXIT
|
||||
(umask 077 && printf '%s' "$SERVICE_ACCOUNT" > "$key")
|
||||
gem install fastlane -v 2.238.0 --no-document
|
||||
SUPPLY_JSON_KEY="$key" fastlane android internal
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -16,3 +16,9 @@ keystore.properties
|
|||
*.jks
|
||||
*.keystore
|
||||
*.p12
|
||||
|
||||
# fastlane writes these next to the configuration it is given.
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
fastlane/Preview.html
|
||||
fastlane/play-service-account.json
|
||||
|
|
|
|||
69
DEVELOPER.md
69
DEVELOPER.md
|
|
@ -15,9 +15,11 @@ Manager package list).
|
|||
```bash
|
||||
./gradlew assembleDebug # unsigned debug APK, installable as-is
|
||||
./gradlew assembleRelease # minified release APK
|
||||
./gradlew bundleRelease # minified release AAB, for Google Play only
|
||||
```
|
||||
|
||||
Output APKs land in `app/build/outputs/apk/{debug,release}/`.
|
||||
Output APKs land in `app/build/outputs/apk/{debug,release}/`, and the
|
||||
bundle in `app/build/outputs/bundle/release/`.
|
||||
|
||||
The debug build is signed with the standard Android debug key, so
|
||||
`app-debug.apk` can be installed directly with `adb install` or by
|
||||
|
|
@ -181,7 +183,8 @@ Pushing the tag is what starts `.github/workflows/release.yml`, which
|
|||
builds and signs the APK in the `release` GitHub environment and
|
||||
attaches it to the release. That environment must hold
|
||||
`LISEUR_KEYSTORE_BASE64`, `LISEUR_KEYSTORE_PASSWORD`,
|
||||
`LISEUR_KEY_ALIAS` and `LISEUR_KEY_PASSWORD`; `hack/release` creates the
|
||||
`LISEUR_KEY_ALIAS`, `LISEUR_KEY_PASSWORD` and
|
||||
`LISEUR_PLAY_SERVICE_ACCOUNT_JSON`; `hack/release` creates the
|
||||
environment and uploads whichever of them are missing straight from
|
||||
`pass`, so an unlocked password store is the only setup a fresh clone
|
||||
needs. To refresh them all, after rotating the key for instance:
|
||||
|
|
@ -193,6 +196,68 @@ hack/release --sync-secrets
|
|||
Signing a build on your own machine is a separate matter, covered under
|
||||
[Signing a release build](#signing-a-release-build-optional) above.
|
||||
|
||||
### Google Play
|
||||
|
||||
Play is the third channel, after the GitHub release and F-Droid, and it
|
||||
is deliberately the least load-bearing of the three. The same tag that
|
||||
publishes the release also builds an app bundle and pushes it to the
|
||||
**internal testing** track:
|
||||
|
||||
```bash
|
||||
make bundle # ./gradlew bundleRelease, for Play only
|
||||
```
|
||||
|
||||
Nothing about the APK path changes. F-Droid's recipe builds
|
||||
`assembleRelease` and never sees `fastlane/Fastfile`, no Gradle
|
||||
publishing plugin is applied, and the bundle is an extra output rather
|
||||
than a replacement — which is also why the Play step in
|
||||
`.github/workflows/release.yml` is `continue-on-error` and skips itself
|
||||
entirely when the service account secret is absent. A fork, or a rejected
|
||||
upload, must not be what makes a release fail.
|
||||
|
||||
The upload runs `fastlane android internal` with
|
||||
`SUPPLY_JSON_KEY` pointing at a service account credential written to
|
||||
`$RUNNER_TEMP` and removed by a trap in the same step. The credential is
|
||||
`android/google.play.service.serviceaccount` in `pass`, shared with the
|
||||
other apps on the account; it needs *Release to testing tracks* on Liseur
|
||||
under Users and permissions.
|
||||
|
||||
Only the changelog is pushed from the repository. The store listing is
|
||||
edited in the console, because Play holds declarations that no file here
|
||||
describes — data safety, content rating, target audience, app access, ads
|
||||
— and those have to be revisited whenever the app gains a permission,
|
||||
talks to something new, or changes what it stores. The privacy policy
|
||||
Play links to is `docs/PRIVACY.md`, served by GitHub Pages from `main`
|
||||
`/docs`; it is a published legal document, so change it in a commit and
|
||||
not in the console.
|
||||
|
||||
Promotion out of internal testing is a manual action in the console, on
|
||||
purpose. Nothing in this repository can put a build in front of the
|
||||
public.
|
||||
|
||||
**The signing key.** Play App Signing holds the same key as the GitHub
|
||||
release, enrolled from `pass` through Google's PEPK tool rather than
|
||||
generated by Google. That means a build installed from Play and a build
|
||||
downloaded from the release page carry the same signature, so a reader
|
||||
can move between them without uninstalling and losing their library. It
|
||||
also means the key cannot be swapped without a rotation request. F-Droid
|
||||
signs with its own key unless reproducible builds are configured, so that
|
||||
third artefact is unaffected either way.
|
||||
|
||||
Recreating the PEPK export, should it ever be needed:
|
||||
|
||||
```bash
|
||||
d=$(mktemp -d) && umask 077
|
||||
pass show android/liseur.keystore.p12 | base64 -d > "$d/release.p12"
|
||||
java -jar pepk.jar --keystore="$d/release.p12" \
|
||||
--alias="$(pass show android/liseur.keystore-alias)" \
|
||||
--output="$d/output.zip" --include-cert \
|
||||
--rsa-aes-encryption --encryption-key-path=encryption_public_key.pem
|
||||
```
|
||||
|
||||
`pepk.jar` and `encryption_public_key.pem` are downloaded from the
|
||||
console, and the temporary directory goes away afterwards.
|
||||
|
||||
### Release notes
|
||||
|
||||
Two things are written for every release, and they are not the same
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -12,12 +12,13 @@ PACKAGE := com.chmouel.liseur
|
|||
ACTIVITY := $(PACKAGE)/.MainActivity
|
||||
DEBUG_APK := app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
.PHONY: help build debug release test lint check clean emulator stop shutdown install run run-bg reset screenshots icon feature-graphic
|
||||
.PHONY: help build debug release bundle test lint check clean emulator stop shutdown install run run-bg reset screenshots icon feature-graphic
|
||||
|
||||
help:
|
||||
@printf '%s\n' \
|
||||
'make build Build the debug APK' \
|
||||
'make release Build the release APK' \
|
||||
'make bundle Build the release AAB for Google Play' \
|
||||
'make test Run JVM unit tests' \
|
||||
'make lint Run Android Lint' \
|
||||
'make check Run tests, lint, and debug build' \
|
||||
|
|
@ -41,6 +42,11 @@ build debug:
|
|||
release:
|
||||
$(GRADLE) assembleRelease
|
||||
|
||||
# Google Play only. F-Droid and the GitHub release both build the APK
|
||||
# above, so this target is additive and nothing else depends on it.
|
||||
bundle:
|
||||
$(GRADLE) bundleRelease
|
||||
|
||||
test:
|
||||
$(GRADLE) testDebugUnitTest
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ When you switch between devices, you have the choice to have your place travel w
|
|||
Everything runs privately and without distraction. There are no trackers, no
|
||||
analytics, no ads, and no attempts to sell you a monthly subscription for books
|
||||
you already own. Liseur only talks to the book servers and dictionary sources
|
||||
you choose to add.
|
||||
you choose to add. The [privacy policy](https://chmouel.github.io/liseur/PRIVACY)
|
||||
spells out exactly what that means.
|
||||
|
||||
## Install
|
||||
|
||||
|
|
|
|||
100
docs/PRIVACY.md
Normal file
100
docs/PRIVACY.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
title: Liseur Privacy Policy
|
||||
---
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
**App:** Liseur (`com.chmouel.liseur`)
|
||||
**Developer:** Chmouel Boudjnah
|
||||
**Last updated:** 19 August 2026
|
||||
|
||||
Liseur is an open-source ebook reader. It has no account, no advertising,
|
||||
no analytics and no trackers, and it collects nothing about you. Its
|
||||
source code is public at
|
||||
<https://github.com/chmouel/liseur>, so every claim on this page can be
|
||||
checked rather than taken on trust.
|
||||
|
||||
## What Liseur collects
|
||||
|
||||
Nothing. There is no server run by the developer, no crash reporting, no
|
||||
usage measurement, no advertising identifier, and no account to sign up
|
||||
for. No data leaves your device except to the servers described below,
|
||||
which are ones you choose and enter yourself.
|
||||
|
||||
## What Liseur stores on your device
|
||||
|
||||
All of this stays in the app's own private storage:
|
||||
|
||||
- Your library: the books you added, their metadata and their covers.
|
||||
- Where you are in each book, along with your reading history and the
|
||||
time you have spent reading.
|
||||
- Your highlights, notes and bookmarks.
|
||||
- Your reading preferences: theme, font, size, spacing, margins.
|
||||
- If you connect a book server, its address and the credentials or token
|
||||
it issued. Those are encrypted with a key held in the Android Keystore,
|
||||
which cannot be exported from the device.
|
||||
|
||||
Uninstalling the app removes all of it.
|
||||
|
||||
Liseur reads EPUB files from a folder you pick through Android's own
|
||||
document picker. It only ever sees the folder you granted, and it does
|
||||
not copy your books anywhere.
|
||||
|
||||
## Network access
|
||||
|
||||
Liseur talks to exactly two kinds of address, both of which are yours to
|
||||
choose:
|
||||
|
||||
**Your book server.** If, and only if, you connect one, Liseur talks to
|
||||
the calibre-web, Komga or liseur-sync server whose address you typed in.
|
||||
It sends the credentials that server asked for, downloads the books and
|
||||
covers you request, and exchanges your reading position so the same book
|
||||
resumes in the right place on another device. That server is operated by
|
||||
you or by whoever you chose to trust; this policy does not cover what it
|
||||
does with the data it receives.
|
||||
|
||||
**A dictionary site, if you enable it.** Looking a word up online is off
|
||||
by default. When you switch it on, Liseur sends the single selected word
|
||||
to the dictionary site configured in Settings — by default the public
|
||||
Wiktionary API — over HTTPS. No identifier, no book title and no account
|
||||
accompanies it. You may point this at any Wiktionary edition or mirror,
|
||||
or leave the feature off and hand words to an offline dictionary app
|
||||
installed on your device instead.
|
||||
|
||||
Liseur never contacts any other host. It requests the `INTERNET` and
|
||||
`ACCESS_NETWORK_STATE` permissions for the two purposes above and for
|
||||
nothing else.
|
||||
|
||||
## Android backup
|
||||
|
||||
Liseur takes part in Android's standard backup, so your library, reading
|
||||
positions, highlights, notes and settings can follow you to a new device.
|
||||
That backup is handled by Android and stored in your own Google account,
|
||||
under Google's terms, not the developer's — Liseur has no access to it.
|
||||
Downloaded book files and generated covers are deliberately excluded.
|
||||
Server credentials are included but arrive unreadable on a new device,
|
||||
because the key that encrypts them never leaves the old one; Liseur
|
||||
notices this and asks you to sign in again.
|
||||
|
||||
You can turn this off in your device's backup settings.
|
||||
|
||||
## Children
|
||||
|
||||
Liseur is not directed at children and collects no personal information
|
||||
from anyone, of any age.
|
||||
|
||||
## Sharing
|
||||
|
||||
Nothing is shared or sold, because nothing is collected. The developer
|
||||
receives no data from the app whatsoever.
|
||||
|
||||
## Changes
|
||||
|
||||
Any change to this policy will be published on this page with a new date
|
||||
above, and its history is visible in the repository.
|
||||
|
||||
## Contact
|
||||
|
||||
Questions, or a problem with this policy: open an issue at
|
||||
<https://github.com/chmouel/liseur/issues> or write to
|
||||
<chmouel@chmouel.com>.
|
||||
4
docs/_config.yml
Normal file
4
docs/_config.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
theme: jekyll-theme-minimal
|
||||
title: Liseur
|
||||
description: An open-source ebook reader for Android
|
||||
show_downloads: false
|
||||
2
fastlane/Appfile
Normal file
2
fastlane/Appfile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
json_key_file(ENV["SUPPLY_JSON_KEY"].to_s)
|
||||
package_name("com.chmouel.liseur")
|
||||
36
fastlane/Fastfile
Normal file
36
fastlane/Fastfile
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Google Play uploads only. Nothing here is part of building Liseur:
|
||||
# F-Droid and the GitHub release both build the APK with plain Gradle and
|
||||
# never see this directory.
|
||||
#
|
||||
# The metadata under fastlane/metadata/android is written for F-Droid and
|
||||
# happens to be supply's native layout, so Play reads the same files. Only
|
||||
# the changelog is pushed from here: the store listing itself is edited in
|
||||
# the console, because Play holds the declarations (data safety, content
|
||||
# rating, target audience) that no file in this repository describes, and
|
||||
# an accidental metadata push is a listing change under review.
|
||||
|
||||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Upload the release bundle to the Play internal testing track"
|
||||
lane :internal do
|
||||
bundle = "app/build/outputs/bundle/release/app-release.aab"
|
||||
UI.user_error!("no bundle at #{bundle} — run ./gradlew bundleRelease") unless File.exist?(bundle)
|
||||
|
||||
upload_to_play_store(
|
||||
track: "internal",
|
||||
aab: bundle,
|
||||
# A track upload is a release; the APK path belongs to GitHub.
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
skip_upload_changelogs: false,
|
||||
# The first upload of all is done by hand, because enrolling the
|
||||
# signing key happens inside the console's release wizard. Every
|
||||
# upload after that goes straight to the testers.
|
||||
release_status: "completed",
|
||||
validate_only: ENV["PLAY_VALIDATE_ONLY"] == "true"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,7 @@ readonly PASS_KEYSTORE="android/liseur.keystore.p12"
|
|||
readonly PASS_KEYSTORE_PASSWORD="android/liseur.keystore-password"
|
||||
readonly PASS_KEY_ALIAS="android/liseur.keystore-alias"
|
||||
readonly PASS_GEMINI_KEY="google/gemini-api"
|
||||
readonly PASS_PLAY_SERVICE_ACCOUNT="android/google.play.service.serviceaccount"
|
||||
readonly NOTES_MODEL="gemini-3.6-flash"
|
||||
readonly REQUIRED_SECRETS=(
|
||||
LISEUR_KEYSTORE_BASE64
|
||||
|
|
@ -25,6 +26,7 @@ readonly REQUIRED_SECRETS=(
|
|||
LISEUR_KEY_ALIAS
|
||||
LISEUR_KEY_PASSWORD
|
||||
GEMINI_API_KEY
|
||||
LISEUR_PLAY_SERVICE_ACCOUNT_JSON
|
||||
)
|
||||
|
||||
usage() {
|
||||
|
|
@ -51,7 +53,8 @@ release wants --no-fdroid. Once the inclusion request is merged, run
|
|||
hack/release --fdroid-only VERSION to catch that release up.
|
||||
|
||||
The signing key lives in pass, under android/liseur.keystore.p12,
|
||||
android/liseur.keystore-password and android/liseur.keystore-alias. Any
|
||||
android/liseur.keystore-password and android/liseur.keystore-alias, and the
|
||||
Play upload credential under android/google.play.service.serviceaccount. Any
|
||||
release secret missing from the GitHub release environment is uploaded
|
||||
from there, so a fresh clone needs nothing but an unlocked password store.
|
||||
EOF
|
||||
|
|
@ -132,6 +135,8 @@ sync_release_secrets() {
|
|||
set_release_secret LISEUR_KEY_PASSWORD \
|
||||
"$(pass_show "$PASS_KEYSTORE_PASSWORD")"
|
||||
set_release_secret GEMINI_API_KEY "$(pass_show "$PASS_GEMINI_KEY")"
|
||||
set_release_secret LISEUR_PLAY_SERVICE_ACCOUNT_JSON \
|
||||
"$(pass_show "$PASS_PLAY_SERVICE_ACCOUNT")"
|
||||
}
|
||||
|
||||
github_release_is_published() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue