Continuous Integration (CI) adalah praktik otomatis menjalankan build dan test setiap kali kode didorong ke repository. CI memberi jaring pengaman: bila sebuah commit merusak build atau gagal test, tim mendapat notifikasi dalam beberapa menit — bukan saat developer lain menarik kode. Untuk proyek C, workflow CI tipikal meliputi: install toolchain (compiler, cmake), konfigurasi build, kompilasi paralel, dan eksekusi test suite. Pada chapter ini kita akan menyusun proyek C kecil dengan CMake dan unit test, lalu menulis GitHub Actions YAML yang membangun dan menguji proyek tersebut dengan beberapa konfigurasi (kombinasi compiler × build type × sanitizer). Karena kita tidak bisa benar-benar menjalankan GitHub Actions di sandbox tutorial, kita akan mereproduksi setiap langkah CI secara lokal dengan command line yang sama persis — ini validasi bahwa workflow akan berhasil saat dijalankan di runner GitHub.
Struktur Proyek Demo #
Proyek demo terdiri dari library greet (satu file source), executable app yang memakai library, dan unit test yang memverifikasi perilaku library:
.
├── CMakeLists.txt
├── src/
│ ├── greet.h
│ ├── greet.c
│ └── main.c
├── tests/
│ └── test_greet.c
└── .github/
└── workflows/
└── ci.ymlHeader greet.h mendeklarasikan fungsi greet yang menulis pesan ke buffer:
#ifndef GREET_H
#define GREET_H
int greet(const char *nama, char *out, int out_size);
#endif/* src/greet.c */
#include <stdio.h>
#include <string.h>
#include "greet.h"
int greet(const char *nama, char *out, int out_size) {
return snprintf(out, (size_t)out_size, "Halo %s!", nama);
}/* src/main.c */
#include <stdio.h>
#include "greet.h"
int main(void) {
char buf[64];
greet("Budi", buf, sizeof(buf));
printf("%s\n", buf);
return 0;
}Test suite tests/test_greet.c memakai assert standar C untuk memverifikasi tiga kasus: nama normal, nama panjang yang dipotong sesuai buffer, dan nama kosong:
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include "greet.h"
int main(void) {
char buf[64];
/* Test 1: nama normal */
greet("Budi", buf, sizeof(buf));
assert(strcmp(buf, "Halo Budi!") == 0);
/* Test 2: nama panjang dipotong */
greet("BudiSangatPanjangSekali", buf, 10);
assert(strlen(buf) == 9); /* dipotong karena buffer terbatas */
/* Test 3: nama kosong */
greet("", buf, sizeof(buf));
assert(strcmp(buf, "Halo !") == 0);
printf("All tests passed!\n");
return 0;
}CMakeLists.txt mengonfigurasi build dengan tiga target: static library greet, executable app, dan executable test_greet. CMake enable_testing() dan add_test() mendaftarkan test_greet sebagai target CTest:
cmake_minimum_required(VERSION 3.10)
project(ci-demo C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
add_compile_options(-Wall -Wextra -Wpedantic)
add_library(greet STATIC src/greet.c)
target_include_directories(greet PUBLIC src)
add_executable(app src/main.c)
target_link_libraries(app PRIVATE greet)
enable_testing()
add_executable(test_greet tests/test_greet.c)
target_link_libraries(test_greet PRIVATE greet)
add_test(NAME test_greet COMMAND test_greet)Workflow CI Minimal #
Berikut GitHub Actions YAML minimal yang membangun dan mengetes proyek. File disimpan di .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest --test-dir build --output-on-failureMari kita telaah baris-baris YAML di atas:
name: CI: nama workflow — muncul di tab “Actions” GitHub. Bisa apa saja; konvensi: nama singkat yang mendeskripsikan tujuan workflow.on:: trigger workflow.pushdenganbranches: [main]menjalankan workflow saat commit didorong ke branchmain.pull_requestdenganbranches: [main]menjalankan saat ada PR yang menargetkanmain.jobs:: daftar job paralel. Setiap job berjalan di runner terpisah (mesin virtual fresh).runs-on: ubuntu-latest: job berjalan di VM Ubuntu terbaru yang disediakan GitHub (Ubuntu 22.04 atau 24.04). Pilihan lain:ubuntu-22.04,macos-latest,windows-latest.steps:: daftar langkah berurutan dalam job. Setiap step memilikiname(deskripsi) danrun(perintah shell).- uses: actions/checkout@v4: action resmi GitHub untuk checkout kode repository ke VM. Tanpa langkah ini, direktori kerja kosong.Install dependencies: memasangbuild-essential(GCC, Make, libc headers) dancmakeviaapt. Mesin runner Ubuntu sudah punya GCC dan Make terinstal, tetapicmakeperlu dipasang eksplisit.Configure: menjalankancmake -B build -DCMAKE_BUILD_TYPE=Releaseuntuk konfigurasi out-of-source build.Build: menjalankancmake --build build --paralleluntuk kompilasi paralel (menggunakan semua core VM).Test: menjalankanctest --test-dir build --output-on-failureuntuk eksekusi test suite. Flag--output-on-failuremenyebabkan output test dicetak hanya bila test gagal — menjaga log CI tetap ringkas saat semua test lulus.
Reproduksi Langkah CI Secara Lokal #
Karena kita tidak menjalankan GitHub Actions di sandbox, mari kita reproduksi setiap langkah dengan command line yang sama persis. Ini adalah praktik baik: selalu uji workflow CI secara lokal sebelum push, agar yakin workflow akan berhasil saat dijalankan di runner GitHub.
# Step 1: Configure
cmake -B build -DCMAKE_BUILD_TYPE=ReleaseOutput yang Dihasilkan:
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done (0.1s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/test_ci/build# Step 2: Build (parallel)
cmake --build build --parallelOutput yang Dihasilkan:
[ 66%] Building C object CMakeFiles/test_greet.dir/tests/test_greet.c.o
[ 83%] Linking C executable app
[100%] Linking C executable test_greet
[100%] Built target app
[100%] Built target test_greet# Step 3: Test
ctest --test-dir build --output-on-failureOutput yang Dihasilkan:
Test project /tmp/test_ci/build
Start 1: test_greet
1/1 Test #1: test_greet ....................... Passed 0.00 sec
100% tests passed out 1
Total Test time (real) = 0.00 secCTest melaporkan 1/1 Test #1: test_greet ... Passed — satu test dijalankan, lulus. Baris 100% tests passed menandakan tidak ada test yang gagal. Bila salah satu assert di test_greet.c gagal, CTest akan menampilkan Failed dengan detail output (karena --output-on-failure), dan exit status menjadi non-zero — yang menyebabkan step CI gagal dan workflow ditandai merah di GitHub.
CI dengan Matriks: Multi-Compiler × Multi-Build-Type #
Workflow berikut menggunakan matrix strategy untuk menjalankan build di kombinasi 2 compiler (gcc, clang) × 2 build type (Debug, Release) = 4 job paralel:
name: CI Matrix
on:
push:
branches: [main]
pull_request:
jobs:
build:
name: ${{ matrix.compiler }} / ${{ matrix.build_type }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
compiler: [gcc, clang]
build_type: [Debug, Release]
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake clang
- name: Configure
env:
CC: ${{ matrix.compiler }}
run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest --test-dir build --output-on-failureMari kita telaah bagian-bagian baru di workflow ini:
strategy.matrix: mendefinisikan variabel matriks.compiler: [gcc, clang]danbuild_type: [Debug, Release]menghasilkan 4 kombinasi:gcc/Debug,gcc/Release,clang/Debug,clang/Release. GitHub menjalankan 4 job paralel, satu untuk setiap kombinasi.fail-fast: false: bila salah satu job gagal, job lain tetap berjalan sampai selesai. Defaulttruemembatalkan semua job saat satu gagal — berguna untuk menghemat compute, tetapi kurang informatif untuk debugging.name: ${{ matrix.compiler }} / ${{ matrix.build_type }}: nama job di UI GitHub menjadigcc / Debug,gcc / Release, dan seterusnya — mudah dibaca di dashboard.env: CC: ${{ matrix.compiler }}: set variabel environmentCCdi step Configure. CMake membacaCCuntuk menentukan compiler C yang dipakai. BilaCC=clang, CMake memakai Clang; bilaCC=gcc, CMake memakai GCC.-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}: interpolasi nilai matriks ke flag CMake. Untuk jobgcc/Debug, command line menjadicmake -B build -DCMAKE_BUILD_TYPE=Debug.
CI dengan Sanitizer #
Job kedua menjalankan build dengan AddressSanitizer dan UndefinedBehaviorSanitizer untuk mendeteksi bug memori (buffer overflow, use-after-free) dan undefined behavior (signed overflow, null dereference):
sanitizer:
name: sanitizer (ASan + UBSan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake
- name: Configure with sanitizers
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
- name: Build
run: cmake --build build --parallel
- name: Test with sanitizers
run: ctest --test-dir build --output-on-failureMari kita reproduksi job sanitizer secara lokal:
cmake -B build_asan -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
cmake --build build_asan --parallel
ctest --test-dir build_asan --output-on-failure
./build_asan/appOutput yang Dihasilkan:
# Configure
-- Configuring done (0.2s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/test_ci/build_asan
# Build
[100%] Linking C executable test_greet
[100%] Built target app
[100%] Built target test_greet
# Test
Test project /tmp/test_ci/build_asan
Start 1: test_greet
1/1 Test #1: test_greet ....................... Passed 0.01 sec
100% tests passed out 1
Total Test time (real) = 0.01 sec
# Run app
Halo Budi!
exit: 0AddressSanitizer melaporkan tidak ada error memori — app keluar dengan status 0. Bila ada bug memori (misalnya greet() menulis lebih dari out_size byte), ASan akan menghentikan program dengan pesan ==PID==ERROR: AddressSanitizer: stack-buffer-overflow dan stack trace. Pesan ini muncul di log CI, menyebabkan step CI gagal, dan workflow ditandai merah.