Skip to main content
Jenkins · lesson 7 of 7
Jenkins intermediate Lesson 7 of 7

Jenkins Plugin Pinning and Upgrades

Pin Jenkins plugin versions, resolve dependency conflicts, check security warnings and available updates, and test a core bump before rolling it out.

By SkillByExample · Updated

jenkins plugins upgrades

plugins.txt and jenkins-plugin-cli from the previous lesson pin four plugins by exact version. This lesson is about everything that pin doesn’t cover: the plugins those four depend on, what breaks when a pin outgrows the Jenkins core underneath it, and how to check both before an upgrade reaches a real controller.

1) A pin doesn’t freeze the whole dependency tree

plugins.txt names four plugins:

configuration-as-code:2136.veb_3a_554550b_f
job-dsl:3732.v9a_c49a_61a_313
credentials:1514.vb_01eb_014e7d4
plain-credentials:199.v9f8e1f741799

The controller built from it has 25:

$ docker exec jcasc-demo jenkins-plugin-cli --list
...
Resulting plugin list:
antisamy-markup-formatter 173.v680e3a_b_69ff3
asm-api 9.11.1-226.vff833a_e4c58e
bootstrap5-api 5.3.8-1048.va_c299057e35c
bouncycastle-api 2.30.1.86.2-310.v4655f6742752
branch-api 2.1303.v9f3b_95dc329d
...
workflow-support 1028.vc22910142650

Done

The other 21 are transitive dependencies, each resolved to whatever the update center’s current metadata says is compatible — jenkins-plugin-cli’s --latest flag defaults to true, meaning “use the newest version of every dependency, even for a pinned top-level plugin.” Building the same plugins.txt again in six months can install different dependency versions than it did today, with the four pins unchanged. Pass --latest false to pin the whole tree instead of just the top level — section 6 shows why that trade-off isn’t free either.

A pinned plugin also brings its own minimum version of a shared dependency, and that minimum can be lower than what’s already pinned at the top level. Pinning job-dsl:3732.v9a_c49a_61a_313 alongside configuration-as-code:2136.veb_3a_554550b_f hits exactly that, because job-dsl only requires an older configuration-as-code as its floor:

$ docker run --rm -v "$(pwd):/work" --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --jenkins-version 2.580.1 --plugin-file /work/plugins-conflict-demo.txt --verbose
...
job-dsl depends on:
commons-lang3-api 3.18.0-98.v3a_674c06072d
commons-text-api 1.13.1-176.v74d88f22034b_
configuration-as-code 1963.v24e046127a_3f
...
Skipping dependency configuration-as-code:1963.v24e046127a_3f and its sub-dependencies, because there is a higher version defined on the top level - configuration-as-code:2136.veb_3a_554550b_f

jenkins-plugin-cli resolves this itself — the higher of the two versions wins, and only one copy of configuration-as-code gets installed, satisfying job-dsl’s floor with room to spare. Section 5 shows the case where that kind of resolution isn’t possible: not a minimum a higher pin can satisfy, but a plugin that needs a Jenkins core newer than the one actually running.

2) Listing what’s actually on a controller

Two more ways to answer “what’s installed” beyond jenkins-plugin-cli --list at build time (section 1): jenkins-cli.jar against a live instance, and the REST API.

$ docker run --rm -v "$(pwd):/work" eclipse-temurin:21 java -jar /work/jenkins-cli.jar -s http://host.docker.internal:8080/ -auth admin:s3cr3t-admin-pw list-plugins | grep -E "^(configuration-as-code|credentials|job-dsl|plain-credentials) "
configuration-as-code     Configuration as Code Plugin          2136.veb_3a_554550b_f
credentials               Credentials Plugin                    1514.vb_01eb_014e7d4
job-dsl                   Job DSL                               3732.v9a_c49a_61a_313
plain-credentials         Plain Credentials Plugin              199.v9f8e1f741799
$ curl -s -u admin:s3cr3t-admin-pw "http://localhost:8080/pluginManager/api/json?pretty=true&tree=plugins%5BshortName,version%5D" | head -11
{
  "_class" : "hudson.LocalPluginManager",
  "plugins" : [
    {
      "shortName" : "antisamy-markup-formatter",
      "version" : "173.v680e3a_b_69ff3"
    },
    {
      "shortName" : "asm-api",
      "version" : "9.11.1-226.vff833a_e4c58e"
    },

jenkins-cli.jar needs a Java 21 runtime — it’s compiled for the same class file version as the controller, and JDKs older than 21 can’t load it, so the command above runs it inside a throwaway eclipse-temurin:21 container instead of directly, reaching the published port through host.docker.internal.

3) Checking for known vulnerabilities before you pin

jenkins-plugin-cli carries its own security-warning feed, independent of the update check in section 4.

$ docker run --rm -v "$(pwd):/work" --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-security-warnings --no-download --plugin-file /work/plugins.txt
Done

Nothing printed beyond Done — none of the four pinned plugins currently carry a known advisory. --no-download keeps this a read-only query instead of actually writing plugin files, but it isn’t what determines whether a warning prints: that depends on whether the queried plugin:version is already sitting on disk wherever you run this. A controller that already has the exact pinned versions installed has nothing left to resolve, so the check silently reports nothing either way, warnings or not — run it against a clean pull of the base image, not the controller you already built (Practice 3 shows this exact gap with the vulnerable versions from section 6). --view-all-security-warnings ignores plugins.txt and lists every plugin in the entire update center with an open advisory, regardless of whether you use it:

$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-all-security-warnings | wc -l
1513
$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-all-security-warnings | head -5
AdaptivePlugin - Arbitrary code execution vulnerability
AnchorChain - Stored XSS vulnerability
ApicaLoadtest - Tokens stored and displayed in plain text
BlazeMeterJenkinsPlugin - Missing permission check allows enumerating credentials IDs
DotCi - RCE vulnerability

Run the scoped form against your own plugins.txt before every upgrade; the unscoped form is a catalog, not a check.

4) Checking for available updates

--available-updates compares named plugin:version pins against the current update center, not just whatever’s installed:

$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --jenkins-version 2.580.1 --available-updates --plugins job-dsl:1.90 configuration-as-code:1700.v09b_045ee9299
Available updates:
job-dsl (1.90) has an available update: 3732.v9a_c49a_61a_313
configuration-as-code (1700.v09b_045ee9299) has an available update: 2136.veb_3a_554550b_f

Run it against the plugins this lesson already pins to the current latest, and there’s nothing to report:

$ docker run --rm -v "$(pwd):/work" --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --available-updates --plugin-file /work/plugins.txt
No available updates

5) When a pin is too new for the core

Jenkins core itself has a required-version floor per plugin release. Try installing the current configuration-as-code pin onto an older core — jenkins/jenkins:2.480-jdk21 instead of the 2.580.1 this topic otherwise uses:

# Dockerfile.oldcore
FROM jenkins/jenkins:2.480-jdk21

COPY plugins-cas-only.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
$ docker build -f Dockerfile.oldcore -t jenkins-oldcore-fail . > build.log 2>&1; echo "exit=$?"
exit=1
$ grep -E "requires a greater version|ERROR: process" build.log
#7 14.05 font-awesome-api (7.2.0-965.ve3840b_696418) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 json-api (20260814-226.v20f9685d642c) requires a greater version of Jenkins (2.492.1) than 2.480,
#7 14.05 asm-api (9.11.1-226.vff833a_e4c58e) requires a greater version of Jenkins (2.492.3) than 2.480,
#7 14.05 configuration-as-code (2136.veb_3a_554550b_f) requires a greater version of Jenkins (2.541.1) than 2.480,
#7 14.05 workflow-support (1015.v785e5a_b_b_8b_22) requires a greater version of Jenkins (2.504.3) than 2.480,
#7 14.05 bootstrap5-api (5.3.8-895.v4d0d8e47fea_d) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 prism-api (1.30.0-717.vb_f8360844b_53) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 plugin-util-api (6.1192.v30fe6e2837ff) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 scm-api (728.vc30dcf7a_0df5) requires a greater version of Jenkins (2.504.3) than 2.480,
#7 14.05 workflow-api (1384.vdc05a_48f535f) requires a greater version of Jenkins (2.504.1) than 2.480
#7 ERROR: process "/bin/sh -c jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt" did not complete successfully: exit code: 1
...
$ grep -oE "[a-zA-Z0-9-]+ \([^)]+\) requires a greater version" build.log | awk '{print $1}' | sort -u | wc -l
10

The build fails before the image even has a chance to run (exit=1) — ten distinct plugins, by that last count, all name the exact core version they need, every one of them higher than 2.480. This is the same category of problem as last lesson’s numExecutor typo (section 4 there): JCasC and jenkins-plugin-cli would both rather fail your build than let you ship a controller that can’t start.

6) Downgrading the plugin instead of the core isn’t automatically safer

The other direction — keep the old core, pin an older, compatible plugin release — installs cleanly, but isn’t free:

# Dockerfile.oldcore-pinned
FROM jenkins/jenkins:2.480-jdk21

COPY plugins-cas-old.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --latest false --plugin-file /usr/share/jenkins/ref/plugins.txt

plugins-cas-old.txt pins configuration-as-code:1810.v9b_c30a_249a_4c, a release contemporaneous with core 2.480:

$ docker build -f Dockerfile.oldcore-pinned -t jenkins-oldcore-pinned . > build2.log 2>&1; echo "exit=$?"
exit=0
$ grep -E "Security warnings|SECURITY-" build2.log | head -3
#7 7.449 Security warnings:
#7 7.449 structs (325.vcb_307d2a_2782): SECURITY-3371 Exposure of secrets through system log https://www.jenkins.io/security/advisory/2024-06-26/#SECURITY-3371
#7 7.450 script-security (1251.vfe552ed55f8d): SECURITY-3341 Multiple sandbox bypass vulnerabilities https://www.jenkins.io/security/advisory/2024-05-02/#SECURITY-3341
$ grep -E "^#7 1[01]\.|^#7 DONE" build2.log
#7 11.47 Done
#7 DONE 11.6s
$ grep -c "SECURITY-" build2.log
15

The build succeeds (exit=0), jenkins-plugin-cli installs it, and the warnings print unprompted during the install — fifteen by that grep -c count — against the old structs and script-security releases that configuration-as-code:1810 pulled in with --latest false. Compare that to section 3’s Done-with-nothing on the current pins against the current core: the plugin downgrade traded a loud build failure for a quiet set of known vulnerabilities. Bumping the core and keeping the plugin current — the previous lesson’s image — is the version of this upgrade that passes both checks.

Practice

These ran against the skillbyexample-jenkins-jcasc:good image, jcasc-demo container, jenkins-cli.jar, and plugins.txt from this lesson and the previous one (Jenkins 2.580.1).

  1. Diff plugins.txt (4 lines) against jenkins-plugin-cli --list’s “Resulting plugin list” (25 lines) run inside the image. Which plugins appear in the second list but not the first, and why are they there if nothing in plugins.txt names them?

    Solution
    $ docker exec jcasc-demo jenkins-plugin-cli --list 2>&1 | sed -n '/Resulting plugin list/,$p' | tail -n +2 | grep -v '^Done$' | grep -v '^$' | awk '{print $1}' > installed.txt
    $ awk -F: '{print $1}' plugins.txt > pinned.txt
    $ comm -23 <(sort installed.txt) <(sort pinned.txt)
    antisamy-markup-formatter
    asm-api
    bootstrap5-api
    bouncycastle-api
    branch-api
    caffeine-api
    cloudbees-folder
    commons-lang3-api
    commons-text-api
    font-awesome-api
    ionicons-api
    json-api
    plugin-util-api
    prism-api
    scm-api
    script-security
    snakeyaml-api
    structs
    workflow-api
    workflow-step-api
    workflow-support

    21 plugins, none named in plugins.txt. Each is a dependency of one of the four pins — configuration-as-code alone needs caffeine-api, commons-lang3-api, commons-text-api, json-api, prism-api, snakeyaml-api, and the entire workflow-* family (workflow-api, workflow-step-api, workflow-support) just to load, and job-dsl separately pulls in branch-api and cloudbees-folder. None of them are optional; jenkins-plugin-cli resolved and installed them automatically from each pinned plugin’s own manifest.

  2. Run --view-all-security-warnings and intersect it by name only against all 25 installed plugins from task 1. Does matching by name agree with section 3’s --view-security-warnings --plugin-file plugins.txt result?

    Solution
    $ docker exec jcasc-demo jenkins-plugin-cli --view-all-security-warnings | awk -F' - ' '{print $1}' | sort -u > all-warned.txt
    $ comm -12 <(sort installed.txt) all-warned.txt
    cloudbees-folder
    configuration-as-code
    credentials
    job-dsl
    plain-credentials
    script-security
    structs
    workflow-support

    No, and that’s the trap this task is pointing at: 8 of the 25 installed plugin names have an advisory somewhere in their history, which looks alarming next to section 3’s clean Done. But --view-all-security-warnings lists a plugin the moment any past release of it had a CVE — it says nothing about which release. Checking the exact installed name:version pairs against a clean pull of the base image (not the controller that already has them installed — section 3 explained why that matters) still reports nothing:

    $ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-security-warnings --no-download --plugins structs:362.va_b_695ef4fdf9 script-security:1436.v1082791135e4 cloudbees-folder:6.1106.v3a_d9a_6d2465e workflow-support:1028.vc22910142650 configuration-as-code:2136.veb_3a_554550b_f credentials:1514.vb_01eb_014e7d4 job-dsl:3732.v9a_c49a_61a_313 plain-credentials:199.v9f8e1f741799
    Done

    The 8 name matches are all plugins whose older releases carried a CVE that the versions actually installed here have long since fixed — the same structs and script-security releases that section 6’s downgrade resurrects. Checking by name alone overstates risk; the scoped, versioned check against a clean image is the one that answers “is what I actually have installed vulnerable.”

  3. Run --view-security-warnings for exactly structs:325.vcb_307d2a_2782 and script-security:1251.vfe552ed55f8d — the two versions Dockerfile.oldcore-pinned resolved in section 6 — against a fresh pull of jenkins/jenkins:2.480-jdk21, the same way task 2 queried the current versions against a fresh lts-jdk21.

    Solution
    $ docker run --rm --entrypoint="" jenkins/jenkins:2.480-jdk21 jenkins-plugin-cli --view-security-warnings --no-download --plugins structs:325.vcb_307d2a_2782 script-security:1251.vfe552ed55f8d
    Security warnings:
    structs (325.vcb_307d2a_2782): SECURITY-3371 Exposure of secrets through system log https://www.jenkins.io/security/advisory/2024-06-26/#SECURITY-3371
    script-security (1251.vfe552ed55f8d): SECURITY-3341 Multiple sandbox bypass vulnerabilities https://www.jenkins.io/security/advisory/2024-05-02/#SECURITY-3341
    ...
    Done

    This reproduces the same warnings Dockerfile.oldcore-pinned’s build already printed — confirming they come from those specific old releases, not from the newer job-dsl/configuration-as-code pins this topic otherwise uses. Running the exact same query against jenkins-oldcore-pinned — the image already built with these plugins on disk — instead of a fresh 2.480-jdk21 pull:

    $ docker run --rm --entrypoint="" jenkins-oldcore-pinned jenkins-plugin-cli --view-security-warnings --no-download --plugins structs:325.vcb_307d2a_2782 script-security:1251.vfe552ed55f8d
    Done

    Done, no warnings at all — the same already-satisfied short-circuit from section 3, now hiding a real finding instead of confirming a clean one. Always run this check against a clean image, never the controller you already built from it.

  4. Pin configuration-as-code to a version between the two already tested — 1971.vf9280461ea_89 (requiredCore: 2.504.1) — against the 2.480 core. Does it succeed, fail outright, or fail with a different message than section 5’s?

    Solution
    $ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --jenkins-version 2.480 --no-download --plugins configuration-as-code:1971.vf9280461ea_89
    Multiple plugin prerequisites not met:
    ...
    configuration-as-code (1971.vf9280461ea_89) requires a greater version of Jenkins (2.504.1) than 2.480,
    ...

    Same failure mode as section 5, just a smaller required-core gap (2.504.1 vs. 2.480, instead of 2.541.1). jenkins-plugin-cli doesn’t have a “close enough” threshold — any requiredCore above the running core fails the same way, whether the gap is one release or twenty.

Frequently Asked Questions

Does a plugins.txt entry like job-dsl:3732.v9a_c49a_61a_313 pin that plugin's dependencies too?
No, not by default. jenkins-plugin-cli resolves each dependency's version from the update center's current metadata unless you pass --latest false, so two Jenkins instances built from the same plugins.txt months apart can end up with different transitive plugin versions.
How do I see exactly what's installed on a running Jenkins controller?
Run jenkins-plugin-cli --list inside the image to see what the build resolved, java -jar jenkins-cli.jar list-plugins against a live controller for the same list from the running instance, or GET /pluginManager/api/json?tree=plugins[shortName,version] over the REST API for the same data without the CLI jar.
What happens if a pinned plugin version requires a newer Jenkins core than the one I'm running?
jenkins-plugin-cli refuses to install it: it prints every plugin whose requiredCore exceeds the running core version and exits with a non-zero status, so a Dockerfile RUN step building the image fails the build instead of producing a controller that can't boot.
Is downgrading a plugin to match an older Jenkins core a safe fix?
Not on its own. An older plugin release resolves its own dependencies to older releases too, and jenkins-plugin-cli prints a Security warnings block unprompted during install whenever one of those resolved versions carries a known CVE. Pinning forward (bump the core, keep the current plugin) is safer than pinning backward.
What's a safe order for rolling out a plugin or core upgrade?
Pin the new version in plugins.txt, build a throwaway image and confirm it boots and jenkins-plugin-cli --view-security-warnings reports nothing new, then bump the real controller's Dockerfile and redeploy — never edit a running controller's plugin set by hand through the UI and hope it matches what's committed.