CI minimalis berarti hanya menjalankan pekerjaan yang benar-benar memberi sinyal kualitas, dengan urutan dan pemisahan yang tepat. Saat runner lambat, CPU sedikit, RAM terbatas, atau antrean build panjang, target utamanya bukan membuat pipeline terlihat canggih, melainkan menjaga feedback tetap cepat, hasil tetap konsisten, dan biaya komputasi tetap masuk akal.
Masalah paling umum pada mesin lambat bukan sekadar “tes terlalu banyak”, tetapi pipeline yang tidak selektif: lint, typecheck, test, build, scan, dan setup environment dicampur dalam satu alur panjang; cache dipasang tanpa diukur; matrix dijalankan untuk semua branch; dan validasi ringan yang bisa ditangani di lokal tetap dibebankan ke CI. Hasilnya, developer menunggu lama untuk hal-hal yang sebenarnya bisa dipisah, dipercepat, atau dihapus.
Artikel ini membahas pendekatan praktis untuk merancang CI/CD yang tetap cepat dan stabil pada runner sederhana, terinspirasi dari semangat The Old Computer Challenge: anggap sumber daya terbatas sebagai batas desain, bukan gangguan sementara.
Mental model: optimalkan sinyal, bukan aktivitas
Pipeline yang baik tidak diukur dari banyaknya langkah, tetapi dari kualitas sinyal per menit. Jika sebuah job memakan waktu lama namun jarang menemukan masalah, mungkin letaknya salah, pemicunya salah, atau sebenarnya tidak perlu dijalankan pada setiap commit.
Untuk mesin lambat, urutan prioritas yang biasanya masuk akal adalah:
- Validasi paling murah dan paling sering gagal dijalankan lebih dulu.
- Validasi yang independen dipisahkan agar kegagalan mudah dibaca.
- Pekerjaan mahal dijalankan hanya saat relevan, misalnya pada pull request, branch utama, atau sebelum rilis.
- Setup environment dipangkas agar job tidak menghabiskan sebagian besar waktu hanya untuk bootstrapping.
Tujuan praktisnya sederhana: developer cepat tahu apakah perubahan aman dilanjutkan, tanpa menunggu build besar yang sebetulnya tidak relevan untuk perubahan kecil.
Audit langkah CI yang boros
Sebelum mengubah konfigurasi, audit dulu pipeline saat ini. Jangan menebak. Ambil 20-50 run terakhir dan catat waktu per job, tingkat kegagalan, serta frekuensi eksekusi. Bahkan pada CI sederhana, tiga pertanyaan berikut sering langsung membuka bottleneck:
- Job mana yang paling lama?
- Job mana yang paling sering gagal?
- Job mana yang paling sering berjalan padahal hasilnya hampir selalu sama?
Tanda job layak dipertanyakan
- Install dependencies lebih lama daripada lint atau test.
- Build production dijalankan di semua push, termasuk branch eksperimen.
- Matrix multi-versi berjalan untuk semua PR, padahal kompatibilitas cukup diverifikasi di branch utama atau nightly.
- End-to-end test memblokir semua perubahan, termasuk perubahan dokumentasi atau refactor kecil di area non-kritis.
- Cache besar di-restore dan di-save terus-menerus, tetapi tidak memberi pengurangan durasi yang nyata.
Metrik minimum yang perlu dipantau
- Total durasi pipeline per jenis trigger: push, pull request, release.
- Durasi per job: lint, typecheck, unit test, integration test, build.
- Queue time: waktu menunggu runner sebelum job mulai.
- Success/failure rate per job.
- Cache hit rate, jika platform CI mendukung.
- Flaky rate: test yang kadang gagal tanpa perubahan kode yang relevan.
Jika queue time lebih besar daripada waktu eksekusi, masalah utamanya mungkin bukan optimasi command, melainkan terlalu banyak workflow yang dipicu.
Pisahkan lint, typecheck, test, dan build
Di mesin lambat, satu job besar sering terasa sederhana, tetapi buruk untuk diagnosis dan boros waktu. Ketika lint, typecheck, unit test, dan build digabung dalam satu job serial, kegagalan di langkah awal tetap menunggu setup penuh, dan kegagalan di langkah akhir membuat developer membaca log panjang yang tidak fokus.
Mengapa dipisah?
- Fail-fast lebih efektif: lint bisa gagal dalam hitungan detik tanpa menunggu test atau build.
- Observabilitas lebih baik: tim langsung tahu kelas masalahnya.
- Kontrol trigger lebih presisi: build tidak harus ikut saat hanya perlu validasi kualitas kode.
- Paralelisme bisa dipakai secukupnya: tidak berlebihan, tapi cukup untuk mempersingkat jalur kritis.
Pemisahan yang umum dan sehat
- Lint: cek format, style, atau static linting. Cepat, murah, wajib awal.
- Typecheck: terpisah dari lint jika ekosistem bahasa Anda mendukung. Ini sering punya profil CPU/RAM berbeda.
- Unit test: validasi utama untuk logika aplikasi.
- Build: verifikasi artefak benar-benar bisa diproduksi, tetapi tidak selalu perlu di setiap event.
Pada proyek kecil, lint dan typecheck kadang bisa digabung bila tooling memang berbagi proses dan waktunya sangat singkat. Namun gabungkan karena ada data, bukan karena terlihat lebih ringkas.
Strategi trigger: pre-commit, pre-push, dan CI
Salah satu cara paling efektif menjaga CI minimalis adalah memindahkan validasi ringan ke momen yang tepat. Tidak semua pemeriksaan harus menunggu server CI.
Apa yang cocok di pre-commit
Pre-commit sebaiknya sangat cepat, idealnya hanya beberapa detik. Tujuannya mencegah kesalahan remeh masuk ke riwayat commit.
- Formatter pada file yang berubah.
- Lint ringan pada file yang berubah.
- Cek whitespace, konflik merge, atau file besar yang tidak sengaja ter-commit.
Hindari test suite penuh di pre-commit. Hook yang lambat akan sering dibypass.
Apa yang cocok di pre-push
Pre-push cocok untuk validasi yang sedikit lebih mahal tetapi masih relevan dijalankan lokal sebelum kode dikirim.
- Unit test yang cepat.
- Typecheck proyek.
- Subset test untuk package atau modul yang berubah.
Ini membantu menurunkan kegagalan CI yang sebenarnya bisa tertangkap sebelum push.
Apa yang tetap harus di CI
- Validasi yang harus konsisten di lingkungan terkontrol.
- Test yang butuh service tambahan, misalnya database atau broker.
- Build artefak rilis.
- Pemeriksaan yang tidak boleh bergantung pada disiplin lokal developer.
Atur pembagian ini sebagai kebijakan tim, bukan preferensi personal. Jika semua orang punya hook berbeda, hasilnya tetap tidak stabil.
Cache yang terukur, bukan cache di mana-mana
Cache dapat mempercepat CI, tetapi pada runner lambat cache juga bisa menjadi biaya: restore memakan bandwidth dan waktu, save memakan I/O, dan cache yang terlalu besar sering tidak sebanding dengan manfaatnya.
Prinsip cache yang aman
- Cache artefak yang mahal dibuat tapi stabil terhadap lockfile, misalnya dependency cache.
- Gunakan key berbasis lockfile atau input yang benar-benar merepresentasikan invalidasi.
- Jangan cache output yang mudah basi dan sulit divalidasi, kecuali Anda paham risikonya.
- Ukur sebelum dan sesudah. Jika penghematan tidak nyata, hapus cache tersebut.
Yang sering keliru
- Menyimpan cache terlalu besar sehingga restore lebih lama daripada install biasa.
- Menggabungkan banyak jenis cache dalam satu key.
- Meng-cache hasil build lintas branch tanpa strategi invalidasi yang jelas.
- Mengandalkan cache untuk menutupi dependency install yang sebenarnya bisa disederhanakan.
Pada banyak kasus, dependency cache cukup. Build cache bisa berguna, tetapi lebih sensitif terhadap invalidasi dan lebih mudah menimbulkan hasil yang membingungkan.
Parallelism secukupnya, jangan melawan keterbatasan runner
Paralelisme terdengar selalu baik, tetapi di mesin lambat ia punya batas. Menjalankan terlalu banyak proses serentak pada runner kecil dapat menambah kontensi CPU, memori, dan I/O. Akibatnya total waktu tidak membaik, bahkan bisa lebih buruk.
Kapan paralelisme membantu
- Job independen yang masing-masing punya setup kecil.
- Runner memiliki cukup resource untuk dua atau tiga job ringan.
- Tujuan utamanya mempercepat critical path, bukan sekadar memecah log.
Kapan paralelisme justru merugikan
- Semua job sama-sama perlu install dependency besar.
- Runner sering kehabisan memori saat test berjalan paralel.
- Waktu antrean bertambah karena terlalu banyak job kecil.
Prinsip praktis: mulai dari serial yang masuk akal, lalu paralelkan hanya job yang memberi penghematan nyata pada waktu tunggu developer.
Matrix seperlunya, bukan default
Matrix sangat berguna untuk memverifikasi banyak kombinasi environment, tetapi mahal. Di mesin lambat, matrix yang dijalankan pada setiap push hampir selalu berlebihan.
Kapan matrix perlu
- Anda memang mendukung beberapa versi runtime yang penting bagi pengguna.
- Perbedaan OS memengaruhi perilaku aplikasi atau toolchain.
- Proyek adalah library, SDK, atau alat lintas platform.
Cara menghemat matrix
- Jalankan satu kombinasi utama pada pull request.
- Jalankan matrix penuh hanya di branch utama, nightly, atau sebelum rilis.
- Batasi matrix untuk job tertentu, misalnya unit test saja, bukan build penuh.
Dengan cara ini, Anda tetap menjaga kompatibilitas tanpa membebani seluruh siklus pengembangan harian.
Fail-fast dan desain dependency antar job
Fail-fast berarti menghentikan alur secepat mungkin saat sinyal awal sudah jelas merah. Pada CI minimalis, ini salah satu penghemat terbesar.
Urutan job yang sering efektif
- Checkout dan setup minimum.
- Lint.
- Typecheck.
- Unit test.
- Build.
- Integration atau release-only checks.
Jika lint gagal, tidak ada alasan langsung membakar menit runner untuk build. Jika unit test gagal, build production biasanya juga tidak perlu berjalan kecuali Anda punya alasan khusus untuk tetap mengumpulkan artefak diagnostik.
Fail-fast tidak berarti mengorbankan visibilitas. Untuk test flaky atau investigasi tertentu, kadang Anda tetap ingin beberapa job lanjut agar tim mendapat konteks lebih lengkap. Gunakan ini sebagai pengecualian, bukan kebiasaan.
Contoh workflow GitHub Actions sederhana
Berikut contoh workflow sederhana yang menekankan pemisahan job, trigger yang wajar, cache dependency, dan fail-fast. Contoh ini dibuat generik untuk proyek JavaScript/TypeScript, tetapi prinsipnya sama untuk ekosistem lain.
name: ci
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- run: npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- run: npm run typecheck
test:
runs-on: ubuntu-latest
needs: [lint, typecheck]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- run: npm test -- --runInBand
build:
runs-on: ubuntu-latest
needs: [test]
if: github.event_name == 'push' || github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- run: npm run buildKenapa contoh ini cocok untuk mesin lambat?
- Lint dan typecheck dipisah, sehingga sinyal awal cepat dan jelas.
- Test menunggu lint dan typecheck, jadi fail-fast terjadi secara alami.
- Build tidak dipicu sembarangan; ia lebih ketat terhadap konteks branch/event.
- Test dijalankan serial lewat
--runInBandsebagai opsi aman bila runner kecil. Ini bukan aturan mutlak; ukur sesuai proyek Anda.
Jika proyek Anda sering terganggu oleh waktu install dependency, langkah berikutnya bukan langsung menambah cache kompleks, tetapi mengevaluasi apakah setup dapat dibagi lewat image yang sudah berisi tool dasar, atau apakah dependency development perlu dipangkas.
Contoh pembagian kebijakan lokal dan CI
Supaya otomasi tim lebih konsisten, definisikan kontrak sederhana seperti ini:
- Pre-commit: formatter + lint file berubah.
- Pre-push: typecheck + unit test cepat.
- Pull request CI: lint penuh, typecheck penuh, unit test, integration test yang relevan.
- Main branch CI: semua di atas + build artefak.
- Release/nightly: matrix tambahan, scan lebih mahal, validasi kompatibilitas lebih luas.
Model ini menjaga pengalaman developer tetap cepat tanpa membuat CI menjadi tempat pertama mendeteksi semua kesalahan dasar.
Anti-pattern umum pada CI minimalis yang gagal
1. Menyamakan “minimalis” dengan “kurang verifikasi”
Minimalis bukan berarti menurunkan standar kualitas. Artinya, verifikasi dipindahkan ke tempat yang paling efisien dan relevan.
2. Satu workflow untuk semua kebutuhan
Push harian, pull request, nightly, dan release punya kebutuhan berbeda. Memaksa semuanya ke jalur yang sama biasanya membuat pipeline lambat dan sulit dipelihara.
3. Terlalu cepat menambah cache
Cache yang salah sering menjadi sumber bug non-deterministik dan waktu restore yang sia-sia.
4. Menjalankan build penuh pada setiap commit
Jika build mahal, jadikan ia checkpoint yang strategis, bukan refleks default.
5. Memecah job terlalu banyak
Terlalu banyak job kecil bisa menaikkan waktu checkout, setup runtime, dan antrean runner. Granularitas harus membantu, bukan memperbanyak overhead.
6. Tidak mengendalikan flaky test
Pada mesin lambat, timeout dan race condition lebih sering terlihat. Jika test flaky dibiarkan, tim akan kehilangan kepercayaan pada CI, secepat apa pun pipeline-nya.
Debugging saat pipeline tetap lambat atau tidak stabil
- Lihat distribusi waktu, bukan rata-rata saja. P95 lebih berguna untuk memahami pengalaman buruk yang berulang.
- Bandingkan warm cache vs cold cache. Jika selisihnya kecil, cache mungkin tidak layak dipertahankan.
- Periksa memory pressure. Test yang tampak “lambat” kadang sebenarnya kena swapping atau garbage collection berat.
- Cek dependency install. Lockfile berubah terlalu sering atau dependency terlalu gemuk bisa lebih mahal daripada test itu sendiri.
- Uji serial vs paralel. Di runner kecil, serial terkadang menang karena overhead lebih rendah.
- Pisahkan flaky test dari sinyal kualitas utama sampai akar masalahnya diperbaiki.
Checklist memangkas durasi pipeline tanpa menurunkan kualitas rilis
- Catat durasi setiap job dan queue time selama beberapa minggu terakhir.
- Urutkan job dari yang paling sering gagal dan paling murah dijalankan.
- Pindahkan formatter dan lint ringan ke pre-commit.
- Pindahkan typecheck atau test cepat ke pre-push bila cocok.
- Pisahkan lint, typecheck, test, dan build agar sinyal lebih jelas.
- Terapkan fail-fast: jangan biarkan build mahal berjalan setelah validasi dasar gagal.
- Jalankan build penuh hanya pada event yang memang membutuhkan artefak.
- Gunakan matrix hanya untuk kompatibilitas yang benar-benar penting.
- Tambahkan cache hanya setelah tahu bottleneck-nya, lalu ukur dampaknya.
- Kurangi paralelisme jika runner sering kehabisan resource.
- Perbaiki atau karantina flaky test agar CI kembali dipercaya.
- Tinjau ulang pipeline setiap kali struktur proyek atau dependency berubah besar.
Penutup
Pada runner terbatas, pipeline cepat lahir dari disiplin desain, bukan dari banyaknya fitur CI yang dipakai. CI minimalis yang baik memisahkan validasi berdasarkan biaya dan nilai sinyal, memanfaatkan cache dengan hati-hati, membatasi matrix, dan menempatkan pemeriksaan di tahap yang tepat: pre-commit, pre-push, atau CI.
Jika Anda harus memilih satu langkah awal, lakukan audit durasi per job dan hapus pekerjaan yang mahal tetapi tidak memberi sinyal penting. Dari sana, pemisahan lint/typecheck/test/build dan fail-fast biasanya memberi dampak paling nyata tanpa menurunkan kualitas rilis.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!