Setelah aplikasi Kotlin siap, kita perlu cara otomatis untuk membangun, menguji, dan men-deploy-nya setiap kali kode berubah. Praktik Continuous Integration / Continuous Deployment (CI/CD) mengotomatisasi proses tersebut sehingga kesalahan tertangkap lebih cepat dan rilis lebih sering dengan risiko rendah. Pada chapter ini kita akan memetakan tahapan pipeline standar, mendemokan simulasi pipeline yang gagal dan berhasil, dan menutup dengan aturan praktis untuk membangun CI/CD yang sehat.

Tahapan Pipeline Standar #

Pipeline CI/CD untuk Kotlin biasanya terdiri atas tahapan berikut:

kotlin
// ci_cd_fundamentals.kt
// Mendemonstrasikan konsep CI/CD fundamental:
// - Tahapan pipeline (build, test, lint, package, deploy)
// - Status pipeline dan fail-fast
// - Build matrix (multi-version, multi-platform)
// - Artifact retention
//
// Kode ini mensimulasikan runner CI/CD dan menampilkan status tiap tahap.

enum class TahapStatus { BERHASIL, GAGAL, LEWAT }

data class HasilTahap(
    val nama: String,
    val durasi: Int,       // detik
    val status: TahapStatus,
    val catatan: String
)

data class Pipeline(
    val nama: String,
    val tahap: List<HasilTahap>
) {
    fun totalDurasi(): Int = tahap.sumOf { it.durasi }
    fun berhasil(): Boolean = tahap.all { it.status != TahapStatus.GAGAL }
}

class PipelineRunner(private val pipeline: Pipeline) {
    fun jalankan(): List<HasilTahap> {
        val hasil = mutableListOf<HasilTahap>()
        for (tahap in pipeline.tahap) {
            println("  -> menjalankan ${tahap.nama}... (${tahap.durasi}s)")
            hasil.add(tahap)
            if (tahap.status == TahapStatus.GAGAL) {
                println("  !! ${tahap.nama} GAGAL: ${tahap.catatan}")
                println("  Pipeline berhenti (fail-fast).")
                return hasil
            }
        }
        return hasil
    }
}

fun main() {
    println("=== CI/CD Fundamentals ===")
    println()

    println("--- 1. Tahapan pipeline standar ---")
    val tahapanStandar = listOf(
        "checkout   - ambil kode dari repository",
        "setup-jdk  - install JDK 21 dan Kotlin 2.0.21",
        "lint       - jalankan detekt / ktlint",
        "compile    - kotlin compiler, hasil build/",
        "test       - unit test (JUnit 5, kotest, mockk)",
        "package    - build fat JAR via Shadow plugin",
        "scan       - SAST, dependency scan (Snyk, Trivy)",
        "publish    - push ke registry (Docker Hub, GHCR)",
        "deploy-staging  - deploy ke environment staging",
        "e2e-test   - smoke test di staging",
        "deploy-prod     - manual approval, deploy ke production"
    )
    tahapanStandar.forEach { println("  $it") }

    println()
    println("--- 2. Simulasi pipeline BERHASIL ---")
    val pipelineOk = Pipeline(
        nama = "build-pr-123",
        tahap = listOf(
            HasilTahap("checkout", 5, TahapStatus.BERHASIL, "ok"),
            HasilTahap("lint", 15, TahapStatus.BERHASIL, "0 violation"),
            HasilTahap("compile", 30, TahapStatus.BERHASIL, "build/classes"),
            HasilTahap("test", 60, TahapStatus.BERHASIL, "128 sukses, 0 gagal"),
            HasilTahap("package", 20, TahapStatus.BERHASIL, "app-1.0.0.jar 42 MB")
        )
    )
    val runnerOk = PipelineRunner(pipelineOk)
    runnerOk.jalankan()
    println("  Total durasi: ${pipelineOk.totalDurasi()}s")
    println("  Pipeline berhasil: ${pipelineOk.berhasil()}")

    println()
    println("--- 3. Simulasi pipeline GAGAL di tahap test ---")
    val pipelineFail = Pipeline(
        nama = "build-pr-124",
        tahap = listOf(
            HasilTahap("checkout", 5, TahapStatus.BERHASIL, "ok"),
            HasilTahap("lint", 18, TahapStatus.BERHASIL, "0 violation"),
            HasilTahap("compile", 28, TahapStatus.BERHASIL, "ok"),
            HasilTahap("test", 12, TahapStatus.GAGAL, "3 test gagal di UserServiceTest")
        )
    )
    val runnerFail = PipelineRunner(pipelineFail)
    runnerFail.jalankan()
    println("  Pipeline berhasil: ${pipelineFail.berhasil()}")

    println()
    println("--- 4. Build matrix (multi-version) ---")
    val matrix = listOf(
        Triple("ubuntu-latest", "JDK 17", true),
        Triple("ubuntu-latest", "JDK 21", true),
        Triple("windows-latest", "JDK 21", true),
        Triple("macos-latest", "JDK 21", false)
    )
    matrix.forEach { (os, jdk, ok) ->
        val status = if (ok) "OK" else "GAGAL"
        println("  $os / $jdk -> $status")
    }

    println()
    println("--- 5. Aturan praktis CI/CD ---")
    val aturan = listOf(
        "1. Build harus deterministik — gunakan lock file (gradle.lockfile)",
        "2. Test harus cepat (<10 menit) — pisahkan integration test",
        "3. Cache dependency (Gradle cache, Maven local)",
        "4. Pakai matrix build untuk dukung multi-platform",
        "5. Fail-fast: hentikan pipeline bila ada tahap gagal",
        "6. Publish artifact hanya bila seluruh pipeline berhasil",
        "7. Manual approval sebelum deploy ke production",
        "8. Rollback plan wajib — revert commit atau redeploy versi lama"
    )
    aturan.forEach { println("  $it") }

    println()
    println("--- 6. Artifact retention ---")
    val retention = mapOf(
        "build/ (thin JAR)" to "7 hari",
        "fat JAR di registry" to "90 hari",
        "container image di GHCR" to "10 versi terakhir",
        "test report HTML" to "30 hari",
        "log pipeline" to "90 hari"
    )
    retention.forEach { (k, v) -> println("  $k: $v") }
}

Output yang Dihasilkan:

text
=== CI/CD Fundamentals ===

--- 1. Tahapan pipeline standar ---
  checkout   - ambil kode dari repository
  setup-jdk  - install JDK 21 dan Kotlin 2.0.21
  lint       - jalankan detekt / ktlint
  compile    - kotlin compiler, hasil build/
  test       - unit test (JUnit 5, kotest, mockk)
  package    - build fat JAR via Shadow plugin
  scan       - SAST, dependency scan (Snyk, Trivy)
  publish    - push ke registry (Docker Hub, GHCR)
  deploy-staging  - deploy ke environment staging
  e2e-test   - smoke test di staging
  deploy-prod     - manual approval, deploy ke production

--- 2. Simulasi pipeline BERHASIL ---
  -> menjalankan checkout... (5s)
  -> menjalankan lint... (15s)
  -> menjalankan compile... (30s)
  -> menjalankan test... (60s)
  -> menjalankan package... (20s)
  Total durasi: 130s
  Pipeline berhasil: true

--- 3. Simulasi pipeline GAGAL di tahap test ---
  -> menjalankan checkout... (5s)
  -> menjalankan lint... (18s)
  -> menjalankan compile... (28s)
  -> menjalankan test... (12s)
  !! test GAGAL: 3 test gagal di UserServiceTest
  Pipeline berhenti (fail-fast).
  Pipeline berhasil: false

--- 4. Build matrix (multi-version) ---
  ubuntu-latest / JDK 17 -> OK
  ubuntu-latest / JDK 21 -> OK
  windows-latest / JDK 21 -> OK
  macos-latest / JDK 21 -> GAGAL

--- 5. Aturan praktis CI/CD ---
  1. Build harus deterministik — gunakan lock file (gradle.lockfile)
  2. Test harus cepat (<10 menit) — pisahkan integration test
  3. Cache dependency (Gradle cache, Maven local)
  4. Pakai matrix build untuk dukung multi-platform
  5. Fail-fast: hentikan pipeline bila ada tahap gagal
  6. Publish artifact hanya bila seluruh pipeline berhasil
  7. Manual approval sebelum deploy ke production
  8. Rollback plan wajib — revert commit atau redeploy versi lama

--- 6. Artifact retention ---
  build/ (thin JAR): 7 hari
  fat JAR di registry: 90 hari
  container image di GHCR: 10 versi terakhir
  test report HTML: 30 hari
  log pipeline: 90 hari

Pada cuplikan di atas, PipelineRunner mensimulasikan runner CI/CD. Setiap tahap dieksekusi berurutan; bila ada yang gagal, pipeline berhenti (fail-fast) dan tahap berikutnya tidak dijalankan. Pola ini umum di semua sistem CI/CD modern — Jenkins, GitHub Actions, GitLab CI, CircleCI, dan Drone.

Build Matrix #

Untuk aplikasi yang dipakai lintas-platform (mis. library yang dikonsumsi tim lain), kita perlu memastikan kode tetap berjalan di berbagai kombinasi sistem operasi dan versi JDK. Build matrix menyelesaikan masalah ini dengan menjalankan pipeline yang sama untuk setiap kombinasi:

text Contoh matrix Kotlin + JDK
matrix:
  os:    [ubuntu-latest, windows-latest, macos-latest]
  jdk:   [17, 21]
  # total: 6 job berjalan paralel

Trade-off matrix build: waktu total pipeline jadi lebih cepat (paralel), tetapi biaya runner CI naik (lebih banyak job). Untuk library publik, matrix build sangat dianjurkan; untuk aplikasi internal yang spesifik perantikeras, satu kombinasi sudah cukup.

Fail-Fast vs Continue-on-Error #

Secara default, pipeline berhenti saat satu tahap gagal. Ini disebut fail-fast. Beberapa kasus ingin sebaliknya — mis. ingin menjalankan seluruh test meskipun beberapa sudah gagal, agar mendapat laporan lengkap. Konfigurasi continue-on-error: true di step memungkinkan hal ini:

yaml continue-on-error di GitHub Actions
steps:
  - name: Lint
    run: ./gradlew detekt
    continue-on-error: true   # catat warning, jangan gagalkan pipeline
  - name: Test
    run: ./gradlew test
    # tetap fail-fast

Aturan praktis: lint dan scan keamanan boleh continue-on-error: true (warning), tetapi build dan test harus false (fail-fast). Bila lint menggagalkan pipeline untuk hal sepele, developer akan cepat lelah dan menonaktifkannya.

Artifact Retention #

Setiap pipeline menghasilkan artefak — JAR, container image, test report, log. Menyimpan semuanya selamanya akan memenuhi storage. Atur retention policy yang masuk akal:

kotlin Retention strategi
val retention = mapOf(
    "build/ (thin JAR)" to "7 hari",
    "fat JAR di registry" to "90 hari",
    "container image di GHCR" to "10 versi terakhir",
    "test report HTML" to "30 hari",
    "log pipeline" to "90 hari"
)

Untuk image container yang dipakai produksi, simpan minimal 10 versi terakhir agar rollback mudah. Untuk log pipeline, 90 hari sudah cukup untuk audit. Untuk thin JAR build, 7 hari cukup karena biasanya hanya dipakai selama pipeline berjalan.

Strategi Branching #

Pipeline berbeda untuk branch berbeda:

BranchPipelineDeploy
feature/*build + testtidak
developbuild + test + integrationstaging
mainbuild + test + integration + scanproduction (manual approval)
release/*build + test + tag + publishproduction (auto)

Untuk pull request, jalankan pipeline ringkas (build + unit test saja) — integration test yang berat bisa tunggu sampai merge ke develop.

Aturan Praktis #

Beberapa aturan yang membuat pipeline sehat:

  1. Build deterministik — gunakan gradle.lockfile atau pip freeze agar dependency terkunci. Build yang sama harus menghasilkan artefak yang sama.
  2. Test cepat — pipeline idealnya selesai dalam 10 menit. Pisahkan integration test ke pipeline terpisah (nightly).
  3. Cache dependency — Gradle cache, Maven cache, dan Docker layer cache mempercepat build signifikan.
  4. Manual approval untuk production — deploy ke production harus melibatkan manusia, bukan otomatis.
  5. Rollback plan wajib — setiap rilis harus punya plan rollback (redeploy versi lama, atau feature flag off).

Referensi Lanjutan #

  • GitHub Actions — dokumentasi resmi GitHub Actions untuk pipeline CI/CD

Selanjutnya #

Setelah memahami konsep CI/CD umum, chapter berikutnya membahas implementasi konkret di GitHub Actions — platform CI/CD yang paling populer untuk proyek open-source Kotlin.