From 76a99f0e77cb7840ada28047f21ef188e45a1be4 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 10:27:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(ci):=20build,=20teste=20et=20empaquette=20?= =?UTF-8?q?un=20APK=20Android=20sign=C3=A9=20sur=20chaque=20push=20main?= 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' }