Compare commits

..

5 commits

Author SHA1 Message Date
4cac56e3bb docs(release): note Forgejo Actions runner is missing on aaron + keystore alias
Some checks failed
ci / analyze (push) Failing after 15s
ci / test-commons-core (push) Failing after 29s
ci / test-app-seeds (push) Failing after 3s
aaron runs forgejo + jenkins but no act_runner, so workflows queue until a runner
is registered (or CI moves to Jenkins). Record that the keystore alias is tane-upload.
2026-07-16 03:15:18 +02:00
15eb9f7007 ci: port analyze/test gate to Forgejo Actions, retire .gitlab-ci.yml
git.comunes.org is Forgejo. Replicates the analyze + commons_core/app_seeds test
gate in .forgejo/workflows/ci.yml (per-push + PR). Dropped the GitLab-only Windows
job (no Forgejo Windows runner).
2026-07-16 03:07:07 +02:00
17d0ff1221 fix(release): port store publishing to Forgejo Actions (canonical remote is Forgejo)
git.comunes.org runs Forgejo Actions, not GitLab CI, so the tag-triggered
signed-AAB build + fastlane Play upload now lives in .forgejo/workflows/release.yml
(single job, no cross-job artifact passing). Removed the equivalent GitLab jobs;
.gitlab-ci.yml keeps only the analyze/test gate. Play service account is reused
from Ğ1nkgo. docs/release.md updated (Forgejo secrets).
2026-07-16 03:05:33 +02:00
eef99975bf feat(store): branded feature graphic (1024x500) + 512 store icon
Noto Sans wordmark on brand green (#2F7D34), white sprout, and the official
value tagline ('keep and share traditional seeds' / 'guarda y comparte...').
Generated by tool/gen_feature_graphic.py for en-US and es-ES.
2026-07-16 02:49:06 +02:00
fd6b9d5f0d 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
2026-07-16 02:42:47 +02:00
16 changed files with 548 additions and 112 deletions

51
.forgejo/workflows/ci.yml Normal file
View file

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

View file

@ -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 <<EOF
storeFile=$GITHUB_WORKSPACE/tane-upload.jks
storePassword=$TANE_KEYSTORE_PASSWORD
keyAlias=$TANE_KEY_ALIAS
keyPassword=$TANE_KEY_PASSWORD
EOF
- name: Build signed AAB + APK
working-directory: apps/app_seeds
run: |
flutter build appbundle --release
flutter build apk --release
- name: Install fastlane
working-directory: apps/app_seeds
run: |
apt-get update -qq
apt-get install -y -qq ruby ruby-dev build-essential
gem install bundler --no-document
bundle install
- name: Publish to Google Play (internal track)
working-directory: apps/app_seeds
env:
SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }}
run: bundle exec fastlane deploy_play
- name: Scrub signing material
if: always()
working-directory: apps/app_seeds
run: rm -f android/key.properties "$GITHUB_WORKSPACE/tane-upload.jks"

View file

@ -1,84 +0,0 @@
# CI for Tane. Mirrors the local verification gate (analyze + test +
# coverage) from docs/design/testing.md. Adapt to GitHub Actions if the
# canonical remote changes; the commands are the same.
#
# Uses the official Flutter Docker image. Pin the version to match the
# developer toolchain (Flutter 3.41.x / Dart 3.11.x).
image: ghcr.io/cirruslabs/flutter:3.41.9
stages:
- analyze
- test
- build
variables:
PUB_CACHE: "$CI_PROJECT_DIR/.pub-cache"
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .pub-cache/
before_script:
# SQLCipher so the "no plaintext at rest" security test actually runs (it
# skips where the library is absent). The -dev package ships the unversioned
# libsqlcipher.so symlink, which package:sqlite3 loads reliably.
- apt-get update -qq && apt-get install -y -qq libsqlcipher-dev
- flutter --version
- flutter pub get
analyze:
stage: analyze
script:
- dart format --output=none --set-exit-if-changed .
- flutter analyze
test:commons_core:
stage: test
script:
- cd packages/commons_core
- dart test
# Windows desktop build. Needs a Windows runner: on gitlab.com the SaaS
# `saas-windows-medium-amd64` shared runners (Windows Server 2022 with Visual
# Studio 2022 Build Tools — the C++ workload Flutter requires). Manual +
# allow_failure until the canonical remote actually has Windows runners.
build:windows:
stage: build
tags:
- saas-windows-medium-amd64
rules:
- when: manual
allow_failure: true
# The global Linux image is ignored on Windows shell runners; override the
# global before_script (apt-get) — this job runs in PowerShell.
before_script:
- git clone --depth 1 -b stable https://github.com/flutter/flutter.git "$env:CI_PROJECT_DIR\.flutter"
- $env:PATH = "$env:CI_PROJECT_DIR\.flutter\bin;$env:PATH"
- flutter --version
- flutter pub get
script:
- cd apps\app_seeds
- dart run slang
- dart run build_runner build --delete-conflicting-outputs
- flutter build windows --release
artifacts:
paths:
- apps/app_seeds/build/windows/x64/runner/Release/
test:app_seeds:
stage: test
script:
- cd apps/app_seeds
# slang is generated via its CLI (disabled in build_runner); Drift via build_runner.
- dart run slang
- dart run build_runner build --delete-conflicting-outputs
- flutter test --coverage
coverage: '/lines\.*: \d+\.\d+\%/'
artifacts:
paths:
- apps/app_seeds/coverage/lcov.info
reports:
coverage_report:
coverage_format: cobertura
path: apps/app_seeds/coverage/cobertura.xml

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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,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")

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
```
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/<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, 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).