Pipeline CI Open Source yang reproducible tidak harus bergantung pada fitur khusus GitHub Actions, GitLab CI, Jenkins, atau layanan CI tertentu. Prinsip utamanya adalah memindahkan logika build, lint, test, dan audit dependency ke antarmuka yang dapat dijalankan secara lokal, lalu menjadikan platform CI hanya sebagai eksekutor.
Pendekatan ini terinspirasi gagasan software freedom: tim seharusnya dapat memahami, menjalankan, mengaudit, dan memindahkan proses pengembangan tanpa terikat pada satu vendor. Makefile atau task runner menjadi kontrak lokal, container menyediakan lingkungan yang konsisten, sedangkan konfigurasi CI cukup mengorkestrasi perintah standar tersebut.
Masalah yang Diselesaikan
Pipeline sering menjadi sulit dipindahkan karena logika penting tersebar di konfigurasi vendor. Contohnya adalah penggunaan action atau plugin khusus untuk instalasi dependency, cache, pengaturan environment, dan publikasi artefak. Ketika platform diganti, tim harus menulis ulang proses, mempelajari format baru, dan memeriksa kembali perilaku yang sebelumnya tersembunyi.
Masalah reproducibility juga muncul ketika hasil pipeline bergantung pada versi tool yang berubah, image container yang memakai tag mutable, mirror package yang tidak konsisten, atau cache yang berisi dependency dari commit lain. Pipeline yang lebih dapat diaudit memiliki beberapa karakteristik:
- Perintah utama dapat dijalankan dari laptop dan container tanpa mengetahui platform CI.
- Versi runtime, dependency, dan tool dikunci melalui lockfile, checksum, atau digest.
- Setiap tahap memiliki input dan output yang jelas.
- Cache hanya mempercepat proses dan tidak menjadi sumber kebenaran.
- Artefak, log, dan metadata build dapat disimpan atau dipindahkan ke sistem lain.
- Secret hanya tersedia pada tahap yang benar-benar membutuhkannya.
Jadikan Makefile sebagai Antarmuka Lokal
Makefile bukan satu-satunya pilihan. Task runner lain juga dapat digunakan, selama perintahnya portable dan terdokumentasi. Keunggulan Makefile adalah tersedia di banyak sistem Unix, dapat dipanggil dari shell maupun CI, dan tidak bergantung pada API vendor.
Target sebaiknya merepresentasikan aktivitas engineering, bukan detail platform. Contoh berikut mengasumsikan repository memiliki perintah runtime yang sesuai dengan proyek. Perintah yang spesifik bahasa atau framework dapat disesuaikan di setiap target.
.PHONY: ci lint test build audit-deps clean
SHELL := /bin/sh
lint:
./scripts/lint.sh
test:
./scripts/test.sh
build:
./scripts/build.sh
audit-deps:
./scripts/audit-deps.sh
ci: lint test audit-deps build
clean:
rm -rf dist coverage buildPemisahan ke dalam script kecil membantu menghindari Makefile yang terlalu kompleks. Script harus mengaktifkan mode gagal secara eksplisit, misalnya dengan set -eu, memeriksa dependency yang diperlukan, dan mengembalikan exit code bukan nol ketika pemeriksaan gagal.
#!/bin/sh
set -eu
command -v runtime-tool >/dev/null 2>&1 || {
echo "runtime-tool tidak ditemukan" >&2
exit 1
}
runtime-tool test --lockedTarget ci menjadi kontrak utama. Developer dapat menjalankan make ci sebelum membuka pull request, sedangkan CI cukup menjalankan perintah yang sama. Dengan demikian, perbedaan antara “berhasil di laptop” dan “gagal di CI” berkurang karena keduanya menggunakan jalur eksekusi yang sama.
Aturan untuk task yang portable
- Gunakan exit code yang benar; jangan menelan error dengan
|| truekecuali perilaku itu memang disengaja. - Jangan mengandalkan direktori kerja tertentu tanpa menetapkannya secara eksplisit.
- Hindari output yang berisi secret atau token.
- Gunakan environment variable hanya untuk konfigurasi yang memang berubah antarlingkungan.
- Pastikan target dapat dijalankan tanpa credential produksi.
- Dokumentasikan dependency sistem seperti Docker, compiler, database, atau browser headless.
Gunakan Container untuk Lingkungan yang Konsisten
Container membantu menyamakan runtime, package sistem, shell, dan tool antara laptop serta runner CI. Namun, container tidak otomatis reproducible. Image dengan tag seperti latest atau tag versi yang dapat dipindahkan masih bisa berubah di kemudian hari.
Gunakan Dockerfile atau format container yang deklaratif, lalu pin image dasar pada digest setelah digest tersebut diverifikasi dan dicatat dalam repository.
FROM runtime-image:stable@sha256:REPLACE_WITH_VERIFIED_DIGEST
WORKDIR /workspace
COPY . .
# Instalasi menggunakan lockfile yang disimpan di repository.
RUN ./scripts/install-dependencies.sh --locked
ENTRYPOINT ["./scripts/ci-entrypoint.sh"]Nilai digest pada contoh harus diganti dengan digest image yang benar-benar dipilih organisasi. Jangan menyalin digest contoh secara membabi buta. Image juga perlu diperbarui secara berkala melalui pull request terjadwal, karena pinning yang tidak pernah diperbarui dapat mempertahankan kerentanan lama.
Ada dua pola umum penggunaan container:
- Container sebagai runner: seluruh target
make cidijalankan di dalam image yang berisi runtime dan tool proyek. - Container sebagai service: pipeline dijalankan pada runner biasa, sementara database, browser, atau service pendukung dijalankan sebagai container.
Pola pertama biasanya lebih mudah dipindahkan karena lebih sedikit bagian bergantung pada host. Pola kedua dapat lebih sederhana ketika platform CI sudah memiliki dukungan service container, tetapi detail orkestrasi tersebut sering berbeda antar-vendor.
Jangan menyamakan container dengan sandbox keamanan
Container menyediakan isolasi proses dan filesystem, tetapi bukan pengganti kebijakan keamanan penuh. Hindari menjalankan build dari kode tidak tepercaya dengan privilege berlebihan, jangan memasang socket container ke job tanpa alasan kuat, dan gunakan runner terisolasi untuk pekerjaan yang memproses pull request dari fork.
Rancang Tahap Lint, Test, Build, dan Audit Dependency
Empat tahap ini sebaiknya memiliki tujuan dan output yang berbeda. Lint mendeteksi masalah gaya atau pola kode sebelum test berjalan. Test memvalidasi perilaku. Build menghasilkan paket deployable. Audit dependency memeriksa integritas lockfile dan risiko dependency yang digunakan.
Lint
Lint harus berjalan tanpa akses ke secret produksi. Jika lint memerlukan format otomatis, pisahkan pemeriksaan dari perubahan file agar CI dapat mendeteksi hasil yang belum diformat.
lint:
./scripts/format-check.sh
./scripts/static-analysis.shTest
Gunakan mode dependency terkunci, bukan instalasi yang menghitung ulang versi berdasarkan rentang semver. Test yang membutuhkan database sebaiknya memakai database sementara di container atau service lokal yang versinya juga dikunci.
test:
./scripts/install-dependencies.sh --locked
./scripts/test.sh --report junit --coverage-dir coveragePastikan nama file laporan dan direktori coverage diketahui oleh pipeline. Jangan membuat test bergantung pada urutan eksekusi atau jaringan eksternal jika hal tersebut tidak diperlukan.
Build
Build sebaiknya menghasilkan direktori atau paket yang bisa dikonsumsi tahap berikutnya. Jika build menghasilkan image container, gunakan konteks source yang bersih dan catat commit, digest base image, serta versi tool yang digunakan.
build:
./scripts/build.sh --output dist
./scripts/write-build-metadata.sh --output dist/build-metadata.jsonPemeriksaan dependency
Dependency audit memiliki beberapa lapisan yang berbeda dan tidak boleh disamakan. Verifikasi lockfile atau checksum memeriksa apakah paket yang diunduh sesuai deklarasi. Pemeriksaan vulnerability membandingkan versi dependency dengan database advisory. Pemeriksaan license membantu memastikan penggunaan paket sesuai kebijakan organisasi.
Jalankan setidaknya verifikasi dependency yang bersifat deterministik pada setiap perubahan. Vulnerability scan dapat berjalan pada setiap pull request dan dijadwalkan ulang secara berkala karena database advisory terus berubah. Simpan konfigurasi pengecualian secara eksplisit, sertakan alasan, pemilik, dan tanggal peninjauan.
Contoh Orkestrasi CI yang Vendor-Neutral
Daripada menaruh logika di dalam YAML vendor, buat satu entry point yang dapat dipanggil oleh platform mana pun.
#!/bin/sh
set -eu
: "${CI_COMMIT_SHA:=local}"
make lint
make test
make audit-deps
make build
mkdir -p artifacts
cp -R dist artifacts/dist
cp -R coverage artifacts/coverage 2>/dev/null || true
printf '%s\n' "$CI_COMMIT_SHA" > artifacts/commit.txtKonfigurasi platform kemudian hanya perlu melakukan empat hal: checkout source, menyiapkan container atau runtime, menjalankan ./scripts/ci.sh, dan mengunggah direktori artifacts. Nama field YAML untuk image, checkout, cache, dan artefak berbeda antar-platform, sehingga tidak ada satu file konfigurasi yang benar-benar universal. Yang portable adalah kontrak proses dan script-nya, bukan sintaks orkestrator.
Struktur repository yang mendukung pola ini dapat terlihat seperti berikut:
.
├── Makefile
├── Dockerfile.ci
├── lockfile
├── scripts/
│ ├── ci.sh
│ ├── lint.sh
│ ├── test.sh
│ ├── build.sh
│ └── audit-deps.sh
├── .ci/
│ ├── pipeline.example.yml
│ └── README.md
└── docs/
└── reproducible-builds.mdFile .ci/pipeline.example.yml dapat berisi pseudokonfigurasi atau contoh adaptasi untuk platform tertentu. Jangan menjadikannya sumber logika utama. Saat migrasi, file tersebut diganti, sementara Makefile, script, container, dan aturan artefak tetap dipertahankan.
Pinning Tool dan Dependency
Reproducibility membutuhkan lebih dari sekadar lockfile aplikasi. Catat versi atau digest untuk runtime, package manager, formatter, linter, test runner, scanner, dan image container.
- Simpan lockfile di repository dan gunakan mode instalasi terkunci.
- Gunakan checksum atau signature yang disediakan ekosistem tool jika tersedia.
- Pin image container dengan digest, bukan hanya tag.
- Pin action, plugin, atau binary eksternal berdasarkan commit atau checksum jika platform mengharuskannya.
- Jangan mengunduh installer tanpa validasi checksum.
- Jadwalkan pembaruan tool melalui pull request yang dapat ditinjau dan diuji.
Pinning menambah pekerjaan pemeliharaan. Karena itu, gunakan automation untuk membuat usulan update, tetapi tetap lakukan review terhadap changelog, advisory keamanan, dan perubahan kompatibilitas. Pinning bukan alasan untuk membekukan seluruh tool selamanya.
Cache yang Aman dan Dapat Dihapus
Cache harus diperlakukan sebagai optimasi, bukan penyimpanan dependency yang wajib ada. Pipeline harus tetap berhasil ketika cache kosong atau rusak. Cache yang aman biasanya menyimpan package download atau hasil kompilasi yang dapat dibuat ulang, bukan secret atau artefak deploy.
Gunakan key yang memasukkan faktor relevan, seperti sistem operasi, arsitektur, versi runtime, dan hash lockfile. Contoh konsep key:
cache-key = toolchain + operating-system + architecture + hash(lockfile)Hindari key yang hanya memakai nama branch. Cache dari branch yang berbeda dapat memasukkan dependency atau hasil build yang tidak sesuai. Untuk pull request dari fork, pertimbangkan menonaktifkan cache bersama atau gunakan cache read-only agar kode tidak tepercaya tidak dapat meracuni cache yang dipakai branch utama.
Jika cache memengaruhi hasil, bukan hanya waktu, berarti pipeline memiliki masalah desain. Instalasi harus tetap memvalidasi checksum dan versi meskipun paket diambil dari cache.
Artefak, Audit, dan Provenance
Artefak yang berguna bukan hanya binary. Simpan laporan test, coverage, log scan, SBOM jika organisasi membutuhkannya, serta metadata build. Metadata minimal dapat berisi commit source, waktu build, versi tool, digest image dasar, dan status pemeriksaan.
Bedakan artefak sementara dengan artefak rilis. Laporan test boleh memiliki retensi singkat, sedangkan paket rilis memerlukan retensi dan kontrol akses yang lebih ketat. Nama artefak sebaiknya tidak bergantung pada ID internal vendor; gunakan commit SHA, versi aplikasi, atau checksum.
Catatan audit: log yang dapat diaudit bukan berarti semua informasi harus dicetak. Redaksi secret, pembatasan akses, dan retensi yang sesuai tetap diperlukan.
Untuk build yang sangat sensitif, tambahkan verifikasi bahwa source tree bersih, hasil build tidak memasukkan file lokal yang tidak dilacak, dan checksum output dicatat. Reproducibility yang kuat berarti pihak lain dapat mengulang build dengan input yang sama dan memeriksa hasilnya, meskipun beberapa tool tetap memiliki keterbatasan terkait timestamp, urutan file, atau metadata platform.
Secret dan Batas Kepercayaan
Secret harus disediakan hanya pada tahap yang memerlukannya. Lint, unit test, dan audit dependency idealnya tidak memiliki akses ke credential deploy. Tahap publikasi dapat dipisahkan dari tahap validasi dan hanya berjalan setelah policy yang diperlukan terpenuhi.
- Jangan menyimpan secret di Makefile, Dockerfile, image, atau cache.
- Jangan meneruskan secret sebagai argumen command line karena dapat terlihat pada process list atau log.
- Masking log membantu, tetapi bukan pengganti desain yang benar.
- Gunakan credential berumur pendek jika sistem identity mendukungnya.
- Batasi scope token untuk registry atau environment tertentu.
- Untuk pull request dari fork, anggap source sebagai kode tidak tepercaya.
Pendekatan portable tidak berarti semua mekanisme secret harus dibuat sendiri. Secret store terkelola tetap dapat digunakan; yang perlu dihindari adalah membuat seluruh pipeline bergantung pada API vendor tertentu. Abstraksikan pengambilan credential melalui environment variable atau file sementara dengan permission yang tepat.
Trade-off dan Kapan Layanan Terkelola Layak Dipakai
Pipeline portable membutuhkan disiplin dan maintenance tambahan. Tim harus mengelola image, runner, cache, retensi artefak, pembaruan tool, serta observability. Self-hosted runner juga memiliki biaya operasional dan tanggung jawab patching.
Layanan CI terkelola tetap layak digunakan ketika organisasi membutuhkan autoscaling runner, integrasi identity dan secret, audit log, concurrency, dukungan compliance, atau waktu operasional yang lebih rendah. Vendor lock-in dapat dikurangi dengan membatasi fitur khusus vendor pada lapisan orkestrasi, sementara logika engineering tetap berada di Makefile, script, container, dan konfigurasi standar.
Pilihan praktisnya bukan selalu “self-hosted versus managed”. Gunakan layanan terkelola sebagai runner, tetapi pastikan make ci dapat dijalankan secara lokal dan di runner lain. Dengan cara ini, biaya operasional managed service ditukar dengan portability yang masih terjaga.
Checklist Migrasi Antar-Platform
- Inventarisasi semua command yang saat ini tersembunyi di job CI, action, plugin, atau template vendor.
- Pindahkan lint, test, build, dan audit dependency ke Makefile atau task runner.
- Pastikan setiap target dapat dijalankan secara lokal tanpa credential produksi.
- Buat container CI dengan runtime dan tool yang dipin.
- Verifikasi lockfile, checksum, dan mode instalasi terkunci.
- Pisahkan cache dari artefak dan pastikan pipeline tetap berhasil tanpa cache.
- Tentukan direktori artefak, format laporan, retensi, dan metadata build.
- Hilangkan secret dari image, log, cache, dan argumen command.
- Buat entry point tunggal seperti
scripts/ci.sh. - Implementasikan entry point tersebut pada platform baru tanpa mengubah logika build.
- Bandingkan exit code, daftar artefak, checksum, dan hasil test di kedua platform.
- Uji skenario pull request dari fork, cache kosong, dependency registry tidak tersedia, dan runner baru.
- Dokumentasikan komponen yang masih spesifik vendor serta rencana penggantiannya.
Desain ini menghasilkan batas yang jelas: repository memiliki definisi proses engineering, container memiliki lingkungan eksekusi, dan platform CI hanya menyediakan orkestrasi. Batas tersebut tidak menghapus seluruh biaya maintenance, tetapi membuat pipeline lebih mudah dipahami, diuji, diaudit, dan dipindahkan ketika kebutuhan organisasi berubah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!