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 Forgejo release with APK uses: actions/forgejo-release@v2 with: direction: upload url: https://git.mowden.top repo: Beartaria/CampgroundTickets tag: ${{ steps.ver.outputs.tag }} token: ${{ secrets.GITHUB_TOKEN }} release-dir: artifacts release-notes: "Camp Scan ${{ steps.ver.outputs.tag }} — install/update via Obtainium." override: true