Compare commits

..

No commits in common. "4cac56e3bb030e00ff06d1038edeed24557687d2" and "dc0d18562fe2f93ab6d6d1c6bbb908d81f18cde9" have entirely different histories.

16 changed files with 112 additions and 548 deletions

View file

@ -1,51 +0,0 @@
# 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

@ -1,78 +0,0 @@
# 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"

84
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,84 @@
# 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,10 +43,3 @@ 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

View file

@ -1,5 +0,0 @@
# 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

@ -1,9 +0,0 @@
# 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

@ -1,37 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,5 +1,5 @@
name: tane
description: "Tane — local-first, encrypted, decentralized traditional-seed inventory and market."
description: "Tane — local-first, encrypted, decentralized traditional-seed inventory."
publish_to: 'none'
version: 0.1.0+1

View file

@ -1,101 +0,0 @@
#!/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

@ -1,62 +0,0 @@
# 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

@ -1,84 +0,0 @@
# 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,21 +1,7 @@
# Releasing Tane
# Releasing Tane (manual, no CI yet)
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.
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.
## Versioning
@ -26,110 +12,38 @@ builds the signed AAB/APK and uploads to Play's **internal** track. No passwords
add a matching per-locale note under
`apps/app_seeds/fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt`.
## One-time setup
## Android signing (one-time)
### Android signing keystore (dedicated to Tane)
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`).
Tane uses its **own** upload keystore, separate from other Comunes apps (Ğ1nkgo,
Fuegos), kept in the shared Comunes signing directory:
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=…
```
```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)
## Build the beta by hand
```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 appbundle --release # AAB for Play
flutter build apk --release # APK for sideload / QA
flutter build apk --release # or: flutter build appbundle --release
```
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.
The APK lands at
`apps/app_seeds/build/app/outputs/flutter-apk/app-release.apk`.
## Store metadata
@ -161,7 +75,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 automated
## Not in this release
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).
CI, Play Store automation, web builds (SQLCipher is unavailable on web), and
the whole social layer (Block 2).