Masalah: Eksekusi Redundan pada Pipeline CI Monorepo

Pada arsitektur monorepo React Native yang memisahkan aplikasi (misal: apps/mobile) dan paket internal (misal: packages/ui, packages/utils), GitHub Actions mengeksekusi linting, TypeScript typecheck (tsc), dan pengujian unit (Jest) secara berulang pada setiap Pull Request. Ketika developer hanya memodifikasi packages/utils, eksekusi tsc dan jest tetap dijalankan di apps/mobile jika skrip CI memakai runner standar.

Runner GitHub Actions bersifat ephemeral: direktori kerja dibuang setelah job selesai. Cache lokal Turborepo (.turbo/cache) hilang bersamaan dengan musnahnya virtual machine runner. Solusinya adalah Turborepo Remote Cache, yang menyimpan artefak komputasi dan hash eksekusi pada object storage eksternal agar bisa digunakan lintas runner dan workstation developer.

Prinsip Task Hashing dan Pencegahan Cache Poisoning

Turborepo menentukan cache hit atau cache miss berdasarkan kalkulasi hash input task. Komponen pembentuk hash mencakup:

  • Hash git commit atau konten file yang ditentukan pada properti inputs.
  • Nilai hash dari dependency graph internal antar-paket monorepo.
  • Nilai environment variable yang didaftarkan pada env dan globalEnv.
  • Konfigurasi task di turbo.json.

Cache poisoning terjadi saat runner mengunduh artefak valid dari hash yang cocok, namun runtime menghasilkan keluaran cacat akibat dependensi tersembunyi yang tidak terdaftar di hash (contoh: membaca file dinamis di luar deklarasi, atau bergantung pada environment variable lokal). Menetapkan inputs dan env secara ketat adalah mitigasi wajib.

Konfigurasi Konkret: turbo.json

Berikut konfigurasi turbo.json yang mengisolasi input spesifik untuk lint, typecheck, dan test, serta mendefinisikan output compiler TypeScript.

{
  "$schema": "https://turbo.build/schema.json",
  "globalEnv": ["CI", "NODE_ENV"],
  "tasks": {
    "lint": {
      "inputs": [
        "src/**/*.{ts,tsx,js,jsx}",
        ".eslintrc*",
        ".eslintignore",
        "package.json"
      ],
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^typecheck"],
      "inputs": [
        "src/**/*.{ts,tsx}",
        "tsconfig*.json",
        "package.json"
      ],
      "outputs": ["tsconfig.tsbuildinfo"]
    },
    "test": {
      "dependsOn": ["^test"],
      "inputs": [
        "src/**/*.{ts,tsx,js,jsx}",
        "jest.config.*",
        "**/__tests__/**",
        "**/__mocks__/**",
        "package.json"
      ],
      "outputs": ["coverage/**"],
      "env": ["API_URL", "JEST_WORKER_ID"]
    }
  }
}
Catatan konfigurasi: Pada typecheck, gunakan opsi --incremental dan --tsBuildInfoFile tsconfig.tsbuildinfo pada skrip package.json agar Turborepo dapat menyimpan artefak incremental cache TS ke remote storage.

Integrasi GitHub Actions dengan Remote Cache

Gunakan autentikasi Vercel Remote Cache atau custom self-hosted remote cache (misal: S3-compatible via turbo-cache server) menggunakan environment variable TURBO_TOKEN dan TURBO_TEAM.

name: CI Pipeline

on:
  pull_request:
    branches: [main, staging]

jobs:
  validate:
    runs-on: ubuntu-latest
    env:
      TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
      TURBO_TEAM: ${{ vars.TURBO_TEAM }}
      TURBO_REMOTE_ONLY: true
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 2

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

      - name: Install pnpm
        run: corepack enable && corepack prepare pnpm@latest --activate

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

      - name: Run Validations via Turborepo
        run: pnpm exec turbo run lint typecheck test --api="https://api.vercel.com" --token="$TURBO_TOKEN" --team="$TURBO_TEAM"

Penggunaan TURBO_REMOTE_ONLY: true mencegah runner menulis cache lokal ke disk runner yang akan segera dimusnahkan, meminimalkan I/O overhead.

Metrik Komparasi: Sebelum vs Sesudah Remote Cache

Data berikut mencerminkan pipeline pada monorepo React Native dengan 1 app target iOS/Android dan 4 shared packages, dijalankan pada GitHub standard runner (2-core vCPU, 7 GB RAM).

Kondisi TaskTanpa Cache (Cold)Dengan Remote Cache (Warm)Efisiensi Durasi
lint (Seluruh Monorepo)1m 45s4s (FULL TURBO)~96%
typecheck (TSC Incremental)2m 30s8s (FULL TURBO)~94%
test (Jest - 1 pkg terisolasi diubah)3m 15s35s (Sebagian hit)~82%
Total Durasi CI7m 30s1m 15s~83%

Panduan Troubleshooting: Mengatasi Cache Miss yang Tidak Diharapkan

1. Absolute Paths pada Snapshots dan Error Logs

Jest atau compiler terkadang menyematkan path absolut (misal: /home/runner/work/repo/...) ke dalam artefak output. Runner lokal developer menggunakan /Users/dev/..., sehingga checksum output berbeda dan memicu invalidasi.

  • Solusi: Konfigurasikan rootDir Jest secara relatif. Hindari path serializer yang mencetak absolute path runner di snapshot test.

2. Line Endings (CRLF vs LF)

Jika developer lokal bekerja di sistem operasi Windows tanpa konfigurasi Git yang tepat, line endings CRLF akan merubah hash file input, menyebabkan cache miss total di runner Linux (Ubuntu).

  • Solusi: Tambahkan file .gitattributes pada root repositori:
* text=auto eol=lf

3. Debugging Task Hashing

Untuk melacak penyebab spesifik cache miss antar commit atau lingkungan lokal vs CI, gunakan flag --summarize atau --dry=json:

pnpm exec turbo run test --summarize

Perintah ini menghasilkan file .turbo/runs/[run-id].json yang mendokumentasikan input files dan representasi hash tiap paket secara granular. Bandingkan file rangkuman antara workstation lokal dan runner GitHub Actions untuk menemukan diff pemicu cache invalidation.