A small "Android build service inside GitHub" — drop a ZIP, run a workflow, get an APK.
AndroidForge is a GitHub-native system that automatically builds an Android APK from an uploaded Android source-code ZIP. It performs real project detection (Gradle, Flutter, multi-module, native C/C++, Kotlin vs Java), chooses a compatible toolchain (JDK / Gradle / AGP / NDK / Flutter versions) on the fly, runs the build, picks the relevant APK, and exposes it as a downloadable GitHub Actions artifact.
A drag-and-drop web interface is hosted on GitHub Pages:
➡️ https://professional-x.github.io/AndroidForge/
You will need a GitHub Personal Access Token (classic PAT with repo + workflow scopes, or a fine-grained PAT with Contents: Read/Write, Actions: Read/Write, Releases: Read/Write). The token is stored only in your browser's localStorage and sent directly to api.github.com — it never touches any other server.
- Open the URL above.
- Enter your fork's owner / repo name / PAT.
- Click Save connection.
- Drag your
.zipinto the upload card (or click to pick). - Choose a build variant (auto / debug / release / bundle).
- Click ⚒️ Build APK.
- Watch live status, step progress, and elapsed time.
- When the build finishes, click the run link to download your APK from the GitHub Actions UI.
- Creates a GitHub Release on tag
web-build-<timestamp>on your fork. - Uploads your
.zipas a release asset via the GitHub REST API. - Triggers
workflow_dispatchon.github/workflows/build-android.ymlwithzip_source=release-assetandrelease_tag=<tag>. - Polls the run status every 8 s and renders a step-by-step progress UI.
- When the run completes, lists the produced artifacts with download links.
To eliminate repeated installation of JDK / Android SDK / Gradle / Flutter on every APK build, this repo ships a prebuilt Docker image that contains the entire Android build environment.
| Layer | Version(s) |
|---|---|
| Base | ubuntu:22.04 |
| JDK | 8, 11, 17 (all three preinstalled; JAVA_HOME defaults to 17) |
| Android SDK | Command-line tools + platform-tools |
| Build-tools | 28.0.3, 29.0.3, 30.0.3, 32.0.0, 33.0.2, 34.0.0 |
| Android platforms | android-23 → android-34 |
| CMake | 3.18.1, 3.22.1 (for native C/C++ code) |
| Gradle | 8.9 system-wide (used only when a project has no usable wrapper) |
| Flutter | 3.27.0 stable (with desktop platforms enabled) |
| Linux desktop libs | libgtk-3-dev, libmpv-dev, libayatana-appindicator3-dev, libsecret-1-dev |
| Utilities | git, curl, wget, unzip, zip, python3, PyYAML |
- Image:
ghcr.io/<owner>/androidforge-build-env:latest(also tagged with short SHA + date) - Build workflow:
.github/workflows/build-android-image.yml— builds and pushes the image to GHCR only whenDockerfile.android(or the workflow itself) changes. It does NOT run on every APK build. - Dockerfile:
Dockerfile.androidat the repo root. - Consumer:
.github/workflows/build-android.ymldeclarescontainer: ghcr.io/<owner>/androidforge-build-env:lateston its build job, so the workflow starts inside the image instead of installing tools from scratch.
After forking the repo, you must publish the image once before the APK workflow can use it:
- Go to the repo's Settings → Actions → General → Workflow permissions and enable "Read and write permissions" (this lets the GITHUB_TOKEN push packages to GHCR).
- Go to Settings → Packages and make sure new packages are visible to your account (default is "private" — the APK workflow needs
packages: readto pull the image, which works even if private). - Trigger the "Build & Publish Android Image" workflow from the Actions tab (or push a change to
Dockerfile.android). First build takes ~15 min and produces a ~3 GB image. - After the image is published, the APK workflow will pull it automatically on its next run.
When you want to bump a tool version (e.g. Flutter 3.27 → 3.30, add a new Android platform, switch Gradle 8.9 → 8.10):
- Edit
Dockerfile.android— bump the relevant version. - Commit and push to
main. Thebuild-android-image.ymlworkflow auto-triggers (because itspaths:filter includesDockerfile.android). - Wait ~10–15 min for the new image to publish. The
:latesttag is moved automatically; old SHA-tagged images are kept for rollbacks. - The next APK build picks up the new image automatically.
The build-android.yml workflow now does this on every run:
- Starts the job inside the prebuilt Docker container (no JDK/SDK/Gradle install).
- Checks out the repo, installs Python deps (PyYAML, ~1 MB).
- Acquires the source ZIP via the existing input/URL/release mechanism.
- Runs
detect_project.pyto identify Gradle vs Flutter vs multi-module, etc. - Runs
setup_environment.pyto pick a compatible JDK / Gradle / Flutter version. - Switches
JAVA_HOMEto the detected JDK (all three are preinstalled — noactions/setup-javaneeded). - Installs only missing SDK components via
sdkmanager(if the project'scompileSdkisn't already in the image, e.g. a project that needs android-22). - Restores the per-project Gradle cache (
actions/cache@v4keyed on gradle version + hash of build.gradle/gradle-wrapper.properties/libs.versions.toml). - Restores the per-project Flutter pub cache for Flutter projects.
- Runs
build_project.py→assembleDebug/assembleRelease/bundleDebug. - Runs
collect_apk.py→ picks the primary APK. - Uploads the APK as a workflow artifact + publishes it to a release tagged
web-build-output-<run_id>. - On failure, runs
diagnose_failure.pyto surface the most likely cause.
Time saved: the old workflow spent ~3–5 min per build installing JDK 17 (~250 MB), Android SDK platforms + build-tools (~1 GB), Gradle (~150 MB), and Flutter (~1.5 GB). The new workflow spends ~30 s pulling the cached Docker layer + zero time installing these tools.
If you prefer not to use the web UI, you can also trigger the build directly from GitHub:
Android Project ZIP
│
▼
Project Detection
│
▼
Toolchain Detection
│
▼
Environment Setup
│
▼
Android Build
│
▼
APK Detection
│
▼
GitHub Artifact
│
▼
Download APK
You do not need to manually configure Android Studio, Gradle, Java, the Android SDK, the NDK, or Flutter. AndroidForge inspects the project and picks everything automatically.
- What this project does
- How to upload / provide a ZIP
- How to start the workflow
- How project detection works
- How the APK is obtained
- Supported project types
- Legacy-project strategy
- Limitations
- Common build failures
- How to extend toolchain compatibility
- GitHub Actions usage requirements
- Repository structure
- Security
AndroidForge turns a GitHub Actions runner into an on-demand Android build service. Instead of forcing every contributor to install a fully-configured Android toolchain on their laptop, the build is delegated to a runner that:
- Auto-detects the kind of Android project you uploaded (Gradle vs Flutter, Java vs Kotlin, multi-module vs single-module, native C/C++ or not).
- Picks a compatible toolchain by consulting
config/toolchain-rules.yaml— a single, modular file that maps every known Android Gradle Plugin (AGP) version to the Gradle version and JDK version it requires. - Installs the right toolchain versions on the runner (JDK, Gradle, Android SDK, NDK, Flutter) with caching so repeated builds become faster.
- Builds the project using the project's own
gradlewwhenever possible, falling back to a system-installed Gradle when the wrapper is too old, missing, or corrupt. - Collects the resulting APK(s) from the build output tree, picks the most relevant one (release > debug > bundle), renames it clearly, and uploads it as a GitHub Actions artifact you can download.
- Diagnoses failures — instead of just printing "BUILD FAILED", it scans the log for known signatures and reports the most likely cause (Java incompatibility, missing SDK, dependency resolution, manifest errors, etc.) directly in the workflow Summary.
The end-user experience is as close as GitHub allows to: upload ZIP → run workflow → wait → download APK.
AndroidForge supports three ZIP sources. Pick whichever fits your workflow best.
- Clone this repository locally.
- Drop your Android source ZIP into
input/(e.g.input/myproject.zip). - Commit and push (or push to a feature branch and open a PR — the workflow will trigger automatically when the ZIP lands on
mainbecause of thepush.paths: input/**.ziptrigger in.github/workflows/build-android.yml).
ℹ️ Committing ZIPs into git is not ideal for huge binaries but is fine for source archives. For very large ZIPs use Option B or C.
- Go to the repository → Releases → Draft a new release.
- Pick a tag (e.g.
build-input/v1). - Attach your ZIP as a release asset.
- Publish the release.
- Run the workflow manually (see section 3) and select
release-assetas the source, providing the tag you just created.
AndroidForge uses the GitHub REST API + the runner's built-in GITHUB_TOKEN to download the asset — no PAT required, no extra auth setup.
- Host the ZIP anywhere publicly downloadable (your own server, another GitHub release, a Google Drive direct link, etc.).
- Run the workflow manually and select
download-urlas the source, providing the URL.
-
Open the repository on GitHub.
-
Go to the Actions tab.
-
In the left sidebar, click Build Android APK.
-
Click Run workflow (top-right).
-
Configure the inputs:
Input Purpose zip_sourceWhere to get the ZIP ( input-directory/download-url/release-asset)zip_urlRequired only if download-urlis selectedrelease_tagRequired only if release-assetis selectedbuild_variantauto(try debug, then release, then bundle) or pick a specific variantenable_ndkForce-install the NDK even if no native code is detected -
Click Run workflow. The build typically takes 5–15 minutes (much faster on repeat runs thanks to caching).
💡 There is also a Diagnostics workflow that runs only the detection + toolchain-config steps (no actual build). Useful for troubleshooting what AndroidForge thinks of a ZIP before committing to a full build.
scripts/detect_project.py is the entry point. It:
- Finds the real project root inside the extracted ZIP — ZIPs often nest everything under
MyProject-1.2.3/, so the script walks up to 3 levels deep looking forsettings.gradle,pubspec.yaml, orbuild.gradle. - Detects indicator files at the project root and one level deep:
settings.gradle(.kts),build.gradle(.kts),gradlew,gradle-wrapper.properties,AndroidManifest.xml,pubspec.yaml,local.properties,gradle.properties,CMakeLists.txt,Android.mk,gradle/libs.versions.toml. - Classifies the project:
flutter—pubspec.yamlpresent at root.gradle—settings.gradle(.kts)orbuild.gradle(.kts)present, then sub-typed intokotlinif the kotlin-android plugin or any.ktfile is found, otherwisejava.
- Parses the Gradle wrapper — reads
gradle/wrapper/gradle-wrapper.propertiesto extract the distribution URL and Gradle version, and flags whether it useshttp://(legacy, will be auto-migrated tohttps://). - Discovers modules — both from
include ':name'lines insettings.gradleand by scanning for sub-directories containingbuild.gradle(.kts). - Detects native code — looks for
CMakeLists.txt/Android.mkand counts.cpp/.c/.cc/.cxxfiles anywhere in the tree. - Extracts versions from all
build.gradle(.kts)files andgradle/libs.versions.toml:- AGP version (multiple patterns supported:
classpath 'com.android.tools.build:gradle:X.Y.Z', version catalogagp = "...", plugins blockversion "...", etc.) - Kotlin version
- NDK version (
ndkVersion = "...") - Java version (
sourceCompatibility = JavaVersion.VERSION_11) compileSdk/targetSdk/minSdk
- AGP version (multiple patterns supported:
- Assesses legacy status — flags the project as legacy if any of: Gradle wrapper version < 7, AGP major < 7, Java ≤ 11,
compileSdk< 30, or no wrapper at all. - Emits a JSON summary to stdout and to
$GITHUB_OUTPUTfor downstream workflow steps.
The detection step never modifies the project tree — it only reads.
After the build step, scripts/collect_apk.py:
- Recursively searches the project tree for
*.apkand*.aabfiles. - Skips intermediate outputs — anything inside
build/intermediates/is ignored. Only outputs inbuild/outputs/apk/orbuild/outputs/bundle/are kept. - Picks a primary APK using this priority:
*-release.apk>*-debug.apk> other.apk>.aab. - Renames each APK to the form
AndroidForge-<module>-<variant>.apk(e.g.AndroidForge-app-release.apk) and copies it into theoutput/directory. - Writes the output paths to
$GITHUB_OUTPUTso the workflow can upload them. - The workflow then uses
actions/upload-artifact@v4to publish the APK as a downloadable artifact namedAndroidForge-APKs.
To download the APK:
- Open the completed workflow run.
- Scroll to the bottom of the run summary.
- Click the AndroidForge-APKs artifact to download a ZIP containing your APK(s).
- If the build failed, also grab the AndroidForge-logs artifact for diagnosis.
| Type | Detected via | Build command |
|---|---|---|
| Gradle (Java) | settings.gradle + no Kotlin plugin + build.gradle present |
./gradlew assembleDebug / assembleRelease |
| Gradle (Kotlin) | settings.gradle + Kotlin plugin or .kt files |
./gradlew assembleDebug / assembleRelease |
| Multi-module Gradle | include ':...' lines in settings.gradle |
Same as above — Gradle handles module resolution |
| Old Gradle wrapper | gradle-wrapper.properties with version < 7 |
Wrapper used; legacy fixes applied |
| Missing/corrupt wrapper | gradle-wrapper.jar absent |
gradle wrapper --gradle-version <X> regenerates it |
| Native C/C++ (CMake) | CMakeLists.txt present |
Same Gradle build — NDK + CMake installed by workflow |
| Native C/C++ (ndk-build) | Android.mk present |
Same Gradle build — NDK installed |
| Flutter | pubspec.yaml at root |
flutter pub get → flutter build apk --debug / --release |
| Version-catalog projects | gradle/libs.versions.toml present |
Versions read from the catalog, fallback to inline declarations |
| Legacy AGP (3.x / 4.x) | AGP version string in build.gradle |
Compatible Gradle + JDK chosen from toolchain-rules.yaml |
If your project type isn't on this list, AndroidForge will still try to build it — it falls back to a generic ./gradlew assembleDebug → assembleRelease → bundleDebug sequence. Detection only gets better as the rules in toolchain-rules.yaml are extended.
Old Android projects are a first-class concern, not an afterthought. The strategy is:
- Prefer the project's own Gradle wrapper — it pins the exact Gradle version the project was authored against. If the wrapper works on the runner, use it.
- Auto-fix common wrapper issues:
- Wrapper distribution URL using
http://→ migrated tohttps://in-place (in the workspace, never in the source ZIP). gradle-wrapper.jarmissing or corrupt → regenerated by runninggradle wrapper --gradle-version <X>using a system-installed Gradle.
- Wrapper distribution URL using
- Fall back to a system-installed Gradle of a compatible version when the wrapper can't be used. The version is picked from the
agp_to_gradlemapping intoolchain-rules.yaml. - Pick a compatible JDK using both the AGP version → JDK and Gradle version → JDK mappings. For example, AGP 3.x → JDK 8, AGP 4.x/7.x → JDK 11, AGP 8.x → JDK 17.
- Install old Android SDK platforms —
platforms;android-23throughplatforms;android-34are pre-installed by the workflow so oldcompileSdkvalues resolve cleanly. - Disable the Gradle daemon for very old Gradle versions (< 4) where daemon mode is unstable.
- Auto-inject
local.propertieswithsdk.dirif the project doesn't have one. - Never modify the source ZIP destructively — every fix is applied in the build workspace, which is discarded after the workflow completes.
- No signing by default. Release builds are unsigned (or use the project's own debug/release config if present). To produce a signed release APK you must add a keystore as a GitHub Actions secret and reference it in your project's
build.gradlesigningConfigs. AndroidForge deliberately does not auto-sign because doing so safely requires project-specific keystore configuration. - No emulator / instrumentation tests. AndroidForge only produces an APK. It does not run instrumentation tests on an emulator. Use a separate workflow for that.
- NDK version must be installable from the SDK Manager. The NDK version declared in
build.gradlemust be one thatsdkmanagerknows about. If you use an obscure NDK version, updateconfig/toolchain-rules.yaml → ndk.legacy_versions. - Network access required. Dependency resolution requires internet access to
maven.google.com,repo1.maven.org,plugins.gradle.org. If your project depends on a private repository, the build will fail unless you make that repo accessible from a public GitHub runner. - Source ZIP is untrusted. AndroidForge treats the uploaded ZIP as potentially malicious: no repository secrets are passed into the build environment, and the build runs on a standard ephemeral GitHub runner. Don't upload a project that executes arbitrary scripts at configuration time unless you've audited it first.
- Build duration. First builds take ~10–15 minutes (Gradle distribution + dependency download). Cached builds usually finish in 3–6 minutes.
- Workflow timeout. Hard limit of 60 minutes per build. If your project exceeds this, split modules or move to a self-hosted runner.
The diagnosis step (scripts/diagnose_failure.py) automatically scans the build log and ranks the most likely cause. Here are the most common ones and what to do:
| Diagnosis | Likely cause | Fix |
|---|---|---|
| Java incompatibility | JDK on the runner is older than the bytecode the project requires | Bump agp_to_jdk / gradle_to_jdk in config/toolchain-rules.yaml |
| Gradle incompatibility | AGP requires a newer Gradle than the one being used | Check agp_to_gradle mapping; the wrapper version may need regenerating |
| Missing Android SDK platform | compileSdk value not installed |
Add the missing platforms;android-N package in the workflow's setup-android step |
| Missing NDK | ndkVersion declared in build.gradle not installed |
Update config/toolchain-rules.yaml → ndk.legacy_versions |
| Dependency resolution failure | Repo URL missing or network issue | Add google() / mavenCentral() to settings.gradle.kts; check dependency_fallbacks |
| Kotlin compilation failure | Kotlin/Java version mismatch | Update kotlin_to_jdk mapping or bump Kotlin version |
| Manifest merger failure | Duplicate attributes across libraries | Inspect app/build/outputs/logs/manifest-merger-*.txt (uploaded as part of the logs artifact) |
| Corrupt or missing Gradle wrapper | gradle-wrapper.jar not in the ZIP |
AndroidForge should auto-regenerate; if it fails, check the regenerate-wrapper.log artifact |
| Unsupported project structure | Module path declared but not present on disk | Verify the project root was detected correctly (run the Diagnostics workflow) |
| Out of memory | Default heap too small | Add org.gradle.jvmargs=-Xmx4g to gradle.properties |
When a build fails, always check two things:
- The Diagnosis section in the workflow summary (rendered by
scripts/diagnose_failure.py). - The AndroidForge-logs artifact — contains the full build log plus all Gradle reports.
The entire compatibility matrix lives in one file: config/toolchain-rules.yaml. To support new AGP / Gradle / Kotlin / JDK versions, edit that file and commit. No script changes required.
agp_to_gradle:
"8.8": "8.10" # add new entry here
agp_to_jdk:
"8.x": "17" # already covers 8.8 — no change neededndk:
default_version: "27.0.12077473" # bump to a newer NDK
legacy_versions:
- "26.1.10909125" # keep the old default as a fallback
- "25.2.9519653"flutter:
default_version: "3.27.0"
channel: "stable"
jdk: "17"If you encounter a build failure that the diagnosis step doesn't recognize, add the failing log signature to scripts/diagnose_failure.py → DIAGNOSTICS:
{
"label": "New error class",
"patterns": [r"some-regex-from-the-log"],
"hint": "What the user should do",
},If you need to support a project type AndroidForge doesn't know yet (e.g. React Native, Capacitor, Cordova), you need to:
- Extend
scripts/detect_project.pyto recognize it (add an indicator file + classification logic). - Extend
scripts/setup_environment.pyto pick the right toolchain. - Extend
scripts/build_project.pyto run the right build command.
Detection / setup / build are intentionally separate modules — change one without touching the others.
- Runner:
ubuntu-latest(Ubuntu 22.04 / 24.04). Standard GitHub-hosted runner; no self-hosted runner required. - Actions used (all official or widely-trusted community actions):
actions/checkout@v4actions/setup-python@v5actions/setup-java@v4actions/cache@v4actions/upload-artifact@v4android-actions/setup-android@v3subosito/flutter-action@v2
- Permissions:
contents: read,actions: write. No secrets are required for the default flow. The release-asset source uses the runner's auto-providedGITHUB_TOKEN(read-only on the same repo). - Caching: JDK (via
setup-javacache), Gradle caches + wrapper, Python deps, and Flutter (viaflutter-actioncache). Cache keys incorporate the chosen Gradle version + a hash of all Gradle build files. - Timeout: 60 minutes per build (configurable in
.github/workflows/build-android.yml). - Concurrency: One build per ref at a time — newer pushes do not cancel in-flight builds, so you always get a result for each input.
To run AndroidForge in your own organization, fork or clone this repository, push it to a new GitHub repo, and you're done. No additional setup is required.
AndroidForge/
├── .github/
│ └── workflows/
│ ├── build-android.yml # Main workflow: ZIP → APK
│ └── diagnostics.yml # Diagnostics-only workflow (detection + setup, no build)
├── scripts/
│ ├── acquire_zip.py # Source ZIP acquisition (input-dir / URL / release-asset)
│ ├── extract_zip.py # Extract ZIP into a clean workspace
│ ├── detect_project.py # Project detection (Gradle / Flutter / multi-module / native)
│ ├── setup_environment.py # Toolchain choice (JDK / Gradle / AGP / NDK / Flutter)
│ ├── build_project.py # Build orchestration (runs gradlew / flutter)
│ ├── collect_apk.py # Locate, rename, copy generated APK(s)
│ ├── diagnose_failure.py # Scan build log for known error signatures
│ ├── build_summary.py # Render Markdown build summary
│ └── requirements.txt # PyYAML
├── config/
│ └── toolchain-rules.yaml # Single source of truth for version mappings
├── input/
│ └── .gitkeep # Drop a ZIP here for the simplest workflow
├── output/
│ └── .gitkeep # Generated APKs land here before being uploaded
├── README.md
├── LICENSE # MIT
└── .gitignore
The system is intentionally modular: detection, environment setup, building, and artifact collection are four separate Python scripts. The workflow YAML orchestrates them. To change one part (e.g. add a new project type), you only touch the relevant script + toolchain-rules.yaml — the rest keeps working.
- The uploaded ZIP is treated as untrusted source code.
- No repository secrets are passed into the build environment. The
permissions:block inbuild-android.ymlonly grantscontents: readandactions: write(for uploading artifacts). - The auto-provided
GITHUB_TOKENis used only to download a release asset from the same repository via the GitHub REST API — it is never printed or logged, and is not exposed to the build commands themselves. - Build commands (
./gradlew …,flutter …) run in the standard ephemeral GitHub-hosted runner sandbox. - If you must include signing keys or other secrets for a release build, declare them as GitHub Actions repository secrets and reference them via
${{ secrets.XXX }}in the workflow — never commit them to the repo.
MIT — see LICENSE.