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
This commit is contained in:
gsxdsm
2026-06-25 17:42:22 -07:00
parent 90c0e9936e
commit dba65933e5
7 changed files with 201 additions and 13 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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");
});
});

View File

@@ -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-<version>-mac-arm64.dmg`, `Fusion-<version>-mac-x64.dmg`, matching `.zip` variants, `.sha256` sidecars, and `.blockmap` files.
- Linux: `Fusion-<version>-linux-x64.AppImage` and `Fusion-<version>-linux-arm64.AppImage` with matching `.sha256` sidecars, plus best-effort `.deb` and `.tar.gz` outputs per arch (`Fusion-<version>-linux-x64.{deb,tar.gz}` / `Fusion-<version>-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.

View File

@@ -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", () => {