Tim kecil di chapter sebelumnya punya satu repository untuk satu produk. Tumbuh sedikit lagi dan bentuknya berubah: satu tim kini memegang situs web, layanan API, dan pustaka utilitas bersama — tiga proyek yang saling menyentuh. Ada dua jawaban struktural: pecah menjadi tiga repository (polyrepo), atau satukan dalam satu repository (monorepo). Monorepo menang saat refactor harus berjalan serentak — mengganti nama fungsi di pustaka beserta seluruh pemakainya dalam satu commit — dan menagih harganya lewat disiplin baru: karena semua tinggal serumah, setiap commit harus menyatakan wilayahnya, dan riwayat yang tadinya alami per repo kini harus bisa dibaca per aplikasi.
Struktur yang Menyatakan Wilayah #
Disiplin monorepo dimulai dari direktori. Struktur berikut adalah repository platform yang kita bangun di sesi ini: tiga wilayah kerja di bawah apps/ dan packages/, satu berkas pemilik di akar:
| Direktori | Isinya | Pemiliknya |
|---|---|---|
apps/web/ | aplikasi situs (HTML, JS) | Budi |
apps/api/ | layanan backend (Python) | Siti |
packages/util/ | pustaka bersama | keduanya |
Struktur ini bukan sekadar kerapian — nama direktori menjadi alamat untuk semua disiplin berikutnya: scope commit, filter riwayat, lingkup CI, sampai pemilik review. Format apps/ + packages/ adalah konvensi yang lazim, bukan ketentuan Git; yang penting konsisten sejak commit pertama.
Commit Scoped dan Riwayat per Path #
Dengan banyak proyek satu rumah, pesan commit wajib menyebut wilayahnya lewat scope — pola type(scope): deskripsi dari Conventional Commits
. Sesi ini menumbuhkan kedua aplikasi beriringan:
mkdir platform && cd platform
git init -b main
git config user.name "Budi Santoso"
git config user.email "[email protected]"
mkdir -p apps/web apps/api packages/util
printf 'beranda web\n' > apps/web/index.html
printf 'print("api")\n' > apps/api/main.py
printf 'util bersama\n' > packages/util/helpers.txt
git add .
git commit -m "chore: scaffold monorepo"
printf '/apps/web/ @budi\n/apps/api/ @siti\n/packages/ @budi @siti\n' > CODEOWNERS
git add CODEOWNERS
git commit -m "chore: tetapkan pemilik berkas"
printf 'function cari() {}\n' > apps/web/cari.js
git add apps/web/cari.js
git commit -m "feat(web): tambah komponen pencarian"
printf 'def cari():\n pass\n' > apps/api/cari.py
git add apps/api/cari.py
git commit -m "feat(api): tambah endpoint pencarian"
printf 'mode gelap\n' > apps/web/tema.css
git add apps/web/tema.css
git commit -m "feat(web): tambah mode gelap"Output yang Dihasilkan (tiga commit terakhir):
[main 6393c24] feat(web): tambah komponen pencarian
1 file changed, 1 insertion(+)
create mode 100644 apps/web/cari.js[main 8f2cbf2] feat(api): tambah endpoint pencarian
1 file changed, 2 insertions(+)
create mode 100644 apps/api/cari.py[main 4f2d80d] feat(web): tambah mode gelap
1 file changed, 1 insertion(+)
create mode 100644 apps/web/tema.cssRiwayat penuhnya bercampur — dan di sinilah path menjadi penyaring. Bandingkan tiga log berikut:
git log --oneline
git log --oneline -- apps/web
git log --oneline -- apps/apiOutput yang Dihasilkan (tiga perintah):
4f2d80d feat(web): tambah mode gelap
8f2cbf2 feat(api): tambah endpoint pencarian
6393c24 feat(web): tambah komponen pencarian
9fc4d9f chore: tetapkan pemilik berkas
bec0f49 chore: scaffold monorepo4f2d80d feat(web): tambah mode gelap
6393c24 feat(web): tambah komponen pencarian
bec0f49 chore: scaffold monorepo8f2cbf2 feat(api): tambah endpoint pencarian
bec0f49 chore: scaffold monorepoHash di mesin pembaca berbeda, bentuknya tidak: log menyaring riwayat — apps/web hanya melihat dua commit fiturnya plus scaffold, apps/api hanya melihat satu. Dengan satu argumen path, git log menjawab “apa yang terjadi pada aplikasi ini” tanpa membaca sisanya — pola yang sama berlaku untuk git diff dan git blame. Inilah alasan scope wajib di pesan commit: tanpa feat(web):, log terfilter tetap terbaca, tetapi daftar commit penuh menjadi teka-teki.
CODEOWNERS: Pemilik per Path #
Berkas CODEOWNERS di akar repository memetakan pola path ke pemiliknya — isinya yang kita commit di sesi di atas:
/apps/web/ @budi
/apps/api/ @siti
/packages/ @budi @sitiSetiap baris berpasangan: pola direktori, lalu pemilik. Platform hosting membacanya dan menuntut persetujuan pemilik setiap pull request yang menyentuh pola itu — PR yang mengubah apps/api/ otomatis menunggu Siti, siapa pun pembuatnya. Mekanisme penegakannya dijelaskan di Protected Branches
, dan perannya dalam mencegah tabrakan antar kontributor dibahas di Menyelesaikan Konflik Kolaborasi
. Format berkasnya didokumentasikan di dokumentasi code owners GitHub
.
Sparse Checkout: CI per Aplikasi #
Pipeline CI untuk apps/web tidak perlu mengerjakan apps/api. Clone penuh tetap memuat seluruh riwayat, tetapi working directory bisa dipangkas ke wilayah yang relevan dengan sparse checkout mode cone:
cd ..
git clone platform ci-web
cd ci-web
git sparse-checkout init --cone
git sparse-checkout set apps/web
ls apps
lsOutput yang Dihasilkan (dua perintah terakhir):
webCODEOWNERS
appsBukti di layar: apps/ hanya berisi web, sementara riwayat Git tetap utuh — git log --oneline -- apps/api tetap bekerja di clone ini. Clone CI menjadi lebih ramping dan pipeline tidak tersesat membaca berkas aplikasi lain; mode cone dan batasannya dibedah lengkap di Sparse Checkout
, rincian perintahnya di dokumentasi git sparse-checkout
.
Tag per Aplikasi atau Tag Global #
Rilis monorepo menuntut satu keputusan penamaan. Tag v1.0.0 tunggal menyatakan “seluruh platform versi 1.0.0” — sederhana, tetapi aplikasi kecil tidak bisa rilis sendiri. Alternatifnya menomori per aplikasi:
| Pendekatan | Bentuk | Cocok saat | Harganya |
|---|---|---|---|
| tag global | v1.3.0 untuk seluruh repo | aplikasi dirilis bersama, satu nomor produk | aplikasi tidak bisa rilis sendiri |
| tag prefiks aplikasi | web-v1.0.0, api-v2.1.0 | aplikasi dirilis independen | konvensi prefiks harus dijaga disiplin |
| campuran | global untuk rilis platform, prefiks untuk darurat per app | kombinasi keduanya | dua konvensi hidup bersamaan |
Pilihan mana pun tetap memakai alat yang sama — tag annotated di titik rilis, pola Workflow Release — yang berbeda hanya konvensi namanya.
Monorepo atau Polyrepo #
Keputusan struktural terakhir ditaruh di meja penuh:
| Aspek | Monorepo | Polyrepo |
|---|---|---|
| Refactor lintas aplikasi | satu commit atomic | PR lintas repo, sinkron manual versi |
| Riwayat per aplikasi | difilter lewat path | alami per repository |
| Ukuran clone | tumbuh besar; sparse checkout meredakan | kecil per repository |
| CI | harus di-scope per path | alami per repository |
| Hak akses per tim | dibatasi lewat CODEOWNERS | alami per repository |
| Keterlihatan lintas proyek | tinggi — semua orang melihat semua | rendah — batas repo membatasi |
Aturan pragmatisnya: refactor serentak yang sering dan tim yang kecil condong ke monorepo; tim yang benar-benar terpisah batasnya, atau kebijakan akses yang berbeda total, condong ke polyrepo.
Monorepo ini papan catur penuh kode. Bentuk repository yang isinya justru bukan kode sama sekali — dokumen, panduan, catatan — punya karakter workflow berbeda: perubahannya murah, pembacanya banyak, dan penilainya bukan reviewer kode melainkan editor. Workflow documentasi, chapter berikutnya, memainkan pola untuk repository yang hidup dari kata-kata.