FN-7032: add signed Android release artifacts

Add secret-gated signed Android release packaging while preserving the unsigned fallback.

- Build signed Android release APK and AAB artifacts when keystore secrets are configured.
- Verify signed APKs, generate checksums for APK/AAB outputs, and include AABs in release aggregation.
- Document Android signing secrets, sideload verification, fallback artifacts, and Play upload scope.
- Cover the release and rehearsal workflow wiring with CLI and desktop workflow tests.

Files changed:
 .github/workflows/release.yml                      | 94 ++++++++++++++++++----
 .github/workflows/test-release.yml                 | 94 ++++++++++++++++++----
 MOBILE.md                                          | 18 ++++-
 RELEASING.md                                       | 34 ++++++--
 packages/cli/src/__tests__/ci-workflow.test.ts     | 24 ++++++
 packages/desktop/README.md                         |  2 +-
 .../desktop/src/__tests__/release-workflow.test.ts | 24 +++++-
 7 files changed, 251 insertions(+), 39 deletions(-)

Fusion-Task-Id: FN-7032
Fusion-Task-Lineage: 0334c026-384e-4531-a2b2-f8a0b5bb7ff0
This commit is contained in:
gsxdsm
2026-06-25 20:20:33 -07:00
parent c0958bfd4f
commit 4a4e389254
7 changed files with 251 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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