From fbd7d8d85e572c4e2f8ae7c10a30b0e3cd8fdd2e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:55:44 +0800 Subject: [PATCH 01/52] Add multi-ABI support with split APK configuration for Android builds (#12) * Initial plan * Update Android build flow to use kqt-kotlin package Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> * Update Android build flow to use new android directory at root Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> * Re-enable Android build workflow with new /android directory Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> * Add multi-ABI support and split APK configuration - Add armeabi-v7a target alongside arm64-v8a - Build native libraries for both ABIs separately - Generate uniffi bindings once using arm64-v8a library - Configure gradle for split APKs per ABI and universal APK - Update artifact names to reflect multiple APKs Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ecc93cb..f3a134d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,6 +15,9 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + ndk { + abiFilters += listOf("arm64-v8a", "armeabi-v7a") + } } buildTypes { @@ -26,6 +29,16 @@ android { ) } } + + splits { + abi { + isEnable = true + reset() + include("arm64-v8a", "armeabi-v7a") + isUniversalApk = true + } + } + compileOptions { sourceCompatibility = org.gradle.api.JavaVersion.VERSION_17 targetCompatibility = org.gradle.api.JavaVersion.VERSION_17 From 099cb7af49bdd67f2ef72f74bfdde285eb35c699 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:41:27 +0000 Subject: [PATCH 02/52] Initial plan From 33be049bbd6e4eb804c3d0546b7073385e0dcfaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:43:17 +0000 Subject: [PATCH 03/52] Add APK signing configuration to Android build workflow Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 16 +++++ android/SIGNING.md | 92 +++++++++++++++++++++++++++++ android/app/.gitignore | 4 +- android/app/build.gradle.kts | 10 ++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 android/SIGNING.md diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 76055de..646c9dc 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -75,8 +75,24 @@ jobs: working-directory: ./android run: ./gradlew assembleDebug --stacktrace + - name: Decode keystore + if: github.event_name != 'pull_request' + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + run: | + if [ -n "$KEYSTORE_BASE64" ]; then + echo "$KEYSTORE_BASE64" | base64 -d > /home/runner/work/kqt/kqt/android/app/release.keystore + else + echo "Warning: KEYSTORE_BASE64 secret not found. Skipping signing." + fi + - name: Build release APK working-directory: ./android + env: + KEYSTORE_FILE: /home/runner/work/kqt/kqt/android/app/release.keystore + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: ./gradlew assembleRelease --stacktrace - name: Upload debug APKs diff --git a/android/SIGNING.md b/android/SIGNING.md new file mode 100644 index 0000000..7289884 --- /dev/null +++ b/android/SIGNING.md @@ -0,0 +1,92 @@ +# Android APK Signing Configuration + +## Overview + +This document describes how to configure APK signing for the Android build workflow. + +## GitHub Secrets Setup + +To enable APK signing in GitHub Actions, you need to configure the following secrets in your repository: + +### Required Secrets + +1. **KEYSTORE_BASE64**: Base64-encoded keystore file +2. **KEYSTORE_PASSWORD**: Password for the keystore +3. **KEY_ALIAS**: Alias of the key in the keystore +4. **KEY_PASSWORD**: Password for the key + +### Creating a Keystore + +If you don't have a keystore yet, create one using the following command: + +```bash +keytool -genkey -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 +``` + +Follow the prompts to set passwords and fill in the certificate information. + +### Encoding the Keystore + +To encode your keystore file to base64: + +```bash +base64 -i release.keystore -o keystore.txt +``` + +On some systems (like macOS), you might need to use: + +```bash +base64 -i release.keystore +``` + +Copy the entire output (it will be a long string). + +### Adding Secrets to GitHub + +1. Go to your repository on GitHub +2. Navigate to **Settings** → **Secrets and variables** → **Actions** +3. Click **New repository secret** +4. Add each of the four secrets: + - `KEYSTORE_BASE64`: Paste the base64-encoded keystore + - `KEYSTORE_PASSWORD`: Enter your keystore password + - `KEY_ALIAS`: Enter your key alias + - `KEY_PASSWORD`: Enter your key password + +## Workflow Behavior + +- **Pull Requests**: APKs are built but not signed (to protect secrets from untrusted code) +- **Push to main/master**: APKs are built and signed if secrets are configured +- **Manual dispatch**: APKs are built and signed if secrets are configured + +If the `KEYSTORE_BASE64` secret is not found, the workflow will log a warning and continue without signing. + +## Local Development + +For local builds, you can set environment variables: + +```bash +export KEYSTORE_FILE=/path/to/your/release.keystore +export KEYSTORE_PASSWORD=your-keystore-password +export KEY_ALIAS=your-key-alias +export KEY_PASSWORD=your-key-password + +cd android +./gradlew assembleRelease +``` + +Alternatively, you can create a `keystore.properties` file (not recommended as it's easy to commit accidentally): + +```properties +storeFile=/path/to/your/release.keystore +storePassword=your-keystore-password +keyAlias=your-key-alias +keyPassword=your-key-password +``` + +**Important**: Never commit your keystore or keystore passwords to version control! + +## Security Notes + +- The keystore file is automatically excluded from git via `.gitignore` +- Secrets are only available to trusted workflows (not pull requests from forks) +- Keep your keystore and passwords secure - losing them means you cannot update your app on the Play Store diff --git a/android/app/.gitignore b/android/app/.gitignore index 42afabf..724b1e0 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +*.keystore +*.jks \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index f3a134d..25eb6b4 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -20,6 +20,15 @@ android { } } + signingConfigs { + create("release") { + storeFile = file(System.getenv("KEYSTORE_FILE") ?: "release.keystore") + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") + } + } + buildTypes { release { isMinifyEnabled = false @@ -27,6 +36,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } } From 049895552ea70669e5255a76c63fd6aeac9a69ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:43:17 +0000 Subject: [PATCH 04/52] Add APK signing configuration to Android build workflow Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- SIGNING.md | 92 ++++++++++++++++++++++++++++++++++++++++++++ app/.gitignore | 4 +- app/build.gradle.kts | 10 +++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 SIGNING.md diff --git a/SIGNING.md b/SIGNING.md new file mode 100644 index 0000000..7289884 --- /dev/null +++ b/SIGNING.md @@ -0,0 +1,92 @@ +# Android APK Signing Configuration + +## Overview + +This document describes how to configure APK signing for the Android build workflow. + +## GitHub Secrets Setup + +To enable APK signing in GitHub Actions, you need to configure the following secrets in your repository: + +### Required Secrets + +1. **KEYSTORE_BASE64**: Base64-encoded keystore file +2. **KEYSTORE_PASSWORD**: Password for the keystore +3. **KEY_ALIAS**: Alias of the key in the keystore +4. **KEY_PASSWORD**: Password for the key + +### Creating a Keystore + +If you don't have a keystore yet, create one using the following command: + +```bash +keytool -genkey -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 +``` + +Follow the prompts to set passwords and fill in the certificate information. + +### Encoding the Keystore + +To encode your keystore file to base64: + +```bash +base64 -i release.keystore -o keystore.txt +``` + +On some systems (like macOS), you might need to use: + +```bash +base64 -i release.keystore +``` + +Copy the entire output (it will be a long string). + +### Adding Secrets to GitHub + +1. Go to your repository on GitHub +2. Navigate to **Settings** → **Secrets and variables** → **Actions** +3. Click **New repository secret** +4. Add each of the four secrets: + - `KEYSTORE_BASE64`: Paste the base64-encoded keystore + - `KEYSTORE_PASSWORD`: Enter your keystore password + - `KEY_ALIAS`: Enter your key alias + - `KEY_PASSWORD`: Enter your key password + +## Workflow Behavior + +- **Pull Requests**: APKs are built but not signed (to protect secrets from untrusted code) +- **Push to main/master**: APKs are built and signed if secrets are configured +- **Manual dispatch**: APKs are built and signed if secrets are configured + +If the `KEYSTORE_BASE64` secret is not found, the workflow will log a warning and continue without signing. + +## Local Development + +For local builds, you can set environment variables: + +```bash +export KEYSTORE_FILE=/path/to/your/release.keystore +export KEYSTORE_PASSWORD=your-keystore-password +export KEY_ALIAS=your-key-alias +export KEY_PASSWORD=your-key-password + +cd android +./gradlew assembleRelease +``` + +Alternatively, you can create a `keystore.properties` file (not recommended as it's easy to commit accidentally): + +```properties +storeFile=/path/to/your/release.keystore +storePassword=your-keystore-password +keyAlias=your-key-alias +keyPassword=your-key-password +``` + +**Important**: Never commit your keystore or keystore passwords to version control! + +## Security Notes + +- The keystore file is automatically excluded from git via `.gitignore` +- Secrets are only available to trusted workflows (not pull requests from forks) +- Keep your keystore and passwords secure - losing them means you cannot update your app on the Play Store diff --git a/app/.gitignore b/app/.gitignore index 42afabf..724b1e0 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +*.keystore +*.jks \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3a134d..25eb6b4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,6 +20,15 @@ android { } } + signingConfigs { + create("release") { + storeFile = file(System.getenv("KEYSTORE_FILE") ?: "release.keystore") + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") + } + } + buildTypes { release { isMinifyEnabled = false @@ -27,6 +36,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } } From 8b26f3e7b5c9b2d6057f17f7100303e52226e164 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:44:09 +0000 Subject: [PATCH 05/52] Address code review feedback: use GITHUB_WORKSPACE and improve security Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 6 +++--- android/app/build.gradle.kts | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 646c9dc..9312802 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -76,12 +76,12 @@ jobs: run: ./gradlew assembleDebug --stacktrace - name: Decode keystore - if: github.event_name != 'pull_request' + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' env: KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} run: | if [ -n "$KEYSTORE_BASE64" ]; then - echo "$KEYSTORE_BASE64" | base64 -d > /home/runner/work/kqt/kqt/android/app/release.keystore + echo "$KEYSTORE_BASE64" | base64 -d > $GITHUB_WORKSPACE/android/app/release.keystore else echo "Warning: KEYSTORE_BASE64 secret not found. Skipping signing." fi @@ -89,7 +89,7 @@ jobs: - name: Build release APK working-directory: ./android env: - KEYSTORE_FILE: /home/runner/work/kqt/kqt/android/app/release.keystore + KEYSTORE_FILE: ${{ github.workspace }}/android/app/release.keystore KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 25eb6b4..4674faf 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,10 +22,23 @@ android { signingConfigs { create("release") { - storeFile = file(System.getenv("KEYSTORE_FILE") ?: "release.keystore") - storePassword = System.getenv("KEYSTORE_PASSWORD") - keyAlias = System.getenv("KEY_ALIAS") - keyPassword = System.getenv("KEY_PASSWORD") + val keystoreFile = System.getenv("KEYSTORE_FILE") + val keystorePass = System.getenv("KEYSTORE_PASSWORD") + val alias = System.getenv("KEY_ALIAS") + val keyPass = System.getenv("KEY_PASSWORD") + + if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { + storeFile = file(keystoreFile) + storePassword = keystorePass + keyAlias = alias + keyPassword = keyPass + } else { + // Fallback for local builds without signing + storeFile = file("release.keystore") + storePassword = "" + keyAlias = "" + keyPassword = "" + } } } From d6a153d9beb70de1d6b0262d5c6d2c34bfd93936 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:44:09 +0000 Subject: [PATCH 06/52] Address code review feedback: use GITHUB_WORKSPACE and improve security Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 25eb6b4..4674faf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,10 +22,23 @@ android { signingConfigs { create("release") { - storeFile = file(System.getenv("KEYSTORE_FILE") ?: "release.keystore") - storePassword = System.getenv("KEYSTORE_PASSWORD") - keyAlias = System.getenv("KEY_ALIAS") - keyPassword = System.getenv("KEY_PASSWORD") + val keystoreFile = System.getenv("KEYSTORE_FILE") + val keystorePass = System.getenv("KEYSTORE_PASSWORD") + val alias = System.getenv("KEY_ALIAS") + val keyPass = System.getenv("KEY_PASSWORD") + + if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { + storeFile = file(keystoreFile) + storePassword = keystorePass + keyAlias = alias + keyPassword = keyPass + } else { + // Fallback for local builds without signing + storeFile = file("release.keystore") + storePassword = "" + keyAlias = "" + keyPassword = "" + } } } From a0628f2f248824de892c2b69b52a15e59aa1f560 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:45:03 +0000 Subject: [PATCH 07/52] Fix signing configuration to gracefully handle missing credentials Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 2 +- android/app/build.gradle.kts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 9312802..c2d2770 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -81,7 +81,7 @@ jobs: KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} run: | if [ -n "$KEYSTORE_BASE64" ]; then - echo "$KEYSTORE_BASE64" | base64 -d > $GITHUB_WORKSPACE/android/app/release.keystore + echo "$KEYSTORE_BASE64" | base64 -d > ${{ github.workspace }}/android/app/release.keystore else echo "Warning: KEYSTORE_BASE64 secret not found. Skipping signing." fi diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4674faf..a36a9bc 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -32,13 +32,9 @@ android { storePassword = keystorePass keyAlias = alias keyPassword = keyPass - } else { - // Fallback for local builds without signing - storeFile = file("release.keystore") - storePassword = "" - keyAlias = "" - keyPassword = "" } + // If environment variables are not set, signing will be skipped + // This allows the build to complete without signing credentials } } @@ -49,7 +45,15 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - signingConfig = signingConfigs.getByName("release") + // Only apply signing config if all credentials are available + val keystoreFile = System.getenv("KEYSTORE_FILE") + val keystorePass = System.getenv("KEYSTORE_PASSWORD") + val alias = System.getenv("KEY_ALIAS") + val keyPass = System.getenv("KEY_PASSWORD") + + if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { + signingConfig = signingConfigs.getByName("release") + } } } From 2ee9136363c947ac3b90b8eeecba2092b1618fd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:45:03 +0000 Subject: [PATCH 08/52] Fix signing configuration to gracefully handle missing credentials Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4674faf..a36a9bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,13 +32,9 @@ android { storePassword = keystorePass keyAlias = alias keyPassword = keyPass - } else { - // Fallback for local builds without signing - storeFile = file("release.keystore") - storePassword = "" - keyAlias = "" - keyPassword = "" } + // If environment variables are not set, signing will be skipped + // This allows the build to complete without signing credentials } } @@ -49,7 +45,15 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - signingConfig = signingConfigs.getByName("release") + // Only apply signing config if all credentials are available + val keystoreFile = System.getenv("KEYSTORE_FILE") + val keystorePass = System.getenv("KEYSTORE_PASSWORD") + val alias = System.getenv("KEY_ALIAS") + val keyPass = System.getenv("KEY_PASSWORD") + + if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { + signingConfig = signingConfigs.getByName("release") + } } } From e851273dec1f3fa8b4f6bf885943a646acd530e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:45:53 +0000 Subject: [PATCH 09/52] Refactor signing configuration to avoid code duplication Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- android/app/build.gradle.kts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a36a9bc..19d7c08 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -20,18 +20,23 @@ android { } } + // Helper function to check if signing credentials are available + val hasSigningCredentials = { + val keystoreFile = System.getenv("KEYSTORE_FILE") + val keystorePass = System.getenv("KEYSTORE_PASSWORD") + val alias = System.getenv("KEY_ALIAS") + val keyPass = System.getenv("KEY_PASSWORD") + keystoreFile != null && keystorePass != null && alias != null && keyPass != null + } + signingConfigs { create("release") { - val keystoreFile = System.getenv("KEYSTORE_FILE") - val keystorePass = System.getenv("KEYSTORE_PASSWORD") - val alias = System.getenv("KEY_ALIAS") - val keyPass = System.getenv("KEY_PASSWORD") - - if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { - storeFile = file(keystoreFile) - storePassword = keystorePass - keyAlias = alias - keyPassword = keyPass + if (hasSigningCredentials()) { + val keystoreFilePath = System.getenv("KEYSTORE_FILE")!! + storeFile = file(keystoreFilePath) + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") } // If environment variables are not set, signing will be skipped // This allows the build to complete without signing credentials @@ -46,12 +51,7 @@ android { "proguard-rules.pro" ) // Only apply signing config if all credentials are available - val keystoreFile = System.getenv("KEYSTORE_FILE") - val keystorePass = System.getenv("KEYSTORE_PASSWORD") - val alias = System.getenv("KEY_ALIAS") - val keyPass = System.getenv("KEY_PASSWORD") - - if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { + if (hasSigningCredentials()) { signingConfig = signingConfigs.getByName("release") } } From 7a0e012e3ebb38d0e61e0118fed09cdc7b10340c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:45:53 +0000 Subject: [PATCH 10/52] Refactor signing configuration to avoid code duplication Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a36a9bc..19d7c08 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,18 +20,23 @@ android { } } + // Helper function to check if signing credentials are available + val hasSigningCredentials = { + val keystoreFile = System.getenv("KEYSTORE_FILE") + val keystorePass = System.getenv("KEYSTORE_PASSWORD") + val alias = System.getenv("KEY_ALIAS") + val keyPass = System.getenv("KEY_PASSWORD") + keystoreFile != null && keystorePass != null && alias != null && keyPass != null + } + signingConfigs { create("release") { - val keystoreFile = System.getenv("KEYSTORE_FILE") - val keystorePass = System.getenv("KEYSTORE_PASSWORD") - val alias = System.getenv("KEY_ALIAS") - val keyPass = System.getenv("KEY_PASSWORD") - - if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { - storeFile = file(keystoreFile) - storePassword = keystorePass - keyAlias = alias - keyPassword = keyPass + if (hasSigningCredentials()) { + val keystoreFilePath = System.getenv("KEYSTORE_FILE")!! + storeFile = file(keystoreFilePath) + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") } // If environment variables are not set, signing will be skipped // This allows the build to complete without signing credentials @@ -46,12 +51,7 @@ android { "proguard-rules.pro" ) // Only apply signing config if all credentials are available - val keystoreFile = System.getenv("KEYSTORE_FILE") - val keystorePass = System.getenv("KEYSTORE_PASSWORD") - val alias = System.getenv("KEY_ALIAS") - val keyPass = System.getenv("KEY_PASSWORD") - - if (keystoreFile != null && keystorePass != null && alias != null && keyPass != null) { + if (hasSigningCredentials()) { signingConfig = signingConfigs.getByName("release") } } From 1fe82217954f0adea8611c68a9b75847f01df7b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:46:38 +0000 Subject: [PATCH 11/52] Fix documentation: use genkeypair instead of deprecated genkey Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- android/SIGNING.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/android/SIGNING.md b/android/SIGNING.md index 7289884..b9f4202 100644 --- a/android/SIGNING.md +++ b/android/SIGNING.md @@ -20,7 +20,7 @@ To enable APK signing in GitHub Actions, you need to configure the following sec If you don't have a keystore yet, create one using the following command: ```bash -keytool -genkey -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 +keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 ``` Follow the prompts to set passwords and fill in the certificate information. @@ -74,15 +74,6 @@ cd android ./gradlew assembleRelease ``` -Alternatively, you can create a `keystore.properties` file (not recommended as it's easy to commit accidentally): - -```properties -storeFile=/path/to/your/release.keystore -storePassword=your-keystore-password -keyAlias=your-key-alias -keyPassword=your-key-password -``` - **Important**: Never commit your keystore or keystore passwords to version control! ## Security Notes From e7735cb8438844003e540a57e5ecbdc7ad067603 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:46:38 +0000 Subject: [PATCH 12/52] Fix documentation: use genkeypair instead of deprecated genkey Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- SIGNING.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/SIGNING.md b/SIGNING.md index 7289884..b9f4202 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -20,7 +20,7 @@ To enable APK signing in GitHub Actions, you need to configure the following sec If you don't have a keystore yet, create one using the following command: ```bash -keytool -genkey -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 +keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 ``` Follow the prompts to set passwords and fill in the certificate information. @@ -74,15 +74,6 @@ cd android ./gradlew assembleRelease ``` -Alternatively, you can create a `keystore.properties` file (not recommended as it's easy to commit accidentally): - -```properties -storeFile=/path/to/your/release.keystore -storePassword=your-keystore-password -keyAlias=your-key-alias -keyPassword=your-key-password -``` - **Important**: Never commit your keystore or keystore passwords to version control! ## Security Notes From c6c4808ced2f6ba6abd1ba3819472d9e6ee2b4e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:47:25 +0000 Subject: [PATCH 13/52] Add keystore cleanup step to prevent sensitive data leakage Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index c2d2770..8bf0459 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -95,6 +95,10 @@ jobs: KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: ./gradlew assembleRelease --stacktrace + - name: Clean up keystore + if: always() + run: rm -f ${{ github.workspace }}/android/app/release.keystore + - name: Upload debug APKs uses: actions/upload-artifact@v4 with: From 00411a5d10bddbb625c4265764b97ff7d64f8569 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:48:13 +0000 Subject: [PATCH 14/52] Improve security: set restrictive keystore permissions and reduce validity period Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 1 + android/SIGNING.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 8bf0459..59ac3dc 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -82,6 +82,7 @@ jobs: run: | if [ -n "$KEYSTORE_BASE64" ]; then echo "$KEYSTORE_BASE64" | base64 -d > ${{ github.workspace }}/android/app/release.keystore + chmod 600 ${{ github.workspace }}/android/app/release.keystore else echo "Warning: KEYSTORE_BASE64 secret not found. Skipping signing." fi diff --git a/android/SIGNING.md b/android/SIGNING.md index b9f4202..ddc4c25 100644 --- a/android/SIGNING.md +++ b/android/SIGNING.md @@ -20,10 +20,10 @@ To enable APK signing in GitHub Actions, you need to configure the following sec If you don't have a keystore yet, create one using the following command: ```bash -keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 +keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 3650 ``` -Follow the prompts to set passwords and fill in the certificate information. +This creates a keystore valid for 10 years (3650 days). Follow the prompts to set passwords and fill in the certificate information. ### Encoding the Keystore From 80b5e54ddadde15f1c7b1052c74640400c558d38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:48:13 +0000 Subject: [PATCH 15/52] Improve security: set restrictive keystore permissions and reduce validity period Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- SIGNING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SIGNING.md b/SIGNING.md index b9f4202..ddc4c25 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -20,10 +20,10 @@ To enable APK signing in GitHub Actions, you need to configure the following sec If you don't have a keystore yet, create one using the following command: ```bash -keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 10000 +keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 3650 ``` -Follow the prompts to set passwords and fill in the certificate information. +This creates a keystore valid for 10 years (3650 days). Follow the prompts to set passwords and fill in the certificate information. ### Encoding the Keystore From 2f4b9bd7ea2604b96504bd394dcaf7b4cced47a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:38:01 +0000 Subject: [PATCH 16/52] Refactor signing to use committed CI keystore with ECDSA and separate APK uploads Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 92 ++++++++++++++++++------- android/SIGNING.md | 102 +++++++++++++++------------- android/app/.gitignore | 3 +- android/app/build.gradle.kts | 27 ++------ android/app/keys/ci.keystore | Bin 0 -> 1166 bytes 5 files changed, 129 insertions(+), 95 deletions(-) create mode 100644 android/app/keys/ci.keystore diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 59ac3dc..9bf7153 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -75,41 +75,85 @@ jobs: working-directory: ./android run: ./gradlew assembleDebug --stacktrace - - name: Decode keystore - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - env: - KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} - run: | - if [ -n "$KEYSTORE_BASE64" ]; then - echo "$KEYSTORE_BASE64" | base64 -d > ${{ github.workspace }}/android/app/release.keystore - chmod 600 ${{ github.workspace }}/android/app/release.keystore - else - echo "Warning: KEYSTORE_BASE64 secret not found. Skipping signing." - fi - - name: Build release APK working-directory: ./android env: - KEYSTORE_FILE: ${{ github.workspace }}/android/app/release.keystore KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} - KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: ./gradlew assembleRelease --stacktrace - - name: Clean up keystore - if: always() - run: rm -f ${{ github.workspace }}/android/app/release.keystore + - name: Get version name + id: version-info + shell: bash + run: | + VERSION=$(grep "versionName" android/app/build.gradle.kts | sed -E 's/.*versionName = "(.*)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Rename debug APKs + shell: bash + run: | + cd android/app/build/outputs/apk/debug + for apk in *.apk; do + if [[ "$apk" == *"arm64-v8a"* ]]; then + mv "$apk" "kqt-debug-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" + elif [[ "$apk" == *"armeabi-v7a"* ]]; then + mv "$apk" "kqt-debug-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" + elif [[ "$apk" == *"universal"* ]]; then + mv "$apk" "kqt-debug-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" + fi + done + + - name: Rename release APKs + shell: bash + run: | + cd android/app/build/outputs/apk/release + for apk in *.apk; do + if [[ "$apk" == *"arm64-v8a"* ]]; then + mv "$apk" "kqt-release-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" + elif [[ "$apk" == *"armeabi-v7a"* ]]; then + mv "$apk" "kqt-release-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" + elif [[ "$apk" == *"universal"* ]]; then + mv "$apk" "kqt-release-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" + fi + done + + - name: Upload debug arm64-v8a APK + uses: actions/upload-artifact@v4 + with: + name: kqt-debug-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: android/app/build/outputs/apk/debug/kqt-debug-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk + if-no-files-found: error + + - name: Upload debug armeabi-v7a APK + uses: actions/upload-artifact@v4 + with: + name: kqt-debug-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: android/app/build/outputs/apk/debug/kqt-debug-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk + if-no-files-found: error + + - name: Upload debug universal APK + uses: actions/upload-artifact@v4 + with: + name: kqt-debug-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: android/app/build/outputs/apk/debug/kqt-debug-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk + if-no-files-found: error + + - name: Upload release arm64-v8a APK + uses: actions/upload-artifact@v4 + with: + name: kqt-release-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: android/app/build/outputs/apk/release/kqt-release-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk + if-no-files-found: error - - name: Upload debug APKs + - name: Upload release armeabi-v7a APK uses: actions/upload-artifact@v4 with: - name: kqt-android-debug-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/debug/*.apk + name: kqt-release-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: android/app/build/outputs/apk/release/kqt-release-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk if-no-files-found: error - - name: Upload release APKs + - name: Upload release universal APK uses: actions/upload-artifact@v4 with: - name: kqt-android-release-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/release/*.apk + name: kqt-release-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: android/app/build/outputs/apk/release/kqt-release-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk if-no-files-found: error diff --git a/android/SIGNING.md b/android/SIGNING.md index ddc4c25..dac8c65 100644 --- a/android/SIGNING.md +++ b/android/SIGNING.md @@ -2,82 +2,88 @@ ## Overview -This document describes how to configure APK signing for the Android build workflow. +This document describes the APK signing setup for the Android build workflow. -## GitHub Secrets Setup - -To enable APK signing in GitHub Actions, you need to configure the following secrets in your repository: - -### Required Secrets - -1. **KEYSTORE_BASE64**: Base64-encoded keystore file -2. **KEYSTORE_PASSWORD**: Password for the keystore -3. **KEY_ALIAS**: Alias of the key in the keystore -4. **KEY_PASSWORD**: Password for the key +## CI Keystore -### Creating a Keystore +The repository includes a debug signing keystore at `android/app/keys/ci.keystore` that is used for signing APKs in CI builds. -If you don't have a keystore yet, create one using the following command: - -```bash -keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 3650 -``` +### Keystore Details -This creates a keystore valid for 10 years (3650 days). Follow the prompts to set passwords and fill in the certificate information. +- **Location**: `android/app/keys/ci.keystore` +- **Alias**: `apk` +- **Algorithm**: ECDSA (EC with 256-bit key) +- **Format**: PKCS12 +- **Validity**: 10 years -### Encoding the Keystore +This is a **debug signing keystore** used only for development and CI builds. It is password-protected and committed to the repository for convenience. -To encode your keystore file to base64: - -```bash -base64 -i release.keystore -o keystore.txt -``` +## GitHub Secrets Setup -On some systems (like macOS), you might need to use: +To enable APK signing in GitHub Actions, you need to configure the following secret: -```bash -base64 -i release.keystore -``` +### Required Secret -Copy the entire output (it will be a long string). +- **KEYSTORE_PASSWORD**: Password for the CI keystore -### Adding Secrets to GitHub +### Adding Secret to GitHub 1. Go to your repository on GitHub 2. Navigate to **Settings** → **Secrets and variables** → **Actions** 3. Click **New repository secret** -4. Add each of the four secrets: - - `KEYSTORE_BASE64`: Paste the base64-encoded keystore - - `KEYSTORE_PASSWORD`: Enter your keystore password - - `KEY_ALIAS`: Enter your key alias - - `KEY_PASSWORD`: Enter your key password +4. Add the secret: + - Name: `KEYSTORE_PASSWORD` + - Value: The keystore password ## Workflow Behavior -- **Pull Requests**: APKs are built but not signed (to protect secrets from untrusted code) -- **Push to main/master**: APKs are built and signed if secrets are configured -- **Manual dispatch**: APKs are built and signed if secrets are configured +- **All branches**: APKs are built and signed using the CI keystore +- The keystore password is provided via the `KEYSTORE_PASSWORD` secret +- If the secret is not configured, builds may fail or produce unsigned APKs -If the `KEYSTORE_BASE64` secret is not found, the workflow will log a warning and continue without signing. +## Creating a New CI Keystore + +If you need to regenerate the CI keystore (e.g., if compromised), use: + +```bash +cd android/app/keys +keytool -genkeypair -v -keystore ci.keystore -alias apk \ + -keyalg EC -keysize 256 -validity 3650 -storetype PKCS12 \ + -storepass "your-password" -keypass "your-password" \ + -dname "CN=KQT CI, OU=Development, O=KQT, L=Unknown, ST=Unknown, C=US" +``` + +Replace `"your-password"` with your chosen password, and update the `KEYSTORE_PASSWORD` secret in GitHub. ## Local Development -For local builds, you can set environment variables: +For local builds, set the environment variable: ```bash -export KEYSTORE_FILE=/path/to/your/release.keystore export KEYSTORE_PASSWORD=your-keystore-password -export KEY_ALIAS=your-key-alias -export KEY_PASSWORD=your-key-password - cd android ./gradlew assembleRelease ``` -**Important**: Never commit your keystore or keystore passwords to version control! +Or provide the password inline: + +```bash +cd android +KEYSTORE_PASSWORD=your-keystore-password ./gradlew assembleRelease +``` + +## Production Signing + +**Important**: The CI keystore is for **development and testing only**. For production releases to the Google Play Store: + +1. Create a separate production keystore with strong security +2. Store it securely (not in version control) +3. Use a different signing configuration for production builds +4. Never share or commit your production keystore ## Security Notes -- The keystore file is automatically excluded from git via `.gitignore` -- Secrets are only available to trusted workflows (not pull requests from forks) -- Keep your keystore and passwords secure - losing them means you cannot update your app on the Play Store +- The CI keystore is intentionally committed to the repository for CI convenience +- It uses password protection as an additional security layer +- This is appropriate for debug/development builds but not for production releases +- Keep your production keystore separate and secure diff --git a/android/app/.gitignore b/android/app/.gitignore index 724b1e0..458eefa 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -1,3 +1,4 @@ /build *.keystore -*.jks \ No newline at end of file +*.jks +!keys/ci.keystore \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 19d7c08..fe7d74e 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -20,26 +20,12 @@ android { } } - // Helper function to check if signing credentials are available - val hasSigningCredentials = { - val keystoreFile = System.getenv("KEYSTORE_FILE") - val keystorePass = System.getenv("KEYSTORE_PASSWORD") - val alias = System.getenv("KEY_ALIAS") - val keyPass = System.getenv("KEY_PASSWORD") - keystoreFile != null && keystorePass != null && alias != null && keyPass != null - } - signingConfigs { create("release") { - if (hasSigningCredentials()) { - val keystoreFilePath = System.getenv("KEYSTORE_FILE")!! - storeFile = file(keystoreFilePath) - storePassword = System.getenv("KEYSTORE_PASSWORD") - keyAlias = System.getenv("KEY_ALIAS") - keyPassword = System.getenv("KEY_PASSWORD") - } - // If environment variables are not set, signing will be skipped - // This allows the build to complete without signing credentials + storeFile = file("keys/ci.keystore") + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "" + keyAlias = "apk" + keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: "" } } @@ -50,10 +36,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - // Only apply signing config if all credentials are available - if (hasSigningCredentials()) { - signingConfig = signingConfigs.getByName("release") - } + signingConfig = signingConfigs.getByName("release") } } diff --git a/android/app/keys/ci.keystore b/android/app/keys/ci.keystore new file mode 100644 index 0000000000000000000000000000000000000000..7a0711c59a8809dfc2879b30f8c805d56e54db02 GIT binary patch literal 1166 zcmXqLV(DUHWHxAGF=6A>YV&CO&dbQoxS)wem8FSA5hyHc(8Q>MBE=}m(!?kT6y^nD zPBw0+E*>sMrUi|A4H|bDq`}p*@)|@MSRlAO2HGqlZf~x?R+C-XVffcUC;Pyei2-Nc znlmw}3otP%81S%hK+I#}WMwdrW#dd}^I%M6W?|G~5eQ2EbSkJ{EL_Wl-RYVC-w7-M zr#@_3QCqt}=|02RE51A$3*9G+yw^v~b>~^^g0{=LEQJ-pAGwul4I=hoO~$DBQ7}B8D7tYz&DE1q|5+ ziU?s*LlG9Ckj&gv1w(UlLlXmIa|<&wBU6JWrjKw%>}(5~m>vOz?lCbk0IBN;8Ad|E zQ@`z<_|Zc_=GWxYf|+YwoW4$)>Pk%T7r4%Dem}#c@yp zUkR3lx0L>{yz$?8_n8XkjD&(<{}=ADcOz#PrZ3x^W+z^HS5G!#`?0!}asfLh{}*p8 z^fcWoR6e`Z+|)Yrp3Kjptq-Qpc(nNJI-T1ao4U#*?ktsgw_?V&zV>+c`bR?Hv;TiM z$zkuj?wGKQSxt|~{qn?@OKmr+TNT(nr+|8EOmc*(R#mu=I>(~2tJ zv(L2%wWK@T%lq}HF!r9O$|;vQ)@Lp)H>-Yp?dknDQ@s3kh^&1!PvG_Ex0URsscwJ& z&pwsGY4z*;!x`tq_J>9J?dd&VsqiJwDayvY##*0q&#H!1jeLvT7qv03+`66bTp~xg z;aYYco>hej)+=kcw#xmyy3vO#_*K>X!=lVpi>fcMSbV-BSJmlweM3$~`SD9ZH{Ttd zu{=uY*psd8Y@5D_)r9#yE>8NTcyagIDi)dC-nx$Vx0XB?rnbCTA~Kb?GhaSNu(kWf zdda9{-4{PZ1m|^R#)M_LuVvWov3`+Yzk+5-O5a`C1?%Fo+9kThQ(c}M@Oo{SdDT_H zg`rc^AoOCwsi;8z^?96eaf+!N%t>itGHY8dtmiqZ;D0_?c+uU7Irryvekm0$EsAcJ za}>{=bwk9g@7=k^=ZrnuG7FoVPv26ho3F#e|FzJ0^PlwIFb%ijSUbk;$Jft)a87e} zL9E4uOg;l&149E|crImPWMyDcc+O&Q`q9s4LCY@gi#?lLW!N7(`|Q~j0k*tNKg1@A g7=^NkOyp^kU%1`mrcr`R+TDf0-^yNCJp(0i0Q3gVbpQYW literal 0 HcmV?d00001 From 1b2e1c3c845d9deed2f46d95c3015098a2c88629 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:38:01 +0000 Subject: [PATCH 17/52] Refactor signing to use committed CI keystore with ECDSA and separate APK uploads Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- SIGNING.md | 102 +++++++++++++++++++++++-------------------- app/.gitignore | 3 +- app/build.gradle.kts | 27 +++--------- app/keys/ci.keystore | Bin 0 -> 1166 bytes 4 files changed, 61 insertions(+), 71 deletions(-) create mode 100644 app/keys/ci.keystore diff --git a/SIGNING.md b/SIGNING.md index ddc4c25..dac8c65 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -2,82 +2,88 @@ ## Overview -This document describes how to configure APK signing for the Android build workflow. +This document describes the APK signing setup for the Android build workflow. -## GitHub Secrets Setup - -To enable APK signing in GitHub Actions, you need to configure the following secrets in your repository: - -### Required Secrets - -1. **KEYSTORE_BASE64**: Base64-encoded keystore file -2. **KEYSTORE_PASSWORD**: Password for the keystore -3. **KEY_ALIAS**: Alias of the key in the keystore -4. **KEY_PASSWORD**: Password for the key +## CI Keystore -### Creating a Keystore +The repository includes a debug signing keystore at `android/app/keys/ci.keystore` that is used for signing APKs in CI builds. -If you don't have a keystore yet, create one using the following command: - -```bash -keytool -genkeypair -v -keystore release.keystore -alias your-key-alias -keyalg RSA -keysize 2048 -validity 3650 -``` +### Keystore Details -This creates a keystore valid for 10 years (3650 days). Follow the prompts to set passwords and fill in the certificate information. +- **Location**: `android/app/keys/ci.keystore` +- **Alias**: `apk` +- **Algorithm**: ECDSA (EC with 256-bit key) +- **Format**: PKCS12 +- **Validity**: 10 years -### Encoding the Keystore +This is a **debug signing keystore** used only for development and CI builds. It is password-protected and committed to the repository for convenience. -To encode your keystore file to base64: - -```bash -base64 -i release.keystore -o keystore.txt -``` +## GitHub Secrets Setup -On some systems (like macOS), you might need to use: +To enable APK signing in GitHub Actions, you need to configure the following secret: -```bash -base64 -i release.keystore -``` +### Required Secret -Copy the entire output (it will be a long string). +- **KEYSTORE_PASSWORD**: Password for the CI keystore -### Adding Secrets to GitHub +### Adding Secret to GitHub 1. Go to your repository on GitHub 2. Navigate to **Settings** → **Secrets and variables** → **Actions** 3. Click **New repository secret** -4. Add each of the four secrets: - - `KEYSTORE_BASE64`: Paste the base64-encoded keystore - - `KEYSTORE_PASSWORD`: Enter your keystore password - - `KEY_ALIAS`: Enter your key alias - - `KEY_PASSWORD`: Enter your key password +4. Add the secret: + - Name: `KEYSTORE_PASSWORD` + - Value: The keystore password ## Workflow Behavior -- **Pull Requests**: APKs are built but not signed (to protect secrets from untrusted code) -- **Push to main/master**: APKs are built and signed if secrets are configured -- **Manual dispatch**: APKs are built and signed if secrets are configured +- **All branches**: APKs are built and signed using the CI keystore +- The keystore password is provided via the `KEYSTORE_PASSWORD` secret +- If the secret is not configured, builds may fail or produce unsigned APKs -If the `KEYSTORE_BASE64` secret is not found, the workflow will log a warning and continue without signing. +## Creating a New CI Keystore + +If you need to regenerate the CI keystore (e.g., if compromised), use: + +```bash +cd android/app/keys +keytool -genkeypair -v -keystore ci.keystore -alias apk \ + -keyalg EC -keysize 256 -validity 3650 -storetype PKCS12 \ + -storepass "your-password" -keypass "your-password" \ + -dname "CN=KQT CI, OU=Development, O=KQT, L=Unknown, ST=Unknown, C=US" +``` + +Replace `"your-password"` with your chosen password, and update the `KEYSTORE_PASSWORD` secret in GitHub. ## Local Development -For local builds, you can set environment variables: +For local builds, set the environment variable: ```bash -export KEYSTORE_FILE=/path/to/your/release.keystore export KEYSTORE_PASSWORD=your-keystore-password -export KEY_ALIAS=your-key-alias -export KEY_PASSWORD=your-key-password - cd android ./gradlew assembleRelease ``` -**Important**: Never commit your keystore or keystore passwords to version control! +Or provide the password inline: + +```bash +cd android +KEYSTORE_PASSWORD=your-keystore-password ./gradlew assembleRelease +``` + +## Production Signing + +**Important**: The CI keystore is for **development and testing only**. For production releases to the Google Play Store: + +1. Create a separate production keystore with strong security +2. Store it securely (not in version control) +3. Use a different signing configuration for production builds +4. Never share or commit your production keystore ## Security Notes -- The keystore file is automatically excluded from git via `.gitignore` -- Secrets are only available to trusted workflows (not pull requests from forks) -- Keep your keystore and passwords secure - losing them means you cannot update your app on the Play Store +- The CI keystore is intentionally committed to the repository for CI convenience +- It uses password protection as an additional security layer +- This is appropriate for debug/development builds but not for production releases +- Keep your production keystore separate and secure diff --git a/app/.gitignore b/app/.gitignore index 724b1e0..458eefa 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,4 @@ /build *.keystore -*.jks \ No newline at end of file +*.jks +!keys/ci.keystore \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19d7c08..fe7d74e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,26 +20,12 @@ android { } } - // Helper function to check if signing credentials are available - val hasSigningCredentials = { - val keystoreFile = System.getenv("KEYSTORE_FILE") - val keystorePass = System.getenv("KEYSTORE_PASSWORD") - val alias = System.getenv("KEY_ALIAS") - val keyPass = System.getenv("KEY_PASSWORD") - keystoreFile != null && keystorePass != null && alias != null && keyPass != null - } - signingConfigs { create("release") { - if (hasSigningCredentials()) { - val keystoreFilePath = System.getenv("KEYSTORE_FILE")!! - storeFile = file(keystoreFilePath) - storePassword = System.getenv("KEYSTORE_PASSWORD") - keyAlias = System.getenv("KEY_ALIAS") - keyPassword = System.getenv("KEY_PASSWORD") - } - // If environment variables are not set, signing will be skipped - // This allows the build to complete without signing credentials + storeFile = file("keys/ci.keystore") + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "" + keyAlias = "apk" + keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: "" } } @@ -50,10 +36,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - // Only apply signing config if all credentials are available - if (hasSigningCredentials()) { - signingConfig = signingConfigs.getByName("release") - } + signingConfig = signingConfigs.getByName("release") } } diff --git a/app/keys/ci.keystore b/app/keys/ci.keystore new file mode 100644 index 0000000000000000000000000000000000000000..7a0711c59a8809dfc2879b30f8c805d56e54db02 GIT binary patch literal 1166 zcmXqLV(DUHWHxAGF=6A>YV&CO&dbQoxS)wem8FSA5hyHc(8Q>MBE=}m(!?kT6y^nD zPBw0+E*>sMrUi|A4H|bDq`}p*@)|@MSRlAO2HGqlZf~x?R+C-XVffcUC;Pyei2-Nc znlmw}3otP%81S%hK+I#}WMwdrW#dd}^I%M6W?|G~5eQ2EbSkJ{EL_Wl-RYVC-w7-M zr#@_3QCqt}=|02RE51A$3*9G+yw^v~b>~^^g0{=LEQJ-pAGwul4I=hoO~$DBQ7}B8D7tYz&DE1q|5+ ziU?s*LlG9Ckj&gv1w(UlLlXmIa|<&wBU6JWrjKw%>}(5~m>vOz?lCbk0IBN;8Ad|E zQ@`z<_|Zc_=GWxYf|+YwoW4$)>Pk%T7r4%Dem}#c@yp zUkR3lx0L>{yz$?8_n8XkjD&(<{}=ADcOz#PrZ3x^W+z^HS5G!#`?0!}asfLh{}*p8 z^fcWoR6e`Z+|)Yrp3Kjptq-Qpc(nNJI-T1ao4U#*?ktsgw_?V&zV>+c`bR?Hv;TiM z$zkuj?wGKQSxt|~{qn?@OKmr+TNT(nr+|8EOmc*(R#mu=I>(~2tJ zv(L2%wWK@T%lq}HF!r9O$|;vQ)@Lp)H>-Yp?dknDQ@s3kh^&1!PvG_Ex0URsscwJ& z&pwsGY4z*;!x`tq_J>9J?dd&VsqiJwDayvY##*0q&#H!1jeLvT7qv03+`66bTp~xg z;aYYco>hej)+=kcw#xmyy3vO#_*K>X!=lVpi>fcMSbV-BSJmlweM3$~`SD9ZH{Ttd zu{=uY*psd8Y@5D_)r9#yE>8NTcyagIDi)dC-nx$Vx0XB?rnbCTA~Kb?GhaSNu(kWf zdda9{-4{PZ1m|^R#)M_LuVvWov3`+Yzk+5-O5a`C1?%Fo+9kThQ(c}M@Oo{SdDT_H zg`rc^AoOCwsi;8z^?96eaf+!N%t>itGHY8dtmiqZ;D0_?c+uU7Irryvekm0$EsAcJ za}>{=bwk9g@7=k^=ZrnuG7FoVPv26ho3F#e|FzJ0^PlwIFb%ijSUbk;$Jft)a87e} zL9E4uOg;l&149E|crImPWMyDcc+O&Q`q9s4LCY@gi#?lLW!N7(`|Q~j0k*tNKg1@A g7=^NkOyp^kU%1`mrcr`R+TDf0-^yNCJp(0i0Q3gVbpQYW literal 0 HcmV?d00001 From 07e021b3a49f464b45c39e1f9b34a1ffeba4a233 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:39:27 +0000 Subject: [PATCH 18/52] Address code review: improve error handling and reduce duplication Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 29 +++++++++++++++++++++++++++++ .github/workflows/android-build.yml | 27 ++++----------------------- android/app/build.gradle.kts | 4 ++-- 3 files changed, 35 insertions(+), 25 deletions(-) create mode 100755 .github/scripts/rename-apks.sh diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh new file mode 100755 index 0000000..fb644cf --- /dev/null +++ b/.github/scripts/rename-apks.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Script to rename APK files with standard naming convention +# Usage: rename-apks.sh + +set -e + +BUILD_TYPE=$1 +VERSION=$2 +COMMIT=$3 + +if [ -z "$BUILD_TYPE" ] || [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then + echo "Usage: $0 " + exit 1 +fi + +cd "android/app/build/outputs/apk/$BUILD_TYPE" + +for apk in *.apk; do + if [[ "$apk" == *"arm64-v8a"* ]]; then + mv "$apk" "kqt-$BUILD_TYPE-arm64-v8a-$VERSION-$COMMIT.apk" + elif [[ "$apk" == *"armeabi-v7a"* ]]; then + mv "$apk" "kqt-$BUILD_TYPE-armeabi-v7a-$VERSION-$COMMIT.apk" + elif [[ "$apk" == *"universal"* ]]; then + mv "$apk" "kqt-$BUILD_TYPE-universal-$VERSION-$COMMIT.apk" + fi +done + +echo "Renamed APKs in $BUILD_TYPE:" +ls -1 *.apk diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 9bf7153..ca83aca 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -83,38 +83,19 @@ jobs: - name: Get version name id: version-info + working-directory: ./android shell: bash run: | - VERSION=$(grep "versionName" android/app/build.gradle.kts | sed -E 's/.*versionName = "(.*)"/\1/') + VERSION=$(./gradlew -q properties --property versionName | grep "^versionName:" | awk '{print $2}') echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Rename debug APKs shell: bash - run: | - cd android/app/build/outputs/apk/debug - for apk in *.apk; do - if [[ "$apk" == *"arm64-v8a"* ]]; then - mv "$apk" "kqt-debug-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" - elif [[ "$apk" == *"armeabi-v7a"* ]]; then - mv "$apk" "kqt-debug-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" - elif [[ "$apk" == *"universal"* ]]; then - mv "$apk" "kqt-debug-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" - fi - done + run: ./.github/scripts/rename-apks.sh debug ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} - name: Rename release APKs shell: bash - run: | - cd android/app/build/outputs/apk/release - for apk in *.apk; do - if [[ "$apk" == *"arm64-v8a"* ]]; then - mv "$apk" "kqt-release-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" - elif [[ "$apk" == *"armeabi-v7a"* ]]; then - mv "$apk" "kqt-release-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" - elif [[ "$apk" == *"universal"* ]]; then - mv "$apk" "kqt-release-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk" - fi - done + run: ./.github/scripts/rename-apks.sh release ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} - name: Upload debug arm64-v8a APK uses: actions/upload-artifact@v4 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fe7d74e..04ce425 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -23,9 +23,9 @@ android { signingConfigs { create("release") { storeFile = file("keys/ci.keystore") - storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "" + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") keyAlias = "apk" - keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: "" + keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") } } From 9ea37d0742a51dab53e4b5065a25948459f2837b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:39:27 +0000 Subject: [PATCH 19/52] Address code review: improve error handling and reduce duplication Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe7d74e..04ce425 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,9 +23,9 @@ android { signingConfigs { create("release") { storeFile = file("keys/ci.keystore") - storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "" + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") keyAlias = "apk" - keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: "" + keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") } } From f6fef48bfb84014e866b8bed97310271a84cc405 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:40:28 +0000 Subject: [PATCH 20/52] Fix Gradle properties command and improve script robustness Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 3 +++ .github/workflows/android-build.yml | 2 +- android/app/build.gradle.kts | 6 ++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index fb644cf..98da610 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -15,6 +15,9 @@ fi cd "android/app/build/outputs/apk/$BUILD_TYPE" +# Enable nullglob to handle case where no APK files exist +shopt -s nullglob + for apk in *.apk; do if [[ "$apk" == *"arm64-v8a"* ]]; then mv "$apk" "kqt-$BUILD_TYPE-arm64-v8a-$VERSION-$COMMIT.apk" diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index ca83aca..0f22d23 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -86,7 +86,7 @@ jobs: working-directory: ./android shell: bash run: | - VERSION=$(./gradlew -q properties --property versionName | grep "^versionName:" | awk '{print $2}') + VERSION=$(./gradlew -q properties | grep "^versionName:" | awk '{print $2}') echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Rename debug APKs diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 04ce425..43a695b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,10 +22,12 @@ android { signingConfigs { create("release") { + val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + ?: error("KEYSTORE_PASSWORD environment variable is required") storeFile = file("keys/ci.keystore") - storePassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") + storePassword = keystorePassword keyAlias = "apk" - keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") + keyPassword = keystorePassword } } From 5161dce758570e4d46475accdaad32c7fb7ecb4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:40:28 +0000 Subject: [PATCH 21/52] Fix Gradle properties command and improve script robustness Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04ce425..43a695b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,10 +22,12 @@ android { signingConfigs { create("release") { + val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + ?: error("KEYSTORE_PASSWORD environment variable is required") storeFile = file("keys/ci.keystore") - storePassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") + storePassword = keystorePassword keyAlias = "apk" - keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") + keyPassword = keystorePassword } } From bc2207d5e90c43d54dee71f686936a303d1c06bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:41:10 +0000 Subject: [PATCH 22/52] Improve documentation accuracy and script error handling Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 10 +++++++++- android/SIGNING.md | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index 98da610..b6fce35 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -13,7 +13,15 @@ if [ -z "$BUILD_TYPE" ] || [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then exit 1 fi -cd "android/app/build/outputs/apk/$BUILD_TYPE" +APK_DIR="android/app/build/outputs/apk/$BUILD_TYPE" + +if [ ! -d "$APK_DIR" ]; then + echo "Error: APK directory does not exist: $APK_DIR" + echo "Make sure the build has completed successfully before renaming APKs" + exit 1 +fi + +cd "$APK_DIR" # Enable nullglob to handle case where no APK files exist shopt -s nullglob diff --git a/android/SIGNING.md b/android/SIGNING.md index dac8c65..9cf6938 100644 --- a/android/SIGNING.md +++ b/android/SIGNING.md @@ -39,7 +39,7 @@ To enable APK signing in GitHub Actions, you need to configure the following sec - **All branches**: APKs are built and signed using the CI keystore - The keystore password is provided via the `KEYSTORE_PASSWORD` secret -- If the secret is not configured, builds may fail or produce unsigned APKs +- If the secret is not configured, the build will fail with an error message ## Creating a New CI Keystore From be08429e4330e17678a621382a552ce11d050221 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:41:10 +0000 Subject: [PATCH 23/52] Improve documentation accuracy and script error handling Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- SIGNING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SIGNING.md b/SIGNING.md index dac8c65..9cf6938 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -39,7 +39,7 @@ To enable APK signing in GitHub Actions, you need to configure the following sec - **All branches**: APKs are built and signed using the CI keystore - The keystore password is provided via the `KEYSTORE_PASSWORD` secret -- If the secret is not configured, builds may fail or produce unsigned APKs +- If the secret is not configured, the build will fail with an error message ## Creating a New CI Keystore From 30b94a84e5399ca005d919c46b434fcd9f5fa4a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:42:09 +0000 Subject: [PATCH 24/52] Final polish: validate version extraction and warn on unexpected APKs Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 2 ++ .github/workflows/android-build.yml | 4 ++++ android/app/build.gradle.kts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index b6fce35..44f48b2 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -33,6 +33,8 @@ for apk in *.apk; do mv "$apk" "kqt-$BUILD_TYPE-armeabi-v7a-$VERSION-$COMMIT.apk" elif [[ "$apk" == *"universal"* ]]; then mv "$apk" "kqt-$BUILD_TYPE-universal-$VERSION-$COMMIT.apk" + else + echo "Warning: Unexpected APK file does not match known patterns: $apk" fi done diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 0f22d23..5f04178 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -87,6 +87,10 @@ jobs: shell: bash run: | VERSION=$(./gradlew -q properties | grep "^versionName:" | awk '{print $2}') + if [ -z "$VERSION" ]; then + echo "Error: Could not extract version name from gradle properties" + exit 1 + fi echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Rename debug APKs diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 43a695b..a089deb 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,7 +22,7 @@ android { signingConfigs { create("release") { - val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + val keystorePassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") storeFile = file("keys/ci.keystore") storePassword = keystorePassword From 9aca50645db8b3906ffa0ad0b1348683c82e3b3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:42:09 +0000 Subject: [PATCH 25/52] Final polish: validate version extraction and warn on unexpected APKs Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 43a695b..a089deb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { signingConfigs { create("release") { - val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + val keystorePassword = System.getenv("KEYSTORE_PASSWORD") ?: error("KEYSTORE_PASSWORD environment variable is required") storeFile = file("keys/ci.keystore") storePassword = keystorePassword From 22d38697f3448b075957c6bfe2a4024301b74cb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:42:56 +0000 Subject: [PATCH 26/52] Add better error handling for edge cases Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 6 +++++- .github/workflows/android-build.yml | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index 44f48b2..2e69fd7 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -39,4 +39,8 @@ for apk in *.apk; do done echo "Renamed APKs in $BUILD_TYPE:" -ls -1 *.apk +if ls *.apk >/dev/null 2>&1; then + ls -1 *.apk +else + echo "Warning: No APK files found in $APK_DIR" +fi diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 5f04178..dc51766 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -86,11 +86,14 @@ jobs: working-directory: ./android shell: bash run: | - VERSION=$(./gradlew -q properties | grep "^versionName:" | awk '{print $2}') + VERSION=$(./gradlew -q properties | grep -E "^versionName:" | awk '{print $2}') if [ -z "$VERSION" ]; then echo "Error: Could not extract version name from gradle properties" + echo "Gradle properties output:" + ./gradlew -q properties | head -20 exit 1 fi + echo "Extracted version: $VERSION" echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Rename debug APKs From 230d024ef57d4173d20998b3a52c43e4f578dec3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:44:06 +0000 Subject: [PATCH 27/52] Fix APK rename script to use continue statements Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index 2e69fd7..38c557a 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -29,10 +29,13 @@ shopt -s nullglob for apk in *.apk; do if [[ "$apk" == *"arm64-v8a"* ]]; then mv "$apk" "kqt-$BUILD_TYPE-arm64-v8a-$VERSION-$COMMIT.apk" + continue elif [[ "$apk" == *"armeabi-v7a"* ]]; then mv "$apk" "kqt-$BUILD_TYPE-armeabi-v7a-$VERSION-$COMMIT.apk" + continue elif [[ "$apk" == *"universal"* ]]; then mv "$apk" "kqt-$BUILD_TYPE-universal-$VERSION-$COMMIT.apk" + continue else echo "Warning: Unexpected APK file does not match known patterns: $apk" fi From fd7c9c719915d047421f9ef358e84aabe83da8b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:49:56 +0000 Subject: [PATCH 28/52] Sign debug builds, simplify uploads, and improve keystore documentation Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 45 +++++------------------------ android/SIGNING.md | 15 ++++++---- android/app/build.gradle.kts | 3 ++ 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index dc51766..ee55f2b 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -73,6 +73,8 @@ jobs: - name: Build debug APK working-directory: ./android + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: ./gradlew assembleDebug --stacktrace - name: Build release APK @@ -104,44 +106,11 @@ jobs: shell: bash run: ./.github/scripts/rename-apks.sh release ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} - - name: Upload debug arm64-v8a APK - uses: actions/upload-artifact@v4 - with: - name: kqt-debug-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/debug/kqt-debug-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk - if-no-files-found: error - - - name: Upload debug armeabi-v7a APK - uses: actions/upload-artifact@v4 - with: - name: kqt-debug-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/debug/kqt-debug-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk - if-no-files-found: error - - - name: Upload debug universal APK - uses: actions/upload-artifact@v4 - with: - name: kqt-debug-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/debug/kqt-debug-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk - if-no-files-found: error - - - name: Upload release arm64-v8a APK - uses: actions/upload-artifact@v4 - with: - name: kqt-release-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/release/kqt-release-arm64-v8a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk - if-no-files-found: error - - - name: Upload release armeabi-v7a APK - uses: actions/upload-artifact@v4 - with: - name: kqt-release-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/release/kqt-release-armeabi-v7a-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk - if-no-files-found: error - - - name: Upload release universal APK + - name: Upload APKs uses: actions/upload-artifact@v4 with: - name: kqt-release-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: android/app/build/outputs/apk/release/kqt-release-universal-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }}.apk + name: kqt-apks-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} + path: | + android/app/build/outputs/apk/debug/kqt-*.apk + android/app/build/outputs/apk/release/kqt-*.apk if-no-files-found: error diff --git a/android/SIGNING.md b/android/SIGNING.md index 9cf6938..42036fa 100644 --- a/android/SIGNING.md +++ b/android/SIGNING.md @@ -47,13 +47,18 @@ If you need to regenerate the CI keystore (e.g., if compromised), use: ```bash cd android/app/keys -keytool -genkeypair -v -keystore ci.keystore -alias apk \ - -keyalg EC -keysize 256 -validity 3650 -storetype PKCS12 \ - -storepass "your-password" -keypass "your-password" \ - -dname "CN=KQT CI, OU=Development, O=KQT, L=Unknown, ST=Unknown, C=US" +keytool -genkeypair -v -keystore ci.keystore -alias apk -keyalg EC -validity 3650 ``` -Replace `"your-password"` with your chosen password, and update the `KEYSTORE_PASSWORD` secret in GitHub. +This command will interactively prompt you for: +- Keystore password (enter it twice for confirmation) +- Key password (you can press Enter to use the same password as the keystore) +- Your name, organizational unit, organization, city, state, and country + +**Notes:** +- The key password can be the same as the keystore password (press Enter when prompted for key password) +- PKCS12 is the default keystore type in modern keytool versions +- After creating the keystore, update the `KEYSTORE_PASSWORD` secret in GitHub with the password you chose ## Local Development diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a089deb..3093e13 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -32,6 +32,9 @@ android { } buildTypes { + debug { + signingConfig = signingConfigs.getByName("release") + } release { isMinifyEnabled = false proguardFiles( From b3bedecba8a090f86e0837474f384f95930e35e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:49:56 +0000 Subject: [PATCH 29/52] Sign debug builds, simplify uploads, and improve keystore documentation Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- SIGNING.md | 15 ++++++++++----- app/build.gradle.kts | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/SIGNING.md b/SIGNING.md index 9cf6938..42036fa 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -47,13 +47,18 @@ If you need to regenerate the CI keystore (e.g., if compromised), use: ```bash cd android/app/keys -keytool -genkeypair -v -keystore ci.keystore -alias apk \ - -keyalg EC -keysize 256 -validity 3650 -storetype PKCS12 \ - -storepass "your-password" -keypass "your-password" \ - -dname "CN=KQT CI, OU=Development, O=KQT, L=Unknown, ST=Unknown, C=US" +keytool -genkeypair -v -keystore ci.keystore -alias apk -keyalg EC -validity 3650 ``` -Replace `"your-password"` with your chosen password, and update the `KEYSTORE_PASSWORD` secret in GitHub. +This command will interactively prompt you for: +- Keystore password (enter it twice for confirmation) +- Key password (you can press Enter to use the same password as the keystore) +- Your name, organizational unit, organization, city, state, and country + +**Notes:** +- The key password can be the same as the keystore password (press Enter when prompted for key password) +- PKCS12 is the default keystore type in modern keytool versions +- After creating the keystore, update the `KEYSTORE_PASSWORD` secret in GitHub with the password you chose ## Local Development diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a089deb..3093e13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,9 @@ android { } buildTypes { + debug { + signingConfig = signingConfigs.getByName("release") + } release { isMinifyEnabled = false proguardFiles( From cb6d5d697612a12cacc65759f617ad2597e2a0c1 Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Wed, 18 Feb 2026 16:58:44 +0800 Subject: [PATCH 30/52] Add: ci.keystore --- android/app/keys/ci.keystore | Bin 1166 -> 1166 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/android/app/keys/ci.keystore b/android/app/keys/ci.keystore index 7a0711c59a8809dfc2879b30f8c805d56e54db02..461e44b22982aa587dea8b2f0420a8d4c0e8438f 100644 GIT binary patch delta 872 zcmV-u1DE`c362SnbQGBQSA;}w`kSDM4C_OrxTMdUS!j`nBx14Ifjzfm2!Lh_q=;Pt zP=o|fRgY4!s8d^jmO!1fc<9H{iShC3+0rz{@aFOvcLwJozeTGT7pY_-J5La^ur3@% zbh)z5`&Y`jVD-MIp@5W%N{Z$lCQ6hAmO2BEy0E!blMVqj2Q@J z+VRSF@~=?hWLH!^VRmUvTx1RRO%HNt{3n3AgW`h)C!4Q=V^r1W!dNallRHEkQ#K>x z5e_Na-7s27_$@p>A0nCE;kp%p%I^Bl#{-YV^{Q(t55muk1FM1(!fTc1e=|ckJ$GXI zHrBLC0RZ_{)@^I9Ry(T}i9L%~M^i=41(q0oSOgvEESec0FvzbY)h60RMIQj9${Sc_ zL!uz3Mz7~9;(lLlQlEgkYG#OJR)-MSGM`lu|F_pvUsr$)k|-_fXamp7QK~>PB-UOR z6O)G*K#G_=(G8&MRXbCRe+Q{q3vC|hJfx<1cLbT}^8@!a*_L-tf3sK?m)eJ`J*A9O zjy;NqlNt^RD{Q?7`M(u7DN+GBt%e9b8lO_UGNSk`2rFesExP(Uf8}GfL)$ir!v!U|+ZuK+NZ!tyfpz1jaL|4#f}M;5>4?AVQf}~HF4D7dl?SHW`p;VI zZW*;`-I&mhG8y7njo9O7zfY>5yk(-GdLAqx_M4JE#I74#K7V6svOF<~!Ws(JHN-lR z4Y15h;$$;~g+;z@e`Y}_nTjpx_x$GEn<^1X#F!*7!?Ko=QYnlMp6HMKmaWF3H_3Yh6 zI*sa%my)aEMF)$4VlSTv%I_pu0oYR)ZoNR|W yaw?a;Jk)_|$vm?)zy7Sp*b{1ktSL>0a0e$Io?gcMOBUJD_U<0w)lG3!tL_ delta 872 zcmV-u1DE`c362SnbQDJH*Xt%7sfaQCKrCy(%#l#c?KhE#Bw|u$^2$<=7FQ}n2SVmg z{g4Du%J8q*9SM>Xf%P`{z!aiWm&wFBRQ2ifjIPv}Qo-C3J)XQs1eJ{NV}E*)66#(t?DP`Q);7lCm}HN6sdn{_ue zJ89k=`Es@3mzd(C&8{rlv4Viyj)z}If8r8XoB#012tPxv#uFSie2f&| zcVX$JJ+mh|a6aY_yz;Fuf3^W2l%HGwwom}+0-`Mjvhm9yB=>91gc5{jK;CZp;&ENx zNhHcdoIA|Yr#5@z*5%*qluAyx6s_i-5bN{pc?UIRM*aVr%4i8X`p@B*&KAE`T28!; z&v_v9ZbDi-H+(xU3B0O+s(}uoN1}!UskOHb&S405F|7v-461Qpe>S?!}m=S|Y~ewTA|>^cH+pPUCZ8`XSM~ zt$G9;ZH<12hwV8G(3OPfq!g76iEkfV5rvD`uNhinE$H|Z5uS)?TvlpFtpK-3uc8r; zASrZYj@=!gu3u`0e;A7wWklq_O6xIc)kPpg0EsCuRncI|T2W80ZV6sqA!P^yVrCW` zt%T6848 Date: Wed, 18 Feb 2026 16:58:44 +0800 Subject: [PATCH 31/52] Add: ci.keystore --- app/keys/ci.keystore | Bin 1166 -> 1166 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/keys/ci.keystore b/app/keys/ci.keystore index 7a0711c59a8809dfc2879b30f8c805d56e54db02..461e44b22982aa587dea8b2f0420a8d4c0e8438f 100644 GIT binary patch delta 872 zcmV-u1DE`c362SnbQGBQSA;}w`kSDM4C_OrxTMdUS!j`nBx14Ifjzfm2!Lh_q=;Pt zP=o|fRgY4!s8d^jmO!1fc<9H{iShC3+0rz{@aFOvcLwJozeTGT7pY_-J5La^ur3@% zbh)z5`&Y`jVD-MIp@5W%N{Z$lCQ6hAmO2BEy0E!blMVqj2Q@J z+VRSF@~=?hWLH!^VRmUvTx1RRO%HNt{3n3AgW`h)C!4Q=V^r1W!dNallRHEkQ#K>x z5e_Na-7s27_$@p>A0nCE;kp%p%I^Bl#{-YV^{Q(t55muk1FM1(!fTc1e=|ckJ$GXI zHrBLC0RZ_{)@^I9Ry(T}i9L%~M^i=41(q0oSOgvEESec0FvzbY)h60RMIQj9${Sc_ zL!uz3Mz7~9;(lLlQlEgkYG#OJR)-MSGM`lu|F_pvUsr$)k|-_fXamp7QK~>PB-UOR z6O)G*K#G_=(G8&MRXbCRe+Q{q3vC|hJfx<1cLbT}^8@!a*_L-tf3sK?m)eJ`J*A9O zjy;NqlNt^RD{Q?7`M(u7DN+GBt%e9b8lO_UGNSk`2rFesExP(Uf8}GfL)$ir!v!U|+ZuK+NZ!tyfpz1jaL|4#f}M;5>4?AVQf}~HF4D7dl?SHW`p;VI zZW*;`-I&mhG8y7njo9O7zfY>5yk(-GdLAqx_M4JE#I74#K7V6svOF<~!Ws(JHN-lR z4Y15h;$$;~g+;z@e`Y}_nTjpx_x$GEn<^1X#F!*7!?Ko=QYnlMp6HMKmaWF3H_3Yh6 zI*sa%my)aEMF)$4VlSTv%I_pu0oYR)ZoNR|W yaw?a;Jk)_|$vm?)zy7Sp*b{1ktSL>0a0e$Io?gcMOBUJD_U<0w)lG3!tL_ delta 872 zcmV-u1DE`c362SnbQDJH*Xt%7sfaQCKrCy(%#l#c?KhE#Bw|u$^2$<=7FQ}n2SVmg z{g4Du%J8q*9SM>Xf%P`{z!aiWm&wFBRQ2ifjIPv}Qo-C3J)XQs1eJ{NV}E*)66#(t?DP`Q);7lCm}HN6sdn{_ue zJ89k=`Es@3mzd(C&8{rlv4Viyj)z}If8r8XoB#012tPxv#uFSie2f&| zcVX$JJ+mh|a6aY_yz;Fuf3^W2l%HGwwom}+0-`Mjvhm9yB=>91gc5{jK;CZp;&ENx zNhHcdoIA|Yr#5@z*5%*qluAyx6s_i-5bN{pc?UIRM*aVr%4i8X`p@B*&KAE`T28!; z&v_v9ZbDi-H+(xU3B0O+s(}uoN1}!UskOHb&S405F|7v-461Qpe>S?!}m=S|Y~ewTA|>^cH+pPUCZ8`XSM~ zt$G9;ZH<12hwV8G(3OPfq!g76iEkfV5rvD`uNhinE$H|Z5uS)?TvlpFtpK-3uc8r; zASrZYj@=!gu3u`0e;A7wWklq_O6xIc)kPpg0EsCuRncI|T2W80ZV6sqA!P^yVrCW` zt%T6848 Date: Wed, 18 Feb 2026 09:06:40 +0000 Subject: [PATCH 32/52] Make signing optional and mark unsigned APKs in filename Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 17 ++++++++++++----- .github/workflows/android-build.yml | 14 ++++++++++++-- android/app/build.gradle.kts | 21 ++++++++++++++------- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index 38c557a..3268c7c 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -1,15 +1,16 @@ #!/bin/bash # Script to rename APK files with standard naming convention -# Usage: rename-apks.sh +# Usage: rename-apks.sh [signed] set -e BUILD_TYPE=$1 VERSION=$2 COMMIT=$3 +SIGNED=${4:-"false"} if [ -z "$BUILD_TYPE" ] || [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then - echo "Usage: $0 " + echo "Usage: $0 [signed]" exit 1 fi @@ -26,15 +27,21 @@ cd "$APK_DIR" # Enable nullglob to handle case where no APK files exist shopt -s nullglob +# Determine suffix based on signing status +SUFFIX="" +if [ "$SIGNED" != "true" ]; then + SUFFIX="-UNSIGNED" +fi + for apk in *.apk; do if [[ "$apk" == *"arm64-v8a"* ]]; then - mv "$apk" "kqt-$BUILD_TYPE-arm64-v8a-$VERSION-$COMMIT.apk" + mv "$apk" "kqt-$BUILD_TYPE-arm64-v8a-$VERSION-$COMMIT$SUFFIX.apk" continue elif [[ "$apk" == *"armeabi-v7a"* ]]; then - mv "$apk" "kqt-$BUILD_TYPE-armeabi-v7a-$VERSION-$COMMIT.apk" + mv "$apk" "kqt-$BUILD_TYPE-armeabi-v7a-$VERSION-$COMMIT$SUFFIX.apk" continue elif [[ "$apk" == *"universal"* ]]; then - mv "$apk" "kqt-$BUILD_TYPE-universal-$VERSION-$COMMIT.apk" + mv "$apk" "kqt-$BUILD_TYPE-universal-$VERSION-$COMMIT$SUFFIX.apk" continue else echo "Warning: Unexpected APK file does not match known patterns: $apk" diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index ee55f2b..f8a3e60 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -98,13 +98,23 @@ jobs: echo "Extracted version: $VERSION" echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Check if signed + id: signing-status + shell: bash + run: | + if [ -n "${{ secrets.KEYSTORE_PASSWORD }}" ]; then + echo "signed=true" >> $GITHUB_OUTPUT + else + echo "signed=false" >> $GITHUB_OUTPUT + fi + - name: Rename debug APKs shell: bash - run: ./.github/scripts/rename-apks.sh debug ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} + run: ./.github/scripts/rename-apks.sh debug ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} ${{ steps.signing-status.outputs.signed }} - name: Rename release APKs shell: bash - run: ./.github/scripts/rename-apks.sh release ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} + run: ./.github/scripts/rename-apks.sh release ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} ${{ steps.signing-status.outputs.signed }} - name: Upload APKs uses: actions/upload-artifact@v4 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3093e13..ef18e77 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -23,17 +23,21 @@ android { signingConfigs { create("release") { val keystorePassword = System.getenv("KEYSTORE_PASSWORD") - ?: error("KEYSTORE_PASSWORD environment variable is required") - storeFile = file("keys/ci.keystore") - storePassword = keystorePassword - keyAlias = "apk" - keyPassword = keystorePassword + if (keystorePassword != null) { + storeFile = file("keys/ci.keystore") + storePassword = keystorePassword + keyAlias = "apk" + keyPassword = keystorePassword + } } } buildTypes { debug { - signingConfig = signingConfigs.getByName("release") + // Only sign if KEYSTORE_PASSWORD is available + if (System.getenv("KEYSTORE_PASSWORD") != null) { + signingConfig = signingConfigs.getByName("release") + } } release { isMinifyEnabled = false @@ -41,7 +45,10 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - signingConfig = signingConfigs.getByName("release") + // Only sign if KEYSTORE_PASSWORD is available + if (System.getenv("KEYSTORE_PASSWORD") != null) { + signingConfig = signingConfigs.getByName("release") + } } } From 1d886512267cf0c33b95c2dff0b7ad62b7b058b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:06:40 +0000 Subject: [PATCH 33/52] Make signing optional and mark unsigned APKs in filename Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3093e13..ef18e77 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,17 +23,21 @@ android { signingConfigs { create("release") { val keystorePassword = System.getenv("KEYSTORE_PASSWORD") - ?: error("KEYSTORE_PASSWORD environment variable is required") - storeFile = file("keys/ci.keystore") - storePassword = keystorePassword - keyAlias = "apk" - keyPassword = keystorePassword + if (keystorePassword != null) { + storeFile = file("keys/ci.keystore") + storePassword = keystorePassword + keyAlias = "apk" + keyPassword = keystorePassword + } } } buildTypes { debug { - signingConfig = signingConfigs.getByName("release") + // Only sign if KEYSTORE_PASSWORD is available + if (System.getenv("KEYSTORE_PASSWORD") != null) { + signingConfig = signingConfigs.getByName("release") + } } release { isMinifyEnabled = false @@ -41,7 +45,10 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - signingConfig = signingConfigs.getByName("release") + // Only sign if KEYSTORE_PASSWORD is available + if (System.getenv("KEYSTORE_PASSWORD") != null) { + signingConfig = signingConfigs.getByName("release") + } } } From 0efce98d56564f3db59a9ccacdde617d6bc57bd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:07:19 +0000 Subject: [PATCH 34/52] Optimize build.gradle.kts to avoid redundant environment variable checks Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- android/app/build.gradle.kts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ef18e77..29368fd 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -20,9 +20,10 @@ android { } } + val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + signingConfigs { create("release") { - val keystorePassword = System.getenv("KEYSTORE_PASSWORD") if (keystorePassword != null) { storeFile = file("keys/ci.keystore") storePassword = keystorePassword @@ -35,7 +36,7 @@ android { buildTypes { debug { // Only sign if KEYSTORE_PASSWORD is available - if (System.getenv("KEYSTORE_PASSWORD") != null) { + if (keystorePassword != null) { signingConfig = signingConfigs.getByName("release") } } @@ -46,7 +47,7 @@ android { "proguard-rules.pro" ) // Only sign if KEYSTORE_PASSWORD is available - if (System.getenv("KEYSTORE_PASSWORD") != null) { + if (keystorePassword != null) { signingConfig = signingConfigs.getByName("release") } } From 2fc539087c238acc9a524f5cee9624825bb25d22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:07:19 +0000 Subject: [PATCH 35/52] Optimize build.gradle.kts to avoid redundant environment variable checks Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef18e77..29368fd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,9 +20,10 @@ android { } } + val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + signingConfigs { create("release") { - val keystorePassword = System.getenv("KEYSTORE_PASSWORD") if (keystorePassword != null) { storeFile = file("keys/ci.keystore") storePassword = keystorePassword @@ -35,7 +36,7 @@ android { buildTypes { debug { // Only sign if KEYSTORE_PASSWORD is available - if (System.getenv("KEYSTORE_PASSWORD") != null) { + if (keystorePassword != null) { signingConfig = signingConfigs.getByName("release") } } @@ -46,7 +47,7 @@ android { "proguard-rules.pro" ) // Only sign if KEYSTORE_PASSWORD is available - if (System.getenv("KEYSTORE_PASSWORD") != null) { + if (keystorePassword != null) { signingConfig = signingConfigs.getByName("release") } } From 31d3e668e8541e692fe82633b8c8ace8713d6058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:04 +0000 Subject: [PATCH 36/52] Fix signing detection in PRs and improve shell script conventions Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 2 +- .github/workflows/android-build.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh index 3268c7c..2548613 100755 --- a/.github/scripts/rename-apks.sh +++ b/.github/scripts/rename-apks.sh @@ -7,7 +7,7 @@ set -e BUILD_TYPE=$1 VERSION=$2 COMMIT=$3 -SIGNED=${4:-"false"} +SIGNED=${4:-false} if [ -z "$BUILD_TYPE" ] || [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then echo "Usage: $0 [signed]" diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index f8a3e60..dd48d68 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -101,8 +101,10 @@ jobs: - name: Check if signed id: signing-status shell: bash + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: | - if [ -n "${{ secrets.KEYSTORE_PASSWORD }}" ]; then + if [ -n "$KEYSTORE_PASSWORD" ]; then echo "signed=true" >> $GITHUB_OUTPUT else echo "signed=false" >> $GITHUB_OUTPUT From 9e2dcccadb133a3ee61e50f2f104aaf21bb69901 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:37:45 +0000 Subject: [PATCH 37/52] Trim keystore password to handle whitespace in secrets Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- android/app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 29368fd..bc24916 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -20,7 +20,7 @@ android { } } - val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + val keystorePassword = System.getenv("KEYSTORE_PASSWORD")?.trim() signingConfigs { create("release") { From 2f43b82f736c319a0091b31fe0943e064e57e0fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:37:45 +0000 Subject: [PATCH 38/52] Trim keystore password to handle whitespace in secrets Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 29368fd..bc24916 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { } } - val keystorePassword = System.getenv("KEYSTORE_PASSWORD") + val keystorePassword = System.getenv("KEYSTORE_PASSWORD")?.trim() signingConfigs { create("release") { From 78a209ae6dabc701cc5f9f8a5e90897c9c1d281e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:50:28 +0000 Subject: [PATCH 39/52] Add better error handling and debugging to version extraction step Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index dd48d68..2a53447 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -87,14 +87,33 @@ jobs: id: version-info working-directory: ./android shell: bash + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: | - VERSION=$(./gradlew -q properties | grep -E "^versionName:" | awk '{print $2}') + # Check if gradlew is executable + if [ ! -x ./gradlew ]; then + echo "Error: gradlew is not executable" + exit 1 + fi + + # Try to get properties with error output + echo "Running gradle properties..." + PROPS_OUTPUT=$(./gradlew -q properties 2>&1) || { + echo "Error: gradlew properties failed with exit code $?" + echo "Output: $PROPS_OUTPUT" + exit 1 + } + + # Extract version name + VERSION=$(echo "$PROPS_OUTPUT" | grep -E "^versionName:" | awk '{print $2}') + if [ -z "$VERSION" ]; then echo "Error: Could not extract version name from gradle properties" echo "Gradle properties output:" - ./gradlew -q properties | head -20 + echo "$PROPS_OUTPUT" | head -20 exit 1 fi + echo "Extracted version: $VERSION" echo "version=$VERSION" >> $GITHUB_OUTPUT From bf8759f1a2bcaf5dec502ab55e2916d3e15903e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:15:58 +0000 Subject: [PATCH 40/52] Extract version from built APK instead of running Gradle properties Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 33 +++++++++++------------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 2a53447..aeabb9c 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -83,38 +83,29 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: ./gradlew assembleRelease --stacktrace - - name: Get version name + - name: Get version from APK id: version-info - working-directory: ./android shell: bash - env: - KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: | - # Check if gradlew is executable - if [ ! -x ./gradlew ]; then - echo "Error: gradlew is not executable" - exit 1 - fi + # Extract version from the universal debug APK using aapt + APK_PATH="android/app/build/outputs/apk/debug/app-universal-debug.apk" - # Try to get properties with error output - echo "Running gradle properties..." - PROPS_OUTPUT=$(./gradlew -q properties 2>&1) || { - echo "Error: gradlew properties failed with exit code $?" - echo "Output: $PROPS_OUTPUT" + if [ ! -f "$APK_PATH" ]; then + echo "Error: Universal debug APK not found at $APK_PATH" + ls -la android/app/build/outputs/apk/debug/ || echo "Debug directory does not exist" exit 1 - } + fi - # Extract version name - VERSION=$(echo "$PROPS_OUTPUT" | grep -E "^versionName:" | awk '{print $2}') + # Use aapt to extract version name from APK + VERSION=$(aapt dump badging "$APK_PATH" | grep "versionName" | sed -E "s/.*versionName='([^']+)'.*/\1/") if [ -z "$VERSION" ]; then - echo "Error: Could not extract version name from gradle properties" - echo "Gradle properties output:" - echo "$PROPS_OUTPUT" | head -20 + echo "Error: Could not extract version name from APK" + aapt dump badging "$APK_PATH" | head -20 exit 1 fi - echo "Extracted version: $VERSION" + echo "Extracted version from APK: $VERSION" echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Check if signed From 5619b0347500c16ed2f6a8e363e9da2c59a390bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:17:05 +0000 Subject: [PATCH 41/52] Use wildcard pattern to find universal APK for version extraction Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index aeabb9c..842247c 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -88,14 +88,18 @@ jobs: shell: bash run: | # Extract version from the universal debug APK using aapt - APK_PATH="android/app/build/outputs/apk/debug/app-universal-debug.apk" + # Use wildcard to match the actual APK name (e.g., app-universal-debug.apk) + APK_PATH=$(ls android/app/build/outputs/apk/debug/*universal*.apk 2>/dev/null | head -1) - if [ ! -f "$APK_PATH" ]; then - echo "Error: Universal debug APK not found at $APK_PATH" + if [ -z "$APK_PATH" ] || [ ! -f "$APK_PATH" ]; then + echo "Error: Universal debug APK not found" + echo "Looking in: android/app/build/outputs/apk/debug/" ls -la android/app/build/outputs/apk/debug/ || echo "Debug directory does not exist" exit 1 fi + echo "Using APK: $APK_PATH" + # Use aapt to extract version name from APK VERSION=$(aapt dump badging "$APK_PATH" | grep "versionName" | sed -E "s/.*versionName='([^']+)'.*/\1/") From 4d1a7c36ce4a6f00ec4db7197b40772fd89c8ecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:17:50 +0000 Subject: [PATCH 42/52] Add better error handling for aapt command Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 842247c..cdaac7e 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -100,12 +100,26 @@ jobs: echo "Using APK: $APK_PATH" + # Check if aapt is available + if ! command -v aapt &> /dev/null; then + echo "Error: aapt command not found in PATH" + echo "Make sure Android SDK is properly set up" + exit 1 + fi + # Use aapt to extract version name from APK - VERSION=$(aapt dump badging "$APK_PATH" | grep "versionName" | sed -E "s/.*versionName='([^']+)'.*/\1/") + AAPT_OUTPUT=$(aapt dump badging "$APK_PATH" 2>&1) || { + echo "Error: aapt command failed with exit code $?" + echo "Output: $AAPT_OUTPUT" + exit 1 + } + + VERSION=$(echo "$AAPT_OUTPUT" | grep "versionName" | sed -E "s/.*versionName='([^']+)'.*/\1/") if [ -z "$VERSION" ]; then echo "Error: Could not extract version name from APK" - aapt dump badging "$APK_PATH" | head -20 + echo "aapt output (first 20 lines):" + echo "$AAPT_OUTPUT" | head -20 exit 1 fi From bbdf2b116344a23044acab6f22f3b4b98fca5921 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:18:36 +0000 Subject: [PATCH 43/52] Clarify why we use debug APK for version extraction Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index cdaac7e..855233f 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -87,12 +87,13 @@ jobs: id: version-info shell: bash run: | - # Extract version from the universal debug APK using aapt + # Extract version from a universal APK using aapt + # We use debug APK since it's built first and has the same version as release # Use wildcard to match the actual APK name (e.g., app-universal-debug.apk) APK_PATH=$(ls android/app/build/outputs/apk/debug/*universal*.apk 2>/dev/null | head -1) if [ -z "$APK_PATH" ] || [ ! -f "$APK_PATH" ]; then - echo "Error: Universal debug APK not found" + echo "Error: Universal APK not found in debug directory" echo "Looking in: android/app/build/outputs/apk/debug/" ls -la android/app/build/outputs/apk/debug/ || echo "Debug directory does not exist" exit 1 From ca959f64470150f9ef9776068210cf66bdcee925 Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Thu, 19 Feb 2026 03:25:07 +0800 Subject: [PATCH 44/52] Changed APK naming --- app/build.gradle.kts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc24916..d4cce52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.OutputFile + plugins { alias(libs.plugins.android.application) alias(libs.plugins.ksp) @@ -68,6 +70,24 @@ android { } } +val appExtension = components.findByName("android") + ?: extensions.getByType(com.android.build.gradle.AppExtension::class.java) + +if (appExtension is com.android.build.gradle.AppExtension) { + appExtension.applicationVariants.all { + val variant = this + outputs.all { + val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl + + // Safe filter extraction + val abi = output.getFilter(OutputFile.ABI) ?: "universal" + + // Set the name + output.outputFileName = "kqt-${abi}-${variant.versionName}-${variant.buildType.name}.apk" + } + } +} + kotlin { jvmToolchain(17) compilerOptions { From 2ae4e7d8b9aa5644df02cc80a4d27d0304b3d6ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:35:32 +0000 Subject: [PATCH 45/52] Use Gradle-generated APK names and collect with commit hash Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 94 ++++++++++++----------------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 855233f..fd82ed9 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -83,50 +83,6 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: ./gradlew assembleRelease --stacktrace - - name: Get version from APK - id: version-info - shell: bash - run: | - # Extract version from a universal APK using aapt - # We use debug APK since it's built first and has the same version as release - # Use wildcard to match the actual APK name (e.g., app-universal-debug.apk) - APK_PATH=$(ls android/app/build/outputs/apk/debug/*universal*.apk 2>/dev/null | head -1) - - if [ -z "$APK_PATH" ] || [ ! -f "$APK_PATH" ]; then - echo "Error: Universal APK not found in debug directory" - echo "Looking in: android/app/build/outputs/apk/debug/" - ls -la android/app/build/outputs/apk/debug/ || echo "Debug directory does not exist" - exit 1 - fi - - echo "Using APK: $APK_PATH" - - # Check if aapt is available - if ! command -v aapt &> /dev/null; then - echo "Error: aapt command not found in PATH" - echo "Make sure Android SDK is properly set up" - exit 1 - fi - - # Use aapt to extract version name from APK - AAPT_OUTPUT=$(aapt dump badging "$APK_PATH" 2>&1) || { - echo "Error: aapt command failed with exit code $?" - echo "Output: $AAPT_OUTPUT" - exit 1 - } - - VERSION=$(echo "$AAPT_OUTPUT" | grep "versionName" | sed -E "s/.*versionName='([^']+)'.*/\1/") - - if [ -z "$VERSION" ]; then - echo "Error: Could not extract version name from APK" - echo "aapt output (first 20 lines):" - echo "$AAPT_OUTPUT" | head -20 - exit 1 - fi - - echo "Extracted version from APK: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Check if signed id: signing-status shell: bash @@ -139,19 +95,49 @@ jobs: echo "signed=false" >> $GITHUB_OUTPUT fi - - name: Rename debug APKs - shell: bash - run: ./.github/scripts/rename-apks.sh debug ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} ${{ steps.signing-status.outputs.signed }} - - - name: Rename release APKs + - name: Collect and rename APKs shell: bash - run: ./.github/scripts/rename-apks.sh release ${{ steps.version-info.outputs.version }} ${{ steps.git-info.outputs.sha_short }} ${{ steps.signing-status.outputs.signed }} + run: | + # Create collection directory + mkdir -p apk-collection + + COMMIT_HASH=${{ steps.git-info.outputs.sha_short }} + SIGNED=${{ steps.signing-status.outputs.signed }} + + # Determine suffix based on signing status + SUFFIX="" + if [ "$SIGNED" != "true" ]; then + SUFFIX="-UNSIGNED" + fi + + # Collect debug APKs + for apk in android/app/build/outputs/apk/debug/kqt-*.apk; do + if [ -f "$apk" ]; then + filename=$(basename "$apk") + # Insert commit hash before .apk extension + new_name="${filename%.apk}-${COMMIT_HASH}${SUFFIX}.apk" + cp "$apk" "apk-collection/$new_name" + echo "Collected: $new_name" + fi + done + + # Collect release APKs + for apk in android/app/build/outputs/apk/release/kqt-*.apk; do + if [ -f "$apk" ]; then + filename=$(basename "$apk") + # Insert commit hash before .apk extension + new_name="${filename%.apk}-${COMMIT_HASH}${SUFFIX}.apk" + cp "$apk" "apk-collection/$new_name" + echo "Collected: $new_name" + fi + done + + echo "All collected APKs:" + ls -1 apk-collection/ - name: Upload APKs uses: actions/upload-artifact@v4 with: - name: kqt-apks-${{ steps.version-info.outputs.version }}-${{ steps.git-info.outputs.sha_short }} - path: | - android/app/build/outputs/apk/debug/kqt-*.apk - android/app/build/outputs/apk/release/kqt-*.apk + name: kqt-apks-${{ steps.git-info.outputs.sha_short }} + path: apk-collection/*.apk if-no-files-found: error From 3d8ae8150d5a632c6ec92ea77f4f1d6cef1e4b68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:35:46 +0000 Subject: [PATCH 46/52] Remove obsolete rename-apks.sh script Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/scripts/rename-apks.sh | 56 ---------------------------------- 1 file changed, 56 deletions(-) delete mode 100755 .github/scripts/rename-apks.sh diff --git a/.github/scripts/rename-apks.sh b/.github/scripts/rename-apks.sh deleted file mode 100755 index 2548613..0000000 --- a/.github/scripts/rename-apks.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Script to rename APK files with standard naming convention -# Usage: rename-apks.sh [signed] - -set -e - -BUILD_TYPE=$1 -VERSION=$2 -COMMIT=$3 -SIGNED=${4:-false} - -if [ -z "$BUILD_TYPE" ] || [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then - echo "Usage: $0 [signed]" - exit 1 -fi - -APK_DIR="android/app/build/outputs/apk/$BUILD_TYPE" - -if [ ! -d "$APK_DIR" ]; then - echo "Error: APK directory does not exist: $APK_DIR" - echo "Make sure the build has completed successfully before renaming APKs" - exit 1 -fi - -cd "$APK_DIR" - -# Enable nullglob to handle case where no APK files exist -shopt -s nullglob - -# Determine suffix based on signing status -SUFFIX="" -if [ "$SIGNED" != "true" ]; then - SUFFIX="-UNSIGNED" -fi - -for apk in *.apk; do - if [[ "$apk" == *"arm64-v8a"* ]]; then - mv "$apk" "kqt-$BUILD_TYPE-arm64-v8a-$VERSION-$COMMIT$SUFFIX.apk" - continue - elif [[ "$apk" == *"armeabi-v7a"* ]]; then - mv "$apk" "kqt-$BUILD_TYPE-armeabi-v7a-$VERSION-$COMMIT$SUFFIX.apk" - continue - elif [[ "$apk" == *"universal"* ]]; then - mv "$apk" "kqt-$BUILD_TYPE-universal-$VERSION-$COMMIT$SUFFIX.apk" - continue - else - echo "Warning: Unexpected APK file does not match known patterns: $apk" - fi -done - -echo "Renamed APKs in $BUILD_TYPE:" -if ls *.apk >/dev/null 2>&1; then - ls -1 *.apk -else - echo "Warning: No APK files found in $APK_DIR" -fi From 48843ae796914b235dfe3615887ce9bd3b5bb6cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:36:22 +0000 Subject: [PATCH 47/52] Quote shell variable assignments for safety Co-authored-by: CircuitCoder <2051572+CircuitCoder@users.noreply.github.com> --- .github/workflows/android-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index fd82ed9..091ccfe 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -101,8 +101,8 @@ jobs: # Create collection directory mkdir -p apk-collection - COMMIT_HASH=${{ steps.git-info.outputs.sha_short }} - SIGNED=${{ steps.signing-status.outputs.signed }} + COMMIT_HASH="${{ steps.git-info.outputs.sha_short }}" + SIGNED="${{ steps.signing-status.outputs.signed }}" # Determine suffix based on signing status SUFFIX="" From 4f132ca10be9c1f13324dcf047aa238e2801567f Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Thu, 19 Feb 2026 03:44:12 +0800 Subject: [PATCH 48/52] Modify gradle config again --- app/build.gradle.kts | 50 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d4cce52..62810a4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,22 +70,50 @@ android { } } -val appExtension = components.findByName("android") - ?: extensions.getByType(com.android.build.gradle.AppExtension::class.java) +tasks.register("collectAndRenameApks") { + description = "Copies, flattens, and renames APKs to the upload directory" + group = "distribution" -if (appExtension is com.android.build.gradle.AppExtension) { - appExtension.applicationVariants.all { - val variant = this - outputs.all { - val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl + // Source: The standard output directory + from(layout.buildDirectory.dir("outputs/apk")) - // Safe filter extraction - val abi = output.getFilter(OutputFile.ABI) ?: "universal" + // Destination: Your upload folder + into(layout.buildDirectory.dir("outputs/collected")) - // Set the name - output.outputFileName = "kqt-${abi}-${variant.versionName}-${variant.buildType.name}.apk" + include("**/*.apk") + + // Flattening and Renaming Logic + eachFile { + // 'this' is a FileCopyDetails object + // The 'path' property includes the relative path, e.g., "release/app-x86-release.apk" + // The 'name' property is just the filename, e.g., "app-x86-release.apk" + + // Regex to parse the standard Gradle output name: "app-[abi]-[buildType].apk" + // Pattern matches: "app-" followed by (ABI) followed by "-" followed by (BuildType) + val matcher = "(.*)-(.*)-(.*)\\.apk".toRegex().matchEntire(name) + val android = project.extensions.getByType(com.android.build.gradle.BaseExtension::class.java) + val appVersionName = android.defaultConfig.versionName + val appName = "kqt" + + if (matcher != null) { + val (prefix, abi, buildType) = matcher.destructured + + // Construct your custom name: some-name-[abi]-[version]-[buildType].apk + // We ignore the original prefix ("app") and use your 'appName' variable + path = "$appName-$abi-$appVersionName-$buildType.apk" + } else { + // Fallback for files that don't match the split pattern (like universal if named differently) + // Tries to insert the version name before the .apk extension + path = name.replace(".apk", "-$appVersionName.apk") } } + + // Ensure we don't copy the empty "debug"/"release" folders + includeEmptyDirs = false +} + +tasks.named("assemble") { + finalizedBy("collectAndRenameApks") } kotlin { From 22fe62dca70dfe7624f34808e67e4c8094f1e0f0 Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Thu, 19 Feb 2026 03:50:50 +0800 Subject: [PATCH 49/52] Updated workflow to match gradle build output --- .github/workflows/android-build.yml | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 091ccfe..bbe3f25 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -99,7 +99,7 @@ jobs: shell: bash run: | # Create collection directory - mkdir -p apk-collection + mkdir -p upload COMMIT_HASH="${{ steps.git-info.outputs.sha_short }}" SIGNED="${{ steps.signing-status.outputs.signed }}" @@ -110,34 +110,23 @@ jobs: SUFFIX="-UNSIGNED" fi - # Collect debug APKs - for apk in android/app/build/outputs/apk/debug/kqt-*.apk; do + # Collect APKs + for apk in android/app/build/outputs/collected/kqt-*.apk; do if [ -f "$apk" ]; then filename=$(basename "$apk") # Insert commit hash before .apk extension new_name="${filename%.apk}-${COMMIT_HASH}${SUFFIX}.apk" - cp "$apk" "apk-collection/$new_name" - echo "Collected: $new_name" - fi - done - - # Collect release APKs - for apk in android/app/build/outputs/apk/release/kqt-*.apk; do - if [ -f "$apk" ]; then - filename=$(basename "$apk") - # Insert commit hash before .apk extension - new_name="${filename%.apk}-${COMMIT_HASH}${SUFFIX}.apk" - cp "$apk" "apk-collection/$new_name" + cp "$apk" "upload/$new_name" echo "Collected: $new_name" fi done echo "All collected APKs:" - ls -1 apk-collection/ + ls -1 upload/ - name: Upload APKs uses: actions/upload-artifact@v4 with: name: kqt-apks-${{ steps.git-info.outputs.sha_short }} - path: apk-collection/*.apk + path: upload/*.apk if-no-files-found: error From 1b8cdca2a96fdf3016af8bf6eb2e501e4b2492c5 Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Thu, 19 Feb 2026 04:03:52 +0800 Subject: [PATCH 50/52] Manually run collection task --- .github/workflows/android-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index bbe3f25..a76202d 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -83,6 +83,10 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: ./gradlew assembleRelease --stacktrace + - name: Collect and rename APKs + working-directory: ./android + run: ./gradlew collectAndRenameApks --stacktrace + - name: Check if signed id: signing-status shell: bash From 700e6ed4f624718649fb1ce84239110ab4941e92 Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Thu, 19 Feb 2026 04:16:25 +0800 Subject: [PATCH 51/52] Fixing collection task --- android/app/build.gradle.kts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 62810a4..bb0bf71 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,6 +5,8 @@ plugins { alias(libs.plugins.ksp) } +val appVersionName = "1.0.0" + android { namespace = "plus.meow.kqt" compileSdk = 36 @@ -14,7 +16,7 @@ android { minSdk = 24 targetSdk = 36 versionCode = 1 - versionName = "1.0" + versionName = appVersionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" ndk { @@ -84,26 +86,12 @@ tasks.register("collectAndRenameApks") { // Flattening and Renaming Logic eachFile { - // 'this' is a FileCopyDetails object - // The 'path' property includes the relative path, e.g., "release/app-x86-release.apk" - // The 'name' property is just the filename, e.g., "app-x86-release.apk" - - // Regex to parse the standard Gradle output name: "app-[abi]-[buildType].apk" - // Pattern matches: "app-" followed by (ABI) followed by "-" followed by (BuildType) val matcher = "(.*)-(.*)-(.*)\\.apk".toRegex().matchEntire(name) - val android = project.extensions.getByType(com.android.build.gradle.BaseExtension::class.java) - val appVersionName = android.defaultConfig.versionName - val appName = "kqt" if (matcher != null) { val (prefix, abi, buildType) = matcher.destructured - - // Construct your custom name: some-name-[abi]-[version]-[buildType].apk - // We ignore the original prefix ("app") and use your 'appName' variable - path = "$appName-$abi-$appVersionName-$buildType.apk" + path = "kqt-$abi-$appVersionName-$buildType.apk" } else { - // Fallback for files that don't match the split pattern (like universal if named differently) - // Tries to insert the version name before the .apk extension path = name.replace(".apk", "-$appVersionName.apk") } } @@ -147,4 +135,4 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file +} From 445cc52a869bf696b04dc7a06c0cc410aa2dbc6d Mon Sep 17 00:00:00 2001 From: Liu Xiaoyi Date: Thu, 19 Feb 2026 04:56:47 +0800 Subject: [PATCH 52/52] Changed APK regex --- android/app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index bb0bf71..f0057a9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -86,7 +86,7 @@ tasks.register("collectAndRenameApks") { // Flattening and Renaming Logic eachFile { - val matcher = "(.*)-(.*)-(.*)\\.apk".toRegex().matchEntire(name) + val matcher = "(.*)-(arm64-v8a|armeabi-v7a|universal)-(.*)\\.apk".toRegex().matchEntire(name) if (matcher != null) { val (prefix, abi, buildType) = matcher.destructured