DevOps
Build Gate: Aturan Arsitektur yang Tak Bisa Dijaga README
September 202611 menit baca

Build gate adalah script milik repository Anda sendiri yang menegaskan sesuatu tentang arsitektur spesifik Anda dan keluar dengan exit code bukan nol ketika penegasan itu gagal. Aturan linter merangkum pengetahuan umum tentang bahasa dan berlaku untuk proyek mana pun. Bedanya penting karena sebagian besar aturan arsitektur adalah fakta tentang satu codebase, misalnya direktori mana yang boleh memuat dependency berat, dan tidak ada linter siap pakai yang bisa mengetahuinya.
ESLint menalar satu file pada satu waktu dan memahami bahasanya, bukan desain Anda. Ia tidak bisa tahu bahwa sebuah komponen yang berjarak tiga import menarik engine 3D ke route yang seharusnya tetap ringan. Menegakkan hal itu menuntut penelusuran import graph lintas file, yang merupakan jenis pemeriksaan berbeda dan biasanya cukup dengan script pendek buatan sendiri.
Serahkan ke review hanya jika aturannya menuntut penilaian soal maksud. Mekanisasi aturannya kalau pelanggarannya mahal sekaligus senyap, karena reviewer tidak bisa diandalkan untuk menyadari rantai import yang menambah berat sebuah route tanpa membaca setiap file di dalamnya. Waktu review adalah lapis pemeriksaan termahal yang Anda punya, jadi pakailah untuk keputusan, bukan untuk invariant yang bisa dijaga script.
Telusuri import graph statis dari setiap file route entry dan laporkan jalur mana pun yang sampai ke dependency tersebut. Ikuti hanya import statis yang punya from-clause, karena dynamic import justru mekanisme yang menjaga dependency itu tetap berada di chunk lazy terpisah, sehingga ia layak diperlakukan sebagai titik potong persis seperti perlakuan bundler.
Ketika aturannya soal selera, ketika pelanggarannya cukup kentara untuk langsung disadari, atau ketika menilai kepatuhannya butuh penimbangan soal maksud. Gate yang menghasilkan false positive akan dilewati lalu dihapus, dan itu lebih buruk daripada tidak punya gate sama sekali, karena tim juga kehilangan kepercayaan pada gate-gate yang sebenarnya bekerja.

Ringkasan Utama
Konvensi tertulis bukanlah batasan. Mengubah aturan arsitektur menjadi script yang menggagalkan build adalah yang membuatnya nyata, dan konversi itu hanya sepadan untuk aturan yang menanggung beban, sering dilanggar, dan bisa diperiksa secara mekanis. Tiga gate prebuild di situs ini memeriksa 49 route entry dalam waktu di bawah tiga detik.
Saya menambahkan dunia 3D yang bisa dijelajahi ke sebuah situs portofolio yang halaman lainnya diukur dalam kilobyte. Aturannya mudah dirumuskan, dan saya menuliskannya di file instruksi repository: game ini tidak boleh membebani halaman yang bukan game. Merumuskannya adalah bagian yang mudah. Satu kalimat di file markdown tidak punya cara untuk gagal, dan three.js justru jenis dependency yang masuk lewat import yang tidak diperiksa siapa pun.
Tulisan ini tentang mengubah kalimat itu menjadi script yang keluar dengan exit code bukan nol, lewat tiga gate yang sekarang berjalan sebelum setiap build situs ini. Saya bahas apa yang dibuktikan masing-masing, berapa biaya menulisnya, dan satu gate yang sempat rilis dalam bentuk lemah, lolos bersih berbulan-bulan, dan selama itu menyembunyikan 168 tanggal yang salah.
Panduan BairesDev tentang maintainable code menyusun lapisan penegakan dalam urutan yang biasanya dibangun tim, dan urutannya memang tepat. Setiap lapis membuat lapis berikutnya lebih murah:
Celahnya ada di bawah ketiganya. Setiap lapis itu memeriksa diff yang ada di depan manusia. Tidak satu pun tahu apa pun tentang arsitektur Anda. ESLint tahu JavaScript; ia tidak tahu bahwa three.js hanya milik satu route di situs ini, dan bahwa satu import dari nav bar akan membatalkan seluruh pengaturan itu. Aturan tersebut bukan code smell. Itu fakta tentang repository ini, dan satu-satunya tempat ia bisa hidup adalah di script milik repository ini sendiri.
Versi aturan yang bisa ditegakkan grep adalah versi lemahnya. Memeriksa bahwa three.js hanya di-import di dalam direktori game hanyalah pencarian string, kira-kira dua puluh baris, dan itu menangkap kesalahan yang kasat mata. Yang tidak tertangkap justru kesalahan yang benar-benar menambah berat halaman.
Aturan yang penting adalah tidak ada route entry yang boleh mencapai engine, dan mencapai itu pertanyaan graph, bukan pertanyaan teks. Satu page yang meng-import satu komponen game yang tampak tidak berbahaya, yang meng-import world, yang meng-import engine, akan mengirim three.js ke first load route tersebut. Tidak ada apa pun di file page itu yang menunjukkannya. Gate harus menelusuri import graph, dan hanya boleh menelusuri edge statis, karena dynamic import justru itulah yang menahan engine keluar dari bundle:
// Static import/export with a from-clause only.
// A bare import("x") has no from-clause, so dynamic imports are cut
// points here by construction — exactly as they are in the bundler.
const STATIC_IMPORT =
/(?:^|\n)\s*(?:import|export)\b([\s\S]*?)from\s*["']([^"']+)["']/g;
function staticImports(file) {
const source = stripComments(fs.readFileSync(file, "utf8"));
const specifiers = [];
for (const [, clause, specifier] of source.matchAll(STATIC_IMPORT)) {
// A type-only import is erased by the compiler and carries no weight.
if (/^\s*type\b/.test(clause)) continue;
specifiers.push(specifier);
}
return specifiers;
}Detail terakhir itulah yang membuat pemeriksaan ini jujur, bukan sekadar perkiraan. Pemanggilan import biasa tidak punya from-clause, jadi tidak pernah cocok dengan pola — artinya dynamic import otomatis menjadi titik potong dalam penelusuran, persis di tempat bundler memotong. Gate menelusuri dari seluruh 49 file route entry di situs ini dan melaporkan rantai import yang bermasalah, bukan cuma nama file, sehingga perbaikannya jelas. Import bertipe type saja dilewati dengan alasan yang sama seperti compiler menghapusnya: tidak menambah berat apa pun.
Registry blog di situs ini terpisah di dua module: satu menyimpan data kartu yang dilihat pembaca, satu lagi menyimpan metadata SEO yang dibaca Google. Keduanya harus sinkron. Versi pertama gate parity membuktikan keduanya sinkron dengan membandingkan dua himpunan key — pemeriksaan yang paling kentara sekaligus paling murah. Gate itu rilis, lolos di setiap build, dan tidak menangkap apa pun.
Tidak menangkap apa pun karena himpunan key memang tidak pernah bergeser. Nilainya yang bergeser. Tanggal yang dilihat pembaca di kartu berasal dari month key dan tahun; tanggal yang dilihat Google berasal dari published date terpisah yang mengisi sitemap dan structured data. Ketika akhirnya saya bandingkan nilainya, bukan key-nya, 168 dari 386 artikel tidak cocok, beberapa meleset sepuluh bulan. Sebuah kartu tertulis Mei 2025 sementara structured data-nya sendiri menyebut Juli 2024. Tidak pernah ada yang memunculkannya, karena kedua nilai itu sah kalau dilihat sendiri-sendiri.
// Version 1: the two key sets agree. Shipped, passed, caught nothing.
const missingMeta = [...staticIds].filter((id) => !metaIds.has(id));
// Version 2: the values agree too. The date a reader sees comes from
// monthKey + year; the date Google sees comes from datePublished.
// Both were individually valid, which is why nothing ever surfaced it.
const dateMismatches = BLOG_POSTS_STATIC.filter((post) => {
const meta = BLOG_META[post.id];
if (!meta) return false;
const [year, month] = meta.datePublished.split("-");
return post.year !== year || post.monthKey !== monthKeyFor(month);
});Gate yang hijau bukan bukti invariant-nya sehat
Centang hijau hanya memberi tahu bahwa assertion yang Anda tulis itu benar. Ia tidak mengatakan apa pun tentang apakah yang Anda assert sudah tepat. Gate parity itu hijau sepanjang masa lemahnya. Saat menulis gate, tanyakan seperti apa bentuk kegagalan yang ingin dicegah itu di dalam data, lalu pastikan gate akan menangkap bentuk spesifik tersebut — paling meyakinkan dengan sengaja merusak sesuatu dan menonton gate-nya gagal.
Penegakan tidak gratis, dan cara jujur untuk memutuskannya adalah dengan menghitung harganya. Berikut setiap aturan yang dimekanisasi situs ini, apa yang dibuktikan, dan berapa ongkosnya:
| Gate | Apa yang dibuktikan | Berapa biayanya |
|---|---|---|
| Lokasi engine | three.js hanya di-import di dalam direktori milik game | Pencarian string, sekitar 20 baris |
| Reachability route | Tidak ada route entry yang mencapai engine lewat import statis | Penelusuran graph depth-first, bagian terbesar dari file 282 baris |
| Prefetch link | Setiap link ke game menonaktifkan viewport prefetch | Pemindaian tag yang sadar kurung kurawal, sekitar 40 baris |
| Parity registry | Tanggal tampil setiap artikel cocok dengan structured data-nya | Sekitar 40 baris, tinggal di dalam module yang dijaganya |
Seluruh rangkaian berjalan sekitar dua setengah detik di setiap build. Angka itu lebih penting daripada jumlah baris: gate yang menambah satu menit ke waktu build akan dimatikan saat insiden pertama, dan tidak pernah kembali. Pemeriksaan prefetch adalah yang paling sulit saya bela kalau berdiri sendiri — tapi nav dan footer dirender di setiap halaman, jadi satu opt-out yang terlewat akan menarik route chunk game ke seluruh situs, sementara pemeriksaannya cuma empat puluh baris. Penegakan murah untuk aturan yang dampaknya luas adalah pertukaran yang mudah diambil.
Sebagian besar konvensi sebaiknya tetap jadi konvensi. Menulis gate untuk aturan yang rusak dua kali dalam sepuluh tahun adalah beban pemeliharaan yang menyamar sebagai jaring pengaman. Empat pertanyaan yang menentukan, dan sebuah aturan harus lolos keempatnya:
Aturan reachability lolos keempatnya, dan itulah alasan ia pantas dibuatkan penelusuran graph. Aturan tentang urutan import tidak lolos satu pun, dan itulah alasan ia tetap urusan linter, atau tidak diurus sama sekali.
Artikel BairesDev berargumen bahwa otomasi tidak bisa menegakkan maksud arsitektural maupun penamaan yang menyampaikan makna bisnis, dan bahwa keduanya butuh penilaian manusia. Soal penamaan, itu persis sesuai pengalaman saya. Soal maksud arsitektural, saya menarik garisnya di tempat lain: maksud yang sudah disederhanakan menjadi klaim struktural bisa dimekanisasi, dan penyederhanaan itulah sebagian besar pekerjaannya. Klaim bahwa engine tidak boleh dicapai route entry adalah maksud arsitektural, sekaligus properti graph yang bisa diputuskan script.
Yang tetap jadi urusan manusia adalah semua yang ada sebelum penyederhanaan itu — memutuskan bahwa game layak punya route sendiri, bahwa batas lazy adalah bentuk yang tepat, bahwa pertukarannya memang sepadan. Script tidak bisa memberi tahu Anda bahwa sebuah module boundary mengikuti garis yang salah. Ia hanya bisa menjaga batas yang sudah Anda pilih. Jadi pembagian yang jujur bukan otomasi lawan penilaian; melainkan penilaian dulu, lalu otomasi supaya penilaian itu tidak terkikis diam-diam.
Sebuah gate hanya sekuat jalur pemanggilan terlemahnya. Gate-gate ini berjalan dari hook prebuild, bukan dari job CI tersendiri, sehingga ikut berjalan di setiap build, di mesin saya maupun di pipeline, tanpa perlu ada yang mengingat satu langkah tambahan:
// package.json — prebuild runs on every "npm run build",
// locally and in CI, with no separate workflow step to forget.
"scripts": {
"prebuild": "node scripts/generate-blog-sources.mjs --check && node scripts/game/check-placements.mjs && node scripts/game/check-imports.mjs",
"build": "next build"
}
// A gate nobody can skip beats a thorough one wired to a job
// that a reviewer is allowed to mark "not required".Hal ini lebih penting daripada kelengkapan pemeriksaan. Job CI terpisah bisa ditandai tidak wajib, dilewati saat hotfix, atau diam-diam dihapus ketika merah karena hal yang tidak berhubungan di suatu Jumat sore. Langkah prebuild menggagalkan build itu sendiri, jadi satu-satunya cara melewatinya adalah memperbaiki masalahnya atau menghapus pemeriksaannya secara sadar — dan penghapusan itu jadi diff yang terlihat dan ditinjau orang. Pesan kegagalannya juga bagian dari desain: masing-masing menyebut file yang bermasalah, rantai import yang menuju ke sana, dan apa yang harus dilakukan, karena gate yang cuma bilang tidak boleh tidak mengajari siapa pun.
Katalog di situs ini sekarang lewat 590 artikel ditambah satu route 3D, dan saya berhenti mengandalkan ingatan sendiri untuk invariant apa pun yang penting. Ujinya sederhana: kalau sebuah aturan benar-benar merugikan saat dilanggar, dan pelanggarannya senyap, tempatnya bukan di file markdown yang cuma bisa dibaca. Tempatnya di script yang keluar dengan exit code bukan nol. Tulis aturannya sekali untuk manusia, lalu tulis sekali lagi untuk mesin, dan biarkan mesin yang tidak pernah lelah.
Sumber dan bacaan lanjutan