Automating Android APK Builds with GitHub Actions (The Sane Way)

Manually building and signing an APK every time you make a change? Nah, that’s a waste of time. The right way to do it is to automate the entire process using GitHub Actions. That way, every push or tag automatically triggers a clean, reproducible build. Here’s how to set up GitHub Actions to build your Android APK (the sane way). 1. Create a .github/workflows/build.yml File Inside your repo, make this directory and file: .github/workflows/build.yml Now, paste this inside: name: Build Android APK on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: name: Build APK runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17' - name: Setup Gradle cache uses: gradle/gradle-build-action@v3 - name: Build debug APK run: ./gradlew assembleDebug - name: Upload APK uses: actions/upload-artifact@v3 with: name: Karui-APK path: app/build/outputs/apk/debug/app-debug.apk This workflow: ✅ Runs on Ubuntu (clean environment) ✅ Uses Java 17 (modify if needed) ✅ Caches Gradle for faster builds ✅ Builds a debug APK ✅ Uploads the APK as an artifact (so you can download it) 2. Adding Signing for a Release APK If you want to distribute a signed APK (for Play Store or F-Droid), modify the workflow: Step 1: Store Your Keystore Securely Convert your keystore to Base64 (so GitHub can store it as a secret): base64 -w 0 my-release-key.jks > keystore.b64 Copy the contents of keystore.b64. Go to GitHub → Your Repo → Settings → Secrets and variables → Actions → New Repository Secret Add a new secret: Name: ANDROID_KEYSTORE Value: Paste the Base64 string Step 2: Add Keystore Passwords as Secrets Also add these secrets: KEYSTORE_PASSWORD – Your keystore password KEY_ALIAS – The alias of your key KEY_PASSWORD – Your key’s password Step 3: Modify build.yml to Sign the APK Now update the workflow: - name: Decode keystore run: echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > my-release-key.jks - name: Build release APK run: ./gradlew assembleRelease - name: Sign APK run: | jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ -keystore my-release-key.jks -storepass ${{ secrets.KEYSTORE_PASSWORD }} \ -keypass ${{ secrets.KEY_PASSWORD }} \ app/build/outputs/apk/release/app-release-unsigned.apk ${{ secrets.KEY_ALIAS }} - name: Verify signature run: jarsigner -verify -verbose -certs app/build/outputs/apk/release/app-release-unsigned.apk - name: Align APK run: | $ANDROID_HOME/build-tools/34.0.0/zipalign -v 4 \ app/build/outputs/apk/release/app-release-unsigned.apk \ app/build/outputs/apk/release/app-release.apk - name: Upload Signed APK uses: actions/upload-artifact@v3 with: name: Karui-Signed-APK path: app/build/outputs/apk/release/app-release.apk This does: ✅ Decodes the keystore ✅ Builds a release APK ✅ Signs it using jarsigner ✅ Verifies the signature ✅ Aligns it (important for efficiency) ✅ Uploads the signed APK 3. Results Now, every push to main will: Build a debug APK (for testing) Build & sign a release APK (ready for distribution) No more "works on my machine" nonsense. Every build is clean and reproducible. Check Out Karui I built Karui using this exact setup. 84KB, Unix-inspired, and built the right way—minimal, efficient, and reproducible. If you’re into simple, lightweight apps, give it a look. Maybe even steal my GitHub Actions workflow for your own projects!

Mar 30, 2025 - 22:16
 0
Automating Android APK Builds with GitHub Actions (The Sane Way)

Manually building and signing an APK every time you make a change? Nah, that’s a waste of time. The right way to do it is to automate the entire process using GitHub Actions. That way, every push or tag automatically triggers a clean, reproducible build.

Here’s how to set up GitHub Actions to build your Android APK (the sane way).

1. Create a .github/workflows/build.yml File

Inside your repo, make this directory and file:

.github/workflows/build.yml

Now, paste this inside:

name: Build Android APK

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

jobs:
  build:
    name: Build APK
    runs-on: ubuntu-latest

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

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

      - name: Setup Gradle cache
        uses: gradle/gradle-build-action@v3

      - name: Build debug APK
        run: ./gradlew assembleDebug

      - name: Upload APK
        uses: actions/upload-artifact@v3
        with:
          name: Karui-APK
          path: app/build/outputs/apk/debug/app-debug.apk

This workflow:
✅ Runs on Ubuntu (clean environment)
✅ Uses Java 17 (modify if needed)
✅ Caches Gradle for faster builds
✅ Builds a debug APK
✅ Uploads the APK as an artifact (so you can download it)

2. Adding Signing for a Release APK

If you want to distribute a signed APK (for Play Store or F-Droid), modify the workflow:

Step 1: Store Your Keystore Securely

  1. Convert your keystore to Base64 (so GitHub can store it as a secret):

base64 -w 0 my-release-key.jks > keystore.b64

  1. Copy the contents of keystore.b64.

  2. Go to GitHub → Your Repo → Settings → Secrets and variables → Actions → New Repository Secret

  3. Add a new secret:

Name: ANDROID_KEYSTORE

Value: Paste the Base64 string

Step 2: Add Keystore Passwords as Secrets

Also add these secrets:

KEYSTORE_PASSWORD – Your keystore password

KEY_ALIAS – The alias of your key

KEY_PASSWORD – Your key’s password

Step 3: Modify build.yml to Sign the APK

Now update the workflow:

- name: Decode keystore
        run: echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > my-release-key.jks

      - name: Build release APK
        run: ./gradlew assembleRelease

      - name: Sign APK
        run: |
          jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
            -keystore my-release-key.jks -storepass ${{ secrets.KEYSTORE_PASSWORD }} \
            -keypass ${{ secrets.KEY_PASSWORD }} \
            app/build/outputs/apk/release/app-release-unsigned.apk ${{ secrets.KEY_ALIAS }}

      - name: Verify signature
        run: jarsigner -verify -verbose -certs app/build/outputs/apk/release/app-release-unsigned.apk

      - name: Align APK
        run: |
          $ANDROID_HOME/build-tools/34.0.0/zipalign -v 4 \
            app/build/outputs/apk/release/app-release-unsigned.apk \
            app/build/outputs/apk/release/app-release.apk

      - name: Upload Signed APK
        uses: actions/upload-artifact@v3
        with:
          name: Karui-Signed-APK
          path: app/build/outputs/apk/release/app-release.apk

This does:
✅ Decodes the keystore
✅ Builds a release APK
✅ Signs it using jarsigner
✅ Verifies the signature
✅ Aligns it (important for efficiency)
✅ Uploads the signed APK

3. Results

Now, every push to main will:

Build a debug APK (for testing)

Build & sign a release APK (ready for distribution)

No more "works on my machine" nonsense. Every build is clean and reproducible.

Check Out Karui

I built Karui using this exact setup. 84KB, Unix-inspired, and built the right way—minimal, efficient, and reproducible.

If you’re into simple, lightweight apps, give it a look. Maybe even steal my GitHub Actions workflow for your own projects!