diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..9331d0a --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,51 @@ +# Per-push gate for git.comunes.org (Forgejo Actions): analyze + tests. +# Mirrors the local verification gate in docs/design/testing.md. Ported from the +# retired .gitlab-ci.yml. Flutter pinned to the developer toolchain (3.41.9). +# +# NOTE: `runs-on` must match a label your Forgejo runner registered with; adjust +# if the Comunes runner uses something other than `docker`. + +name: ci + +on: + push: + branches: ['**'] + pull_request: + +jobs: + analyze: + runs-on: docker + container: + image: ghcr.io/cirruslabs/flutter:3.41.9 + steps: + - uses: actions/checkout@v4 + - run: flutter pub get + - run: dart format --output=none --set-exit-if-changed . + - run: flutter analyze + + test-commons-core: + runs-on: docker + container: + image: ghcr.io/cirruslabs/flutter:3.41.9 + steps: + - uses: actions/checkout@v4 + - run: flutter pub get + - name: dart test + working-directory: packages/commons_core + run: dart test + + test-app-seeds: + runs-on: docker + container: + image: ghcr.io/cirruslabs/flutter:3.41.9 + steps: + - uses: actions/checkout@v4 + # SQLCipher so the "no plaintext at rest" security test actually runs. + - run: apt-get update -qq && apt-get install -y -qq libsqlcipher-dev + - run: flutter pub get + - name: Generate code + test + working-directory: apps/app_seeds + run: | + dart run slang + dart run build_runner build --delete-conflicting-outputs + flutter test --coverage diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..558a89c --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,78 @@ +# Release automation for git.comunes.org (Forgejo Actions). +# +# Password-free: pushing a tag `v*` builds a signed AAB/APK and uploads it to +# Google Play's internal track via fastlane. All credentials come from repo +# secrets (Settings > Actions > Secrets), never typed: +# TANE_KEYSTORE_BASE64 base64 of the dedicated 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 (reused from Ğ1nkgo) +# +# Build + deploy live in ONE job so the AAB never has to cross a job boundary. +# NOTE: `runs-on` must match a label your Forgejo runner registered with; adjust +# if the Comunes runner uses something other than `docker`. + +name: release + +on: + push: + tags: + - 'v*' + +jobs: + android: + runs-on: docker + container: + image: ghcr.io/cirruslabs/flutter:3.41.9 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate code (i18n + Drift) + working-directory: apps/app_seeds + run: | + flutter pub get + dart run slang + dart run build_runner build --delete-conflicting-outputs + + - name: Materialize signing material from secrets + working-directory: apps/app_seeds + env: + TANE_KEYSTORE_BASE64: ${{ secrets.TANE_KEYSTORE_BASE64 }} + TANE_KEYSTORE_PASSWORD: ${{ secrets.TANE_KEYSTORE_PASSWORD }} + TANE_KEY_ALIAS: ${{ secrets.TANE_KEY_ALIAS }} + TANE_KEY_PASSWORD: ${{ secrets.TANE_KEY_PASSWORD }} + run: | + echo "$TANE_KEYSTORE_BASE64" | base64 -d > "$GITHUB_WORKSPACE/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/fastlane/metadata/android/en-US/images/featureGraphic.png b/apps/app_seeds/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 0000000..f320591 Binary files /dev/null and b/apps/app_seeds/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/apps/app_seeds/fastlane/metadata/android/en-US/images/icon.png b/apps/app_seeds/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000..72b550d Binary files /dev/null and b/apps/app_seeds/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/apps/app_seeds/fastlane/metadata/android/es-ES/images/featureGraphic.png b/apps/app_seeds/fastlane/metadata/android/es-ES/images/featureGraphic.png new file mode 100644 index 0000000..d12b286 Binary files /dev/null and b/apps/app_seeds/fastlane/metadata/android/es-ES/images/featureGraphic.png differ diff --git a/apps/app_seeds/fastlane/metadata/android/es-ES/images/icon.png b/apps/app_seeds/fastlane/metadata/android/es-ES/images/icon.png new file mode 100644 index 0000000..72b550d Binary files /dev/null and b/apps/app_seeds/fastlane/metadata/android/es-ES/images/icon.png differ 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/apps/app_seeds/tool/gen_feature_graphic.py b/apps/app_seeds/tool/gen_feature_graphic.py new file mode 100644 index 0000000..63ea2bc --- /dev/null +++ b/apps/app_seeds/tool/gen_feature_graphic.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Generate Play Store feature graphics (1024x500) + store icon (512) for Tane. + +Brand green #2F7D34, white sprout from the adaptive foreground, 'Tane' wordmark +in Noto Sans (the site's brand typeface), and the official value-proposition +tagline. On-brand and replaceable with a designed asset later. + +Run from apps/app_seeds/: python3 tool/gen_feature_graphic.py +""" +import os +from PIL import Image, ImageDraw, ImageFont + +REPO_APP = "/home/vjrj/proyectos/dev/tane/.claude/worktrees/interesting-cray-6261fd/apps/app_seeds" +NOTO = "/usr/share/fonts/truetype/noto" +F_BOLD = f"{NOTO}/NotoSans-Bold.ttf" +F_MED = f"{NOTO}/NotoSans-Medium.ttf" +F_REG = f"{NOTO}/NotoSans-Regular.ttf" +FG = f"{REPO_APP}/assets/icon_foreground.png" +ICON = f"{REPO_APP}/assets/icon.png" + +GREEN = (47, 125, 52) # #2F7D34 +GREEN_DARK = (34, 82, 30) # #22521E +WHITE = (255, 255, 255) +FAINT = (208, 228, 210) + +W, H = 1024, 500 + +# value line = what the app is (explains it); attrs = the differentiators. +COPY = { + "en-US": ("Keep and share traditional seeds", "Offline · Encrypted · No account"), + "es-ES": ("Guarda y comparte semillas tradicionales", "Sin conexión · Cifrado · Sin cuenta"), +} + + +def sprout(height): + im = Image.open(FG).convert("RGBA") + bb = im.getbbox() + if bb: + im = im.crop(bb) + s = height / im.height + return im.resize((max(1, int(im.width * s)), height), Image.LANCZOS) + + +def bg(): + top = Image.new("RGB", (W, H), GREEN) + bot = Image.new("RGB", (W, H), GREEN_DARK) + mask = Image.new("L", (W, H)) + md = ImageDraw.Draw(mask) + for y in range(H): + md.line([(0, y), (W, y)], fill=int(60 * (y / H))) + return Image.composite(bot, top, mask) + + +def center_line(d, text, font, top, fill): + b = d.textbbox((0, 0), text, font=font) + w = b[2] - b[0] + d.text(((W - w) / 2 - b[0], top - b[1]), text, font=font, fill=fill) + return b[3] - b[1] + + +def feature_graphic(locale, out): + img = bg() + d = ImageDraw.Draw(img) + value, attrs = COPY[locale] + + # Row 1: sprout + 'Tane', centered as a group in the upper half. + spr = sprout(150) + wf = ImageFont.truetype(F_BOLD, 120) + wb = d.textbbox((0, 0), "Tane", font=wf) + ww, wh = wb[2] - wb[0], wb[3] - wb[1] + gap = 26 + gw = spr.width + gap + ww + gx = (W - gw) // 2 + row_top = 92 + cy = row_top + spr.height / 2 + img.paste(spr, (gx, row_top), spr) + d.text((gx + spr.width + gap - wb[0], cy - wh / 2 - wb[1]), "Tane", font=wf, fill=WHITE) + y = row_top + spr.height + 34 + + # Row 2: value proposition (explains the app). + vf = ImageFont.truetype(F_MED, 42) + y += center_line(d, value, vf, y, WHITE) + 18 + + # Row 3: differentiators, quieter. + af = ImageFont.truetype(F_REG, 30) + center_line(d, attrs, af, y, FAINT) + + img.save(out, "PNG") + print("wrote", out, img.size) + + +def store_icon(out): + Image.open(ICON).convert("RGB").resize((512, 512), Image.LANCZOS).save(out, "PNG") + print("wrote", out) + + +for loc in ("en-US", "es-ES"): + d = f"{REPO_APP}/fastlane/metadata/android/{loc}/images" + os.makedirs(d, exist_ok=True) + feature_graphic(loc, f"{d}/featureGraphic.png") + store_icon(f"{d}/icon.png") 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..b03f092 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 +``` + +Forgejo Actions ([`../.forgejo/workflows/release.yml`](../.forgejo/workflows/release.yml)) +builds the signed AAB/APK and uploads to Play's **internal** track. No passwords are typed. ## Versioning @@ -12,38 +26,110 @@ 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, Forgejo → repo Settings → Actions → Secrets) + +Set these so releases never prompt for a password: + +| Secret | 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 **reused from Ğ1nkgo** (`ginkgo-play-uploader@…`, +`/home/vjrj/etc/ginkgo_play_api_key.json`) — it belongs to the Comunes Play account, +not to one app, so it just needs access granted to `org.comunes.tane` in Play Console. +`fastlane supply` uses it to upload without a human. The first upload of the brand-new +app must still be done by hand in the console (Play requires the app to exist before +the API accepts uploads). + +## CI runner (Forgejo Actions) — action needed + +The release + gate workflows (`.forgejo/workflows/*.yml`) run on **Forgejo Actions**, +which needs a **registered runner**. As of 2026-07-16 the `aaron` host runs the +`forgejo` and `jenkins` containers but **no Forgejo Actions runner** — so the +workflows will queue and never start until one is registered. Two options: + +- **Register an act_runner** on `aaron` (a `forgejo-runner`/`act_runner` container + registered against `git.comunes.org` with a label matching `runs-on:` — currently + `docker`). Then tag → build → publish works as designed. +- **Or move CI to Jenkins** (already running on `aaron`) via a Jenkinsfile; the build + and `fastlane deploy_play` steps are the same shell commands. + +Also: the keystore's real alias is **`tane-upload`**, so the Forgejo secret +`TANE_KEY_ALIAS` and local `key.properties` must both use `tane-upload`. + +## 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 +161,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).