DeployU
Interviews / DevOps & Cloud Infrastructure / Teams need automated CI for all branches with different deployment targets. Implement multi-branch pipelines.

Questions

Teams need automated CI for all branches with different deployment targets. Implement multi-branch pipelines.

practical Multi-branch Pipelines Interactive Quiz Code Examples

The Scenario

Your team has grown and adopted a branching strategy:

  • main - Production deployments
  • develop - Staging deployments
  • feature/* - Dev environment + PR checks
  • hotfix/* - Fast-track to production

Currently, you manually create a new job for each branch. With 50 active branches across 20 repositories, this is unsustainable. Teams want:

  • Automatic pipeline creation for new branches
  • Different behaviors per branch type
  • PR status checks in GitHub
  • Automatic cleanup when branches are deleted

The Challenge

Implement a multi-branch pipeline architecture that automatically manages CI/CD for all branches with appropriate deployment targets and integrations.

Wrong Approach

A junior engineer might create separate Jenkinsfiles per branch, manually create jobs for each feature branch, or use a single job with complex conditional logic. These approaches don't scale, create maintenance nightmares, and are error-prone.

Right Approach

A senior engineer implements Multi-branch Pipeline with Organization Folders, uses a single Jenkinsfile with branch-aware logic, integrates with GitHub for automatic discovery and PR status updates, and implements proper environment promotion based on branch patterns.

Step 1: Create Multi-branch Pipeline Job

// Jenkinsfile in repository root
pipeline {
    agent any

    options {
        buildDiscarder(logRotator(
            numToKeepStr: env.BRANCH_NAME == 'main' ? '50' : '10'
        ))
        timestamps()
        timeout(time: 60, unit: 'MINUTES')
    }

    environment {
        APP_NAME = 'my-application'
        REGISTRY = 'registry.company.com'
        // Set based on branch
        DEPLOY_ENV = getBranchEnvironment()
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        script: 'git rev-parse --short HEAD',
                        returnStdout: true
                    ).trim()
                    env.IMAGE_TAG = "${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
                }
            }
        }

        stage('Build') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }

        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit'
                    }
                }
                stage('Lint') {
                    steps {
                        sh 'npm run lint'
                    }
                }
            }
        }

        stage('Security Scan') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    changeRequest()  // PRs
                }
            }
            steps {
                sh 'npm audit --production'
                sh 'trivy fs --exit-code 1 --severity HIGH,CRITICAL .'
            }
        }

        stage('Build Docker Image') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    branch pattern: 'release/*', comparator: 'GLOB'
                }
            }
            steps {
                sh """
                    docker build -t ${REGISTRY}/${APP_NAME}:${IMAGE_TAG} .
                    docker push ${REGISTRY}/${APP_NAME}:${IMAGE_TAG}
                """
            }
        }

        stage('Deploy to Dev') {
            when {
                branch pattern: 'feature/*', comparator: 'GLOB'
            }
            steps {
                deployTo(environment: 'dev', tag: env.IMAGE_TAG)
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                deployTo(environment: 'staging', tag: env.IMAGE_TAG)
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                script {
                    // Require manual approval for production
                    timeout(time: 24, unit: 'HOURS') {
                        input message: 'Deploy to Production?',
                              ok: 'Deploy',
                              submitter: 'release-managers'
                    }
                }
                deployTo(environment: 'production', tag: env.IMAGE_TAG)
            }
        }
    }

    post {
        always {
            junit allowEmptyResults: true, testResults: '**/test-results/*.xml'
        }
        success {
            script {
                if (env.CHANGE_ID) {  // It's a PR
                    pullRequest.comment("Build passed! Image: `${REGISTRY}/${APP_NAME}:${IMAGE_TAG}`")
                }
            }
        }
        failure {
            script {
                if (env.BRANCH_NAME == 'main' || env.BRANCH_NAME == 'develop') {
                    slackSend(
                        channel: '#build-failures',
                        color: 'danger',
                        message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
                    )
                }
            }
        }
    }
}

def getBranchEnvironment() {
    if (env.BRANCH_NAME == 'main') return 'production'
    if (env.BRANCH_NAME == 'develop') return 'staging'
    if (env.BRANCH_NAME?.startsWith('release/')) return 'staging'
    if (env.BRANCH_NAME?.startsWith('feature/')) return 'dev'
    if (env.BRANCH_NAME?.startsWith('hotfix/')) return 'production'
    return 'dev'
}

def deployTo(Map args) {
    def env = args.environment
    def tag = args.tag

    withCredentials([file(credentialsId: "kubeconfig-${env}", variable: 'KUBECONFIG')]) {
        sh """
            kubectl set image deployment/${APP_NAME} \
                app=${REGISTRY}/${APP_NAME}:${tag} \
                -n ${APP_NAME}-${env}

            kubectl rollout status deployment/${APP_NAME} \
                -n ${APP_NAME}-${env} \
                --timeout=300s
        """
    }
}

Step 2: Configure Organization Folder

// JCasC configuration for GitHub Organization scanning
jenkins:
  jobs:
    - script: >
        organizationFolder('my-org') {
          description('All repositories in my-org')
          displayName('My Organization')

          organizations {
            github {
              repoOwner('my-org')
              credentialsId('github-app')
              traits {
                gitHubBranchDiscovery {
                  strategyId(1)  // Exclude branches also filed as PRs
                }
                gitHubPullRequestDiscovery {
                  strategyId(1)  // Merge PR with target branch
                }
                gitHubForkDiscovery {
                  strategyId(1)
                  trust {
                    gitHubTrustPermissions()
                  }
                }
              }
            }
          }

          projectFactories {
            workflowMultiBranchProjectFactory {
              scriptPath('Jenkinsfile')
            }
          }

          buildStrategies {
            buildRegularBranches()
            buildChangeRequests {
              ignoreTargetOnlyChanges(false)
            }
            buildTags {
              atLeastDays('-1')
              atMostDays('7')
            }
          }

          orphanedItemStrategy {
            discardOldItems {
              daysToKeep(7)
              numToKeep(20)
            }
          }

          triggers {
            periodicFolderTrigger {
              interval('1h')
            }
          }
        }

Step 3: GitHub Integration for PR Status

// Enhanced Jenkinsfile with GitHub status updates
pipeline {
    agent any

    options {
        // Report build status to GitHub
        githubProjectProperty(projectUrlStr: 'https://github.com/my-org/my-app')
    }

    stages {
        stage('PR Validation') {
            when {
                changeRequest()
            }
            steps {
                script {
                    // Validate PR requirements
                    def prInfo = pullRequest

                    // Check PR size
                    def changedFiles = prInfo.changedFiles
                    if (changedFiles > 50) {
                        echo "WARNING: Large PR with ${changedFiles} files"
                    }

                    // Check for required labels
                    def labels = prInfo.labels.collect { it.toLowerCase() }
                    if (!labels.any { it in ['bug', 'feature', 'enhancement', 'docs'] }) {
                        error 'PR must have a type label (bug, feature, enhancement, or docs)'
                    }

                    // Add reviewer suggestions based on files changed
                    if (prInfo.files.any { it.path.startsWith('security/') }) {
                        prInfo.addLabel('needs-security-review')
                        prInfo.comment('@security-team - Please review security changes')
                    }
                }
            }
        }

        stage('Build & Test') {
            steps {
                // GitHub sees each stage as a separate check
                script {
                    setBuildStatus('pending', 'Build started')
                }
                sh 'npm ci && npm run build && npm test'
                script {
                    setBuildStatus('success', 'Build passed')
                }
            }
        }

        stage('Integration Tests') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    changeRequest target: 'main'
                    changeRequest target: 'develop'
                }
            }
            steps {
                sh 'npm run test:integration'
            }
        }
    }

    post {
        success {
            script {
                if (env.CHANGE_ID) {
                    // Update PR with success status
                    setBuildStatus('success', 'All checks passed')

                    // Add deployment preview URL for feature branches
                    def previewUrl = "https://${env.CHANGE_ID}.preview.company.com"
                    pullRequest.comment("""
                        Build successful!

                        **Preview URL:** ${previewUrl}
                        **Image Tag:** `${env.IMAGE_TAG}`
                    """)
                }
            }
        }
        failure {
            script {
                if (env.CHANGE_ID) {
                    setBuildStatus('failure', 'Build failed')
                }
            }
        }
    }
}

def setBuildStatus(String state, String description) {
    step([
        $class: 'GitHubCommitStatusSetter',
        reposSource: [$class: 'ManuallyEnteredRepositorySource', url: env.GIT_URL],
        commitShaSource: [$class: 'ManuallyEnteredShaSource', sha: env.GIT_COMMIT],
        contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: 'ci/jenkins'],
        statusResultSource: [$class: 'ConditionalStatusResultSource', results: [
            [$class: 'AnyBuildResult', state: state, message: description]
        ]]
    ])
}

Step 4: Branch-Specific Behaviors

// More complex branch logic
pipeline {
    agent any

    stages {
        stage('Initialize') {
            steps {
                script {
                    // Determine build configuration based on branch
                    env.RUN_E2E = shouldRunE2E()
                    env.DEPLOY_ENABLED = shouldDeploy()
                    env.SONAR_ENABLED = shouldRunSonar()
                }
            }
        }

        stage('E2E Tests') {
            when {
                expression { env.RUN_E2E == 'true' }
            }
            steps {
                sh 'npm run test:e2e'
            }
        }

        stage('SonarQube Analysis') {
            when {
                expression { env.SONAR_ENABLED == 'true' }
            }
            steps {
                withSonarQubeEnv('sonar-server') {
                    sh 'npm run sonar'
                }
                timeout(time: 10, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }
}

def shouldRunE2E() {
    // Run E2E for main branches and PRs targeting them
    if (env.BRANCH_NAME in ['main', 'develop']) return true
    if (env.CHANGE_TARGET in ['main', 'develop']) return true
    return false
}

def shouldDeploy() {
    // Only deploy from specific branches
    def deployBranches = ['main', 'develop', ~/release\/.*/]
    return deployBranches.any { pattern ->
        if (pattern instanceof java.util.regex.Pattern) {
            return env.BRANCH_NAME ==~ pattern
        }
        return env.BRANCH_NAME == pattern
    }
}

def shouldRunSonar() {
    // Run SonarQube on main branches and weekly on features
    if (env.BRANCH_NAME in ['main', 'develop']) return true
    if (env.BRANCH_NAME?.startsWith('feature/')) {
        // Only on scheduled builds, not every push
        return currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause')
    }
    return false
}

Multi-branch Pipeline Matrix

Branch PatternBuildTestDeployApprovalCleanup
mainFullAllProductionRequiredNever
developFullAllStagingAutoNever
release/*FullAllStagingRequiredAfter merge
feature/*QuickUnitDevAuto7 days
hotfix/*FullAllProductionRequiredAfter merge
PR to mainFullAllPreviewN/AAfter close

Practice Question

In a Jenkins Multi-branch Pipeline, what happens when a branch is deleted from the repository?