Created
August 12, 2026 13:09
-
-
Save cccaternberg/1b5c91ba19ef6845c4dc30c8c97e9704 to your computer and use it in GitHub Desktop.
Custom Update Center
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 "=================================================================" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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