• 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:

ComponentResponsibility
GitHub repositoryStores source code and the release workflow
GitHub ActionsTests, builds, and signs the APK
GitHub ReleasePublishes the versioned APK as a public asset
ObtainiumDetects 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:

  1. Check out the tagged commit.
  2. Configure JDK 17 and the required Android SDK packages.
  3. Decode the keystore into the temporary runner.
  4. Run unit tests and build the signed release APK.
  5. Give the asset a predictable versioned filename.
  6. 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.