From 8e0f798721fb0255cee42be05339a39f27a691af Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Wed, 9 Sep 2026 17:17:33 +0200 Subject: [PATCH 1/4] ci: build signed Play bundles from main --- .github/workflows/play-bundle.yml | 61 +++++++++++++++++++++++++++++++ docs/PLAY-RELEASES.md | 13 +++++++ 2 files changed, 74 insertions(+) create mode 100644 .github/workflows/play-bundle.yml create mode 100644 docs/PLAY-RELEASES.md diff --git a/.github/workflows/play-bundle.yml b/.github/workflows/play-bundle.yml new file mode 100644 index 0000000..0bff850 --- /dev/null +++ b/.github/workflows/play-bundle.yml @@ -0,0 +1,61 @@ +name: Play release bundle +on: + push: + branches: [main] + paths: ['app/**', 'i18n/**', 'gradle/**', '*.gradle.kts', 'gradle.properties', '.github/workflows/play-bundle.yml'] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: play-bundle + cancel-in-progress: false +jobs: + bundle: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: play-internal + timeout-minutes: 30 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 + with: + distribution: temurin + java-version: '21' + - name: Release tests and lint + run: ./gradlew testReleaseUnitTest lintRelease --no-daemon --no-configuration-cache + - name: Sign release bundle + env: + KEY_BASE64: ${{ secrets.PINAKES_KEYSTORE_BASE64 }} + PINAKES_KEYSTORE_PASSWORD: ${{ secrets.PINAKES_KEYSTORE_PASSWORD }} + PINAKES_KEY_ALIAS: ${{ secrets.PINAKES_KEY_ALIAS }} + PINAKES_KEY_PASSWORD: ${{ secrets.PINAKES_KEY_PASSWORD }} + run: | + set -euo pipefail + umask 077 + test -n "$KEY_BASE64" + test -n "$PINAKES_KEYSTORE_PASSWORD" + test -n "$PINAKES_KEY_ALIAS" + test -n "$PINAKES_KEY_PASSWORD" + export PINAKES_KEYSTORE="$RUNNER_TEMP/pinakes-upload.jks" + trap 'rm -f "$PINAKES_KEYSTORE"' EXIT + printf '%s' "$KEY_BASE64" | base64 --decode > "$PINAKES_KEYSTORE" + ./gradlew bundleRelease --no-daemon --no-configuration-cache + jarsigner -verify app/build/outputs/bundle/release/app-release.aab + sha256sum app/build/outputs/bundle/release/app-release.aab > app/build/outputs/bundle/release/SHA256SUMS.txt + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: pinakes-play-${{ github.sha }} + path: | + app/build/outputs/bundle/release/*.aab + app/build/outputs/bundle/release/SHA256SUMS.txt + app/build/outputs/mapping/release/mapping.txt + if-no-files-found: error + retention-days: 30 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: play-reports-${{ github.sha }} + path: app/build/reports/ + retention-days: 14 diff --git a/docs/PLAY-RELEASES.md b/docs/PLAY-RELEASES.md new file mode 100644 index 0000000..6d12ee0 --- /dev/null +++ b/docs/PLAY-RELEASES.md @@ -0,0 +1,13 @@ +# Play releases + +GitHub is the source of truth. Pull requests run Android CI. After merging Android changes into main, `Play release bundle` runs release unit tests and lint, then signs an AAB and retains the bundle, checksum, R8 mapping and reports as GitHub artifacts. Manual dispatch is supported on main only. No PR code receives signing secrets. + +Environment `play-internal` must allow main only. Required environment secrets: `PINAKES_KEYSTORE_BASE64`, `PINAKES_KEYSTORE_PASSWORD`, `PINAKES_KEY_ALIAS`, `PINAKES_KEY_PASSWORD`. Use the existing release key; never generate a replacement silently. Keys are materialized in runner temporary storage and removed after signing; configuration caching is disabled. + +This workflow builds bundles; it does NOT upload to Play or publish to production. Publisher API credentials and app-scoped Play permissions are still required for that separate stage. Increment versionCode before each new Play upload, including corrective rebuilds after a code has been accepted. Debug GitHub releases keep their existing path and are not Play artifacts. + +Review library: https://biblioteca.fabiodalez.it. Mobile API was enabled on 2026-09-09. A dedicated standard reader was created; credentials are private outside Git, not in this document or release artifacts. + +Remaining release gates: accurate privacy policy including Sentry, account-deletion path and public request URL, reviewer access test, minified-device smoke test, native 16 KB compatibility and Play questionnaires. The personal developer account requires the closed-test period before production access. Do not interpret a green bundle build as policy approval. + +The older PLAY_STORE_COMPLIANCE.md audit is historical: its no-third-party-SDK and disabled-R8 statements no longer describe the app. Sentry is present, R8 is enabled, and logout does not delete the user's server account. From 8a7fa3c8b240572e099a4717195a776f0480c37a Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Wed, 9 Sep 2026 17:18:42 +0200 Subject: [PATCH 2/4] docs: correct privacy claims for diagnostics and account deletion --- PRIVACY.md | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 99a00b6..2794c9a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,10 +1,13 @@ # Privacy Policy — Pinakes Android -_Last updated: 2026-06-19_ +_Technical privacy description updated: 2026-09-09. This document still requires +the publisher's controller/contact and retention details before use as the final +Google Play privacy policy._ Pinakes Android is an open-source client app that connects to a **Pinakes -library instance chosen by you**. The app itself has no backend of its own: all -your data lives on the library server you point it at, operated by that library. +library instance chosen by you**. Library services use that instance's server. +The app also integrates Sentry for crash diagnostics and loads media from URLs +supplied by the library; not all requests are limited to the library's domain. ## What the app stores on your device @@ -22,15 +25,22 @@ when you log out or disconnect from the instance. ## What the app sends, and to whom -The app communicates **only** with the Pinakes instance URL you configure. It +For library services, the app communicates with the Pinakes instance you configure. It sends your email and password (over HTTPS) at login, and your bearer token on subsequent requests, to authenticate and to perform the actions you initiate (search, loans/reservations, wishlist, profile edits, contact messages). It also fetches book cover images from URLs returned by that instance. -The app does **not** include third-party analytics, advertising, or tracking -SDKs, and does not transmit your data to the app's authors or any party other -than your chosen library instance. +The app includes **Sentry crash reporting**, configured during app startup. +Default PII attachment is disabled and performance trace sampling is zero. +These settings do not mean no data is transmitted: error events, technical +device/app information and diagnostic context can reach Sentry. Retention and +the precise diagnostic fields must be verified against the publisher's Sentry +configuration before completing the Play Data Safety declaration. + +Catalog and HTTP caches are also stored locally. Signing out or switching +instances clears the relevant caches. Signing out revokes the session; it does +**not** delete the account, loans or other records held by the library. ## Network security @@ -40,16 +50,22 @@ required to be HTTPS. ## Push notifications (optional) -When enabled by your library and by you, push delivery uses **UnifiedPush** -through a distributor of your choosing. Registration data (a push endpoint and -WebPush keys) is sent to your library instance only. +The app has a notification feed and UnifiedPush-related API support. A complete +distributor integration has not been verified for this release; do not describe +background push delivery as universally available. Any enabled distributor and +its data handling must be included in the deployment's privacy review. ## Data controller -Your data is controlled by the **library operating the Pinakes instance** you -connect to. For requests about access, correction, or deletion of your account -data, contact that library directly. For questions about the app itself, open an -issue on the project repository. +The library operates the account and circulation services. Contact that library +for access, correction or deletion requests; do not post credentials, loan +history or other private data in public GitHub issues. The app publisher must +also identify its role and private contact channel for app diagnostics. + +The app supports account registration. A compliant in-app account-deletion +request path and an external request URL remain release gates; logout must not +be presented as account deletion. A library may need to retain specific records +under its obligations, which its policy must explain accurately. > Libraries publishing their own build: replace this section with your > organisation's contact details and host this policy at a public URL, then link From 553d0b942f7047d9343765f29ebc4fa9a6935eb0 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Wed, 9 Sep 2026 17:22:16 +0200 Subject: [PATCH 3/4] docs: identify Pinakes publisher and privacy contact --- PRIVACY.md | 27 +++++++++++++++++---------- docs/PLAY-RELEASES.md | 2 ++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 2794c9a..5725698 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,8 +1,8 @@ # Privacy Policy — Pinakes Android -_Technical privacy description updated: 2026-09-09. This document still requires -the publisher's controller/contact and retention details before use as the final -Google Play privacy policy._ +_Technical privacy description updated: 2026-09-09. The publisher has confirmed +the controller and contact below. Retention details and the remaining release +gates still need verification before this becomes the final Play privacy policy._ Pinakes Android is an open-source client app that connects to a **Pinakes library instance chosen by you**. Library services use that instance's server. @@ -57,16 +57,23 @@ its data handling must be included in the deployment's privacy review. ## Data controller -The library operates the account and circulation services. Contact that library -for access, correction or deletion requests; do not post credentials, loan -history or other private data in public GitHub issues. The app publisher must -also identify its role and private contact channel for app diagnostics. +The publisher and controller for this Pinakes Android distribution and its app +diagnostics is **D'Alessandro Fabio Gaetano**, reachable at +**[info@fabiodalez.it](mailto:info@fabiodalez.it)**. This is also the contact for +the reference library at **https://biblioteca.fabiodalez.it**. + +When you connect to another independently operated library, that library +operates its own account and circulation services. Consult its privacy policy +and contact it for access, correction or deletion requests relating to those +records. Do not post credentials, loan history or other private data in public +GitHub issues. Never send your password in a privacy request. The app supports account registration. A compliant in-app account-deletion request path and an external request URL remain release gates; logout must not be presented as account deletion. A library may need to retain specific records under its obligations, which its policy must explain accurately. -> Libraries publishing their own build: replace this section with your -> organisation's contact details and host this policy at a public URL, then link -> that URL in the Google Play Console (App content → Privacy policy). +For the reference library, account and privacy requests may be sent to +info@fabiodalez.it, identifying the library URL and account email. Identity +verification may be necessary before processing a request. This contact does +not yet replace the in-app and public-web deletion paths listed above. diff --git a/docs/PLAY-RELEASES.md b/docs/PLAY-RELEASES.md index 6d12ee0..9cdf6a0 100644 --- a/docs/PLAY-RELEASES.md +++ b/docs/PLAY-RELEASES.md @@ -8,6 +8,8 @@ This workflow builds bundles; it does NOT upload to Play or publish to productio Review library: https://biblioteca.fabiodalez.it. Mobile API was enabled on 2026-09-09. A dedicated standard reader was created; credentials are private outside Git, not in this document or release artifacts. +Publisher/controller confirmed by the owner on 2026-09-09: **D'Alessandro Fabio Gaetano**. Public support/privacy email: **info@fabiodalez.it**. Use these for this distribution, not as the controller of independently operated third-party library instances. + Remaining release gates: accurate privacy policy including Sentry, account-deletion path and public request URL, reviewer access test, minified-device smoke test, native 16 KB compatibility and Play questionnaires. The personal developer account requires the closed-test period before production access. Do not interpret a green bundle build as policy approval. The older PLAY_STORE_COMPLIANCE.md audit is historical: its no-third-party-SDK and disabled-R8 statements no longer describe the app. Sentry is present, R8 is enabled, and logout does not delete the user's server account. From da223827d262bcf7abd7500151bba3326afbc710 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Wed, 9 Sep 2026 17:29:33 +0200 Subject: [PATCH 4/4] ci: fail the Play bundle when the artifact is not actually signed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jarsigner -verify exits 0 on an unsigned artifact — it prints "jar is unsigned" and reports success. Paired with the signing config's deliberate fallback to an unsigned bundle when the credentials do not reach Gradle, an exit code alone would have let an unsigned bundle travel as a verified one. The step now requires the positive "jar verified" statement and fails loudly otherwise. --- .github/workflows/play-bundle.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/play-bundle.yml b/.github/workflows/play-bundle.yml index 0bff850..48028a8 100644 --- a/.github/workflows/play-bundle.yml +++ b/.github/workflows/play-bundle.yml @@ -42,7 +42,17 @@ jobs: trap 'rm -f "$PINAKES_KEYSTORE"' EXIT printf '%s' "$KEY_BASE64" | base64 --decode > "$PINAKES_KEYSTORE" ./gradlew bundleRelease --no-daemon --no-configuration-cache - jarsigner -verify app/build/outputs/bundle/release/app-release.aab + # `jarsigner -verify` exits 0 on an UNSIGNED artifact — it just prints + # "jar is unsigned". Since the build deliberately falls back to an + # unsigned bundle when the credentials do not reach Gradle, an exit + # code alone would let an unsigned bundle pass as verified. Require + # the positive statement instead. + verify_out="$(jarsigner -verify app/build/outputs/bundle/release/app-release.aab)" + printf '%s\n' "$verify_out" + case "$verify_out" in + *"jar verified"*) ;; + *) echo "::error::release bundle is not signed"; exit 1 ;; + esac sha256sum app/build/outputs/bundle/release/app-release.aab > app/build/outputs/bundle/release/SHA256SUMS.txt - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: