Republish setup: fastlane + Forgejo CI, signing/version fixes, Play-policy link
Some checks failed
ci / analyze (push) Failing after 2m3s
ci / test (push) Failing after 1m26s
release / android (push) Failing after 5m51s

- support_page: repoint comunes.org link to project page (Play re-approval)
- build.gradle: conditional release signing (debug fallback), versionCode/Name
  from pubspec, drop duplicate google-services plugin
- pubspec: version 1.10.0+10 (versionCode must exceed last uploaded)
- track AndroidManifest.xml (no secrets in it)
- fastlane: Appfile/Fastfile (deploy_play/deploy_metadata/promote_production) +
  Gemfile + es/en/gl store metadata
- .forgejo: ci.yml (analyze+test) + release.yml (tag v* -> signed AAB -> Play internal)
- README + RELEASE.md
This commit is contained in:
vjrj 2026-07-16 20:49:28 +02:00
parent cea395007d
commit 4461481668
24 changed files with 446 additions and 15 deletions

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

@ -0,0 +1,54 @@
# Per-push gate for git.comunes.org (Forgejo Actions): analyze + tests.
# Flutter pinned to the developer toolchain (3.41.9).
#
# NOTE: `runs-on` must match a label your Forgejo runner registered with.
# We check out with plain git (a `run:` step) instead of actions/checkout@v4:
# the cirruslabs/flutter image has no Node.js, and JS-based actions need it
# ("exec: node: not found"). Manual checkout keeps the workflow Node-free.
name: ci
on:
push:
branches: ['**']
pull_request:
jobs:
analyze:
runs-on: docker
container:
image: ghcr.io/cirruslabs/flutter:3.41.9
steps:
- name: Checkout
env:
TOKEN: ${{ github.token }}
run: |
git config --global --add safe.directory '*'
git init -q .
git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git"
git fetch -q --depth 1 origin "${GITHUB_SHA}"
git checkout -q FETCH_HEAD
- run: flutter pub get
- name: Generate code (json_serializable)
run: dart run build_runner build --delete-conflicting-outputs
- run: flutter analyze
test:
runs-on: docker
container:
image: ghcr.io/cirruslabs/flutter:3.41.9
steps:
- name: Checkout
env:
TOKEN: ${{ github.token }}
run: |
git config --global --add safe.directory '*'
git init -q .
git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git"
git fetch -q --depth 1 origin "${GITHUB_SHA}"
git checkout -q FETCH_HEAD
- run: flutter pub get
- name: Generate code + test
run: |
dart run build_runner build --delete-conflicting-outputs
flutter test

View file

@ -0,0 +1,91 @@
# Release automation for git.comunes.org (Forgejo Actions).
#
# Password-free: pushing a tag `v*` builds a signed AAB and uploads it to
# Google Play's internal track via fastlane. All credentials come from repo
# secrets (Settings > Actions > Secrets), never typed:
# FIRES_KEYSTORE_BASE64 base64 of the org.comunes.fires upload keystore
# FIRES_KEYSTORE_PASSWORD store password
# FIRES_KEY_ALIAS key alias
# FIRES_KEY_PASSWORD key password
# SUPPLY_JSON_KEY_DATA Google Play service-account JSON (shared Comunes)
# FIRES_PRIVATE_SETTINGS_JSON contents of assets/private-settings.json
# FIRES_GOOGLE_SERVICES_JSON base64 of android/app/google-services.json
#
# 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.
name: release
on:
push:
tags:
- 'v*'
jobs:
android:
runs-on: docker
container:
image: ghcr.io/cirruslabs/flutter:3.41.9
steps:
# Manual git checkout (no Node): the flutter image has no node, so JS
# actions like actions/checkout@v4 fail with "exec: node: not found".
- name: Checkout
env:
TOKEN: ${{ github.token }}
run: |
git config --global --add safe.directory '*'
git init -q .
git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git"
git fetch -q --depth 1 origin "${GITHUB_SHA}"
git checkout -q FETCH_HEAD
- name: Materialize runtime secrets (Firebase + private settings)
env:
FIRES_GOOGLE_SERVICES_JSON: ${{ secrets.FIRES_GOOGLE_SERVICES_JSON }}
FIRES_PRIVATE_SETTINGS_JSON: ${{ secrets.FIRES_PRIVATE_SETTINGS_JSON }}
run: |
echo "$FIRES_GOOGLE_SERVICES_JSON" | base64 -d > android/app/google-services.json
printf '%s' "$FIRES_PRIVATE_SETTINGS_JSON" > assets/private-settings.json
- name: Materialize signing material from secrets
env:
FIRES_KEYSTORE_BASE64: ${{ secrets.FIRES_KEYSTORE_BASE64 }}
FIRES_KEYSTORE_PASSWORD: ${{ secrets.FIRES_KEYSTORE_PASSWORD }}
FIRES_KEY_ALIAS: ${{ secrets.FIRES_KEY_ALIAS }}
FIRES_KEY_PASSWORD: ${{ secrets.FIRES_KEY_PASSWORD }}
run: |
echo "$FIRES_KEYSTORE_BASE64" | base64 -d > "$GITHUB_WORKSPACE/fires-upload.jks"
cat > android/key.properties <<EOF
storeFile=$GITHUB_WORKSPACE/fires-upload.jks
storePassword=$FIRES_KEYSTORE_PASSWORD
keyAlias=$FIRES_KEY_ALIAS
keyPassword=$FIRES_KEY_PASSWORD
EOF
- name: Generate code (json_serializable)
run: |
flutter pub get
dart run build_runner build --delete-conflicting-outputs
- name: Build signed AAB + APK (production flavor)
run: |
flutter build appbundle --release --flavor production -t lib/main_prod.dart
flutter build apk --release --flavor production -t lib/main_prod.dart
- name: Install fastlane
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)
env:
SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }}
run: bundle exec fastlane deploy_play
- name: Scrub secrets
if: always()
run: |
rm -f android/key.properties "$GITHUB_WORKSPACE/fires-upload.jks"
rm -f android/app/google-services.json assets/private-settings.json

1
.gitignore vendored
View file

@ -21,7 +21,6 @@ pubspec.lock
doc/api/ doc/api/
assets/private-settings.json assets/private-settings.json
android/app/src/main/AndroidManifest.xml
.flutter-plugins .flutter-plugins
android/app/src/main/gen/ android/app/src/main/gen/

5
Gemfile Normal file
View file

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

View file

@ -22,14 +22,21 @@ also you can run with `watch` instead of `build` to build with any code change.
Generate apk with: Generate apk with:
``` ```
flutter build apk -t lib/mainProd.dart --flavor production flutter build apk --release -t lib/main_prod.dart --flavor production
``` ```
also you can run with `-t lib/mainDev.dart --flavor development`. More info about flavors [here](https://medium.com/@salvatoregiordanoo/flavoring-flutter-392aaa875f36). also you can run with `-t lib/main_dev.dart --flavor development`. More info about flavors [here](https://medium.com/@salvatoregiordanoo/flavoring-flutter-392aaa875f36).
## Testing ## Testing
Run `flutter test` for doing unit testing. Run `flutter test` for doing unit testing.
## Release
Publishing to Google Play is automated and password-free via Forgejo Actions on
`git.comunes.org`. See [RELEASE.md](RELEASE.md). TL;DR: bump `version:` in
`pubspec.yaml`, then `git tag vX.Y && git push origin vX.Y` — CI builds a signed
AAB and uploads it to the Play *internal* track.
## Data source acknowledgements ## Data source acknowledgements
*We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*. *We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*.

85
RELEASE.md Normal file
View file

@ -0,0 +1,85 @@
# Releasing Tod@s contra el Fuego
Publishing to Google Play is **automated and password-free**: pushing a signed
git tag to `git.comunes.org` triggers Forgejo Actions, which builds a signed AAB
and uploads it to Play's **internal** track. Modeled on the `tane` pipeline.
## TL;DR — cut a release
```bash
# bump version in pubspec.yaml (version: X.Y.Z+CODE), add changelogs/<CODE>.txt
git tag v1.10 && git push origin v1.10
```
[`.forgejo/workflows/release.yml`](.forgejo/workflows/release.yml) builds the
signed AAB/APK (production flavor, `lib/main_prod.dart`) and uploads via
`fastlane deploy_play`. No passwords are typed.
## Versioning
- `version:` in [`pubspec.yaml`](pubspec.yaml) as `MAJOR.MINOR.PATCH+CODE`.
Flutter maps `+CODE` to Android `versionCode` (read via `flutter.versionCode`
in `android/app/build.gradle`) and the rest to `versionName`.
- **`versionCode` must be strictly greater than the highest ever uploaded** to
the `org.comunes.fires` listing. This re-launch starts at `+10` (was 9).
- Add a per-locale note under
`fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt`.
## One-time setup
### Signing keystore
The app already exists in Play with **Play App Signing**, so releases MUST be
signed with the **existing upload key** that signed `org.comunes.fires` before
(`android/app/key.jks` → shared Comunes signing dir). Do **not** generate a new
one. Get the alias with:
```bash
keytool -list -v -keystore android/app/key.jks
```
Local signed builds read a **gitignored** `android/key.properties`:
```properties
storeFile=/abs/path/to/key.jks
storePassword=…
keyAlias=…
keyPassword=…
```
Without `key.properties`, release builds fall back to **debug** signing so
contributors/CI can still build.
### CI secrets (Forgejo → repo → Settings → Actions → Secrets)
| Secret | Contents |
|---|---|
| `FIRES_KEYSTORE_BASE64` | `base64 -w0 android/app/key.jks` |
| `FIRES_KEYSTORE_PASSWORD` / `FIRES_KEY_ALIAS` / `FIRES_KEY_PASSWORD` | keystore credentials |
| `SUPPLY_JSON_KEY_DATA` | Google Play service-account JSON (shared Comunes account) |
| `FIRES_PRIVATE_SETTINGS_JSON` | raw contents of `assets/private-settings.json` |
| `FIRES_GOOGLE_SERVICES_JSON` | `base64 -w0 android/app/google-services.json` |
### First upload
The app was removed by Google and an update was rejected. Before the API accepts
uploads: resolve any enforcement flag in the Play Console (appeal / re-submit),
and confirm the flagged content is addressed (the `comunes.org` support link was
repointed to the project page — see `lib/support_page.dart`). After the listing
accepts a build again, `fastlane deploy_play` is fully automated.
## Manual / local fallback
```bash
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter build appbundle --release --flavor production -t lib/main_prod.dart
# AAB: build/app/outputs/bundle/productionRelease/app-production-release.aab
SUPPLY_JSON_KEY_DATA="$(cat play-service-account.json)" bundle exec fastlane deploy_play
```
Promote internal → production (reuses the reviewed AAB, no rebuild):
```bash
bundle exec fastlane promote_production
```

View file

@ -12,9 +12,14 @@ if (localPropertiesFile.exists()) {
} }
} }
// Release signing is opt-in: only load key.properties if it exists. Without it
// (CI analyze/test jobs, contributors) builds fall back to debug signing.
def keystorePropertiesFile = rootProject.file("key.properties") def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) def hasReleaseKeystore = keystorePropertiesFile.exists()
if (hasReleaseKeystore) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
compileSdkVersion 36 compileSdkVersion 36
@ -29,23 +34,28 @@ android {
applicationId "org.comunes.fires" applicationId "org.comunes.fires"
minSdkVersion flutter.minSdkVersion minSdkVersion flutter.minSdkVersion
targetSdkVersion 36 targetSdkVersion 36
versionCode 9 // versionCode/versionName come from pubspec.yaml (version: X.Y.Z+CODE)
versionName "1.9" // so a `v*` git tag maps the release version, matching the tane flow.
versionCode flutter.versionCode
versionName flutter.versionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
signingConfigs { signingConfigs {
release { if (hasReleaseKeystore) {
keyAlias keystoreProperties['keyAlias'] release {
keyPassword keystoreProperties['keyPassword'] keyAlias keystoreProperties['keyAlias']
storeFile file(keystoreProperties['storeFile']) keyPassword keystoreProperties['keyPassword']
storePassword keystoreProperties['storePassword'] storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
} }
} }
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release // Sign with the release key when available, else debug (contributors/CI).
signingConfig hasReleaseKeystore ? signingConfigs.release : signingConfigs.debug
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
@ -83,5 +93,3 @@ dependencies {
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.gms.google-services'

View file

@ -0,0 +1,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.comunes.fires">
<!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:label="@string/app_name"
android:icon="@mipmap/launch_image"
android:enableOnBackInvokedCallback="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<!-- <meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" /> -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

9
fastlane/Appfile Normal file
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.fires")
json_key_data_raw(ENV["SUPPLY_JSON_KEY_DATA"]) if ENV["SUPPLY_JSON_KEY_DATA"]

54
fastlane/Fastfile Normal file
View file

@ -0,0 +1,54 @@
# Fastlane lanes for Tod@s contra el Fuego (org.comunes.fires).
#
# 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>/.
default_platform(:android)
# Release bundle produced by `flutter build appbundle --release --flavor
# production`, relative to the project root (where fastlane runs).
AAB_PATH = "build/app/outputs/bundle/productionRelease/app-production-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
# Promote the release currently on the internal track to production, reusing
# the SAME already-reviewed AAB (no rebuild, no new versionCode). Requires the
# service account to have "Release to production" permission in Play Console.
# Usage: bundle exec fastlane promote_production
desc "Promote the internal-track release to production (no rebuild)"
lane :promote_production do
supply(
track: "internal",
track_promote_to: "production",
skip_upload_aab: true,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
end
end

View file

@ -0,0 +1,2 @@
All Against The Fire! is back. Upgraded to a recent Flutter, stability
improvements and code cleanup. Fire alerts in your area based on NASA FIRMS data.

View file

@ -0,0 +1,17 @@
All Against The Fire! notifies you about fires detected in an area of your
interest. It helps the early detection of fires and facilitates local
mobilization while the professional extinction services arrive.
HOW IT WORKS
• Pick the area you want to watch.
• Get notified when a heat spot is detected in that area.
• See active fires on the map, with their location and time.
• Share an alert to mobilise people around you.
DATA
Heat spots come from data and imagery from LANCE FIRMS operated by the
NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding
provided by NASA/HQ.
Free software (GNU AGPL-3.0). No ads, non-profit: a community tool to prevent
and respond to wildfires.

View file

@ -0,0 +1 @@
Fire alerts in your area for early detection and local response.

View file

@ -0,0 +1 @@
All Against The Fire!

View file

@ -0,0 +1,3 @@
Vuelve ¡Tod@s contra el Fuego! Actualización a Flutter reciente, mejoras de
estabilidad y limpieza de código. Avisos de incendios en tu zona basados en
datos de NASA FIRMS.

View file

@ -0,0 +1,17 @@
¡Tod@s contra el Fuego! te avisa de los incendios detectados en una zona de tu
interés. Ayuda a la detección temprana de incendios y facilita la movilización
local mientras llegan los servicios profesionales de extinción.
CÓMO FUNCIONA
• Elige la zona que quieres vigilar.
• Recibe notificaciones cuando se detecta un foco de calor en esa zona.
• Consulta los incendios activos sobre el mapa, con su ubicación y hora.
• Comparte un aviso para movilizar a la gente de tu entorno.
DATOS
Los focos de calor proceden de datos e imágenes de LANCE FIRMS, operado por el
Sistema de Datos e Información de Ciencias de la Tierra (ESDIS) de NASA/GSFC,
con financiación de NASA/HQ.
Software libre (GNU AGPL-3.0). Sin anuncios y sin ánimo de lucro: una
herramienta comunitaria para prevenir y responder a los incendios.

View file

@ -0,0 +1 @@
Avisos de incendios en tu zona para la detección temprana y la respuesta local.

View file

@ -0,0 +1 @@
¡Tod@s contra el Fuego!

View file

@ -0,0 +1,3 @@
Volve Tod@s contra o Lume! Actualización a Flutter recente, melloras de
estabilidade e limpeza de código. Avisos de lumes na túa zona baseados en datos
de NASA FIRMS.

View file

@ -0,0 +1,17 @@
Tod@s contra o Lume! avísache dos lumes detectados nunha zona do teu interese.
Axuda á detección temperá de incendios e facilita a mobilización local mentres
chegan os servizos profesionais de extinción.
COMO FUNCIONA
• Escolle a zona que queres vixiar.
• Recibe notificacións cando se detecta un foco de calor nesa zona.
• Consulta os lumes activos sobre o mapa, coa súa localización e hora.
• Comparte un aviso para mobilizar á xente do teu contorno.
DATOS
Os focos de calor proceden de datos e imaxes de LANCE FIRMS, operado polo
Sistema de Datos e Información de Ciencias da Terra (ESDIS) de NASA/GSFC, con
financiamento de NASA/HQ.
Software libre (GNU AGPL-3.0). Sen anuncios e sen ánimo de lucro: unha
ferramenta comunitaria para previr e responder aos incendios.

View file

@ -0,0 +1 @@
Avisos de lumes na túa zona para a detección temperá e a resposta local.

View file

@ -0,0 +1 @@
Tod@s contra o Lume!

View file

@ -26,7 +26,11 @@ class _SupportPageState extends State<SupportPage> {
icon: const Icon(Icons.favorite_border), icon: const Icon(Icons.favorite_border),
label: Text(S.of(context).comunesSupportBtn), label: Text(S.of(context).comunesSupportBtn),
onPressed: () async { onPressed: () async {
final Uri url = Uri.parse('https://comunes.org/'); // Play policy: la app fue retirada por enlazar a comunes.org (que
// tiene página de donaciones). Apuntamos a la página del proyecto
// (sin donaciones) para la re-aprobación. Reversible una vez aprobada.
final Uri url = Uri.parse(
'https://git.comunes.org/comunes/todos-contra-el-fuego-mobile');
if (await canLaunchUrl(url)) { if (await canLaunchUrl(url)) {
await launchUrl(url); await launchUrl(url);
} }

View file

@ -1,5 +1,8 @@
name: fires_flutter name: fires_flutter
description: All Against Fire description: All Against Fire
# MAJOR.MINOR.PATCH+versionCode — Flutter maps +CODE to Android versionCode.
# Must be strictly greater than the highest versionCode ever uploaded to Play.
version: 1.10.0+10
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"