Mert Özen Her Satırda Daha İleri
DevOps CI/CD Temelleri — #02

Pipeline Anatomisi: Trigger, Stage, Job, Step ve Fail Fast

Mert Özen 8 Kas 2026 10 dk 5 görüntülenme
Pipeline Anatomisi: Trigger, Stage, Job, Step ve Fail Fast

Bir pipeline trigger'larla başlar, birbirine bağımlı job'lardan (Jenkins'te stage) ve sırayla koşan step'lerden oluşur; build mantığını lokalde çalışan bir script'e koyup pipeline'ı sadece orkestra şefi yapmak, ucuz kontrolleri öne almak ve paralel dallarda fail fast kullanmak geri bildirimi hızlandırır. GitHub Actions, Jenkins ve Azure DevOps terim eşlemesi, act ve Jenkins ile koşulmuş gerçek çıktılar.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), Angular 22.2, Node.js 24, act 0.2.97, Jenkins 2.580.2 LTS (Long-Term Support) ve Docker 29 ile denenmiştir.

Önce Tahmin Et

Bookstore'un pipeline'ında iki iş paralel koşuyor: API (Application Programming Interface) tarafında api, Angular tarafında web. Birisi C# dosyasına fazladan iki boşluk bırakarak commit ediyor. Jenkins'te pipeline'ın başında parallelsAlwaysFailFast() seçeneği açık. api formatting kontrolünde kırıldığında web ne yapar: sonuna kadar koşup yeşil mi biter, yoksa yarıda mı kesilir? Ve iki işin ardından gelen Publish aşaması çalışır mı? Cevabı yazının ortasındaki gerçek Jenkins çıktısında göreceksin.

Pipeline'ın Parçaları: Trigger, Stage, Job, Step

Her CI (Continuous Integration) ve CD (Continuous Delivery) tool'u kendi kelimelerini kullanıyor ama altta yatan yapı aynı. Bir trigger pipeline'ı başlatır. Pipeline birbirine bağımlı iş birimlerinden oluşur; her birim bir makinede (runner, agent) koşar ve sırayla çalışan adımlardan oluşur. Adım, bir shell komutu ya da hazır bir action/plugin çağrısıdır. Bazı tool'larda iş birimlerinin üstünde mantıksal bir gruplama daha vardır.

Tool'lar arasında geçiş yaparken en çok kafa karıştıran şey, aynı kelimenin farklı katmanı anlatması. Eşleme tablosu:

KavramGitHub ActionsJenkins (declarative)Azure DevOps
Tanım dosyası.github/workflows/*.ymlJenkinsfileazure-pipelines.yml
Bütün akışworkflowpipelinepipeline
Mantıksal grupyok (job'lar ve needs)stagestage
Bir makinede koşan birimjobstage içindeki agentjob
Tek adımstep (run ya da uses)step (sh, echo…)step (script ya da task)

Bu yazıda kavramları anlatmak için GitHub Actions ve Jenkins parçalarını yan yana kullanacağım. Tool'a özgü ayrıntılar (matrix, reusable workflow, shared library) ilerideki GitHub Actions, Jenkins ve Azure DevOps Pipelines serilerinin konusu.

Trigger Türleri

Bookstore'un pipeline'ı beş farklı olayla başlıyor:

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
  schedule:
    - cron: '0 3 * * 1'
  workflow_dispatch:

push trunk'a giren her değişikliği doğrular; CI'ın kalbi bu. tags filtresi, v1.4.0 gibi bir tag push edildiğinde release akışını başlatmak için. pull_request, değişikliği merge edilmeden önce sınar; GitHub bu olayda PR (pull request) branch'ini hedef branch ile birleştiren geçici bir merge commit'i build eder, yani test ettiğin şey "merge edilirse ne olacağı"dır. schedule her pazartesi 03:00'te (UTC, Coordinated Universal Time) koşar; kimse dokunmasa bile dışarıdan gelen değişiklikleri, örneğin yeni yayınlanmış bir paket sürümünü veya süresi dolmuş bir sertifikayı yakalamak için işe yarar. workflow_dispatch ise elle tetiklemedir.

Jenkins'te aynı fikir triggers bloğuyla ifade ediliyor: cron('H 3 * * 1'). Buradaki H, dakikayı job adına göre hash'ten türetir; yüz job aynı anda 03:00'e kurulduğunda hepsinin aynı saniyede başlamasını engeller. Push ve PR trigger'ları Jenkins'te genellikle webhook ve multibranch pipeline ile kurulur.

Önce Script, Sonra Pipeline

Pipeline tanımına doğrudan onlarca run: satırı yazmak cazip ama bir süre sonra iki problem doğuruyor: aynı adımları lokalde koşturamıyorsun ve tool değiştirmek istediğinde her şeyi yeniden yazıyorsun. Daha sağlam yol, build mantığını repository'deki bir script'e koymak ve pipeline'ı sadece orkestra şefi yapmak. Bookstore'un build.sh'ı:

#!/usr/bin/env bash
# Kullanım: ./build.sh [api|web|all]
set -euo pipefail

api() {
    dotnet restore --locked-mode
    dotnet format --verify-no-changes --no-restore
    dotnet build --no-restore -c Release
    dotnet test --no-build -c Release
    dotnet publish src/Bookstore.Api --no-build -c Release -o artifacts/api
}

web() {
    (
        cd web/bookstore-web
        npm ci
        npx ng test --watch=false
        npx ng build
    )
}

case "${1:-all}" in
    api) api ;;
    web) web ;;
    all) api; web ;;
    *) echo "unknown target: $1" >&2; exit 2 ;;
esac

set -euo pipefail üç şeyi garanti ediyor: bir komut hata verirse script durur, tanımsız değişken kullanmak hatadır ve pipe içindeki bir hata gizlenmez. Bu satır olmadan dotnet test kırılsa bile script publish'e devam eder ve pipeline yeşil görünür.

Sıralamaya da dikkat et: restore → format kontrolü → build → test → publish. Her adım bir öncekinin çıktısını kullanıyor; --no-restore ve --no-build aynı işin iki kez yapılmasını engelliyor. --locked-mode, packages.lock.json'daki sürümlerin dışına çıkılmasına izin vermiyor; bunu bir sonraki yazıda açacağız.

Aynı script'i lokalde koşturduğumda ./build.sh api yaklaşık 5 saniyede bitti. Pipeline kırıldığında yapacağın ilk şey, aynı komutu kendi makinende denemek olmalı; script bunu mümkün kılıyor.

Paralel Job'lar ve DAG

API ve web birbirinden bağımsız; birini beklemek için diğerini sıraya koymanın anlamı yok. Paketlemek içinse ikisinin de yeşil olması gerekiyor. Bu bağımlılıklar bir DAG (Directed Acyclic Graph) oluşturuyor:

02-pipeline-dag-tr.png

GitHub Actions'ta job'lar varsayılan olarak paralel koşar; sıralamayı needs belirler:

jobs:
  api:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          global-json-file: global.json
      - run: ./build.sh api
      - uses: actions/upload-artifact@v5
        with:
          name: api
          path: artifacts/api

  web:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: ./build.sh web
      - uses: actions/upload-artifact@v5
        with:
          name: web
          path: web/bookstore-web/dist/bookstore-web/browser

  publish:
    needs: [api, web]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v5
      - run: ls api web

Her job ayrı ve temiz bir makinede başlıyor; o yüzden api'nin ürettiği dosyalar publish'e kendiliğinden gelmiyor. Job'lar arasında dosya taşımanın yolu artifact yüklemek ve indirmek. Artifact action'larının v5'ini bilinçli seçtim: act 0.2.97'nin yerel artifact server'ı v6 ve v7 ile çalışmadı, v5 ile çalıştı. GitHub'ın kendi runner'larında güncel sürümleri kullanabilirsin.

act ile koşulmuş pipeline'ın özeti:

[pipeline/web]   ✅  Success - Main ./build.sh web [7.594339042s]
[pipeline/api]   | Passed!  - Failed:     0, Passed:     6, Skipped:     0, Total:     6, Duration: 158 ms - Bookstore.Api.Tests.dll (net10.0)
[pipeline/api]   ✅  Success - Main ./build.sh api [10.411911584s]
[pipeline/publish]   | Found 2 artifact(s)
[pipeline/publish]   | Total of 2 artifact(s) downloaded
[pipeline/publish]   | api:
[pipeline/publish]   | Bookstore.Api
[pipeline/publish]   | Bookstore.Api.deps.json
[pipeline/publish]   | Bookstore.Api.dll
[pipeline/publish]   | Bookstore.Api.pdb
[pipeline/publish]   | Bookstore.Api.runtimeconfig.json
[pipeline/publish]   | Bookstore.Api.staticwebassets.endpoints.json
[pipeline/publish]   | appsettings.Development.json
[pipeline/publish]   | appsettings.json
[pipeline/publish]   | web.config
[pipeline/publish]   |
[pipeline/publish]   | web:
[pipeline/publish]   | config.json
[pipeline/publish]   | favicon.ico
[pipeline/publish]   | index.html
[pipeline/publish]   | main-S3Z6CNMD.js
[pipeline/publish]   | styles-5INURTSO.css
[pipeline/publish] 🏁  Job succeeded

web 7,6 saniyede, api 10,4 saniyede bitti; ikisi aynı anda koştuğu için toplam süreye yaklaşık 10 saniye eklediler, sıralı olsalar 18 saniye ekleyeceklerdi. İki job'lık bir projede fark küçük, ama job sayısı arttıkça paralellik pipeline süresini en uzun dalın süresine indiriyor.

Aynı Pipeline Jenkins'te

Script sayesinde Jenkinsfile da kısa kalıyor:

pipeline {
    agent any
    options {
        timeout(time: 15, unit: 'MINUTES')
        parallelsAlwaysFailFast()
    }
    triggers {
        cron('H 3 * * 1')
    }
    stages {
        stage('Build and test') {
            parallel {
                stage('api') {
                    steps {
                        sh './build.sh api'
                    }
                }
                stage('web') {
                    steps {
                        sh './build.sh web'
                    }
                }
            }
        }
        stage('Publish') {
            steps {
                archiveArtifacts artifacts: 'artifacts/api/**, web/bookstore-web/dist/bookstore-web/browser/**'
            }
        }
    }
}

Bunu yerelde bir Jenkins container'ında, repository'yi Git üzerinden çekecek şekilde koşturdum. Konsoldaki iskelet, GitHub Actions'taki DAG'in aynısı:

[Pipeline] { (Build and test)
[Pipeline] parallel
[Pipeline] { (Branch: api)
[Pipeline] { (Branch: web)
+ ./build.sh api
+ ./build.sh web
Passed!  - Failed:     0, Passed:     6, Skipped:     0, Total:     6, Duration: 94 ms - Bookstore.Api.Tests.dll (net10.0)
[Pipeline] { (Publish)
[Pipeline] archiveArtifacts
Archiving artifacts
[Pipeline] End of Pipeline
Finished: SUCCESS

Farka dikkat: GitHub Actions'ta api ve web iki ayrı makinede koştu, Jenkins'te ise aynı agent'ın aynı workspace'inde. Paralel dallar aynı klasöre yazıyorsa birbirinin dosyasını ezebilir; burada api artifacts/'a, web web/ altına yazdığı için sorun yok, ama paylaşılan workspace'te bu her zaman kontrol edilmesi gereken bir şey.

Fail Fast: Ucuz Kontrolleri Öne Al

Fail fast iki anlama geliyor. Birincisi sıralama: bir kontrol ne kadar ucuzsa o kadar önde olmalı. build.sh'ta formatting kontrolü build'den önce. Hatalı bir commit'te lokal çıktı:

$ ./build.sh api
  Determining projects to restore...
  All projects are up-to-date for restore.
src/Bookstore.Api/Catalog/BookCatalog.cs(14,31): error WHITESPACE: Fix whitespace formatting. Delete 2 characters. [src/Bookstore.Api/Bookstore.Api.csproj]
src/Bookstore.Api/Catalog/BookCatalog.cs(14,36): error WHITESPACE: Fix whitespace formatting. Delete 2 characters. [src/Bookstore.Api/Bookstore.Api.csproj]
$ echo $?
2

Hata 3 saniyede geldi; build ve test hiç başlamadı. Kontrol testlerden sonra olsaydı aynı bilgiyi tam bir build ve test turunun sonunda alacaktın.

İkinci anlam paralel dallarla ilgili: bir dal kırıldıysa diğerlerinin sonucunu beklemenin anlamı var mı? Aynı bozuk commit Jenkins'te koştuğunda:

[Pipeline] parallel
[Pipeline] { (Branch: api)
[Pipeline] { (Branch: web)
+ ./build.sh api
+ ./build.sh web
/var/jenkins_home/workspace/bookstore/src/Bookstore.Api/Catalog/BookCatalog.cs(14,31): error WHITESPACE: Fix whitespace formatting. Delete 2 characters. [/var/jenkins_home/workspace/bookstore/src/Bookstore.Api/Bookstore.Api.csproj]
added 291 packages, and audited 292 packages in 3s
❯ Building...
Failed in branch api
Sending interrupt signal to process
✖ Building... [FAILED: The service was stopped]
Terminated
Failed in branch web
[Pipeline] { (Publish)
Stage "Publish" skipped due to earlier failure(s)
ERROR: script returned exit code 2
Finished: FAILURE

Giriş sorusunun cevabı burada: api kırıldığı anda Jenkins web'e interrupt sinyali gönderdi ve Angular build'i yarıda kesildi; Publish atlandı. Agent boşa çalışmadı ve sonuç daha erken geldi. Bedeli şu: web'de de bir hata olsaydı onu bu turda göremeyecektin. Fail fast, "ilk hatayı hızlı göster" ile "bütün hataları tek turda göster" arasında bir tercih.

GitHub Actions'ta bağımsız job'lar varsayılan olarak birbirini durdurmaz; biri kırılsa diğeri sonuna kadar koşar, sadece ona needs ile bağlı job'lar atlanır. Aynı fikir matrix job'larda strategy.fail-fast ayarıyla kontrol ediliyor.

Pipeline as Code

Workflow dosyası da Jenkinsfile da repository'de duruyor ve her değişiklik PR'dan geçiyor. Bu üç şey kazandırıyor: pipeline'daki değişiklik kodla aynı review'dan geçer, geçmişi git log'da görünür ve bir commit'i checkout ettiğinde o commit'in nasıl build edildiği de elinde olur. Tool'un web arayüzünden tıklanarak kurulan pipeline'ların bunların hiçbiri yok; kimin neyi ne zaman değiştirdiği çoğu zaman bilinmez.

Pipeline dosyaları production'a erişebilen kodu tanımladığı için onlara normal koddan daha sıkı bir review kuralı koymak mantıklı. Git & GitHub serisinin "Git ile Ekip Çalışması: Branch Stratejileri, Rulesets ve Conventional Commits" yazısında anlattığım CODEOWNERS dosyasıyla .github/workflows/ ve Jenkinsfile için platform ekibinin onayını zorunlu kılabilirsin. Pipeline güvenliğini yedinci yazıda ayrıca ele alacağız.

Üretimde Dikkat

Her job'a bir zaman sınırı koy. GitHub Actions'ta varsayılan job timeout'u 360 dakika; asılı kalan bir test altı saat boyunca runner'ı meşgul eder ve faturaya yansır. Bookstore'da timeout-minutes: 15, Jenkins'te timeout(time: 15, unit: 'MINUTES') kullandım. Değeri normal süreye göre seç: işin normalde 2 dakika sürüyorsa 15 dakika rahat bir üst sınır.

Pipeline süresini ölçerken toplamla yetinme, adımlara bak. act koşusunda api job'ında asıl iş olan ./build.sh api 10,4 saniye sürdü, ondan önceki setup-dotnet adımı ise 15,1 saniye. Yani job'ın yarısından fazlası SDK kurulumuydu. Pipeline'ı hızlandırmak istediğinde ilk iş, zamanın nereye gittiğini görmek; cache ve paralelliği son yazıda ölçerek ele alacağız.

Son olarak log'lara dikkat. Paralel dalların çıktısı Jenkins konsolunda iç içe akar; yukarıdaki fail fast çıktısında npm satırları ile dotnet format satırları karışık geldi. Bir dalı ayrı okumak için tool'un dal görünümünü (Jenkins'te stage view, GitHub'da job sayfası) kullan.

Sık Düşülen Tuzaklar

Lokalde çalıştırılamayan "sihirli" adımlar: pipeline'da tanımlı bir ortam değişkeni, runner'a önceden kurulmuş bir tool ya da YAML'ın (YAML Ain't Markup Language) içine gömülmüş 40 satırlık bir shell bloğu. Pipeline kırıldığında bunları lokalde tekrar üretemezsin ve debugging "commit at, bekle, log'a bak" döngüsüne döner.

Her şeyi tek dev job'a koymak: restore, build, unit test, integration test, e2e (end-to-end) test ve paketleme tek job'da sıralı koşunca hem paralellikten vazgeçersin hem de hangi aşamanın kırıldığını bulmak için uzun bir log'u kazırsın.

Mantığı ikiye bölmek: aynı adımın bir versiyonu script'te, biraz farklı bir versiyonu YAML'da. Lokalde yeşil, pipeline'da kırmızı build'lerin çoğu buradan çıkar. Tek doğru kaynak script olmalı.

set -e'yi unutmak: hata veren komuttan sonra script devam eder ve pipeline yeşil biter. Bu, yanlış alarmdan çok daha kötü; kırık bir build'i başarılı gösterir.

Küçük Bir Deneme

★☆☆ Kendi projenin pipeline'ındaki her job'ın süresini not et. En uzun dal hangisi ve toplam sürenin yüzde kaçı?

★★☆ Projen için bir build.sh (ya da build.ps1) yaz ve pipeline'daki run: adımlarını bu script'e taşı. Sonra script'i lokalde temiz bir klon üzerinde koştur; pipeline'ın sahip olup senin makinende olmayan bir şey çıktı mı?

★★★ Jenkinsfile'daki parallelsAlwaysFailFast() satırını kaldırıp aynı bozuk commit'i tekrar koştur. web dalı bu sefer ne kadar sürdü ve Publish yine atlandı mı?

Bir sonraki yazıda publish aşamasının ürettiği şeye odaklanacağız: artifact'ı bir kez build edip her environment'a aynısını taşımak, sürüm damgalamak ve aynı commit'ten iki kez build almanın neden aynı paketi üretmediği.