Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Azure DevOps can automate an Android app’s tests, builds, signing and delivery; Gradle and the Android toolchain do the actual compiling. A practical setup starts with a pipeline that runs the repository’s Gradle wrapper, tests the project, builds a debug APK and publishes it as a downloadable pipeline artifact. Add release signing and Google Play deployment only after that baseline works.
How the pipeline fits together
Azure DevOps is the automation layer, not an Android app-building environment. Your repository supplies the Gradle wrapper and project configuration; the pipeline runs them on an agent with a compatible JDK and Android SDK.
Git push or pull request
→ Azure Pipeline
→ Gradle tests and checks
→ APK or AAB build
→ optional release signing
→ pipeline artifact
→ optional Google Play release
Azure Repos or GitHub stores the source. Azure Pipelines runs the YAML workflow. An agent pool supplies the machine. Secure Files and protected variables hold release credentials; a service connection can authorize Google Play deployment. Pipeline artifacts retain build outputs for download. Environments and approvals can gate promotion to production. See Microsoft’s Azure Pipelines overview and its agent documentation.
Free tools Windows power users keep installed
One-click scans. No signup required.
For new pipelines, use the Gradle task or call the wrapper directly. Microsoft marks the older AndroidBuild@1 task deprecated. The example below uses Gradle@4, but task versions and agent images can change; check the current Gradle build and artifact guidance when adapting it.
#1 Best Overall
Before you start
- An Azure DevOps organization and project, plus a Git repository containing the Android project.
- A project that builds locally, including
gradlew(orgradlew.bat) andgradle/wrapper/. - The project’s Android modules, Gradle files and any required repository configuration committed to source control.
- The required JDK and Android SDK/build-tools versions for the project’s Android Gradle Plugin (AGP).
- A release keystore and Google Play Console access only if you plan to create a signed release or publish to Play.
Do not assume one JDK, Gradle, AGP, compile SDK or build-tools version works for every app. Determine the project’s requirements from its Gradle configuration and AGP compatibility, then select a matching agent/toolchain. The JDK version shown in the YAML is only an example.
Use the wrapper rather than a globally installed Gradle version: it selects the Gradle version recorded by the repository and helps make local and CI builds consistent. Task names depend on modules, variants and product flavors. Inspect them locally with ./gradlew tasks. Common examples include ./gradlew test, ./gradlew lint, ./gradlew assembleDebug and ./gradlew bundleRelease. A flavored app might instead use ./gradlew :app:testDebugUnitTest or ./gradlew :app:bundleProductionRelease.
Create a validation pipeline and publish a debug APK
In your Azure DevOps project, open Pipelines > New pipeline, choose the repository provider, select the repository, and choose a starter YAML pipeline or an existing YAML file. Save the pipeline definition as azure-pipelines.yml in the repository. The exact screens can change, but the flow is to connect the repository, commit the YAML and run it. First get validation and a debug artifact working; do not give pull-request builds access to production signing credentials.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →This template runs unit tests, builds a debug APK, and publishes matching APK files as a pipeline artifact. Change the JDK, Gradle tasks, working directory, agent image and output patterns for your project. If it uses a subdirectory, set the working directory or wrapper path accordingly.
trigger:
branches:
include:
- main
pr:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
variables:
GRADLE_USER_HOME: $(Pipeline.Workspace)/.gradle
steps:
- checkout: self
clean: true
# Illustrative only: choose the JDK required by this project's AGP.
- task: JavaToolInstaller@0
displayName: 'Use required JDK'
inputs:
versionSpec: '17'
jdkArchitectureOption: 'x64'
jdkSourceOption: 'PreInstalled'
- bash: chmod +x ./gradlew
displayName: 'Make Gradle wrapper executable'
- task: Cache@2
displayName: 'Cache Gradle dependencies'
inputs:
key: 'gradle | "$(Agent.OS)" | **/gradle-wrapper.properties'
restoreKeys: |
gradle | "$(Agent.OS)"
path: $(GRADLE_USER_HOME)
- task: Gradle@4
displayName: 'Run unit tests'
inputs:
gradleWrapperFile: 'gradlew'
workingDirectory: ''
tasks: 'test'
publishJUnitResults: true
testResultsFiles: '**/TEST-*.xml'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.17'
gradleOptions: '-Xmx3072m'
sonarQubeRunAnalysis: false
- task: Gradle@4
displayName: 'Build debug APK'
inputs:
gradleWrapperFile: 'gradlew'
workingDirectory: ''
tasks: 'assembleDebug'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.17'
gradleOptions: '-Xmx3072m'
- task: CopyFiles@2
displayName: 'Collect APK'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: '**/build/outputs/apk/**/*.apk'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
flattenFolders: false
- task: PublishPipelineArtifact@1
displayName: 'Publish APK artifact'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'android-package'
The sample assumes the wrapper is at the repository root, the selected JDK is installed on the agent, and the task names and output glob match the project. A multi-module app may need an explicit task such as :app:assembleDebug. On a Windows agent, use gradlew.bat and Windows-appropriate script steps rather than the Linux chmod command. If the selected image does not have the project’s required Android SDK packages, install or otherwise provide those packages as part of the pipeline or use a prepared self-hosted agent.
When the run completes, open the run’s Summary and download the android-package artifact. It should contain the APK if the build succeeded and the copy pattern matched. If it is empty, check the Gradle task, module and variant output path before changing the publisher step. The Microsoft Gradle artifact example uses the same general copy-and-publish pattern.
Add lint, reports and reliable caching
Unit tests run without an Android device. Add a lint task if the project has Android lint configured; Kotlin projects may also use checks such as Detekt when that plugin is installed and configured:
./gradlew lint
./gradlew detekt
Use the task names for your variants if the default tasks do not cover the intended build. Keep test XML and lint reports available when a run fails. The Gradle task can publish JUnit results, or a pipeline can publish test results separately with PublishTestResults@2. Collect reports alongside the APK/AAB if they are useful for review.
Caching can reduce dependency downloads, but a cache is an optimization, not a source of truth. Use keys that reflect the OS and wrapper or dependency lock state rather than sharing an indiscriminate cache. If a build starts failing after dependency changes, stop Gradle and retry without the cache:
./gradlew --stop
./gradlew clean --refresh-dependencies
Choose APK or AAB
- APK: installable package useful for QA, direct installation and some distribution services. A debug APK is for testing, not a production release.
- AAB: Android App Bundle, generally the preferred format for Google Play releases.
- Release variant: uses release configuration and must be signed appropriately; do not confuse it with a debug build.
Build an app bundle with ./gradlew bundleRelease. A typical output is app/build/outputs/bundle/release/app-release.aab, but modules and flavors change the path and filename. Confirm the actual output in the pipeline rather than relying on a fixed glob. For an obfuscated release, preserve the matching mapping file so crash reports can be de-obfuscated.
Microsoft’s AndroidSigning@3 task is documented for signing and aligning APK files with apksigner; it is not a general-purpose AAB signing step. For AAB releases, configure signing as part of the project’s Gradle release build. See the AndroidSigning@3 reference.
Sign release builds without committing the key
A release keystore identifies the app’s signing identity. Do not commit the keystore, passwords, Google service-account JSON or generated signing-property files to Git. Microsoft’s mobile signing guidance describes storing a keystore as an Azure Pipelines Secure File and retrieving it at build time.
Rank #3
- Generate or obtain the keystore outside the pipeline and keep a protected backup.
- Upload it under Pipelines > Library > Secure files, then authorize only the pipeline(s) that need it.
- Store passwords and aliases as secret variables, preferably in a protected variable group. Restrict access to that group and its consuming pipelines.
- Download the secure file only in the release job, and pass its temporary path and secret values through the signing mechanism expected by the project.
- Publish the intended signed output and avoid printing secrets or exposing them in verbose command logs.
One illustrative pattern for a project whose Gradle configuration supports Android’s injected signing properties is:
variables:
- group: android-release-secrets
steps:
- task: DownloadSecureFile@1
name: releaseKeystore
displayName: 'Download release keystore'
inputs:
secureFile: 'release.keystore'
- bash: |
./gradlew bundleRelease
-Pandroid.injected.signing.store.file="$(releaseKeystore.secureFilePath)"
-Pandroid.injected.signing.store.password="$(keystorePassword)"
-Pandroid.injected.signing.key.alias="$(keyAlias)"
-Pandroid.injected.signing.key.password="$(keyPassword)"
displayName: 'Build signed release bundle'
This is not a universal Gradle signing interface: the project must consume those properties. Some projects use environment variables or a protected signing-properties file instead. Do not echo secret values; masking in logs is not a substitute for keeping secrets out of output. Restrict Secure File and variable-group permissions, especially for production credentials. Azure DevOps documents these as protected pipeline resources.
If you build an APK unsigned by Gradle and need to sign it in a separate step, AndroidSigning@3 is an option. Its inputs require a keystore and credentials, and its task reference lists agent version 2.182.1 or later. Match the file pattern to the actual variant output; a pattern that matches nothing will not produce a signed file. Do not use this APK task as a substitute for Gradle release signing an AAB.
Publish useful release outputs
In addition to the APK or AAB, consider retaining the mapping file, unit-test XML, lint reports, instrumentation-test results and build metadata such as version name, version code, commit SHA and pipeline build number. A project-specific collection step can look like this:
- task: CopyFiles@2
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/build/outputs/**/*.apk
**/build/outputs/**/*.aab
**/build/outputs/mapping/**/*.txt
**/build/reports/**
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'android-release'
Review the collected files to ensure the mapping file belongs to the release artifact and that reports do not contain information you do not want retained. Set artifact retention according to your team’s release and audit needs.
Instrumentation tests need a device strategy
Instrumented tests require an Android device or emulator, unlike ordinary unit tests. Microsoft’s Android pipeline guidance cautions that Microsoft-hosted Ubuntu agents do not provide hardware acceleration for Android emulators. A hosted agent can therefore be a poor fit for reliable or fast emulator testing. Options include a self-hosted Linux agent with a configured emulator, a hosted device-testing provider, or a separate scheduled test pipeline. Keep fast unit tests and static checks on pull requests even if device tests run less often.
For emulator failures, verify the system image and AVD name, run headless, allow a longer boot timeout, and try disabling snapshots if they preserve stale state. Confirm that the agent supports the required acceleration. Save Logcat and test reports as artifacts so failures can be diagnosed after the agent is gone.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Optional: deploy to Google Play internal testing
Play deployment requires more than an Azure task: the app must be registered in Play Console, the account and application must be accessible to a Google service account, the signed APK/AAB must be valid, and the version code must be greater than the one already uploaded. Configure a Google Play service connection in Azure DevOps and protect it as a production resource. Install the Google Play extension if required by the task, then use its release task; task inputs and file patterns can vary with the installed extension version.
An illustrative release task for an AAB is:
- task: GooglePlayRelease@4
displayName: 'Publish to Google Play internal testing'
inputs:
apkFile: '$(Pipeline.Workspace)/**/*.aab'
serviceEndpoint: 'GooglePlay-Production'
track: 'internal'
Despite the input name shown in this example, verify the installed task’s accepted file format and inputs before running it; the Google Play extension’s task documentation is authoritative for your installed version. Use the internal track first, then promote only after validation. Microsoft also documents GooglePlayPromote@3 for moving a release between tracks in its Android pipeline guidance. Add an environment approval before production, and do not expose the Play service connection to untrusted pull-request code.
Hosted or self-hosted agent?
For ordinary Gradle builds, unit tests, lint and artifact creation, start with a Microsoft-hosted agent. It offers a clean machine for each run and avoids maintaining build infrastructure, though available tools can change and caches are less durable. Hosted-agent jobs are subject to organization-level parallel-job capacity and time limits.
Choose a self-hosted agent when you need emulator acceleration, private network access, a controlled SDK image or persistent caches enough to justify operating the machine. Your team then owns patching, cleanup, monitoring and credential protection. Persistent workspaces can retain stale outputs or secrets, and a compromised agent can expose source and signing material. See the current parallel jobs and licensing limits before estimating capacity: free allocations and billing conditions can depend on organization eligibility and configuration. Azure DevOps Services and Azure DevOps Server have different operating and licensing models.
Recommended Free Tools
Troubleshoot common failures
“Works locally, fails in Azure”
Check that the wrapper is executable on Linux, the JDK matches the project, required Android SDK packages are available, and private Maven repositories are reachable. Look for local-only files, environment variables, case-sensitive path differences, and signing files that were never committed (or should not be committed). Useful diagnostics include:
Best Value
chmod +x ./gradlew
./gradlew --version
./gradlew tasks
./gradlew clean test --stacktrace
Capture the agent image/tool information, test XML, lint output and a listing of generated artifact filenames. A Gradle build scan can help if your organization permits it.
Missing APK or AAB
Confirm the build task succeeded, then inspect the output path and filename for the exact module and flavor. Adjust the CopyFiles@2 glob to that path. Do not assume an app named app or a variant named release.
Signing fails
Check the keystore password, alias and key password separately; confirm the Secure File is authorized; verify the release variant uses the intended signing configuration; and ensure the signing step’s glob matches a real file. Print filenames, not credentials. For APKs, validate the result with apksigner verify. Check the package name and version code before publishing.
Dependencies or cache cause a failure
Stop the Gradle daemon, retry with refreshed dependencies and temporarily bypass the cache. If the uncached build succeeds, revise the cache key to reflect the wrapper or dependency changes rather than relying on a stale global cache.
Google Play rejects the upload
Check that the service account has the necessary Play Console permissions, the app exists in the console, the package name matches, the version code is new, and the artifact is signed with the expected key. Store metadata or declarations may also be required. Verify the selected track and service-connection authorization before retrying.
Harden the release workflow
- Run pull-request validation without production signing keys or Play credentials.
- Restrict signing files, secret variable groups and service connections to trusted release pipelines.
- Gate production with branch policies, successful checks and an environment approval.
- Keep version codes increasing and retain the matching mapping file for obfuscated releases.
- Review artifact retention, clean self-hosted workspaces, and rotate credentials under your release policy.
Azure DevOps provides protected resources and permissions for this separation; configure them so a change to arbitrary YAML cannot automatically consume a production credential.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

