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:
94
.github/workflows/release.yml
vendored
94
.github/workflows/release.yml
vendored
@@ -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"
|
||||
|
||||
94
.github/workflows/test-release.yml
vendored
94
.github/workflows/test-release.yml
vendored
@@ -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
|
||||
|
||||
18
MOBILE.md
18
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
|
||||
|
||||
|
||||
34
RELEASING.md
34
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)
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user