Ritme rilis kita sejauh ini masih manual: kompilasi di laptop, mvn package, docker build, salin JAR ke server, jalankan ulang. Setiap langkah adalah tempat salah ketik, kelupaan, dan hasil yang berbeda-beda antar mesin — dan yang paling mahal: seluruhnya mengikat waktu developer. CI/CD memindahkan rantai itu ke server otomatis yang bereaksi pada setiap perubahan kode. Bab ini membahas konsepnya sebelum kita menyentuh implementasi konkret GitHub Actions di chapter berikutnya: apa yang diotomatiskan, kapan pipeline boleh gagal, dan mengapa artefak yang naik ke produksi harus artefak yang sama dengan yang diuji.

Continuous Integration #

Integrasi berkelanjutan berarti setiap push ke repository memicu build dan pengujian otomatis atas kode yang digabungkan. Aturan mainnya ketat pada satu hal: cabang utama harus selalu sehat. Build merah pada cabang utama menghentikan seluruh penggabungan lain sampai diperbaiki — masalah ditemukan dalam hitungan menit setelah diperkenalkan, bukan seminggu kemudian saat rilis.

Praktik pendukungnya saling menguatkan: commit kecil dan sering, rangkaian pengujian yang cepat (chapter Testing membahas strukturnya), dan laporan status yang terlihat semua anggota tim. Untuk proyek Java, tahap integrasi lazimnya berisi javac/mvn compile, pengujian unit, dan pengujian integrasi — persis yang kita definisikan sebagai gerbang di bawah ini.

Continuous Delivery vs Continuous Deployment #

AspekContinuous IntegrationContinuous DeliveryContinuous Deployment
Pemicusetiap pushsetiap pushsetiap push
Hingga ke mana otomatisbuild + testartefak siap rilisnaik ke produksi
Langkah manual—keputusan rilistanpa langkah manual
Kadar risikorendahrendahbutuh pengamanan kuat

Delivery menghentikan otomatisasi tepat sebelum produksi: artefak lolos pengujian dan siap dideploy dengan satu tombol (atau satu perintah). Deployment melangkah lebih jauh — perubahan yang lulus seluruh gerbang naik sendiri ke produksi, sehingga kualitas gerbang terakhir (metrik, canary) menjadi penentu rasa aman.

Anatomi Pipeline #

Pipeline tersusun dari tahap (stage) berurutan yang masing-masing memuat pekerjaan (job) — pekerjaan dalam satu tahap boleh berjalan serentak, tahap baru berjalan bila tahap sebelumnya hijau. Perilaku inilah yang disebut fail-fast: kegagalan menutup jalur di depannya. Konfigurasi pipeline hidup sebagai berkas YAML di dalam repository:

yaml pipa-ci.yml — konsep pipeline yang akan kita wujudkan di GitHub Actions
stages: [compile, test, package, publish]

jobs:
  maven-compile:
    stage: compile
    script: mvn -q compile
  unit-test:
    stage: test
    script: mvn -q test
  integration-test:
    stage: test
    script: mvn -q verify -Pintegration
  maven-package:
    stage: package
    script: mvn -q package
  push-image:
    stage: publish
    script: docker build -t registry.internal/kasirapp:2.1.0 . && docker push registry.internal/kasirapp:2.1.0

Mesin yang mengeksekusi konfigurasi seperti ini bisa kita pahami utuh dalam program kecil berikut:

java PipaCi.java
import java.util.List;

/** Pipeline CI/CD mini: tahapan berurutan dengan perilaku fail-fast. */
public class PipaCi {

    /** Satu pekerjaan dalam satu tahap: nama dan status kelulusannya. */
    record Pekerjaan(String nama, boolean lulus) {}

    /** Satu tahap pipeline memuat beberapa pekerjaan yang boleh berjalan serentak. */
    record Tahap(String nama, List<Pekerjaan> pekerjaan) {}

    /** Jalankan pipeline: tahap berhenti begitu ada pekerjaan yang gagal,
     *  tahap yang belum sempat dieksekusi ditandai DILEWATI. */
    static String jalankan(String judul, List<Tahap> pipeline) {
        System.out.println("== " + judul + " ==");
        String gagalDi = null;
        for (Tahap t : pipeline) {
            if (gagalDi != null) {
                for (Pekerjaan p : t.pekerjaan()) {
                    System.out.printf("  [%-9s] %-26s %s%n", t.nama(), p.nama(), "DILEWATI");
                }
                continue;
            }
            for (Pekerjaan p : t.pekerjaan()) {
                String status = p.lulus() ? "LULUS" : "GAGAL";
                System.out.printf("  [%-9s] %-26s %s%n", t.nama(), p.nama(), status);
                if (!p.lulus() && gagalDi == null) {
                    gagalDi = t.nama();
                }
            }
        }
        String akhir = (gagalDi == null) ? "SUCCEEDED" : "FAILED";
        System.out.println("  status akhir: " + akhir
                + (gagalDi == null ? " (artefak dipublikasikan)" : " (gagal di tahap " + gagalDi + ")"));
        System.out.println();
        return akhir;
    }

    public static void main(String[] args) {
        List<Tahap> hijau = List.of(
                new Tahap("compile", List.of(new Pekerjaan("maven-compile", true))),
                new Tahap("test", List.of(
                        new Pekerjaan("unit-test", true),
                        new Pekerjaan("integration-test", true))),
                new Tahap("package", List.of(new Pekerjaan("maven-package", true))),
                new Tahap("publish", List.of(new Pekerjaan("push-image", true))));
        jalankan("Pipeline 1: seluruh pengujian lulus", hijau);

        List<Tahap> merah = List.of(
                new Tahap("compile", List.of(new Pekerjaan("maven-compile", true))),
                new Tahap("test", List.of(
                        new Pekerjaan("unit-test", true),
                        new Pekerjaan("integration-test", false))),
                new Tahap("package", List.of(new Pekerjaan("maven-package", true))),
                new Tahap("publish", List.of(new Pekerjaan("push-image", true))));
        jalankan("Pipeline 2: integration-test gagal", merah);
    }
}

Output yang Dihasilkan:

text
== Pipeline 1: seluruh pengujian lulus ==
  [compile  ] maven-compile              LULUS
  [test     ] unit-test                  LULUS
  [test     ] integration-test           LULUS
  [package  ] maven-package              LULUS
  [publish  ] push-image                 LULUS
  status akhir: SUCCEEDED (artefak dipublikasikan)

== Pipeline 2: integration-test gagal ==
  [compile  ] maven-compile              LULUS
  [test     ] unit-test                  LULUS
  [test     ] integration-test           GAGAL
  [package  ] maven-package              DILEWATI
  [publish  ] push-image                 DILEWATI
  status akhir: FAILED (gagal di tahap test)

Mari kita bedah perilakunya:

  1. Pekerjaan dan Tahap: dua record membentuk struktur data pipeline — daftar tahap berisi daftar pekerjaan, persis bentuk YAML di atas. Status lulus dimodelkan boolean karena konfigurasi nyata mengisinya dari hasil perintah yang dijalankan runner.
  2. jalankan(...): lingkaran for pertama menjalankan pekerjaan; begitu satu gagal, variabel gagalDi terisi dan cabang if (gagalDi != null) mengubah seluruh pekerjaan sisanya menjadi DILEWATI — fail-fast yang tidak sekadar berhenti, tetapi melaporkan apa saja yang tidak jadi dieksekusi.
  3. Pipeline 1: seluruh pekerjaan LULUS, status akhir SUCCEEDED, artefak dipublikasikan. Inilah jalur yang diharapkan setiap push.
  4. Pipeline 2: integration-test gagal — package dan push-image tidak dieksekusi. Image berbahasa “lulus compile tetapi gagal uji” tidak pernah tersentuh registry, sehingga tidak ada yang bisa menariknya ke produksi.
  5. Urutan tahap sebagai gerbang: menempatkan pengujian sebelum package bukan kebetulan — gerbang paling murah ditempatkan paling awal agar kegagalan paling cepat terdeteksi.

Runner dan Executor #

Pipeline dieksekusi oleh runner — mesin (fisik atau container) yang menarik kode, menjalankan skrip tahap, dan melaporkan hasilnya. Runner bisa disediakan layanan publik (GitHub-hosted), atau dipasang sendiri di jaringan kantor (self-hosted) bila build membutuhkan akses ke registry internal atau mesin uji yang tidak terbuka ke internet. Spesifikasi runner penting untuk Java: build Maven dengan dependensi besar dan pengujian integrasi memakan memori — runner dengan 2 GB RAM sering menjadi penyebab build “berhasil di laptop, gagal di CI”.

Konsepnya lengkap: pemicu, tahap, gerbang, promosi artefak, dan runner. Chapter berikutnya mewujudkan seluruhnya di platform yang paling mudah dijangkau proyek Java modern: GitHub Actions — lengkap dengan caching dependensi, matrix JDK, dan publikasi artefak.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PipaCi.java

Unduh berkas
java
import java.util.List;

/** Pipeline CI/CD mini: tahapan berurutan dengan perilaku fail-fast. */
public class PipaCi {

    /** Satu pekerjaan dalam satu tahap: nama dan status kelulusannya. */
    record Pekerjaan(String nama, boolean lulus) {}

    /** Satu tahap pipeline memuat beberapa pekerjaan yang boleh berjalan serentak. */
    record Tahap(String nama, List<Pekerjaan> pekerjaan) {}

    /** Jalankan pipeline: tahap berhenti begitu ada pekerjaan yang gagal,
     *  tahap yang belum sempat dieksekusi ditandai DILEWATI. */
    static String jalankan(String judul, List<Tahap> pipeline) {
        System.out.println("== " + judul + " ==");
        String gagalDi = null;
        for (Tahap t : pipeline) {
            if (gagalDi != null) {
                for (Pekerjaan p : t.pekerjaan()) {
                    System.out.printf("  [%-9s] %-26s %s%n", t.nama(), p.nama(), "DILEWATI");
                }
                continue;
            }
            for (Pekerjaan p : t.pekerjaan()) {
                String status = p.lulus() ? "LULUS" : "GAGAL";
                System.out.printf("  [%-9s] %-26s %s%n", t.nama(), p.nama(), status);
                if (!p.lulus() && gagalDi == null) {
                    gagalDi = t.nama();
                }
            }
        }
        String akhir = (gagalDi == null) ? "SUCCEEDED" : "FAILED";
        System.out.println("  status akhir: " + akhir
                + (gagalDi == null ? " (artefak dipublikasikan)" : " (gagal di tahap " + gagalDi + ")"));
        System.out.println();
        return akhir;
    }

    public static void main(String[] args) {
        List<Tahap> hijau = List.of(
                new Tahap("compile", List.of(new Pekerjaan("maven-compile", true))),
                new Tahap("test", List.of(
                        new Pekerjaan("unit-test", true),
                        new Pekerjaan("integration-test", true))),
                new Tahap("package", List.of(new Pekerjaan("maven-package", true))),
                new Tahap("publish", List.of(new Pekerjaan("push-image", true))));
        jalankan("Pipeline 1: seluruh pengujian lulus", hijau);

        List<Tahap> merah = List.of(
                new Tahap("compile", List.of(new Pekerjaan("maven-compile", true))),
                new Tahap("test", List.of(
                        new Pekerjaan("unit-test", true),
                        new Pekerjaan("integration-test", false))),
                new Tahap("package", List.of(new Pekerjaan("maven-package", true))),
                new Tahap("publish", List.of(new Pekerjaan("push-image", true))));
        jalankan("Pipeline 2: integration-test gagal", merah);
    }
}