Skip to content

fix(android): tie the Flutter engine lifecycle back to the activity #621

fix(android): tie the Flutter engine lifecycle back to the activity

fix(android): tie the Flutter engine lifecycle back to the activity #621

Workflow file for this run

name: Build NexAI
on:
push:
branches:
- "**"
workflow_dispatch:
inputs:
build_target:
description: "Build target platform"
required: true
default: "all"
type: choice
options:
- windows
- android
- web
- all
permissions:
contents: read
jobs:
build-android:
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.build_target == 'android' || github.event.inputs.build_target == 'all' }}
runs-on: ubuntu-latest
permissions:
contents: write
packages: read
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5
with:
distribution: "zulu"
java-version: "21"
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
flutter-version: "3.44.5"
channel: "stable"
cache: true
- name: Cache Gradle dependencies
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.gradle/caches/modules-2
~/.gradle/caches/jars-*
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/*.gradle*', 'android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties', 'pubspec.lock') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Cache Flutter pub dependencies
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-pub-
- name: Clean stale pub git cache
run: rm -rf ~/.pub-cache/git/
- name: Subset JetBrains Mono font
run: python scripts/subset_jetbrains_mono.py
- name: Install dependencies
run: flutter pub get
- name: Analyze Dart code
run: flutter analyze --no-fatal-infos
- name: Run Flutter tests
run: flutter test
- name: Resolve lumen-crash SDK
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
chmod +x scripts/resolve-lumen-crash.sh
bash scripts/resolve-lumen-crash.sh
echo "lumenCrashVersion=$(grep '^lumenCrashVersion=' android/gradle.properties | cut -d= -f2-)"
echo "local-maven contents:"
find android/local-maven -type f | sed 's/^/ /' || true
- name: Write signing config
run: |
# Ensure gradle.properties has Unix line endings
if [ -f "android/gradle.properties" ]; then
sed -i 's/\r$//' android/gradle.properties
# Ensure file ends with newline
[ -n "$(tail -c1 android/gradle.properties)" ] && echo "" >> android/gradle.properties
fi
missing=()
if [ -z "${{ secrets.KEYSTORE_BASE64 }}" ]; then
missing+=("KEYSTORE_BASE64")
fi
if [ -z "${{ secrets.KEYSTORE_PASSWORD }}" ]; then
missing+=("KEYSTORE_PASSWORD")
fi
if [ -z "${{ secrets.KEY_ALIAS }}" ]; then
missing+=("KEY_ALIAS")
fi
if [ -z "${{ secrets.KEY_PASSWORD }}" ]; then
missing+=("KEY_PASSWORD")
fi
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Missing Android release signing secrets: ${missing[*]}"
exit 1
fi
echo "✓ Decoding keystore from secret..."
if ! echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > ${{ github.workspace }}/nexai.jks; then
echo "::error::Failed to decode KEYSTORE_BASE64"
exit 1
fi
if [ ! -s "${{ github.workspace }}/nexai.jks" ]; then
echo "::error::Keystore file not created"
exit 1
fi
printf 'NEXAI_STORE_FILE=${{ github.workspace }}/nexai.jks\n' >> android/gradle.properties
printf 'NEXAI_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}\n' >> android/gradle.properties
printf 'NEXAI_KEY_ALIAS=${{ secrets.KEY_ALIAS }}\n' >> android/gradle.properties
printf 'NEXAI_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}\n' >> android/gradle.properties
echo "✓ Release signing configured"
echo "=== gradle.properties content (masked) ==="
grep -v "PASSWORD\|ALIAS" android/gradle.properties || true
echo "Has NEXAI_STORE_FILE: $(grep -c 'NEXAI_STORE_FILE' android/gradle.properties)"
- name: Accept Android SDK licenses
run: yes | flutter doctor --android-licenses || true
- name: Run build.ps1 (version control)
run: pwsh scripts/build.ps1 -Arg android
- name: Build Android APK
env:
NEXAI_APP_SIGN_SECRET: ${{ secrets.NEXAI_APP_SIGN_SECRET }}
run: |
# NEXAI_APP_SIGN_SECRET (GitHub Actions secret) signs anonymous/gated
# NexAI requests (e.g. /api/nexai/security/status) before login.
# Optional: if unset, client falls back to unsigned soft-skip (see
# lib/utils/request_signer.dart). Never add this define to Web builds
# — dart-define values are inspectable in the compiled web bundle.
flutter build apk --release --split-per-abi \
--obfuscate \
--tree-shake-icons \
--split-debug-info=debug_symbols/android \
--dart-define-from-file=nexai_release.json \
--dart-define=NEXAI_APP_SIGN_SECRET="$NEXAI_APP_SIGN_SECRET" \
--build-number=${{ env.VERSION_CODE }}
# - name: Build Android App Bundle
# run: flutter build appbundle --release --dart-define-from-file=nexai_release.json --build-number=${{ env.VERSION_CODE }}
- name: Rename APK outputs for release
run: |
VERSION="${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
echo "Version: $VERSION"
echo "=== Original APK files ==="
ls -lh build/app/outputs/flutter-apk/
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
if [ -f "$file" ]; then
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
echo "Processing: $file -> NexAI_android_${VERSION}_${abi}.apk"
cp "$file" "build/app/outputs/flutter-apk/NexAI_android_${VERSION}_${abi}.apk"
fi
done
echo "=== Renamed APK files ==="
ls -lh build/app/outputs/flutter-apk/NexAI_android_*.apk
- name: Generate APK checksums
run: |
cd build/app/outputs/flutter-apk
sha256sum NexAI_android_*.apk > APK_SHA256SUMS.txt
python - <<'PY'
import hashlib, json, pathlib, os
tag = "v${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
assets = []
for path in sorted(pathlib.Path(".").glob("NexAI_android_*.apk")):
data = path.read_bytes()
assets.append({
"name": path.name,
"sha256": hashlib.sha256(data).hexdigest(),
"size": len(data),
})
pathlib.Path("release-manifest.json").write_text(json.dumps({
"schemaVersion": 1,
"tag": tag,
"generatedAt": "${{ github.run_id }}",
"assets": assets,
}, indent=2), encoding="utf-8")
PY
{
echo "## NexAI ${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
echo ""
echo "### APK SHA256"
while read -r hash file; do
echo "- \`$file\`"
echo " - sha256:$hash"
done < APK_SHA256SUMS.txt
} > ${{ github.workspace }}/RELEASE_NOTES.md
- name: Automatic release
if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
make_latest: true
body_path: RELEASE_NOTES.md
tag_name: "v${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
prerelease: false
name: "NexAI ${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
files: |
build/app/outputs/flutter-apk/NexAI_android_*.apk
build/app/outputs/flutter-apk/APK_SHA256SUMS.txt
build/app/outputs/flutter-apk/release-manifest.json
# - name: Rename AAB output
# run: |
# VERSION="${{ env.version }}"
# AAB_FILE="build/app/outputs/bundle/release/app-release.aab"
# if [ -f "$AAB_FILE" ]; then
# echo "Renaming AAB: $AAB_FILE -> NexAI_android_${VERSION}.aab"
# cp "$AAB_FILE" "build/app/outputs/bundle/release/NexAI_android_${VERSION}.aab"
# fi
#
# echo "=== AAB files ==="
# ls -lh build/app/outputs/bundle/release/
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: android-debug-symbols
path: debug_symbols/
retention-days: 90
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: NexAI-Android-APK
path: build/app/outputs/flutter-apk/*.apk
retention-days: 30
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: android-outputs
path: build/app/outputs/
# - name: Commit generated files
# run: |
# git config user.name "github-actions[bot]"
# git config user.email "github-actions[bot]@users.noreply.github.com"
# git add -A
# git diff --cached --quiet || git commit -m "ci: generate android build files [skip ci]"
# git push || true
# env:
# GITHUB_
build-web:
# if: false # Disabled - Web build not needed
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.build_target == 'web' || github.event.inputs.build_target == 'all' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
flutter-version: "3.44.5"
channel: "stable"
cache: true
- name: Cache Flutter pub dependencies
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-pub-
- name: Subset JetBrains Mono font
run: python scripts/subset_jetbrains_mono.py
- name: Install dependencies
run: flutter pub get
- name: Analyze Dart code
run: flutter analyze --no-fatal-infos
- name: Run Flutter tests
run: flutter test
- name: Run build.ps1 (version control)
run: pwsh scripts/build.ps1
- name: Build Web
run: flutter build web --release --dart-define-from-file=nexai_release.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: NexAI-Web
path: build/web/
retention-days: 30
build-windows:
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.build_target == 'windows' || github.event.inputs.build_target == 'all' }}
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: '9.0.x'
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
- name: Compute version metadata
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$commitHash = (git rev-parse HEAD).Trim()
$shortHash = $commitHash.Substring(0, 9)
$versionName = '1.0.7'
if (Test-Path 'pubspec.yaml') {
$line = Select-String -Path 'pubspec.yaml' -Pattern '^\s*version:\s*([\d\.]+)' | Select-Object -First 1
if ($line -and $line.Matches.Count -gt 0) {
$versionName = $line.Matches[0].Groups[1].Value
}
}
$displayName = "$versionName-$shortHash"
"VERSION_NAME=$versionName" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"SHORT_HASH=$shortHash" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"VERSION_DISPLAY=$displayName" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "WinUI version: $displayName"
- name: Restore WinUI solution
shell: pwsh
run: |
msbuild winui\NexAI.WinUI3.sln /t:Restore /p:Configuration=Release /p:Platform=x64 /v:m
- name: Precheck WinUI resource keys
shell: pwsh
run: |
# Fail fast with ALL PRI resource/scope collisions in one pass.
# MSBuild PRI generation typically only surfaces the first collision.
pwsh -File scripts/check-winui-resw.ps1 -CheckCodeReferences
- name: Build WinUI3 client
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
$log = (Join-Path $PWD 'artifacts/winui-build.log')
# Avoid ';' inside /flp: PowerShell/GHA argument parsing can split it and
# MSBuild then treats the log path as a second project (MSB1008).
$msbuildArgs = @(
'winui/NexAI.WinUI3/NexAI.WinUI3.csproj',
'/p:Configuration=Release',
'/p:Platform=x64',
'/p:WindowsPackageType=None',
'/p:WindowsAppSDKSelfContained=true',
'/p:Restore=false',
'/v:n',
'/fl',
"/flp:LogFile=$log",
'/flp:Verbosity=normal'
)
Write-Host ("msbuild " + ($msbuildArgs -join ' '))
& msbuild @msbuildArgs
$code = $LASTEXITCODE
if ($code -ne 0) {
Write-Host '--- WinUI build failed. Emitting error/warning summary from log ---'
if (Test-Path -LiteralPath $log) {
Select-String -LiteralPath $log -Pattern ' error | warning ' | ForEach-Object { $_.Line }
} else {
Write-Host "Log file not found: $log"
}
exit $code
}
- name: Upload WinUI build log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: winui-build-log
path: artifacts/winui-build.log
if-no-files-found: ignore
retention-days: 14
- name: Package Windows full zip
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$candidates = @(
'winui/NexAI.WinUI3/bin/x64/Release/net9.0-windows10.0.19041.0',
'winui/NexAI.WinUI3/bin/Release/net9.0-windows10.0.19041.0',
'winui/NexAI.WinUI3/bin/x64/Release/net9.0-windows10.0.19041.0/win-x64',
'winui/NexAI.WinUI3/bin/Release/net9.0-windows10.0.19041.0/win-x64'
)
$source = $null
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
$exe = Get-ChildItem -Path $candidate -Filter NexAI.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($exe) {
$source = $exe.Directory.FullName
break
}
}
}
if (-not $source) {
$exe = Get-ChildItem -Path 'winui/NexAI.WinUI3/bin' -Filter NexAI.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($exe) { $source = $exe.Directory.FullName }
}
if (-not $source) {
Get-ChildItem -Recurse winui/NexAI.WinUI3/bin -ErrorAction SilentlyContinue | Select-Object -First 80 FullName
throw 'WinUI build output not found.'
}
$version = if ($env:VERSION_DISPLAY) { $env:VERSION_DISPLAY } else { 'dev' }
$zipName = "NexAI_windows_${version}_x64.zip"
$zipPath = Join-Path $PWD $zipName
if (Test-Path $zipPath) { Remove-Item -Force $zipPath }
Write-Host "Packaging from: $source"
Compress-Archive -Path (Join-Path $source '*') -DestinationPath $zipPath -Force
$hash = (Get-FileHash -Algorithm SHA256 -Path $zipPath).Hash.ToLowerInvariant()
Set-Content -Path 'WINDOWS_SHA256SUMS.txt' -Value "$hash $zipName" -Encoding utf8
"WINDOWS_ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"WINDOWS_ZIP_PATH=$zipPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Get-Item $zipPath | Format-List FullName, Length
Get-Content 'WINDOWS_SHA256SUMS.txt'
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: NexAI-Windows
path: |
${{ env.WINDOWS_ZIP_PATH }}
WINDOWS_SHA256SUMS.txt
retention-days: 30
- name: Publish Windows zip to GitHub Release
if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
make_latest: true
tag_name: "v${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
prerelease: false
name: "NexAI ${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}"
body: |
## NexAI ${{ env.VERSION_NAME }}-${{ env.SHORT_HASH }}
### Windows (WinUI3)
- Full runnable package: `${{ env.WINDOWS_ZIP_NAME }}`
- Native WinUI3 client from `winui/`
- Checksum file: `WINDOWS_SHA256SUMS.txt`
files: |
${{ env.WINDOWS_ZIP_PATH }}
WINDOWS_SHA256SUMS.txt