Продвинутый CI/CD для Kotlin проектов на GitHub Actions
Автоматизация сборки, тестирования и развертывания — критически важный этап в разработке современных Kotlin-приложений. Если базовая настройка CI/CD уже освоена, самое время перейти к продвинутым техникам. В этой статье мы разберем, как выжать максимум из GitHub Actions для Kotlin проектов: матричные сборки, интеллектуальное кэширование, параллельные тесты и гибкий деплой.
1. Матричные сборки: тестируем на нескольких версиях и ОС
Одна из самых мощных фич GitHub Actions — матричная стратегия (strategy.matrix). Она позволяет запускать один и тот же workflow с разными параметрами. Для Kotlin это незаменимо, если вы поддерживаете несколько версий JVM или целевых платформ (например, JVM + Native).
Пример матричной сборки для Kotlin Multiplatform проекта:
name: Matrix Build Kotlin
on: [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 # не останавливаем остальные сборки при ошибке в одной
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 }}
Рекомендации:
- Используйте
fail-fast: false, чтобы не терять результаты других комбинаций, если одна из них упала. - Ограничивайте матрицу только релевантными версиями, чтобы не тратить лишние минуты Actions.
- Для Windows обязательно настройте правильные пути к Gradle и проверьте переносы строк (CRLF).
2. Интеллектуальное кэширование Gradle и зависимостей
Kotlin-проекты на Gradle могут загружать десятки мегабайт зависимостей при каждом запуске CI. Без кэширования сборка будет занимать 5–10 минут, а с ним — 30–40 секунд. Используйте встроенные actions для кэширования.
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-daemon
Продвинутые трюки:
- Добавьте в ключ кэша хеш от
gradle.lockfile(если используете--write-locks) — это гарантирует инвалидацию кэша при изменении версий зависимостей. - Используйте
gradle-build-actionот Gradle (action:gradle/gradle-build-action@v3) — он автоматически управляет кэшированием и включает поддержку--build-cache. - Не кэшируйте
~/.gradle/daemon— это может привести к конфликтам между разными версиями демона.
3. Параллельные тесты и динамическое распределение (sharding)
Для больших проектов с сотнями тестов время выполнения может быть узким местом. GitHub Actions позволяет запускать тесты параллельно, разделяя их на шарды. Это особенно полезно для Kotlin + JUnit 5.
Пример разделения тестов по шардам:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4] # 4 параллельных шарда
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Run tests (shard ${{ matrix.shard }})
run: |
./gradlew test \
-Dshard=${{ matrix.shard }} \
-DtotalShards=${{ strategy.job-total }}
Для реализации шардинга в коде тестов используйте аннотации JUnit 5 или собственный Gradle-плагин. Пример Kotlin-кода для распределения:
// TestShardExtension.kt
import org.junit.jupiter.api.extension.*
import java.lang.System.getProperty
class ShardExtension : BeforeEachCallback {
override fun beforeEach(context: ExtensionContext?) {
val shard = getProperty("shard")?.toIntOrNull() ?: return
val total = getProperty("totalShards")?.toIntOrNull() ?: return
val testClass = context?.testClass?.get() ?: return
if (testClass.hashCode().mod(total) + 1 != shard) {
throw TestAbortedException("Skipped by shard filter")
}
}
}
Совет: Используйте test-retry плагин для Gradle, чтобы автоматически перезапускать упавшие тесты — это снижает вероятность ложных срабатываний в CI.
4. Условный деплой и семантическое версионирование
Продвинутый CI/CD подразумевает не только сборку, но и автоматический деплой в зависимости от контекста: например, публикация в Maven Central только при создании тега, или деплой на staging при пуше в develop.
Пример workflow с условным деплоем:
name: Publish Kotlin Library
on:
push:
tags:
- 'v*' # только теги вида v1.0.0
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build and publish
run: ./gradlew publishToMavenCentral
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONAT