Skip to content

Build Android APK

Build Android APK #17

Workflow file for this run

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 }}`