name: Build Android APK on: push: branches: [master] tags: ['v*'] paths: - 'android/**' - 'server/src/ledgrab/**' - '.gitea/workflows/build-android.yml' workflow_dispatch: inputs: version: description: 'Version label (e.g. dev, 0.3.0-test)' required: false default: 'dev' jobs: build-android: runs-on: ubuntu-latest env: JAVA_VERSION: '17' PYTHON_VERSION: '3.11' ANDROID_CMDLINE_TOOLS_VERSION: '11076708' ANDROID_SDK_PLATFORM: 'android-34' ANDROID_BUILD_TOOLS: '34.0.0' ANDROID_NDK_VERSION: '26.1.10909125' steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Resolve build label id: label run: | REF="${{ gitea.ref_name }}" if echo "$REF" | grep -qE '^v[0-9]'; then LABEL="${REF#v}" IS_RELEASE="true" elif [ -n "${{ inputs.version }}" ]; then LABEL="${{ inputs.version }}" IS_RELEASE="false" else LABEL="dev-${{ gitea.sha }}" IS_RELEASE="false" fi LABEL="${LABEL:0:40}" echo "label=$LABEL" >> "$GITHUB_OUTPUT" echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT" echo "Build label: $LABEL (release=$IS_RELEASE)" - name: Setup JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ env.JAVA_VERSION }} - name: Setup Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup Android SDK + NDK run: | set -euo pipefail SDK_ROOT="$HOME/android-sdk" mkdir -p "$SDK_ROOT/cmdline-tools" cd "$SDK_ROOT/cmdline-tools" curl -sSL --retry 3 \ -o cmdline-tools.zip \ "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" unzip -q cmdline-tools.zip rm cmdline-tools.zip mv cmdline-tools latest export ANDROID_SDK_ROOT="$SDK_ROOT" export ANDROID_HOME="$SDK_ROOT" SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" yes | "$SDKMANAGER" --licenses > /dev/null 2>&1 || true "$SDKMANAGER" --install \ "platform-tools" \ "platforms;${ANDROID_SDK_PLATFORM}" \ "build-tools;${ANDROID_BUILD_TOOLS}" \ "ndk;${ANDROID_NDK_VERSION}" > /dev/null echo "ANDROID_SDK_ROOT=$SDK_ROOT" >> "$GITHUB_ENV" echo "ANDROID_HOME=$SDK_ROOT" >> "$GITHUB_ENV" echo "ANDROID_NDK_HOME=$SDK_ROOT/ndk/${ANDROID_NDK_VERSION}" >> "$GITHUB_ENV" echo "$SDK_ROOT/platform-tools" >> "$GITHUB_PATH" echo "$SDK_ROOT/cmdline-tools/latest/bin" >> "$GITHUB_PATH" - name: Create local.properties run: | echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties - name: Link Python source (junction equivalent) run: | # Chaquopy reads Python modules from android/app/src/main/python/ # On Windows dev machines this is a directory junction; on Linux CI use a symlink. ln -sfn "$(pwd)/server/src/ledgrab" android/app/src/main/python/ledgrab ls -la android/app/src/main/python/ - name: Decode signing keystore id: keystore if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} run: | mkdir -p android/keystore echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/keystore/release.jks echo "path=$(pwd)/android/keystore/release.jks" >> "$GITHUB_OUTPUT" echo "present=true" >> "$GITHUB_OUTPUT" - name: Build APK working-directory: android env: ANDROID_KEYSTORE_PATH: ${{ steps.keystore.outputs.path }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | chmod +x gradlew if [ "${{ steps.keystore.outputs.present }}" = "true" ] && [ "${{ steps.label.outputs.is_release }}" = "true" ]; then echo "Building signed release APK" ./gradlew --no-daemon assembleRelease else echo "Building debug APK (no signing keystore available or not a release tag)" ./gradlew --no-daemon assembleDebug fi - name: Locate and rename APK id: apk run: | set -euo pipefail SRC=$(ls android/app/build/outputs/apk/release/*.apk 2>/dev/null | head -1 || true) if [ -z "$SRC" ]; then SRC=$(ls android/app/build/outputs/apk/debug/*.apk | head -1) VARIANT="debug" else VARIANT="release" fi DEST="build/LedGrab-${{ steps.label.outputs.label }}-android-${VARIANT}.apk" mkdir -p build cp "$SRC" "$DEST" echo "path=$DEST" >> "$GITHUB_OUTPUT" echo "name=$(basename "$DEST")" >> "$GITHUB_OUTPUT" ls -lh "$DEST" - name: Upload APK artifact uses: actions/upload-artifact@v3 with: name: LedGrab-${{ steps.label.outputs.label }}-android path: ${{ steps.apk.outputs.path }} retention-days: 90 - name: Attach APK to Gitea release if: ${{ steps.label.outputs.is_release == 'true' }} env: GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }} run: | set -euo pipefail TAG="${{ gitea.ref_name }}" BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" APK_PATH="${{ steps.apk.outputs.path }}" APK_NAME="${{ steps.apk.outputs.name }}" # Fetch release by tag (created by release.yml `create-release` job) RELEASE_ID=$(curl -s "$BASE_URL/releases/tags/$TAG" \ -H "Authorization: token $GITEA_TOKEN" \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") if [ -z "$RELEASE_ID" ]; then echo "::warning::No release found for tag $TAG — skipping asset upload" exit 0 fi # Replace existing asset if present (re-run safety) EXISTING_ID=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \ -H "Authorization: token $GITEA_TOKEN" \ | python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$APK_NAME'),''))" 2>/dev/null) if [ -n "$EXISTING_ID" ]; then curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \ -H "Authorization: token $GITEA_TOKEN" fi curl -s -X POST \ "$BASE_URL/releases/$RELEASE_ID/assets?name=$APK_NAME" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary "@$APK_PATH" echo "Uploaded: $APK_NAME"