Skip to content

Instantly share code, notes, and snippets.

@cccaternberg
Created August 12, 2026 13:09
Show Gist options
  • Select an option

  • Save cccaternberg/1b5c91ba19ef6845c4dc30c8c97e9704 to your computer and use it in GitHub Desktop.

Select an option

Save cccaternberg/1b5c91ba19ef6845c4dc30c8c97e9704 to your computer and use it in GitHub Desktop.
Custom Update Center
import jenkins.model.Jenkins
import hudson.model.UpdateSite
import hudson.model.UpdateCenter
import hudson.PluginWrapper
// --- CONFIGURATION ---
def targetPluginName = "matrix-auth" // Replace with your root plugin ID
def dynamicLoad = true // Attempt to load without restarting if possible
def includeOptionalPlugins = true // Set to true to include optional dependencies in PLUGIN_SPECS
def instance = Jenkins.get()
def pm = instance.getPluginManager()
def uc = instance.getUpdateCenter()
println "================================================================="
println "Promoting and Installing '${targetPluginName}'"
println "Configuration: includeOptionalPlugins = ${includeOptionalPlugins}"
println "================================================================="
// Step 1: Force refresh of all configured Update Sites
println "[1/3] Force refreshing Update Center metadata..."
uc.updateAllSites()
if (uc.sites.isEmpty()) {
println "❌ ERROR: No Update Center sites are registered in Jenkins!"
return
}
println "✓ Active Update Center site(s): ${uc.sites*.id.join(', ')}\n"
// Helper: Extract dependencies handling both standard Jenkins & CloudBees CAP formats
def getPluginDependencies(UpdateSite.Plugin ucPlugin) {
def result = []
def deps = ucPlugin.dependencies
if (deps) {
if (deps instanceof Map) {
// CloudBees CAP format (Map<String, String>)
deps.keySet().each { String depId ->
result.add([name: depId, optional: false])
}
} else {
// Standard Jenkins format
deps.each { dep ->
boolean isOpt = false
try {
isOpt = dep.optional
} catch (Exception ignored) {
isOpt = false
}
String depName = dep.metaClass.hasProperty(dep, 'name') ? dep.name : dep.toString()
result.add([name: depName, optional: isOpt])
}
}
}
// Safely check for separate optionalDependencies property if available
try {
def optDeps = ucPlugin.optionalDependencies
if (optDeps) {
if (optDeps instanceof Map) {
optDeps.keySet().each { String depId ->
if (!result.any { it.name == depId }) {
result.add([name: depId, optional: true])
}
}
} else if (optDeps instanceof Collection) {
optDeps.each { dep ->
String depName = dep.metaClass.hasProperty(dep, 'name') ? dep.name : dep.toString()
if (!result.any { it.name == depName }) {
result.add([name: depName, optional: true])
}
}
}
}
} catch (Exception ignored) {}
return result
}
// Track plugins for deployment and total required tree list
def pluginsToInstall = [] as Set // Queue for actual deployment (mandatory only)
def allSpecPlugins = [] as Set // Set for PLUGIN_SPECS report
def visitedForQueue = [] as Set
def buildDependencyTree(String pluginId, UpdateCenter updateCenter, int depth, boolean isOptional, Set queue, Set allSpecs, Set visitedForQueue, Set currentPath, boolean allowOptionalInSpecs) {
def node = [
id: pluginId,
depth: depth,
isDirect: (depth == 1),
isTransitive: (depth > 1),
isOptional: isOptional,
children: []
]
PluginWrapper installed = Jenkins.get().pluginManager.getPlugin(pluginId)
UpdateSite.Plugin ucPlugin = updateCenter.getPlugin(pluginId)
if (!ucPlugin) {
node.status = "NOT_FOUND"
node.details = "⚠️ Not found in active Update Centers"
return node
}
node.ucVersion = ucPlugin.version
node.installedVersion = installed ? installed.version : null
if (installed == null) {
node.status = "INSTALL"
node.details = "↳ Flagged for installation (v${ucPlugin.version})"
} else if (installed.version != ucPlugin.version) {
node.status = "UPGRADE"
node.details = "↳ Flagged for upgrade (${installed.version} -> ${ucPlugin.version})"
} else {
node.status = "MATCHED"
node.details = "✓ Up to date (${installed.version})"
}
// Circular dependency safeguard
if (currentPath.contains(pluginId)) {
node.details += " (circular reference detected)"
return node
}
currentPath.add(pluginId)
// Collect into PLUGIN_SPECS set based on configuration flag
if ((!isOptional || allowOptionalInSpecs) && node.status != "NOT_FOUND") {
allSpecs.add(pluginId)
}
// Process children recursively
def childDeps = getPluginDependencies(ucPlugin)
childDeps.each { depInfo ->
boolean childIsOptional = isOptional || depInfo.optional
if (depInfo.optional && !allowOptionalInSpecs) {
node.children.add([
id: depInfo.name,
depth: depth + 1,
isDirect: (depth + 1 == 1),
isTransitive: (depth + 1 > 1),
isOptional: true,
status: "SKIPPED_OPTIONAL",
details: "Skipped (Optional)",
children: []
])
} else {
def childNode = buildDependencyTree(
depInfo.name, updateCenter, depth + 1, childIsOptional,
queue, allSpecs, visitedForQueue, new HashSet(currentPath), allowOptionalInSpecs
)
node.children.add(childNode)
}
}
// Add plugin to deployment queue ONLY if mandatory (not optional) and needs install/upgrade
if (!isOptional && (node.status == "INSTALL" || node.status == "UPGRADE")) {
if (!visitedForQueue.contains(pluginId)) {
visitedForQueue.add(pluginId)
queue.add(ucPlugin)
}
}
return node
}
// Helper: Print formatted ASCII Dependency Graph
def printTree(Map node, String prefix = "", boolean isLast = true, boolean isRoot = true) {
if (isRoot) {
println "${node.id} (v${node.ucVersion ?: 'unknown'}) [Root Plugin]"
} else {
String connector = isLast ? "└── " : "├── "
String typeLabel = node.isDirect ? "Direct" : "Transitive"
String reqLabel = node.isOptional ? "Optional" : "Required"
println "${prefix}${connector}${node.id} [${typeLabel} | ${reqLabel}] -> ${node.details}"
}
def children = node.children
for (int i = 0; i < children.size(); i++) {
boolean lastChild = (i == children.size() - 1)
String childPrefix = isRoot ? "" : prefix + (isLast ? " " : "")
printTree(children[i], childPrefix, lastChild, false)
}
}
// Step 2: Build tree & output report
println "[2/3] Calculating dependency graph and resolving dependencies..."
println "-----------------------------------------------------------------"
def treeRoot = buildDependencyTree(
targetPluginName, uc, 0, false,
pluginsToInstall, allSpecPlugins, visitedForQueue, [] as Set, includeOptionalPlugins
)
printTree(treeRoot)
println "-----------------------------------------------------------------\n"
if (pluginsToInstall.isEmpty()) {
println "✅ Root plugin '${targetPluginName}' and all mandatory dependencies are already up to date."
} else {
// Step 3: Deployment
println "[3/3] Deploying ${pluginsToInstall.size()} required plugin(s)..."
pluginsToInstall.each { UpdateSite.Plugin plugin ->
println "Downloading and installing: ${plugin.name} (${plugin.version})..."
def future = plugin.deploy(dynamicLoad)
Throwable error = future.get().getError()
if (error != null) {
println "❌ ERROR installing ${plugin.name}: ${error.message}"
} else {
println "✅ Successfully deployed ${plugin.name}"
}
}
}
// Step 4: Summary Report for Copy/Pasting
println "\n================================================================="
println "Copy/Paste PLUGIN_SPECS Map Summary (includeOptionalPlugins = ${includeOptionalPlugins}):"
println "=================================================================\n"
def specPlugins = allSpecPlugins.sort()
if (specPlugins) {
int maxKeyLength = specPlugins.collect { "\"${it}\"".length() }.max()
println " def PLUGIN_SPECS = ["
specPlugins.each { pluginId ->
String formattedKey = "\"${pluginId}\"".padRight(maxKeyLength)
println " ${formattedKey} : BaseData.LATEST_VERSION_STRING,"
}
println " ]"
} else {
println " // No valid plugins found in Update Center."
}
println "\n================================================================="
println "Process Complete."
println "================================================================="
/*
* Run in Operations Center > Manage Jenkins > Script Console.
*
* Downloads (caches) and promotes a list of plugin versions on one or more
* Update Center items so connected controllers can see/install them from
* their Plugin Manager.
*
* Customize PLUGIN_SPECS and UPDATE_CENTER_NAME below, then run.
*/
import com.cloudbees.plugins.updatecenter.UpdateCenter
import com.cloudbees.plugins.updatecenter.PluginData
import com.cloudbees.plugins.updatecenter.BaseData
import jenkins.model.Jenkins
// ---------------------------------------------------------------------------
// CONFIG
// ---------------------------------------------------------------------------
// Plugin id -> version to promote. Use BaseData.LATEST_VERSION_STRING
// ("latest") to always track the newest available version instead of a
// pinned one.
// In the sample list below, we have the job-dsl as a 3rd party plugin we want to install.
// All other plugins are direct- or indirect (transitive)-dependencies of job-dsl.
def PLUGIN_SPECS = [
// "beer": "1.2.3", // pin to a specific version
"branch-api" : BaseData.LATEST_VERSION_STRING,
"caffeine-api" : BaseData.LATEST_VERSION_STRING,
"cloudbees-folder" : BaseData.LATEST_VERSION_STRING,
"commons-lang3-api" : BaseData.LATEST_VERSION_STRING,
"commons-text-api" : BaseData.LATEST_VERSION_STRING,
"ionicons-api" : BaseData.LATEST_VERSION_STRING,
"job-dsl" : BaseData.LATEST_VERSION_STRING,
"scm-api" : BaseData.LATEST_VERSION_STRING,
"script-security" : BaseData.LATEST_VERSION_STRING,
"structs" : BaseData.LATEST_VERSION_STRING,
]
// Full item name/path of the Update Center to operate on, e.g. "operations-center-uc"
// or "myfolder/operations-center-uc". Leave null to apply to every
// UpdateCenter item found in this OC instance.
def UPDATE_CENTER_NAME = null
// If true, only prints what it would do - no downloads, no promotions, no save().
def DRY_RUN = false
// How long to wait for an async plugin download to land in the cache before
// giving up on promoting it.
def DOWNLOAD_TIMEOUT_SECONDS = 180
def POLL_INTERVAL_MS = 2000
// ---------------------------------------------------------------------------
// SCRIPT
// ---------------------------------------------------------------------------
def jenkins = Jenkins.get()
Collection<UpdateCenter> updateCenters
if (UPDATE_CENTER_NAME) {
def item = jenkins.getItemByFullName(UPDATE_CENTER_NAME, UpdateCenter.class)
if (item == null) {
println "ERROR: no UpdateCenter item found at '${UPDATE_CENTER_NAME}'"
return
}
updateCenters = [item]
} else {
updateCenters = jenkins.getAllItems(UpdateCenter.class)
}
if (updateCenters.isEmpty()) {
println "ERROR: no UpdateCenter items found on this Operations Center."
return
}
def waitForVersionCached = { PluginData pd, String pluginId, def vn ->
def deadline = System.currentTimeMillis() + (DOWNLOAD_TIMEOUT_SECONDS * 1000L)
while (System.currentTimeMillis() < deadline) {
if (pd.getVersions()?.containsKey(vn)) {
return true
}
sleep(POLL_INTERVAL_MS)
}
return false
}
updateCenters.each { UpdateCenter uc ->
println "== Update Center: ${uc.getFullName()} =="
uc.checkPermission(UpdateCenter.STORE)
uc.checkPermission(UpdateCenter.PROMOTE)
boolean modified = false
PLUGIN_SPECS.each { pluginId, requestedVersion ->
def pd = uc.getPlugin(pluginId)
if (pd == null) {
println " [${pluginId}] SKIP - not found in this update center's data"
return
}
// Resolve "latest" (or a pinned version string) to the actual key object
// used by pd.getVersions()/getUpdates() - avoid constructing a new
// VersionNumber ourselves since the concrete class/constructor used by
// this update center's data model isn't part of this plugin's own code.
def known = ((pd.getVersions()?.keySet() ?: []) + (pd.getUpdates()?.keySet() ?: []))
boolean trackLatest = (requestedVersion == BaseData.LATEST_VERSION_STRING)
def vn
if (trackLatest) {
vn = known.max()
if (vn == null) {
println " [${pluginId}] SKIP - no known versions to resolve LATEST against"
return
}
} else {
vn = known.find { it.toString() == requestedVersion }
if (vn == null) {
println " [${pluginId}] SKIP - version ${requestedVersion} not found upstream or locally"
return
}
}
// --- STORE: download the plugin bits into the update center's local cache ---
def alreadyCached = pd.getVersions()?.containsKey(vn)
if (!alreadyCached) {
def entry = pd.getUpdates()?.get(vn)
if (entry == null) {
println " [${pluginId}] SKIP - version ${vn} not found upstream to download"
return
}
if (DRY_RUN) {
println " [${pluginId}] DRY-RUN would store version ${vn}"
} else {
println " [${pluginId}] storing version ${vn} ..."
uc.checkPermission(UpdateCenter.STORE)
uc.downloadPlugin(entry)
if (!waitForVersionCached(pd, pluginId, vn)) {
println " [${pluginId}] ERROR - timed out waiting for ${vn} to be stored, skipping promotion"
return
}
println " [${pluginId}] version ${vn} stored"
modified = true
}
} else {
println " [${pluginId}] version ${vn} already stored"
}
// --- PROMOTE: make the stored version visible to connected controllers ---
def promoteValue = trackLatest ? BaseData.LATEST_VERSION_STRING : vn.toString()
if (DRY_RUN) {
println " [${pluginId}] DRY-RUN would set promoted version to ${promoteValue}"
} else {
pd.setPromotedVersion(promoteValue)
println " [${pluginId}] promoted version set to ${promoteValue}"
modified = true
}
}
if (modified && !DRY_RUN) {
uc.save()
println " saved ${uc.getFullName()}"
}
}
println "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment