chore(release): beta packaging — signing, changelog, store metadata, scale test

- Android release signing reads a gitignored key.properties, falling back
  to debug keys so contributors can still build --release.
- CHANGELOG.md (human, newest-first) + Fastlane/F-Droid metadata (en/es)
  with descriptions derived from docs/intro.md.
- docs/release.md: the by-hand build + manual smoke checklist (no CI yet).
- inventory_scale_test guards the list query against N+1 at 3000 varieties.
This commit is contained in:
vjrj 2026-07-09 23:10:01 +02:00
parent 5d2b41a110
commit 4acf227e30
12 changed files with 260 additions and 4 deletions

32
CHANGELOG.md Normal file
View file

@ -0,0 +1,32 @@
# Changelog
All notable changes to Tanemaki are recorded here. Human-readable, newest
first. Dates are ISO (YYYY-MM-DD).
## [Unreleased] — Block 1 beta prep
Block 1 is the offline, encrypted seed inventory — useful with zero network.
### Added
- **Local sharing.** Mark a batch as to give away, to swap or for sale (gift
and swap first, sale never the default). A "what I share" filter and a
printable catalog (a paper bridge for fairs) surface it.
- **Sealed backups & recovery.** "Save a backup" writes a single encrypted
file; restoring on your own device is silent, and a copy from another
identity opens with the printed recovery code — which also brings your
identity back. A printable recovery sheet (QR + code) lives in Settings.
- **Full editing.** Lots and vernacular names can be edited and removed, not
just added.
- **Portuguese** interface, alongside Spanish and English; right-to-left
layouts verified.
### Changed
- "How much I have" is answered by the coarse abundance scale (plenty to
share … running low), which now nudges toward sharing when you have plenty.
A precise gram/seed-count field is deferred until network offers need it.
### Notes
- Everything is local-first and encrypted at rest (SQLCipher). No account, no
server, no tracking.
- The social layer (network offers, messaging, web of trust) is Block 2 and
intentionally not part of this release.

View file

@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@ -5,6 +8,17 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
// Release signing is opt-in: drop a `key.properties` (gitignored) next to this
// module's android/ dir to sign release builds with your own keystore. Without
// it, release builds fall back to debug keys so contributors can still run
// `flutter run --release`. See docs/release.md.
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
val hasReleaseKeystore = keystorePropertiesFile.exists()
if (hasReleaseKeystore) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "org.comunes.tane" namespace = "org.comunes.tane"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@ -20,7 +34,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "org.comunes.tane" applicationId = "org.comunes.tane"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
@ -30,11 +43,27 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
if (hasReleaseKeystore) {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // Sign with the real release keystore when key.properties is present,
// Signing with the debug keys for now, so `flutter run --release` works. // otherwise fall back to debug keys so `flutter run --release` works
signingConfig = signingConfigs.getByName("debug") // for contributors without the signing material.
signingConfig = if (hasReleaseKeystore) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
} }
} }
} }

View file

@ -0,0 +1,6 @@
First public beta of Block 1: the offline, encrypted seed inventory.
- Local sharing (give away / swap / sell) with a printable catalog.
- Encrypted backups and a printable recovery sheet.
- Edit and delete lots and vernacular names.
- Portuguese added; right-to-left layouts verified.

View file

@ -0,0 +1,27 @@
Tanemaki (種まき, "to sow seeds") is a local-first, decentralized app for
keeping and sharing traditional seeds.
Every traditional seed is a letter written by thousands of generations, passed
from hand to hand. Tanemaki helps people and seed-saver collectives keep a
friendly inventory of their bank, decide what they offer, and share it locally
— without a central intermediary that could control, censor, or be fined for
it.
MANAGE YOUR BANK
• Add a seed with just a photo and a name — no Latin required.
• Note the year, how much you have, where it came from, germination tests.
• Search and filter; snap a photo now and name it later.
YOURS, AND ONLY YOURS
• Works fully offline. No account, no server, no trackers.
• Everything is encrypted at rest on your device.
• Save an encrypted backup and keep a printed recovery sheet — restore your
whole bank on a new device.
SHARE, THE WAY IT'S ALWAYS BEEN DONE
• Mark what you have spare to give away, swap or sell.
• Print a catalog of what you share to take to a seed fair.
Tanemaki is free software (AGPL-3.0). No ads, no commissions, no business model
— it exists to support traditional varieties and push back against the seed
monopoly.

View file

@ -0,0 +1 @@
Your seed bank in your pocket — offline, encrypted, no account.

View file

@ -0,0 +1 @@
Tanemaki

View file

@ -0,0 +1,6 @@
Primera beta pública del Bloque 1: el inventario de semillas sin conexión y cifrado.
- Compartir localmente (regalar / intercambiar / vender) con catálogo imprimible.
- Copias cifradas y hoja de recuperación imprimible.
- Editar y borrar lotes y nombres vernáculos.
- Añadido portugués; disposiciones de derecha a izquierda verificadas.

View file

@ -0,0 +1,27 @@
Tanemaki (種まき, "sembrar semillas") es una app local-first y descentralizada
para guardar y compartir semillas tradicionales.
Cada semilla tradicional es una carta escrita por miles de generaciones, que
pasa de mano en mano. Tanemaki ayuda a personas y colectivos de guardianas de
semillas a llevar un inventario amable de su banco, decidir qué ofrecen y
compartirlo localmente, sin un intermediario central que pueda controlarlo,
censurarlo o ser multado por ello.
GESTIONA TU BANCO
• Añade una semilla con solo una foto y un nombre; sin latín.
• Anota el año, cuánta tienes, de dónde viene, pruebas de germinación.
• Busca y filtra; haz una foto ahora y ponle nombre después.
TUYO, Y SOLO TUYO
• Funciona totalmente sin conexión. Sin cuenta, sin servidor, sin rastreadores.
• Todo va cifrado en tu dispositivo.
• Guarda una copia cifrada y ten una hoja de recuperación impresa: recupera
todo tu banco en un dispositivo nuevo.
COMPARTIR, COMO SE HA HECHO SIEMPRE
• Marca lo que te sobra para regalar, intercambiar o vender.
• Imprime un catálogo de lo que compartes para llevar a una feria de semillas.
Tanemaki es software libre (AGPL-3.0). Sin anuncios, sin comisiones, sin modelo
de negocio: existe para apoyar las variedades tradicionales y plantar cara al
monopolio de las semillas.

View file

@ -0,0 +1 @@
Tu banco de semillas en el bolsillo: sin conexión, cifrado, sin cuenta.

View file

@ -0,0 +1 @@
Tanemaki

View file

@ -0,0 +1,54 @@
import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import '../support/test_support.dart';
/// A real seed bank can hold thousands of accessions. This guards that the
/// list query stays a handful of batched queries (not N+1) and returns in a
/// human blink even at that scale the pre-beta performance check.
void main() {
late AppDatabase db;
late VarietyRepository repo;
setUp(() {
db = newTestDatabase();
repo = newTestRepository(db);
});
tearDown(() => db.close());
test('the inventory view loads 3000 varieties quickly', () async {
const count = 3000;
for (var i = 0; i < count; i++) {
final id = await repo.addQuickVariety(
label: 'Variety $i',
category: i.isEven ? 'Poaceae' : 'Fabaceae',
);
// Every few varieties, add a lot so the batched lot/type/share queries
// do real work.
if (i % 5 == 0) {
await repo.addLot(
varietyId: id,
harvestYear: 2020 + (i % 5),
quantity: const Quantity(kind: QuantityKind.handful),
offerStatus: i % 25 == 0 ? OfferStatus.shared : OfferStatus.private,
);
}
}
final sw = Stopwatch()..start();
final view = await repo.watchInventoryView().first;
sw.stop();
expect(view.items, hasLength(count));
// Generous ceiling for CI; typical local runs are well under 500ms. The
// point is to catch an accidental N+1 regression, not to micro-benchmark.
expect(
sw.elapsedMilliseconds,
lessThan(3000),
reason: 'inventory view took ${sw.elapsedMilliseconds}ms for $count rows',
);
});
}

71
docs/release.md Normal file
View file

@ -0,0 +1,71 @@
# Releasing Tanemaki (manual, no CI yet)
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
- Semantic version in [`apps/app_seeds/pubspec.yaml`](../apps/app_seeds/pubspec.yaml)
as `MAJOR.MINOR.PATCH+BUILD` (e.g. `0.1.0+1`). Flutter maps `+BUILD` to
Android `versionCode` and the rest to `versionName`.
- Record every user-visible change in [`../CHANGELOG.md`](../CHANGELOG.md) and
add a matching per-locale note under
`apps/app_seeds/fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt`.
## Android signing (one-time)
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`).
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=…
```
## 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 apk --release # or: flutter build appbundle --release
```
The APK lands at
`apps/app_seeds/build/app/outputs/flutter-apk/app-release.apk`.
## Store metadata
F-Droid / Fastlane-style metadata lives under
`apps/app_seeds/fastlane/metadata/android/`. Descriptions are derived from
[`intro.md`](intro.md); icon and splash are generated by
`flutter_launcher_icons` / `flutter_native_splash` (see `pubspec.yaml`).
## Manual smoke test before shipping
Automated tests cover behaviour; a release build still gets a human pass on a
real device (the one place we allow manual testing — it validates the packaged
artifact, it does not replace tests):
1. Cold start → intro carousel → home.
2. Quick-add a seed with a photo; it appears in the list.
3. Mark a lot to share; the "I share" filter and printable catalog appear.
4. Save a backup, then restore it (and test the recovery sheet + code on a
second install).
5. Switch language (es / en / pt); check an RTL locale if the device offers one.
## Not in this release
CI, Play Store automation, web builds (SQLCipher is unavailable on web), and
the whole social layer (Block 2).