Chapter CTest memberi kita pelari tes yang memeriksa puluhan program dengan satu perintah — tetapi seluruh rangkaian itu masih kita jalankan tangan di komputer sendiri. Begitu satu orang lupa meng-compile ulang atau melompati tes, regresi lolos tanpa terdeteksi. Continuous Integration (CI) memindahkan rangkaian build-tes-lapor ke mesin lain yang berjalan otomatis setiap kali kode berubah: mesin bersih tanpa konfigurasi lokal, yang menolak perubahan bila salah satu langkahnya gagal. Chapter ini merancang pipeline GitHub Actions untuk proyek C++ — matrix dua compiler, cache, job sanitizer, lint, dan artefak build — lalu menutupnya dengan versi mininya yang kita jalankan sungguhan di lingkungan praktikum.

Satu hal ditegaskan sejak awal: seluruh YAML di chapter ini adalah konfigurasi ilustratif untuk GitHub Actions — platform CI yang menjalankan pipeline di server milik GitHub — dan tidak bisa dieksekusi di sandbox ini, karena di sini tidak ada runner, tidak ada git remote, dan clang-format/clang-tidy tidak terinstal. Yang benar-benar kita eksekusi adalah skrip mini pada bagian akhir chapter.

Pipeline GitHub Actions untuk C++ #

GitHub Actions membaca definisi pipeline dari file YAML yang disimpan di folder .github/workflows/ proyek. Struktur terluarnya tiga lapis: event (on: — kapan pipeline dijalankan), job (satuan pekerjaan yang berjalan di mesin virtual bernama runner), dan step (perintah run: atau aksi siap pakai lewat uses:). Proyek Kalkulator dari section ini — dengan src/, tests/, dan add_test dari chapter CTest — memberi kita bahan yang pas:

yaml .github/workflows/ci.yml (kerangka)
name: ci-kalkulator

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build-dan-tes:
    runs-on: ubuntu-latest
    steps:
      - name: Ambil kode sumber
        uses: actions/checkout@v4
      - name: Configure dengan CMake
        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
      - name: Build semua target
        run: cmake --build build --parallel 2
      - name: Jalankan seluruh tes
        working-directory: build
        run: ctest --output-on-failure

Pipeline ini terbaca dari atas ke bawah. Event push ke main dan pull_request apa pun memicu job build-dan-tes pada runner ubuntu-latest. Aksi actions/checkout@v4 menyalin isi repositori ke runner — tanpa langkah itu, langkah berikutnya tidak punya bahan. Tiga perintah CMake melanjutkan kebiasaan kita sejak chapter CMake: configure ke folder build, build semua target, lalu ctest dijalankan dari folder build. ctest yang menemukan satu tes gagal mengembalikan exit code selain 0, sehingga step terakhir gagal dan job dinyatakan gagal.

Matrix GCC dan Clang #

Compiler yang berbeda memberi peringatan yang berbeda dan mengimplementasikan detail standar dengan sedikit variasi — proyek yang hanya dites dengan satu compiler menanggung risiko yang tidak perlu. GitHub Actions menjawabnya dengan matrix: satu definisi job, dijalankan beberapa kali dengan kombinasi nilai yang berbeda.

yaml Job dengan matrix dua compiler
jobs:
  build-dan-tes:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        compiler: [gcc, clang]
        include:
          - compiler: gcc
            cc: gcc
            cxx: g++
          - compiler: clang
            cc: clang
            cxx: clang++
    env:
      CC: ${{ matrix.cc }}
      CXX: ${{ matrix.cxx }}
    steps:
      - uses: actions/checkout@v4
      - name: Pasang clang
        run: sudo apt-get update && sudo apt-get install -y clang
      - name: Configure, build, dan tes
        run: |
          cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
          cmake --build build --parallel 2
          ctest --test-dir build --output-on-failure

Kunci matrix.compiler berisi dua nilai, sehingga GitHub menjalankan dua salinan job yang identik kecuali nilai ${{ matrix.cc }} dan ${{ matrix.cxx }}. Variabel lingkungan CC dan CXX dibaca CMake saat configure untuk memilih compiler. Opsi fail-fast: false membuat salinan yang gagal tidak membatalkan salinan lain, sehingga laporan kedua compiler tetap utuh.

Cache Folder Build #

Runner baru dibangun dari nol untuk setiap job, sehingga tanpa bantuan, semua unit kompilasi di-compile ulang setiap kali pipeline berjalan. Aksi actions/cache menyimpan folder tertentu antar run, dikunci oleh sebuah key:

yaml Step cache sebelum langkah build
      - name: Cache folder build
        uses: actions/cache@v4
        with:
          path: build
          key: build-${{ matrix.compiler }}-${{ hashFiles('src/**', 'CMakeLists.txt') }}

hashFiles(...) menghitung checksum dari seluruh source dan file CMake. Bila source tidak berubah, key-nya sama, folder build dipulihkan dari cache, dan CMake cukup meng-compile ulang yang berubah; bila source berubah, key menjadi baru dan cache lama diabaikan. Suffix matrix.compiler menjaga cache kedua compiler tidak saling menimpa. Untuk proyek besar, mekanisme yang sama dipakai men-cache direktori compiler cache seperti ccache.

Job Sanitizer: ASan dan UBSan #

Sanitizer — ASan dan UBSan yang kita jalankan lewat flag -fsanitize=address,undefined — paling efektif pada build Debug: optimasi dimatikan sehingga pelacakan memorinya akurat. Pipeline yang sehat menjalankan sanitizer sebagai job terpisah, karena build-nya lebih lambat dan pesannya memuat alamat memori yang tidak stabil untuk dibandingkan antar mesin:

yaml Job sanitizer ASan + UBSan
  sanitizer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Debug dengan ASan dan UBSan
        run: |
          cmake -S . -B build-asan \
                -DCMAKE_BUILD_TYPE=Debug \
                -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
          cmake --build build-asan --parallel 2
      - name: Tes dengan sanitizer aktif
        working-directory: build-asan
        run: ctest --output-on-failure

Flag -fno-omit-frame-pointer menjaga frame pointer tetap ada sehingga jejak panggilan pada laporan sanitizer lebih mudah dibaca. Bila salah satu tes menyentuh perilaku yang terdeteksi — akses di luar batas, use-after-free, atau undefined behavior yang dilacak UBSan — program dihentikan dengan exit code selain 0, ctest melaporkan kegagalan itu, dan job berubah merah.

Lint: clang-format dan clang-tidy #

Dua alat ini sudah kita kenal di section Debugging dan Code Quality: clang-format menata gaya tulis, clang-tidy menjalankan ratusan check statis. Di pipeline, keduanya berperan sebagai gerbang: laporannya boleh dibaca santai saat berkembang, tetapi job lint yang gagal menahan merge. Flag --dry-run membuat clang-format tidak menulis file apa pun — hanya membandingkan dan melaporkan file yang formatnya menyimpang; ditambah --Werror, setiap penyimpangan berubah menjadi exit code gagal.

yaml Job lint
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Pasang alat lint
        run: sudo apt-get update && sudo apt-get install -y clang-format clang-tidy
      - name: clang-format tanpa menulis file
        run: clang-format --dry-run --Werror src/*.cpp include/*.h
      - name: clang-tidy
        run: clang-tidy src/*.cpp -- -std=c++23 -Iinclude

Baris clang-tidy memperhatikan pemisah --: argumen di kirinya diperuntukkan alat, argumen di kanannya diteruskan ke fase kompilasi. Pemisah itu wajib karena clang-tidy membaca kode dengan flag compiler yang sama dengan build sungguhan — -std=c++23 dan -Iinclude — tanpa itu, header proyek tidak ditemukan dan laporannya menyesatkan.

Istilah pre-commit merujuk ke kebiasaan menjalankan pemeriksaan yang sama sebelum commit dibuat — secara manual atau otomatis lewat git hook. Job lint di CI adalah jaring pengaman; versi pre-commit-nya lebih murah karena masalah ditemukan di komputer sendiri, detik-detik sebelum commit, bukan menit-menit kemudian di server setelah antrean build selesai.

Artefak Build #

Runner dihapus setelah job selesai, dan hasil build-nya ikut tidak tertinggal — kecuali kita simpan sebagai artefak: berkas yang diunggah dari job dan bisa diunduh dari halaman run:

yaml Step unggah artefak (di job build-dan-tes)
      - name: Unggah artefak hasil build
        uses: actions/upload-artifact@v4
        with:
          name: kalkulator-${{ matrix.compiler }}
          path: build/kalkulator
          retention-days: 7

path menunjuk hasil build yang diunggah, name memberinya label yang membedakan salinan gcc dan clang, dan retention-days mengatur masa simpannya. Artefak menjawab pertanyaan yang sering muncul setelah job hijau — “program yang dites tadi bisa diunduh?” — dan anggota tim bisa mengunduh biner yang dihasilkan konfigurasi yang sama, tanpa membangun ulang sendiri.

CI Mini yang Dijalankan Sungguhan #

Di balik seluruh YAML tadi ada inti yang sederhana: loop — ambil daftar sumber, build, tes, lapor, lalu tutup dengan exit code yang benar. Skrip berikut memadatkan inti itu ke bentuk yang bisa dijalankan di mana saja g++ terinstal, termasuk sandbox ini: semua .cpp di sebuah folder di-compile satu per satu dengan flag yang kita ajarkan sejak awal section, hasilnya dijalankan, dan setiap langkah mendapat vonis [LULUS]/[GAGAL]. Pemakaiannya: ./ci-lokal.sh [folder-sumber] — tanpa argumen, folder saat ini yang dipakai.

bash
#!/usr/bin/env bash
set -u

folder="${1:-.}"
folderBuild="build-ci"
mkdir -p "$folderBuild"

gagal=0
total=0

for sumber in "$folder"/*.cpp; do
    [ -e "$sumber" ] || { echo "Tidak ada file .cpp di folder: $folder"; exit 1; }
    nama="$(basename "$sumber" .cpp)"
    total=$((total + 1))
    echo "== kompilasi $sumber"
    if g++ -std=c++23 -Wall -Wextra "$sumber" -o "$folderBuild/$nama"; then
        echo "== jalankan  $nama"
        if "$folderBuild/$nama"; then
            echo "[LULUS] $nama"
        else
            echo "[GAGAL] $nama (exit code $?)"
            gagal=$((gagal + 1))
        fi
    else
        echo "[GAGAL] $nama (kompilasi)"
        gagal=$((gagal + 1))
    fi
done

echo
echo "Ringkasan: $((total - gagal)) dari $total file lulus."
[ "$gagal" -eq 0 ]

Satu keputusan desain perlu dipikirkan sejak awal: skrip runner TIDAK memakai set -e. set -e menghentikan skrip pada perintah pertama yang gagal — keputusan yang tepat untuk skrip persiapan yang setiap langkahnya menjadi prasyarat langkah berikutnya, tetapi keliru untuk runner: kegagalan satu file justru informasi yang harus dikumpulkan sampai tuntas, karena laporan “2 dari 3 file lulus” jauh lebih berguna daripada skrip yang berhenti di file pertama tanpa ringkasan. Karena itu setiap status diperiksa eksplisit lewat if, sementara set -u tetap dipasang untuk menangkap pemakaian variabel yang belum didefinisikan.

Sekarang kita jalankan sungguhan. Sesi di bawah membuat tiga program demo — dua sehat, satu sengaja diberi kesalahan klasik berupa titik koma yang hilang — lalu memanggil skrip dan menampilkan exit code-nya:

bash
cat > hitung.cpp << 'EOF'
#include <iostream>

int main() {
    std::cout << "hitung: 6 * 7 = " << 6 * 7 << "\n";
    return 0;
}
EOF
cat > sapa.cpp << 'EOF'
#include <iostream>
#include <string>

int main() {
    std::string nama = "CI lokal";
    std::cout << "sapa: hai, " << nama << "\n";
    return 0;
}
EOF
cat > rusak.cpp << 'EOF'
#include <iostream>

int main() {
    std::cout << "baris ini kehilangan titik koma"
    return 0;
}
EOF
chmod +x ci-lokal.sh
./ci-lokal.sh
echo "exit code: $?"

Output yang Dihasilkan:

text
== kompilasi ./hitung.cpp
== jalankan  hitung
hitung: 6 * 7 = 42
[LULUS] hitung
== kompilasi ./rusak.cpp
./rusak.cpp: In function ‘int main()’:
./rusak.cpp:4:51: error: expected ‘;’ before ‘return’
    4 |     std::cout << "baris ini kehilangan titik koma"
      |                                                   ^
      |                                                   ;
    5 |     return 0;
      |     ~~~~~~                                         
[GAGAL] rusak (kompilasi)
== kompilasi ./sapa.cpp
== jalankan  sapa
sapa: hai, CI lokal
[LULUS] sapa

Ringkasan: 2 dari 3 file lulus.
exit code: 1

Perhatikan dua hal dari log di atas. Pertama, sapa tetap dites setelah rusak gagal — skrip mengumpulkan semua kegagalan sampai selesai, kontrak yang sama yang dianut CTest. Kedua, pesan error compiler muncul apa adanya: g++ menulisnya ke stderr dan skrip tidak menyembunyikannya, sehingga penyebab kegagalan terbaca langsung dari log. Pencocokan pola *.cpp tidak menelusuri subfolder — untuk proyek bersusun, daftar sumber sebaiknya diserahkan ke build system seperti CMake yang kita rakit di section ini, dan skrip mini ini paling pas untuk folder kecil, latihan, serta cek cepat sebelum push.

Bedah baris demi baris:

  1. Baris 1-2 (shebang dan set -u): shebang #!/usr/bin/env bash memilih bash dari lokasi yang ditemukan env, bukan path keras ke /bin/bash. set -u menjadikan pemakaian variabel yang belum didefinisikan sebagai galat — perlindungan murah untuk skrip yang menangani banyak variabel.
  2. Baris 4-6 (parameter dan folder build): ${1:-.} memberi nilai default, artinya folder saat ini bila argumen pertama tidak diberi. Hasil kompilasi dikumpulkan di build-ci yang dibuat dengan mkdir -p, sehingga folder sumber tidak tercecer binary.
  3. Baris 11-12 (glob dan penjaga): pola "$folder"/*.cpp menghasilkan daftar sumber; bila tidak ada yang cocok, bash mengoper pola mentahnya sebagai satu string, dan [ -e ] menangkap kejadian itu lalu keluar dengan exit code 1.
  4. Baris 16-27 (kompilasi, eksekusi, vonis): g++ ditempatkan sebagai kondisi if, sehingga statusnya tidak pernah menghentikan skrip — hanya memilih cabang. Program yang berhasil di-compile langsung dijalankan, dan $? pada cabang [GAGAL] masih memuat exit code program yang baru saja gagal.
  5. Baris 30-32 (ringkasan dan exit code): ringkasan menghitung selisih total dan gagal; baris terakhir [ "$gagal" -eq 0 ] adalah perintah biasa yang status keluarnya menjadi exit code skrip. Kontraknya sama dengan program tes pada chapter CTest — exit code 0 berarti semua lulus — sehingga skrip ini sendiri bisa dipanggil runner lain, termasuk job CI yang mengeksekusinya sebagai step run:.

Pipeline tadi menulis -DCMAKE_BUILD_TYPE=Release dan Debug tanpa kita bahas isinya — dan itu bukan sekadar nama dekoratif: pilihan build type mengubah flag compiler, kecepatan program, bahkan perilaku assert. Chapter berikutnya membongkar apa yang sebenarnya terjadi di balik Debug dan Release.

Referensi: clang.llvm.org — ClangFormat , clang.llvm.org — clang-tidy .