kmp-ci-github-actions
Skill15 starsChanged 22 days agoApache-2.0View on GitHub
Use when setting up, fixing or speeding up GitHub Actions CI for a Kotlin Multiplatform (Android + iOS) project - .github/workflows YAML, "add CI", macos-latest or macos-15-intel runners, iosSimulatorArm64Test in CI, ~/.konan re-downloading every run, setup-gradle caching, Xcode version selection, slow or expensive macOS jobs, test or Kover reports as artifacts, or a release workflow with signing secrets.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- ronjunevaldoz/kmp-agent-skillstheir own version, first seen Aug 2, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- ronjunevaldoz/kmp-agent-skills★ 5same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- android-to-kmp-migrationSkill
- compose-design-system-componentSkill
- compose-multiplatform-localizationSkill
- compose-multiplatform-navigationSkill
- compose-multiplatform-screenSkill
- figma-to-mobile-uiSkill
- kmp-api-endpointSkill
- kmp-build-checkSkill
- kmp-build-failure-triageSkill
- kmp-code-reviewSkill
- kmp-crash-triageSkill
- kmp-data-layerSkill
- kmp-dev-machine-cleanupSkill
- kmp-expect-actualSkill
- kmp-feature-moduleSkill
- kmp-flaky-test-triageSkill
- kmp-gradle-version-catalogSkill
- kmp-koin-diSkill
- kmp-module-healthSkill
- kmp-room-databaseSkill
- kmp-secure-storageSkill
- kmp-swiftui-interopSkill
- kmp-test-coverage-backfillSkill
- kmp-testing-patternsSkill
- kmp-xcframework-distributionSkill
- mobile-accessibility-test-idsSkill
- mobile-bug-fixSkill
- mobile-feature-implementSkill
- mobile-feature-planSkill
- mobile-performance-fixSkill
- mobile-platform-paritySkill
- mobile-releaseSkill
- mobile-security-reviewSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 65
Run android and ios in parallel.
- Very specific: line 111
If the Kotlin version is not in libs.versions.toml, change the sed to read wherever it lives.
- Distinctive: line 122
Check the runner-images list for current labels and the Xcode versions each image carries.
- Very specific: line 148
Use distinct artifact names per job.
- Very specific: line 166
Never trigger it from pull_request or pull_request_target.
- Very specific: line 170
Keep the R8 mapping.txt and the iOS dSYMs as artifacts — they are needed to symbolicate crashes.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
KMP CI on GitHub Actions
Split the pipeline by what each host can build: Linux runs the shared code, Android compile, host tests and lint cheaply; macOS runs only what needs Apple tooling — the iOS framework link and the iOS simulator tests. Signing secrets never appear in the pull-request workflow; they live in a separate release workflow behind a protected environment.
When to use
- Adding CI to a KMP repository, or porting a pipeline from another CI system to GitHub Actions.
- CI is slow: Kotlin/Native toolchain downloaded on every run, cold Gradle caches, macOS minutes burned on work Linux could do.
- iOS compile or link errors only show up after merge.
- Choosing runner labels, Xcode versions, or adding an x86_64 simulator job.
- Adding a release workflow that signs and uploads both apps.
When not to use
- A specific build failure you need to diagnose: use
kmp-build-failure-triage. - A test that fails only in CI or only sometimes: use
kmp-flaky-test-triage. - Versioning, store tracks, Fastlane lanes and the release checklist: use
mobile-release(this skill only runs those lanes). - Slow Kotlin/Native builds in general (local and CI): see the Kotlin native build performance skill linked below.
Step 0 — Read the project first
ls .github/workflows 2>/dev/null && cat .github/workflows/*.yml # existing pipelines to extend, not replace
ls .gitlab-ci.yml bitrise.yml .circleci 2>/dev/null # pipeline being ported
cat gradle/wrapper/gradle-wrapper.properties # Gradle version
grep -nE '^(kotlin|agp|android-gradle|androidGradlePlugin|kover)' gradle/libs.versions.toml
grep -rnE "jvmToolchain|JvmTarget" --include='*.kts' . | head # JDK the build expects
cat .java-version .tool-versions 2>/dev/null
grep -rnE "iosX64|iosArm64|iosSimulatorArm64" --include=build.gradle.kts . | head
grep -nE "configuration-cache|org.gradle.caching|jvmargs" gradle.properties
find . -maxdepth 2 \( -name '*.xcodeproj' -o -name '*.xcworkspace' -o -name Gemfile -o -name fastlane \)
./gradlew :shared:tasks --all | grep -iE 'test|lint|kover|link.*Framework' | head -40Record: module and task names, JDK version, iOS targets declared, whether Kover/detekt/ktlint are
applied, the Xcode project path and scheme, and whether Fastlane exists. Use the real task names in
every run: step.
Workflow
1. Split jobs by host
| Job | Runner | Runs | Why there |
android | ubuntu-latest | compileCommonMainKotlinMetadata, Android host tests, lint, assembleDebug, Kover | Cheapest runner; Apple targets are skipped automatically on Linux |
ios | macos-latest (arm64) | linkDebugFrameworkIosSimulatorArm64, iosSimulatorArm64Test, optional xcodebuild build | Kotlin/Native for Apple targets needs macOS + Xcode |
ios-x64 (optional) | macos-15-intel | iosX64Test | Only when x86_64 simulator slices must be tested |
Run android and ios in parallel. Do not run Android work on macOS: macOS minutes cost more
and the queue is longer.
Done when: every check the team runs locally before a PR is in exactly one job.
2. Start from the asset
Copy assets/ci.yml to .github/workflows/ci.yml and replace every TODO with the
names from Step 0. It already contains the pieces below; adjust rather than rewrite.
3. Java and Gradle
actions/setup-java@v6withdistribution: temurinand the JDK the build expects — usually 17 or 21. Recent AGP and Gradle 9 need JDK 17 or newer.java-version-file: .java-versionreads it from the repo instead.gradle/actions/setup-gradle@v6owns Gradle caching and validates the wrapper checksum. Do not also setcache: gradleon setup-java; two caches of the same directory waste time and quota.- Cache provider (decide once per organisation). In v6 the default
enhancedprovider is a closed-source component under Gradle's Terms of Use (free for public repositories, preview for private ones).cache-provider: basicis MIT-licensed, built onactions/cache, free everywhere, with coarser keys. The asset usesbasic; switch if the team accepts the terms. - By default only the default branch writes the Gradle cache; pull requests read it. Keep that.
- Configuration cache:
org.gradle.configuration-cache=trueingradle.propertiesis a project choice, not a CI flag. setup-gradle v6 does not persist configuration-cache entries between runs (its experimental support was removed), so in CI it mainly proves compatibility. Put all tasks in one Gradle invocation per job so configuration happens once.
4. Kotlin/Native toolchain cache
Kotlin/Native downloads its compiler and LLVM dependencies into ~/.konan on first use — large,
and slow to fetch. setup-gradle does not cache it. Cache it with actions/cache@v6, keyed on the
Kotlin version read from the catalog plus runner OS and architecture:
- name: Read Kotlin version
id: kotlin
run: |
version=$(sed -nE 's/^kotlin[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' gradle/libs.versions.toml | head -n 1)
echo "version=${version:-unknown}" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ runner.arch }}-${{ steps.kotlin.outputs.version }}No restore-keys: an old Kotlin's toolchain is useless to a new one and only bloats the entry.
If the Kotlin version is not in libs.versions.toml, change the sed to read wherever it lives.
5. Xcode and runners
- Select Xcode explicitly with
maxim-lobanov/setup-xcode@v1(xcode-version: latest-stable, or a pinned'26.6'). Pin in release workflows; the image's default Xcode moves without notice. macos-latestis arm64 (currently macOS 26);macos-15andmacos-26are arm64 too. Pin a versioned label when an image change breaks the build.- Intel (x86_64):
macos-15-intelis the standard-runner label;-largelabels are paid larger runners. GitHub has announced it stops supporting Intel macOS after the macOS 15 image retires (announced for fall 2027). Plan the x86_64 job as temporary. - Check the runner-images list for current labels and the Xcode versions each image carries.
6. Triggers, concurrency, path filters
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}New pushes cancel the stale PR run; main runs always finish. Use paths-ignore for docs-only
changes — but if a job is a required status check, a skipped workflow leaves the check pending
and blocks merge. Either keep required workflows unfiltered, or filter inside the job.
Set permissions: contents: read at the top. Add more only to the job that needs it.
7. Reports as artifacts
Upload with actions/upload-artifact@v7 and if: ${{ !cancelled() }} so reports survive a failing
test step:
- tests:
**/build/reports/tests/and**/build/test-results/ - lint:
**/build/reports/lint-results-*.html - Kover: run
koverXmlReport(andkoverHtmlReportif wanted), upload**/build/reports/kover/
Use distinct artifact names per job; v4+ artifacts are immutable and a duplicate name fails.
Set retention-days (7 for CI is plenty).
8. Optional iOS app build
linkDebugFrameworkIosSimulatorArm64 proves the framework links; it does not prove the Swift code
still compiles against the changed Kotlin API. Add the commented xcodebuild … CODE_SIGNING_ALLOWED=NO
build step from the asset when the iOS app has meaningful Swift code. If the Xcode project runs a
Gradle build phase, the JDK from setup-java must be set up first in that job.
9. Release workflow
Copy assets/release.yml. It triggers on app tags (android-v*, ios-v*) or
a manual run, checks that the tag matches the version in gradle.properties, and runs the Fastlane
lanes from mobile-release. Rules:
- Secrets exist only in this workflow, only on a GitHub environment (
release) with required reviewers and a deployment-branch/tag rule. - Never trigger it from
pull_requestorpull_request_target; fork code must never see signing secrets. - Decode the keystore into
$RUNNER_TEMP, never into the workspace; delete it in anif: always()step. - Keep the R8
mapping.txtand the iOS dSYMs as artifacts — they are needed to symbolicate crashes. cache-read-only: trueon setup-gradle: release jobs must not write the shared cache.
10. Porting from GitLab CI
stages become needs: between jobs; rules: changes: become paths/paths-ignore; protected
variables become environment secrets; tags: selecting a macOS runner become runs-on: labels.
Quick reference
| Need | Use |
| Checkout | actions/checkout@v7 |
| JDK | actions/setup-java@v6 + distribution: temurin |
| Gradle + cache | gradle/actions/setup-gradle@v6 (cache-provider: basic or default enhanced) |
~/.konan | actions/cache@v6, key on Kotlin version + runner.os + runner.arch |
| Xcode | maxim-lobanov/setup-xcode@v1 |
| Reports | actions/upload-artifact@v7, if: ${{ !cancelled() }} |
| Ruby + Fastlane | ruby/setup-ruby@v1 with bundler-cache: true |
| Linux | ubuntu-latest (x64) · ubuntu-24.04-arm (arm64) |
| macOS arm64 | macos-latest, macos-26, macos-15 |
| macOS x86_64 | macos-15-intel (standard), *-large (paid) |
Verify
Report each check as PASS, FAIL or NOT RUN with the reason. Never mark a check PASS that you
did not run.
ruby -ryaml -e 'ARGV.each { |f| YAML.safe_load(File.read(f)); puts "#{f}: ok" }' .github/workflows/*.yml # YAML parses
python3 -c 'import sys, yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]; print("ok")' .github/workflows/*.yml # if PyYAML is installed
actionlint # if installed: schema, expressions, shellcheck of run steps
./gradlew help --task :shared:iosSimulatorArm64Test # every task named in the YAML exists (repeat per task)
grep -n "secrets\." .github/workflows/ci.yml # expect no output: no secrets in PR CIThen push a branch and open a draft PR: confirm both jobs start, the iOS job restores ~/.konan on
the second run (cache hit in the step log), and the report artifacts appear on the run page.
A workflow that only parses is NOT RUN for execution until a real run has been observed.
Common mistakes
| Mistake | Why it bites | Fix |
| Everything on macOS | Slower queue, higher cost, same result for Android work | Linux for common/Android, macOS for Apple targets only |
No ~/.konan cache | Every iOS run re-downloads the Kotlin/Native toolchain | actions/cache keyed on the Kotlin version |
restore-keys on the konan cache | Restores an old toolchain next to the new one; the entry keeps growing | Exact key only |
cache: gradle on setup-java and setup-gradle | Double caching of the same directory | Let setup-gradle own it |
| One Gradle call per step | Configuration and daemon start-up repeated per step | One invocation with --continue |
macos-latest assumed to be Intel | It has been arm64 since 2024; x86_64 simulator testing belongs on an Intel runner | iosSimulatorArm64Test on arm64; macos-15-intel for x64 |
| Unpinned Xcode in release | The image default changes and the App Store build changes with it | Pin xcode-version |
| Signing secrets in the PR workflow | Any workflow change in a PR can exfiltrate them | Separate release workflow + protected environment |
paths-ignore on a required check | Docs-only PRs stay blocked on a pending check | Unfiltered required workflow or in-job filtering |
| Reports uploaded only on success | The run you need to debug has no report | if: ${{ !cancelled() }} |
| Same artifact name in two jobs | Upload fails: artifacts are immutable | One name per job |
| Moving to the v6 cache provider without reading its terms | Licence surprise for a private repository | Pick basic or enhanced deliberately |
Related skills
kmp-build-check— the local both-platform check this CI mirrors.mobile-release— versioning, tags, store tracks and the Fastfile the release workflow calls.kmp-build-failure-triage,kmp-flaky-test-triage— when a CI run fails.kmp-test-coverage-backfill— raising Kover numbers once reports are published.kmp-xcframework-distribution— publishing a framework from CI.- External: gradle/actions · actions/runner-images · Kotlin native build performance skill · fastlane docs.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

