Jika Anda mengelola proyek Next.js di monorepo Turborepo, kebutuhan utamanya biasanya bukan sekadar “CI jalan”, tetapi CI yang cepat, konsisten, dan tidak membuang waktu developer. Pipeline yang baik harus bisa menjalankan lint, type-check, test, dan build hanya untuk paket yang terdampak, memanfaatkan cache, lalu membuat preview per pull request tanpa meninggalkan deployment usang.
Artikel ini membahas implementasi praktis Next.js: CI Pipeline Turborepo untuk Build, Lint, dan Preview PR menggunakan GitHub Actions. Fokusnya adalah otomasi yang masuk akal untuk tim kecil sampai menengah: struktur repo yang jelas, selective task berbasis affected packages, concurrency untuk membatalkan job lama, artifact untuk memisahkan tahap build dan deploy, serta langkah untuk menjaga environment tetap konsisten.
Tujuan pipeline yang akan dibangun
Pipeline yang sehat untuk monorepo biasanya memenuhi beberapa tujuan berikut:
- Cepat: tidak menjalankan semua task untuk semua package jika perubahan hanya terjadi di sebagian repo.
- Konsisten: versi Node.js, package manager, dan command yang dipakai lokal sama dengan CI.
- Dapat dipercaya: lint, type-check, test, dan build berjalan dengan urutan yang jelas.
- Aman untuk kolaborasi: setiap pull request mendapat preview, dan preview lama dibersihkan atau ditimpa agar tidak membingungkan reviewer.
- Efisien: cache dependency dan cache task Turborepo dipakai dengan benar, tanpa mengorbankan reproducibility.
Struktur monorepo yang disarankan
Untuk pembahasan ini, anggap repo memiliki struktur seperti berikut:
repo/
apps/
web/ # aplikasi Next.js utama
docs/ # aplikasi lain, opsional
packages/
ui/ # shared UI components
eslint-config/ # shared lint config
typescript-config/# shared tsconfig
.github/workflows/
ci.yml
preview-cleanup.yml
package.json
turbo.json
pnpm-workspace.yamlNama folder tidak harus sama, tetapi pola apps/ dan packages/ membantu memisahkan aplikasi yang dibuild/deploy dari library internal yang menjadi dependency.
Kenapa struktur ini membantu CI
- Dependency graph lebih jelas: Turborepo dapat menentukan package mana yang terdampak ketika library berubah.
- Task mudah dipisah: lint/test/build untuk app dan package bisa didefinisikan konsisten.
- Selective execution lebih efektif: perubahan di
packages/uibisa memicu buildapps/web, tetapi perubahan di dokumen internal tidak harus membangun semua app.
Menyiapkan task di package.json dan turbo.json
Langkah pertama adalah memastikan setiap workspace memiliki script yang konsisten. Misalnya di root package.json:
{
"name": "my-monorepo",
"private": true,
"packageManager": "pnpm@9",
"scripts": {
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"test": "turbo run test",
"build": "turbo run build"
}
}Lalu di aplikasi Next.js, misalnya apps/web/package.json:
{
"name": "web",
"scripts": {
"dev": "next dev",
"lint": "next lint",
"type-check": "tsc --noEmit",
"test": "vitest run",
"build": "next build"
}
}Untuk konfigurasi Turborepo, gunakan turbo.json yang eksplisit soal dependency task dan output cache:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"lint": {
"outputs": []
},
"type-check": {
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"build": {
"dependsOn": ["^build"],
"outputs": [
".next/**",
"dist/**",
"build/**",
"!.next/cache/**"
]
}
}
}Catatan: output cache harus mencerminkan hasil build yang benar-benar diproduksi oleh package. Jangan memasukkan terlalu banyak path yang berubah-ubah jika tidak diperlukan, karena itu bisa menurunkan efektivitas cache.
Mengapa dependsOn penting
Pada monorepo, aplikasi Next.js sering bergantung pada package internal seperti ui atau config. Dengan dependsOn: ["^build"], saat apps/web dibuild, Turborepo tahu bahwa package dependency di atasnya juga perlu dibuild lebih dulu jika memang punya task build.
Selective task berdasarkan package yang terdampak
Salah satu keuntungan terbesar Turborepo adalah kemampuan menjalankan task hanya pada package yang terdampak perubahan. Dalam konteks pull request, pendekatan umum adalah membandingkan branch saat ini dengan branch target, lalu menjalankan:
turbo run lint type-check test build --filter=...[origin/main]Filter tersebut secara konsep berarti: jalankan task pada workspace yang berubah dan package yang bergantung padanya. Ini penting karena perubahan di library shared bisa memengaruhi aplikasi Next.js yang menggunakannya.
Kapan selective task efektif
- Repo memiliki beberapa app/package dan perubahan biasanya lokal pada subset tertentu.
- Dependency graph internal rapi.
- Task lint/test/build per package sudah terdefinisi konsisten.
Kapan perlu fallback ke full run
- Perubahan menyentuh file root yang memengaruhi seluruh workspace, seperti lockfile, base tsconfig, shared eslint config, atau util build yang dipakai semua package.
- Anda belum yakin dependency graph internal sudah mencerminkan relasi sebenarnya.
- Debugging pipeline sedang dilakukan dan Anda ingin menyingkirkan variabel selective execution.
Praktiknya, banyak tim memakai selective run untuk pull request dan full run untuk branch utama atau release branch.
Workflow GitHub Actions untuk lint, type-check, test, dan build
Berikut contoh workflow GitHub Actions yang bisa dijadikan basis. Contoh ini menggunakan beberapa praktik penting:
- Trigger pada
pull_requestdanpushke branch utama. - Concurrency untuk membatalkan run lama pada branch/PR yang sama.
- Cache dependency melalui setup package manager.
- Artifact untuk menyimpan hasil build preview sebelum deploy.
- Selective task untuk PR, dan full run untuk branch utama bila diperlukan.
name: ci
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- main
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Determine turbo filter for PR
id: scope
shell: bash
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "filter=--filter=...[origin/${{ github.base_ref }}]" >> $GITHUB_OUTPUT
else
echo "filter=" >> $GITHUB_OUTPUT
fi
- name: Lint
run: pnpm turbo run lint ${{ steps.scope.outputs.filter }}
- name: Type check
run: pnpm turbo run type-check ${{ steps.scope.outputs.filter }}
- name: Test
run: pnpm turbo run test ${{ steps.scope.outputs.filter }}
- name: Build
run: pnpm turbo run build ${{ steps.scope.outputs.filter }}
- name: Pack preview artifact
if: github.event_name == 'pull_request'
run: |
tar -czf preview-web.tar.gz apps/web/.next apps/web/package.json
- name: Upload preview artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: preview-web
path: preview-web.tar.gz
retention-days: 3Penjelasan trigger dan concurrency
Bagian ini sering diabaikan, padahal dampaknya langsung terasa oleh tim:
pull_request: pipeline berjalan untuk validasi dan preview setiap ada update PR.pushkemain: berguna untuk memastikan branch utama tetap sehat, termasuk saat merge queue atau squash merge mengubah commit history.concurrency: run lama dibatalkan ketika developer push commit baru ke PR yang sama. Ini mengurangi antrean runner, menghemat waktu, dan mencegah preview dari commit lama menyelesaikan deploy lebih belakangan.
Tanpa cancel-in-progress: true, Anda bisa mengalami kondisi di mana PR sudah punya commit baru, tetapi hasil preview yang muncul justru berasal dari run sebelumnya yang lebih lambat selesai.
Mengapa artifact dipisahkan dari job deploy
Memisahkan job build dari job deploy preview memberi beberapa keuntungan:
- Deploy hanya memakai hasil yang sudah lolos build.
- Debugging lebih mudah karena hasil build bisa diunduh dari workflow.
- Jika deploy gagal karena masalah platform preview, Anda tidak perlu mengulang lint/test/build dari nol.
Dalam praktik nyata, isi artifact dapat berupa output build, metadata commit, atau bundle yang sesuai dengan platform preview yang Anda pakai.
Workflow deploy preview per pull request
Karena platform preview berbeda-beda, contoh di bawah ini dibuat generik. Polanya tetap sama: unduh artifact dari job validasi, deploy ke environment bernama unik berdasarkan nomor PR, lalu publikasikan URL preview ke PR.
name: preview
on:
workflow_run:
workflows: ["ci"]
types:
- completed
concurrency:
group: preview-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
deploy-preview:
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: preview-web
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract artifact
run: tar -xzf preview-web.tar.gz
- name: Deploy preview
id: deploy
run: |
# Ganti perintah ini dengan CLI platform preview yang Anda gunakan
# Simpan URL hasil deploy ke output
echo "url=https://preview.example.com/pr-${{ github.event.workflow_run.pull_requests[0].number }}" >> $GITHUB_OUTPUT
- name: Comment preview URL
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.workflow_run.pull_requests[0];
const url = '${{ steps.deploy.outputs.url }}';
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `Preview siap: ${url}`
});Alur trigger yang perlu dipahami
Contoh di atas menggunakan workflow_run, artinya deploy preview hanya berjalan setelah workflow CI selesai dan statusnya sukses. Ini memisahkan concern validasi dari concern deployment.
Alternatifnya, Anda bisa menaruh deploy preview di workflow yang sama setelah build selesai. Pendekatan terpisah lebih modular, tetapi ada trade-off:
- Kelebihan: alur lebih bersih, deployment tidak berjalan jika CI gagal, debugging lebih mudah.
- Kekurangan: koordinasi artifact antar workflow bisa sedikit lebih rumit.
Untuk tim kecil-menengah, dua pendekatan sama-sama valid. Jika proses masih sederhana, satu workflow dengan beberapa job sering lebih mudah dikelola.
Mencegah preview usang
Masalah umum pada preview PR adalah URL atau environment lama yang masih aktif walau PR sudah diperbarui atau ditutup. Ini membingungkan reviewer dan bisa memboroskan resource.
Strategi yang disarankan
- Gunakan nama environment yang deterministik, misalnya
pr-123. Dengan begitu deploy baru akan menimpa preview lama untuk PR yang sama. - Aktifkan concurrency pada job preview agar deploy lama dibatalkan.
- Bersihkan preview saat PR ditutup menggunakan workflow terpisah.
Contoh workflow cleanup:
name: preview-cleanup
on:
pull_request:
types: [closed]
jobs:
cleanup-preview:
runs-on: ubuntu-latest
steps:
- name: Remove preview environment
run: |
# Ganti dengan CLI/SDK platform preview Anda
echo "Remove preview for PR-${{ github.event.pull_request.number }}"Prinsip penting: preview sebaiknya dipetakan ke nomor PR, bukan ke SHA commit. Jika dipetakan ke SHA, Anda akan menghasilkan banyak preview sementara yang sulit dikelola.
Cache dependency dan cache build: apa yang benar-benar membantu
Pipeline lambat biasanya bukan karena satu hal, tetapi kombinasi dari install dependency, restore cache, task yang terlalu luas, dan build aplikasi yang berulang. Ada dua lapisan cache yang relevan di sini:
1. Cache dependency
Biasanya dikelola oleh action setup package manager. Tujuannya mempercepat instalasi paket dari lockfile yang sama. Ini efektif jika:
- Lockfile tidak sering berubah drastis.
- Semua workspace memakai package manager yang sama.
- CI selalu menjalankan install berbasis lockfile, misalnya
--frozen-lockfile.
Kesalahan umum adalah mengandalkan cache dependency tetapi membiarkan install berjalan tanpa lockfile yang ketat. Akibatnya environment lokal dan CI bisa berbeda.
2. Cache task/build Turborepo
Turborepo menyimpan hasil task berdasarkan input file, dependency graph, environment tertentu, dan output yang dideklarasikan. Ini berguna untuk task seperti build package internal, test tertentu, atau lint pada workspace besar.
Agar cache Turborepo efektif:
- Deklarasikan outputs dengan benar di
turbo.json. - Hindari task yang membaca file acak di luar workspace tanpa terdeteksi input-nya.
- Jaga script build tetap deterministik.
- Jika Anda memakai variabel environment yang memengaruhi output, pastikan nilainya konsisten di CI.
Jangan cache semuanya secara membabi buta
Misalnya, cache folder yang sangat besar tetapi volatil bisa membuat restore cache lebih lambat daripada membangun ulang. Uji bottleneck utama lebih dulu: apakah waktu habis di install, test, atau build Next.js.
Menjaga konsistensi environment antara lokal dan CI
Banyak kegagalan CI di monorepo bukan karena kode rusak, melainkan karena environment drift. Beberapa langkah sederhana bisa mencegahnya:
- Pin versi Node.js dengan
.nvmrcatau mekanisme serupa, lalu pakai file itu juga di GitHub Actions. - Pin package manager di root
package.json. - Gunakan lockfile dan install dengan mode ketat seperti
--frozen-lockfile. - Samakan command lokal dan CI: jika lokal memakai
pnpm turbo run build, jangan gunakan command lain di CI kecuali ada alasan kuat. - Hindari script yang diam-diam bergantung pada tool global.
Untuk tim kecil-menengah, aturan paling praktis adalah: semua task yang dijalankan di CI harus bisa dijalankan developer dari root repo dengan command yang sama.
Mengurangi waktu pipeline tanpa mengorbankan kualitas
Berikut beberapa optimasi yang biasanya memberi hasil nyata:
Pisahkan validasi cepat dan lambat
Lint dan type-check sering memberi feedback lebih cepat daripada build penuh. Jika ingin, Anda bisa memecah job menjadi:
- fast-check: lint + type-check
- test-build: test + build, berjalan setelah fast-check atau paralel sesuai kebutuhan
Trade-off-nya: workflow lebih kompleks, tetapi feedback awal lebih cepat.
Gunakan selective run untuk pull request
Ini adalah penghemat waktu terbesar pada monorepo yang sehat. Namun tetap sediakan satu jalur full validation pada main agar perubahan lintas package tetap tertangkap.
Batalkan job lama
Concurrency sering lebih terasa dampaknya daripada optimasi kecil di script. Jika satu developer push 5 kali ke PR yang sama, Anda tidak ingin 5 build Next.js penuh berjalan bersamaan.
Hindari rebuild yang tidak perlu untuk preview
Bangun artifact sekali di CI, lalu deploy artifact tersebut. Jangan build ulang di job deploy kecuali platform preview memang mewajibkan.
Kesalahan umum dan cara debugging
1. Filter affected tidak menghasilkan package yang diharapkan
Penyebab umum:
fetch-depthterlalu dangkal sehingga base branch tidak tersedia.- Dependency internal tidak dideklarasikan benar di workspace.
- Perubahan ada di file root yang tidak diperlakukan sesuai strategi filter.
Langkah debug:
- Pastikan checkout memakai
fetch-depth: 0. - Jalankan command filter yang sama secara lokal.
- Periksa apakah package internal benar-benar terhubung sebagai dependency.
2. Cache terasa tidak membantu
Penyebab umum:
- Output cache tidak akurat.
- Task tidak deterministik.
- Terlalu banyak file yang berubah sehingga hit rate rendah.
Langkah debug:
- Tinjau
outputsditurbo.json. - Pastikan script tidak menulis timestamp atau file acak ke output.
- Bandingkan durasi restore cache dengan durasi task tanpa cache.
3. Preview URL tidak sesuai commit terbaru
Ini biasanya masalah concurrency atau penamaan environment. Gunakan satu environment tetap per nomor PR dan batalkan deploy lama yang masih berjalan.
4. Build lolos lokal tetapi gagal di CI
Fokuskan pemeriksaan pada:
- Versi Node.js
- Perbedaan lockfile
- Dependency yang tidak tercantum tetapi tersedia secara kebetulan di mesin lokal
- Script yang membaca environment variable yang tidak ada di CI
Checklist adopsi untuk tim kecil-menengah
Jika Anda ingin mengadopsi pipeline ini tanpa membuat perubahan besar sekaligus, gunakan checklist berikut:
- Rapikan struktur monorepo ke pola
apps/danpackages/bila memungkinkan. - Standarkan script
lint,type-check,test, danbuilddi tiap workspace. - Tambahkan
turbo.jsondengandependsOndanoutputsyang jelas. - Samakan environment: pin Node.js, package manager, dan pakai lockfile ketat.
- Buat workflow CI dasar untuk install, lint, type-check, test, build.
- Aktifkan selective run untuk pull request setelah dependency graph tervalidasi.
- Tambahkan concurrency agar run lama dibatalkan otomatis.
- Pisahkan build dan deploy preview dengan artifact jika preview Anda butuh langkah deploy tersendiri.
- Gunakan environment preview per PR, bukan per commit.
- Tambahkan cleanup preview saat pull request ditutup.
- Evaluasi bottleneck nyata sebelum menambah cache atau paralelisasi yang lebih kompleks.
Penutup
Pipeline Next.js: CI Pipeline Turborepo untuk Build, Lint, dan Preview PR yang baik bukan soal menambahkan sebanyak mungkin langkah, tetapi soal mengotomasi hal yang tepat. Untuk monorepo, kombinasi yang paling berdampak biasanya adalah selective task berbasis affected packages, cache yang wajar, concurrency untuk membatalkan job lama, dan preview PR yang selalu merepresentasikan commit terbaru.
Jika Anda mulai dari setup sederhana lalu mengukur bottleneck secara berkala, tim kecil-menengah bisa mendapatkan feedback yang cepat tanpa membuat workflow CI/CD terlalu rumit untuk dipelihara.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!