This content originally appeared on DEV Community and was authored by ronynn
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!
This content originally appeared on DEV Community and was authored by ronynn