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@v5
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@v5
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!
Top comments (3)
Artifact actions v3 is deprecated and actions using that will fail instantly.
github.blog/changelog/2024-04-16-d...
Thanks for bringing it to my attention. I'll update the post.
I've seen even random google searches about actions leading to my own post even when other people are searching it, so I believe this post has been useful to others apart from myself. :)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.