a026f0b349
Move the keystore guard from after the Decode step (step 9) to right after Resolve build label (step 3). A release tag pushed without ANDROID_KEYSTORE_BASE64 configured now fails in seconds instead of after JDK + Python + Android SDK + NDK install (~3-5 min of wasted runner time). Switched the condition from steps.keystore.outputs.present to env.ANDROID_KEYSTORE_BASE64 since the env var is set at job level and the keystore decode step has not yet run at the new position.
249 lines
10 KiB
YAML
249 lines
10 KiB
YAML
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"
|