Monorepo CI dengan Turborepo cocok ketika pipeline mulai lambat karena setiap push menjalankan lint, test, dan build untuk seluruh workspace, padahal perubahan sering hanya menyentuh sebagian package. Dengan Turborepo, Anda bisa membangun task graph, memanfaatkan cache, dan mem-filter task agar CI hanya mengerjakan target yang benar-benar terdampak.

Untuk tim JavaScript/TypeScript, pendekatan ini biasanya memberi dua manfaat utama: feedback loop lebih cepat dan penggunaan resource CI lebih efisien. Namun hasilnya bergantung pada konfigurasi yang benar, terutama pada deklarasi dependency task, input/output cache, serta environment variable yang memengaruhi build.

Kapan pendekatan ini relevan

Pendekatan ini paling berguna jika repo Anda memiliki beberapa aplikasi atau package yang saling bergantung, misalnya frontend, backend, shared UI, config package, dan library internal. Jika setiap perubahan masih diuji dengan pipeline penuh, waktu tunggu akan meningkat seiring pertumbuhan monorepo.

Di sisi lain, jika repo hanya berisi satu aplikasi kecil atau dependency antar package sangat sederhana, kompleksitas Turborepo mungkin belum sepadan. Pipeline penuh per push lebih mudah dipahami dan lebih murah secara operasional untuk tim kecil.

Struktur monorepo sederhana

Berikut contoh struktur repo yang umum untuk tim JavaScript/TypeScript:

repo/
├─ apps/
│  ├─ web/
│  │  ├─ package.json
│  │  └─ src/
│  └─ api/
│     ├─ package.json
│     └─ src/
├─ packages/
│  ├─ ui/
│  │  ├─ package.json
│  │  └─ src/
│  ├─ config-eslint/
│  │  └─ package.json
│  └─ tsconfig/
│     └─ package.json
├─ package.json
├─ turbo.json
└─ pnpm-workspace.yaml

Contoh di atas tidak bergantung pada package manager tertentu, tetapi pola monorepo semacam ini paling sering dipakai dengan workspace tool seperti pnpm, npm workspaces, atau Yarn workspaces.

Cara kerja task graph di Turborepo

Konsep inti Turborepo adalah task graph. Setiap task seperti lint, test, atau build didefinisikan di turbo.json, lalu Turborepo menyusun urutan eksekusi berdasarkan dependency antar package dan antar task.

Misalnya, jika apps/web bergantung pada packages/ui, maka task build untuk web harus menunggu build milik ui. Ini biasanya dinyatakan dengan dependensi seperti ^build, yang berarti jalankan task build pada dependency package terlebih dahulu.

Contoh konfigurasi dasar:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "lint": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    }
  }
}

Penjelasannya:

  • lint umumnya tidak menghasilkan artefak, jadi outputs bisa kosong.
  • test kadang perlu hasil build dependency, terutama jika package library perlu dibundel atau dikompilasi sebelum diuji oleh package lain.
  • build mendeklarasikan artefak yang boleh dicache, misalnya dist/** untuk library atau .next/** untuk aplikasi tertentu.

Jangan memasukkan output yang terlalu luas. Jika Anda mencache folder yang berisi file sementara, log, atau artefak yang tidak stabil, peluang cache miss palsu atau cache hit yang salah akan meningkat.

Strategi memisahkan lint, test, dan build

Salah satu kesalahan umum adalah menggabungkan semua pemeriksaan ke satu task besar. Ini membuat cache kurang efektif dan menyulitkan debugging. Lebih baik pisahkan concern per task:

  • lint: validasi statis, cepat, tanpa output.
  • test: verifikasi perilaku, bisa tanpa output atau hanya laporan tertentu jika diperlukan.
  • build: menghasilkan artefak yang bisa digunakan package lain atau dipakai deploy.

Contoh skrip pada package:

{
  "name": "@repo/ui",
  "scripts": {
    "lint": "eslint src",
    "test": "vitest run",
    "build": "tsup src/index.ts --dts --format esm,cjs"
  }
}

Untuk aplikasi:

{
  "name": "@repo/web",
  "scripts": {
    "lint": "eslint .",
    "test": "vitest run",
    "build": "next build"
  }
}

Pemisahan ini penting karena:

  1. Lint biasanya paling cepat, sehingga cocok dijalankan lebih awal untuk gagal cepat.
  2. Test bisa diparalelkan setelah dependency yang relevan siap.
  3. Build adalah task paling mahal, sehingga cache memberi dampak terbesar di sini.

Cache lokal vs remote cache

Cache lokal

Cache lokal disimpan di mesin tempat task dijalankan. Ini sangat berguna untuk pengembangan lokal karena developer dapat mengulang command tanpa membangun ulang task yang sama.

Kelebihan:

  • Sederhana, tidak perlu layanan tambahan.
  • Cepat di mesin yang sama.
  • Cocok untuk workflow developer sehari-hari.

Kekurangan:

  • Tidak membantu banyak di CI ephemeral, karena runner biasanya baru setiap job.
  • Cache tidak dibagikan antar developer atau antar eksekusi CI yang berbeda.

Remote cache

Remote cache memungkinkan hasil task dibagikan antar mesin. Jika satu job CI sudah menjalankan build untuk package tertentu dengan input yang sama, job lain atau developer lain dapat mengambil hasilnya dari cache tanpa menghitung ulang.

Ini paling terasa manfaatnya saat:

  • CI menggunakan runner sementara yang selalu bersih.
  • Banyak branch aktif dengan perubahan yang berulang pada package yang sama.
  • Build frontend atau library cukup mahal.

Trade-off utamanya:

  • Ada biaya penyimpanan dan transfer data.
  • Perlu perhatian pada keamanan artefak dan kredensial.
  • Konfigurasi input/output yang buruk akan membuat cache tidak akurat.

Secara praktis, cache lokal menjawab kebutuhan developer productivity, sedangkan remote cache menjawab kebutuhan CI scalability.

Konfigurasi dasar turbo.json yang praktis

Berikut contoh konfigurasi yang lebih realistis untuk pipeline CI monorepo:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "lint": {
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "coverage/**"]
    }
  }
}

Namun ada catatan penting: jangan otomatis memasukkan coverage/** ke output build kecuali memang coverage dihasilkan sebagai artefak task yang ingin dicache dan isinya stabil. Banyak tim justru memisahkan coverage dari cache utama karena file-nya sering berubah dan memperbesar ukuran cache tanpa manfaat besar.

Konfigurasi yang lebih aman sering kali seperti ini:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "lint": {
      "outputs": []
    },
    "typecheck": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    }
  }
}

Intinya, cache paling efektif jika artefak:

  • mahal untuk dihitung ulang,
  • deterministik,
  • benar-benar dipakai ulang oleh task lain atau oleh CI berikutnya.

Filter task agar hanya package terdampak yang berjalan

Fitur penting lain pada Monorepo CI dengan Turborepo adalah filter task. Tujuannya agar CI tidak menjalankan task untuk seluruh workspace, melainkan hanya package yang berubah beserta dependensi atau dependent yang relevan.

Contoh penggunaan umum:

turbo run lint test build --filter=...[origin/main]

Secara konsep, filter ini membandingkan perubahan terhadap branch acuan dan memilih workspace yang terdampak. Detail perilaku filter dapat berbeda tergantung bentuk ekspresi yang Anda pakai, tetapi prinsipnya sama: jalankan hanya task pada area monorepo yang terpengaruh perubahan.

Contoh lain untuk package tertentu:

turbo run build --filter=@repo/web

Atau untuk package tertentu beserta dependency-nya:

turbo run build --filter=@repo/web...

Dalam praktik CI, pola yang paling sering dipakai adalah:

  • PR: jalankan task hanya untuk package terdampak.
  • Push ke branch utama: jalankan task terdampak, dan bila perlu tambahkan job penuh terjadwal atau pada event tertentu.
  • Release: jalankan verifikasi lebih luas atau penuh, tergantung tingkat risiko.

Keuntungan filter task:

  • Waktu feedback lebih singkat untuk perubahan kecil.
  • Biaya runner berkurang.
  • Lebih mudah menskalakan monorepo besar.

Keterbatasannya:

  • Butuh pemahaman dependency graph yang benar.
  • Perubahan pada file global seperti konfigurasi lint, TypeScript, atau environment build bisa berdampak lebih luas daripada yang tampak dari file yang berubah.

Integrasi dasar dengan GitHub Actions

Berikut contoh workflow GitHub Actions sederhana untuk PR. Fokusnya pada checkout penuh, setup runtime, install dependency, lalu menjalankan task Turborepo dengan filter terdampak.

name: ci

on:
  pull_request:
  push:
    branches:
      - main

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

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

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint, test, build affected packages
        run: pnpm turbo run lint test build --filter=...[origin/main]

Ada dua hal penting pada contoh tersebut:

  • fetch-depth: 0 diperlukan agar Git memiliki riwayat yang cukup untuk menghitung perubahan terhadap branch acuan.
  • Perintah filter berbasis Git akan lebih andal jika ref pembanding memang tersedia di runner.

Jika Anda memakai remote cache, biasanya perlu menambahkan environment variable atau secret yang dibutuhkan oleh penyedia cache tersebut. Nama variabelnya bergantung pada layanan yang digunakan, jadi jangan mengasumsikan format yang sama di semua implementasi.

Pemisahan job CI

Untuk repo yang lebih besar, pertimbangkan memisahkan job:

  • quality: lint dan typecheck.
  • test: unit/integration test untuk package terdampak.
  • build: build package terdampak, sering kali paling cocok memanfaatkan remote cache.

Namun pemisahan terlalu granular juga punya biaya:

  • lebih banyak startup time per job,
  • lebih rumit mengelola artifact dan dependency antar job,
  • potensi duplikasi install dependency jika tidak dioptimalkan.

Jika monorepo masih menengah, satu job dengan task Turborepo sering cukup sederhana dan efektif.

Menghindari cache salah hit dan cache miss yang tidak perlu

Cache hanya berguna jika akurat. Dua masalah utama adalah:

  • cache miss berlebihan: task sebenarnya bisa dipakai ulang, tetapi dianggap berubah.
  • cache hit yang salah: task dianggap sama padahal hasil seharusnya berbeda.

1. Environment variable yang memengaruhi output

Jika build bergantung pada environment tertentu, cache harus membedakan nilai yang relevan. Contohnya:

  • NODE_ENV
  • base URL API saat proses build statis
  • flag feature yang di-inject ke bundle

Kesalahan umum adalah membiarkan build memakai environment yang berubah, tetapi cache tidak mengetahuinya. Akibatnya, hasil build dari satu environment bisa terpakai di environment lain.

Prinsip aman:

  • Minimalkan env yang memengaruhi artefak build.
  • Jangan menyuntikkan nilai yang tidak stabil jika tidak perlu.
  • Pastikan task yang sensitif terhadap env diperlakukan berbeda antar environment.

2. Artefak tidak deterministik

Beberapa tool menulis timestamp, path absolut, atau metadata mesin ke output. Ini membuat hasil build berubah meskipun source code sama. Dampaknya cache menjadi kurang efektif.

Yang perlu diperiksa:

  • apakah tool build menghasilkan file dengan timestamp,
  • apakah path runner masuk ke source map atau metadata,
  • apakah file output memuat urutan yang tidak stabil.

Jika artefak seperti ini tidak bisa dibuat deterministik, pertimbangkan untuk tidak mencache output tersebut atau pisahkan dari task yang lebih stabil.

3. File global yang terlupakan

Dalam monorepo, perubahan pada file seperti berikut sering berdampak luas:

  • tsconfig dasar,
  • konfigurasi ESLint bersama,
  • script build shared,
  • lockfile dependency.

Jika perubahan file global tidak masuk ke perhitungan task yang relevan, hasil cache bisa salah. Ini sering muncul ketika package terlihat tidak berubah, padahal perilaku build/test sebenarnya berubah karena konfigurasi bersama.

4. Output cache terlalu luas

Jangan masukkan seluruh direktori project sebagai output. Fokus pada artefak final yang memang dibutuhkan, misalnya dist/** atau folder build framework. File sementara seperti cache test runner, log, dan screenshot debug biasanya tidak cocok dimasukkan ke remote cache utama.

Pola pipeline yang umum dipakai

Pola 1: Semua task terdampak pada setiap PR

Ini pola paling seimbang untuk banyak tim.

  • PR menjalankan lint, test, build hanya untuk package terdampak.
  • Main branch tetap cepat.
  • Remote cache membantu branch lain yang memiliki state serupa.

Cocok jika monorepo cukup aktif dan build mulai terasa mahal.

Pola 2: PR selektif, verifikasi penuh terjadwal

Tambahkan pipeline penuh secara nightly atau sebelum release.

  • PR tetap cepat.
  • Ada jaring pengaman terhadap dependency tersembunyi atau salah konfigurasi graph.
  • Biaya CI tetap terkontrol.

Ini sering menjadi kompromi praktis antara kecepatan dan rasa aman.

Pola 3: Pipeline penuh per push

Masih layak jika:

  • repo kecil,
  • build/test cepat,
  • tim ingin kesederhanaan maksimal,
  • dependency graph belum stabil.

Jangan memaksakan selective CI jika biaya kompleksitas lebih besar daripada penghematan waktu.

Trade-off biaya, kecepatan feedback, dan kompleksitas

Turborepo bukan hanya soal mempercepat build. Ia mengubah cara Anda mendesain CI.

Keuntungan

  • Feedback lebih cepat untuk perubahan kecil.
  • Runner lebih efisien karena pekerjaan yang tidak perlu dieliminasi.
  • Skalabilitas lebih baik saat jumlah package bertambah.

Biaya dan konsekuensi

  • Konfigurasi lebih sensitif: dependency task dan output harus benar.
  • Debugging lebih kompleks dibanding pipeline linear penuh.
  • Remote cache menambah biaya operasional dan perlu pengelolaan akses.

Kapan cocok

  • Monorepo memiliki banyak package atau aplikasi.
  • Build lint/test mulai memakan waktu signifikan.
  • Tim siap menjaga hygiene konfigurasi monorepo.

Kapan kurang cocok

  • Repo masih kecil dan pipeline penuh masih cepat.
  • Build tidak deterministik dan sulit distabilkan.
  • Tim belum punya visibilitas dependency antar package.

Tips debugging saat hasil CI terasa aneh

  • Bandingkan hasil menjalankan task dengan dan tanpa cache untuk melihat apakah masalah berasal dari cache atau dari tool build itu sendiri.
  • Periksa apakah file konfigurasi global ikut memengaruhi task yang salah.
  • Pastikan base branch untuk filter benar-benar tersedia di runner CI.
  • Audit output task: apakah ada file sementara, timestamp, atau artefak debug yang ikut tercache.
  • Jika test bergantung pada hasil build dependency, pastikan dependency task dideklarasikan jelas, bukan mengandalkan urutan kebetulan.

Masalah selective CI paling sering bukan pada Turborepo itu sendiri, melainkan pada asumsi dependency yang tidak eksplisit. Jika package A diam-diam memakai hasil dari package B tanpa deklarasi yang benar, filter task dan cache akan terlihat “salah” padahal graph-nya yang tidak lengkap.

Checklist implementasi

  1. Susun workspace monorepo dengan dependency package yang jelas.
  2. Definisikan script lint, test, build di setiap package yang relevan.
  3. Buat turbo.json dengan dependsOn yang mencerminkan dependency graph nyata.
  4. Tentukan outputs hanya untuk artefak final yang stabil dan berguna.
  5. Pisahkan task quality, test, dan build agar cache lebih efektif.
  6. Gunakan filter task di CI untuk menjalankan package terdampak saja.
  7. Pastikan checkout Git di CI memiliki history yang cukup untuk operasi berbasis perubahan.
  8. Aktifkan remote cache jika runner CI bersifat ephemeral dan build cukup mahal.
  9. Audit environment variable yang memengaruhi hasil build.
  10. Hindari mencache artefak yang tidak deterministik atau terlalu besar.
  11. Tambahkan verifikasi penuh berkala jika selective CI belum sepenuhnya dipercaya.

Penutup

Monorepo CI dengan Turborepo memberi hasil terbaik ketika tiga hal dijaga bersamaan: task graph yang akurat, cache yang disiplin, dan filter task yang sesuai dependency nyata. Jika dikonfigurasi dengan benar, tim JavaScript/TypeScript bisa mengurangi waktu tunggu CI tanpa mengorbankan kualitas verifikasi.

Mulailah dari konfigurasi sederhana: pisahkan lint, test, dan build, aktifkan filter untuk package terdampak, lalu tambahkan remote cache saat bottleneck CI mulai jelas. Dengan pendekatan bertahap, Anda bisa mendapatkan manfaat performa tanpa langsung menambah kompleksitas yang belum diperlukan.