Files
fusion/.github/workflows/test-release.yml
gsxdsm bc38f0c842 FN-7209: require JDK 21 for Android CI builds
Align Android Gradle build environments with Capacitor's Java 21 source compatibility.

- Update mobile, release, and test-release workflows to provision Temurin JDK 21.
- Document Java 21 as the required Android Gradle build JDK.
- Add CI workflow coverage that checks Android build jobs satisfy @capacitor/android sourceCompatibility.

Files changed:
 .github/workflows/mobile.yml                   |  6 +-
 .github/workflows/release.yml                  |  6 +-
 .github/workflows/test-release.yml             |  6 +-
 MOBILE.md                                      |  4 +-
 packages/cli/src/__tests__/ci-workflow.test.ts | 99 +++++++++++++++++++++++++-
 5 files changed, 112 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-7209

Fusion-Task-Lineage: d9e50bee-d7f8-4e87-b33e-136ffffc93af

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-28 16:56:35 -07:00

520 lines
21 KiB
YAML

# Test Release workflow
#
# Manual workflow for testing binary builds without creating a real release.
# Triggered via workflow_dispatch from the GitHub Actions UI.
name: Test Release
on:
workflow_dispatch:
jobs:
# ── Build and test platform-specific binaries ─────────────────────────
build-binaries:
name: Build & Test ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: bun-linux-x64
binary: fn-linux-x64
- os: ubuntu-24.04-arm
target: bun-linux-arm64
binary: fn-linux-arm64
- os: macos-latest
target: bun-darwin-arm64
binary: fn-darwin-arm64
# bun-darwin-x64 (Intel) dropped: macos-13 runner scarcity — CLI is
# Apple-Silicon-only for macOS. Keep in sync with release.yml.
- os: windows-latest
target: bun-windows-x64
binary: fn-windows-x64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Build
run: pnpm build
- name: Build binary
run: pnpm --filter @runfusion/fusion build:exe -- --target ${{ matrix.target }}
- name: Verify binary exists
shell: bash
run: test -f packages/cli/dist/${{ matrix.binary }}
- name: Smoke test (Linux/macOS)
if: runner.os != 'Windows'
run: |
chmod +x packages/cli/dist/${{ matrix.binary }}
packages/cli/dist/${{ matrix.binary }} --help
- name: Smoke test (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
& packages/cli/dist/${{ matrix.binary }} --help
- name: Sign macOS binary
if: runner.os == 'macOS' && env.APPLE_CERTIFICATE_BASE64 != ''
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: bash scripts/sign-macos.sh packages/cli/dist/${{ matrix.binary }} packages/cli/dist/runtime
- name: Sign Windows binary
if: runner.os == 'Windows' && env.WINDOWS_CERTIFICATE_BASE64 != ''
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: pwsh scripts/sign-windows.ps1 packages/cli/dist/${{ matrix.binary }}
- name: Generate checksum (Linux)
if: runner.os == 'Linux'
run: |
cd packages/cli/dist
sha256sum ${{ matrix.binary }} > ${{ matrix.binary }}.sha256
- name: Generate checksum (macOS)
if: runner.os == 'macOS'
run: |
cd packages/cli/dist
shasum -a 256 ${{ matrix.binary }} > ${{ matrix.binary }}.sha256
- name: Generate checksum (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
cd packages/cli/dist
$hash = (Get-FileHash ${{ matrix.binary }} -Algorithm SHA256).Hash.ToLower()
"$hash ${{ matrix.binary }}" | Out-File -Encoding ascii ${{ matrix.binary }}.sha256
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary }}
path: |
packages/cli/dist/${{ matrix.binary }}
packages/cli/dist/${{ matrix.binary }}.sha256
packages/cli/dist/runtime/**/*
# ── Build Windows desktop EXE artifacts ──────────────────────────────
# Code-signing with WINDOWS_CERTIFICATE_BASE64 / WINDOWS_CERTIFICATE_PASSWORD
# is intentionally deferred to FN-5592; ARM64 support is tracked in FN-5594.
build-desktop-windows:
name: Build Desktop Windows EXE
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
- name: Build
run: pnpm build
- name: Build desktop package
run: pnpm --filter @fusion/desktop build
- name: Package Windows desktop EXE
run: pnpm --filter @fusion/desktop dist:win -- --publish never
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify desktop EXE artifacts
shell: pwsh
run: |
$exes = Get-ChildItem packages/desktop/dist-electron -Filter "Fusion-*-win-*.exe"
if ($exes.Count -eq 0) {
Write-Error "No Fusion Windows EXE artifacts produced"
exit 1
}
- name: Generate desktop EXE checksums
shell: pwsh
run: |
$exes = Get-ChildItem packages/desktop/dist-electron -Filter "Fusion-*-win-*.exe"
foreach ($exe in $exes) {
$hash = (Get-FileHash $exe.FullName -Algorithm SHA256).Hash.ToLower()
"$hash $($exe.Name)" | Out-File -Encoding ascii "$($exe.FullName).sha256"
}
- name: Upload desktop Windows artifacts
uses: actions/upload-artifact@v4
with:
name: fusion-desktop-windows
path: |
packages/desktop/dist-electron/Fusion-*-win-*.exe
packages/desktop/dist-electron/Fusion-*-win-*.exe.sha256
packages/desktop/dist-electron/Fusion-*-win-*.exe.blockmap
packages/desktop/dist-electron/latest.yml
# ── Build macOS desktop artifacts ────────────────────────────────────
build-desktop-macos:
name: Build Desktop macOS DMG/ZIP
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
- name: Build
run: pnpm build
- name: Build desktop package
run: pnpm --filter @fusion/desktop build
- name: Package signed macOS desktop DMG/ZIP
if: ${{ env.APPLE_CERTIFICATE_BASE64 != '' }}
run: pnpm --filter @fusion/desktop dist:mac -- --publish never
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_IDENTITY_AUTO_DISCOVERY: "true"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Package unsigned macOS desktop DMG/ZIP
if: ${{ env.APPLE_CERTIFICATE_BASE64 == '' }}
run: pnpm --filter @fusion/desktop dist:mac -- --publish never -c.mac.notarize=false
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CSC_IDENTITY_AUTO_DISCOVERY: "false"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify signed and notarized macOS artifacts
if: ${{ env.APPLE_CERTIFICATE_BASE64 != '' }}
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
dmgs=(packages/desktop/dist-electron/Fusion-*-mac-*.dmg)
zips=(packages/desktop/dist-electron/Fusion-*-mac-*.zip)
if [ ${#dmgs[@]} -eq 0 ]; then
echo "No Fusion macOS DMG artifacts produced" >&2
exit 1
fi
if [ ${#zips[@]} -eq 0 ]; then
echo "No Fusion macOS ZIP artifacts produced" >&2
exit 1
fi
for dmg in "${dmgs[@]}"; do
echo "Verifying signed DMG: $dmg"
codesign --verify --deep --strict --verbose=2 "$dmg"
spctl --assess --type open --context context:primary-signature -v "$dmg"
xcrun stapler validate "$dmg"
done
for zip in "${zips[@]}"; do
echo "Verifying ZIP app bundle: $zip"
tmpdir="$(mktemp -d)"
unzip -q "$zip" -d "$tmpdir"
app="$(find "$tmpdir" -type d -name '*.app' -print -quit)"
if [ -z "$app" ]; then
echo "No .app bundle found in $zip" >&2
rm -rf "$tmpdir"
exit 1
fi
codesign --verify --deep --strict --verbose=2 "$app"
spctl --assess --type exec -v "$app"
xcrun stapler validate "$app"
rm -rf "$tmpdir"
done
- name: Generate desktop macOS checksums
shell: bash
run: |
shopt -s nullglob
for file in packages/desktop/dist-electron/Fusion-*-mac-*.dmg packages/desktop/dist-electron/Fusion-*-mac-*.zip; do
shasum -a 256 "$file" > "$file.sha256"
done
- name: Upload desktop macOS artifacts
uses: actions/upload-artifact@v4
with:
name: fusion-desktop-macos
path: |
packages/desktop/dist-electron/Fusion-*-mac-*.dmg
packages/desktop/dist-electron/Fusion-*-mac-*.dmg.sha256
packages/desktop/dist-electron/Fusion-*-mac-*.zip
packages/desktop/dist-electron/Fusion-*-mac-*.zip.sha256
packages/desktop/dist-electron/Fusion-*-mac-*.blockmap
packages/desktop/dist-electron/latest-mac.yml
# ── Build Linux desktop artifacts ────────────────────────────────────
build-desktop-linux:
name: Build Desktop Linux Artifacts
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
- name: Build
run: pnpm build
- name: Build desktop package
run: pnpm --filter @fusion/desktop build
- name: Package Linux desktop artifacts
# Linux desktop code-signing is deferred to FN-5605; Linux ARM64 CLI binaries are tracked in FN-5606.
run: pnpm --filter @fusion/desktop dist:linux -- --x64 --arm64 --publish never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify desktop Linux AppImage artifacts
shell: bash
run: |
shopt -s nullglob
arm64_appimages=(packages/desktop/dist-electron/Fusion-*-linux-arm64.AppImage)
x64_appimages=(packages/desktop/dist-electron/Fusion-*-linux-x86_64.AppImage)
if [ ${#arm64_appimages[@]} -eq 0 ]; then
echo "No Fusion Linux arm64 AppImage artifacts produced" >&2
exit 1
fi
if [ ${#x64_appimages[@]} -eq 0 ]; then
echo "No Fusion Linux x64 AppImage artifacts produced" >&2
exit 1
fi
- name: Sign Linux desktop artifacts
if: ${{ env.LINUX_GPG_PRIVATE_KEY != '' }}
env:
LINUX_GPG_PRIVATE_KEY: ${{ secrets.LINUX_GPG_PRIVATE_KEY }}
LINUX_GPG_PASSPHRASE: ${{ secrets.LINUX_GPG_PASSPHRASE }}
LINUX_GPG_KEY_ID: ${{ secrets.LINUX_GPG_KEY_ID }}
shell: bash
run: |
shopt -s nullglob
artifacts=(
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage
packages/desktop/dist-electron/Fusion-*-linux-*.deb
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz
)
bash scripts/sign-linux.sh "${artifacts[@]}"
- name: Generate desktop Linux checksums
shell: bash
run: |
shopt -s nullglob
for file in packages/desktop/dist-electron/Fusion-*-linux-*.AppImage packages/desktop/dist-electron/Fusion-*-linux-*.deb packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz; do
sha256sum "$file" > "$file.sha256"
done
- name: Upload desktop Linux artifacts
uses: actions/upload-artifact@v4
with:
# Single glob set covers both linux-x64 and linux-arm64 artifact filenames.
name: fusion-desktop-linux
if-no-files-found: ignore
path: |
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage.sha256
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage.asc
packages/desktop/dist-electron/Fusion-*-linux-*.deb
packages/desktop/dist-electron/Fusion-*-linux-*.deb.sha256
packages/desktop/dist-electron/Fusion-*-linux-*.deb.asc
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.sha256
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.asc
packages/desktop/dist-electron/latest-linux.yml
# ── 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/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
uses: actions/checkout@v4
- name: Setup Node and install dependencies
uses: ./.github/actions/setup-node-pnpm
# FNXC:MobileAndroidBuild 2026-06-28-00:00:
# Capacitor 7 @capacitor/android compiles its Android library with JavaVersion.VERSION_21; rehearsal APK/AAB Gradle builds must provision JDK 21 because JDK 17 fails with `invalid source release: 21`.
- name: Setup Java 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- 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: 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 unsigned Android APK asset
if: ${{ env.ANDROID_KEYSTORE_BASE64 == '' }}
run: |
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 artifact checksums
run: |
cd packages/mobile/dist
for file in fusion-android*.apk fusion-android-release.aab; do
[ -f "$file" ] || continue
sha256sum "$file" > "$file.sha256"
done
- 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-release.aab
packages/mobile/dist/fusion-android-release.aab.sha256
# ── Collect all artifacts ─────────────────────────────────────────────
collect:
name: Collect Artifacts
needs: [build-binaries, build-desktop-windows, build-desktop-macos, build-desktop-linux, build-android]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- 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 "*.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
uses: actions/upload-artifact@v4
with:
name: all-binaries
path: combined/*