From 4e3969c56c4d43a987bc5b95637c532a9bac43c3 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:29:43 +0200 Subject: [PATCH 1/4] ci: add CI/CD workflows, dependabot, and release signing - CI workflow: build debug APK + lint on push/PR to main - Release workflow: signed AAB+APK on version tags (v*) - Dependabot: weekly Gradle, monthly GitHub Actions, grouped updates - GitGuardian: suppress false positives on build.gradle.kts - build.gradle.kts: dynamic VERSION_NAME/VERSION_CODE from Gradle properties, CASHPILOT_* signing config, buildConfig enabled, modern kotlin compilerOptions DSL --- .gitguardian.yml | 2 ++ .github/dependabot.yml | 53 ++++++++++++++++++++++++++++ .github/workflows/ci.yml | 34 ++++++++++++++++++ .github/workflows/release.yml | 65 +++++++++++++++++++++++++++++++++++ app/build.gradle.kts | 38 +++++++++++++++++--- 5 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 .gitguardian.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.gitguardian.yml b/.gitguardian.yml new file mode 100644 index 0000000..c156c6a --- /dev/null +++ b/.gitguardian.yml @@ -0,0 +1,2 @@ +paths-ignore: + - "app/build.gradle.kts" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c073aaa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,53 @@ +version: 2 + +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + commit-message: + prefix: "deps" + labels: + - "dependencies" + - "automated" + groups: + minor-and-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + gradle-security: + applies-to: security-updates + patterns: + - "*" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + commit-message: + prefix: "ci" + labels: + - "ci" + - "automated" + groups: + actions-minor-and-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + actions-security: + applies-to: security-updates + patterns: + - "*" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..790ee12 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build debug APK + run: ./gradlew assembleDebug + + - name: Run lint + run: ./gradlew lintDebug + + - name: Upload debug APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: app/build/outputs/apk/debug/app-debug.apk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5110e90 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Compute version from tag + id: version + run: | + TAG="${GITHUB_REF#refs/tags/}" + VERSION="${TAG#v}" + # versionCode: major*10000 + minor*100 + patch (e.g. 1.2.3 -> 10203) + IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" + CODE=$(( ${MAJOR:-0} * 10000 + ${MINOR:-0} * 100 + ${PATCH:-0} )) + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "name=$VERSION" >> "$GITHUB_OUTPUT" + echo "code=$CODE" >> "$GITHUB_OUTPUT" + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Decode keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.keystore + + - name: Build release AAB + run: ./gradlew bundleRelease -PVERSION_NAME=${{ steps.version.outputs.name }} -PVERSION_CODE=${{ steps.version.outputs.code }} + env: + CASHPILOT_KEYSTORE_PATH: release.keystore + CASHPILOT_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + CASHPILOT_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + CASHPILOT_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + + - name: Build release APK + run: ./gradlew assembleRelease -PVERSION_NAME=${{ steps.version.outputs.name }} -PVERSION_CODE=${{ steps.version.outputs.code }} + env: + CASHPILOT_KEYSTORE_PATH: release.keystore + CASHPILOT_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + CASHPILOT_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + CASHPILOT_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: ${{ steps.version.outputs.tag }} + generate_release_notes: true + files: | + app/build/outputs/bundle/release/app-release.aab + app/build/outputs/apk/release/app-release.apk diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce692c2..60798f6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,31 @@ android { applicationId = "com.cashpilot.android" minSdk = 26 // Android 8.0 — NotificationListenerService, NetworkStatsManager targetSdk = 35 - versionCode = 1 - versionName = "0.1.0" + + val ciVersionName = findProperty("VERSION_NAME") as String? ?: "0.1.0" + val ciVersionCode = (findProperty("VERSION_CODE") as String?)?.toIntOrNull() ?: 1 + versionCode = ciVersionCode + versionName = ciVersionName + } + + signingConfigs { + create("release") { + val keystorePath = findProperty("CASHPILOT_KEYSTORE_PATH") as String? + ?: System.getenv("CASHPILOT_KEYSTORE_PATH") + val keystorePass = findProperty("CASHPILOT_KEYSTORE_PASSWORD") as String? + ?: System.getenv("CASHPILOT_KEYSTORE_PASSWORD") + val keyAliasValue = findProperty("CASHPILOT_KEY_ALIAS") as String? + ?: System.getenv("CASHPILOT_KEY_ALIAS") + val keyPass = findProperty("CASHPILOT_KEY_PASSWORD") as String? + ?: System.getenv("CASHPILOT_KEY_PASSWORD") + + if (keystorePath != null && keystorePass != null && keyAliasValue != null && keyPass != null) { + storeFile = file(keystorePath) + storePassword = keystorePass + keyAlias = keyAliasValue + keyPassword = keyPass + } + } } buildTypes { @@ -25,6 +48,10 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + val releaseConfig = signingConfigs.findByName("release") + if (releaseConfig?.storeFile != null) { + signingConfig = releaseConfig + } } } @@ -33,11 +60,14 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } } buildFeatures { + buildConfig = true compose = true } } From f45459e0aee75f84c64677096c2886310339770d Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:49:35 +0200 Subject: [PATCH 2/4] fix: settings.gradle.kts API + remove keystore after release build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix dependencyResolution → dependencyResolutionManagement with RepositoriesMode.FAIL_ON_PROJECT_REPOS (matches Pumperly) - Remove keystore from disk after both Gradle steps complete --- .github/workflows/release.yml | 4 ++++ settings.gradle.kts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5110e90..b260d12 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,6 +54,10 @@ jobs: CASHPILOT_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} CASHPILOT_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + - name: Remove keystore + if: always() + run: rm -f app/release.keystore + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/settings.gradle.kts b/settings.gradle.kts index 3aa8928..99c09a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,9 @@ pluginManagement { } } -dependencyResolution { +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() From 30d1f0921956f38dff6f76cf64310c3c9e5c4233 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:55:34 +0200 Subject: [PATCH 3/4] fix: use containsKey instead of in on ConcurrentHashMap Kotlin's `in` operator on ConcurrentHashMap maps to containsValue, not containsKey (KT-18053). Use explicit containsKey for correct package lookup. --- .../com/cashpilot/android/service/AppNotificationListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cashpilot/android/service/AppNotificationListener.kt b/app/src/main/java/com/cashpilot/android/service/AppNotificationListener.kt index 0693088..8ad0789 100644 --- a/app/src/main/java/com/cashpilot/android/service/AppNotificationListener.kt +++ b/app/src/main/java/com/cashpilot/android/service/AppNotificationListener.kt @@ -55,7 +55,7 @@ class AppNotificationListener : NotificationListenerService() { fun isAppNotificationActive(packageName: String): Boolean { if (!connected.get()) return false - return packageName in activeNotifications_ + return activeNotifications_.containsKey(packageName) } fun isConnected(): Boolean = connected.get() From adf3e4027ae506ef6b01e3e2da9831dff74643f7 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 4 Apr 2026 10:01:04 +0200 Subject: [PATCH 4/4] fix: add lint baseline for expected ProtectedPermissions errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PACKAGE_USAGE_STATS and READ_NETWORK_USAGE_HISTORY are system permissions accessed via UsageStatsManager after user grants access in settings — lint ProtectedPermissions errors are expected. --- app/build.gradle.kts | 4 ++++ app/lint-baseline.xml | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 app/lint-baseline.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 60798f6..f3dbfbd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,6 +66,10 @@ android { } } + lint { + baseline = file("lint-baseline.xml") + } + buildFeatures { buildConfig = true compose = true diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000..d9e2dd4 --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + +