Merhaba dostlar! Bir önceki yazımda CI/CD ile ilgili konulara çok değinemediğimden ve bu konuları ayrı bir yazı olarak sizler ile paylaşacağımdan bahsetmiştim. İşte bu yazı onu vücut bulmuş hali.
Altyapıyı kurarken en çok vakit alan kısımlardan birisi de CI/CD olabiliyor; birçok kendine has hususu içerdiği için ben de buraya taşıdım. Güzel ve önemli tarafı da şu: burada anlatacaklarımın çoğu SDLPainter’a özel değil aslında, herhangi bir modern C++/CMake/Conan projesine birebir taşıyabilirsiniz.
CI/CD’yi “sona bırakılan angarya” olarak değil, projenin asıl çıktıları ve altyapılarından biri olarak değerlendirdim. Gerekçem ise basit: kodlamaya başladıktan sonra altyapıyla uğraşmak bazen zul gelebiliyor. Her push’ta iki platformda derlenen, testleri headless koşan, formatı denetleyen bir pipelinenımın olması, sonraki fazlarda da gönül rahatlığıyla hareket etmeme olan sağlıyor.
Burada bahsettiğim pipeline’a yönelik hususlar gitlab’a özgü, bununla birlikte, gitlabda kotamın çok hızlı bir şekilde bitebildiğini gördüm 🙂 Bu yazımda her iki pipeline’a da yönelik konulara değineceğim.
Beş Aşamalı Pipeline
Öncelikle pipeline’în aşamalarından başlayalım. Pipeline’ım beş aşamadan oluşuyor:
1 2 3 4 5 6 | stages: - build - test - quality - docs - release |
| Aşama | Job’lar | Ne yapıyor? |
|---|---|---|
| build | build:linux:debug/release, build:windows:debug/release | İki platform × iki konfigürasyon |
| test | test:unit, test:unit:asan | GTest (headless) + ASan/UBSan |
| quality | quality:clang-format, quality:clang-tidy | Format + statik analiz |
| docs | pages | Doxygen → GitLab Pages (yalnız main) |
| release | release:publish | v*.*.* tag’inde otomatik release |
Burada ben SDLPainter için hem windows hem de linux pipeline’ları oluşturdum ama siz hedef sadece bir hedef platform için de oluşturabilirsiniz.
![]()
Hazır İmaj Yerine Kendi Docker İmajımız
İlk olarak, bu tarz uygulamalar için klasik yolu izledim:
ubuntu:24.04 imajı, her job’ın başında apt-get install ile derleyici + araçlar, sonra conan install. Çalışıyor — ama her pipeline dakikalarca aynı paketleri indirip SDL3’ü baştan derliyor. SDL3’ün X11/Wayland/audio gibi bir düzine sistem bağımlılığı olduğunu da hesaba katınca bu süre ciddi oranda uzuyor.Peki çözüm ne?: tüm araçları ve önceden oluşturulmuş Conan cache’i içeren özel bir Docker imajı oluşturmak. Bunu da
debian:13-slim tabanlı, multi-stage bir Dockerfile ile üç varyant üreterek yapıyoruz:- builder — GCC 13, Clang 18 (format + tidy dahil), CMake, Ninja, Conan 2 ve SDL3’ün tüm sistem bağımlılıkları. Asıl numara şurada: imaj build edilirken
conanfile.pykopyalanıp Debug + Release (Vulkan’lı varyantlar dahil) içinconan install --build=missingçalıştırılıyor. Yani SDL3, spdlog, GTest ve arkadaşları imajın içinde derlenmiş halde bekliyor. Burası önemli, bir daha oluşturulmuyor ya da indirilmiyor. - ci — builder üzerine coverage araçları (lcov, gcovr) ve headless test için Mesa’nın yazılım renderer’ları ekleniyor (birazdan geleceğiz).
- windows-cross— builder üzerine MinGW-w64; Linux container içinde Windows
.exeüretmek için. Bir uyarı: bu hedefte Vulkan **desteklenmiyor** —vulkan-loaderrecipe’ı Windows’taUSE_MASM=True‘yu hardcode ettiği için MinGW gcc derleyemiyor;conanfile.py configure()bu hedeftewith_vulkan‘ı otomatikFalseçekiyor. Windows’ta Vulkan gerekiyorsa native MSVC build kullanılmalı, buna da yönelik bir girişimim oldu ona da değineceğim.
Sonuç olarak, job içindeki
conan install artık cache’ten besleniyor ve bu adımlar saniyeler sürüyor. Branch bazlı CI cache’iyle uğraşmaya da gerek kalmadı — cache, imajın kendisi.Bu arada imajları sizler de build etmek zorunda değilsin: hazır build’leri Docker Hub’da da yayınlıyorum 🙂
ci-v1.0 (Linux, ~2,2 GB) ile çekip projeyi mount’layarak lokalde de CI ile birebir aynı ortamda derleyebilirsin; MinGW cross-compile (windows-cross-v1.0) ve native MSVC (windows-v1.0) varyantları da orada 😉1 2 3 4 5 6 7 8 9 10 | build:linux:debug: stage: build image: $CI_REGISTRY_IMAGE # Önceden hazırlanmış Conan cache'li özel imaj script: - | conan install . --output-folder=build/linux-debug/generators --build=missing \ -s build_type=Debug \ -o "&:with_vulkan=True" - cmake --preset linux-debug - cmake --build --preset linux-debug |
Dikkat: lokalde çalıştırdığımız komutların aynısı. Preset’lerin meyvesi burada toplanıyor.
Peki bu yaklaşımın bir handikapı yok mu? Var tabii:
conanfile.py‘a yeni bağımlılık eklediğinde imajı yeniden build edip registry’ye push etmemiz gerekiyor. Ama bağımlılık listesi her commit’te değişmiyor; haftada bir imaj güncellemek, her pipeline’da 10 dakika beklemekten çok daha ucuz.Docker’a yönelik detay bilgiler için aşağıdaki adreslere de göz atabilirsiniz:
GPU’suz Makinede Grafik Testi: Headless CI
CI runner’larında ne ekran var ne GPU. Peki SDL penceresi açan, OpenGL context isteyen testler nasıl koşacak? İki ortam değişkeniyle:
1 2 3 4 5 6 7 8 | test:unit: stage: test needs: ["build:linux:debug"] variables: SDL_VIDEODRIVER: "offscreen" VK_ICD_FILENAMES: "/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" script: - ctest --preset linux-debug --output-on-failure |
SDL_VIDEODRIVER=offscreen— SDL3’e “pencereyi görünmez çiz” diyor. X11/Wayland gerekmiyor.VK_ICD_FILENAMES— Vulkan tarafında Mesa’nın **lavapipe** sürücüsünü işaret ediyor: GPU olmadan Vulkan API’sini CPU üzerinde sunan bir yazılım renderer’ı. OpenGL kod yolları için de imajdaMESA_LOADER_DRIVER_OVERRIDE=llvmpipeayarlı.
Bu üçlü sayesinde renderer testleri gerçek bir grafik API’siyle, ama tamamen CPU üzerinde koşuyor. Yavaş mı? Evet. Birim test için yeterli mi? Fazlasıyla.
ASan/UBSan: Bellek Hatalarını Üretime Sızmadan Yakala
test:unit:asan job’ı linux-debug-asan preset’iyle kendi build’ini yapıp testleri AddressSanitizer + UndefinedBehaviorSanitizer aktif koşuyor. Use-after-free, buffer overflow, signed overflow gibi C++ klasikleri lokalde sessizce çalışıp CI’da kırmızı yanıyor — tam da istediğimiz bu.Bu job’da küçük ama önemli bir detay var:
1 2 | test:unit:asan: needs: [] # hiçbir job'ı bekleme, pipeline başında hemen başla |
GitLab’da
needs ile pipeline bir Directed Acyclic Graph (DAG)’a dönüşüyor: ASan job’ı kendi derlemesini yaptığı için build aşamasını beklemesine gerek yok. Boşuna sırada bekletmek yerine pipeline’ın ilk saniyesinde start alıyor. Aynı numara quality:clang-format için de geçerli — o da yalnızca kaynak kodu okuyor, build çıktısına ihtiyacı yok. Ek bir kazanım daha: alakasız bir build job’ı (mesela Windows cross-compile) patladığında bu job’lar SKIP olmuyor, sonuçlarını yine üretiyor.Quality: Format Zorunlu Gibi, Tidy Yol Gösterici
1 2 3 4 5 6 7 8 9 10 11 12 13 | quality:clang-format: stage: quality needs: [] script: - find include/ src/ -name '*.h' -o -name '*.cpp' | xargs clang-format-18 --dry-run --Werror allow_failure: true quality:clang-tidy: stage: quality needs: ["build:linux:debug"] script: - find src/ -name '*.cpp' | xargs clang-tidy-18 -p build/linux-debug/ --extra-arg=-Wno-unknown-warning-option allow_failure: true |
İkisi de şu an
allow_failure: true — pipeline’ı kırmıyor, sarı ünlemle uyarıyor. Format (clang-format) için hedef bunu zamanla zorunluya çevirmek olabilir, şimdilik bu şekilde buraktım. Tidy (clang-tidy) ise statik analiz uyarıları temizlendikçe sıkılaşacak. Sıfırıncı günden her şeyi --Werror yapmak kulağa disiplinli geliyor ama pratikte “pipeline yine kırmızı, boş ver” kültürüne davetiye çıkarıyor. Önce sinyali görünür kıl, sonra zorunlu kıl.clang-tidy satırındaki
--extra-arg=-Wno-unknown-warning-option da saatlerce uğraştıran cinsten bir detay: compile_commands.json GCC ile üretildiği için içinde GCC’ye özel flag’ler var (-Wduplicated-cond gibi). clang-tidy bunları tanımayınca her dosyada hata fırlatıyor — ve bu sürücü seviyesi diagnostic, .clang-tidy dosyasındaki Checks filtresiyle de kapatılamıyor. Tek çare komut satırından susturmak.Docs ve Release: Pipeline’ın Tatlı Bonusları
pages job’ı yalnızca main branch’inde çalışıyor; Doxygen ile API dokümantasyonunu üretip GitLab Pages’e yayınlıyor. Doxygen yapılandırması job içinde dinamik üretiliyor — proje versiyonunu CMakeLists.txt‘den grep‘liyor, tema olarak doxygen-awesome-css kullanıyor. Her merge’de dokümantasyon kendi kendini güncelliyor; “doc güncellemeyi unuttuk” diye bir şey yok.release:publish ise vX.Y.Z formatında bir tag push’landığında devreye giriyor:1 2 3 4 5 6 7 8 | release:publish: stage: release rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ release: tag_name: $CI_COMMIT_TAG name: "SDLPainter $CI_COMMIT_TAG" description: ./CHANGELOG.md |
Tag attığınızda, release notlarını
CHANGELOG.md‘den alan bir GitLab Release otomatik oluşmakta.Aynı Repo İki Pipeline: GitLab + GitHub Actions
Proje GitLab’da yaşıyor ama aynı pipeline’ı GitHub Actions’a da taşıdım (
.github/workflows/ci.yml). Hatta başta da bahsettiğim gibi artık, github tarafına daha çok bakıyorum 🙂Job isimlerini de bile bilerek aynı tuttum (
build:linux:debug, test:unit:asan…) — iki arayüzde de aynı adımları görüyorsun. Linux job’ları yine aynı Docker imajını kullanıyor; yani Conan cache avantajı GitHub tarafında da geçerli.İki önemli fark var:
- Birincisi Windows stratejisi. GitLab’da Windows build’i Linux container içinde MinGW-w64 cross-compile ile yapılıyordu (SaaS Windows runner’larla uğraşmamak için). GitHub’da ise
windows-latestrunner’ı hazır geldiğinden native MSVC’i kullanabiliyoruz:
1 2 3 4 5 6 7 8 9 | build-windows-release: runs-on: windows-latest steps: - uses: ilammy/msvc-dev-cmd@v1 # MSVC ortamını hazırla - name: Cache Conan packages uses: actions/cache@v4 with: path: ${{ env.CONAN_HOME }} key: conan-windows-release-${{ hashFiles('conanfile.py') }} |
Windows’ta özel imaj olmadığından Conan cache’i
actions/cache ile, anahtarı conanfile.py‘ın hash’ine bağlayarak çözüyoruz: bağımlılıklar değişmedikçe cache geçerli, değiştiği commit’te otomatik tazeleniyor.- İkinci fark ise:
Concurrency. GitHub tarafında art arda push’larda eski pipeline’ı otomatik iptal eden bir ayar var — bu sayede, runner dakikası sınırlıysa bizlere avantaj sağlıyor ki bu önemli bir ayrıntı.
1 2 3 | concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} |
Tag pipeline’ları bilinçli olarak iptal kapsamı dışında — yarım kalan bir release istemeyiz.
Sonuç olarak github pipelinemız:
![]()
Ve gitlab pipelinemız:
![]()
Sonuç
CI/CD konularaına yönelik çıkarılacak en önemli ders: pipeline’ı hızlı tut, hataları görünür kıl, lokal ile CI’ı hizala, gerisi artık detay kalıyor 🙂
Pratikte bunu sağlayan üç önemli etken var: önceden hazırlanmış Conan cache’li özel Docker imajları (dakikaları, yaklaşık 30 dk, saniyeye indiren asıl kahraman), CMake Presets ile lokal–CI komut eşitliği ve
needs DAG’ı ile bağımsız job’ların pipeline başında hemen koşması. Bu altyapının çoğu SDLPainter’dan bağımsız; sıradaki C++ projenizde, sizler de aynısını rahatlıkla uygulayabilirsiniz. Uygularsanız veya yardıma ihtiyacınız olursa,beni de haberdar etmeyi unutmayın!İlgili repolar için aşağıdaki adreslere başvurabilirsiniz. Şimdilik aktif olarak github’a commitleri yapacağım. Bununla birlikte hem gitlab hem github CI/CD betikleri aynı repoda yaşayabiliyor.
Kaynak kod:
