All checks were successful
Build Android APK / build-apk (push) Successful in 40m23s
The build succeeds but publishing failed with "$FORGEJO_PATH: ambiguous redirect" — the moving actions/forgejo-release@v2 tag updated to a broken version. Replace it with direct Forgejo API calls (create release, delete any prior same-named asset, upload the APK) using the built-in token, so the last step is under our control. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
168 lines
8 KiB
YAML
168 lines
8 KiB
YAML
name: Build Android APK
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
build-apk:
|
|
runs-on: docker
|
|
container:
|
|
image: node:22-bookworm
|
|
# Persistent caches across runs (Docker named volumes). These must be
|
|
# allowed in the runner's config.yaml `container.valid_volumes` — the
|
|
# deploy-runner.sh script sets that up. First run populates them (~1h);
|
|
# later runs reuse the SDK/NDK, Gradle deps + build cache, and npm cache,
|
|
# dropping the build to a few minutes.
|
|
volumes:
|
|
- camptickets-android-sdk:/opt/android-sdk
|
|
- camptickets-gradle:/root/.gradle
|
|
- camptickets-npm:/root/.npm
|
|
env:
|
|
ANDROID_HOME: /opt/android-sdk
|
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
GRADLE_USER_HOME: /root/.gradle
|
|
# Force IPv4 for all JVMs (Gradle launcher, daemon, Kotlin/CMake workers).
|
|
# The runner host has no working IPv6 route, so Maven Central (which has
|
|
# AAAA records) was unreachable — this makes Java ignore AAAA and use IPv4.
|
|
JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true
|
|
# Cap the C/C++ (ninja) compiler jobs per native-build task to 2.
|
|
# (Memory/parallelism caps live in gradle.properties — see the step below.)
|
|
CMAKE_BUILD_PARALLEL_LEVEL: "2"
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Resolve version from tag
|
|
id: ver
|
|
run: |
|
|
TAG="${GITHUB_REF_NAME:-v0.0.0}"
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
node ci/set-version.mjs "$TAG"
|
|
|
|
- name: Install JDK 17 and tools
|
|
run: |
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip wget git
|
|
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
|
|
|
|
- name: Cap Gradle & Kotlin memory (runner host is small)
|
|
run: |
|
|
set -eu
|
|
# The runner host (~20GB/4-core, shared with Forgejo + databases) OOM-
|
|
# thrashes if this build runs unbounded. org.gradle.jvmargs only bounds
|
|
# the Gradle JVM — the Kotlin compile daemons fork separately and
|
|
# default to a huge heap (~5GB each), so kotlin.daemon.jvmargs is the
|
|
# load-bearing cap here. Written to the persistent GRADLE_USER_HOME.
|
|
mkdir -p "$GRADLE_USER_HOME"
|
|
cat > "$GRADLE_USER_HOME/gradle.properties" <<'PROPS'
|
|
org.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true
|
|
org.gradle.daemon=false
|
|
org.gradle.parallel=false
|
|
org.gradle.caching=true
|
|
org.gradle.workers.max=2
|
|
kotlin.daemon.jvmargs=-Xmx1536m
|
|
PROPS
|
|
echo "Wrote $GRADLE_USER_HOME/gradle.properties:"; cat "$GRADLE_USER_HOME/gradle.properties"
|
|
|
|
- name: Install Android SDK (cached)
|
|
run: |
|
|
set -eu
|
|
# Skip the whole install when the cache volume already has the SDK.
|
|
# (NDK + CMake are auto-installed by Gradle into the same volume on
|
|
# the first build, so they persist too.)
|
|
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
|
echo "Installing Android command-line tools..."
|
|
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
|
cd /tmp
|
|
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdtools.zip
|
|
unzip -q cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
|
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
|
else
|
|
echo "Android SDK found in cache volume — skipping download."
|
|
fi
|
|
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
|
yes | sdkmanager --licenses >/dev/null 2>&1 || true
|
|
# Self-heal a corrupt/incomplete NDK or CMake left by a failed run
|
|
# (AGP's auto-installer sometimes leaves a truncated archive -> the
|
|
# "Archive is not a ZIP archive" failure). A valid package always has
|
|
# a source.properties; if it's missing, wipe the dir so sdkmanager
|
|
# reinstalls cleanly.
|
|
NDK_VER=27.1.12297006
|
|
CMAKE_VER=3.22.1
|
|
for p in "ndk/$NDK_VER" "cmake/$CMAKE_VER"; do
|
|
if [ -d "$ANDROID_HOME/$p" ] && [ ! -f "$ANDROID_HOME/$p/source.properties" ]; then
|
|
echo "Removing incomplete $p"; rm -rf "$ANDROID_HOME/$p"
|
|
fi
|
|
done
|
|
# Install everything (incl. NDK + CMake) via sdkmanager, which is
|
|
# checksum-verified and robust — instead of letting Gradle/AGP auto-
|
|
# download the NDK mid-build. No-op for packages already valid.
|
|
sdkmanager --install "platform-tools" \
|
|
"platforms;android-36" "platforms;android-35" \
|
|
"build-tools;36.0.0" "build-tools;35.0.0" \
|
|
"ndk;$NDK_VER" "cmake;$CMAKE_VER" >/dev/null
|
|
echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH"
|
|
echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
|
|
|
|
- name: Install app dependencies
|
|
working-directory: app
|
|
run: npm install --no-audit --no-fund
|
|
|
|
- name: Expo prebuild (android)
|
|
working-directory: app
|
|
run: npx expo prebuild -p android --no-install
|
|
|
|
- name: Decode release keystore
|
|
run: |
|
|
echo "${{ secrets.ANDROID_KEYSTORE_B64 }}" | base64 -d > "$RUNNER_TEMP/release.keystore"
|
|
|
|
- name: Build release APK
|
|
working-directory: app/android
|
|
env:
|
|
CAMPSCAN_STORE_FILE: ${{ runner.temp }}/release.keystore
|
|
CAMPSCAN_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
CAMPSCAN_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
CAMPSCAN_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
run: |
|
|
chmod +x ./gradlew
|
|
# Build a single ABI (arm64-v8a covers all modern phones) to cut native
|
|
# compilation ~4x and keep the host load down. --max-workers=2 bounds
|
|
# concurrent tasks on top of the workers.max setting.
|
|
./gradlew assembleRelease \
|
|
--init-script ../../ci/signing.gradle \
|
|
--no-daemon --build-cache --max-workers=2 \
|
|
-PreactNativeArchitectures=arm64-v8a
|
|
mkdir -p "$GITHUB_WORKSPACE/artifacts"
|
|
cp app/build/outputs/apk/release/app-release.apk \
|
|
"$GITHUB_WORKSPACE/artifacts/camp-scan-${{ steps.ver.outputs.tag }}.apk"
|
|
|
|
- name: Publish APK to Forgejo release
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ steps.ver.outputs.tag }}
|
|
run: |
|
|
set -eu
|
|
API="https://git.mowden.top/api/v1/repos/Beartaria/CampgroundTickets"
|
|
APK="$GITHUB_WORKSPACE/artifacts/camp-scan-${TAG}.apk"
|
|
AUTH="Authorization: token ${TOKEN}"
|
|
# Create the release for this tag (ignore failure if it already exists).
|
|
curl -sS -X POST "$API/releases" -H "$AUTH" -H "Content-Type: application/json" \
|
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Camp Scan ${TAG}\",\"body\":\"Install/update via Obtainium.\"}" \
|
|
-o /dev/null -w "create release: %{http_code}\n" || true
|
|
# Look up the release id by tag.
|
|
REL_ID=$(curl -sS "$API/releases/tags/${TAG}" -H "$AUTH" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
|
echo "release id: ${REL_ID}"
|
|
test -n "$REL_ID"
|
|
# Remove a same-named asset from a prior run, then upload the APK.
|
|
EXISTING=$(curl -sS "$API/releases/${REL_ID}/assets" -H "$AUTH" \
|
|
| tr '}' '\n' | grep -F "camp-scan-${TAG}.apk" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*' || true)
|
|
if [ -n "${EXISTING:-}" ]; then
|
|
curl -sS -X DELETE "$API/releases/${REL_ID}/assets/${EXISTING}" -H "$AUTH" -o /dev/null -w "delete old asset: %{http_code}\n"
|
|
fi
|
|
curl -sS -f -X POST "$API/releases/${REL_ID}/assets?name=camp-scan-${TAG}.apk" \
|
|
-H "$AUTH" -F "attachment=@${APK};type=application/vnd.android.package-archive" \
|
|
-o /dev/null -w "upload apk: %{http_code}\n"
|
|
echo "Published camp-scan-${TAG}.apk"
|