ci: add a signed release-APK workflow#5
Conversation
Add .github/workflows/release.yml that builds a signed release APK and attaches it to a GitHub Release on a v* tag (or uploads it as an artifact via manual dispatch). Signing uses the KEYSTORE_BASE64/PASSWORD/ALIAS secrets when present (stable signature for in-place updates), else generates an ephemeral keystore so the build still yields an installable, signed APK. Wire app/build.gradle.kts release buildType to an env-driven signing config (KEYSTORE_FILE/PASSWORD, KEY_ALIAS/PASSWORD); when unset (e.g. local assembleDebug) no signing config is attached. Document the release flow in docs/ANDROID_APP.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbHpkJ1MdVfGz9bi6Ekb1i
There was a problem hiding this comment.
Code Review
This pull request introduces release signing configuration to the Android application build setup, driven by environment variables, and adds comprehensive documentation on how to build and sign release APKs. The review feedback suggests improving the robustness of the Gradle build script by validating all required signing environment variables and resolving the keystore path relative to the root project. Additionally, it recommends using a portable base64 command in the documentation to ensure compatibility across both Linux and macOS environments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| val keystoreFile = System.getenv("KEYSTORE_FILE") | ||
| signingConfigs { | ||
| create("release") { | ||
| if (keystoreFile != null) { | ||
| storeFile = file(keystoreFile) | ||
| storePassword = System.getenv("KEYSTORE_PASSWORD") | ||
| keyAlias = System.getenv("KEY_ALIAS") | ||
| keyPassword = System.getenv("KEY_PASSWORD") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| buildTypes { | ||
| release { | ||
| isMinifyEnabled = false | ||
| proguardFiles( | ||
| getDefaultProguardFile("proguard-android-optimize.txt"), | ||
| "proguard-rules.pro", | ||
| ) | ||
| if (keystoreFile != null) { | ||
| signingConfig = signingConfigs.getByName("release") | ||
| } |
There was a problem hiding this comment.
Using file(keystoreFile) resolves relative paths relative to the subproject directory (app/), which can cause build failures if the keystore is placed in the root directory (a common pattern in CI workflows and local builds). Additionally, checking only keystoreFile != null can lead to cryptic build failures if other required environment variables (like KEYSTORE_PASSWORD) are missing or empty.
We can make this robust by:
- Validating that all required environment variables are present and non-empty.
- Resolving the keystore path relative to the root project using
rootProject.file(...)so both relative and absolute paths work seamlessly. - Conditionally creating and assigning the
releasesigning configuration only when all signing environment variables are fully configured.
val envKeystoreFile = System.getenv("KEYSTORE_FILE")
val envKeystorePassword = System.getenv("KEYSTORE_PASSWORD")
val envKeyAlias = System.getenv("KEY_ALIAS")
val envKeyPassword = System.getenv("KEY_PASSWORD")
val hasSigningEnv = !envKeystoreFile.isNullOrEmpty() &&
!envKeystorePassword.isNullOrEmpty() &&
!envKeyAlias.isNullOrEmpty() &&
!envKeyPassword.isNullOrEmpty()
if (hasSigningEnv) {
signingConfigs {
create("release") {
storeFile = rootProject.file(envKeystoreFile)
storePassword = envKeystorePassword
keyAlias = envKeyAlias
keyPassword = envKeyPassword
}
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
if (hasSigningEnv) {
signingConfig = signingConfigs.getByName("release")
}| ```bash | ||
| keytool -genkeypair -v -keystore release.jks -alias iotracer \ | ||
| -keyalg RSA -keysize 2048 -validity 10000 | ||
| base64 -w0 release.jks # paste into the KEYSTORE_BASE64 repo secret |
There was a problem hiding this comment.
The -w0 flag is specific to GNU base64 (Linux) and will fail on macOS with base64: invalid option -- w.
Using base64 release.jks | tr -d '\n' is fully portable and works seamlessly on both Linux and macOS to produce a single-line base64 string.
| base64 -w0 release.jks # paste into the KEYSTORE_BASE64 repo secret | |
| base64 release.jks | tr -d '\n' # paste into the KEYSTORE_BASE64 repo secret |
Address PR #5 review: - Gate release signing on all four env vars being present and non-empty (isNullOrEmpty, not just != null), and resolve the keystore path with rootProject.file so relative paths work from the repo root, not app/. - Keep a loud failure when KEYSTORE_FILE is set but the rest are missing, so a half-configured keystore errors instead of silently shipping an unsigned release. - Docs: use `base64 release.jks | tr -d '\n'` (portable; macOS base64 has no -w0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbHpkJ1MdVfGz9bi6Ekb1i
Summary
Adds a CI pipeline that builds a signed release APK and publishes it, so a release artifact can be produced on demand. (The APK is built by CI — it needs the Android SDK, which isn't in the dev sandbox.)
Changes
.github/workflows/release.yml— builds:app:assembleRelease, signs it, and:v*tag push → publishes a GitHub Release with the APK attached;app/build.gradle.kts— releasebuildTypenow uses an env-driven signing config (KEYSTORE_FILE/KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORD). When those are unset (e.g. a localassembleDebug), no signing config is attached, so debug builds are unaffected.docs/ANDROID_APP.md— a "Releases" section documenting the tag/dispatch flow, keystore-secret setup, and a local signed-build command.Signing strategy
KEYSTORE_BASE64+KEYSTORE_PASSWORD+KEY_ALIAS+KEY_PASSWORDsecrets → signs with that keystore (stable signature; updates install in place).Notes
isMinifyEnabled = false); only signing differs. The app still requires root at runtime.Android CIwill run on this PR (build.gradle changed) and validates that the signing-config change doesn't break the debug build.Producing the APK
After merge, push a tag (e.g.
git tag v0.1.0 && git push origin v0.1.0) to generate the signed APK as a GitHub Release — I can do that step on your go-ahead.🤖 Generated with Claude Code
https://claude.ai/code/session_01VbHpkJ1MdVfGz9bi6Ekb1i
Generated by Claude Code