Automating Android Releases with GitHub Actions CI/CD
This guide walks you through setting up a complete Continuous Integration and Continuous Deployment (CI/CD) pipeline using GitHub Actions. This pipeline will automatically build a signed release APK for your Android application whenever you push a git tag (e.g., v1.0.0
) or manually trigger the workflow.
Prerequisites
- An Android project within your repository (likely in an
android
subfolder if integrated with another framework). - A GitHub repository for your project.
- Node.js and npm (or yarn) installed locally if your project requires JS dependencies (common in cross-platform frameworks).
- Java Development Kit (JDK) installed locally for generating the keystore.
Step 1: Generate Your Release Keystore
The release keystore contains the private key required to sign your Android application, verifying you as the author.
-
Open your terminal or command prompt.
-
Navigate to the
android/app
directory within your project (or wherever you want to store the keystore initially). -
Run the
keytool
command (part of the JDK):Terminal window keytool -genkey -v -keystore release.keystore -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias your-key-alias -
Important:
- Replace
your-key-alias
with a unique name for your key. Remember this alias. - You will be prompted to create passwords for the keystore (
storePassword
) and the key alias (keyPassword
). Choose strong passwords and remember them securely. These will be needed for GitHub Secrets. - You’ll also be asked for identification details (Name, Organization, etc.). Fill these out appropriately.
This command generates a
release.keystore
file in your current directory. Do not commit this file directly to your Git repository. - Replace
Step 2: Configure Android Gradle Build (build.gradle
)
Modify your app-level build.gradle
file (usually android/app/build.gradle
) to read signing information from environment variables, which will be securely provided by GitHub Actions secrets.
-
Locate the
android { ... }
block in yourbuild.gradle
file. -
Inside the
android
block, find or add thesigningConfigs { ... }
block. -
Inside
signingConfigs
, modify or add therelease { ... }
block as follows:android {// ... other configurations like compileSdkVersion, defaultConfig ...signingConfigs {debug {// Debug signing config (usually default)storeFile file('../debug.keystore') // Or your debug keystore pathstorePassword 'android'keyAlias 'androiddebugkey'keyPassword 'android'}release {// Read signing configuration from environment variables// Set by GitHub Actions secretsif (System.getenv("ANDROID_RELEASE_KEYSTORE_PATH") != null &&System.getenv("ANDROID_STORE_PASSWORD") != null &&System.getenv("ANDROID_KEY_ALIAS") != null &&System.getenv("ANDROID_KEY_PASSWORD") != null) {storeFile file(System.getenv("ANDROID_RELEASE_KEYSTORE_PATH")) // We'll set this path in the workflowstorePassword System.getenv("ANDROID_STORE_PASSWORD")keyAlias System.getenv("ANDROID_KEY_ALIAS")keyPassword System.getenv("ANDROID_KEY_PASSWORD")} else {println("Warning: Release signing environment variables not fully set. Release signing disabled.")// Optionally disable release signing or throw an error if variables aren't set// signingConfig null // Uncomment this line if you want builds to fail without secrets}}}buildTypes {debug {// Debug build type configsigningConfig signingConfigs.debug}release {// ... other release build type settings like minifyEnabled, shrinkResources ...// Ensure the release build type uses the release signing configsigningConfig signingConfigs.release // <--- MAKE SURE THIS LINE USES signingConfigs.release}}// ... rest of the android block ...} -
Crucially, ensure that within the
buildTypes { release { ... } }
block, thesigningConfig
is set tosigningConfigs.release
.
Step 3: Encode Keystore to Base64
GitHub Secrets cannot store binary files directly. You need to encode your release.keystore
file into a Base64 text string.
-
On Linux/macOS:
Terminal window base64 release.keystore > release.keystore.base64Or, to copy directly to clipboard on macOS:
Terminal window base64 release.keystore | pbcopyOn Linux (requires
xclip
):Terminal window base64 release.keystore | xclip -selection clipboard -
On Windows (PowerShell):
Terminal window [Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File -FilePath release.keystore.base64.txt -Encoding ASCIIOr copy to clipboard:
Terminal window [Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Set-Clipboard
Copy the entire Base64 encoded string generated by these commands. You’ll need it for the next step.
Step 4: Configure GitHub Secrets
Securely store your signing credentials and the encoded keystore in your GitHub repository settings.
-
Navigate to your GitHub Repository.
-
Go to Settings > Secrets and variables > Actions.
-
Under Repository secrets, click New repository secret four times to create the following secrets:
-
Name:
ANDROID_RELEASE_KEYSTORE_BASE64
Secret: Paste the entire Base64 encoded string of yourrelease.keystore
file from Step 3. -
Name:
ANDROID_STORE_PASSWORD
Secret: Enter the plain text password you created for therelease.keystore
file itself. -
Name:
ANDROID_KEY_ALIAS
Secret: Enter the plain text alias name you chose for your key (e.g.,your-key-alias
). -
Name:
ANDROID_KEY_PASSWORD
Secret: Enter the plain text password you created for the specific key alias (this might be the same as the store password, or different).
-
Step 5: Create the GitHub Actions Workflow File
Create a YAML file that defines the CI/CD pipeline.
-
In your project’s root directory, create the folders
.github/workflows/
if they don’t exist. -
Inside
.github/workflows/
, create a file namedandroid-release.yml
. -
Paste the following content into
android-release.yml
:.github/workflows/android-release.yml name: Android Release Buildon:# Allows manual triggering from the GitHub Actions UIworkflow_dispatch:# Triggers on pushing tags matching the v*.*.* pattern (e.g., v1.0.0, v2.3.4)push:tags:- 'v*.*.*'jobs:build-android:name: Build Signed Android Release APKruns-on: ubuntu-latest# Permissions needed to create a GitHub Releasepermissions:contents: write# Define environment variables accessible to all steps in this jobenv:# Standard signing credentials from secretsANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}# Define the path where the keystore will be decoded within the runnerANDROID_RELEASE_KEYSTORE_PATH: ${{ github.workspace }}/android/app/release.keystore# === WORKAROUND FOR CHECKING SECRET PRESENCE ===# Sets 'true'/'false' string based on whether the Base64 secret exists and is non-emptyKEYSTORE_SECRET_IS_SET: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 != '' }}# ==============================================steps:# Step 1: Checkout Repository- name: Checkout Repositoryuses: actions/checkout@v4# Step 2: Set up JDK 17 (Adjust version if needed)- name: Set up JDK 17uses: actions/setup-java@v4with:java-version: '17'distribution: 'temurin'cache: 'gradle'# Step 3: Set up Node.js (If your project uses Node.js)- name: Set up Node.jsuses: actions/setup-node@v4with:node-version: '20' # Adjust version as neededcache: 'npm' # Use 'yarn' if your project uses Yarn# Step 4: Install JavaScript Dependencies (If applicable)- name: Install JavaScript Dependencies# Only run if package-lock.json or package.json existsif: hashFiles('package-lock.json', 'package.json') != ''run: npm ci # Or: yarn install --frozen-lockfile# Step 5: Setup Android SDK- name: Setup Android SDKuses: android-actions/setup-android@v3# Step 6: Decode Keystore (Conditional Step)- name: Decode Keystore# Only run if the KEYSTORE_SECRET_IS_SET env var is 'true'if: env.KEYSTORE_SECRET_IS_SET == 'true'run: |echo "KEYSTORE_SECRET_IS_SET is true. Decoding keystore..."# Ensure the target directory existsmkdir -p android/app# Decode the Base64 secret and write to the path defined in env varecho "${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}" | base64 --decode > ${{ env.ANDROID_RELEASE_KEYSTORE_PATH }}echo "Keystore decoded successfully to ${{ env.ANDROID_RELEASE_KEYSTORE_PATH }}"# Verify the file was createdif [ ! -f "${{ env.ANDROID_RELEASE_KEYSTORE_PATH }}" ]; thenecho "::error::Keystore file was not created after decoding!"exit 1fils -l ${{ env.ANDROID_RELEASE_KEYSTORE_PATH }}# Step 6b: Handle missing keystore secret (Error handling)- name: Handle Missing Keystore Secret# Only run if the KEYSTORE_SECRET_IS_SET env var is 'false'if: env.KEYSTORE_SECRET_IS_SET == 'false'run: |echo "::error::Required secret ANDROID_RELEASE_KEYSTORE_BASE64 is not set or is empty. Cannot sign the release build."exit 1 # Fail the workflow explicitly# Step 7: Make gradlew executable- name: Make gradlew executablerun: chmod +x ./android/gradlew# Step 8: Build Release APK- name: Build Release APKworking-directory: ./android # Navigate to the android directory# Run the Gradle wrapper command to clean and assemble the release buildrun: ./gradlew clean assembleRelease# Add --no-daemon if you encounter Gradle daemon issues: ./gradlew clean assembleRelease --no-daemon# Step 9: Upload Release APK Artifact- name: Upload Release APK Artifactuses: actions/upload-artifact@v4with:name: release-apks # Name of the artifact bundle# Path to the generated APK(s) - may vary slightly based on flavors/splitspath: |android/app/build/outputs/apk/release/*.apkretention-days: 7 # How long to keep the artifact# Step 10: Create GitHub Release (Only runs on tag push)- name: Create GitHub Release# Condition: Only run if the trigger was a tag pushif: startsWith(github.ref, 'refs/tags/')uses: softprops/action-gh-release@v2with:# Use the tag name (e.g., v1.0.0) for the releasetag_name: ${{ github.ref_name }}name: Release ${{ github.ref_name }} # Name of the GitHub Release# Files to attach to the releasefiles: |android/app/build/outputs/apk/release/*.apkenv:# The GITHUB_TOKEN is automatically provided by GitHub ActionsGITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -
Commit and push this new file (
.github/workflows/android-release.yml
) to your repository.
Step 6: Triggering the Workflow
The workflow will now run automatically when:
- You push a tag to your repository that matches the pattern
v*.*.*
(e.g.,git tag v1.0.0
,git push origin v1.0.0
). - You manually trigger it from the GitHub Actions tab in your repository (Select the “Android Release Build” workflow and click “Run workflow”).
Step 7: Workflow Execution and Outcome
When the workflow runs successfully:
- It checks out your code.
- Sets up Java, Node.js (if needed), and the Android SDK.
- Decodes the Base64 keystore secret back into the
release.keystore
file within the runner environment at the path specified (android/app/release.keystore
). - Runs the
./gradlew assembleRelease
command, which uses the decoded keystore and the passwords/alias from the secrets (via environment variables) to build and sign your APK. - Uploads the generated
*.apk
file(s) as a build artifact namedrelease-apks
. You can download this artifact from the workflow run summary page. - If triggered by a tag push, it creates a new GitHub Release named after the tag (e.g., “Release v1.0.0”) and attaches the signed
*.apk
file(s) to it.