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 #
| Aspek | Continuous Integration | Continuous Delivery | Continuous Deployment |
|---|---|---|---|
| Pemicu | setiap push | setiap push | setiap push |
| Hingga ke mana otomatis | build + test | artefak siap rilis | naik ke produksi |
| Langkah manual | — | keputusan rilis | tanpa langkah manual |
| Kadar risiko | rendah | rendah | butuh 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:
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.0Mesin yang mengeksekusi konfigurasi seperti ini bisa kita pahami utuh dalam program kecil berikut:
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:
== 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:
PekerjaandanTahap: duarecordmembentuk struktur data pipeline — daftar tahap berisi daftar pekerjaan, persis bentuk YAML di atas. Statuslulusdimodelkan boolean karena konfigurasi nyata mengisinya dari hasil perintah yang dijalankan runner.jalankan(...): lingkaranforpertama menjalankan pekerjaan; begitu satu gagal, variabelgagalDiterisi dan cabangif (gagalDi != null)mengubah seluruh pekerjaan sisanya menjadiDILEWATI— fail-fast yang tidak sekadar berhenti, tetapi melaporkan apa saja yang tidak jadi dieksekusi.- Pipeline 1: seluruh pekerjaan
LULUS, status akhirSUCCEEDED, artefak dipublikasikan. Inilah jalur yang diharapkan setiap push. - Pipeline 2:
integration-testgagal —packagedanpush-imagetidak dieksekusi. Image berbahasa “lulus compile tetapi gagal uji” tidak pernah tersentuh registry, sehingga tidak ada yang bisa menariknya ke produksi. - Urutan tahap sebagai gerbang: menempatkan pengujian sebelum
packagebukan 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 #
- Martin Fowler: Continuous Integration
- GitHub: About GitHub Actions
- GitHub: Docs — Automate your workflow
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PipaCi.java
Unduh berkasimport 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);
}
}