Build Android APK #17
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 Android APK | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| build_type: | |
| description: 'Build type' | |
| required: true | |
| default: 'debug' | |
| type: choice | |
| options: | |
| - debug | |
| - release | |
| permissions: | |
| contents: write | |
| actions: read | |
| jobs: | |
| build-libtailscale: | |
| uses: ./.github/workflows/build-libtailscale-aar.yml | |
| build: | |
| needs: build-libtailscale | |
| runs-on: ubuntu-latest | |
| env: | |
| ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Install web dependencies | |
| run: npm ci | |
| working-directory: web | |
| - name: Build web app | |
| run: npm run build | |
| working-directory: web | |
| - name: Setup JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v3 | |
| - name: Install Android 34 SDK packages | |
| run: sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" | |
| - name: Accept Android SDK licenses | |
| run: yes | sdkmanager --licenses | |
| - name: Add Capacitor Android platform | |
| run: | | |
| if [ ! -d android ]; then | |
| npx cap add android | |
| fi | |
| working-directory: web | |
| - name: Generate Android app icon assets | |
| run: npx @capacitor/assets generate --android --assetPath public --iconBackgroundColor '#0b1020' --iconBackgroundColorDark '#0b1020' | |
| working-directory: web | |
| - name: Sync Capacitor project | |
| run: npx cap sync android | |
| working-directory: web | |
| - name: Download libtailscale AAR | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ needs.build-libtailscale.outputs.artifact_name }} | |
| path: web/android/app/libs | |
| - name: Apply Tailscale Gradle config | |
| run: | | |
| BUILDGRADLE="web/android/app/build.gradle" | |
| # Set minSdk to 26 (required by libtailscale) | |
| sed -i 's/minSdkVersion [0-9]*/minSdkVersion 26/g' "$BUILDGRADLE" | |
| sed -i 's/minSdk [0-9]*/minSdk 26/g' "$BUILDGRADLE" | |
| # Add libtailscale AAR dependency + kotlinx-coroutines | |
| python3 -c "import re; f='$BUILDGRADLE'; c=open(f).read(); c=re.sub(r'(dependencies\s*\{)', r'\1\n implementation fileTree(dir: \"libs\", include: [\"*.aar\", \"*.jar\"])\n implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3\"', c); open(f,'w').write(c)" | |
| - name: Add Tailscale permissions to AndroidManifest | |
| run: | | |
| # Add INTERNET and FOREGROUND_SERVICE permissions if not already present | |
| for perm in "android.permission.INTERNET" "android.permission.FOREGROUND_SERVICE"; do | |
| grep -q "$perm" web/android/app/src/main/AndroidManifest.xml || \ | |
| sed -i "/<manifest/a\ <uses-permission android:name=\"$perm\" />" web/android/app/src/main/AndroidManifest.xml | |
| done | |
| - name: Read app version | |
| id: version | |
| run: | | |
| APP_VERSION=$(node -p "require('./web/package.json').version") | |
| VERSION_CODE=$(node -e "const [major, minor, patch] = process.argv[1].split('.').map(Number); console.log((major * 10000) + (minor * 100) + patch)" "$APP_VERSION") | |
| echo "app_version=$APP_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT" | |
| - name: Sync Android version metadata | |
| run: node web/scripts/sync-android-version.mjs "${{ steps.version.outputs.app_version }}" "${{ steps.version.outputs.version_code }}" | |
| - name: Build APK | |
| run: ./gradlew assemble${{ inputs.build_type == 'debug' && 'Debug' || 'Release' }} --stacktrace --no-daemon | |
| working-directory: web/android | |
| - name: Require signing secrets for tagged release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| run: | | |
| test -n "$ANDROID_KEYSTORE_BASE64" || (echo "Missing ANDROID_KEYSTORE_BASE64" && exit 1) | |
| test -n "$ANDROID_KEYSTORE_PASSWORD" || (echo "Missing ANDROID_KEYSTORE_PASSWORD" && exit 1) | |
| test -n "$ANDROID_KEY_ALIAS" || (echo "Missing ANDROID_KEY_ALIAS" && exit 1) | |
| test -n "$ANDROID_KEY_PASSWORD" || (echo "Missing ANDROID_KEY_PASSWORD" && exit 1) | |
| - name: Decode Android release keystore | |
| if: ${{ inputs.build_type == 'release' && env.ANDROID_KEYSTORE_BASE64 != '' && env.ANDROID_KEYSTORE_PASSWORD != '' && env.ANDROID_KEY_ALIAS != '' && env.ANDROID_KEY_PASSWORD != '' }} | |
| run: | | |
| echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > web/android/release.keystore | |
| - name: Sign release APK | |
| if: ${{ inputs.build_type == 'release' && env.ANDROID_KEYSTORE_BASE64 != '' && env.ANDROID_KEYSTORE_PASSWORD != '' && env.ANDROID_KEY_ALIAS != '' && env.ANDROID_KEY_PASSWORD != '' }} | |
| run: | | |
| "$ANDROID_HOME/build-tools/34.0.0/apksigner" sign \ | |
| --ks web/android/release.keystore \ | |
| --ks-pass pass:"$ANDROID_KEYSTORE_PASSWORD" \ | |
| --ks-key-alias "$ANDROID_KEY_ALIAS" \ | |
| --key-pass pass:"$ANDROID_KEY_PASSWORD" \ | |
| --out web/android/app/build/outputs/apk/release/app-release-signed.apk \ | |
| web/android/app/build/outputs/apk/release/app-release-unsigned.apk | |
| - name: Verify release APK signature | |
| if: ${{ inputs.build_type == 'release' && env.ANDROID_KEYSTORE_BASE64 != '' && env.ANDROID_KEYSTORE_PASSWORD != '' && env.ANDROID_KEY_ALIAS != '' && env.ANDROID_KEY_PASSWORD != '' }} | |
| run: | | |
| "$ANDROID_HOME/build-tools/34.0.0/apksigner" verify --verbose web/android/app/build/outputs/apk/release/app-release-signed.apk | |
| - name: Select APK path | |
| id: apk | |
| run: | | |
| if [ "${{ inputs.build_type }}" = "debug" ]; then | |
| echo "path=web/android/app/build/outputs/apk/debug/app-debug.apk" >> "$GITHUB_OUTPUT" | |
| elif [ -f web/android/app/build/outputs/apk/release/app-release-signed.apk ]; then | |
| echo "path=web/android/app/build/outputs/apk/release/app-release-signed.apk" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "path=web/android/app/build/outputs/apk/release/app-release-unsigned.apk" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Upload APK artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: opencode-remote-${{ inputs.build_type }}-apk-v${{ steps.version.outputs.app_version }} | |
| path: ${{ steps.apk.outputs.path }} | |
| - name: Publish GitHub release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: OpenCode Remote v${{ steps.version.outputs.app_version }} | |
| files: ${{ steps.apk.outputs.path }} | |
| generate_release_notes: true | |
| body: | | |
| ## Release | |
| Stable Android release APK for OpenCode Remote `${{ steps.version.outputs.app_version }}`. | |
| ## Notes | |
| - Built from `${{ github.sha }}` | |
| - Version code `${{ steps.version.outputs.version_code }}` |