- Sep 22, 2026
- 6 min read
GitHub Releases and Obtainium for Android APK Updates
You can distribute an Android application from a public GitHub repository and receive update notifications through Obtainium without publishing it on Google Play. The important part is not Obtainium itself: every APK must keep the same package ID and Android signing identity across releases.
I used this setup for my native Kotlin DJI Tello controller. Version 0.1.1
was the first permanent installation; Obtainium then detected 0.1.2, Android
installed it in place, and the app displayed its new version on a Pixel 9 Pro.
This article explains the trust model and the release pipeline behind that result.
What each component does
The four moving pieces have different jobs:
| Component | Responsibility |
|---|---|
| GitHub repository | Stores source code and the release workflow |
| GitHub Actions | Tests, builds, and signs the APK |
| GitHub Release | Publishes the versioned APK as a public asset |
| Obtainium | Detects releases and gives the APK to Android’s installer |
Obtainium is a release watcher and installer frontend. It does not sign the application, bypass Android’s package verifier, or need the private signing key.
For a normal public GitHub repository, Obtainium also does not need a GitHub token. It reads public release metadata and downloads the public APK.
Android decides whether an APK is an update
Android accepts a replacement for an installed application only when the new APK has:
- The same package ID
- A compatible, higher version code
- The expected signing certificate
That means the durable identity lives in the signing key, not in the GitHub account or Obtainium entry.
source tag
↓
GitHub Actions → signed APK → GitHub Release
↓
Obtainium
↓
Android verifies package + version + signer
If the signing key changes, Android does not treat the APK as an ordinary update. A new repository owner cannot override that device-side check merely by uploading another file with the same name.
Create the signing identity once
Generate the production keystore outside the Git repository. For a PKCS#12 keystore, a representative command is:
umask 077
mkdir -p "$HOME/.local/share/my-app-signing"
chmod 700 "$HOME/.local/share/my-app-signing"
keytool -genkeypair \
-keystore "$HOME/.local/share/my-app-signing/release.p12" \
-storetype PKCS12 \
-alias release \
-keyalg RSA \
-keysize 4096 \
-validity 10000
chmod 600 "$HOME/.local/share/my-app-signing/release.p12"
Do this once for the application identity. Do not generate a replacement key for every version.
Back up the exact .p12 file and its password before publishing. Keep at least
one encrypted copy in a different failure domain. GitHub Actions secrets are
not backups: their values cannot be retrieved later.
The public certificate fingerprint and the file’s SHA-256 checksum are safe to
record. The .p12, its password, and any Base64 representation of the file are
secret.
Put the signing material into GitHub Actions secrets
The workflow needs the keystore data, passwords, and alias. One common mapping is:
ANDROID_KEYSTORE_BASE64
ANDROID_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS
ANDROID_KEY_PASSWORD
The keystore can be piped directly to GitHub CLI without leaving an encoded copy on disk:
base64 -w 0 "$HOME/.local/share/my-app-signing/release.p12" \
| gh secret set ANDROID_KEYSTORE_BASE64
read -rsp "Keystore password: " RELEASE_PASSWORD
echo
printf '%s' "$RELEASE_PASSWORD" | gh secret set ANDROID_KEYSTORE_PASSWORD
printf '%s' 'release' | gh secret set ANDROID_KEY_ALIAS
printf '%s' "$RELEASE_PASSWORD" | gh secret set ANDROID_KEY_PASSWORD
unset RELEASE_PASSWORD
Base64 is encoding, not encryption. Its purpose here is to move binary content through a text secret; GitHub is responsible for protecting the stored secret.
Build releases from immutable tags
My workflow runs for tags matching android-v*. A release job needs to:
- Check out the tagged commit.
- Configure JDK 17 and the required Android SDK packages.
- Decode the keystore into the temporary runner.
- Run unit tests and build the signed release APK.
- Give the asset a predictable versioned filename.
- Create a GitHub Release and upload the APK.
The signing environment should exist only for the Gradle build step. The
workflow can use its temporary per-run GITHUB_TOKEN to create the release;
there is no need to embed a personal GitHub token in the YAML.
Version codes need special attention. Android compares the integer
versionCode, not a human interpretation of 0.1.2. A CI run number is
convenient only while the workflow’s run-number sequence remains monotonic.
Changing workflow identity or build strategy must not make the next release’s
version code lower than the installed one.
Create and push an annotated tag only from the intended clean commit:
git status --short
git tag -a android-v0.1.3 -m "Android 0.1.3"
git push origin android-v0.1.3
Do not move or reuse a public release tag. If its build fails, fix the cause in a new commit and publish the next version.
Configure Obtainium
For the Tello application, the source is simply:
https://github.com/JAlcocerT/tello-kotlin
An asset filter prevents unrelated release files from being selected:
^tello-native-android-v.*\.apk$
Leave prereleases disabled unless the device is intentionally on a beta channel. Obtainium needs neither the Android keystore nor its password.
Android asks for permission to install unknown applications from Obtainium. That permission lets Obtainium invoke the package installer; it does not disable signature validation. You can revoke the installer permission between updates if background installation is not desired.
Verify what CI published
Do not stop at a green workflow. Download the exact public artifact and inspect it independently:
gh release download android-v0.1.3 \
--repo OWNER/REPOSITORY \
--pattern '*.apk'
APK_PATH="$(find . -maxdepth 1 -type f -name '*.apk' -print -quit)"
apksigner verify --verbose --print-certs "$APK_PATH"
aapt dump badging "$APK_PATH" | head
sha256sum "$APK_PATH"
Confirm the package ID, version name, increasing version code, signer fingerprint, and checksum. Recording these details makes a release reproducible instead of relying on a browser page that merely says “success.”
Prove updates with a low-risk release
An initial installation proves only that one APK can be installed. It does not prove that the update chain works.
For the next Tello release, I added a visible version label and disabled an unneeded Android backup feature. I deliberately left flight, networking, and video behavior unchanged. Obtainium detected the release and Android updated the existing app without an uninstall.
That controlled change verified:
- Repository and release discovery
- Asset filtering
- Package ID continuity
- Signing-certificate continuity
- Increasing version code
- In-place Android installation
For software that controls hardware, separating distribution validation from flight-behavior changes keeps the test easier to interpret and safer to run.
Common failures
Obtainium asks for the signing key
It should not. A public repository URL and optional APK filter are sufficient. The keystore belongs in protected developer/CI custody.
Android says the package conflicts
The installed copy may be signed with a debug or different production key. Before the first permanent release, uninstall the incompatible build. After users install the permanent identity, normal updates must keep that key.
A CI warning looks like the failure
Read the first failed step and its actual error. In this project the first tag
failed because an old Android setup action requested the removed SDK package
named tools; nearby Node.js deprecation messages were only warnings.
The update is older even though its tag looks newer
Inspect versionCode. Android does not order semantic-looking tag strings on
your behalf.
The durable operating rule
GitHub and Obtainium make distribution convenient. The Android signing key makes updates continuous.
Protect the repository account, but separately preserve the .p12 and its
password. If repository access is lost, releases can be moved to another
source. If the permanent self-managed signing key is lost, the already-installed
application normally cannot receive another update under that identity.