From dba65933e5ab00ed5853cf6c449daea63c37f2c3 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 17:42:22 -0700 Subject: [PATCH] FN-7014: publish Android APK release artifacts Adds Android APK generation to binary release and rehearsal workflows so GitHub releases ship mobile assets. - Add Android build jobs that sync Capacitor, assemble the debug APK, and upload APK/checksum artifacts. - Include Android artifacts in release and test-release collection dependencies and file matching. - Document Android release outputs and extend workflow shape tests for the new asset path. Files changed: .github/workflows/release.yml | 86 +++++++++++++++++++++- .github/workflows/test-release.yml | 85 ++++++++++++++++++++- MOBILE.md | 2 +- RELEASING.md | 10 ++- packages/cli/src/__tests__/ci-workflow.test.ts | 6 +- packages/desktop/README.md | 1 + .../desktop/src/__tests__/release-workflow.test.ts | 24 +++++- 7 files changed, 201 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-7014 Fusion-Task-Lineage: 8ed94f64-399b-433f-add4-0294fc5722d1 --- .github/workflows/release.yml | 86 ++++++++++++++++++- .github/workflows/test-release.yml | 85 +++++++++++++++++- MOBILE.md | 2 +- RELEASING.md | 10 ++- .../cli/src/__tests__/ci-workflow.test.ts | 6 +- packages/desktop/README.md | 1 + .../src/__tests__/release-workflow.test.ts | 24 +++++- 7 files changed, 201 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e41ab8137..8dd63c157a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -358,10 +358,92 @@ jobs: packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.asc packages/desktop/dist-electron/latest-linux.yml + + # ── Build Android APK artifact ─────────────────────────────────────── + # 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. + build-android: + name: Build Android APK + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node and install dependencies + uses: ./.github/actions/setup-node-pnpm + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Cache Android Gradle caches + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('pnpm-lock.yaml', 'packages/mobile/capacitor.config.ts') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build dashboard client + run: pnpm --filter @fusion/dashboard build + + - name: Create and sync Capacitor Android project + run: | + # FNXC:Release 2026-06-25-12:00: + # The Capacitor Android platform directory is gitignored and absent in + # clean release checkouts, so initialize it from pinned package metadata + # before syncing web assets instead of silently skipping the APK leg. + if [ ! -d packages/mobile/android ]; then + pnpm --filter @fusion/mobile cap add android + fi + pnpm --filter @fusion/mobile cap sync android + + - name: Build Android APK + run: | + cd packages/mobile/android + chmod +x gradlew + ./gradlew assembleDebug + + - name: Normalize Android APK asset + 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 + exit 1 + fi + mkdir -p packages/mobile/dist + cp "$APK" packages/mobile/dist/fusion-android.apk + + - name: Generate Android APK checksum + run: | + cd packages/mobile/dist + sha256sum fusion-android.apk > fusion-android.apk.sha256 + + - name: Upload Android APK artifact + uses: actions/upload-artifact@v4 + with: + name: fusion-android-apk + path: | + packages/mobile/dist/fusion-android.apk + packages/mobile/dist/fusion-android.apk.sha256 + # ── Create GitHub Release ───────────────────────────────────────────── github-release: name: Create GitHub Release - needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux] + needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux, build-android] # Run as long as the workflow wasn't cancelled, even if some build legs failed, # so a single failing matrix leg doesn't suppress publishing the ones that did # build. Gated to tag pushes only: a workflow_dispatch run on a branch is a @@ -394,7 +476,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 "*.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 "*.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 ea2f157ac0..1acd26e0be 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -345,10 +345,91 @@ jobs: packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.asc packages/desktop/dist-electron/latest-linux.yml + + # ── Build Android APK artifact ─────────────────────────────────────── + # 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. + build-android: + name: Build Android APK + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node and install dependencies + uses: ./.github/actions/setup-node-pnpm + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Cache Android Gradle caches + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('pnpm-lock.yaml', 'packages/mobile/capacitor.config.ts') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build dashboard client + run: pnpm --filter @fusion/dashboard build + + - name: Create and sync Capacitor Android project + run: | + # FNXC:Release 2026-06-25-12:00: + # The Capacitor Android platform directory is gitignored and absent in + # clean release checkouts, so initialize it from pinned package metadata + # before syncing web assets instead of silently skipping the APK leg. + if [ ! -d packages/mobile/android ]; then + pnpm --filter @fusion/mobile cap add android + fi + pnpm --filter @fusion/mobile cap sync android + + - name: Build Android APK + run: | + cd packages/mobile/android + chmod +x gradlew + ./gradlew assembleDebug + + - name: Normalize Android APK asset + 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 + exit 1 + fi + mkdir -p packages/mobile/dist + cp "$APK" packages/mobile/dist/fusion-android.apk + + - name: Generate Android APK checksum + run: | + cd packages/mobile/dist + sha256sum fusion-android.apk > fusion-android.apk.sha256 + + - name: Upload Android APK artifact + uses: actions/upload-artifact@v4 + with: + name: fusion-android-apk + path: | + packages/mobile/dist/fusion-android.apk + packages/mobile/dist/fusion-android.apk.sha256 + # ── Collect all artifacts ───────────────────────────────────────────── collect: name: Collect Artifacts - needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux] + needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux, build-android] runs-on: ubuntu-latest steps: @@ -360,7 +441,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 "*.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 "*.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 831916a68f..419b8047e1 100644 --- a/MOBILE.md +++ b/MOBILE.md @@ -111,7 +111,7 @@ 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 are retained for 30 days. +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. ## Replacing PWA Icons diff --git a/RELEASING.md b/RELEASING.md index 88efb2c3f0..2b9caa3e1e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -57,16 +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 - - Creates a **GitHub Release** with all binaries and checksums attached + - Generates SHA256 checksums for all binaries and the Android APK + - Creates a **GitHub Release** with all binaries, the Android APK, 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 + checksums | +| GitHub Release | `release.yml` | Version tag (`v*`) | Signed platform binaries, Android APK + checksums | ## Platform binaries @@ -75,6 +76,7 @@ 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) | > 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. @@ -83,7 +85,7 @@ When you merge the Version Packages PR: 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, runs smoke tests, and uploads artifacts +2. The workflow builds all 4 platform binaries plus the Android APK, 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 2acc6da897..8ec939b65b 100644 --- a/packages/cli/src/__tests__/ci-workflow.test.ts +++ b/packages/cli/src/__tests__/ci-workflow.test.ts @@ -414,8 +414,9 @@ describe("Binary release workflow (.github/workflows/release.yml)", () => { expect(workflow.permissions.contents).toBe("write"); }); - it("has github-release job that depends on build-binaries", () => { + it("has github-release job that depends on binary and Android builds", () => { expect(workflow.jobs["github-release"].needs).toContain("build-binaries"); + expect(workflow.jobs["github-release"].needs).toContain("build-android"); }); }); @@ -478,9 +479,10 @@ describe("Test-release workflow (.github/workflows/test-release.yml)", () => { expect(content).toContain("actions/upload-artifact"); }); - it("has a collect job that combines artifacts", () => { + it("has a collect job that combines binary and Android artifacts", () => { expect(workflow.jobs.collect).toBeDefined(); expect(workflow.jobs.collect.needs).toContain("build-binaries"); + expect(workflow.jobs.collect.needs).toContain("build-android"); expect(content).toContain("all-binaries"); }); }); diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 5956f23806..77232796a3 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -364,6 +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. - 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 d8d0c51c1f..d7b9fd8e68 100644 --- a/packages/desktop/src/__tests__/release-workflow.test.ts +++ b/packages/desktop/src/__tests__/release-workflow.test.ts @@ -45,7 +45,7 @@ describe("desktop release workflow wiring", () => { const release = await readRepoFile(".github/workflows/release.yml"); expect(release).toContain( - "needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux]", + "needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux, build-android]", ); expect(release).toContain('find artifacts -type f \\('); expect(release).toContain('-name "*.exe"'); @@ -63,10 +63,30 @@ describe("desktop release workflow wiring", () => { const testRelease = await readRepoFile(".github/workflows/test-release.yml"); expect(testRelease).toContain( - "needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux]", + "needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux, build-android]", ); expect(testRelease).toContain('-name "latest*.yml"'); }); + + it("adds Android APK 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"); + + for (const workflow of [release, testRelease]) { + expect(workflow).toContain("build-android:"); + expect(workflow).toContain("runs-on: ubuntu-latest"); + expect(workflow).toContain("actions/setup-java@v4"); + 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("./gradlew assembleDebug"); + expect(workflow).toContain("packages/mobile/android/app/build/outputs/apk/debug/app-debug.apk"); + expect(workflow).toContain("packages/mobile/dist/fusion-android.apk"); + expect(workflow).toContain("sha256sum fusion-android.apk > fusion-android.apk.sha256"); + expect(workflow).toContain("name: fusion-android-apk"); + expect(workflow).toContain('-name "*.apk"'); + } + }); }); describe("desktop macos signing wiring", () => {