Jika monorepo Anda selalu menjalankan lint, test, dan build untuk semua aplikasi dan package di setiap commit, biaya CI akan cepat membesar dan waktu feedback tim menjadi lambat. Selective CI monorepo dengan Nx Affected menyelesaikan masalah ini dengan menghitung project mana saja yang benar-benar terdampak perubahan, lalu hanya menjalankan target pada project tersebut.

Pendekatan ini cocok ketika struktur dependensi antar project cukup jelas dan konfigurasi monorepo dijaga dengan baik. Dengan strategi yang tepat untuk menentukan base dan head commit, serta fallback full run di kondisi tertentu, Anda bisa memangkas pekerjaan CI tanpa mengorbankan keandalan pipeline.

Mengapa pipeline monorepo sering boros?

Masalah umum di monorepo adalah satu perubahan kecil memicu proses untuk semua project. Misalnya, perubahan di satu library utilitas frontend ikut memicu build backend, aplikasi admin, aplikasi publik, package internal, dan seluruh rangkaian tes integrasi, padahal tidak semuanya relevan.

Dampaknya biasanya terlihat di tiga area:

  • Waktu feedback tim lebih lambat, karena hasil CI baru keluar setelah semua job selesai.
  • Biaya runner meningkat, terutama jika pipeline berjalan di banyak branch dan pull request.
  • Pipeline makin rapuh, karena semakin banyak job yang berjalan, semakin besar peluang ada kegagalan yang sebenarnya tidak terkait perubahan.

Selective CI tidak menghilangkan kebutuhan untuk sesekali menjalankan semua job. Namun untuk alur harian, ia membantu memfokuskan eksekusi hanya pada area yang terpengaruh.

Bagaimana Nx Affected bekerja?

Nx menyimpan pemahaman tentang hubungan antar project di dalam monorepo. Ketika Anda menjalankan perintah affected, Nx membandingkan perubahan file antara dua titik referensi Git, lalu memetakan file yang berubah ke project terkait. Setelah itu, Nx menelusuri dependency graph untuk menentukan project mana yang ikut terdampak secara transitif.

Sederhananya, prosesnya seperti ini:

  1. Tentukan base commit dan head commit.
  2. Lihat file apa saja yang berubah di rentang tersebut.
  3. Petakan file ke project Nx yang memilikinya.
  4. Ikuti hubungan dependensi untuk mencari project yang terkena dampak.
  5. Jalankan target seperti lint, test, atau build hanya pada project yang ditemukan.

Contoh sederhana: jika apps/web bergantung pada libs/ui, lalu Anda mengubah file di libs/ui, maka Nx dapat menandai libs/ui dan apps/web sebagai affected. Inilah alasan selective CI bisa tetap aman: ia tidak hanya melihat file yang diubah, tetapi juga memperhitungkan dependensi.

Konsep dependency graph

Dependency graph adalah peta relasi antar project di monorepo. Nx menggunakannya untuk mengetahui siapa bergantung pada siapa. Nilai utama graph ini adalah kemampuan untuk menghitung dampak perubahan secara transitif, bukan sekadar langsung.

Tanpa graph, CI selektif mudah salah karena hanya melihat lokasi file. Dengan graph, perubahan di library bersama dapat memicu aplikasi yang memakainya, meskipun aplikasi itu sendiri tidak tersentuh pada commit terbaru.

Catatan: selective CI hanya seandal graph dan konfigurasi dependensinya. Jika ada relasi yang tidak terdeteksi, hasil affected bisa terlalu sempit.

Alur lokal: mengecek project terdampak sebelum masuk CI

Sebelum membahas CI, penting memahami alur lokal. Di mesin developer, Anda bisa membandingkan branch aktif dengan branch utama untuk melihat apa yang terdampak.

nx affected -t lint --base=origin/main --head=HEAD
nx affected -t test --base=origin/main --head=HEAD
nx affected -t build --base=origin/main --head=HEAD

Intinya:

  • --base adalah titik pembanding awal, biasanya branch utama atau commit target merge.
  • --head adalah kondisi terbaru yang ingin diuji, umumnya HEAD.
  • -t menentukan target yang dijalankan, misalnya lint, test, atau build.

Pola ini berguna untuk validasi cepat sebelum membuat pull request. Developer bisa mengetahui apakah perubahan di satu area akan memengaruhi project lain. Ini juga membantu saat melakukan refactor library bersama, karena dampaknya langsung terlihat.

Kapan memakai base/head seperti di atas?

Untuk pengembangan lokal, pendekatan yang umum adalah:

  • base=origin/main untuk membandingkan branch kerja dengan kondisi terbaru branch utama.
  • head=HEAD untuk mewakili keadaan working branch saat ini.

Namun, pastikan referensi Git tersebut benar-benar tersedia di lingkungan lokal. Jika branch lokal belum sinkron dengan remote, hasilnya bisa tidak akurat. Biasakan melakukan git fetch sebelum menjalankan perintah affected.

Menentukan base dan head commit di CI

Di CI, pemilihan base dan head lebih penting daripada di lokal. Jika rentang commit salah, daftar project terdampak juga akan salah. Ini bisa membuat pipeline terlalu banyak berjalan, atau lebih buruk, melewatkan project yang semestinya diproses.

Prinsip pemilihan commit

Tujuan utama pemilihan base dan head adalah: bandingkan perubahan yang relevan dengan konteks pipeline.

  • Untuk pull request, biasanya aman membandingkan branch PR dengan branch target merge.
  • Untuk push ke branch utama, sering kali lebih tepat membandingkan commit sebelumnya dengan commit saat ini.
  • Untuk rerun pipeline atau branch tertentu, Anda mungkin ingin fallback ke full run jika konteks Git tidak jelas.

Poin pentingnya bukan format perintah tertentu, tetapi memastikan bahwa range commit merepresentasikan perubahan yang benar-benar ingin diuji.

Contoh alur di GitHub Actions

Berikut contoh konfigurasi ringkas GitHub Actions untuk menjalankan selective CI menggunakan Nx Affected. Contoh ini menunjukkan ide dasarnya: ambil histori Git yang cukup, tentukan base/head sesuai event, lalu jalankan target affected.

name: ci

on:
  pull_request:
  push:
    branches:
      - main
      - develop

jobs:
  affected:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Set base and head
        id: sha
        shell: bash
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            echo "base=${{ github.event.pull_request.base.sha }}" >> $GITHUB_OUTPUT
            echo "head=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
          else
            echo "base=${{ github.event.before }}" >> $GITHUB_OUTPUT
            echo "head=${{ github.sha }}" >> $GITHUB_OUTPUT
          fi

      - name: Lint affected
        run: npx nx affected -t lint --base=${{ steps.sha.outputs.base }} --head=${{ steps.sha.outputs.head }}

      - name: Test affected
        run: npx nx affected -t test --base=${{ steps.sha.outputs.base }} --head=${{ steps.sha.outputs.head }}

      - name: Build affected
        run: npx nx affected -t build --base=${{ steps.sha.outputs.base }} --head=${{ steps.sha.outputs.head }}

Ada beberapa hal penting dari contoh ini:

  • fetch-depth: 0 penting agar histori Git yang dibutuhkan tersedia. Checkout dangkal sering menyebabkan Nx tidak bisa membandingkan commit dengan benar.
  • Untuk pull request, gunakan SHA dari branch dasar dan branch kepala PR.
  • Untuk push, gunakan commit sebelumnya dan commit saat ini jika tersedia dari event.

Jika CI Anda berjalan di sistem selain GitHub Actions, konsepnya sama: pastikan dua commit referensi tersedia dan bisa diakses oleh Git di runner.

Kapan selective CI aman dipakai?

Selective CI aman dipakai ketika monorepo Anda memenuhi beberapa syarat praktis:

  • Dependensi antar project dideklarasikan dengan jelas dan konsisten.
  • Project berbagi kode melalui library atau package yang dapat dilacak, bukan melalui pola impor acak.
  • Perubahan konfigurasi global yang berdampak luas dikenali dan ditangani secara eksplisit.
  • Tim memahami bahwa tidak semua perubahan cocok untuk strategi affected murni.

Selective CI paling efektif untuk monorepo yang punya banyak aplikasi dan library internal, dengan perubahan harian yang biasanya terlokalisasi pada area tertentu. Dalam kondisi ini, penghematan waktu dan biaya runner biasanya terasa langsung.

Kapan perlu hati-hati atau tidak cukup hanya affected?

Ada situasi di mana selective CI saja tidak cukup:

  • Perubahan dependency lockfile dapat memengaruhi banyak project sekaligus.
  • Perubahan toolchain seperti konfigurasi TypeScript, bundler, linter, atau test runner bisa berdampak global.
  • Perubahan file shared non-code seperti environment template, schema, generator, atau script build dapat sulit dipetakan otomatis.
  • Tes end-to-end lintas aplikasi sering kali lebih aman dijalankan terpisah dengan aturan sendiri.

Dalam kasus seperti ini, selective CI sebaiknya digabung dengan aturan fallback yang memaksa full run.

Strategi fallback full run untuk branch atau kondisi tertentu

Salah satu praktik terbaik adalah tidak memaksa semua pipeline menjadi selective 100%. Lebih aman jika Anda menetapkan kondisi kapan pipeline harus menjalankan semua target untuk semua project.

Contoh kondisi yang sering dipakai:

  • Push ke branch utama atau branch rilis tertentu.
  • Perubahan file global seperti konfigurasi root, lockfile, atau script CI.
  • Kegagalan penentuan base/head, misalnya histori Git tidak lengkap.
  • Job terjadwal harian atau mingguan untuk validasi penuh.

Secara praktis, Anda bisa membuat logika sederhana di CI:

if perubahan menyentuh file global atau branch adalah release/main:
  jalankan nx run-many untuk semua project
else:
  jalankan nx affected

Misalnya, untuk full run Anda bisa menggunakan pola umum seperti:

nx run-many -t lint,test,build --all

Perintah spesifik bisa berbeda tergantung cara tim Anda mengatur target, tetapi idenya tetap sama: selective CI untuk pekerjaan harian, full run untuk titik kontrol yang lebih ketat.

Risiko umum yang sering membuat hasil affected keliru

1. Dependency implicit yang tidak terdeteksi

Ini adalah sumber masalah paling umum. Misalnya, sebuah aplikasi membaca file konfigurasi bersama di root repository, tetapi hubungan itu tidak tercermin dalam graph Nx. Saat file konfigurasi berubah, aplikasi tersebut seharusnya dianggap terdampak, tetapi Nx belum tentu mengetahuinya secara otomatis.

Tanda-tandanya:

  • Build lokal sukses saat menjalankan full run, tetapi gagal di produksi setelah selective CI melewatkan project tertentu.
  • Perubahan file shared tidak memicu aplikasi yang sebenarnya bergantung padanya.

Solusi praktis:

  • Kurangi dependency tersembunyi di luar batas project yang jelas.
  • Pusatkan kode shared ke library yang benar-benar tercatat dalam graph.
  • Tambahkan aturan fallback untuk file global atau area yang sulit dimodelkan.

2. Perubahan file shared berdampak luas

Beberapa file memang secara alami memengaruhi banyak project: lockfile, konfigurasi root, base tsconfig, script bundling, aturan lint bersama, dan template environment. Jika file seperti ini berubah, selective CI yang terlalu optimistis bisa menyesatkan.

Solusi paling aman adalah membuat daftar file atau path yang memicu full run. Pendekatan ini lebih konservatif, tetapi jauh lebih dapat diandalkan.

3. Cache yang menyesatkan

Cache sangat membantu performa CI, tetapi juga bisa membuat debugging lebih sulit. Jika cache mengembalikan hasil lama untuk target tertentu, tim bisa mengira selective CI berjalan benar padahal ada masalah dalam penentuan dampak atau input build.

Beberapa tips:

  • Jika hasil terasa janggal, lakukan satu kali eksekusi tanpa cache untuk verifikasi.
  • Pastikan input yang memengaruhi target memang tercakup dalam konfigurasi task dan cache.
  • Jangan langsung menyalahkan Nx Affected jika akar masalahnya adalah invalidasi cache yang tidak lengkap.

Prinsip debugging: pisahkan dulu masalah penentuan project affected dari masalah cache. Jalankan affected tanpa optimasi lain untuk melihat apakah daftar project yang dipilih sudah benar.

4. Checkout Git yang terlalu dangkal

Jika runner CI hanya mengambil satu commit terbaru, base commit bisa tidak tersedia. Akibatnya, perhitungan perubahan menjadi gagal atau tidak akurat. Ini kesalahan operasional yang sering terjadi saat tim mencoba mempercepat checkout.

Solusinya sederhana: pastikan histori yang diperlukan tersedia di runner, terutama untuk event pull request dan push yang membandingkan lebih dari satu commit.

Pola implementasi yang praktis dan andal

Jika Anda ingin selective CI yang hemat tetapi tetap aman, pola berikut biasanya bekerja baik:

  1. Gunakan Nx Affected untuk PR dan branch kerja harian.
  2. Pastikan base/head commit selalu dihitung dari konteks event CI yang benar.
  3. Terapkan fetch Git yang memadai, jangan terlalu dangkal.
  4. Buat daftar file global yang memicu full run.
  5. Jalankan full run di branch utama, branch rilis, atau job terjadwal.
  6. Audit dependency tersembunyi secara berkala saat ada hasil CI yang mencurigakan.

Pola ini menyeimbangkan tiga tujuan yang sering saling tarik-menarik:

  • Feedback cepat untuk developer.
  • Biaya runner lebih rendah karena job yang tidak relevan tidak dijalankan.
  • Keandalan pipeline karena masih ada titik kontrol konservatif.

Tips debugging saat hasil affected tidak sesuai ekspektasi

Ketika selective CI terasa salah, cek hal-hal ini secara berurutan:

  1. Validasi range commit: apakah base dan head benar?
  2. Cek histori Git di runner: apakah commit pembanding tersedia?
  3. Tinjau file yang berubah: apakah perubahan terjadi di file global atau shared?
  4. Periksa dependency graph: apakah relasi antar project benar-benar tercermin?
  5. Bandingkan dengan full run: apakah masalahnya di daftar project affected atau di cache/task itu sendiri?

Debugging akan lebih cepat jika Anda menyimpan log SHA pembanding di output CI. Dengan begitu, tim bisa mereproduksi perintah yang sama di lokal dan membandingkan hasilnya.

Penutup

Selective CI monorepo dengan Nx Affected agar build lebih hemat adalah pendekatan yang sangat praktis untuk mengurangi pemborosan pipeline. Dengan hanya menjalankan lint, test, dan build pada project yang benar-benar terdampak, tim mendapatkan feedback lebih cepat dan biaya runner lebih terkendali.

Namun, selective CI bukan fitur yang bisa dilepas tanpa pengamanan. Keberhasilannya bergantung pada dependency graph yang akurat, pemilihan base/head commit yang tepat, serta fallback full run untuk perubahan global atau branch penting. Jika tiga hal itu dijaga, Nx Affected bisa menjadi fondasi CI monorepo yang lebih efisien tanpa mengorbankan keandalan.