This repository provides a modular and reusable Jenkins Shared Library to manage pipeline logic using Groovy and a class-based approach.
BasePipe: Abstract base pipeline class that defines the structure and behavior for stage management.DoguPipe: A concrete implementation tailored for Dogu builds.StageDefinition: Data class for holding individual stage metadata.
Located in BasePipe.groovy, this abstract class provides:
BasePipe(script)Initializes the pipeline with a Jenkins script context.
Adds a new stage to the pipeline.
agentLabel: Optional agent label to execute the stage.parallel: If true, this stage will be run in parallel with other stages on the same agent.
insertStageAfter(String afterName, String newName, Closure block, String agentLabel = defaultAgent, boolean parallel = false)
Inserts a stage after a given one.
overrideStage(String name, Closure newBlock, String newAgentLabel = null, Boolean newParallel = null)
Overrides an existing stage's logic or properties.
Removes a stage by name.
Reorders a stage after another stage.
Assigns or reassigns an agent to a stage.
Assigns multiple stages to different agents using a map.
Groups all added stages by their agent and:
- Executes sequential and parallel stages on the appropriate agents.
- Handles empty scripts or stage lists gracefully with debug logs.
Found in StageDefinition.groovy, used to represent each stage:
class StageDefinition {
String name
Closure block
String agentLabel
boolean parallel
}All stages added via StageGroup.stage(...) are implicitly executed in PipelineMode.FULL.
This means:
- Any stage registered with
stage(...)will always run when the pipeline is executed inFULLmode - Even if you specify a different
PipelineMode,FULLis automatically added
group.stage("Build", PipelineMode.INTEGRATION) { ... }If you don't want that behavior you can use:
group.raw_stage("Build", PipelineMode.RELEASE) { ... }DoguPipe.groovy is a concrete subclass of BasePipe tailored to build and release Dogu containers.
DoguPipe(script, Map config)Registers all built-in Dogu Stage Modules into agent-scoped StageGroups.
Instead of hardcoding stages, addDefaultStages() composes the pipeline by loading
independent stage modules that each contribute their own stages:
| Module | Responsibility |
|---|---|
StaticStages |
Linting, shellcheck, markdown, Sonar, unit tests |
IntegrationStages |
Provisioning, setup, build, Trivy, integration tests |
MultinodeStages |
Multi-VM / multi-cluster integration tests |
ReleaseStages |
Triggers the gitflow via pipeline when Integration Mode is RELEASE |
Internally this looks like:
addStageGroup(agentStatic) { new StaticStages().register(this, it) }
addStageGroup(agentVagrant) { new IntegrationStages().register(this, it) }
addStageGroup(agentVagrant) { new ReleaseStages().register(this, it) }
addStageGroup(agentMultinode){ new MultinodeStages().register(this, it) }DoguConfig is the compiled, runtime-ready configuration for a Dogu pipeline.
It takes the raw Jenkinsfile config map, applies defaults and normalization,
instantiates all required build systems (Git, EcoSystem, Docker, Vagrant, etc.),
and injects pipeline-specific helpers into them so that the pipeline runs in a
fully initialized, self-contained build environment.
In addition, DoguPipe transparently exposes all properties of DoguConfig
via Groovy’s propertyMissing mechanism.
This means:
pipe.ecoSystem
pipe.git
pipe.cypressImage
pipe.runIntegrationTestspipe.config.ecoSystem
pipe.config.git
pipe.config.cypressImage
pipe.config.runIntegrationTestsbut without the caller needing to know or care that a DoguConfig object exists.
This makes DoguPipe behave like a live view of the pipeline configuration while still keeping all configuration, defaults, and runtime wiring isolated inside DoguConfig.
executeShellTests(): Runs Bats-based shell tests.runCypress(): Runs Cypress integration tests.setBuildProperties(List customParams): Configures Jenkins parameters.checkout_updatemakefiles(boolean updateSubmodules): Checks out code and updates Makefiles with the latest version.
@Library('pipebuildlib') _
import com.cloudogu.sos.pipebuildlib.DoguPipe
def pipe = new DoguPipe(this, [
doguName: 'my-dogu',
runIntegrationTests: true,
shellScripts: 'scripts/*.sh'
])
pipe.setBuildProperties()
pipe.addDefaultStages()
pipe.run()- Default agent label is
"sos"if not specified.
Library versioning & class resolution
The versions of
pipe-build-lib,ces-build-lib, anddogu-build-libare not defined in the Jenkinsfile.
They are resolved by Jenkins via Manage Jenkins → Global Pipeline Libraries, which pins each library to a specific Git ref (branch, tag, or commit).Jenkins loads each shared library into its own classloader.
Because of this, classes from another library are not visible unless they are imported.That means this will crash:
// ❌ Will fail if ces-build-lib is not imported new Docker(this)And this is the correct way:
@Library(['pipe-build-lib', 'ces-build-lib', 'dogu-build-lib']) _ // full path required! new com.cloudogu.ces.cesbuildlib.Docker(this)Even though the class has a fully-qualified name, Jenkins will not load it unless the library is explicitly imported. Fully-qualified names avoid ambiguity, but they do not bypass Jenkins’ library isolation.
Stages in PipeBuildLib are not executed individually --- they are grouped into StageGroups, and each group is bound to a specific Jenkins agent label.
addStageGroup(agentStatic) { group -> ... }
addStageGroup(agentVagrant) { group -> ... }
addStageGroup(agentMultinode) { group -> ... }Each StageGroup represents one execution lane on one Jenkins
agent.
Scenario What happens
Two groups with Run in parallel on different machines
different agent
labels
Two groups with the Run sequentially on the same machine same agent label
Some parts of a Dogu build: - Must share a workspace (same agent) - Must not run in parallel (race conditions) - Need more powerful machines (Vagrant, Docker, GCP)
Other parts: - Are safe to run independently - Should run in parallel to save time
StageGroup encodes that infrastructure intent directly into the
pipeline.
Think of a StageGroup as:
"A queue of stages that must run on the same machine."
Different queues → different machines → parallel execution.
class MyCustomPipe extends BasePipe {
MyCustomPipe(script) {
super(script)
addStage("Checkout", {
script.echo "Checking out source..."
})
addStage("Build", {
script.echo "Building..."
}, agentLabel: "builder", parallel: false)
addStage("Test", {
script.echo "Running tests..."
}, parallel: true)
}
}@Library('your-shared-library') _
def pipe = new MyCustomPipe(this)
pipe.run()pipe.overrideStage("Build", {
script.echo "Overridden build step"
})
pipe.removeStage("Test")
pipe.insertStageAfter("Checkout", "Lint", {
script.echo "Linting code..."
})@Library([
'pipe-build-lib',
'ces-build-lib',
'dogu-build-lib'
]) _
// Create instance of DoguPipe with configuration parameters
def pipe = new com.cloudogu.sos.pipebuildlib.DoguPipe(this, [
doguName : "jenkins",
// Optional behavior settings
shellScripts : "resources/startup.sh resources/upgrade-notification.sh resources/pre-upgrade.sh",
dependencies : ["cas", "usermgt"],
checkMarkdown : true,
runIntegrationTests: true,
cypressImage : "cypress/included:13.16.1"
])
// Set default or custom build parameters (can also pass a list to override defaults)
pipe.setBuildProperties()
// add default stages based on config map
pipe.addDefaultStages()
// Insert a custom post-integration stage directly after the "Integration Tests" stage
pipe.insertStageAfter("Integration Tests", "Test: Change Global Admin Group", {
def eco = pipe.ecoSystem
// Change the global admin group and restart jenkins
eco.changeGlobalAdminGroup("newAdminGroup")
eco.restartDogu("jenkins")
eco.waitForDogu("jenkins")
// Run Cypress tests again without video/screenshot recording
eco.runCypressIntegrationTests([
cypressImage : "cypress/included:13.16.1",
enableVideo : false,
enableScreenshots: false
])
})
// Run the pipeline – this will execute all previously added stages
pipe.run()@Library([
'pipe-build-lib',
'ces-build-lib',
'dogu-build-lib'
]) _
def pipe = new com.cloudogu.sos.pipebuildlib.DoguPipe(this, [
doguName : "portainer",
shellScripts : "./resources/startup.sh",
checkMarkdown : true,
doBatsTests : true,
runIntegrationTests: true,
doSonarTests : true
])
pipe.setBuildProperties()
pipe.addDefaultStages()
// Get ecosystem object from pipe-build-lib initiated by the pipe-build-lib
com.cloudogu.ces.dogubuildlib.EcoSystem eco = pipe.ecoSystem
pipe.insertStageAfter("Bats Tests","build & test carp") {
def ctx = pipe.script
new com.cloudogu.ces.cesbuildlib.Docker(ctx)
.image('golang:1.23')
.mountJenkinsUser()
.inside('-e ENVIRONMENT=ci')
{
ctx.sh 'make carp-clean'
ctx.sh 'make build-carp'
ctx.sh 'make carp-unit-test'
}
}
pipe.overrideStage("Integration tests")
{
eco.runCypressIntegrationTests([ enableVideo : params.EnableVideoRecording,
enableScreenshots: params.EnableScreenshotRecording,
cypressImage: pipe.cypressImage])
// Test special case with restricted access
eco.vagrant.ssh "sudo etcdctl set /config/portainer/user_access_restricted true"
eco.restartDogu(pipe.doguName)
eco.runCypressIntegrationTests([ enableVideo : params.EnableVideoRecording,
enableScreenshots: params.EnableScreenshotRecording,
cypressImage: pipe.cypressImage,
additionalCypressArgs:
"--config '{\"excludeSpecPattern\": [\"cypress/e2e/dogu_integration_test_lib/*\"]}'"])
}
pipe.run()#!groovy
@Library([
'pipe-build-lib',
'ces-build-lib',
'dogu-build-lib'
]) _
def pipe = new com.cloudogu.sos.pipebuildlib.DoguPipe(this, [
doguName : 'redmine',
shellScripts : ['''
resources/startup.sh
resources/post-upgrade.sh
resources/pre-upgrade.sh
resources/util.sh
resources/upgrade-notification.sh
resources/default-config.sh
resources/update-password-policy.sh
resources/util.sh
resources/delete-plugin.sh
'''],
dependencies : ['cas', 'usermgt', 'postgresql'],
doBatsTests : true,
runIntegrationTests: true,
cypressImage : "cypress/included:13.14.2"
])
com.cloudogu.ces.dogubuildlib.EcoSystem ecoSystem = pipe.ecoSystem
pipe.setBuildProperties()
pipe.addDefaultStages()
pipe.overrideStage('Setup') {
ecoSystem.loginBackend('cesmarvin-setup')
ecoSystem.setup([additionalDependencies: ['official/postgresql']])
}
pipe.run()pipe.overrideStage('Setup') {
ecoSystem.loginBackend('cesmarvin-setup')
def settingsJson = '{"custom_menu_entries":[{"name":"Handbuch \uD83D\uDD17","url":"https://docs.cloudogu.com/de/usermanual/easyredmine/er12/1_administrators_checklist/","icon":"icon-help"}]}'
def escapedSettings = settingsJson.replaceAll('"', '\\\\"')
ecoSystem.setup([
additionalDependencies: ['official/mysql', 'official/redis'],
// setting custom menu entry for docs
registryConfig: """
"easyredmine": {
"logging": {
"root": "ERROR"
},
"default_data": {
"usertype_settings": "${escapedSettings}"
}
},
"_global": {
"password-policy": {
"must_contain_capital_letter": "false",
"must_contain_lower_case_letter": "true",
"must_contain_digit": "true",
"must_contain_special_character": "false",
"min_length": "1"
}
}
"""
])
}