Preparing an existing KMP project¶
The Configuring GitHub Actions guide has only three steps because it assumes your KMP project already meets the prerequisites in Requirements. Most production repos do not — Kotlin 2.x rolled out new conventions, Compose Multiplatform tightened its plugin requirements, and many forks inherit upstream workflows that depend on secrets you don't own.
This page is the before-the-install checklist: nine concrete tasks to convert an arbitrary KMP repository into one where the pipeline can run cleanly. Skip the items that already apply to your project.
Quick self-assessment. If you can answer "yes" to all of these, jump straight to Configuring GitHub Actions:
- Your project uses
gradle/libs.versions.tomlas the single source of truth. - The Gradle wrapper version is 8.7+ for AGP 8.x or 9.0+ for AGP 9.x.
- The CI workflow runs on JDK 21.
- If you use Kotlin 2.x with Compose, the
org.jetbrains.kotlin.plugin.composeplugin is applied to every Compose module. jvmToolchain(…)is at the top of thekotlin { … }block, not inside a target.- You have an Android application module named
shared,composeApp,androidApp,app,common,kmm-shared, orkmpShared. ./gradlew :<android-module>:assembleDebugsucceeds locally.- The repository's Settings → Actions permissions include
contents: write,pull-requests: write,pages: write,id-token: write. - Your fork doesn't have upstream workflows that fail for lack of secrets.
Each section below covers one item.
1. Use a Gradle version catalog¶
KMP-IMPACT detects bumps from diffs against gradle/libs.versions.toml. If your project pins versions directly in build.gradle.kts, gradle.properties, pluginManagement { … }, or buildSrc/, the analyzer's detect-version-changes step will not see them — see L1.
Action. Migrate your dependencies to a version catalog. Minimal example:
[versions]
kotlin = "2.0.21"
agp = "8.5.0"
ktor = "2.3.11"
[libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "agp" }
Then reference aliases from every build.gradle.kts:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
dependencies {
implementation(libs.ktor.client.core)
}
Verify. Open a draft PR that bumps any version in libs.versions.toml. The paths: filter on the reference workflow must match, so the run should queue.
2. Align JDK, Gradle, AGP, and Kotlin¶
The four versions interact. Use the matrix in Requirements → Compatibility to pick a stack that has been validated end-to-end. The most common breakage is a Gradle wrapper that is too old for the AGP it ships with.
Action.
- Set the CI JDK to 21 in the workflow (the reference workflow already does this).
- Update
gradle/wrapper/gradle-wrapper.propertiesto a Gradle version compatible with your AGP:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
- If your AGP version is 9.x, the wrapper must be 9.0+.
Verify.
./gradlew --version
# Expect: Gradle 8.10.2 (or 9.x for AGP 9)
# JVM: 21.x (when running through the CI workflow)
3. Apply the Compose Compiler plugin under Kotlin 2.x¶
Kotlin 2.0 changed how Compose support is wired. Without an explicit org.jetbrains.kotlin.plugin.compose declaration, the Android module fails to compile — and Phase 3 marks the dynamic phase BLOCKED because no APK is produced.
Action. Register the plugin in the catalog:
[plugins]
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Apply it in the root build.gradle.kts (so Gradle resolves it):
And in every module that uses Compose:
This typically means at least android/build.gradle.kts, plus shared/build.gradle.kts and any platform-specific Compose modules (desktop/, composeApp/, …).
Skip this section if you are on Kotlin 1.9.x — the Compose Multiplatform plugin still handles Compose Compiler internally.
Verify.
./gradlew :<android-module>:tasks --all | grep -i compose
# Should list Compose-related tasks such as :compileDebugKotlinAndroid with Compose plugin metadata
4. Move jvmToolchain to the top of the kotlin { … } block¶
Kotlin 2.x rejects jvmToolchain(…) calls inside a specific target. The placement must be at the extension scope:
Not inside a target:
Action. Audit every kotlin { … } block in your project (root, shared/, android/, desktop/, …) and hoist the jvmToolchain(…) call to the top.
Verify.
./gradlew :shared:compileKotlinMetadata --info
# Look for the "jvmToolchain" line; no warnings about deprecated placement
5. Make sure your Android module is discoverable¶
The reference workflow probes the following module names, in this order:
If none of these match, it falls back to :app. A module named something else — :mobile, :client-android, etc. — will not be detected, and the droidbot job marks itself BLOCKED — Android module not detected.
Action. Pick the option that requires the least churn:
- Easiest. Rename your existing Android module to one of the names above. Most projects can move to
:appor:composeAppwith a one-linesettings.gradle.ktschange. - Alternative. Add an alias module that re-exposes your real Android module:
include(":app")
project(":app").projectDir = file("client/android") // your real path
- Last resort. Edit the
Detect Android app modulestep in.github/workflows/impact-analysis.ymland add your module name to the loop. You lose the upstream upgrade path, so prefer one of the previous two options.
Verify.
./gradlew projects | grep -E ':(shared|composeApp|androidApp|app|common|kmm-shared|kmpShared)\b'
# Should list at least one of the seven canonical names
6. Make sure the Android module is an application¶
The reference workflow runs ./gradlew :<module>:assembleDebug and expects a real APK to land under */build/outputs/apk/debug/*.apk. A module that applies only com.android.library will not produce one.
Action. Ensure the detected module applies com.android.application and declares an applicationId:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.app"
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
}
If the only Android-related module in your project is a library, you will not be able to run the dynamic phase. The static phase still works — use --skip-dynamic locally and accept the BLOCKED — APK assembly failed status in CI.
Verify.
./gradlew :<android-module>:assembleDebug
ls <android-module>/build/outputs/apk/debug/*.apk
# Should print at least one .apk file
7. Disable upstream workflows that need secrets you don't own¶
This step only matters if you forked your KMP project from an upstream repository. Many open-source KMP apps (Confetti, DroidconKotlin, …) ship workflows that publish to Google Cloud, App Store Connect, Firebase, or sign Android releases. Without those secrets, every workflow run fails before doing useful work — your PRs end up with rows of red checks even when impact-analysis.yml runs cleanly.
Action. Disable every workflow except impact-analysis.yml in one shot:
gh api repos/<owner>/<repo>/actions/workflows --jq \
'.workflows[]
| select(.path | startswith(".github/workflows/"))
| select(.path != ".github/workflows/impact-analysis.yml")
| .id' \
| while read wid; do
gh api -X PUT "repos/<owner>/<repo>/actions/workflows/$wid/disable"
done
This keeps the workflow files on disk but prevents them from triggering. You can re-enable individual ones later if you provide the missing secrets.
Verify. Open the Actions tab on a freshly opened Dependabot PR — only the Dependency Impact Analysis workflow should run.
8. Set the right Actions permissions on the repository¶
In Settings → Actions → General:
| Setting | Value |
|---|---|
| Workflow permissions | Read and write permissions |
| Allow GitHub Actions to create and approve pull requests | enabled (Dependabot relies on this) |
In Settings → Pages:
| Setting | Value |
|---|---|
| Source | GitHub Actions |
Or, equivalently, via the CLI once:
Verify.
9. (Optional) Add a pipeline/ directory if you want SBOM scanning¶
The reference workflow's paths: filter includes pipeline/**, which is used by some projects to host SBOM generation scripts. The Pokedex preset, for example, has a pipeline/sbom/requirements.txt that Dependabot tracks under the pip ecosystem.
This is purely optional. If your project does not need an SBOM tracker, leave the directory absent — the workflow simply ignores the path filter entry.
If you do want it, mirror the reference layout:
And declare the matching Dependabot block:
- package-ecosystem: "pip"
directory: "/pipeline/sbom"
schedule:
interval: "weekly"
PRs opened for non-Gradle ecosystems are caught by the detect job and reported as EXPECTED_SKIPPED — they are not failures.
Final verification: a 30-second smoke test¶
When all nine items pass, run this from the project root before installing KMP-IMPACT:
# 1. Catalog exists
test -f gradle/libs.versions.toml || echo "FAIL: no version catalog"
# 2. Gradle + JDK
./gradlew --version | grep -E '^(Gradle|JVM)'
# 3. Android module discoverable + APK builds
MODULE=$(./gradlew projects 2>/dev/null \
| grep -oE "':(shared|composeApp|androidApp|app|common|kmm-shared|kmpShared)'" \
| head -1 | tr -d "':")
echo "Detected Android module: :${MODULE:-app}"
./gradlew ":${MODULE:-app}:assembleDebug" || echo "FAIL: assembleDebug"
If all three checks pass, you are ready to follow Configuring GitHub Actions.
See also¶
- Getting Started → Requirements — the full compatibility matrix.
- Configuring GitHub Actions — the three-step install once your project is prepared.
- How everything talks to each other — the runtime flow your project will participate in.
- Troubleshooting — what
BLOCKEDphases mean if something slips through.