Integrasi berkelanjutan (continuous integration, CI) adalah praktik menjalankan pemeriksaan otomatis pada setiap perubahan kode, biasanya setiap push ke repositori bersama. Pemeriksaan yang paling umum adalah linting dan pengujian, dan keduanya berjalan pada komputer layanan, bukan pada komputer anggota tim. Topik ini membahas cara menulis satu workflow CI dengan GitHub Actions, cara mensimulasikan langkah-langkahnya di komputer lokal, dan cara membaca hasil jalannya. Seluruh langkah yang dapat dieksekusi dijalankan sungguhan beserta keluaran aslinya. Setelah membacanya, satu push yang buruk tidak lagi mampu merusak branch utama tanpa terdeteksi.
Workflow pertama dengan GitHub Actions#
GitHub Actions adalah layanan CI bawaan GitHub. Satu workflow didefinisikan sebagai berkas YAML di dalam folder .github/workflows/, dan isi berkas itu menyatakan kapan workflow dipicu, pada komputer apa workflow berjalan, serta langkah-langkah apa saja yang dikerjakannya. Berkas berikut adalah workflow lengkap untuk proyek kecil yang memeriksa gaya kodenya lalu menjalankan pengujian pada setiap push maupun pull request, yaitu usulan penggabungan branch yang ditinjau sebelum digabung.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
uji:
runs-on: ubuntu-latest
steps:
- name: Mengambil kode
uses: actions/checkout@v4
- name: Menyiapkan Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Memasang alat pemeriksa
run: pip install ruff pytest
- name: Linting
run: ruff check .
- name: Pengujian
run: python3 -m pytest -qKunci on menyatakan pemicunya, yaitu push ke main dan setiap perubahan pada pull request. Bagian jobs memuat satu job bernama uji yang berjalan pada runner, yaitu komputer layanan yang disewakan GitHub untuk mengeksekusi job. Setiap butir pada steps adalah satu langkah, dan langkahnya terbagi dua jenis.
Langkah uses memanggil action siap pakai, seperti actions/checkout@v4 untuk mengambil kode dan actions/setup-python@v5 untuk memasang Python 3.12. Langkah run mengeksekusi perintah di shell runner. Dua langkah terakhir itulah pemeriksaan sesungguhnya, dan keduanya memakai perintah yang sama dengan yang dijalankan secara lokal.
Memeriksa langkah CI secara lokal#
Langkah pada workflow hanyalah perintah biasa, sehingga semuanya dapat dicoba di komputer sendiri sebelum dikirim ke repositori. Proyek latihannya terdiri atas dua berkas berikut, yaitu modul statistik dan berkas pengujian untuknya.
"""Fungsi statistik sederhana yang diuji oleh CI."""
def rata_rata(nilai):
"""Mengembalikan rata-rata daftar angka."""
return sum(nilai) / len(nilai)
def median(nilai):
"""Mengembalikan nilai tengah daftar angka yang sudah diurutkan."""
urut = sorted(nilai)
tengah = len(urut) // 2
if len(urut) % 2 == 1:
return urut[tengah]
return (urut[tengah - 1] + urut[tengah]) / 2Berkas pengujian mengimpor fungsi dari modul statistik dan selalu dieksekusi lewat perintah pytest, bukan langsung sebagai skrip.
# tidak-dijalankan: berkas pengujian dieksekusi lewat perintah pytest
"""Pengujian untuk statistik.py, dijalankan CI pada setiap push."""
from statistik import median, rata_rata
def test_rata_rata():
assert rata_rata([70, 80, 90]) == 80
def test_median_ganjil():
assert median([90, 60, 78]) == 78
def test_median_genap():
assert median([60, 70, 78, 90]) == 74.0Perintah pengujian pada workflow adalah python3 -m pytest -q. Mode -q mencetak satu titik untuk setiap pengujian yang lulus beserta baris rangkuman berisi jumlahnya dan durasi eksekusi yang berubah-ubah, sehingga keluaran lengkapnya tidak ditangkap di sini. Yang dibaca oleh CI bukan teksnya, melainkan kode keluarnya, yaitu nol ketika seluruh pengujian lulus dan selain nol ketika ada yang gagal. Keduanya dapat dibuktikan dengan menangkap kode keluarnya saja.
# menjalankan pengujian sebagaimana langkah CI, keluaran lengkapnya disembunyikan
python3 -m pytest test_statistik.py -q > /dev/null
# kode keluar 0 berarti seluruh pengujian lulus
echo "kode keluar pytest: $?"kode keluar pytest: 0Perilaku ketika gagal sama pentingnya, sebab itulah yang menghentikan merge pada sebuah pull request. Rangkaian berikut menambahkan satu berkas uji sementara yang sengaja gagal, menjalankannya, lalu menghapusnya kembali.
# menambahkan berkas uji sementara yang sengaja gagal
printf 'def test_sementara():\n assert 1 == 2\n' > test_sementara.py
# pytest mengembalikan kode keluar 1 ketika ada pengujian yang gagal
python3 -m pytest test_sementara.py -q > /dev/null
echo "kode keluar pytest: $?"
# menghapus berkas uji sementara
rm test_sementara.pykode keluar pytest: 1Langkah linting memakai ruff, dan perintahnya dapat dicoba secara lokal bila ruff sudah terpasang.
# tidak-dijalankan
# ruff tidak terpasang pada lingkungan penulisan, langkah ini berjalan di runner CI
pip install ruff
ruff check .Alur satu kali push#
Setiap push yang memenuhi pemicu memulai satu run, yaitu satu kali eksekusi workflow pada runner yang baru. Langkah-langkahnya dikerjakan berurutan, dan langkah yang gagal menghentikan seluruh job, sehingga langkah pengujian tidak pernah berjalan ketika langkah linting sudah menemukan pelanggaran. Alur itu digambarkan diagram berikut.
Status akhirnya muncul pada halaman commit maupun pull request yang bersangkutan, berupa tanda hijau ketika semua langkah lulus dan tanda merah ketika salah satunya gagal. Pada proyek tim, status inilah yang menjadi syarat penggabungan, sehingga kode yang belum lolos pemeriksaan tidak pernah masuk main. Log setiap langkah tersimpan pada halaman run, dan baris pertama yang merah biasanya sudah menyebut penyebabnya. Nama langkah pada berkas workflow menjadi judul setiap bagian log, sehingga letak kegagalannya dapat diarahkan dari judulnya saja.
Kesalahan umum#
Kesalahan pertama adalah membuat CI yang tidak pernah gagal. Perintah yang dibungkam agar tidak menggagalkan langkah, misalnya dengan memaksa kode keluarnya selalu nol, membuat run selalu hijau padahal pengujian tidak berjalan benar. CI yang selalu hijau sama nilainya dengan tidak ada CI, sebab tanda hijau tidak lagi berarti apa pun. Perbaikannya adalah membiarkan perintah yang gagal menggagalkan langkahnya, lalu membaca log ketika merah muncul.
Kesalahan kedua adalah menjalankan CI tanpa menetapkan versi Python-nya. Workflow tanpa actions/setup-python memakai versi bawaan runner yang dapat berubah dari waktu ke waktu, sehingga hasil pemeriksaan bergeser tanpa ada perubahan kode. Tetapkan versinya sebagaimana kunci python-version pada contoh, dan perluas dengan berkas dependensi yang versinya terkunci apabila proyeknya memakai package pihak ketiga.
Kesalahan ketiga adalah mendorong kode tanpa menjalankan pemeriksaan lokal. Setiap koreksi kecil menjadi satu push baru yang memicu satu run baru, sehingga siklus perbaikannya lambat dan riwayat commit penuh dengan perbaikan yang sebetulnya bisa ditangkap lebih awal. Jalankan pytest secara lokal terlebih dahulu sebagaimana bagian simulasi, lalu kirim ketika kode sudah lulus.
Ringkasan#
- Workflow CI ditulis sebagai berkas YAML di
.github/workflows/yang menyatakan pemicu, runner, dan langkah-langkahnya. - Langkah CI berupa perintah biasa, sehingga linting dan pengujian dapat disimulasikan lokal sebelum dikirim ke repositori.
- CI membaca kode keluar perintah, yaitu nol untuk lulus dan selain nol untuk gagal, dan langkah yang gagal menghentikan job.
- Status run menjadi syarat penggabungan pada proyek tim, sehingga kode yang belum lolos tidak pernah masuk
main.
Referensi#
- GitHub Actions documentation — GitHub — Dokumentasi resmi GitHub Actions untuk konsep workflow, job, dan step.
- About continuous integration — GitHub — Penjelasan resmi GitHub tentang konsep CI dan pemeriksaan otomatisnya.
- pytest: helps write better programs — pytest project — Dokumentasi pytest untuk perintah menjalankan pengujian dan kode keluarnya.
- Ruff documentation — Astral
— Dokumentasi ruff untuk subperintah
checkyang dipakai langkah linting.