Bab 8 menunda pembahasan mekanisme source dengan janji "dibahas lebih lanjut di Bab 26", dan Bab 25.3.2 menegaskan ulang janji itu lebih spesifik: fungsi error() dan die() yang baru ditulis di sana disebut "kandidat ideal untuk dipindah ke satu file lib.sh bersama... alih-alih disalin ulang manual ke setiap script baru, pola pengorganisasian yang dibahas lebih lanjut di Bab 26.1". Bab 25.1.1 malah sudah menyinggung lebih jauh, menyebut kustomisasi PS4 berguna "untuk script yang sudah dipecah jadi banyak file lewat source (topik Bab 26.1)". Sementara itu, fungsi log() yang pertama kali ditulis di Bab 19.2.2 diam-diam sudah disalin ulang empat kali ke script berbeda: persis sama di dashboard-server.sh (Bab 19.4.2) dan alert-notifikasi.sh (Bab 20.4.2), lalu mulai bergeser formatnya di deploy.sh (Bab 21.3.2) dan deploy-pipeline.sh (Bab 21.4.2) yang memakai tee -a tanpa parameter level sama sekali. Bab ini menepati seluruh janji itu sekaligus, dibagi jadi empat bagian: mengorganisasi script yang sudah menumpuk lewat multi-file dan source (26.1), mendokumentasikan script lewat teks bantuan, komentar header, dan README (26.2), mengelola perubahan lewat Git supaya kolaborasi antar Sysadmin dan Developer tetap rapi (26.3), lalu menutup dengan pengantar pengujian otomatis lewat bats (26.4).
26.1 Mengorganisasi Script Besar: Multi-File dan Sourcing
Satu script tunggal yang terus bertambah panjang, seperti ops.sh di Bab 24.3.3 atau toolkit-keamanan.sh di Bab 23.4.3, cepat atau lambat mulai menampung logika yang sebenarnya juga dibutuhkan script lain. Bagian ini membahas kapan saatnya memecah logika itu ke file terpisah, dan cara source bekerja untuk menyatukannya kembali saat dijalankan.
26.1.1 Kapan Script Perlu Dipecah Jadi Multi-File
Fungsi log() yang disinggung di bagian pembuka bab ini adalah contoh nyata kenapa duplikasi kode lintas script berbahaya, bukan cuma soal kerapian. Versi awalnya di Bab 19.2.2 menerima level sebagai argumen pertama lalu menuliskannya ke stderr tambahan khusus untuk level ERROR. Versi yang disalin ke dashboard-server.sh dan alert-notifikasi.sh masih setia ke bentuk itu, tapi versi di deploy.sh dan deploy-pipeline.sh sudah menyederhanakannya jadi satu baris tee -a tanpa parameter level. Kalau suatu saat format timestamp log() perlu diperbaiki, atau ditambah dukungan mengirim level ERROR otomatis ke kirim_alert() dari Bab 20.4.2, perbaikan itu harus disalin manual ke lima file berbeda, dan sangat mudah lupa satu di antaranya, apalagi kalau kelima file itu sudah sempat "mekar" jadi bentuk yang sedikit berbeda seperti kasus log() di atas.
Aturan praktis yang layak dipegang: begitu sebuah fungsi (seperti log(), atau error()/die() dari Bab 25.3.2) dipakai lebih dari satu script, itu sinyal kuat untuk dipindah ke satu library, istilah untuk file yang isinya murni kumpulan definisi fungsi dan variabel untuk dipakai ulang, bukan script yang dijalankan langsung. Sebaliknya, jangan buru-buru memecah script sederhana yang baru dipakai di satu tempat menjadi banyak file sejak baris pertama; abstraksi yang dibuat sebelum benar-benar dibutuhkan cuma menambah lapisan yang harus dilacak tanpa manfaat nyata. Toolkit yang sudah menumpuk di sepanjang Bagian 3 dan Bab 24, ops.sh, deploy-pipeline.sh, provision-server.sh, toolkit-keamanan.sh, dan alert-notifikasi.sh, sudah lewat titik itu: kelimanya sama-sama butuh logging yang konsisten, dan setidaknya ops.sh juga akan diuntungkan dari error()/die() untuk validasi argumennya.
26.1.2 Cara Kerja source dan Menyusun lib.sh Bersama
Menurut GNU Bash Reference Manual dan spesifikasi POSIX Shell Command Language untuk dot special built-in, builtin source (dan bentuk singkatnya, satu titik .) membaca dan mengeksekusi isi sebuah file di dalam shell yang sedang berjalan saat itu juga, bukan di subshell atau proses anak terpisah. Kedua sumber ini sepakat pada satu konsekuensi penting: fungsi dan variabel yang didefinisikan di file yang di-source langsung tersedia bagi shell pemanggil begitu source selesai, tanpa perlu export sama sekali, persis kebalikan dari menjalankan script lewat ./script.sh yang selalu berjalan di proses anak terpisah dan tidak pernah membocorkan fungsi atau variabelnya balik ke shell pemanggil. Sourcing, istilah umum untuk tindakan memuat file lewat mekanisme ini, sebenarnya sudah dipakai berulang kali di sepanjang series tanpa penjelasan detail: Bab 16.2.4 memuat common.sh lewat source ~/.bashrc, dan Bab 23.4.3 memuat source /etc/toolkit-keamanan/kredensial.env untuk mengambil kredensial dari file terpisah, bukti bahwa source tidak cuma dipakai untuk fungsi, tapi juga konfigurasi atau variabel sensitif yang sengaja dipisah dari script utamanya.
Satu detail yang wajib diperhatikan saat menulis file library: karena isinya dieksekusi di shell yang sama, statement exit di dalam file yang di-source menghentikan seluruh shell pemanggil, bukan cuma "kembali" dari file itu. Begitu juga set -e, set -u, atau set -o pipefail yang ditulis di dalam file library ikut aktif permanen di shell pemanggil setelah source selesai, karena tidak ada isolasi apa pun antara file yang di-source dan shell yang men-source-nya. Kebiasaan aman untuk file lib.sh: biarkan file itu murni berisi definisi fungsi dan variabel, jangan taruh set, exit tanpa syarat, atau efek samping lain di dalamnya; biarkan script pemanggil yang menentukan opsi set apa yang berlaku untuk dirinya sendiri.
#!/bin/bash
# lib.sh - fungsi bersama untuk toolkit-ops, hanya berisi definisi
: "${LOG_FILE:=/var/log/toolkit-ops.log}"
log() {
local level="$1"
shift
printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" >> "$LOG_FILE"
}
error() {
printf 'Error: %s\n' "$1" >&2
}
die() {
error "$1"
exit "${2:-1}"
}lib.sh di atas menyatukan log() versi Bab 19.2.2 dengan error()/die() dari Bab 25.3.2, jadi satu tempat rujukan. Baris : "${LOG_FILE:=/var/log/toolkit-ops.log}" memakai kembali parameter expansion nilai default dari Bab 6.4.4, kali ini dalam bentuk := yang benar-benar meng-assign nilai default ke LOG_FILE kalau variabel itu belum di-set, bukan cuma menampilkannya sesaat seperti :-; builtin titik dua : di depan cuma perintah kosong (no-op) yang dipakai supaya ekspansi itu punya konteks perintah untuk dijalankan, tanpa mencetak apa pun. Pola ini membiarkan script pemanggil menimpa LOG_FILE miliknya sendiri sebelum memanggil source, sambil tetap punya nilai default yang wajar kalau tidak ditimpa.
#!/bin/bash
set -uo pipefail
source "./lib.sh"
[[ -n "${1:-}" ]] || die "Argumen wajib diisi"
log INFO "Menerima argumen: $1"26.1.3 Sourcing Relatif Lewat BASH_SOURCE
Baris source "./lib.sh" di atas cuma berhasil kalau script dijalankan persis dari direktori yang sama dengan lib.sh. Begitu ops.sh dipanggil dari direktori lain, misalnya lewat path absolut /opt/toolkit-ops/ops.sh saat working directory saat itu adalah /home/deploy, atau dijadwalkan lewat cron (Bab 18) yang tidak menjamin working directory tertentu sama sekali, source "./lib.sh" gagal mencari file itu di lokasi yang salah. Variabel bawaan BASH_SOURCE, yang elemen pertamanya sudah dipakai di Bab 25.1.1 untuk kustomisasi PS4, menyimpan path file script itu sendiri terlepas dari direktori mana script itu dipanggil, sesuai daftar variabel bawaan di GNU Bash Reference Manual. Kombinasi dirname dan BASH_SOURCE[0] jadi cara paling andal menemukan lokasi lib.sh relatif terhadap script itu sendiri, bukan relatif terhadap direktori tempat script dipanggil.
#!/bin/bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib.sh"
[[ -n "${1:-}" ]] || die "Argumen wajib diisi"
log INFO "Menerima argumen: $1"cd "$(dirname "${BASH_SOURCE[0]}")" && pwd pindah sesaat ke direktori tempat script itu berada lalu mencetak path absolutnya, dijalankan di dalam command substitution supaya tidak benar-benar memindahkan working directory shell yang sedang berjalan. Hasilnya, SCRIPT_DIR selalu menunjuk ke lokasi ops.sh yang sebenarnya, dan source "${SCRIPT_DIR}/lib.sh" tetap berhasil dipanggil dari mana pun, termasuk dari dalam crontab yang working directory defaultnya nyaris tidak pernah sama dengan lokasi script. Perlu dicatat, ${BASH_SOURCE[0]} berbeda dari $0: keduanya kebetulan sama saat script dijalankan langsung, tapi BASH_SOURCE tetap menunjuk file yang benar bahkan saat dipanggil dari dalam fungsi atau saat file itu sendiri di-source dari script lain, sesuai definisi resminya di GNU Bash Reference Manual.
Verifikasi pola ini dengan memanggil ops.sh dari beberapa direktori berbeda, bukan cuma dari lokasi lib.sh itu sendiri.
cd /tmp && /opt/toolkit-ops/ops.sh status
crontab -l # pastikan baris cron memanggil path absolut, bukan nama file polosKalau tetap muncul lib.sh: No such file or directory, penyebab paling umum adalah lib.sh ternyata belum ikut disalin ke server saat proses deploy, atau ops.sh dan lib.sh tidak lagi berada di direktori yang sama padahal SCRIPT_DIR mengasumsikan keduanya bertetangga; pastikan struktur direktori 26.1.4 tetap utuh setiap kali toolkit ini disalin ke lokasi baru.
26.1.4 Struktur Direktori Proyek Multi-Script
Setelah lib.sh dipisah, seluruh toolkit yang menumpuk sejak Bab 20 sampai 24 layak diletakkan dalam satu direktori proyek yang jelas strukturnya.
toolkit-ops/
├── lib.sh
├── ops.sh
├── deploy-pipeline.sh
├── provision-server.sh
├── toolkit-keamanan.sh
├── alert-notifikasi.sh
└── README.mdHanya script yang benar-benar dijalankan langsung dari terminal atau cron, seperti ops.sh dan deploy-pipeline.sh, yang perlu chmod +x. lib.sh tidak pernah dieksekusi sendiri, cuma di-source, jadi permission eksekusinya tidak relevan; menjalankannya langsung lewat ./lib.sh pun cuma memuat definisi fungsi lalu keluar tanpa efek apa pun, karena isinya memang sengaja tidak punya baris pemanggilan langsung. Untuk toolkit seukuran ini, satu file lib.sh saja lebih dari cukup; memecahnya lebih jauh lagi jadi lib/log.sh, lib/error.sh, dan seterusnya cuma masuk akal kalau lib.sh sendiri sudah membengkak jadi ratusan baris dengan tanggung jawab yang jelas terpisah, bukan sebagai langkah standar yang wajib diambil sejak awal.
26.2 Dokumentasi Script: Usage, Help Text, dan README
Struktur direktori yang rapi di 26.1 belum tentu langsung dipahami orang lain yang baru pertama kali membuka proyek ini. Bagian ini membahas tiga lapis dokumentasi yang saling melengkapi: teks bantuan yang tampil saat script dijalankan, komentar header untuk yang membuka kode sumbernya, dan README untuk yang baru mengenal proyeknya sama sekali.
26.2.1 Heredoc Bertanda Kutip untuk Teks Bantuan Statis
Bab 12.3.2 memperkenalkan heredoc lewat <<EOF untuk menulis pesan bantuan multi-baris, dan tampilkan_bantuan() di ops.sh (Bab 24.3.3) memakai bentuk sedikit berbeda, <<'USAGE', dengan delimiter yang dibungkus tanda kutip. Menurut GNU Bash Reference Manual, perbedaan keduanya krusial: kalau delimiter heredoc tidak dikutip seperti <<EOF, seluruh baris di dalamnya tunduk pada parameter expansion, command substitution, dan arithmetic expansion persis seperti string dalam tanda kutip ganda; kalau delimiternya dikutip seperti <<'EOF' atau <<'USAGE', seluruh baris di dalamnya diperlakukan apa adanya, tidak ada ekspansi sama sekali. Spesifikasi POSIX Shell Command Language menegaskan aturan yang sama untuk here-document: kutipan pada kata pembatas (baik lewat tanda kutip maupun backslash) menonaktifkan seluruh substitusi di badan dokumennya, konsisten dengan penjelasan GNU Bash Reference Manual di atas.
# Tanpa kutip: $HOME ikut di-expand jadi path sebenarnya
cat <<EOF
Contoh: ./ops.sh deploy --config \$HOME/.ops.conf
EOF
# Dengan kutip: $HOME tercetak apa adanya
cat <<'EOF'
Contoh: ./ops.sh deploy --config $HOME/.ops.conf
EOFUntuk teks bantuan yang isinya statis dan tidak pernah dimaksudkan untuk benar-benar dievaluasi shell, bentuk berkutip adalah pilihan yang lebih aman sebagai kebiasaan default, persis seperti yang sudah dipraktikkan ops.sh di Bab 24.3.3 dan konfigurasi ~/.msmtprc di Bab 20.1.4. Risikonya bukan cuma salah cetak: kalau set -u (Bab 25.1.3) aktif dan teks bantuan tanpa sengaja menyebut variabel yang belum pernah di-set, misalnya sekadar mencontohkan sintaks ${1:-default} di dalam heredoc tanpa kutip, script bisa berhenti dengan error unbound variable justru saat mencoba menampilkan pesan bantuan, ironi yang sepenuhnya bisa dihindari dengan membiasakan delimiter berkutip untuk konten yang memang statis.
26.2.2 Komentar Header: Deskripsi, Dependency, dan Usage
Komentar singkat tepat setelah shebang, berisi tiga baris inti, deskripsi ringkas, dependency yang dibutuhkan, dan satu baris contoh pemakaian, memberi konteks cepat bagi siapa pun yang membuka file itu langsung di editor sebelum sempat menjalankan --help sama sekali.
#!/bin/bash
# toolkit-keamanan.sh - health check + hardening dasar (Bab 23.4)
# Dependency: curl, iptables, flock; wajib dijalankan lewat sudo
# Usage: ./toolkit-keamanan.sh -h <hostname>Komentar header ini melengkapi, bukan menggantikan, fungsi tampilkan_bantuan() dari 26.2.1: komentar header dibaca orang yang sedang membuka kode sumbernya di editor atau meninjau pull request (Bab 26.3), sementara --help dibaca orang yang cuma perlu tahu cara memakainya dari terminal tanpa pernah membuka satu baris kode sumber pun. Jaga komentar header tetap singkat, cukup beberapa baris inti seperti contoh di atas; komentar header yang berubah jadi paragraf panjang justru gampang basi begitu isi script berubah, dan detail selengkap itu lebih pas ditaruh di README (26.2.4).
26.2.3 -h/--help sebagai Konvensi, Bukan Aturan Mutlak
Pola -h|--help) dari parser opsi manual Bab 12.3.2, dipadukan dengan cabang help|*) pada dispatcher Bab 24.3.2, adalah konvensi yang dikenal luas di tool command-line: siapa pun yang lupa cara memakai sebuah script cukup mencoba -h atau --help lebih dulu. Konvensi ini bukan aturan mutlak yang selalu bisa diikuti apa adanya. toolkit-keamanan.sh di Bab 23.4.3 justru memakai -h untuk menerima argumen hostname lewat getopts ":h:", bukan untuk menampilkan bantuan, karena huruf mnemonic paling wajar untuk "host" kebetulan sama dengan huruf mnemonic paling wajar untuk "help".
Situasi seperti ini bukan alasan untuk diam-diam menghilangkan dukungan bantuan sama sekali. Karena getopts tidak bisa mengenali opsi panjang (keterbatasan yang sudah dijelaskan tuntas di Bab 12.3.1), script yang huruf -h-nya sudah terpakai untuk keperluan lain tetap bisa mendukung --help lewat parser manual while/case dari Bab 12.3.2, atau paling minimal menyebutkan eksplisit di pesan bantuannya sendiri bahwa -h di script ini berarti sesuatu yang berbeda dari kebiasaan umum. Yang penting, pengguna tidak pernah dibiarkan menebak sendiri tanpa satu pun jalan resmi meminta bantuan.
26.2.4 Menulis README.md untuk Proyek Multi-Script
README.md menjawab pertanyaan yang tidak dijawab --help maupun komentar header: apa proyek ini secara keseluruhan, apa saja script di dalamnya, dan bagaimana memulainya dari nol. README yang baik untuk toolkit seperti toolkit-ops/ di 26.1.4 setidaknya mencakup lima bagian berikut.
# toolkit-ops
Kumpulan script operasional untuk deploy, monitoring, dan hardening
server aplikasi-web.example.com.
## Prasyarat
- Bash 5.x
- Akses SSH ke `[email protected]` (lihat Bab 21.2.1)
- `curl`, `flock`, `iptables`
## Instalasi
```
git clone [email protected]:tim-ops/toolkit-ops.git
cd toolkit-ops
chmod +x ops.sh deploy-pipeline.sh toolkit-keamanan.sh
```
## Script yang Tersedia
| Script | Fungsi |
|--------------------------|-------------------------------------------|
| `ops.sh` | Wrapper deploy/rollback/status/logs |
| `deploy-pipeline.sh` | Pipeline deploy dengan rollback otomatis |
| `toolkit-keamanan.sh` | Health check dan hardening dasar |
## Pemakaian
```
./ops.sh status
./ops.sh deploy
```
## Testing
Jalankan `bats tests/` sebelum membuka pull request (lihat Bab 26.4).Prinsip pentingnya, README mendokumentasikan cara memakai dan berkontribusi ke seluruh proyek, bukan mengulang detail satu script yang sudah dijelaskan --help-nya sendiri. Sama pentingnya, README wajib diperbarui di komit yang sama dengan perubahan kode yang mengubah perilakunya (Bab 26.3.3), bukan "nanti diperbarui belakangan". README yang sudah basi (menyebut subperintah yang sudah dihapus, atau melewatkan yang baru ditambahkan) lebih berbahaya daripada tidak ada README sama sekali, karena pembaca mempercayainya begitu saja tanpa curiga.
26.3 Versioning Script dengan Git
Proyek toolkit-ops/ yang sudah terstruktur rapi di 26.1 dan terdokumentasi di 26.2 tetap rawan kalau perubahannya cuma diedit langsung lewat vim di server produksi, tanpa jejak siapa mengubah apa dan kenapa. Bagian ini membahas Git sebagai version control, sistem yang mencatat riwayat perubahan sekumpulan file dari waktu ke waktu sehingga versi mana pun bisa ditelusuri atau dikembalikan kapan saja, standar de facto yang dipakai nyaris semua tim software di dunia nyata.
26.3.1 Kenapa Toolkit Script Butuh Version Control
toolkit-keamanan.sh (Bab 23.4.3) berjalan lewat sudo, dan ops.sh (Bab 24.3.3) memegang akses deploy ke server produksi lewat kunci SSH; keduanya bukan script iseng yang bebas diubah sembarangan. Tanpa version control, satu salah ketik yang diedit langsung di server tidak punya jalan mudah untuk dibatalkan, dan tidak ada catatan siapa yang mengubahnya atau alasannya, persis masalah yang sudah disinggung soal pesan error tanpa konteks di Bab 25.3. Begitu lebih dari satu Sysadmin atau Developer ikut mengubah script yang sama, kebutuhan itu makin mendesak: perubahan yang tumpang tindih tanpa alat bantu yang jelas gampang saling menimpa tanpa disadari.
26.3.2 Inisialisasi Repository dan .gitignore
Inisialisasi dimulai persis di direktori toolkit-ops/ dari 26.1.4.
cd toolkit-ops/
git init
git add lib.sh ops.sh deploy-pipeline.sh provision-server.sh toolkit-keamanan.sh alert-notifikasi.sh README.md
git commit -m "Inisialisasi toolkit-ops dari script Bab 19-24"Menambahkan file satu per satu lewat namanya secara eksplisit, seperti contoh di atas, jauh lebih aman dibanding git add -A atau git add ., karena kita tahu persis apa yang benar-benar masuk ke commit. Ini penting khususnya untuk kredensial.env dari Bab 23.4.3: file itu sengaja disimpan di /etc/toolkit-keamanan/, di luar direktori toolkit-ops/ sama sekali, justru supaya secara fisik tidak mungkin ikut ter-commit tanpa sengaja, cara yang jauh lebih andal dibanding cuma mengandalkan .gitignore. Untuk file lain yang tetap perlu dijaga tidak ikut masuk repository, seperti log runtime atau berkas .env lokal yang mungkin dibuat siapa pun untuk pengujian pribadi, .gitignore tetap wajib ada sebagai lapisan pertahanan tambahan.
*.log
*.env26.3.3 Commit Message yang Jelas
Pesan commit yang baik menjelaskan alasan sebuah perubahan dibuat, bukan sekadar mengulang apa yang sudah terlihat jelas dari git diff itu sendiri.
# Kurang informatif
git commit -m "update ops.sh"
# Jauh lebih membantu
git commit -m "Tambah subperintah restart ke ops.sh
Sebelumnya restart service cuma bisa lewat deploy penuh, padahal
kadang cukup restart tanpa perlu tarik kode baru sama sekali."Baris pertama pesan commit sebaiknya tetap ringkas dan berbentuk perintah aktif ("Tambah", "Perbaiki", "Hapus"), sementara detail alasan yang lebih panjang ditaruh di baris-baris berikutnya, dipisah satu baris kosong dari judulnya. Kebiasaan ini terasa manfaatnya justru berbulan-bulan kemudian, saat git log jadi satu-satunya sumber yang menjelaskan kenapa sebuah baris kode ditulis seperti itu, bukan cuma apa isinya.
26.3.4 Branch dan Tag untuk Perubahan Berisiko
Branch, cabang riwayat perubahan yang terpisah dari jalur utama, cocok dipakai sebelum menyentuh script berisiko tinggi seperti deploy-pipeline.sh, supaya perubahan yang belum teruji tidak langsung bercampur dengan versi yang sudah stabil dipakai tim.
git checkout -b tambah-rollback-otomatis
# ...edit deploy-pipeline.sh...
git add deploy-pipeline.sh
git commit -m "Tambah rollback otomatis saat health check gagal"
git checkout main
git merge tambah-rollback-otomatisgit merge lokal seperti di atas cukup untuk satu orang yang bekerja sendirian, tapi begitu lebih dari satu Sysadmin atau Developer ikut menyentuh toolkit-ops/ seperti yang jadi tema bab ini, praktik yang jauh lebih umum di lapangan adalah men-push branch itu ke server Git bersama (GitHub, GitLab, atau sejenisnya) lalu membuka pull request (istilah GitHub) atau merge request (istilah GitLab), bukan langsung git merge ke main di komputer sendiri.
git push origin tambah-rollback-otomatis
# lanjutkan lewat antarmuka web GitHub/GitLab: buka pull request/merge requestPull request memberi rekan tim kesempatan meninjau perubahan sebelum masuk ke main, tempat paling wajar menempelkan komentar seperti "kenapa set -e sengaja tidak dipakai di sini" atau "baris ini lupa di-shellcheck", persis dua gerbang kualitas yang sudah dibahas Bab 25.4 dan 26.4. Kalau repository ini sudah terhubung ke pipeline CI/CD dari Bab 24.1, job lint dan test dari 26.4.3 lazimnya dikonfigurasi berjalan otomatis tepat saat pull request dibuka, sehingga peninjau manusia bisa fokus menilai logika perubahannya, bukan ikut memeriksa hal-hal yang sudah bisa diperiksa mesin.
Setelah sebuah versi toolkit dianggap stabil dan sudah diuji seperti pada 26.3.1, menandainya lewat tag memudahkan penelusuran kembali ke versi persis itu kapan pun dibutuhkan.
git tag -a v1.0.0 -m "Rilis awal toolkit-ops: deploy, rollback, status, logs"
git push origin v1.0.0Opsi -a di atas membuat annotated tag, yang menyimpan metadata tambahan (nama penandanya, tanggal, dan pesan) selain sekadar menunjuk satu commit, sesuai dokumentasi resmi git tag, direkomendasikan untuk menandai rilis dibanding lightweight tag polos tanpa -a. Skema penomoran MAJOR.MINOR.PATCH seperti v1.0.0 mengikuti konvensi semantic versioning yang dipopulerkan semver.org, memberi sinyal cepat seberapa besar risiko sebuah pembaruan sebelum benar-benar menariknya ke server produksi. Kalau versi baru toolkit ternyata bermasalah, git checkout v1.0.0 mengembalikan seluruh isi direktori ke kondisi persis saat tag itu dibuat, sebuah rollback pada level toolkit itu sendiri, berbeda dari cmd_rollback() di ops.sh (Bab 24.3.3) yang me-rollback aplikasi yang di-deploy oleh toolkit ini, bukan toolkit-nya sendiri.
26.3.5 Troubleshooting: Secret yang Tidak Sengaja Ter-commit
Kebiasaan memisahkan kredensial.env ke luar direktori toolkit-ops/ (26.3.2) sengaja dirancang supaya kejadian ini idealnya tidak pernah terjadi, tapi kesalahan manusia tetap mungkin terjadi, misalnya seseorang di tim membuat file .env lokal untuk pengujian lalu lupa .gitignore-nya belum menangkap nama file itu. Langkah pemulihannya beda jauh tergantung apakah commit yang bermasalah sudah sempat di-push ke server bersama atau belum.
Kalau commit itu masih murni lokal dan belum pernah di-push, git commit --amend aman dipakai untuk membuang file itu dari riwayat sebelum siapa pun sempat menariknya.
git rm --cached kredensial.env
echo "kredensial.env" >> .gitignore
git add .gitignore
git commit --amend --no-editBegitu commit itu sudah ter-push ke origin dan berpotensi sudah ditarik rekan tim lain, amend lalu force-push bukan lagi solusi aman, karena menulis ulang riwayat yang sudah dibagikan bisa mengacaukan salinan lokal siapa pun yang sudah sempat menarik commit lama itu ke komputernya. Menurut dokumentasi resmi GitHub soal "Removing sensitive data from a repository", langkah paling andal begitu sebuah kredensial sudah sempat ter-push ke server bersama adalah segera mencabut atau mengganti kredensial itu sendiri di sisi layanan yang mengeluarkannya (misalnya token API atau SSH_PRIVATE_KEY yang disinggung Bab 24.1.4), bukan sekadar membersihkan riwayat Git; dokumentasi yang sama menegaskan bahwa perkakas pembersih riwayat seperti git filter-repo atau BFG Repo-Cleaner cuma menjamin commit lama tidak muncul lagi di clone berikutnya, tapi tidak bisa menjamin kredensial itu belum sempat disalin, di-cache, atau ditarik oleh pihak lain sebelum riwayatnya dibersihkan. Urutan prioritas yang benar: ganti dulu kredensialnya di sisi layanan aslinya, baru bersihkan riwayat Git sebagai langkah tambahan, bukan sebaliknya.
26.4 Testing Script Bash: Pengantar Bats
shellcheck di Bab 25.4 memeriksa kode secara statis, tanpa pernah benar-benar menjalankannya, jadi tidak bisa memastikan die() dari 26.1.2 benar-benar keluar dengan status yang tepat, atau validasi_hostname() di toolkit-keamanan.sh (Bab 23.4.3) benar-benar menolak input seperti "api.example.com; id". Bagian ini menutup celah itu lewat automated testing, kebiasaan menulis test case yang benar-benar menjalankan fungsi sungguhan dengan input tertentu, lalu memverifikasi hasilnya lewat assertion terhadap nilai yang diharapkan.
26.4.1 Instalasi bats-core
bats (Bash Automated Testing System), tepatnya fork aktifnya bats-core, adalah framework testing yang dirancang khusus untuk script Bash. Sesuai README resmi bats-core/bats-core di GitHub, ada beberapa cara instalasi: git clone repository-nya lalu jalankan ./install.sh, lewat npm install -g bats kalau Node.js sudah terpasang, atau brew install bats-core di macOS lewat Homebrew. Sebagian distro Linux modern menyediakannya lewat package manager bawaan dengan nama paket bats; cek dulu versinya (apt show bats atau setara) sebelum mengandalkannya, karena sebagian rilis distro masih membawa versi bats lama sebelum proyeknya diteruskan sebagai bats-core. Pastikan instalasi berhasil lewat:
bats --version26.4.2 Test Case Pertama dengan run, status, dan output
File test bats berekstensi .bats, biasanya diletakkan di direktori tests/ terpisah dari script yang diujinya.
#!/usr/bin/env bats
setup() {
dir_proyek="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
source "${dir_proyek}/lib.sh"
}
@test "die keluar dengan status default 1" {
run die "argumen wajib diisi"
[ "$status" -eq 1 ]
}
@test "die mencetak pesan lewat error()" {
run die "argumen wajib diisi"
[ "$output" = "Error: argumen wajib diisi" ]
}
@test "die menerima exit code kustom di argumen kedua" {
run die "dependensi hilang" 3
[ "$status" -eq 3 ]
}Fungsi setup() adalah nama khusus yang dikenali bats, dijalankan otomatis sebelum setiap blok @test; variabel bawaan BATS_TEST_FILENAME menyimpan path file test itu sendiri, dipakai di sini persis seperti pola BASH_SOURCE di 26.1.3 untuk mencari lib.sh relatif terhadap lokasi file, bukan lokasi bats dipanggil. Helper run, inti dari bats, menjalankan perintah yang diberikan sebagai argumennya lalu menangkap exit status ke variabel $status dan gabungan stdout/stderr-nya ke $output, tanpa membiarkan exit status bukan nol menghentikan seluruh file test. Ini krusial khusus untuk menguji die(): memanggilnya langsung tanpa run akan benar-benar mengeksekusi exit di dalam proses test itu sendiri dan menghentikannya lebih awal, sementara run die ... membiarkan bats menangkap hasil exit itu sebagai data biasa untuk diperiksa lewat [ "$status" -eq ... ].
bats tests/Menurut dokumentasi resmi bats-core, memberikan sebuah direktori seperti tests/ di atas membuat bats otomatis menjalankan seluruh file berekstensi .bats di dalamnya, hasilnya ditampilkan mengikuti format Test Anything Protocol (TAP), satu baris status lulus atau gagal untuk tiap blok @test. Test case yang gagal menunjukkan baris [ ] mana yang tidak terpenuhi beserta nilai $status atau $output sebenarnya, titik awal yang jauh lebih cepat untuk menelusuri regresi dibanding menjalankan manual satu per satu setiap kali ada perubahan di lib.sh.
26.4.3 Mengintegrasikan bats ke Pipeline CI/CD
Job lint di Bab 25.4.3 cuma menjamin script lolos pemeriksaan gaya dan pola berbahaya lewat shellcheck, belum menjamin perilakunya benar. Menambahkan job test di antara lint dan deploy menutup celah itu di level pipeline, bukan cuma di komputer masing-masing kontributor.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Lint script dengan ShellCheck
run: shellcheck lib.sh ops.sh deploy-pipeline.sh toolkit-keamanan.sh
test:
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Pasang bats-core
run: |
git clone https://github.com/bats-core/bats-core.git
./bats-core/install.sh /usr/local
- name: Jalankan test dengan bats
run: bats tests/
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Deploy lewat SSH
run: echo "Deploy berjalan setelah lint dan test lolos"Berbeda dari shellcheck yang menurut daftar software resmi actions/runner-images sudah bawaan di runner ubuntu-latest (Bab 25.4.3), bats-core umumnya tidak tersedia bawaan di runner yang sama, jadi langkah instalasi eksplisit lewat git clone di atas tetap diperlukan; cek ulang daftar software runner terbaru kalau ingin memastikan status ini belum berubah. Rantai needs: lint pada job test, lalu needs: test pada job deploy, memastikan urutan gerbang kualitas berjalan bertahap: kode yang gagal lolos gaya penulisan tidak pernah sampai diuji perilakunya, dan kode yang gagal diuji perilakunya tidak pernah sampai di-deploy ke server produksi.
Bab ini menutup pembahasan soal merapikan proyek yang sudah menumpuk sejak Bagian 3, memecahnya jadi lib.sh yang bisa di-source dari mana saja (26.1), mendokumentasikannya lewat teks bantuan, komentar header, dan README (26.2), mengelola perubahannya lewat Git supaya kolaborasi antar Sysadmin dan Developer tetap terlacak (26.3), dan memverifikasi perilakunya lewat bats alih-alih cuma mengandalkan pengecekan manual (26.4). Bab 27 berikutnya mundur selangkah dari detail teknis semacam ini, membahas kapan Bash sendiri sudah bukan lagi pilihan tepat dan saatnya "naik kelas" ke Python, Go, atau bahasa lain, sebelum seluruh kemampuan yang sudah dibangun sepanjang series ini disatukan jadi proyek akhir di Bab 28, termasuk toolkit administrasi server mini di Bab 28.2 yang akan memakai persis struktur proyek, dokumentasi, version control, dan testing yang baru dibahas di bab ini.

