# Build a signed release APK and publish it as a Forgejo release on every v* tag. # Obtainium then tracks releases and offers the update. # # Requires: # - A Forgejo Actions RUNNER registered to this repo/instance (see docs/DISTRIBUTION.md), # with a label this job's `runs-on` matches (adjust below to your runner's labels). # - Repo Actions secrets: ANDROID_KEYSTORE_B64, ANDROID_KEYSTORE_PASSWORD, # ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD (already set). # # Release: bump `expo.version` in app/app.json, then: git tag v0.1.1 && git push origin v0.1.1 name: build-apk on: push: tags: - 'v*' jobs: build: # Match this to a label your runner registered with (commonly "docker" or "ubuntu-latest"). runs-on: docker # A purpose-built RN Android image (Android SDK + NDK + Node). Runner needs Docker. container: image: reactnativecommunity/react-native-android:latest # Hard caps so the build can't starve the 20GB/4-core host: 2 CPUs, 8 GB RAM, # and NO container swap (--memory-swap == --memory) so it OOM-fails cleanly # instead of thrashing the disk with swap. With the JVM caps below it uses ~5 GB. options: --cpus=2 --memory=8g --memory-swap=8g # Persistent caches (whitelisted in the runner's config.yaml valid_volumes). # Gradle home = downloaded Maven deps + build cache; npm = package cache. # Repeat builds skip the re-download/re-compile that dominate a cold build. volumes: - bigbrainparking-gradle:/opt/cache/gradle - bigbrainparking-npm:/opt/cache/npm env: GRADLE_USER_HOME: /opt/cache/gradle npm_config_cache: /opt/cache/npm # Applies to EVERY JVM the build spawns (Gradle workers, artifact transforms, # Kotlin daemon) — the runner container has no IPv6 route, so without this the # transform workers try dl.google.com over IPv6 and get "Network is unreachable". JAVA_TOOL_OPTIONS: "-Djava.net.preferIPv4Stack=true" steps: - uses: actions/checkout@v4 - name: Install dependencies run: npm install - name: Build the API client run: npm run build --workspace parksmarter-client # Unit tests (mocked fetch) gate the build; integration tests skip here # because the runner can't reach the IP-restricted non-prod instances. - name: Run client tests run: npm test --workspace parksmarter-client # Pre-install the NDK + CMake the RN native modules need. Do it here (not via # Gradle's mid-build auto-install) and retry: the SDK mirror sometimes serves # a truncated archive ("Archive is not a ZIP archive"), which self-heals on a # clean retry. Clearing the partial dir between tries avoids reusing the bad file. - name: Ensure NDK + CMake run: | yes | sdkmanager --licenses >/dev/null 2>&1 || true ok=0 for i in 1 2 3; do if sdkmanager "ndk;27.1.12297006" "cmake;3.22.1"; then ok=1; break; fi echo "sdkmanager attempt $i failed; clearing partial CMake and retrying" rm -rf "${ANDROID_HOME:-/opt/android}/cmake/3.22.1" "${ANDROID_SDK_ROOT:-/opt/android}/cmake/3.22.1" 2>/dev/null || true sleep 5 done [ "$ok" = 1 ] || echo "WARNING: NDK/CMake pre-install did not confirm; the build may retry it." - name: Restore signing keystore run: echo "${{ secrets.ANDROID_KEYSTORE_B64 }}" | base64 -d > "${{ github.workspace }}/app/release.keystore" - name: Expo prebuild (generate android/) working-directory: app run: CI=1 npx expo prebuild --platform android --no-install # The real memory hog is the Kotlin daemon (default ~5.4 GB heap EACH, several # spawned). Cap every JVM in gradle.properties so the build fits comfortably. - name: Cap Gradle/Kotlin memory working-directory: app/android run: | cat >> gradle.properties <<'PROPS' # --- CI resource caps (host is small; don't thrash swap) --- org.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true org.gradle.workers.max=2 org.gradle.parallel=false org.gradle.daemon=false org.gradle.caching=true kotlin.daemon.jvmargs=-Xmx1536m PROPS - name: Assemble signed release working-directory: app/android env: BBP_UPLOAD_STORE_FILE: ${{ github.workspace }}/app/release.keystore BBP_UPLOAD_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} BBP_UPLOAD_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} BBP_UPLOAD_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} # Also cap the no-daemon launcher JVM (org.gradle.jvmargs targets the daemon). GRADLE_OPTS: "-Xmx1536m -Djava.net.preferIPv4Stack=true" # --max-workers=2 limits concurrent tasks; container --cpus=2 caps clang -j. # --build-cache reuses task outputs from GRADLE_USER_HOME across runs. run: >- ./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon --max-workers=2 --build-cache # Self-contained publish via the Forgejo API (no external action to fetch). - name: Publish release + upload APK env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e TAG="${GITHUB_REF_NAME}" APK="$(ls app/android/app/build/outputs/apk/release/*.apk | head -1)" ASSET="BigBrainParking-${TAG}.apk" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" REL="$(curl -sf -X POST "$API/releases" -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"main\",\"name\":\"BigBrainParking $TAG\",\"body\":\"Install/update via Obtainium.\"}")" ID="$(printf '%s' "$REL" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).id))")" curl -sf -X POST "$API/releases/$ID/assets?name=$ASSET" \ -H "Authorization: token $TOKEN" -F "attachment=@$APK" echo "Published $TAG"