This guide walks you through building a Jenkins CI/CD pipeline to build and publish a signed Android App Bundle (AAB) to the Google Play Store. It includes secure signing credentials stored in HashiCorp Vault, manual approval for releases, and multilingual Play Store publishing – all automated with Jenkins and Gradle.
Overview of the Pipeline
The pipeline covers:
- Setting up Google Play credentials.
- Checking out source code.
- Fetching signing credentials from Vault.
- Building a signed Android App Bundle (AAB).
- Requiring manual approval before release.
- Publishing to Google Play with multilingual release notes.
1. Setting Up Google Play Credentials in Jenkins
Before publishing to Google Play, you must authenticate Jenkins using a service account from Google Cloud.
Note: You must install the Google Play Android Publisher plugin in Jenkins before continuing. This plugin enables Jenkins to upload Android App Bundles (AAB) to the Google Play Store.
Step 1: Create a Service Account
- Go to Google Cloud Console.
- Create a service account named something like jenkins-play-publisher.
- Assign the Service Account User role.
- In the Google Play Console, go to Settings → API Access, link the Google Cloud project, and grant access to the service account.
Step 2: Download the JSON Key
- Go to the “Keys” tab of the service account.
- Click “Add Key” → “Create new key” → JSON.
- Save the file securely (service-account.json).
Step 3: Add JSON Key to Jenkins
- In Jenkins, go to Manage Jenkins → Credentials.
- Under a suitable scope (e.g., Global), click Add Credentials:- Kind: Secret file
- File: Upload the downloaded service-account.json
- ID: google-play-service-account
- Description: Google Play Service Account
 
- Kind: 
This key will be referenced in your Jenkins pipeline:
androidApkUpload googleCredentialsId: 'google-play-service-account', ...(Optional) .p12 Key for Legacy Tools
If required (older plugins):
openssl pkcs12 -export -inkey private-key.pem -in service-account.pem -out service-account.p12Modern plugins use JSON, which is preferred.
2. Checking Out Source Code
This step uses Jenkins’ checkout scm to retrieve the source code from your Git repository.
stage('Checkout') {
    steps {
        checkout scm
    }
}3. Fetching Signing Credentials from Vault
We fetch the Android keystore and credentials from HashiCorp Vault securely:
stage('Fetch Signing Credentials') {
    steps {
        script {
            def response = sh(script: "curl -sS -H 'X-Vault-Token: $VAULT_TOKEN' $VAULT_ADDR/${env.SIGNING_KEYSTORE_PATH}", returnStdout: true).trim()
            def jsonData = readJSON text: response
            writeFile file: "${WORKSPACE}/release-keystore.jks", text: jsonData.data.data["release-keystore.jks"]
            env.SIGNING_KEY_ALIAS = jsonData.data.data.keyAlias
            env.SIGNING_KEY_PASSWORD = jsonData.data.data.keyPassword
            env.SIGNING_STORE_PASSWORD = jsonData.data.data.storePassword
        }
    }
}4. Building a Signed AAB
Gradle will build the release AAB using signing properties:
stage('Build AAB') {
    steps {
        script {
            sh """
                ./gradlew bundleProdRelease \
                  -PbuildNumber=${params.VERSION_CODE} \
                  -PversionName=${params.VERSION_NAME} \
                  -PstoreFile='${WORKSPACE}/release-keystore.jks' \
                  -PkeyAlias='${env.SIGNING_KEY_ALIAS}' \
                  -PkeyPassword='${env.SIGNING_KEY_PASSWORD}' \
                  -PstorePassword='${env.SIGNING_STORE_PASSWORD}'
            """
        }
    }
}5. Manual Approval Before Release
Before deploying to Google Play, request manual approval via Jenkins UI:
stage('Approval') {
    steps {
        script {
            timeout(time: 1, unit: 'HOURS') {
                input(
                    message: "Release to Google Play? Version Code: ${params.VERSION_CODE}",
                    submitter: 'admin,developer',
                    ok: 'Proceed'
                )
            }
        }
    }
}6. Publishing to Google Play
The Jenkins Google Play plugin will upload the AAB using the credentials added earlier:
stage('Publish to Google Play') {
    steps {
        script {
            androidApkUpload googleCredentialsId: 'google-play-service-account',
                filesPattern: '**/build/outputs/bundle/prodRelease/*.aab',
                trackName: params.RELEASE_TRACK,
                rolloutPercentage: params.ROLLOUT_PERCENTAGE.toInteger().toString(),
                releaseName: params.VERSION_NAME,
                recentChangeList: [
                    [language: 'en-US', text: params.RELEASE_NOTES_EN]
                ]
        }
    }
}7. Gradle Configuration for Signing
Here’s the signing config in your build.gradle:
signingConfigs {
    release {
        storeFile file(project.property('storeFile'))
        storePassword project.property('storePassword')
        keyAlias project.property('keyAlias')
        keyPassword project.property('keyPassword')
    }
}
buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}Full Jenkinsfile
After you start a pipeline with “Build with Parameters” you will need to set VERSION_CODE, VERSION_NAME, RELEASE_TRACK, ROLLOUT_PERCENTAGE and RELEASE_NOTES

pipeline {
    agent any
    environment {
        ANDROID_HOME = '/usr/lib/android-sdk/'
        SIGNING_KEYSTORE_PATH = 'v1/google-signing-key/data/key'
    }
    options {
        timestamps()
        skipDefaultCheckout(true)
        disableConcurrentBuilds()
    }
    parameters {
        string(name: 'VERSION_CODE', defaultValue: '1')
        string(name: 'VERSION_NAME', defaultValue: '1.0.0')
        choice(name: 'RELEASE_TRACK', choices: ['internal', 'alpha', 'beta', 'production'])
        string(name: 'ROLLOUT_PERCENTAGE', defaultValue: '100')
        text(name: 'RELEASE_NOTES_EN', defaultValue: 'Bug fixes and improvements.')
    }
    stages {
        stage('Checkout') {
            steps {
                container('gradle') {
                    checkout scm
                }
            }
        }
        stage('Fetch Signing Credentials') {
            steps {
                container('gradle') {
                    script {
                        println("Fetching signing credentials from Vault")
                        
                        def printWithNoTrace = { cmd ->
                            return sh(script: "#!/bin/sh -e\n${cmd}", returnStdout: true).trim()
                        }
                        
                        def response = printWithNoTrace("curl -sS -H 'X-Vault-Token: $VAULT_TOKEN' $VAULT_ADDR/${env.SIGNING_KEYSTORE_PATH}")
                        def jsonData = readJSON text: response
                        
                        writeFile file: "${WORKSPACE}/release-keystore.jks", text: jsonData.data.data["release-keystore.jks"]
                        
                        env.SIGNING_KEY_ALIAS = jsonData.data.data.keyAlias
                        env.SIGNING_KEY_PASSWORD = jsonData.data.data.keyPassword
                        env.SIGNING_STORE_PASSWORD = jsonData.data.data.storePassword
                        
                        println("Signing credentials fetched successfully")
                    }
                }
            }
        }
        stage('Build AAB') {
            steps {
                container('gradle') {
                    script {
                        println("Building AAB")
                        println("ANDROID_HOME: ${env.ANDROID_HOME}")
                        try {
                            withEnv(["ANDROID_HOME=${env.ANDROID_HOME}"]) {
                                sh(script: """
                                    #!/bin/bash
                                    set +x
                                    ./gradlew bundleProdRelease -PbuildNumber=${params.VERSION_CODE} -PversionName=${params.VERSION_NAME} -PstoreFile='${WORKSPACE}/release-keystore.jks' -PkeyAlias='${env.SIGNING_KEY_ALIAS}' -PkeyPassword='${env.SIGNING_KEY_PASSWORD}' -PstorePassword='${env.SIGNING_STORE_PASSWORD}'
                                """)
                            }
                        } catch (Exception e) {
                            error "Building AAB failed: ${e.getMessage()}"
                        }
                    }
                }
            }
        }
        stage('Approval') {
            steps {
                script {
                    timeout(time: 1, unit: 'HOURS') {
                        input(
                            message: "Release to Google Play? Version Code: ${params.VERSION_CODE}",
                            submitter: 'admin,developer',
                            ok: 'Proceed'
                        )
                    }
                }
            }
        }
        stage('Publish to Google Play') {
            steps {
                container('gradle') {
                    script {
                        println("Publishing to Google Play")
                        try {
                            androidApkUpload googleCredentialsId: 'google-play-service-account',
                                         filesPattern: '**/build/outputs/bundle/prodRelease/*.aab',
                                         trackName: params.RELEASE_TRACK,
                                         rolloutPercentage: params.ROLLOUT_PERCENTAGE.toInteger().toString(),
                                         releaseName: params.VERSION_NAME,
                                         recentChangeList: [
                                             [language: 'en-US', text: params.RELEASE_NOTES_EN]
                                         ]
                        } catch (Exception e) {
                            error "Publishing to Google Play failed: ${e.getMessage()}"
                        }
                    }
                }
            }
        }
    }
}
This Jenkins pipeline builds, signs, and deploys Android AABs securely to Google Play, with approval gating, multilingual notes, and Vault-based key storage. It’s a robust, production-ready solution that simplifies your mobile release lifecycle.
