feat(release): automate store publishing (fastlane supply + GitLab CI) and F-Droid recipe

- fastlane: Appfile/Fastfile/Gemfile with deploy_play lane (AAB -> Play internal track)
- CI: tag-gated build:android (signed AAB/APK from CI secrets) + deploy:play jobs
- F-Droid: fdroiddata build recipe at docs/fdroid/org.comunes.tane.yml
- Play compliance: Data Safety / content-rating answer sheet (docs/legal/internal)
- docs/release.md: automated tag-triggered flow, dedicated tane-upload keystore
- pubspec: description now mentions the market, not just the inventory
This commit is contained in:
vjrj 2026-07-16 02:42:47 +02:00
parent dc0d18562f
commit fd6b9d5f0d
9 changed files with 361 additions and 28 deletions

View file

@ -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 <<EOF
storeFile=$CI_PROJECT_DIR/tane-upload.jks
storePassword=$TANE_KEYSTORE_PASSWORD
keyAlias=$TANE_KEY_ALIAS
keyPassword=$TANE_KEY_PASSWORD
EOF
- flutter build appbundle --release
- flutter build apk --release
# Don't leave signing material in the workspace/cache.
- rm -f android/key.properties "$CI_PROJECT_DIR/tane-upload.jks"
artifacts:
paths:
- apps/app_seeds/build/app/outputs/bundle/release/app-release.aab
- apps/app_seeds/build/app/outputs/flutter-apk/app-release.apk
expire_in: 90 days
# Upload the AAB + store listing to Google Play (internal track) via fastlane.
# Needs a Ruby image, not the Flutter one, so it overrides the global setup.
deploy:play:
stage: deploy
image: ruby:3.2
needs:
- job: build:android
artifacts: true
rules:
- if: '$CI_COMMIT_TAG'
before_script:
- cd apps/app_seeds
- bundle install --path vendor/bundle
script:
- bundle exec fastlane deploy_play
cache:
key: "fastlane-gems"
paths:
- apps/app_seeds/vendor/bundle/

View file

@ -43,3 +43,10 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Release automation (fastlane supply). Credentials come from CI secrets or a
# gitignored local file; generated reports are not tracked.
/play-service-account.json
/fastlane/report.xml
/fastlane/Preview.html
/Gemfile.lock

5
apps/app_seeds/Gemfile Normal file
View file

@ -0,0 +1,5 @@
# Ruby toolchain for release automation (fastlane supply -> Google Play).
# Run from apps/app_seeds/: bundle install && bundle exec fastlane deploy_play
source "https://rubygems.org"
gem "fastlane"

View file

@ -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"]

View file

@ -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/<locale>/ — 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

View file

@ -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

View file

@ -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<version>` (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

View file

@ -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 ~280 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 3style**, with the UGC/communication flags set.
## 3. Target audience & content
- Target age: **not directed at children.** Age band spans adults; the phrase "1099"
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).

View file

@ -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/<code>.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/<locale>/changelogs/<versionCode>.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).