Jenkins ile Çoklu Branch Pipeline Yönetimi

Büyük bir ekiple çalışıyorsanız ve her developer kendi branch’inde özgürce geliştirme yapıyorsa, bu branch’lerin hepsini ayrı ayrı pipeline ile yönetmek ciddi bir baş ağrısına dönüşebilir. Jenkins’in Multibranch Pipeline özelliği tam da bu noktada hayat kurtarıyor. Tek bir pipeline tanımıyla onlarca branch’i otomatik olarak keşfedip yönetebiliyorsunuz. Bu yazıda gerçek dünya senaryolarıyla Jenkins’te çoklu branch pipeline yönetimini en ince detayına kadar inceleyeceğiz.

Jenkins Multibranch Pipeline Nedir?

Klasik Jenkins job’larında her branch için ayrı bir job tanımlamanız gerekir. Yeni branch açıldığında biri unutur, CI çalışmaz, build kırık kalır. Multibranch Pipeline bu sorunu kökten çözüyor.

Jenkins, repository’nizi düzenli olarak tarar (scan), yeni branch’leri otomatik keşfeder ve her branch için ayrı bir pipeline oluşturur. Branch silindiğinde Jenkins’teki karşılığı da temizlenir. Bütün bu mantık tek bir Jenkinsfile üzerinden yürür.

Temel avantajlar şöyle sıralanabilir:

  • Otomatik branch keşfi: Yeni branch açıldığı an pipeline hazır olur
  • İzolasyon: Her branch kendi bağımsız build geçmişine sahiptir
  • Merkezi konfigürasyon: Tek Jenkinsfile tüm branch’leri yönetir
  • Pull Request desteği: GitHub/GitLab PR’larını da otomatik build eder
  • Temiz kaynak: Silinen branch’lerin pipeline’ı da temizlenir

Kurulum ve Gereksinimler

Önce Jenkins’in güncel olduğundan ve gerekli plugin’lerin yüklü olduğundan emin olalım.

# Jenkins versiyon kontrolü
java -jar jenkins-cli.jar -s http://localhost:8080 version

# Gerekli plugin'leri CLI üzerinden yüklemek için
java -jar jenkins-cli.jar -s http://localhost:8080 
  -auth admin:your-api-token 
  install-plugin workflow-multibranch 
  install-plugin branch-api 
  install-plugin git 
  install-plugin github-branch-source 
  install-plugin pipeline-stage-view

Jenkins UI üzerinden de Manage Jenkins > Manage Plugins > Available sekmesinden şu plugin’leri aratıp kurabilirsiniz:

  • Pipeline: Multibranch: Temel multibranch desteği
  • GitHub Branch Source: GitHub entegrasyonu
  • GitLab Branch Source: GitLab entegrasyonu
  • Bitbucket Branch Source: Bitbucket entegrasyonu
  • Pipeline: Stage View: Pipeline görselleştirme

İlk Multibranch Pipeline Job’ını Oluşturmak

Jenkins dashboard’dan New Item diyoruz, bir isim veriyoruz ve Multibranch Pipeline seçiyoruz. Açılan ekranda dikkat etmemiz gereken birkaç kritik ayar var.

Branch Sources bölümünde repository bilgilerini giriyoruz. GitHub kullanıyorsanız credentials olarak Personal Access Token ya da SSH key eklemeniz gerekiyor. Credentials’ı Jenkins’e şu şekilde ekleyebilirsiniz:

# Jenkins CLI ile credential eklemek
cat <<EOF | java -jar jenkins-cli.jar -s http://localhost:8080 
  -auth admin:your-api-token create-credentials-by-xml 
  system::system::jenkins _
<com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
  <scope>GLOBAL</scope>
  <id>github-credentials</id>
  <description>GitHub PAT</description>
  <username>your-github-username</username>
  <password>ghp_your_personal_access_token</password>
</com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
EOF

Scan Multibranch Pipeline Triggers ayarında Periodically if not otherwise run seçip interval’ı 1 dakika yapmanızı öneririm. Webhook kurduğunuzda zaten anlık tetiklenecek, bu sadece fallback için.

Jenkinsfile Yapısı ve Branch Bazlı Mantık

İşte burada asıl güç ortaya çıkıyor. Jenkinsfile içinde env.BRANCH_NAME değişkenini kullanarak her branch’e özel davranış tanımlayabilirsiniz.

pipeline {
    agent any
    
    environment {
        DOCKER_REGISTRY = 'registry.mycompany.com'
        APP_NAME = 'myapp'
        SLACK_CHANNEL = '#ci-cd-alerts'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    // Branch adını temizle (feature/my-feature -> feature-my-feature)
                    env.SAFE_BRANCH_NAME = env.BRANCH_NAME.replaceAll('/', '-')
                    env.BUILD_TAG = "${env.APP_NAME}-${env.SAFE_BRANCH_NAME}-${env.BUILD_NUMBER}"
                    echo "Building branch: ${env.BRANCH_NAME}"
                    echo "Build tag: ${env.BUILD_TAG}"
                }
            }
        }
        
        stage('Unit Tests') {
            steps {
                sh './gradlew test'
            }
            post {
                always {
                    junit 'build/test-results/**/*.xml'
                }
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    def image = docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${env.BUILD_TAG}")
                    env.DOCKER_IMAGE = image.id
                }
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                echo "Deploying to staging environment..."
                sh """
                    kubectl set image deployment/${APP_NAME} 
                    ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${env.BUILD_TAG} 
                    -n staging
                """
            }
        }
        
        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                timeout(time: 10, unit: 'MINUTES') {
                    input message: "Production'a deploy edilsin mi?", 
                          ok: "Deploy Et",
                          submitter: "devops-team"
                }
                sh """
                    kubectl set image deployment/${APP_NAME} 
                    ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${env.BUILD_TAG} 
                    -n production
                """
            }
        }
    }
}

Bu Jenkinsfile’da when { branch 'develop' } direktifi sayesinde staging deploy sadece develop branch’inde çalışır. Feature branch’lerinde bu stage tamamen atlanır.

Branch Filtreleme ve Keşif Stratejileri

Her branch’i build etmek istemeyebilirsiniz. Özellikle büyük repo’larda deneme amaçlı açılan branch’ler gereksiz kaynak tüketir.

// Jenkins Job DSL ile Multibranch Pipeline programatik oluşturma
multibranchPipelineJob('my-application') {
    branchSources {
        github {
            id('my-app-github')
            repoOwner('my-organization')
            repository('my-application')
            credentialsId('github-credentials')
            
            // Branch keşif stratejisi
            traits {
                // Sadece PR'ları build et (origin branch değil)
                headRegexFilter {
                    regex('(main|develop|release/.*|feature/.*|hotfix/.*)')
                }
                
                // Stale branch'leri görmezden gel (30 gün aktif değilse)
                branchDiscoveryTrait {
                    strategyId(3) // 1: sadece PR olmayan, 2: sadece PR olan, 3: her ikisi
                }
            }
        }
    }
    
    orphanedItemStrategy {
        discardOldItems {
            numToKeep(10)
            daysToKeep(30)
        }
    }
    
    triggers {
        periodic(1) // 1 dakikada bir scan
    }
}

Branch isimlerini regex ile filtrelemek için Jenkinsfile içinde de bunu yapabilirsiniz:

pipeline {
    agent any
    
    stages {
        stage('Integration Tests') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    branch pattern: 'release/.*', comparator: 'REGEXP'
                    branch pattern: 'hotfix/.*', comparator: 'REGEXP'
                }
            }
            steps {
                sh './gradlew integrationTest'
            }
        }
        
        stage('Security Scan') {
            when {
                not {
                    branch pattern: 'dependabot/.*', comparator: 'REGEXP'
                }
            }
            steps {
                sh 'trivy image ${DOCKER_IMAGE}'
            }
        }
    }
}

Shared Libraries ile Kod Tekrarını Önleme

Çok sayıda repository’niz varsa her birinde aynı Jenkinsfile’ı kopyalamak zorunda kalmak kabustur. Jenkins Shared Libraries bu sorunu çözüyor.

Önce shared library repo’sunuzu oluşturun. Yapısı şu şekilde olmalı:

jenkins-shared-library/
├── vars/
│   ├── buildAndDeploy.groovy
│   ├── runTests.groovy
│   └── dockerBuild.groovy
├── src/
│   └── com/
│       └── mycompany/
│           └── pipeline/
│               └── Utils.groovy
└── resources/
    └── scripts/
        └── health-check.sh

vars/buildAndDeploy.groovy içeriği:

def call(Map config = [:]) {
    def appName = config.appName ?: error("appName zorunludur!")
    def registry = config.registry ?: 'registry.mycompany.com'
    def namespace = config.namespace ?: 'default'
    
    pipeline {
        agent {
            kubernetes {
                yaml """
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: docker
    image: docker:24-dind
    securityContext:
      privileged: true
  - name: kubectl
    image: bitnami/kubectl:latest
    command: ['cat']
    tty: true
"""
            }
        }
        
        stages {
            stage('Build') {
                steps {
                    container('docker') {
                        sh "docker build -t ${registry}/${appName}:${env.BUILD_NUMBER} ."
                        sh "docker push ${registry}/${appName}:${env.BUILD_NUMBER}"
                    }
                }
            }
            
            stage('Deploy') {
                when {
                    anyOf {
                        branch 'main'
                        branch 'develop'
                    }
                }
                steps {
                    container('kubectl') {
                        script {
                            def targetNamespace = env.BRANCH_NAME == 'main' ? 'production' : 'staging'
                            sh """
                                kubectl set image deployment/${appName} 
                                ${appName}=${registry}/${appName}:${env.BUILD_NUMBER} 
                                -n ${targetNamespace}
                                kubectl rollout status deployment/${appName} -n ${targetNamespace}
                            """
                        }
                    }
                }
            }
        }
    }
}

Bu shared library’yi kullanan minimal bir Jenkinsfile:

@Library('jenkins-shared-library@main') _

buildAndDeploy(
    appName: 'payment-service',
    registry: 'registry.mycompany.com',
    namespace: 'payments'
)

Shared library’yi Jenkins’e tanıtmak için Manage Jenkins > Configure System > Global Pipeline Libraries bölümüne gidin ve repo bilgilerini ekleyin.

Webhook Kurulumu ile Anlık Tetikleme

Periyodik scan yerine webhook kullanmak çok daha verimli. GitHub webhook’u kurmak için:

# GitHub webhook'u curl ile oluşturma
curl -X POST 
  -H "Authorization: token ghp_your_token" 
  -H "Content-Type: application/json" 
  -d '{
    "name": "web",
    "active": true,
    "events": ["push", "pull_request", "create", "delete"],
    "config": {
      "url": "https://jenkins.mycompany.com/github-webhook/",
      "content_type": "json",
      "secret": "your-webhook-secret",
      "insecure_ssl": "0"
    }
  }' 
  https://api.github.com/repos/my-organization/my-application/hooks

Jenkins tarafında webhook secret’ı doğrulamak için Manage Jenkins > Configure System > GitHub > Advanced bölümüne gidin. Secret’ı buraya gireceksiniz.

Paralel Stage’ler ile Build Süresini Kısaltma

Feature branch’lerinde hızlı feedback önemli. Paralel stage’lerle test sürelerini dramatik biçimde düşürebilirsiniz.

pipeline {
    agent any
    
    stages {
        stage('Parallel Tests') {
            parallel {
                stage('Unit Tests') {
                    agent {
                        label 'java-agent'
                    }
                    steps {
                        sh './gradlew test --tests "*UnitTest*"'
                    }
                    post {
                        always {
                            junit 'build/test-results/test/*.xml'
                        }
                    }
                }
                
                stage('Integration Tests') {
                    when {
                        anyOf {
                            branch 'develop'
                            branch 'main'
                            branch pattern: 'release/.*', comparator: 'REGEXP'
                        }
                    }
                    agent {
                        label 'java-agent'
                    }
                    steps {
                        sh './gradlew integrationTest'
                    }
                }
                
                stage('Static Analysis') {
                    agent {
                        label 'java-agent'
                    }
                    steps {
                        sh './gradlew sonarqube 
                            -Dsonar.projectKey=my-app 
                            -Dsonar.host.url=https://sonar.mycompany.com 
                            -Dsonar.login=$SONAR_TOKEN'
                    }
                }
            }
        }
        
        stage('Quality Gate') {
            when {
                anyOf {
                    branch 'develop'
                    branch 'main'
                }
            }
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }
}

Gerçek Dünya Senaryosu: Gitflow ile Tam Pipeline

Ekibiniz Gitflow kullanıyorsa şu branch stratejisi çok işe yarar:

  • feature/*: Sadece build ve unit test
  • develop: Build, tüm testler, staging deploy
  • release/*: Build, tüm testler, UAT deploy, tag oluşturma
  • main: Build, smoke test, production deploy (onaylı)
  • hotfix/*: Build, kritik testler, hızlı production deploy
pipeline {
    agent any
    
    environment {
        REGISTRY = credentials('docker-registry-url')
        KUBECONFIG = credentials('kubeconfig')
    }
    
    stages {
        stage('Build') {
            steps {
                script {
                    env.IMAGE_TAG = sh(
                        script: "git rev-parse --short HEAD",
                        returnStdout: true
                    ).trim()
                    
                    sh "docker build -t ${REGISTRY}/app:${env.IMAGE_TAG} ."
                    sh "docker push ${REGISTRY}/app:${env.IMAGE_TAG}"
                }
            }
        }
        
        stage('Tests') {
            parallel {
                stage('Unit') {
                    steps { sh 'make test-unit' }
                }
                stage('Lint') {
                    steps { sh 'make lint' }
                }
            }
        }
        
        stage('Integration Tests') {
            when {
                not {
                    branch pattern: 'feature/.*', comparator: 'REGEXP'
                }
            }
            steps {
                sh 'make test-integration'
            }
        }
        
        stage('Deploy Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh """
                    helm upgrade --install app ./helm/app 
                    --namespace staging 
                    --set image.tag=${env.IMAGE_TAG} 
                    --set ingress.host=staging.mycompany.com 
                    --wait --timeout 5m
                """
            }
        }
        
        stage('Deploy UAT') {
            when {
                branch pattern: 'release/.*', comparator: 'REGEXP'
            }
            steps {
                sh """
                    helm upgrade --install app ./helm/app 
                    --namespace uat 
                    --set image.tag=${env.IMAGE_TAG} 
                    --set ingress.host=uat.mycompany.com 
                    --wait --timeout 5m
                """
                // Release tag'i oluştur
                sh """
                    git tag -a "rc-${env.BUILD_NUMBER}" -m "Release candidate"
                    git push origin "rc-${env.BUILD_NUMBER}"
                """
            }
        }
        
        stage('Deploy Production') {
            when {
                anyOf {
                    branch 'main'
                    branch pattern: 'hotfix/.*', comparator: 'REGEXP'
                }
            }
            steps {
                script {
                    if (env.BRANCH_NAME.startsWith('hotfix/')) {
                        echo "Hotfix tespiti: Manual onay atlanıyor..."
                    } else {
                        timeout(time: 30, unit: 'MINUTES') {
                            input message: "Production deploy onayı gerekiyor",
                                  ok: "Onayla",
                                  submitter: "release-managers"
                        }
                    }
                    
                    sh """
                        helm upgrade --install app ./helm/app 
                        --namespace production 
                        --set image.tag=${env.IMAGE_TAG} 
                        --set ingress.host=mycompany.com 
                        --wait --timeout 10m
                    """
                }
            }
        }
    }
    
    post {
        failure {
            slackSend(
                channel: env.SLACK_CHANNEL,
                color: 'danger',
                message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} - Branch: ${env.BRANCH_NAME}"
            )
        }
        success {
            script {
                if (env.BRANCH_NAME == 'main') {
                    slackSend(
                        channel: '#releases',
                        color: 'good',
                        message: "Production deploy başarılı: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
                    )
                }
            }
        }
        cleanup {
            cleanWs()
        }
    }
}

Orphaned Branch Temizliği ve Kaynak Yönetimi

Zamanla silinmiş branch’lerin Jenkins’te kalıntıları birikir. Otomatik temizlik için:

# Jenkins Groovy script ile orphaned branch job'larını temizleme
# Manage Jenkins > Script Console'dan çalıştırın

def daysToKeep = 7
def cutoff = new Date() - daysToKeep

Jenkins.instance.getAllItems(WorkflowMultiBranchProject.class).each { project ->
    project.items.findAll { branch ->
        def lastBuild = branch.getLastBuild()
        if (lastBuild == null) return true
        def buildDate = new Date(lastBuild.getTimeInMillis())
        return buildDate < cutoff
    }.each { staleBranch ->
        println "Siliniyor: ${staleBranch.fullName}"
        staleBranch.delete()
    }
}

Job konfigürasyonunda da şu ayarları yapın:

  • Discard old items aktif olsun
  • Days to keep old items: 30
  • Max # of old items to keep: 10

Bu sayede her branch en fazla son 10 build’ini saklasın ve 30 günden eski build’ler otomatik silinsin.

Sorun Giderme: Yaygın Hatalar ve Çözümleri

Multibranch pipeline kurulumunda sıkça karşılaşılan sorunlar:

Branch keşfedilmiyor

  • Credentials doğru mu kontrol edin
  • Repository URL’i erişilebilir mi test edin
  • Scan Multibranch Pipeline Now ile manuel tarama başlatın
  • Jenkins log’larında [BranchDiscovery] satırlarına bakın

Jenkinsfile bulunamıyor

  • Branch’in kök dizininde Jenkinsfile (büyük J) olduğundan emin olun
  • Job konfigürasyonundaki script path’i kontrol edin
  • Default değer Jenkinsfile olmalı

Webhook çalışmıyor

# Webhook delivery geçmişini kontrol edin
curl -H "Authorization: token ghp_your_token" 
  https://api.github.com/repos/org/repo/hooks/HOOK_ID/deliveries 
  | jq '.[0:5] | .[] | {id, event, created_at, status: .status}'

Agent bulunamıyor

// Label yerine any kullanarak test edin
agent any
// Çalışıyorsa agent label'larını kontrol edin

Sonuç

Jenkins Multibranch Pipeline, modern yazılım geliştirme süreçlerinde vazgeçilmez bir araç haline geldi. Tek bir Jenkinsfile ile tüm branch stratejinizi kodlayabilir, her branch’e özel davranış tanımlayabilir ve ekibinizin iş akışını otomatikleştirebilirsiniz.

Kurulumda dikkat etmeniz gereken kritik noktalar şunlar: doğru credential yönetimi, akıllıca branch filtreleme, shared library kullanımıyla kod tekrarından kaçınma ve orphaned branch temizliği. Özellikle büyük ekiplerde shared library olmadan yönetim kaosa dönüşebilir, bu yüzden ortak pipeline mantığını mümkün olan en erken aşamada library’e taşımanızı öneriyorum.

Webhook entegrasyonunu da ihmal etmeyin. Periyodik scan ile başlayabilirsiniz ama production ortamında webhook her zaman daha güvenilir ve anlık feedback sağlar. Geliştiricileriniz push yaptıktan sonra saniyeler içinde build başlıyorsa, CI süreciniz gerçekten işe yarıyor demektir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir