| name | kmp-ci-cd |
| description | GitHub Actions CI/CD for Kotlin Multiplatform — matrix builds (ubuntu for JVM/Android/JS, macOS for iOS), caching, Maven Central / SPM / CocoaPods publishing. Use when wiring CI or a release pipeline. |
KMP CI/CD
Instructions
KMP CI needs both Linux (for Android, JVM, JS, Wasm) and macOS (for iOS). Split the matrix so macOS runners — the expensive ones — only do what they must.
1. Matrix workflow
name: CI
on:
push: { branches: [ main ] }
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
jvm-and-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew :shared:jvmTest :shared:testDebugUnitTest :shared:jsTest --no-daemon --stacktrace
ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- uses: gradle/actions/setup-gradle@v4
- run: sudo xcode-select -s /Applications/Xcode_15.4.app
- run: ./gradlew :shared:iosSimulatorArm64Test --no-daemon --stacktrace
- run: ./gradlew :shared:assembleSharedReleaseXCFramework --no-daemon
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew ktlintCheck detekt --no-daemon
2. Caching
gradle/actions/setup-gradle@v4 already caches the Gradle build cache, configuration cache, and wrapper. For Kotlin/Native, add:
- name: Cache konan
uses: actions/cache@v4
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml') }}
restore-keys: konan-${{ runner.os }}-
~/.konan holds the Kotlin/Native toolchain and precompiled platform libs — ~2 GB, but saves 5-10 min per macOS job.
3. Android app build + instrumentation
android-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew :androidApp:assembleRelease --no-daemon
- uses: actions/upload-artifact@v4
with: { name: android-apk, path: androidApp/build/outputs/apk/release/*.apk }
4. Publishing to Maven Central
publish:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- uses: gradle/actions/setup-gradle@v4
- env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --no-configuration-cache
Use the vanniktech Maven publish plugin, which handles all KMP publications (kotlinMultiplatform, androidRelease, iosArm64, etc.) out of the box.
5. XCFramework → Swift Package Manager
Publish the release XCFramework as a GitHub release asset and point a Package.swift at it:
let package = Package(
name: "Shared",
platforms: [.iOS(.v14)],
products: [.library(name: "Shared", targets: ["Shared"])],
targets: [
.binaryTarget(
name: "Shared",
url: "https://github.com/me/my-kmp/releases/download/v1.2.3/Shared.xcframework.zip",
checksum: "…compute via `swift package compute-checksum`…"
),
]
)
Automate the release:
- name: Zip XCFramework
run: |
cd shared/build/XCFrameworks/release
zip -r Shared.xcframework.zip Shared.xcframework
- name: Compute checksum
id: sum
run: echo "sum=$(swift package compute-checksum shared/build/XCFrameworks/release/Shared.xcframework.zip)" >> $GITHUB_OUTPUT
- uses: softprops/action-gh-release@v2
with:
files: shared/build/XCFrameworks/release/Shared.xcframework.zip
body: |
XCFramework checksum: `${{ steps.sum.outputs.sum }}`
Then open a PR to the SPM repo bumping URL + checksum.
6. CocoaPods publishing (only if consumers still use pods)
- run: ./gradlew :shared:podPublishReleaseXCFramework --no-daemon
- run: pod trunk push shared/build/cocoapods/publish/release/Shared.podspec
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
7. Release hygiene
- Tag with semver (
v1.2.3); the publish workflow keys on tags.
- Fail builds on XCFramework size growth beyond a threshold (e.g. +10%).
- Keep one linting job (
ktlintCheck, detekt) required for PR merges.
- Gradle configuration cache should be on in CI; if a publish task breaks it, pass
--no-configuration-cache only to that job.
Checklist