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:

text
.
├── CMakeLists.txt
├── src/
│   ├── greet.h
│   ├── greet.c
│   └── main.c
├── tests/
│   └── test_greet.c
└── .github/
    └── workflows/
        └── ci.yml

Header greet.h mendeklarasikan fungsi greet yang menulis pesan ke buffer:

c
#ifndef GREET_H
#define GREET_H

int greet(const char *nama, char *out, int out_size);

#endif
c
/* 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);
}
c
/* 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:

c
#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
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:

yaml
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-failure

Mari kita telaah baris-baris YAML di atas:

  1. name: CI: nama workflow — muncul di tab “Actions” GitHub. Bisa apa saja; konvensi: nama singkat yang mendeskripsikan tujuan workflow.
  2. on:: trigger workflow. push dengan branches: [main] menjalankan workflow saat commit didorong ke branch main. pull_request dengan branches: [main] menjalankan saat ada PR yang menargetkan main.
  3. jobs:: daftar job paralel. Setiap job berjalan di runner terpisah (mesin virtual fresh).
  4. 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.
  5. steps:: daftar langkah berurutan dalam job. Setiap step memiliki name (deskripsi) dan run (perintah shell).
  6. - uses: actions/checkout@v4: action resmi GitHub untuk checkout kode repository ke VM. Tanpa langkah ini, direktori kerja kosong.
  7. Install dependencies: memasang build-essential (GCC, Make, libc headers) dan cmake via apt. Mesin runner Ubuntu sudah punya GCC dan Make terinstal, tetapi cmake perlu dipasang eksplisit.
  8. Configure: menjalankan cmake -B build -DCMAKE_BUILD_TYPE=Release untuk konfigurasi out-of-source build.
  9. Build: menjalankan cmake --build build --parallel untuk kompilasi paralel (menggunakan semua core VM).
  10. Test: menjalankan ctest --test-dir build --output-on-failure untuk eksekusi test suite. Flag --output-on-failure menyebabkan 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.

bash
# Step 1: Configure
cmake -B build -DCMAKE_BUILD_TYPE=Release

Output yang Dihasilkan:

text
-- 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
bash
# Step 2: Build (parallel)
cmake --build build --parallel

Output yang Dihasilkan:

text
[ 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
bash
# Step 3: Test
ctest --test-dir build --output-on-failure

Output yang Dihasilkan:

text
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 sec

CTest 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:

yaml
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-failure

Mari kita telaah bagian-bagian baru di workflow ini:

  1. strategy.matrix: mendefinisikan variabel matriks. compiler: [gcc, clang] dan build_type: [Debug, Release] menghasilkan 4 kombinasi: gcc/Debug, gcc/Release, clang/Debug, clang/Release. GitHub menjalankan 4 job paralel, satu untuk setiap kombinasi.
  2. fail-fast: false: bila salah satu job gagal, job lain tetap berjalan sampai selesai. Default true membatalkan semua job saat satu gagal — berguna untuk menghemat compute, tetapi kurang informatif untuk debugging.
  3. name: ${{ matrix.compiler }} / ${{ matrix.build_type }}: nama job di UI GitHub menjadi gcc / Debug, gcc / Release, dan seterusnya — mudah dibaca di dashboard.
  4. env: CC: ${{ matrix.compiler }}: set variabel environment CC di step Configure. CMake membaca CC untuk menentukan compiler C yang dipakai. Bila CC=clang, CMake memakai Clang; bila CC=gcc, CMake memakai GCC.
  5. -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}: interpolasi nilai matriks ke flag CMake. Untuk job gcc/Debug, command line menjadi cmake -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):

yaml
  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-failure

Mari kita reproduksi job sanitizer secara lokal:

bash
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/app

Output yang Dihasilkan:

text
# 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: 0

AddressSanitizer 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.