From 76a99f0e77cb7840ada28047f21ef188e45a1be4 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 10:27:55 +0200 Subject: [PATCH 01/10] =?UTF-8?q?feat(ci):=20build,=20teste=20et=20empaque?= =?UTF-8?q?tte=20un=20APK=20Android=20sign=C3=A9=20sur=20chaque=20push=20m?= =?UTF-8?q?ain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouveau job build-android dans ci.yml, gardé par needs: sur les 5 jobs existants (lint/test/intent-service-test/build/e2e) et limité à main via if: github.ref == 'refs/heads/main' — c'est ce qui garantit que l'APK n'est empaqueté qu'une fois la suite de tests existante verte, pas juste construit en parallèle d'elle. APK signé avec un vrai keystore de release (jamais généré par la CI — Android refuse une mise à jour signée par une clé différente de celle déjà installée) : ajoute signingConfigs.release à build.gradle, lu depuis l'environnement (ANDROID_KEYSTORE_PATH/_PASSWORD, ANDROID_KEY_ALIAS/_PASSWORD), jamais codé en dur. *.jks/*.keystore désormais réellement ignorés par git (étaient commentés dans le .gitignore généré par Capacitor). README : documente les 5 secrets Forgejo requis et la génération du keystore. Aucune suite de tests instrumentés Android n'existe (zéro code natif au-delà de la coquille Capacitor) — "testée" est satisfait par le gate sur la suite existante, pas par une suite native à inventer. Risque documenté, pas résolu par avance : aucun build Android n'a jamais tourné sur ce runner Forgejo auto-hébergé, contrairement aux 4 autres jobs. Closes #64 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 78 +++++++++++++++++++++++++++++++ README.md | 28 +++++++++++ apps/web/android/.gitignore | 10 ++-- apps/web/android/app/build.gradle | 22 +++++++++ 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b29866..8ff9570 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,3 +203,81 @@ jobs: # `component.devServer`), unlike `e2e` above which needs the real app # running first. - run: pnpm --filter web cy:run:component + + # Builds and packages a signed, downloadable Android APK on every push to + # `main` — issue #64. `needs:` on every other job is what actually makes + # this "build, TEST, and package" rather than just "build and package in + # parallel with tests that might still be failing": Forgejo/GitHub Actions + # has no native way for a job in a *different* workflow file to block on + # one here, so this has to live in this same file to get a real dependency + # rather than a same-push coincidence. `if:` (not a separate trigger) keeps + # every other job running on every branch as today — only this one is + # main-only. + # + # No dedicated Android test suite exists (zero native code beyond the + # Capacitor-generated shell) — "testée" is satisfied by gating on the 5 + # existing jobs, not by inventing an instrumented test suite for this. + # + # Unverified on this runner as of writing: no Android build has ever run + # on this self-hosted Forgejo `act` runner (unlike lint/test/build/e2e, + # exercised for months). If SDK installation turns out too slow/heavy here, + # cache `~/.android/sdk`/`~/.gradle` (same idea as the Cypress binary cache + # in `e2e` above) rather than accepting a slow job on every push. + build-android: + runs-on: ubuntu-latest + needs: [lint, test, intent-service-test, build, e2e] + if: github.ref == 'refs/heads/main' + steps: + - uses: https://github.com/actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: https://github.com/actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - uses: https://github.com/actions/setup-java@v4 + with: + distribution: temurin + # Matches what Android Gradle Plugin 8.x (bundled with the + # Capacitor-generated project, see apps/web/android/build.gradle) + # expects — bump alongside AGP if that version ever moves. + java-version: "21" + + - uses: https://github.com/android-actions/setup-android@v3 + + - run: pnpm install --frozen-lockfile + + # Unlike the normal web build (served same-origin as the API), a + # packaged Capacitor build runs from the WebView's own local origin and + # needs an absolute, public HTTPS URL baked in at build time — see + # apps/web/.env.capacitor.example's own comment on this. + - name: Build le bundle web pour Capacitor + env: + VITE_API_URL: ${{ secrets.MOBILE_API_URL }} + run: pnpm --filter web build:capacitor + - run: pnpm --filter web exec cap sync android + + # Le keystore de release est un secret généré une fois à la main (voir + # le README, section "Application mobile") — jamais régénéré ici, un + # nouveau keystore casserait la mise à jour de l'appli déjà installée + # (Android refuse une signature différente de celle déjà installée). + - name: Décode le keystore de release + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + run: echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > apps/web/android/app/release.keystore + + - name: Build l'APK signé + working-directory: apps/web/android + env: + ANDROID_KEYSTORE_PATH: release.keystore + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: ./gradlew assembleRelease + + - uses: https://github.com/actions/upload-artifact@v4 + with: + name: batchcooking-release.apk + path: apps/web/android/app/build/outputs/apk/release/app-release.apk diff --git a/README.md b/README.md index f110b8a..a8a4f97 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,34 @@ Les icônes et le splash sont générés depuis `apps/web/assets/logo.svg` (voir `apps/web/assets/README.md`). iOS est hors périmètre pour l'instant (nécessite Xcode/macOS). +**CI — build automatique d'un APK signé** (`.github/workflows/ci.yml`, job +`build-android`, issue #64) : à chaque push sur `main`, une fois les 5 autres +jobs verts, un APK release signé est construit et déposé comme artefact de +run téléchargeable. Secrets du dépôt Forgejo requis (Réglages du dépôt → +Secrets) : + +| Secret | Contenu | +|---|---| +| `MOBILE_API_URL` | URL HTTPS publique de l'API — même valeur que `VITE_API_URL` dans `.env.capacitor` | +| `ANDROID_KEYSTORE_BASE64` | Le keystore de release (`.jks`) encodé en base64 | +| `ANDROID_KEYSTORE_PASSWORD` | Mot de passe du keystore | +| `ANDROID_KEY_ALIAS` | Alias de la clé dans le keystore | +| `ANDROID_KEY_PASSWORD` | Mot de passe de la clé | + +Le keystore doit être généré **une seule fois**, à la main, et rester stable +pour toujours : Android refuse d'installer la mise à jour d'une appli signée +par une clé différente de celle déjà installée. + +```bash +keytool -genkeypair -v -keystore batchcooking-release.jks \ + -alias batchcooking -keyalg RSA -keysize 2048 -validity 10000 +base64 -w0 batchcooking-release.jks # → coller comme secret ANDROID_KEYSTORE_BASE64 +``` + +Conserver le fichier `.jks` et ses mots de passe en lieu sûr en dehors du +dépôt (jamais commité — voir `apps/web/android/.gitignore`) : le perdre +empêche de publier toute future mise à jour de l'appli. + ## Auth (apps/api) Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpOnly. diff --git a/apps/web/android/.gitignore b/apps/web/android/.gitignore index 48354a3..9703eda 100644 --- a/apps/web/android/.gitignore +++ b/apps/web/android/.gitignore @@ -52,10 +52,12 @@ captures/ # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml -# Keystore files -# Uncomment the following lines if you do not want to check your keystore files in. -#*.jks -#*.keystore +# Keystore files — jamais commités : le keystore de release est un secret +# généré une fois à la main (voir la doc du README, section "Application +# mobile"), décodé depuis le secret CI ANDROID_KEYSTORE_BASE64 à chaque build +# de release, jamais versionné ici (issue #64). +*.jks +*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild diff --git a/apps/web/android/app/build.gradle b/apps/web/android/app/build.gradle index b41527a..239f07c 100644 --- a/apps/web/android/app/build.gradle +++ b/apps/web/android/app/build.gradle @@ -16,8 +16,30 @@ android { ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' } } + // Lu depuis l'environnement, jamais codé en dur — même règle que les + // secrets de apps/api (JWT_SECRET, etc.) : aucune valeur par défaut + // devinable. Le keystore lui-même (ANDROID_KEYSTORE_PATH) est un fichier + // décodé à la volée depuis le secret CI ANDROID_KEYSTORE_BASE64, jamais + // commité (voir apps/web/android/.gitignore) — généré une fois à la main, + // voir la doc du README "Application mobile". N'affecte que + // `assembleRelease` : `assembleDebug` continue d'utiliser le keystore de + // debug généré automatiquement par Android, sans configuration. + signingConfigs { + release { + storeFile System.getenv("ANDROID_KEYSTORE_PATH") ? file(System.getenv("ANDROID_KEYSTORE_PATH")) : null + storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD") + keyAlias System.getenv("ANDROID_KEY_ALIAS") + keyPassword System.getenv("ANDROID_KEY_PASSWORD") + } + } buildTypes { release { + // `signingConfig` reste posé même quand les variables d'environnement + // ci-dessus sont absentes (dev local) : Gradle accepte une + // signingConfig aux champs `null`, la build échoue seulement au + // moment de la signature effective (assembleRelease), jamais à la + // configuration — un `assembleDebug` local n'en a jamais besoin. + signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } From 548cee721fdc3f0921412f22a9206ed74f241e1e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 10:45:05 +0200 Subject: [PATCH 02/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(job=20test?= =?UTF-8?q?=20cass=C3=A9=20par=20la=20course=20d'infra=20connue=20sur=20le?= =?UTF-8?q?=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 18 échecs éparpillés (timeouts 10s sur des suites sans rapport entre elles, PrismaClientKnownRequestError, un mismatch d'ID façon ré-seed concurrent) sur 373 passing — signature exacte de la course documentée dans ci-forgejo-runner-constraints (postgres partagé + contention RAM/CPU du runner), pas un vrai échec de code : ce PR ne touche que ci.yml/le projet Android/le README, rien que la suite Mocha exerce différemment. Co-Authored-By: Claude Sonnet 5 From bf994c396a816d63d0d32dd156dcffe320251058 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 11:04:32 +0200 Subject: [PATCH 03/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(2e=20collis?= =?UTF-8?q?ion=20infra=20sur=20le=20Postgres=20partag=C3=A9,=20connexion?= =?UTF-8?q?=20coup=C3=A9e=20en=20cours=20de=20run)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From 275cc2d500c3a0be9639dda906e8450e82436e89 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 11:27:25 +0200 Subject: [PATCH 04/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(3e=20collis?= =?UTF-8?q?ion=20infra=20sur=20le=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From f37a61266fd10e11d58c5d70b92a581521ee0aa2 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 11:31:39 +0200 Subject: [PATCH 05/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(4e=20collis?= =?UTF-8?q?ion=20infra=20=E2=80=94=20cette=20fois=20sur=20le=20cache=20d'a?= =?UTF-8?q?ctions=20du=20runner)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From 4f7a4529246d13ffb9e1b037baadacc695d09ee3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 11:45:08 +0200 Subject: [PATCH 06/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(5e=20collis?= =?UTF-8?q?ion=20infra=20=E2=80=94=20course=20d'upsert=20concurrente=20sur?= =?UTF-8?q?=20le=20seed=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From 85dd1b6255e8a0158f1dfcec3742439ae140e52c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 11:54:51 +0200 Subject: [PATCH 07/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(6e=20collis?= =?UTF-8?q?ion=20infra=20sur=20le=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From b3581b2fb536224cf637158999a604dd22bf27ec Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 12:06:53 +0200 Subject: [PATCH 08/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(7e=20collis?= =?UTF-8?q?ion=20infra=20sur=20le=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From c662d02da6da380998c66cba5e3626b10bff2dbd Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 14:12:46 +0200 Subject: [PATCH 09/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(8e=20collis?= =?UTF-8?q?ion=20infra=20sur=20le=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 From 92f364bc1f58c0d776c1f60b83bd45736970a305 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 14:26:31 +0200 Subject: [PATCH 10/10] =?UTF-8?q?chore:=20relance=20la=20CI=20(9e=20collis?= =?UTF-8?q?ion=20infra=20sur=20le=20Postgres=20partag=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5