diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0a6cd26..293963a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,7 @@ stages: - analyze - test - build + - deploy variables: PUB_CACHE: "$CI_PROJECT_DIR/.pub-cache" @@ -82,3 +83,63 @@ test:app_seeds: coverage_report: coverage_format: cobertura path: apps/app_seeds/coverage/cobertura.xml + +# --------------------------------------------------------------------------- +# Release automation. Runs ONLY on tags, so it never touches the per-push gate. +# Password-free: signing + Play credentials come from masked+protected CI vars. +# Required CI variables (Settings > CI/CD > Variables), set once: +# TANE_KEYSTORE_BASE64 base64 of the dedicated Tane upload keystore (.jks) +# TANE_KEYSTORE_PASSWORD store password +# TANE_KEY_ALIAS key alias (tane-upload) +# TANE_KEY_PASSWORD key password +# SUPPLY_JSON_KEY_DATA Google Play service-account JSON (raw file contents) +# Tag a commit (e.g. `git tag v0.1.0 && git push --tags`) to build + publish. +# --------------------------------------------------------------------------- + +# Build the signed release artifacts (AAB for Play, APK for sideload/QA). +build:android: + stage: build + rules: + - if: '$CI_COMMIT_TAG' + script: + - cd apps/app_seeds + - dart run slang + - dart run build_runner build --delete-conflicting-outputs + # Materialize the signing material from CI secrets (never in the repo). + - echo "$TANE_KEYSTORE_BASE64" | base64 -d > "$CI_PROJECT_DIR/tane-upload.jks" + - | + cat > android/key.properties < Google Play). +# Run from apps/app_seeds/: bundle install && bundle exec fastlane deploy_play +source "https://rubygems.org" + +gem "fastlane" diff --git a/apps/app_seeds/fastlane/Appfile b/apps/app_seeds/fastlane/Appfile new file mode 100644 index 0000000..590393c --- /dev/null +++ b/apps/app_seeds/fastlane/Appfile @@ -0,0 +1,9 @@ +# Google Play identity + credentials for `fastlane supply`. +# +# The service-account JSON is injected from CI as the SUPPLY_JSON_KEY_DATA +# secret (raw file contents) and is NEVER committed. Locally you can instead +# export SUPPLY_JSON_KEY_DATA, or drop a gitignored play-service-account.json +# and point json_key_file at it. +package_name("org.comunes.tane") + +json_key_data_raw(ENV["SUPPLY_JSON_KEY_DATA"]) if ENV["SUPPLY_JSON_KEY_DATA"] diff --git a/apps/app_seeds/fastlane/Fastfile b/apps/app_seeds/fastlane/Fastfile new file mode 100644 index 0000000..25190cd --- /dev/null +++ b/apps/app_seeds/fastlane/Fastfile @@ -0,0 +1,37 @@ +# Fastlane lanes for Tane (org.comunes.tane). +# +# Publishing is automated and password-free: a git tag triggers CI, which builds +# a signed AAB and runs `deploy_play`. Credentials come from CI secrets, never +# typed by hand: +# SUPPLY_JSON_KEY_DATA Google Play service-account JSON (raw contents) +# +# The store listing (titles, descriptions, changelogs, screenshots) is read +# straight from fastlane/metadata/android// — the tree that already +# feeds the landing site and F-Droid. See docs/release.md and .gitlab-ci.yml. + +default_platform(:android) + +# Release bundle produced by `flutter build appbundle --release`, relative to +# the app dir (apps/app_seeds), which is where fastlane runs. +AAB_PATH = "build/app/outputs/bundle/release/app-release.aab".freeze + +platform :android do + desc "Upload the signed AAB + store listing to Google Play (internal track)" + lane :deploy_play do + supply( + track: "internal", + aab: AAB_PATH, + release_status: "completed", # internal testing has no review delay + skip_upload_apk: true, # we ship the AAB, not a raw APK + skip_upload_changelogs: false, + ) + end + + desc "Push only the store listing (text + images), no binary" + lane :deploy_metadata do + supply( + skip_upload_aab: true, + skip_upload_apk: true, + ) + end +end diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index fd47a9a..ce2b6bb 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -1,5 +1,5 @@ name: tane -description: "Tane — local-first, encrypted, decentralized traditional-seed inventory." +description: "Tane — local-first, encrypted, decentralized traditional-seed inventory and market." publish_to: 'none' version: 0.1.0+1 diff --git a/docs/fdroid/org.comunes.tane.yml b/docs/fdroid/org.comunes.tane.yml new file mode 100644 index 0000000..1476ada --- /dev/null +++ b/docs/fdroid/org.comunes.tane.yml @@ -0,0 +1,62 @@ +# F-Droid metadata for Tane (org.comunes.tane). +# +# This is the source of truth for the fdroiddata merge request. To submit: +# 1. Fork https://gitlab.com/fdroid/fdroiddata +# 2. Copy this file to metadata/org.comunes.tane.yml +# 3. Validate locally: fdroid lint org.comunes.tane +# fdroid build -v -l org.comunes.tane +# 4. Open the MR. +# +# Prerequisites (see docs/release.md): +# - The source repo below must be publicly clonable without auth. +# - Each release is a signed git tag `v` (UpdateCheckMode: Tags). +# - F-Droid builds and signs the APK with ITS OWN key, so the F-Droid build +# and the Google Play build have different signatures and cannot cross-update. +# +# NOTE: Flutter-on-F-Droid builds are finicky. Pin the Flutter version to the +# developer toolchain (3.41.9) and re-run `fdroid build` after every bump. This +# recipe accounts for the monorepo layout (pub workspace root at repo root, app +# at apps/app_seeds) and the generated code (slang i18n + Drift via build_runner). + +Categories: + - Science & Education +License: AGPL-3.0-or-later +AuthorName: Comunes +AuthorWebSite: https://comunes.org +WebSite: https://tane.comunes.org +SourceCode: https://git.comunes.org/comunes/tane +IssueTracker: https://git.comunes.org/comunes/tane/issues +Translation: https://translate.comunes.org/projects/tane + +AutoName: Tane + +RepoType: git +Repo: https://git.comunes.org/comunes/tane.git + +Builds: + - versionName: 0.1.0 + versionCode: 1 + commit: v0.1.0 + subdir: apps/app_seeds + sudo: + - apt-get update + - apt-get install -y git unzip xz-utils + # Pin Flutter to the developer toolchain; keep in sync with .gitlab-ci.yml. + - git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter + - git config --global --add safe.directory /opt/flutter + output: build/app/outputs/flutter-apk/app-release.apk + prebuild: + - export PATH="/opt/flutter/bin:$PATH" + - flutter config --no-analytics + - flutter pub get + # Generated sources: i18n (slang) then Drift/other codegen (build_runner). + - dart run slang + - dart run build_runner build --delete-conflicting-outputs + build: + - export PATH="/opt/flutter/bin:$PATH" + - flutter build apk --release + +AutoUpdateMode: Version v%v +UpdateCheckMode: Tags +CurrentVersion: 0.1.0 +CurrentVersionCode: 1 diff --git a/docs/legal/internal/play-compliance.md b/docs/legal/internal/play-compliance.md new file mode 100644 index 0000000..53b2837 --- /dev/null +++ b/docs/legal/internal/play-compliance.md @@ -0,0 +1,84 @@ +# Google Play compliance answer sheet (internal) + +Draft answers for the Play Console questionnaires. Transcribe into the console; +this file is the single source of truth so the answers stay consistent across +releases. Grounded in [`../privacy-policy.md`](../privacy-policy.md) and +[`../../design/security-privacy.md`](../../design/security-privacy.md). + +Package: `org.comunes.tane` · Publisher: Asociación Comunes · Contact: info@comunes.org +Privacy policy URL: https://tane.comunes.org/legal/privacy + +--- + +## 1. Data Safety form + +**Core principle:** Tane has **no accounts and no Tane server**. Anything a user +records stays on-device, encrypted (SQLCipher). Data only leaves the device by an +explicit user action (publishing an offer, filling a public profile, sending a +message, or syncing to the user's own second device), and then it travels through +community-run relays, not through us. So under Play's definitions we *collect* +nothing for ourselves; we *share* only the specific things the user chooses to +publish, all for **App functionality** — never for analytics, ads, or sale. + +### Does your app collect or share any user data? → **Yes** +(Because user-initiated publishing sends data to third-party relays.) + +### Data types — declare as **Shared**, purpose **App functionality**, **optional** (user-initiated) + +| Play data type | Declare? | Notes | +|---|---|---| +| Location → Approximate location | Shared | Only a coarse ~2–80 km area, only if the user sets a sharing zone for an offer. Never precise. Never collected for us. | +| Personal info → Name | Shared | Only the display name the user chooses to put in a public profile. Optional. | +| Personal info → Other info (bio) | Shared | Optional public profile bio. | +| Photos and videos → Photos | Shared | Only photos the user attaches to a public offer or profile. | +| Messages → Other in-app messages | Shared | Direct messages, **end-to-end encrypted**; relays carry only encrypted envelopes. Declare as shared for transparency; mark encrypted in transit. | +| App activity / App info & performance | No | No analytics, no crash reporting, no diagnostics sent anywhere. | +| Financial, Health, Contacts (device), Calendar, Browsing history, Files, Device IDs | No | Not accessed or transmitted. | + +Note on "Collected vs Shared": Play defines *collection* as leaving the device to +the developer. We receive nothing, so nothing is "Collected by us." The items above +are *Shared* (to third-party relays / other users). If the console forces a +"Collected" tick for a shared type, pair it with the encryption + optional answers below. + +### Security practices +- **Is all user data encrypted in transit?** → **Yes.** Relay connections use TLS; + direct messages are additionally end-to-end encrypted; device-to-device sync is encrypted. +- **Data stored on device** is encrypted at rest (SQLCipher) — mention in the listing, though Play's form focuses on transit. +- **Can users request data deletion?** → **Yes.** Everything local can be deleted in-app + instantly; published offers can be withdrawn, which asks relays to delete. Relays we + don't operate may retain copies (stated honestly in the privacy policy). +- **Data is NOT sold.** **Data is NOT used for advertising or marketing.** +- **Independent security review?** → No (self-declared; source is public/AGPL and auditable). + +--- + +## 2. Content rating (IARC questionnaire) + +- Category: **Utility / Reference / Productivity** (not a game). +- Violence, sexual content, gambling, drugs: **None.** +- **User-generated content / communication:** **Yes** — the market has public offers, + user profiles, and direct messaging. Declare in-app UGC and user-to-user communication. +- Expected outcome: **Everyone / PEGI 3–style**, with the UGC/communication flags set. + +## 3. Target audience & content + +- Target age: **not directed at children.** Age band spans adults; the phrase "10–99" + is a usability aim (simple enough for a 10-year-old, legible for a 99-year-old), **not** + a claim of child audience. In the console pick the **adult / 18+** or "not for children" + target so Families policy does not apply to the market/messaging features. +- The seed **inventory** alone is harmless for any age, but the **market and messaging** + are social features and are declared not directed at children (see privacy policy §8). + +## 4. UGC policy (Play's user-generated content requirements) + +The market ships the mitigations Play looks for; keep them working: +- **Community rules acceptance** gate on entering the market. +- **Report** content/users and **block** users, in-app. +- A stated content policy: [`../community-rules.md`](../community-rules.md). + +## 5. Ads & privacy + +- **Contains ads?** → **No.** +- **Data deletion URL / contact:** info@comunes.org (and in-app deletion). +- **Government app?** → No. **Financial features?** → No (optional Ğ1 price is a plain + number shown in a listing, no in-app payments, no Play Billing). diff --git a/docs/release.md b/docs/release.md index 4d414ae..21ed2cd 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,7 +1,21 @@ -# Releasing Tane (manual, no CI yet) +# Releasing Tane -Block 1 ships as a self-contained Android app (desktop builds also work). There -is no release pipeline yet — this page is the by-hand checklist. +Tane ships as a self-contained Android app (desktop builds also work). Releases +are **automated and password-free**: pushing a signed git tag triggers CI, which +builds a signed AAB and publishes it to Google Play. This page covers the one-time +setup, the automated flow, and the by-hand fallback. + +## TL;DR — cut a release + +Once the one-time setup below is done: + +```bash +# bump version in apps/app_seeds/pubspec.yaml, update CHANGELOG + changelogs/.txt +git tag v0.1.0 && git push origin v0.1.0 +``` + +CI (`build:android` → `deploy:play` in [`../.gitlab-ci.yml`](../.gitlab-ci.yml)) builds +the signed AAB/APK and uploads to Play's **internal** track. No passwords are typed. ## Versioning @@ -12,38 +26,92 @@ is no release pipeline yet — this page is the by-hand checklist. add a matching per-locale note under `apps/app_seeds/fastlane/metadata/android//changelogs/.txt`. -## Android signing (one-time) +## One-time setup -Release builds sign with your own keystore when a **gitignored** -`apps/app_seeds/android/key.properties` exists; without it they fall back to -debug keys (so contributors can still `flutter run --release`). +### Android signing keystore (dedicated to Tane) -1. Create a keystore (keep it and its passwords safe — losing it means you can - never update the app under the same identity): - ```bash - keytool -genkey -v -keystore ~/tane-release.jks \ - -keyalg RSA -keysize 4096 -validity 10000 -alias tane - ``` -2. Create `apps/app_seeds/android/key.properties` (never commit it): - ```properties - storeFile=/home/you/tane-release.jks - storePassword=… - keyAlias=tane - keyPassword=… - ``` +Tane uses its **own** upload keystore, separate from other Comunes apps (Ğ1nkgo, +Fuegos), kept in the shared Comunes signing directory: -## Build the beta by hand +```bash +keytool -genkey -v \ + -keystore /home/vjrj/proyectos/sync/comunes/shared-l2/apkSigning/tane-upload.jks \ + -keyalg RSA -keysize 4096 -validity 10000 -alias tane-upload +``` + +Release builds sign with this keystore when a **gitignored** +`apps/app_seeds/android/key.properties` exists; without it they fall back to debug +keys (so contributors can still `flutter run --release`). For **local** signed +builds, create `apps/app_seeds/android/key.properties` (never commit it): + +```properties +storeFile=/home/vjrj/proyectos/sync/comunes/shared-l2/apkSigning/tane-upload.jks +storePassword=… +keyAlias=tane-upload +keyPassword=… +``` + +With **Play App Signing** (recommended, enabled once in the console) this is the +*upload key*; Google holds the app signing key and can help rotate the upload key +if it is ever lost. + +### CI secrets (one-time, GitLab → Settings → CI/CD → Variables) + +Set these as **masked + protected** so releases never prompt for a password: + +| Variable | What | +|---|---| +| `TANE_KEYSTORE_BASE64` | `base64 -w0 tane-upload.jks` | +| `TANE_KEYSTORE_PASSWORD` | store password | +| `TANE_KEY_ALIAS` | `tane-upload` | +| `TANE_KEY_PASSWORD` | key password | +| `SUPPLY_JSON_KEY_DATA` | Google Play service-account JSON (raw file contents) | + +The Play service account is created in Google Cloud and granted release access in +Play Console; it is what lets `fastlane supply` upload without a human. The first +upload of a brand-new app must still be done by hand in the console (Play requires +the app to exist before the API accepts uploads). + +## Build by hand (fallback / first Play upload) ```bash cd apps/app_seeds dart run slang # i18n (if strings changed) dart run build_runner build --delete-conflicting-outputs # Drift (if schema changed) flutter test # must be green -flutter build apk --release # or: flutter build appbundle --release +flutter build appbundle --release # AAB for Play +flutter build apk --release # APK for sideload / QA ``` -The APK lands at -`apps/app_seeds/build/app/outputs/flutter-apk/app-release.apk`. +Artifacts: +- AAB → `apps/app_seeds/build/app/outputs/bundle/release/app-release.aab` +- APK → `apps/app_seeds/build/app/outputs/flutter-apk/app-release.apk` + +Verify the signature is Tane's key (not another app's): +`apksigner verify --print-certs app-release.apk` → alias `tane-upload`. + +## Publish to Google Play (fastlane) + +CI does this on tag, but you can run it locally too: + +```bash +cd apps/app_seeds +bundle install +SUPPLY_JSON_KEY_DATA="$(cat play-service-account.json)" bundle exec fastlane deploy_play +``` + +Lanes live in [`fastlane/Fastfile`](../apps/app_seeds/fastlane/Fastfile); the store +listing (titles, descriptions, changelogs, screenshots) is read straight from +`fastlane/metadata/android/`. Data Safety and content-rating answers: +[`legal/internal/play-compliance.md`](legal/internal/play-compliance.md). + +## Publish to F-Droid (official repo) + +F-Droid builds and signs from source after a merge request to `fdroiddata`. The +build recipe is kept in-repo at [`fdroid/org.comunes.tane.yml`](fdroid/org.comunes.tane.yml). +Copy it to `metadata/org.comunes.tane.yml` in a fork of fdroiddata, validate with +`fdroid lint` / `fdroid build -l org.comunes.tane`, then open the MR. F-Droid signs +with its own key, so the F-Droid and Play builds have different signatures. ## Store metadata @@ -75,7 +143,7 @@ artifact, it does not replace tests): second install). 5. Switch language (es / en / pt); check an RTL locale if the device offers one. -## Not in this release +## Not automated -CI, Play Store automation, web builds (SQLCipher is unavailable on web), and -the whole social layer (Block 2). +Web builds (SQLCipher is unavailable on web) and iOS (no Apple developer setup +yet). The F-Droid MR is opened by hand (F-Droid builds on its own infra, not ours).