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' }