In complex CI/CD environments, Jenkinsfiles often grow large and repetitive, especially when dealing with Android AOSP builds and artifact uploads. We end up duplicating shell scripts, credential setup, or deployment logic across multiple projects.
This post shows how to extract and reuse common code in Jenkins using Shared Libraries, focusing on modularity and maintainability, not reusing entire pipelines.
- Organize your repository
Structure your Git repo like this:
jenkins-pipelines/
├── Jenkinsfile
├── vars/
│ ├── buildAosp.groovy
│ └── uploadArtifacts.groovy
vars/
holds reusable Groovy functions. Each file defines a callable step (e.g., buildAosp()
).
- Add shared functions
Create your Groovy functions in vars/
.
vars/buildAosp.groovy
:
def call(Map config = [:]) {
docker.image(config.dockerImage ?: 'aosp-builder')
.inside("-v ${config.ccachePath}:/ccache") {
sshagent([config.sshCredential]) {
sh """#!/bin/bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -p 29418 -H gerrit.maksonlee.com >> ~/.ssh/known_hosts
repo init -u ${config.manifestUrl} -b ${config.branch}
repo sync -c -j1
source build/envsetup.sh
lunch ${config.lunchTarget}
m
"""
}
}
}
vars/uploadArtifacts.groovy
:
def call(Map config = [:]) {
def timestamp = new Date().format("yyyyMMdd.HHmmss", TimeZone.getTimeZone('UTC'))
def fileItegRev = "${timestamp}-${env.BUILD_NUMBER}"
def targetBasePath = "${config.repo}/${config.orgPath}/${config.module}/${config.baseRev}-${config.folderItegRev}"
def specMap = [
files: config.artifacts.collect {
[
pattern: it.pattern,
target: "${targetBasePath}/${config.module}-${config.baseRev}-${fileItegRev}-${it.classifier}.${it.ext}"
]
}
]
echo "Uploading artifacts to: ${targetBasePath}"
rtUpload(
serverId: "artifactory",
spec: groovy.json.JsonOutput.toJson(specMap),
failNoOp: true
)
}
- Register the repo as a Shared Library in Jenkins
- Go to Manage Jenkins → System
- Scroll to Global Trusted Pipeline Libraries
- Click Add, and fill in:
Field | Value |
---|---|
Name | jenkins-pipelines |
Default Version | main |
Load Implicitly | leave unchecked |
Allow Default Override | recommended |
SCM | Git |
Repository URL | Git URL of this repo (e.g. GitHub) |
Credentials | If private, add credentials |
Library Path | (Leave blank) |
Even if your Jenkinsfile is in the same repo, this step is required!
- Use the functions in your Jenkinsfile
Jenkinsfile
:
@Library('jenkins-pipelines') _
pipeline {
agent { label 'ssh-agent-with-docker' }
environment {
CCACHE_PATH = "/home/administrator/.cache/ccache"
}
stages {
stage('Build AOSP') {
steps {
buildAosp(
ccachePath: env.CCACHE_PATH,
dockerImage: 'aosp-builder',
sshCredential: 'gerrit-ssh-maksonlee',
manifestUrl: 'ssh://maksonlee@gerrit.maksonlee.com:29418/platform/manifest',
branch: 'android-15.0.0_r30',
lunchTarget: 'aosp_arm64-trunk_staging-userdebug'
)
}
}
stage('Upload Artifacts') {
steps {
uploadArtifacts(
orgPath: 'com/maksonlee',
module: '1234',
baseRev: '003-vanilla-continuous',
folderItegRev: 'SNAPSHOT',
repo: 'product-snapshots',
artifacts: [
[pattern: 'out/target/product/generic_arm64/system.img', classifier: 'system', ext: 'img'],
[pattern: 'out/target/product/generic_arm64/vbmeta.img', classifier: 'vbmeta', ext: 'img'],
[pattern: 'out/target/product/generic_arm64/ramdisk.img', classifier: 'ramdisk', ext: 'img']
]
)
}
}
}
}
Conclusion: Reuse Code, Not Pipelines
This approach isn’t about reusing entire Jenkinsfiles. It’s about reusing common code, build logic, artifact uploads, credential setup, across all your jobs.
By centralizing logic in vars/
and using @Library(...)
:
- Your Jenkinsfiles become shorter and easier to maintain
- You fix bugs once, and apply them everywhere
- You scale your CI setup cleanly and consistently
If you’re maintaining more than one Jenkins job, this is the cleanest, most maintainable way forward.
Really appreciated the clarity and depth in this piece.
Fantastic resource!
Thank you so much for your kind words! I’m really glad to hear that you found the article clear and helpful. Let me know if there’s anything else you’d like to see covered!