Jenkins Branch-Specific Pipeline Libraries
Problem
Our team recently developed a new product using a Node.js stack with a streamlined single-branch development strategy. The workflow follows a clear pattern: developers create feature branches for new work, then merge completed code to the main branch through pull requests. To automate versioning, we’ve implemented a GitHub probot that handles PR processing, automatically incrementing the version in package.json by comparing it against the main branch’s current version.
Our CI/CD infrastructure relies on Jenkins and Groovy pipelines. Initially, the main branch pipeline worked flawlessly, handling Docker image builds, Helm chart creation, deployment to the development landscape, and security scanning. However, as we approached our release milestone, we encountered architectural challenges when introducing a dedicated release branch. This new branch required different pipeline logic—specifically, the need to move/copy Docker images from the development registry to a production registry. This created a fundamental conflict: merging code from main to release would overwrite the release branch’s specialized pipeline configuration with the main branch’s pipeline settings.
Technical Solution
Pipeline Architecture Analysis
Our current pipeline architecture follows different patterns for each branch:
Main:
Release:
The core issue arises when merging code from main to release—the Jenkinsfile references get overwritten, causing the release branch to incorrectly use jenkins-share-lib@main instead of its intended jenkins-share-lib@release.
Dynamic Library Loading Solution
Through investigation, we discovered Jenkins supports two approaches for loading shared libraries:
Static Annotation Approach:
@Library(['oss-share-lib', 'app-share-lib@main']) _Dynamic Loading Approach:
@Library(['oss-share-lib']) _library("app-share-lib@${BRANCH_NAME}")The dynamic approach solves our branch-specific library loading problem by referencing the current branch name at runtime.
after testing on the environment, it turns out we missed the pull request branch, so we need to add condition to check if the branch is pull request or not, hence improved code like
if (env.CHANGE_ID && env.CHANGE_TARGET) { // Pull Request build library("icumit-pipeline-lib@dssa-${CHANGE_TARGET}")
} else if (env.BRANCH_NAME && !env.CHANGE_ID) { // Regular branch build library("icumit-pipeline-lib@dssa-${BRANCH_NAME}")
}We need to first check if it is a PR, since a PR has all three environment variables (env.CHANGE_ID, env.CHANGE_TARGET, and env.BRANCH_NAME). If we check env.BRANCH_NAME first, we will encounter an error because we don’t have a library with the name of the PR’s env.BRANCH_NAME.
Configuration Management Strategy
Additionally, we faced configuration conflicts with .pipeline/config.yaml being overwritten during branch merges. The solution involves moving branch-specific configurations into the shared library’s resources directory:
share-library/├── var/│ ├── stage-init.groovy│ ├── stage-build.groovy│ └── ...├── resources/│ ├── config-main.yaml│ └── config-release.yaml├── README.mdThis approach allows us to dynamically load the appropriate configuration based on the current branch:
def pipelineConfig = libraryResource('config-' + env.BRANCH_NAME + '.yaml')writeFile file: '.pipeline/config.yaml', text: pipelineConfigBy implementing both dynamic library loading and centralized configuration management, we ensure each branch maintains its appropriate pipeline behavior while preventing configuration conflicts during code merges.
Back to Blog