Skip to content

Publish a Jenkins controller image on merge to main - #10

Merged
enk21 merged 1 commit into
mainfrom
claude/publish-image
Aug 20, 2026
Merged

enk21 merged 1 commit into
mainfrom
claude/publish-image

Conversation

@jacobecox

Copy link
Copy Markdown
Collaborator

The plugin ships as a .hpi with manual install instructions today, so there is nothing a Control Plane marketplace template can actually deploy. This adds the image and the workflow that publishes it.

File What it is
docker/Dockerfile Multi-stage — maven:3.9-eclipse-temurin-21 builds the .hpi, jenkins/jenkins:lts-jdk21 bakes it in. The JDK is pinned because the plugin's baseline is 2.462.3; the build must not depend on whichever JDK a runner happens to have.
docker/plugins.txt JCasC, credentials, pipeline, git, matrix-auth and the usual operability set.
docker/casc.yaml Admin user, authorization, numExecutors: 0 on the built-in node per the README's own guidance.
.github/workflows/publish-image.yml On merge to main: newest vX.Y.Z tag → patch bump → build → push :{version} and :latest → push the tag.
.dockerignore The context is the repo root, which carries target/ and a 2 MB .hpi under docs/.

Image: ghcr.io/controlplane-com/cpln-jenkins. The first merge publishes 2.0.0 (seeded from the pom), and each merge after bumps the patch. A README- or docs-only merge publishes nothing. workflow_dispatch takes an explicit version for a minor or major bump.

The one change to look at: Cloud.java

Everything else here is build tooling. This is plugin source, so it is your call whether it ships in this PR.

JCasC could not configure this cloud at all. The descriptor carries no @Symbol, and JCasC refuses a jenkins.clouds entry it cannot name — both cpln and the fully-qualified io.jenkins.plugins.cpln.Cloud were rejected with No hudson.slaves.Cloud implementation found. Asking the running controller confirmed why:

class=io.jenkins.plugins.cpln.Cloud  display='Control Plane'  @Symbol=null

The clearest evidence is JCasC's own export. Given a cloud instance already in memory, it wrote:

clouds:
  - ? ''
    : agentImage: "jenkins/inbound-agent:latest"
      gvc: "probe-gvc"

An empty key — so the configuration does not round-trip in either direction. @Symbol("cpln") fixes it. After the fix, a mounted casc file produced a cloud with every field intact including the Secret apiKey:

cloud count: 1
  class      : io.jenkins.plugins.cpln.Cloud
  name       : cpln
  org/gvc    : probe-org / probe-gvc
  apiKey set : true
numExecutors on built-in node: 0

The annotation is additive: it changes no existing behaviour and does not affect config.xml serialisation, so existing installs are unaffected. org.jenkinsci.Symbol resolves through the parent POM — no new dependency.

Three defects that only running it would have found

  • jenkins-plugin-cli failed with Unable to move json-api to the plugin directory. A COPY created /usr/share/jenkins/ref/plugins root-owned before the CLI — which runs as jenkins — could write to it. Plugin resolution now runs first.
  • --latest false was actively harmful. It resolves each plugin to the oldest version its dependencies permit, and the build log listed seven advisories in the resolved set, including a script-security sandbox bypass and an OS command injection in git-client. Dropping the flag clears all seven.
  • Two of my own casc keys abort boot. excludeClientIPFromCrumb is not an attribute JCasC accepts on DefaultCrumbIssuer, and globalJobDslSecurityConfiguration needs the job-dsl plugin, which is not installed. Either one stops Jenkins outright.

Verified

Behaviour, on a running container: anonymous 403, admin 200, wrong password 401, plugin active at 2.0.0, and the controller answering on container loopback — which is what makes cpln port-forward a usable path for a privately deployed controller in the template that follows.

Version logic, executed against real git tags rather than reviewed: seeds 2.0.0 from the pom when no tag exists, v2.0.9 → 2.0.10 and v10.0.0 → 10.0.1 (neither of which a lexicographic sort gets right), and it rejects both a malformed and an already-tagged explicit version. The git tag | head -1 pipe was also removed — under set -o pipefail a SIGPIPE there can abort the step.

Not addressed here

GitHub reports 5 existing Dependabot vulnerabilities on main (2 high, 3 moderate). Those predate this branch and I have not touched them.

🤖 Generated with Claude Code

The plugin currently ships as a .hpi with manual install instructions, so
there is nothing a Control Plane template can deploy. This adds the image and
the workflow that publishes it.

  docker/Dockerfile   multi-stage: maven:3.9-eclipse-temurin-21 builds the
                      .hpi, jenkins/jenkins:lts-jdk21 bakes it in. The JDK is
                      pinned because the plugin's baseline is 2.462.3 and the
                      build must not depend on whatever JDK a runner has.
  docker/plugins.txt  JCasC, credentials, pipeline, git, matrix-auth and the
                      usual operability set.
  docker/casc.yaml    admin user, authorization, numExecutors 0 on the
                      built-in node per the README's guidance.
  publish-image.yml   on merge to main: newest vX.Y.Z tag -> patch bump ->
                      build -> push :version and :latest -> push the tag.
                      Seeds from the pom version when no tag exists.
                      workflow_dispatch takes an explicit version.

The Cloud.java change is the part to look at, because it is not a CI file.

JCasC could not configure this cloud at all. The descriptor carries no
@symbol, and JCasC rejects a jenkins.clouds entry it cannot name -- both
`cpln` and the fully-qualified class name were refused with "No
hudson.slaves.Cloud implementation found". Its own export is the clearest
evidence: given a cloud instance in memory it wrote

  clouds:
    - ? ''
      : agentImage: "jenkins/inbound-agent:latest"

an EMPTY key, so the configuration does not round-trip in either direction.
@symbol("cpln") fixes it, and after the fix a mounted casc file produced a
cloud with every field intact, the Secret apiKey included.

The annotation is additive: it changes no existing behaviour and does not
affect config.xml serialisation, so existing installs are unaffected.

Verified by running it rather than by reading it, which is how three real
defects surfaced:

  - jenkins-plugin-cli failed with "Unable to move json-api to the plugin
    directory" because a COPY created the plugins directory root-owned before
    the CLI (running as jenkins) could. Plugin resolution now runs first.
  - --latest false resolved plugins to the oldest versions their dependencies
    allowed, which the build log flagged with seven advisories including a
    script-security sandbox bypass and an OS command injection in git-client.
    Dropping the flag clears all seven.
  - excludeClientIPFromCrumb is not an attribute JCasC accepts on
    DefaultCrumbIssuer, and globalJobDslSecurityConfiguration needs a plugin
    that is not installed. Either one aborts boot outright.

Also verified: anonymous 403, admin 200, bad password 401, the plugin active
at 2.0.0, and the controller answering on container loopback -- which is what
makes `cpln port-forward` usable for a privately deployed controller later.

The version logic was executed against real git tags rather than reviewed:
seeds 2.0.0 from the pom with no tags, v2.0.9 -> 2.0.10, v10.0.0 -> 10.0.1
(neither of which a lexicographic sort gets right), and it rejects a
malformed or already-tagged explicit version.
@enk21
enk21 merged commit b5ff79e into main Aug 20, 2026
2 checks passed
@enk21
enk21 deleted the claude/publish-image branch August 20, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants