Advanced CI/CD for Kotlin Projects on GitHub Actions
Automating build, testing, and deployment is a critically important stage in the development of modern Kotlin applications. If basic CI/CD setup is already mastered, it's time to move on to advanced techniques. In this article, we will analyze how to get the most out of GitHub Actions for Kotlin projects: matrix builds, intelligent caching, parallel tests, and flexible deployment.
1. Matrix Builds: Testing on Multiple Versions and OS
One of the most powerful features of GitHub Actions is the matrix strategy (strategy.matrix). It allows you to run the same workflow with different parameters. For Kotlin, this is indispensable if you support multiple JVM versions or target platforms (e.g., JVM + Native).
Example of a matrix build for a Kotlin Multiplatform project:
name: Matrix Build Kotlinon: [push, pull_request]
jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] java: [11, 17, 21] kotlin-version: [1.9.22, 2.0.0] fail-fast: false # do not stop other builds if one fails
steps: - uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} distribution: 'temurin'
- name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@v2
- name: Build with Gradle run: ./gradlew build -Dkotlin.version=${{ matrix.kotlin-version }}Recommendations:
- Use
fail-fast: falseto avoid losing results from other combinations if one fails. - Limit the matrix to only relevant versions to avoid wasting Actions minutes.
- For Windows, be sure to configure the correct paths to Gradle and check line endings (CRLF).
2. Intelligent Caching of Gradle and Dependencies
Kotlin projects on Gradle can download tens of megabytes of dependencies on every CI run. Without caching, the build will take 5–10 minutes, but with it — 30–40 seconds. Use built-in actions for caching.
steps: - uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper build/ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle-
- name: Build with Gradle run: ./gradlew build --build-cache --no-daemonAdvanced tricks:
- Add a hash from
gradle.lockfileto the cache key (if using--write-locks) — this guarantees cache invalidation when dependency versions change. - Use
gradle-build-actionfrom Gradle (action:gradle/gradle-build-action@v3) — it automatically manages caching and includes support for--build-cache. - Do not cache
~/.gradle/daemon— this can lead to conflicts between different daemon versions.
3. Parallel Tests and Dynamic Distribution (Sharding)
For large projects with hundreds of tests, execution time can be a bottleneck. GitHub Actions allows you to run tests in parallel by splitting them into shards. This is especially useful for Kotlin + JUnit 5.
Example of splitting tests into shards:
jobs: test: runs-on: ubuntu-latest strategy: matrix: shard: [1, 2, 3, 4] # 4 parallel shards steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin'