Setting up continuous integration with GitHub Actions
Continuous integration (CI) gives Android teams a fast, repeatable way to compile, test and package every change. GitHub Actions runs checks when a pull request is opened or updated, or when code lands on the main branch, catching problems before they reach a device or release channel.
This guide builds a practical workflow for an Android Studio project using Gradle. It covers YAML configuration, unit tests, lint, debug artefacts, signing secrets and emulator checks, with commands that can also be reproduced on a local machine.
Choose the right checks for each event
Choose the event before choosing tools. A pull request needs fast feedback, while a protected branch and release tag justify heavier verification and publishing. Keep release jobs separate from routine checks.
| Event | Main goal | Suggested checks | Release access |
|---|---|---|---|
| Pull request | Validate proposed changes | Unit tests, lint, debug APK | None |
| Push to main | Confirm integration | Tests, lint, assemble | None |
| Manual dispatch | Reproduce or diagnose | Selected Gradle tasks | Optional |
| Release tag | Build a store release | Signed AAB or APK | Protected environment |
Make the verification job a required status check in the repository’s branch protection settings. A pull request can then be merged only when the required pipeline succeeds, giving everyone one consistent signal instead of relying on a message in chat.
A team spread across Sydney, Melbourne and Brisbane usually shares a UTC+10 working window. However, New South Wales and Victoria move to UTC+11 during daylight saving, while Perth remains on UTC+8. A schedule set for 23:00 UTC starts at 9:00 am in Brisbane during standard time and 10:00 am during daylight saving, so document the intended local start time.
Create your first GitHub Actions workflow
Create .github/workflows/android-ci.yml in the Android project repository and add the following basic pipeline. The Java and Android SDK versions should match those used by the project.
name: Android CI
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
- uses: android-actions/setup-android@v3
- name: Install Android SDK packages
run: sdkmanager "platforms;android-35" "build-tools;35.0.0" "platform-tools"
- name: Run verification
run: chmod +x gradlew && ./gradlew testDebugUnitTest lintDebug assembleDebug --stacktrace
- uses: actions/upload-artifact@v4
with:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk
actions/checkout retrieves the repository, while setup-java provides a predictable Java runtime. The Gradle setup action caches downloaded dependencies, reducing the time spent waiting on Maven repositories and other external services. If the app uses a different compile SDK or build-tools version, replace the sample version numbers accordingly.
Commit the workflow and run it through workflow_dispatch to confirm the configuration. The uploaded APK gives reviewers something they can install without rebuilding the project locally. The Gradle wrapper is used in CI so the build uses the same dependency versions defined by the repository.
Protect signing credentials and release artefacts
Keep signing material out of Git history. Store the upload keystore as an encoded GitHub Actions secret, and place passwords and aliases in a protected environment. Ordinary verification jobs need read-only repository access, while only the release job should receive publishing authority.
A decoded keystore can be created inside a release step without printing its contents:
- name: Decode upload key
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/upload.jks"
The Gradle signing configuration should read the keystore path, store password, key alias and key password from environment variables. Never pass secrets directly on the command line because that can expose them in logs. Use a dedicated upload key rather than a personal developer key, and rotate both the key and its stored credentials according to the project’s security policy.
Use a protected environment for release automation, with required reviewers where the team needs approval. For Australian users, release notes can use Australian spelling, while Google Play can present prices in AUD and handle applicable taxes. The CI pipeline is responsible for producing a signed Android App Bundle; publishing and store metadata remain controlled through Play Console.
Test databases and Android behaviour
Database checks belong in the same automated pipeline as compilation and linting. For schema and query fixtures, the Android SQLite examples provide practical starting points for adding local data scenarios to JUnit, Robolectric or instrumented tests.
A separate job can use reactivecircus/android-emulator-runner to start an Android virtual device and execute ./gradlew connectedDebugAndroidTest. Choose an API level supported by the test matrix and keep emulator tests separate from the faster unit-test job. This makes ordinary pull request feedback quicker while still allowing device-level behaviour to be checked before release.
Use Australian locales and time zones in relevant tests. Separate cases for Australia/Sydney, Australia/Brisbane and Australia/Perth can expose midnight, daylight-saving and date-format mistakes without waiting for a particular night. Include en-AU formatting where dates, numbers or currency appear, and verify that changing the device time zone does not alter critical business logic.
Diagnose failures and keep builds reliable
Start with the failed step in the GitHub Actions log and identify the exact Gradle task that failed. Run the same command in Android Studio or a terminal, adding --stacktrace or --info when more detail is required. A permission error on gradlew usually means the executable bit was lost, which the workflow handles with chmod +x gradlew.
Common setup failures include a missing Android platform, a Java version that differs from the project configuration, unresolved dependencies or absent signing secrets. Compare the CI versions with gradle-wrapper.properties and the Android Gradle Plugin configuration. Dependency caching makes repeat builds faster, but an inconsistent cache can conceal a repository problem, so diagnose network and credential errors before deleting cached data.
Keep required checks fast, split slow instrumented tests into another job and set a sensible timeout. Pin third-party actions to reviewed versions or commit hashes, and protect release branches with the same verification used for pull requests. An Australian team might say “CI’s all good” in chat once the required jobs pass, but the pull request should still identify the failed task and log clearly. A green required check then provides a dependable signal that the change is ready for review.