name: Build Android APK on: # Release tags only — building the ~100 MB APK on every master push # burned Gitea runner minutes without producing a useful artifact. # Use workflow_dispatch for on-demand dev builds. push: tags: ['v*'] 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' # Surfaced at job level (not step level) so the `if: env.X != ''` # check on the Decode step actually sees it — step-level env is # NOT available in that step's own `if:` expression, which # silently skipped the decode and produced debug-signed release # APKs until it was noticed. ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} 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: Guard release tag against missing keystore # Release tags MUST produce a release-signed APK, otherwise existing # installs can't upgrade (signature mismatch). Fail loudly instead # of silently falling back to the debug signing config. # Runs before JDK/Python/SDK/NDK setup so a misconfigured release # tag fails in seconds instead of after several minutes of setup. if: ${{ steps.label.outputs.is_release == 'true' && env.ANDROID_KEYSTORE_BASE64 == '' }} run: | echo "::error::Release tag ${{ gitea.ref_name }} requires ANDROID_KEYSTORE_BASE64 (plus KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD) to be configured in Gitea → Settings → Secrets." exit 1 - 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: | set -euo pipefail # 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. The parent dir is .gitignore'd (the junction # target is the real content), so we have to create it first. mkdir -p android/app/src/main/python ln -sfn "$(pwd)/server/src/ledgrab" android/app/src/main/python/ledgrab ls -la android/app/src/main/python/ # Sanity check — readlink resolves the link and the directory exists. test -d android/app/src/main/python/ledgrab - name: Decode signing keystore id: keystore if: env.ANDROID_KEYSTORE_BASE64 != '' run: | set -euo pipefail 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 (upsert) 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 }}" if [ -z "${GITEA_TOKEN:-}" ]; then echo "::error::DEPLOY_TOKEN secret not configured — cannot attach APK" exit 1 fi # Upsert: look up release by tag. If it exists, reuse it; if 404, # create one. Makes the Android workflow self-sufficient — no # ordering dependency on release.yml's create-release job. HTTP=$(curl -s -o /tmp/release.json -w "%{http_code}" \ "$BASE_URL/releases/tags/$TAG" \ -H "Authorization: token $GITEA_TOKEN") case "$HTTP" in 200) RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['id'])") echo "Found existing release id=$RELEASE_ID" ;; 404) echo "No release for tag $TAG — creating one" IS_PRE="false" if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then IS_PRE="true" fi CREATE_HTTP=$(curl -s -o /tmp/created.json -w "%{http_code}" \ -X POST "$BASE_URL/releases" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"$TAG\",\"name\":\"LedGrab $TAG\",\"draft\":false,\"prerelease\":$IS_PRE}") if [ "$CREATE_HTTP" != "201" ] && [ "$CREATE_HTTP" != "200" ]; then echo "::error::Failed to create release (HTTP $CREATE_HTTP)" cat /tmp/created.json exit 1 fi RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/created.json'))['id'])") echo "Created release id=$RELEASE_ID" ;; *) echo "::error::Unexpected HTTP $HTTP when looking up release for tag $TAG" cat /tmp/release.json exit 1 ;; esac # Replace existing asset if present (re-run safety). EXISTING_ID=$(curl -fsS "$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'),''))") if [ -n "$EXISTING_ID" ]; then curl -fsS -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ID" \ -H "Authorization: token $GITEA_TOKEN" echo "Replaced existing asset: $APK_NAME" fi # -f: exit non-zero on 4xx/5xx so a broken token fails the job # loudly instead of the previous silent "Uploaded" lie. curl -fsS -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"