From efba360d8aa3e96f346024e12dca46ac6a5494a3 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Tue, 23 Jun 2026 05:46:11 +0300 Subject: [PATCH] fix(ci): add release-compile safeguard to catch debugImplementation regressions (#336) - Reverts okhttp.logging dependency from debugImplementation back to implementation so it's on the release classpath - Adds compileReleaseKotlin CI job to catch release-variant compilation errors on every PR - Fixes signing config to allow compileReleaseKotlin without keystore env vars (graceful fallback instead of hard require()) - Updates AGENTS.md with dependency scoping pitfall and CI docs Co-authored-by: M57 --- .github/workflows/android.yml | 42 +++++++++++++++++++++++++++++++---- AGENTS.md | 7 ++++++ app/build.gradle.kts | 26 +++++++++++++--------- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 95a3a0d4..c4aef066 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -191,7 +191,40 @@ jobs: retention-days: 7 # ───────────────────────────────────────────────────────────────────── - # Job 5: Instrumented tests — runs on Android emulator. + # Job 5: Release Compile — verifies release variant compiles. + # Catches issues like debugImplementation-scoped dependencies that + # main source code depends on (e.g. okhttp3.logging.HttpLoggingInterceptor). + # debug-only CI jobs (ktlint, lint, tests) won't catch these. + # ───────────────────────────────────────────────────────────────────── + release-compile: + name: Release Compile Check + needs: [ktlint, android-lint, unit-tests] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + with: + validate-wrappers: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTED_KEY }} + + - name: Grant Execute Permission for Gradlew + run: chmod +x gradlew + + - name: Compile release Kotlin + run: ./gradlew compileReleaseKotlin + + # ───────────────────────────────────────────────────────────────────── + # Job 6: Instrumented tests — runs on Android emulator. # Required for Compose UI tests (TEST-09, TEST-14) and Navigation3 tests # that need a real Activity lifecycle. Cannot run on JVM. # ───────────────────────────────────────────────────────────────────── @@ -256,11 +289,11 @@ jobs: retention-days: 7 # ───────────────────────────────────────────────────────────────────── - # Job 6: PR status summary — aggregates all checks. + # Job 7: PR status summary — aggregates all checks. # ───────────────────────────────────────────────────────────────────── ci-summary: name: CI Summary - needs: [ktlint, android-lint, unit-tests, build] + needs: [ktlint, android-lint, unit-tests, build, release-compile] runs-on: ubuntu-latest timeout-minutes: 2 if: always() @@ -271,9 +304,10 @@ jobs: LINT_RESULT: ${{ needs.android-lint.result }} TEST_RESULT: ${{ needs.unit-tests.result }} BUILD_RESULT: ${{ needs.build.result }} + RELEASE_COMPILE_RESULT: ${{ needs.release-compile.result }} run: | FAILED=0 - for entry in "ktlint:$KTLINT_RESULT" "android-lint:$LINT_RESULT" "unit-tests:$TEST_RESULT" "build:$BUILD_RESULT"; do + for entry in "ktlint:$KTLINT_RESULT" "android-lint:$LINT_RESULT" "unit-tests:$TEST_RESULT" "build:$BUILD_RESULT" "release-compile:$RELEASE_COMPILE_RESULT"; do name="${entry%%:*}" result="${entry#*:}" echo "::notice::$name = $result" diff --git a/AGENTS.md b/AGENTS.md index c0d4e47f..aabcc3ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,7 @@ find app/src -name '*.kt' | xargs ./ktlint --format # fix all | `android-lint` | Android Lint | | `unit-tests` | JUnit | | `build` | assembleDebug (gated by the 3 above) | +| `release-compile` | compileReleaseKotlin — catches release-variant compilation issues (e.g. debugImplementation-scoped deps referenced in main source) | | `ci-summary` | Aggregator (`if: always()`) — branch protection gates on THIS check | Every Gradle job validates `gradle-wrapper.jar` and uses the remote build cache @@ -145,6 +146,12 @@ gh pr create --title "fix(#N): description" --body "Closes #N" `val` at the composable scope first. - Don't add new screens without checking if an existing one already covers the functionality (19+ screens exist). Extend rather than duplicate. +- **⚠ Never scope a dependency to `debugImplementation` if its import is used in + `main/` source code.** The CI `release-compile` job catches this, but save the + cycle. `okhttp3.logging.HttpLoggingInterceptor` is the classic example — it's + imported in `ApiClient.kt` (main source) so it must be `implementation`, not + `debugImplementation`. If you want it debug-only, wrap the usage in + `if (BuildConfig.DEBUG)` or extract it behind an interface. ## Project Layout diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8f5b9cf4..57bd1fad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,15 +41,21 @@ android { val alias = System.getenv("KEY_ALIAS") val keyPass = System.getenv("KEY_PASSWORD") - require(!storePath.isNullOrEmpty()) { "KEYSTORE_PATH environment variable is not set" } - require(!storePass.isNullOrEmpty()) { "KEYSTORE_PASSWORD environment variable is not set" } - require(!alias.isNullOrEmpty()) { "KEY_ALIAS environment variable is not set" } - require(!keyPass.isNullOrEmpty()) { "KEY_PASSWORD environment variable is not set" } - - storeFile = file(storePath) - storePassword = storePass - keyAlias = alias - keyPassword = keyPass + if (storePath != null && storePass != null && alias != null && keyPass != null) { + storeFile = file(storePath) + storePassword = storePass + keyAlias = alias + keyPassword = keyPass + } else { + // Env vars missing — signing deferred to release workflow. + // Don't hard-require(): that would break compileReleaseKotlin + // in CI where keystore secrets aren't set. + logger.warn("Release signing config missing env vars — signing deferred to release workflow") + storeFile = file("dummy.keystore") + storePassword = "dummy" + keyAlias = "dummy" + keyPassword = "dummy" + } } else { // Dummy values for evaluation configuration during non-release builds storeFile = file("dummy.keystore") @@ -128,7 +134,7 @@ dependencies { // Networking implementation(libs.okhttp) - debugImplementation(libs.okhttp.logging) + implementation(libs.okhttp.logging) implementation(libs.retrofit) implementation(libs.retrofit.converter.gson) implementation(libs.gson)