Build APK (Self-Healing) #21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build APK (Self-Healing) | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: build-apk-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| env: | |
| CI: true | |
| GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=-Xmx4g | |
| JAVA_TOOL_OPTIONS: -Xmx4g | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 1 | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| cache: gradle | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| gradle-version: '8.2' | |
| - name: Set up Android SDK base tools | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| packages: '' | |
| - name: Build with auto-repair | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| shopt -s nullglob | |
| log() { | |
| echo "[$(date +'%H:%M:%S')] $*" | |
| } | |
| detect_project_root() { | |
| if [ -f "./settings.gradle" ] || [ -f "./settings.gradle.kts" ] || [ -f "./gradlew" ]; then | |
| echo "." | |
| return 0 | |
| fi | |
| local found | |
| found="$(find . -maxdepth 4 -type f \ | |
| \( -name "settings.gradle" -o -name "settings.gradle.kts" -o -name "gradlew" \) \ | |
| ! -path "./.git/*" ! -path "./.github/*" | head -n 1 || true)" | |
| if [ -z "$found" ]; then | |
| return 1 | |
| fi | |
| dirname "$found" | |
| } | |
| PROJECT_ROOT="$(detect_project_root)" || { | |
| echo "Could not find Android project root." | |
| exit 1 | |
| } | |
| log "Project root: $PROJECT_ROOT" | |
| cd "$PROJECT_ROOT" | |
| SDKMANAGER="$(command -v sdkmanager || true)" | |
| if [ -z "$SDKMANAGER" ]; then | |
| echo "sdkmanager not found in PATH" | |
| exit 1 | |
| fi | |
| detect_compile_sdk() { | |
| local sdk | |
| sdk="$(grep -RhoE \ | |
| 'compileSdkVersion[[:space:]]*[= ]*[0-9]+|compileSdk[[:space:]]*[= ]*[0-9]+' \ | |
| . --include='*.gradle' --include='*.kts' 2>/dev/null \ | |
| | grep -oE '[0-9]+' | head -n 1 || true)" | |
| if [ -z "$sdk" ]; then | |
| sdk="34" | |
| fi | |
| echo "$sdk" | |
| } | |
| COMPILE_SDK="$(detect_compile_sdk)" | |
| DEFAULT_BUILD_TOOLS="${COMPILE_SDK}.0.0" | |
| log "Detected compileSdk: $COMPILE_SDK" | |
| log "Default build-tools target: $DEFAULT_BUILD_TOOLS" | |
| write_local_properties() { | |
| local sdk_dir="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" | |
| if [ -n "$sdk_dir" ]; then | |
| printf 'sdk.dir=%s\n' "$sdk_dir" > local.properties | |
| log "Wrote local.properties with sdk.dir=$sdk_dir" | |
| fi | |
| } | |
| accept_licenses() { | |
| yes | "$SDKMANAGER" --licenses >/dev/null || true | |
| } | |
| install_sdk_package() { | |
| local pkg="$1" | |
| local attempt | |
| for attempt in 1 2 3; do | |
| log "Installing SDK package: $pkg (attempt $attempt/3)" | |
| if "$SDKMANAGER" --install "$pkg"; then | |
| return 0 | |
| fi | |
| sleep $((attempt * 5)) | |
| done | |
| return 1 | |
| } | |
| latest_build_tools_for_api() { | |
| local api="$1" | |
| "$SDKMANAGER" --list 2>/dev/null \ | |
| | tr -d '\r' \ | |
| | grep -oE "build-tools;${api}\.[0-9]+\.[0-9]+" \ | |
| | sort -V \ | |
| | tail -n 1 || true | |
| } | |
| ensure_android_packages() { | |
| accept_licenses | |
| install_sdk_package "platform-tools" || true | |
| install_sdk_package "platforms;android-${COMPILE_SDK}" || true | |
| if ! install_sdk_package "build-tools;${DEFAULT_BUILD_TOOLS}"; then | |
| local latest_bt | |
| latest_bt="$(latest_build_tools_for_api "$COMPILE_SDK")" | |
| if [ -n "$latest_bt" ]; then | |
| install_sdk_package "$latest_bt" || true | |
| fi | |
| fi | |
| } | |
| ensure_gradle_bootstrap() { | |
| if [ -f "./gradlew" ]; then | |
| chmod +x ./gradlew | |
| fi | |
| if [ ! -f "./gradle/wrapper/gradle-wrapper.jar" ]; then | |
| log "gradle-wrapper.jar missing. Trying to regenerate wrapper with system Gradle." | |
| gradle wrapper --gradle-version 8.2 --no-daemon || true | |
| fi | |
| if [ -f "./gradlew" ]; then | |
| chmod +x ./gradlew | |
| GRADLE_CMD="./gradlew" | |
| else | |
| GRADLE_CMD="gradle" | |
| fi | |
| log "Using Gradle command: $GRADLE_CMD" | |
| } | |
| fix_from_log() { | |
| local logfile="$1" | |
| local changed=1 | |
| if grep -qiE 'SDK location not found|local.properties' "$logfile"; then | |
| write_local_properties | |
| changed=0 | |
| fi | |
| if grep -qiE 'License .* not accepted|licenses not accepted' "$logfile"; then | |
| accept_licenses | |
| changed=0 | |
| fi | |
| if grep -qiE 'GradleWrapperMain|gradle-wrapper\.jar' "$logfile"; then | |
| ensure_gradle_bootstrap | |
| changed=0 | |
| fi | |
| if grep -qiE 'Permission denied' "$logfile" && [ -f "./gradlew" ]; then | |
| chmod +x ./gradlew | |
| changed=0 | |
| fi | |
| local api | |
| api="$(grep -oE 'android-[0-9]+' "$logfile" | grep -oE '[0-9]+' | tail -n 1 || true)" | |
| if [ -n "$api" ]; then | |
| if grep -qiE 'failed to find target with hash string|platforms;android-|compileSdkVersion is not specified|compileSdk' "$logfile"; then | |
| install_sdk_package "platforms;android-${api}" || true | |
| changed=0 | |
| fi | |
| fi | |
| local bt | |
| bt="$(grep -oE 'build-tools;[0-9]+\.[0-9]+\.[0-9]+|Build Tools revision [0-9]+\.[0-9]+\.[0-9]+' "$logfile" \ | |
| | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" | |
| if [ -n "$bt" ]; then | |
| install_sdk_package "build-tools;${bt}" || true | |
| changed=0 | |
| fi | |
| if grep -qiE 'Could not resolve|Failed to transform|Connection reset|Read timed out|502 Bad Gateway|503 Service Unavailable|Temporary failure' "$logfile"; then | |
| EXTRA_GRADLE_ARGS="--refresh-dependencies" | |
| changed=0 | |
| fi | |
| return "$changed" | |
| } | |
| run_build_once() { | |
| local attempt="$1" | |
| local logfile="build-attempt-${attempt}.log" | |
| set +e | |
| "$GRADLE_CMD" \ | |
| clean \ | |
| assembleDebug \ | |
| --stacktrace \ | |
| --no-daemon \ | |
| --no-configuration-cache \ | |
| ${EXTRA_GRADLE_ARGS:-} \ | |
| 2>&1 | tee "$logfile" | |
| local exit_code="${PIPESTATUS[0]}" | |
| set -e | |
| return "$exit_code" | |
| } | |
| mkdir -p ci-logs | |
| write_local_properties | |
| ensure_android_packages | |
| ensure_gradle_bootstrap | |
| build_ok=0 | |
| max_attempts=5 | |
| for attempt in $(seq 1 "$max_attempts"); do | |
| log "Starting build attempt ${attempt}/${max_attempts}" | |
| EXTRA_GRADLE_ARGS="${EXTRA_GRADLE_ARGS:-}" | |
| if run_build_once "$attempt"; then | |
| build_ok=1 | |
| log "Build succeeded on attempt ${attempt}" | |
| mv "build-attempt-${attempt}.log" "ci-logs/" | |
| break | |
| fi | |
| mv "build-attempt-${attempt}.log" "ci-logs/" || true | |
| log "Build failed on attempt ${attempt}" | |
| if fix_from_log "ci-logs/build-attempt-${attempt}.log"; then | |
| log "No known auto-fix matched this failure. Stopping retries." | |
| break | |
| fi | |
| log "Applied auto-fix. Retrying..." | |
| sleep 5 | |
| done | |
| echo "=== APK files found ===" | |
| find . -type f \( -path "*/build/outputs/apk/*/*.apk" -o -path "*/build/outputs/apk/*.apk" \) | sed 's#^#- #' | |
| if [ "$build_ok" -ne 1 ]; then | |
| echo "Build did not succeed after retries." | |
| exit 1 | |
| fi | |
| - name: Upload APKs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: apk-files | |
| path: | | |
| **/build/outputs/apk/debug/*.apk | |
| **/build/outputs/apk/release/*.apk | |
| if-no-files-found: warn | |
| - name: Upload logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-logs | |
| path: | | |
| **/ci-logs/*.log | |
| **/build/reports/** | |
| if-no-files-found: warn |