diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8dd63c157a..3c1d69f153 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -359,16 +359,28 @@ jobs: packages/desktop/dist-electron/latest-linux.yml - # ── Build Android APK artifact ─────────────────────────────────────── + # ── Build Android APK/AAB artifacts ────────────────────────────────── # FNXC:Release 2026-06-25-12:00: # Android release assets used to be limited to the manual mobile workflow's # short-lived CI artifacts. Tagged binary releases now build the Capacitor # Android shell in this workflow so the public GitHub Release includes a # stable APK and checksum beside desktop and CLI binaries. + # FNXC:Release 2026-06-25-18:10: + # Android signing is optional and secret-gated on ANDROID_KEYSTORE_BASE64, + # ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, and ANDROID_KEY_PASSWORD. + # The Capacitor Android project is gitignored/regenerated, so CI injects + # signing with android.injected.signing.* Gradle properties instead of + # committing native build.gradle edits. When the keystore is absent, keep the + # FN-7014 unsigned debug APK fallback; Play Store upload remains out of scope + # and is tracked separately from sideload release artifacts. build-android: - name: Build Android APK + name: Build Android APK/AAB runs-on: ubuntu-latest timeout-minutes: 30 + # Job-level env mirrors the desktop signing pattern: step `if:` conditions + # can inspect env values, but cannot read secrets.* directly. + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} steps: - name: Checkout @@ -407,18 +419,67 @@ jobs: fi pnpm --filter @fusion/mobile cap sync android - - name: Build Android APK + - name: Decode Android signing keystore + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + run: | + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/fusion-release.keystore" + + - name: Build signed Android release APK and AAB + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + env: + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + cd packages/mobile/android + chmod +x gradlew + ./gradlew assembleRelease bundleRelease \ + -Pandroid.injected.signing.store.file="$RUNNER_TEMP/fusion-release.keystore" \ + -Pandroid.injected.signing.store.password="$ANDROID_KEYSTORE_PASSWORD" \ + -Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS" \ + -Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD" + + - name: Normalize signed Android release assets + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + run: | + APK="packages/mobile/android/app/build/outputs/apk/release/app-release.apk" + AAB="packages/mobile/android/app/build/outputs/bundle/release/app-release.aab" + if [ ! -f "$APK" ]; then + echo "::error::Expected signed Android APK missing at $APK" >&2 + exit 1 + fi + if [ ! -f "$AAB" ]; then + echo "::error::Expected signed Android AAB missing at $AAB" >&2 + exit 1 + fi + mkdir -p packages/mobile/dist + cp "$APK" packages/mobile/dist/fusion-android-release.apk + cp "$AAB" packages/mobile/dist/fusion-android-release.aab + + - name: Verify signed Android APK signature + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + run: | + APK="packages/mobile/dist/fusion-android-release.apk" + APKSIGNER="" + if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT/build-tools" ]; then + APKSIGNER=$(find "$ANDROID_SDK_ROOT/build-tools" -maxdepth 2 -type f -name apksigner | sort -V | tail -n 1 || true) + fi + if [ -n "$APKSIGNER" ]; then + "$APKSIGNER" verify --verbose "$APK" + else + jarsigner -verify -strict "$APK" + fi + + - name: Build unsigned Android debug APK + if: ${{ env.ANDROID_KEYSTORE_BASE64 == '' }} run: | cd packages/mobile/android chmod +x gradlew ./gradlew assembleDebug - - name: Normalize Android APK asset + - name: Normalize unsigned Android APK asset + if: ${{ env.ANDROID_KEYSTORE_BASE64 == '' }} run: | - # FNXC:Release 2026-06-25-12:00: - # Ship the secret-free debug APK because this repo has no Android - # signing keystore configured; signed release APK/AAB distribution is a - # separate product task, not a binary-release plumbing prerequisite. APK="packages/mobile/android/app/build/outputs/apk/debug/app-debug.apk" if [ ! -f "$APK" ]; then echo "::error::Expected Android APK missing at $APK" >&2 @@ -427,18 +488,23 @@ jobs: mkdir -p packages/mobile/dist cp "$APK" packages/mobile/dist/fusion-android.apk - - name: Generate Android APK checksum + - name: Generate Android artifact checksums run: | cd packages/mobile/dist - sha256sum fusion-android.apk > fusion-android.apk.sha256 + for file in fusion-android*.apk fusion-android-release.aab; do + [ -f "$file" ] || continue + sha256sum "$file" > "$file.sha256" + done - - name: Upload Android APK artifact + - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: fusion-android-apk path: | - packages/mobile/dist/fusion-android.apk - packages/mobile/dist/fusion-android.apk.sha256 + packages/mobile/dist/fusion-android*.apk + packages/mobile/dist/fusion-android*.apk.sha256 + packages/mobile/dist/fusion-android-release.aab + packages/mobile/dist/fusion-android-release.aab.sha256 # ── Create GitHub Release ───────────────────────────────────────────── github-release: @@ -476,7 +542,7 @@ jobs: id: collect run: | mkdir release-files - find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -exec cp {} release-files/ \; + find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.aab" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -exec cp {} release-files/ \; ls -la release-files/ count=$(find release-files -type f | wc -l | tr -d ' ') echo "count=$count" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index 1acd26e0be..a314be59a1 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -346,15 +346,27 @@ jobs: packages/desktop/dist-electron/latest-linux.yml - # ── Build Android APK artifact ─────────────────────────────────────── + # ── Build Android APK/AAB artifacts ────────────────────────────────── # FNXC:Release 2026-06-25-12:00: # Keep the tag-less rehearsal workflow in parity with release.yml so APK # generation, checksum output, and artifact collection are validated before a # version tag tries to publish the Android asset publicly. + # FNXC:Release 2026-06-25-18:10: + # Android signing is optional and secret-gated on ANDROID_KEYSTORE_BASE64, + # ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, and ANDROID_KEY_PASSWORD. + # The Capacitor Android project is gitignored/regenerated, so CI injects + # signing with android.injected.signing.* Gradle properties instead of + # committing native build.gradle edits. When the keystore is absent, keep the + # FN-7014 unsigned debug APK fallback; Play Store upload remains out of scope + # and is tracked separately from sideload release artifacts. build-android: - name: Build Android APK + name: Build Android APK/AAB runs-on: ubuntu-latest timeout-minutes: 30 + # Job-level env mirrors the desktop signing pattern: step `if:` conditions + # can inspect env values, but cannot read secrets.* directly. + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} steps: - name: Checkout @@ -393,18 +405,67 @@ jobs: fi pnpm --filter @fusion/mobile cap sync android - - name: Build Android APK + - name: Decode Android signing keystore + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + run: | + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/fusion-release.keystore" + + - name: Build signed Android release APK and AAB + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + env: + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + cd packages/mobile/android + chmod +x gradlew + ./gradlew assembleRelease bundleRelease \ + -Pandroid.injected.signing.store.file="$RUNNER_TEMP/fusion-release.keystore" \ + -Pandroid.injected.signing.store.password="$ANDROID_KEYSTORE_PASSWORD" \ + -Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS" \ + -Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD" + + - name: Normalize signed Android release assets + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + run: | + APK="packages/mobile/android/app/build/outputs/apk/release/app-release.apk" + AAB="packages/mobile/android/app/build/outputs/bundle/release/app-release.aab" + if [ ! -f "$APK" ]; then + echo "::error::Expected signed Android APK missing at $APK" >&2 + exit 1 + fi + if [ ! -f "$AAB" ]; then + echo "::error::Expected signed Android AAB missing at $AAB" >&2 + exit 1 + fi + mkdir -p packages/mobile/dist + cp "$APK" packages/mobile/dist/fusion-android-release.apk + cp "$AAB" packages/mobile/dist/fusion-android-release.aab + + - name: Verify signed Android APK signature + if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + run: | + APK="packages/mobile/dist/fusion-android-release.apk" + APKSIGNER="" + if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT/build-tools" ]; then + APKSIGNER=$(find "$ANDROID_SDK_ROOT/build-tools" -maxdepth 2 -type f -name apksigner | sort -V | tail -n 1 || true) + fi + if [ -n "$APKSIGNER" ]; then + "$APKSIGNER" verify --verbose "$APK" + else + jarsigner -verify -strict "$APK" + fi + + - name: Build unsigned Android debug APK + if: ${{ env.ANDROID_KEYSTORE_BASE64 == '' }} run: | cd packages/mobile/android chmod +x gradlew ./gradlew assembleDebug - - name: Normalize Android APK asset + - name: Normalize unsigned Android APK asset + if: ${{ env.ANDROID_KEYSTORE_BASE64 == '' }} run: | - # FNXC:Release 2026-06-25-12:00: - # Ship the secret-free debug APK because this repo has no Android - # signing keystore configured; signed release APK/AAB distribution is a - # separate product task, not a binary-release plumbing prerequisite. APK="packages/mobile/android/app/build/outputs/apk/debug/app-debug.apk" if [ ! -f "$APK" ]; then echo "::error::Expected Android APK missing at $APK" >&2 @@ -413,18 +474,23 @@ jobs: mkdir -p packages/mobile/dist cp "$APK" packages/mobile/dist/fusion-android.apk - - name: Generate Android APK checksum + - name: Generate Android artifact checksums run: | cd packages/mobile/dist - sha256sum fusion-android.apk > fusion-android.apk.sha256 + for file in fusion-android*.apk fusion-android-release.aab; do + [ -f "$file" ] || continue + sha256sum "$file" > "$file.sha256" + done - - name: Upload Android APK artifact + - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: fusion-android-apk path: | - packages/mobile/dist/fusion-android.apk - packages/mobile/dist/fusion-android.apk.sha256 + packages/mobile/dist/fusion-android*.apk + packages/mobile/dist/fusion-android*.apk.sha256 + packages/mobile/dist/fusion-android-release.aab + packages/mobile/dist/fusion-android-release.aab.sha256 # ── Collect all artifacts ───────────────────────────────────────────── collect: @@ -441,7 +507,7 @@ jobs: - name: Combine artifacts run: | mkdir combined - find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -exec cp {} combined/ \; + find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.aab" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -exec cp {} combined/ \; ls -la combined/ - name: Upload combined archive diff --git a/MOBILE.md b/MOBILE.md index 419b8047e1..11ea34a507 100644 --- a/MOBILE.md +++ b/MOBILE.md @@ -111,7 +111,23 @@ Mobile CI is defined in `.github/workflows/mobile.yml`. - `build-ios` (sync/build iOS when `packages/mobile/ios/` exists) - `build-android` (sync/build Android when `packages/mobile/android/` exists) -Artifacts from the Mobile Builds workflow are retained for 30 days. Tagged binary releases also run the Android build leg in `.github/workflows/release.yml` and publish `fusion-android.apk` plus `fusion-android.apk.sha256` as GitHub Release assets; `.github/workflows/test-release.yml` mirrors that path in its tag-less rehearsal artifact. +Artifacts from the Mobile Builds workflow are retained for 30 days. Tagged binary releases also run the Android build leg in `.github/workflows/release.yml`; `.github/workflows/test-release.yml` mirrors that path in its tag-less rehearsal artifact. + +When the repository has Android signing secrets configured (`ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`), the release pipeline publishes signed `fusion-android-release.apk` and `fusion-android-release.aab` assets plus `.sha256` checksums. Without those secrets, the pipeline preserves the secret-free fallback and publishes the unsigned debug APK as `fusion-android.apk` plus `fusion-android.apk.sha256`. + +Install the signed APK by enabling **Install unknown apps** for the transfer source on the device, then sideloading it: + +```bash +adb install fusion-android-release.apk +``` + +Verify the APK signer before distribution when Android SDK build-tools are available: + +```bash +apksigner verify --print-certs fusion-android-release.apk +``` + +The `.aab` file is for Play distribution and is not directly sideloadable with `adb install`. Automated Play Store / Play Console upload remains out of scope for now because it needs a Google service-account JSON secret, a published Play listing, and fastlane or `r0adkll/upload-google-play` wiring; that work is tracked separately in FN-7043 from the sideload-first release assets. ## Replacing PWA Icons diff --git a/RELEASING.md b/RELEASING.md index 2b9caa3e1e..22194a7403 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -57,17 +57,17 @@ When you merge the Version Packages PR: - It creates a git tag `v{version}` based on the `kb` CLI package version - The tag push triggers `release.yml`, which: - Builds platform-specific binaries for Linux x64, macOS x64, macOS arm64, and Windows x64 - - Builds the Android APK as `fusion-android.apk` - - Signs macOS binaries (codesign + notarization) and Windows binaries (Authenticode) - - Generates SHA256 checksums for all binaries and the Android APK - - Creates a **GitHub Release** with all binaries, the Android APK, and checksums attached + - Builds Android release assets: signed `fusion-android-release.apk` + `fusion-android-release.aab` when Android signing secrets are configured, otherwise unsigned debug `fusion-android.apk` + - Signs macOS binaries (codesign + notarization), Windows binaries (Authenticode), and Android release artifacts when keystore secrets are available + - Generates SHA256 checksums for all binaries and Android artifacts + - Creates a **GitHub Release** with all binaries, Android artifacts, and checksums attached ## Release channels | Channel | Workflow | Trigger | Output | |---------|----------|---------|--------| | npm | `version.yml` | Push to `main` | npm packages with provenance | -| GitHub Release | `release.yml` | Version tag (`v*`) | Signed platform binaries, Android APK + checksums | +| GitHub Release | `release.yml` | Version tag (`v*`) | Signed platform binaries, Android APK/AAB + checksums | ## Platform binaries @@ -76,16 +76,36 @@ When you merge the Version Packages PR: | Linux x64 | `fusion-linux-x64` | — | | macOS arm64 | `fusion-darwin-arm64` | ✓ (codesign + notarization) | | Windows x64 | `fusion-windows-x64.exe` | ✓ (Authenticode) | -| Android | `fusion-android.apk` | — (debug/unsigned APK) | +| Android | `fusion-android-release.apk`, `fusion-android-release.aab` | ✓ when Android keystore secrets are configured | +| Android fallback | `fusion-android.apk` | — (debug/unsigned APK when Android keystore secrets are absent) | > macOS Intel (`darwin-x64`) is intentionally not shipped: the CLI is Apple-Silicon-only because `macos-13` GitHub runners are too scarce to build reliably. The desktop macOS DMG/ZIP remains universal. +## Android release signing + +`release.yml` and the tag-less `test-release.yml` rehearsal workflow publish signed Android release artifacts when all Android signing secrets are configured: + +- `ANDROID_KEYSTORE_BASE64` — base64-encoded `.jks` / `.keystore` file +- `ANDROID_KEYSTORE_PASSWORD` +- `ANDROID_KEY_ALIAS` +- `ANDROID_KEY_PASSWORD` + +Encode the keystore before saving it as a GitHub Actions secret: + +```bash +base64 -w0 release.keystore +``` + +The Android native project under `packages/mobile/android/` is generated and gitignored, so CI does not commit signing configuration into Gradle files. Instead, the release job injects signing at build time with Android Gradle Plugin `android.injected.signing.*` properties, builds `assembleRelease` and `bundleRelease`, verifies the APK signature, and uploads `fusion-android-release.apk`, `fusion-android-release.aab`, and matching `.sha256` files. If `ANDROID_KEYSTORE_BASE64` is absent, the workflow preserves the secret-free path by building the unsigned debug APK as `fusion-android.apk` with `fusion-android.apk.sha256`. + +Automated Play Store / Play Console upload is intentionally out of scope for the release pipeline right now. It needs a Google service-account JSON secret, a published Play listing, and fastlane or `r0adkll/upload-google-play` wiring; that work is tracked separately in FN-7043 while Fusion remains sideload-first for pre-1.0 Android distribution. + ## Testing binary builds Use the **Test Release** workflow (`test-release.yml`) to manually test binary builds without creating a real release: 1. Go to **Actions** → **Test Release** → **Run workflow** -2. The workflow builds all 4 platform binaries plus the Android APK, runs smoke tests, and uploads artifacts +2. The workflow builds all 4 platform binaries plus the Android APK/AAB path (signed when Android signing secrets are available, unsigned debug APK otherwise), runs smoke tests, and uploads artifacts 3. Download the `all-binaries` artifact to inspect the output ## Manual release (fallback) diff --git a/packages/cli/src/__tests__/ci-workflow.test.ts b/packages/cli/src/__tests__/ci-workflow.test.ts index 8ec939b65b..e1e3853857 100644 --- a/packages/cli/src/__tests__/ci-workflow.test.ts +++ b/packages/cli/src/__tests__/ci-workflow.test.ts @@ -418,6 +418,18 @@ describe("Binary release workflow (.github/workflows/release.yml)", () => { expect(workflow.jobs["github-release"].needs).toContain("build-binaries"); expect(workflow.jobs["github-release"].needs).toContain("build-android"); }); + + it("wires signed Android AAB artifacts into release aggregation", () => { + const androidJob = workflow.jobs["build-android"]; + const collectStep = workflow.jobs["github-release"].steps.find((step: any) => step.name === "Collect release files"); + + expect(androidJob.env.ANDROID_KEYSTORE_BASE64).toBe("${{ secrets.ANDROID_KEYSTORE_BASE64 }}"); + expect(content).toContain("./gradlew assembleRelease bundleRelease"); + expect(content).toContain("fusion-android-release.aab"); + expect(collectStep.run).toContain('-name "*.apk"'); + expect(collectStep.run).toContain('-name "*.aab"'); + expect(collectStep.run).toContain('-name "*.sha256"'); + }); }); describe("Test-release workflow (.github/workflows/test-release.yml)", () => { @@ -485,6 +497,18 @@ describe("Test-release workflow (.github/workflows/test-release.yml)", () => { expect(workflow.jobs.collect.needs).toContain("build-android"); expect(content).toContain("all-binaries"); }); + + it("wires signed Android AAB artifacts into rehearsal aggregation", () => { + const androidJob = workflow.jobs["build-android"]; + const combineStep = workflow.jobs.collect.steps.find((step: any) => step.name === "Combine artifacts"); + + expect(androidJob.env.ANDROID_KEYSTORE_BASE64).toBe("${{ secrets.ANDROID_KEYSTORE_BASE64 }}"); + expect(content).toContain("./gradlew assembleRelease bundleRelease"); + expect(content).toContain("fusion-android-release.aab"); + expect(combineStep.run).toContain('-name "*.apk"'); + expect(combineStep.run).toContain('-name "*.aab"'); + expect(combineStep.run).toContain('-name "*.sha256"'); + }); }); describe("Code signing — Release workflow secrets", () => { diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 77232796a3..9c0f114e5a 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -364,7 +364,7 @@ Desktop packaging is configured in `electron-builder.yml`. - Windows: x64 + arm64 outputs (NSIS + portable), matching `.exe.sha256` sidecars, and `.blockmap` files. - macOS: `Fusion--mac-arm64.dmg`, `Fusion--mac-x64.dmg`, matching `.zip` variants, `.sha256` sidecars, and `.blockmap` files. - Linux: `Fusion--linux-x64.AppImage` and `Fusion--linux-arm64.AppImage` with matching `.sha256` sidecars, plus best-effort `.deb` and `.tar.gz` outputs per arch (`Fusion--linux-x64.{deb,tar.gz}` / `Fusion--linux-arm64.{deb,tar.gz}`) and sidecars when available on the runner image. - - Android: `fusion-android.apk` and `fusion-android.apk.sha256` from the Capacitor/Gradle debug APK build. + - Android: when Android signing secrets are configured, `fusion-android-release.apk`, `fusion-android-release.apk.sha256`, `fusion-android-release.aab`, and `fusion-android-release.aab.sha256`; otherwise the secret-free fallback publishes `fusion-android.apk` and `fusion-android.apk.sha256` from the Capacitor/Gradle debug APK build. - Tag-less release rehearsal workflow (`.github/workflows/test-release.yml`) mirrors that artifact collection path without publishing a real GitHub Release. - Linux ARM64 artifacts are cross-built from the `ubuntu-latest` x64 runner by passing `electron-builder --linux --x64 --arm64`; running/validating arm64 installers still requires an arm64 Linux device or emulator. - Linux desktop artifacts can include detached GPG signature sidecars (`*.AppImage.asc`, `*.deb.asc`, `*.tar.gz.asc`) when Linux signing secrets are configured in CI; full Linux desktop code-signing rollout remains tracked in FN-5605. diff --git a/packages/desktop/src/__tests__/release-workflow.test.ts b/packages/desktop/src/__tests__/release-workflow.test.ts index d7b9fd8e68..b1c5cdb92b 100644 --- a/packages/desktop/src/__tests__/release-workflow.test.ts +++ b/packages/desktop/src/__tests__/release-workflow.test.ts @@ -68,7 +68,7 @@ describe("desktop release workflow wiring", () => { expect(testRelease).toContain('-name "latest*.yml"'); }); - it("adds Android APK build and aggregation wiring to release workflows", async () => { + it("adds signed Android APK/AAB build and aggregation wiring to release workflows", async () => { const release = await readRepoFile(".github/workflows/release.yml"); const testRelease = await readRepoFile(".github/workflows/test-release.yml"); @@ -79,12 +79,32 @@ describe("desktop release workflow wiring", () => { expect(workflow).toContain('java-version: "17"'); expect(workflow).toContain("pnpm --filter @fusion/mobile cap add android"); expect(workflow).toContain("pnpm --filter @fusion/mobile cap sync android"); + expect(workflow).toContain("ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}"); + expect(workflow).toContain("ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"); + expect(workflow).toContain("ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}"); + expect(workflow).toContain("ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}"); + expect(workflow).toContain("env.ANDROID_KEYSTORE_BASE64 != ''"); + expect(workflow).toContain("env.ANDROID_KEYSTORE_BASE64 == ''"); + expect(workflow).toContain("./gradlew assembleRelease bundleRelease"); + expect(workflow).toContain("android.injected.signing.store.file"); + expect(workflow).toContain("android.injected.signing.store.password"); + expect(workflow).toContain("android.injected.signing.key.alias"); + expect(workflow).toContain("android.injected.signing.key.password"); expect(workflow).toContain("./gradlew assembleDebug"); expect(workflow).toContain("packages/mobile/android/app/build/outputs/apk/debug/app-debug.apk"); + expect(workflow).toContain("packages/mobile/android/app/build/outputs/apk/release/app-release.apk"); + expect(workflow).toContain("packages/mobile/android/app/build/outputs/bundle/release/app-release.aab"); expect(workflow).toContain("packages/mobile/dist/fusion-android.apk"); - expect(workflow).toContain("sha256sum fusion-android.apk > fusion-android.apk.sha256"); + expect(workflow).toContain("packages/mobile/dist/fusion-android-release.apk"); + expect(workflow).toContain("packages/mobile/dist/fusion-android-release.aab"); + expect(workflow).toContain("apksigner"); + expect(workflow).toContain("jarsigner -verify -strict"); + expect(workflow).toContain("sha256sum \"$file\" > \"$file.sha256\""); expect(workflow).toContain("name: fusion-android-apk"); + expect(workflow).toContain("fusion-android-release.apk"); + expect(workflow).toContain("fusion-android-release.aab"); expect(workflow).toContain('-name "*.apk"'); + expect(workflow).toContain('-name "*.aab"'); } }); });