This guide explains how to reproduce repository checks locally on Linux, macOS, and Windows.
- JDK 17
- Android SDK command-line tools
- Android platform-tools
- Android platform
android-36 - Android build-tools
36.0.0 - Git
Validated locally:
- foundation checks
- debug build and unit tests
- coverage
- release assemble
- release lint
- CodeQL build
- preflight
- APK task validation
- gitleaks
- emulator instrumentation
Environment used:
- Homebrew
openjdk@17 - Android SDK root:
~/Library/Android/sdk - emulator target:
android-34,google_apis,arm64-v8a
Validated locally on Ubuntu 24.04 arm64:
- JDK 17 setup
- Android SDK command-line tools setup
sdkmanagerpackage install- preflight
Observed differences:
- if
local.propertiespoints to an SDK path from another OS, Gradle prefers it overANDROID_SDK_ROOT .github/scripts/security/run_gitleaks.shfails on arm64 because it downloads alinux_x64binary- Android lint/build path failed with
Aapt2InternalException: Failed to start AAPT2 process
This is the closest path to GitHub CI and should be treated as the reference Linux environment for full parity.
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
export ANDROID_API_LEVEL=36
export ANDROID_BUILD_TOOLS=36.0.0
export INSTALL_SYSTEM_IMAGE=false
export ANDROID_SYSTEM_IMAGE=
bash .github/scripts/setup/install_android_sdk_packages.shexport JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
export ANDROID_API_LEVEL=36
export ANDROID_BUILD_TOOLS=36.0.0
export INSTALL_SYSTEM_IMAGE=false
export ANDROID_SYSTEM_IMAGE=
bash .github/scripts/setup/install_android_sdk_packages.shIf your Linux host is arm64, native SDK installation can still work, but Android Gradle tasks may differ from x86_64 CI behavior.
$env:ANDROID_API_LEVEL = "36"
$env:ANDROID_BUILD_TOOLS = "36.0.0"
$env:INSTALL_SYSTEM_IMAGE = "false"
$env:ANDROID_SYSTEM_IMAGE = ""
.\.github\scripts\setup\install_android_sdk_packages_windows.ps1If the working tree was copied from another machine or another OS, check local.properties.
If it contains a stale sdk.dir, Gradle uses that value before ANDROID_SDK_ROOT.
Fix it with:
printf 'sdk.dir=%s\n' "$HOME/Android/Sdk" > local.propertiesOn macOS:
printf 'sdk.dir=%s\n' "$HOME/Library/Android/sdk" > local.propertiesUse this as the default pre-push validation path.
bash .github/scripts/quality/run_foundation.sh
bash .github/scripts/quality/run_debug_build_and_unit_tests.sh
bash .github/scripts/quality/run_coverage.sh.\.github\scripts\quality\run_debug_build_and_unit_tests_windows.ps1
.\gradlew.bat ktlintCheck detekt ai:lintDebug app:lintDebug data:lintDebug domain:lintDebug ai:testDebugUnitTest app:testDebugUnitTest data:testDebugUnitTest domain:testDebugUnitTest koverXmlReport koverVerify --stacktraceCI script: run_foundation.sh
Tasks:
ktlintCheckdetektai:lintDebugapp:lintDebugdata:lintDebugdomain:lintDebug
Linux or macOS:
bash .github/scripts/quality/run_foundation.shWindows:
.\gradlew.bat ktlintCheck detekt ai:lintDebug app:lintDebug data:lintDebug domain:lintDebug --stacktraceOutputs:
**/build/reports/detekt/**/build/reports/ktlint/**/build/reports/lint-results-*.html**/build/reports/lint-results-*.xml
Linux arm64 note:
- on Ubuntu 24.04 arm64, the Android lint/build path failed with
Aapt2InternalException: Failed to start AAPT2 process
CI scripts:
Tasks:
ai:assembleDebugapp:assembleDebugapp:assembleDebugAndroidTestdata:assembleDebugdomain:assembleDebugai:testDebugUnitTestapp:testDebugUnitTestdata:testDebugUnitTestdomain:testDebugUnitTest
Linux or macOS:
bash .github/scripts/quality/run_debug_build_and_unit_tests.shWindows:
.\.github\scripts\quality\run_debug_build_and_unit_tests_windows.ps1Outputs:
app/build/outputs/apk/debug/app-debug.apkapp/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk**/build/test-results/**/build/reports/tests/**/build/outputs/unit_test_code_coverage/
CI script: run_coverage.sh
Tasks:
ai:testDebugUnitTestapp:testDebugUnitTestdata:testDebugUnitTestdomain:testDebugUnitTestkoverXmlReportkoverVerify
Linux or macOS:
bash .github/scripts/quality/run_coverage.shWindows:
.\gradlew.bat ai:testDebugUnitTest app:testDebugUnitTest data:testDebugUnitTest domain:testDebugUnitTest koverXmlReport koverVerify --stacktraceOutput:
**/build/reports/kover/
CI scripts:
Linux or macOS:
bash .github/scripts/release/assemble_release.sh
bash .github/scripts/release/lint_release.shWindows:
.\gradlew.bat ai:assembleRelease app:assembleRelease data:assembleRelease domain:assembleRelease --stacktrace
.\gradlew.bat ai:lintRelease app:lintRelease data:lintRelease domain:lintRelease --stacktraceCI script: run_gitleaks.sh
Linux x86_64:
bash .github/scripts/security/run_gitleaks.shNative gitleaks path for Linux arm64, macOS, and Windows:
gitleaks detect --source . --report-format sarif --report-path build/reports/gitleaks/gitleaks.sarif --redactLinux arm64 note:
.github/scripts/security/run_gitleaks.shfailed withcannot execute binary file: Exec format error
CI script: classify_changes.sh
The script expects GITHUB_OUTPUT, so a plain local invocation is not enough.
Example:
tmpfile="$(mktemp)"
GITHUB_OUTPUT="$tmpfile" \
EVENT_NAME=pull_request \
BASE_SHA="$(git merge-base upstream/main HEAD)" \
HEAD_SHA="$(git rev-parse HEAD)" \
BEFORE_SHA="" \
CURRENT_SHA="$(git rev-parse HEAD)" \
bash .github/scripts/preflight/classify_changes.sh
cat "$tmpfile"
rm -f "$tmpfile"CI script: build_for_codeql.sh
Linux or macOS:
bash .github/scripts/codeql/build_for_codeql.shWindows:
.\gradlew.bat clean ai:assembleDebug app:assembleDebug data:assembleDebug domain:assembleDebug --no-build-cache --rerun-tasks --stacktraceLimitation:
- this reproduces the build input
- full local parity still requires local CodeQL CLI setup
CI scripts:
GitHub CI target:
- Linux
- API 34
x86_64pixel_7
Validated local macOS target:
- macOS arm64
- API 34
arm64-v8apixel_7
Linux arm64 status:
- not validated end-to-end
- upstream Android build path already failed at AAPT2 startup
Task validation:
Linux or macOS:
bash .github/scripts/android/validate_debug_apk_tasks.shWindows:
.\gradlew.bat app:assembleDebug app:assembleDebugAndroidTest -m --stacktraceBasic manual flow:
./gradlew app:assembleDebug app:assembleDebugAndroidTest --stacktrace
APK_DIR=app/build/outputs/apk bash .github/scripts/android/run_emulator_instrumentation.shValidated macOS arm64 flow:
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH:$HOME/Library/Android/sdk/emulator:$HOME/Library/Android/sdk/platform-tools"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
printf 'no\n' | avdmanager create avd -n pixel7api34Arm64Local -k 'system-images;android-34;google_apis;arm64-v8a' -d 'pixel_7'
./gradlew app:assembleDebug app:assembleDebugAndroidTest --stacktrace
emulator -avd pixel7api34Arm64Local -no-snapshot-save -no-window -noaudio -no-boot-anim -gpu swiftshader_indirect &
adb wait-for-device
until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do sleep 5; done
ANDROID_SERIAL=emulator-5554 APK_DIR=app/build/outputs/apk bash .github/scripts/android/run_emulator_instrumentation.sh
adb -s emulator-5554 emu killValidated result:
com.itlab.notes.ExampleInstrumentedTest:.
Time: 0.006
OK (1 test)
- dependency review
- workflow summaries
- artifact upload
- full CodeQL action lifecycle
For pre-push debugging, reproducing the Gradle tasks and repository scripts above is usually enough.
On Windows, use Git Bash or WSL for the bash-based Android helper scripts. The Gradle commands themselves are platform-specific, but the repository's emulator helper scripts are shell scripts, not PowerShell scripts.