This repository contains a manually triggered GitHub Actions workflow for building installable Android artifacts (APK / AAB) from the current repository or from another target repository.
The workflow defined in android.yml:
- Runs from
workflow_dispatchmanual inputs. - Checks out a target repository with recursive submodules.
- Uses the target repository default branch when
branchis empty. - Supports a
CHECKOUT_TOKENsecret for private repositories or private submodules, and falls back togithub.tokenfor normal access. - Sets up Temurin JDK, Android NDK, and Gradle cache through the shared
.github/actions/android-ci-preparecomposite action used by setup, test, and build jobs. - Verifies that the requested NDK revision is installed completely, reusing
nttld/setup-ndklocal cache entries when possible and repairing or installing withsdkmanageronly when needed. - Keeps shared CI logic in
.github/actions/android-ci-prepare/and.github/scripts/android-ci/so NDK verification, pre-build resolution, Gradle option parsing, unsigned option handling, and summary rendering are not duplicated across jobs. - Resolves source metadata: branch/ref, full SHA, short SHA, repository name, and version name.
- Reads
gradle.propertiesforversionName,VERSION_NAME, orversion; if none is found, the GitHub run number is used asversion_name. - Uses Gradle task introspection to detect Android application modules and real build variants.
- Builds matching variants through a matrix.
- Optionally runs a full
./gradlew buildtest job before the build matrix. - Optionally runs a pre-build shell command, including an auto mode for NekoBox-style
libcorebuilds. - Uploads APK / AAB files as GitHub Actions artifacts.
- Optionally publishes a GitHub Release in the repository that runs this workflow.
Place the workflow and helper scripts at:
.github/workflows/android.yml
.github/actions/android-ci-prepare/action.yml
.github/scripts/android-ci/ndk_versions.py
.github/scripts/android-ci/verify-ndk.sh
.github/scripts/android-ci/resolve-prebuild.py
.github/scripts/android-ci/run-prebuild.sh
.github/scripts/android-ci/parse-gradle-options.py
.github/scripts/android-ci/resolve-signing.py
.github/scripts/android-ci/unsigned_gradle_options.py
.github/scripts/android-ci/run-gradle.sh
.github/scripts/android-ci/print-workflow-inputs.py
The workflow checks out the workflow repository at github.sha into a separate helper path, then checks out the target repository. This keeps the composite action and helper scripts versioned with the workflow even when repository points to another target repository.
Shared helper responsibilities:
| Path | Responsibility |
|---|---|
.github/actions/android-ci-prepare/action.yml |
Shared JDK/NDK verification, optional pre-build resolution, Go setup, pre-build cache, and pre-build execution for setup-matrix, test, and build. |
.github/scripts/android-ci/ndk_versions.py |
Single NDK alias-to-full-revision map used by NDK verification. |
.github/scripts/android-ci/verify-ndk.sh |
Validates or repairs the requested NDK and exports ANDROID_NDK_HOME / ANDROID_NDK_ROOT. |
.github/scripts/android-ci/resolve-prebuild.py and run-prebuild.sh |
Resolve and run optional pre-build commands. |
.github/scripts/android-ci/parse-gradle-options.py |
Normalize raw build_options into a JSON array once. |
.github/scripts/android-ci/resolve-signing.py |
Detect whether signing should stay enabled or fall back to unsigned mode. |
.github/scripts/android-ci/unsigned_gradle_options.py |
Single implementation of effective -Punsigned detection and appending. |
.github/scripts/android-ci/run-gradle.sh |
Runs Gradle with the resolved options and signing retry behavior. |
.github/scripts/android-ci/print-workflow-inputs.py |
Renders the workflow step summary and console input report from one JSON object. |
| Input | Description | Type | Default |
|---|---|---|---|
| repository | Target repository in owner/repo format. Leave empty to use the repository that runs this workflow. |
string |
empty |
| branch | Target branch, tag, or SHA. Leave empty to use the target repository default branch. | string |
empty |
| module | Target module(s), comma-separated. Use values such as app or :app. If empty, the workflow auto-detects installable Android application modules. |
string |
app |
| build_flavor | Build flavor filter(s), comma-separated. Leave empty to accept all flavors. Matching is normalized and substring-based. | string |
empty |
| build_type | Build output selector. | choice: debug, release, aab, all |
debug |
| build_options | Extra Gradle arguments appended to ./gradlew build and build tasks. Accepts shell-style quoting or a JSON array of strings. |
string |
empty |
| signing | Whether release signing is allowed. Defaults to disabled; release/AAB tasks use -Punsigned unless a complete signing config is detected. Debug tasks keep Gradle/Android's default debug signing and only retry with -Punsigned after a signing-related failure. |
boolean |
false |
| pre_build_command | Pre-build command before Gradle. Use auto, none, skip, or a custom shell command. |
string |
auto |
| go_version | Go version passed to actions/setup-go when the pre-build command needs Go. |
string |
^1.25 |
| build_test | Run a separate ./gradlew build job before the build matrix. |
boolean |
false |
| upload_release | Publish a GitHub Release after successful builds. | boolean |
false |
| os_version | Runner image for test/build jobs. setup-matrix and release stay on ubuntu-latest. |
choice: ubuntu-latest, ubuntu-24.04, ubuntu-22.04, macos-latest, macos-26, macos-15, macos-26-intel, macos-15-intel |
ubuntu-latest |
| jdk_version | Temurin JDK version. | choice: 26, 25, 24, 23, 22, 21, 17, 11, 8 |
21 |
| ndk_version | Android NDK alias. | choice: r29, r28c, r27d, r26d, r25c, r24, r23c, r22b, r21e, r20b |
r27d |
os_version controls only the test and build jobs. The lightweight setup-matrix job and the final release job always run on ubuntu-latest so module discovery and release publishing remain stable.
Available runner labels include Linux x64 and macOS runners:
| Runner label | Platform / architecture | Suggested use |
|---|---|---|
ubuntu-latest |
Linux x64, GitHub's current stable Ubuntu image | Default Android builds. |
ubuntu-24.04 |
Linux x64, pinned Ubuntu 24.04 | Reproducible Linux x64 builds. |
ubuntu-22.04 |
Linux x64, pinned Ubuntu 22.04 | Older toolchain compatibility. |
macos-latest |
macOS arm64, GitHub's current stable macOS image | macOS build validation without pinning a version. |
macos-26 |
macOS arm64, pinned macOS 26 | Reproducible Apple Silicon builds. |
macos-15 |
macOS arm64, pinned macOS 15 | Apple Silicon builds requiring macOS 15. |
macos-26-intel |
macOS Intel x86_64, pinned macOS 26 | x86_64 macOS builds. |
macos-15-intel |
macOS Intel x86_64, pinned macOS 15 | x86_64 macOS builds with older macOS 15 image. |
The shared preparation action runs JDK setup, nttld/setup-ndk, and NDK verification in every job that needs Android tooling. setup-matrix calls it with run_prebuild=false; test and build call it with run_prebuild=true so they also resolve, cache, and run pre-build steps.
The NDK verification helper detects the host OS and checks the matching NDK toolchain directory: linux-x86_64 on Linux and darwin-x86_64 on macOS. NDK aliases are resolved by .github/scripts/android-ci/ndk_versions.py. Verification searches the canonical SDK directory, ANDROID_NDK_HOME / ANDROID_NDK_ROOT, ~/.setup-ndk/<alias>, ~/.setup-ndk/<revision>, the broader ~/.setup-ndk cache, and $ANDROID_SDK_ROOT/ndk. A candidate is accepted only when source.properties reports the requested full revision and the expected LLVM toolchain binary exists. If a complete cached NDK is found outside the canonical SDK directory, the helper symlinks it to $ANDROID_SDK_ROOT/ndk/<full-revision> for Gradle ndkVersion resolution. It falls back to sdkmanager --install ndk;<full-revision> only when no complete candidate is available.
Pre-build cache keys include runner.os, runner.arch, repository name, source SHA, requested NDK, Go version, and a runtime SHA256 over run, buildScript, and libcore. This avoids using hashFiles(...) inside the composite action and prevents macOS Intel, macOS arm64, and Linux runs from sharing incompatible cached outputs.
build_type |
Gradle tasks matched | Output |
|---|---|---|
debug |
assemble*Debug |
APK |
release |
assemble*Release |
APK |
aab |
bundle*Release |
AAB |
all |
assemble*Debug, assemble*Release, bundle*Release |
APK and AAB |
Notes:
- AAB output is release-only.
install*tasks are used only to help detect installable application modules; they are not built directly.- Multi-flavor Gradle variants are represented by the flavor part of the Gradle task name, for example
assembleFreeGoogleReleasehas flavorFreeGoogle.
- Module names are normalized to Gradle path format. For example,
appbecomes:app. - If
moduleis provided, every requested module must exist in./gradlew projects. - If
moduleis empty, the workflow scans all Gradle subprojects and keeps modules that expose app-style install or bundle tasks. - If a requested module exists but does not expose installable Android application tasks, the workflow fails fast.
build_flavoraccepts comma-separated filters such asfree,paid.- Flavor matching is case-insensitive after removing non-alphanumeric characters.
- The workflow treats a filter as matching when it is contained in the normalized Gradle flavor name.
- If no variants match the requested module, flavor, and build type filters, the workflow fails fast and prints available tasks.
build_options is normalized once in setup-matrix and reused by the test and build jobs as a JSON array. This catches quoting mistakes before the build matrix starts and avoids each job reparsing the raw input differently. The same normalized JSON array is also passed to the signing and unsigned-option helpers, so all jobs make decisions from the same parsed argument list.
Supported formats:
--info --scan '-Pchannel=free beta'
["--info", "--scan", "-Pchannel=free beta"]
The shell-style form uses POSIX shell-like parsing, so quoted values with spaces are preserved. Empty arguments and newline-containing arguments are rejected.
signing defaults to false. The workflow no longer appends -Punsigned globally during setup; instead run-gradle.sh applies it only to release/AAB or aggregate tasks such as build, assemble, and bundle. Debug tasks keep Gradle/Android's default debug keystore behavior. The effective unsigned handling is centralized in .github/scripts/android-ci/unsigned_gradle_options.py, so resolve-signing.py and run-gradle.sh use the same rules for explicit -Punsigned, -Punsigned=true, and -Punsigned=false.
When signing=true, the workflow checks the target repository and the workflow repository's GitHub Secrets/Variables during setup-matrix. Signing stays enabled only when a complete signing config is detected. If no complete config is found, even when build.gradle / build.gradle.kts declares a signingConfig, release/AAB and aggregate tasks fall back to -Punsigned to avoid failures caused by missing keystores, aliases, or passwords. Non-release tasks are not overridden up front; if one fails with a signing-related error, it is retried once with -Punsigned as a safety net.
Detected sources include:
- Target-repository files such as
keystore.properties,key.properties,signing.properties,release.properties,gradle.properties, keystore files, and signing declarations in Gradle files. - Complete signing properties passed in
build_options, such as-Pandroid.injected.signing.*or common signing property names. - Workflow repository Secrets/Variables using common groups such as
SIGNING_KEYSTORE_BASE64+SIGNING_STORE_PASSWORD+SIGNING_KEY_ALIAS+SIGNING_KEY_PASSWORD,KEYSTORE_BASE64+KEYSTORE_PASSWORD+KEY_ALIAS+KEY_PASSWORD,RELEASE_KEYSTORE_BASE64+RELEASE_STORE_PASSWORD+RELEASE_KEY_ALIAS+RELEASE_KEY_PASSWORD,ANDROID_INJECTED_SIGNING_*, and similar variants.
Logs and step summaries show only variable names/sources, never secret values.
pre_build_command is resolved by the shared .github/scripts/android-ci/resolve-prebuild.py helper before the test job and before each build matrix job.
| Value | Behavior |
|---|---|
empty / none / skip |
Do not run a pre-build command. |
auto |
If both ./run and libcore/build.sh exist, run ./run lib core; otherwise do nothing. |
| any other value | Run the value as a shell command with bash -eo pipefail -c. |
When auto mode detects a NekoBox-style libcore build:
- Go is installed with the requested
go_version. app/libs/libcore.aaris used as the expected output and cache path../run,libcore/*.sh, andbuildScript/*.share made executable when present.- If the cached output already exists, the pre-build command is skipped.
- If the command finishes but the expected output is missing, the job fails.
For manual commands, Go is installed when the command text contains go, gomobile, or ./run lib core.
Security note: custom
pre_build_commandis arbitrary shell. Only allow trusted maintainers to trigger this workflow with custom commands.
When build_test=true, the workflow runs a separate test job before the build matrix:
./gradlew build --no-daemon --stacktrace <per-task Gradle options>The test job checks out the exact source SHA resolved by setup-matrix, sets up JDK/NDK, optionally runs the same pre-build flow, and then runs the full Gradle build.
Each build matrix entry runs one Gradle task:
./gradlew <module>:<gradle_task> --no-daemon --stacktrace <per-task Gradle options>Artifacts are uploaded from the matching module directory:
<module_dir>/build/outputs/**/*.apk
<module_dir>/build/outputs/**/*.aab
Artifact names include:
<repository-name>-<module>-<flavor>-<debug|release|aab>-<UTC timestamp>
The flavor part is omitted when the variant has no flavor.
upload_release=false only uploads workflow artifacts.
upload_release=true creates or updates a GitHub Release after all build matrix jobs succeed:
- Release repository: the repository that runs this workflow.
- Tag:
v<version_name>. - Release name:
<repository_name> v<version_name>. - Release files: downloaded build artifacts matching
<repository_name>-*.
The release job uses contents: write; other jobs use read-only contents permission.
The workflow passes resolved inputs to .github/scripts/android-ci/print-workflow-inputs.py as a single WORKFLOW_INPUTS_JSON object. The script uses that one object to render both the GitHub step summary and the console report, then appends dynamic NDK fields such as the resolved revision and ANDROID_NDK_HOME. This avoids maintaining separate field lists for environment wiring, Markdown output, and console output.
The summary contains:
- Resolved repository and repository name.
- Branch input and resolved branch/ref.
- Source SHA.
- Module, flavor, build type, raw build options, base Gradle options, per-task signing decision, unsigned scope, pre-build command, and Go version.
- Build/test/release options.
- Runner label, JDK, requested NDK, resolved NDK revision, and
ANDROID_NDK_HOME. - Version name.
- Final build matrix with module, flavor, build type, artifact type, Gradle task, and artifact name.
repository: ""
branch: ""
module: app
build_type: debug
signing: falserepository: some-owner/some-repo
branch: main
module: app
build_type: aab
signing: true
upload_release: truemodule: app,store
build_flavor: free,paid
build_type: allmodule: app
build_type: release
pre_build_command: auto
go_version: ^1.25module: app
build_type: release
pre_build_command: ./scripts/prepare-native-libs.shUse GitHub's workflow_dispatch API.
Request:
POST /repos/{owner}/{repo}/actions/workflows/android.yml/dispatchesExample payload:
{
"ref": "main",
"inputs": {
"repository": "owner/repo",
"branch": "main",
"module": "app",
"build_flavor": "free,paid",
"build_type": "all",
"build_options": "--info '-Pchannel=free beta'",
"signing": "false",
"pre_build_command": "auto",
"go_version": "^1.25",
"build_test": "true",
"upload_release": "false",
"os_version": "ubuntu-latest",
"jdk_version": "21",
"ndk_version": "r27d"
}
}Authentication can use:
- GitHub Personal Access Token
- GitHub App token
- Any token with permission to dispatch workflows in the workflow repository
For private target repositories or private recursive submodules, add a repository secret named CHECKOUT_TOKEN with read access to the target repository and its submodules.
gh workflow run android.yml \
--repo owner/workflow-repo \
-f repository=owner/target-repo \
-f branch=main \
-f module=app \
-f build_flavor=free,paid \
-f build_type=all \
-f build_options="--info '-Pchannel=free beta'" \
-f signing=false \
-f pre_build_command=auto \
-f go_version='^1.25' \
-f build_test=true \
-f upload_release=false \
-f os_version=ubuntu-latest \
-f jdk_version=21 \
-f ndk_version=r27d- Target repository not found or access denied.
- Target branch, tag, or SHA not found.
- Private repository or submodule requires
CHECKOUT_TOKEN. - Module name typo or module does not exist in
./gradlew projects. - Requested module is not an installable Android application module.
- Requested flavor/build type combination does not exist.
build_optionshas invalid shell quoting / JSON syntax, or contains arguments that Gradle does not understand.signing=truewas selected but signing variables/repository config are incomplete; the workflow falls back to-Punsignedfor release/AAB/aggregate tasks, but the target project also needs to support that unsigned property.- Custom
pre_build_commandfails. - Auto pre-build mode runs but does not produce
app/libs/libcore.aar. - Requested NDK version cannot be installed, no complete cached NDK is found, the host-specific NDK toolchain is missing, or repair still leaves the NDK incomplete.
Copyright (c) 2026 shiguobaona
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.