fix(android): tie the Flutter engine lifecycle back to the activity #621
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |