sdl-painter – Proje Altyapısı(faz 0) – CI/CD

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:

AşamaJob’larNe yapıyor?
buildbuild:linux:debug/releasebuild:windows:debug/releaseİki platform × iki konfigürasyon
testtest:unittest:unit:asanGTest (headless) + ASan/UBSan
qualityquality:clang-formatquality:clang-tidyFormat + statik analiz
docspagesDoxygen → GitLab Pages (yalnız main)
releaserelease:publishv*.*.* 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.py kopyalanıp Debug + Release (Vulkan’lı varyantlar dahil) için conan 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-loader recipe’ı Windows’ta USE_MASM=True‘yu hardcode ettiği için MinGW gcc derleyemiyor; conanfile.py configure() bu hedefte with_vulkan‘ı otomatik False ç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 😉

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:
  • 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 imajda MESA_LOADER_DRIVER_OVERRIDE=llvmpipe ayarlı.
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:

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

İ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:
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-latest runner’ı hazır geldiğinden native MSVC’i kullanabiliyoruz:

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

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: