From d79eb67964bbfea7b4823d662254a84e342c0ad9 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:11:50 -0400 Subject: [PATCH 1/4] chore(signing): inject release keystore from CI secret; skip signing when absent The inherited release.keystore was removed from the repo (gitignored). Wire CI to recreate it from a new KEYSTORE_B64 secret (decoded to release.keystore before assembleOssRelease in build.yml/release.yml), and make Helpers.kt only enable release signing when the keystore file exists AND a non-blank password is set, so debug / PR / keyless builds skip signing instead of failing on the missing file. Signing verified end-to-end locally with a freshly generated 4096-bit RSA key (CN=NekoBox, OU=hawkff): assembleOssRelease produces an APK signed with our key (apksigner cert SHA-256 matches the keystore). GitHub secrets KEYSTORE_B64/ KEYSTORE_PASS/ALIAS_NAME/ALIAS_PASS set on the repo. --- .github/workflows/build.yml | 4 ++++ .github/workflows/release.yml | 4 ++++ buildSrc/src/main/kotlin/Helpers.kt | 11 +++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a3e3a28b..f1124110b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -128,6 +128,10 @@ jobs: echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" ./run init action gradle + # release.keystore is not committed; decode it from the KEYSTORE_B64 secret. + if [ -n "${{ secrets.KEYSTORE_B64 }}" ]; then + echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore + fi KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS}}" ./gradlew app:assembleOssRelease APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') APK=$(dirname $APK) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db3a97b57..20f9308d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,6 +130,10 @@ jobs: echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" ./run init action gradle + # release.keystore is not committed; decode it from the KEYSTORE_B64 secret. + if [ -n "${{ secrets.KEYSTORE_B64 }}" ]; then + echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore + fi KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS}}" ./gradlew app:assembleOssRelease APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') APK=$(dirname $APK) diff --git a/buildSrc/src/main/kotlin/Helpers.kt b/buildSrc/src/main/kotlin/Helpers.kt index 3561cdd1e..ef8ad16c0 100644 --- a/buildSrc/src/main/kotlin/Helpers.kt +++ b/buildSrc/src/main/kotlin/Helpers.kt @@ -118,12 +118,19 @@ fun Project.setupAppCommon() { val keystorePwd = lp.getProperty("KEYSTORE_PASS") ?: System.getenv("KEYSTORE_PASS") val alias = lp.getProperty("ALIAS_NAME") ?: System.getenv("ALIAS_NAME") val pwd = lp.getProperty("ALIAS_PASS") ?: System.getenv("ALIAS_PASS") + // release.keystore is intentionally NOT committed (gitignored). CI decodes it from + // the KEYSTORE_B64 secret before the release build. Only wire up release signing when + // the keystore file is actually present and a (non-blank) password is provided; + // otherwise skip it so debug / PR / keyless builds don't fail. Empty-string env vars + // (unset GitHub secrets expand to "") count as absent. + val releaseKeystore = rootProject.file("release.keystore") + val canSign = !keystorePwd.isNullOrBlank() && releaseKeystore.exists() android.apply { - if (keystorePwd != null) { + if (canSign) { signingConfigs { create("release") { - storeFile = rootProject.file("release.keystore") + storeFile = releaseKeystore storePassword = keystorePwd keyAlias = alias keyPassword = pwd From 6fdfc82a6149602ef12076cdeaefd0c45bfb60c9 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:12:20 -0400 Subject: [PATCH 2/4] chore: gitignore buildSrc/build (symlink not matched by build/ rule) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3debf396c..44eaae38a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ __MACOSX/ .gradle/ .kotlin/ build/ +# buildSrc/build is sometimes a symlink to APFS /tmp (exFAT AppleDouble workaround); +# the trailing-slash 'build/' rule above doesn't match a symlink, so ignore it explicitly. +buildSrc/build # Android / native build outputs /captures/ From 08cbf7bdda4d2988a3b1045879b864a31077fbf7 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:19:31 -0400 Subject: [PATCH 3/4] review: gate release signing on all signing inputs (alias + key password) Per Greptile: canSign only checked the keystore file + store password, so if ALIAS_NAME/ALIAS_PASS were absent while the others were present, Gradle would build a signing config with null alias/password and fail at packaging with an opaque error. Require all of keystore-exists + storePass + alias + keyPass non-blank before enabling release signing; otherwise skip cleanly. --- buildSrc/src/main/kotlin/Helpers.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Helpers.kt b/buildSrc/src/main/kotlin/Helpers.kt index ef8ad16c0..0a758e6a8 100644 --- a/buildSrc/src/main/kotlin/Helpers.kt +++ b/buildSrc/src/main/kotlin/Helpers.kt @@ -124,7 +124,10 @@ fun Project.setupAppCommon() { // otherwise skip it so debug / PR / keyless builds don't fail. Empty-string env vars // (unset GitHub secrets expand to "") count as absent. val releaseKeystore = rootProject.file("release.keystore") - val canSign = !keystorePwd.isNullOrBlank() && releaseKeystore.exists() + val canSign = releaseKeystore.exists() && + !keystorePwd.isNullOrBlank() && + !alias.isNullOrBlank() && + !pwd.isNullOrBlank() android.apply { if (canSign) { From f8d3781587f55ab6ebfed3572aef41f3986a41b0 Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:23:49 -0400 Subject: [PATCH 4/4] chore(ci): normalize spacing in ALIAS_PASS secret expansion --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1124110b..e9629caa3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,7 +132,7 @@ jobs: if [ -n "${{ secrets.KEYSTORE_B64 }}" ]; then echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore fi - KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS}}" ./gradlew app:assembleOssRelease + KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS }}" ./gradlew app:assembleOssRelease APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') APK=$(dirname $APK) echo "APK=$APK" >> $GITHUB_ENV diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20f9308d7..4b4c344a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,7 +134,7 @@ jobs: if [ -n "${{ secrets.KEYSTORE_B64 }}" ]; then echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore fi - KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS}}" ./gradlew app:assembleOssRelease + KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS }}" ./gradlew app:assembleOssRelease APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') APK=$(dirname $APK) echo "APK=$APK" >> $GITHUB_ENV