Skip to content

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.

  1. Open your terminal or command prompt.

  2. Navigate to the android/app directory within your project (or wherever you want to store the keystore initially).

  3. 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
  4. 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.

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.

  1. Locate the android { ... } block in your build.gradle file.

  2. Inside the android block, find or add the signingConfigs { ... } block.

  3. Inside signingConfigs, modify or add the release { ... } block as follows:

    android {
    // ... other configurations like compileSdkVersion, defaultConfig ...
    signingConfigs {
    debug {
    // Debug signing config (usually default)
    storeFile file('../debug.keystore') // Or your debug keystore path
    storePassword 'android'
    keyAlias 'androiddebugkey'
    keyPassword 'android'
    }
    release {
    // Read signing configuration from environment variables
    // Set by GitHub Actions secrets
    if (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 workflow
    storePassword 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 config
    signingConfig signingConfigs.debug
    }
    release {
    // ... other release build type settings like minifyEnabled, shrinkResources ...
    // Ensure the release build type uses the release signing config
    signingConfig signingConfigs.release // <--- MAKE SURE THIS LINE USES signingConfigs.release
    }
    }
    // ... rest of the android block ...
    }
  4. Crucially, ensure that within the buildTypes { release { ... } } block, the signingConfig is set to signingConfigs.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.base64

    Or, to copy directly to clipboard on macOS:

    Terminal window
    base64 release.keystore | pbcopy

    On 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 ASCII

    Or 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.

  1. Navigate to your GitHub Repository.

  2. Go to Settings > Secrets and variables > Actions.

  3. 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 your release.keystore file from Step 3.

    • Name: ANDROID_STORE_PASSWORD Secret: Enter the plain text password you created for the release.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.

  1. In your project’s root directory, create the folders .github/workflows/ if they don’t exist.

  2. Inside .github/workflows/, create a file named android-release.yml.

  3. Paste the following content into android-release.yml:

    .github/workflows/android-release.yml
    name: Android Release Build
    on:
    # Allows manual triggering from the GitHub Actions UI
    workflow_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 APK
    runs-on: ubuntu-latest
    # Permissions needed to create a GitHub Release
    permissions:
    contents: write
    # Define environment variables accessible to all steps in this job
    env:
    # Standard signing credentials from secrets
    ANDROID_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 runner
    ANDROID_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-empty
    KEYSTORE_SECRET_IS_SET: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 != '' }}
    # ==============================================
    steps:
    # Step 1: Checkout Repository
    - name: Checkout Repository
    uses: actions/checkout@v4
    # Step 2: Set up JDK 17 (Adjust version if needed)
    - name: Set up JDK 17
    uses: actions/setup-java@v4
    with:
    java-version: '17'
    distribution: 'temurin'
    cache: 'gradle'
    # Step 3: Set up Node.js (If your project uses Node.js)
    - name: Set up Node.js
    uses: actions/setup-node@v4
    with:
    node-version: '20' # Adjust version as needed
    cache: '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 exists
    if: hashFiles('package-lock.json', 'package.json') != ''
    run: npm ci # Or: yarn install --frozen-lockfile
    # Step 5: Setup Android SDK
    - name: Setup Android SDK
    uses: 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 exists
    mkdir -p android/app
    # Decode the Base64 secret and write to the path defined in env var
    echo "${{ 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 created
    if [ ! -f "${{ env.ANDROID_RELEASE_KEYSTORE_PATH }}" ]; then
    echo "::error::Keystore file was not created after decoding!"
    exit 1
    fi
    ls -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 executable
    run: chmod +x ./android/gradlew
    # Step 8: Build Release APK
    - name: Build Release APK
    working-directory: ./android # Navigate to the android directory
    # Run the Gradle wrapper command to clean and assemble the release build
    run: ./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 Artifact
    uses: actions/upload-artifact@v4
    with:
    name: release-apks # Name of the artifact bundle
    # Path to the generated APK(s) - may vary slightly based on flavors/splits
    path: |
    android/app/build/outputs/apk/release/*.apk
    retention-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 push
    if: startsWith(github.ref, 'refs/tags/')
    uses: softprops/action-gh-release@v2
    with:
    # Use the tag name (e.g., v1.0.0) for the release
    tag_name: ${{ github.ref_name }}
    name: Release ${{ github.ref_name }} # Name of the GitHub Release
    # Files to attach to the release
    files: |
    android/app/build/outputs/apk/release/*.apk
    env:
    # The GITHUB_TOKEN is automatically provided by GitHub Actions
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  4. 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:

  1. 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).
  2. 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:

  1. It checks out your code.
  2. Sets up Java, Node.js (if needed), and the Android SDK.
  3. Decodes the Base64 keystore secret back into the release.keystore file within the runner environment at the path specified (android/app/release.keystore).
  4. 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.
  5. Uploads the generated *.apk file(s) as a build artifact named release-apks. You can download this artifact from the workflow run summary page.
  6. 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.