Post

Optimizing Android Builds in GitHub Actions: Environments, Caching, and Best Practices

Optimizing Android Builds in GitHub Actions: Environments, Caching, and Best Practices

Building Android applications in GitHub Actions can be a resource-intensive process, often taking several minutes to complete. However, with the right optimizations, you can significantly reduce build times and improve the security of your CI/CD pipeline. In this article, we’ll explore how to optimize Android builds in GitHub Actions with a focus on environments, caching strategies, and security best practices.

The Challenge with Android Builds

Android builds are notoriously slow due to several factors:

  1. Large dependency trees that need to be downloaded and processed
  2. Resource-intensive compilation of Java/Kotlin code
  3. APK packaging and signing processes
  4. Emulator startup times for instrumented tests

Without proper optimization, a simple Android build can take anywhere from 10-30 minutes, which can severely impact development velocity.

Leveraging GitHub Actions Environments for Security

One of the most important aspects of CI/CD is managing sensitive information like signing keys and credentials. GitHub Actions environments provide a secure way to manage these secrets.

Creating Your Build Environment

  1. Navigate to your GitHub repository
  2. Click on the Settings tab
  3. In the left sidebar, select Environments
  4. Click the New environment button and name it android-build
  5. Add your secrets to this environment:
    • RELEASE_KEYSTORE_BASE64
    • RELEASE_STORE_PASSWORD
    • RELEASE_KEY_ALIAS
    • RELEASE_KEY_PASSWORD

Linking Your Workflow to the Environment

To access these secrets, you need to link your workflow job to the environment:

1
2
3
4
5
6
7
jobs:
  build-android:
    runs-on: ubuntu-latest
    environment: android-build

    steps:
      # Your build steps here

This approach ensures that sensitive data is only available to jobs that explicitly require it, following the principle of least privilege.

Optimizing Build Performance with Caching

Caching is crucial for reducing build times. Android builds can benefit from multiple caching layers.

Caching Node Dependencies

If you’re building a React Native app or using Node.js tools:

1
2
3
4
5
6
7
8
- name: Set up Node.js with Yarn caching
  uses: actions/setup-node@v4
  with:
    node-version: '18'
    cache: 'yarn'

- name: Install Yarn dependencies
  run: yarn install --frozen-lockfile

Caching Gradle Dependencies

For Android builds, Gradle dependencies are a major time sink:

1
2
3
4
5
6
7
8
9
- name: Set up Gradle caching
  uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: $-gradle-$
    restore-keys: |
      $-gradle-

Caching Build Outputs

Cache intermediate build artifacts to avoid redundant compilation:

1
2
3
4
5
6
7
- name: Cache build outputs
  uses: actions/cache@v4
  with:
    path: |
      android/app/build
      !android/app/build/outputs/apk
    key: $-android-build-$

Complete Workflow Example

Here’s a comprehensive workflow that incorporates all the optimizations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
name: Android Build

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-android:
    runs-on: ubuntu-latest
    environment: android-build

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Set up Node.js (for React Native)
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'yarn'

      - name: Install Node dependencies
        run: yarn install --frozen-lockfile

      - name: Cache Gradle wrapper
        uses: actions/cache@v4
        with:
          path: ~/.gradle/wrapper
          key: $-gradle-wrapper-$

      - name: Cache Gradle dependencies
        uses: actions/cache@v4
        with:
          path: ~/.gradle/caches
          key: $-gradle-$
          restore-keys: |
            $-gradle-

      - name: Decode Keystore
        run: |
          echo "$" | base64 -d > android/app/release-key.keystore

      - name: Create keystore.properties
        run: |
          echo "storeFile=release-key.keystore" > android/keystore.properties
          echo "storePassword=$" >> android/keystore.properties
          echo "keyAlias=$" >> android/keystore.properties
          echo "keyPassword=$" >> android/keystore.properties

      - name: Build Android Release
        run: |
          cd android
          ./gradlew assembleRelease --daemon --parallel --configure-on-demand

      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: app-release.apk
          path: android/app/build/outputs/apk/release/app-release.apk

Advanced Optimization Techniques

Parallel Builds

For multi-module projects, you can parallelize builds:

1
2
3
4
- name: Build modules in parallel
  run: |
    cd android
    ./gradlew :module1:assembleRelease :module2:assembleRelease --parallel

Build Cache with Gradle

Enable Gradle’s built-in build cache:

1
2
3
4
5
6
- name: Setup Gradle Build Cache
  run: |
    mkdir -p ~/.gradle
    echo "org.gradle.caching=true" >> ~/.gradle/gradle.properties
    echo "org.gradle.parallel=true" >> ~/.gradle/gradle.properties
    echo "org.gradle.configureondemand=true" >> ~/.gradle/gradle.properties

Conditional Builds

Only build on certain branches or conditions:

1
2
3
4
jobs:
  build-android:
    if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'build-android')
    # ... rest of the job

Security Best Practices

  1. Never commit secrets to your repository
  2. Use environments to scope secrets to specific deployment targets
  3. Regularly rotate your signing keys and passwords
  4. Limit permissions on your GitHub Actions workflows
  5. Use protected branches to prevent direct pushes to critical branches

Monitoring and Debugging

For troubleshooting build issues:

1
2
3
4
5
6
7
8
9
10
- name: Debug information
  run: |
    echo "Java version:"
    java -version
    echo "Node version:"
    node --version
    echo "Yarn version:"
    yarn --version
    echo "Gradle version:"
    cd android && ./gradlew --version

Conclusion

Optimizing Android builds in GitHub Actions requires a combination of proper caching, security practices, and build configuration. By leveraging GitHub Actions environments for secret management and implementing multi-layered caching strategies, you can reduce build times from 20+ minutes to just a few minutes while maintaining security best practices.

The key takeaways are:

  1. Use environments to securely manage secrets
  2. Implement comprehensive caching for dependencies and build outputs
  3. Optimize Gradle settings for parallel execution
  4. Monitor build performance and adjust caching strategies as needed

With these optimizations, your Android builds will be faster, more secure, and more reliable, enabling you to deliver features to your users more quickly.

This post is licensed under CC BY 4.0 by the author.