Full Code of jenkinsci/docker for AI

master 2167b62427c1 cached
88 files
229.7 KB
70.5k tokens
1 requests
Download .txt
Showing preview only (250K chars total). Download the full file or copy to clipboard to get everything.
Repository: jenkinsci/docker
Branch: master
Commit: 2167b62427c1
Files: 88
Total size: 229.7 KB

Directory structure:
gitextract_gxy0y0_6/

├── .ci/
│   └── publish.sh
├── .git-blame-ignore-revs
├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   ├── release-drafter.yml
│   └── workflows/
│       ├── release-drafter.yml
│       └── updatecli.yaml
├── .gitignore
├── .gitmodules
├── .hadolint.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── HACKING.adoc
├── Jenkinsfile
├── LICENSE.txt
├── Makefile
├── README.md
├── SECURITY.md
├── alpine/
│   └── hotspot/
│       └── Dockerfile
├── debian/
│   └── Dockerfile
├── docker-bake.hcl
├── jdk-download-url.sh
├── jdk-download.sh
├── jenkins-plugin-cli.ps1
├── jenkins-plugin-cli.sh
├── jenkins-support
├── jenkins-support.psm1
├── jenkins.io-2026.key
├── jenkins.ps1
├── jenkins.sh
├── make.ps1
├── rhel/
│   └── Dockerfile
├── tests/
│   ├── bake.bats
│   ├── functions/
│   │   ├── .ssh/
│   │   │   └── config
│   │   ├── Dockerfile
│   │   └── Dockerfile-windows
│   ├── functions.Tests.ps1
│   ├── functions.bats
│   ├── golden/
│   │   ├── expected_env_vars_except_hostname.txt
│   │   ├── expected_platforms.txt
│   │   ├── expected_tags.txt
│   │   ├── expected_tags_latest_lts.txt
│   │   └── expected_tags_latest_weekly.txt
│   ├── jenkinsfile.bats
│   ├── plugins-cli/
│   │   ├── Dockerfile
│   │   ├── Dockerfile-windows
│   │   ├── custom-war/
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile-windows
│   │   │   └── WEB-INF/
│   │   │       └── plugins/
│   │   │           └── my-happy-plugin.hpi
│   │   ├── java-opts/
│   │   │   └── Dockerfile
│   │   ├── no-war/
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile-windows
│   │   │   └── plugins.txt
│   │   ├── pluginsfile/
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile-windows
│   │   │   └── plugins.txt
│   │   ├── ref/
│   │   │   ├── Dockerfile
│   │   │   └── Dockerfile-windows
│   │   └── update/
│   │       ├── Dockerfile
│   │       └── Dockerfile-windows
│   ├── plugins-cli.Tests.ps1
│   ├── plugins-cli.bats
│   ├── runtime.Tests.ps1
│   ├── runtime.bats
│   ├── test_helpers.bash
│   ├── test_helpers.psm1
│   ├── update-golden-file.sh
│   └── upgrade-plugins/
│       ├── Dockerfile
│       └── Dockerfile-windows
├── tini_pub.gpg
├── tools/
│   ├── hadolint
│   └── shellcheck
├── updatecli/
│   ├── scripts/
│   │   └── rhel-latest-tag.sh
│   ├── updatecli.d/
│   │   ├── alpine.yaml
│   │   ├── debian.yaml
│   │   ├── git-lfs.yaml
│   │   ├── hadolint.yaml
│   │   ├── jdk17.yaml
│   │   ├── jdk21.yaml
│   │   ├── jdk25.yaml
│   │   ├── jenkins-version-simulated-lts.yaml
│   │   ├── jenkins-version.yaml
│   │   ├── plugin-installation-manager-tool.yaml
│   │   ├── rhel.yaml
│   │   └── shellcheck.yaml
│   └── values.github-action.yaml
└── windows/
    └── windowsservercore/
        └── hotspot/
            └── Dockerfile

================================================
FILE CONTENTS
================================================

================================================
FILE: .ci/publish.sh
================================================
#!/bin/bash -eu

# Publish any versions of the docker image not yet pushed to ${JENKINS_REPO}
# Arguments:
#   -n dry run, do not build or publish images
#   -d debug

: "${JENKINS_VERSION:?Variable \$JENKINS_VERSION not set or empty.}"

set -eu -o pipefail

: "${DOCKERHUB_ORGANISATION:=jenkins}"
: "${DOCKERHUB_REPO:=jenkins}"
: "${BAKE_TARGET:=linux}"

export JENKINS_REPO="${DOCKERHUB_ORGANISATION}/${DOCKERHUB_REPO}"

function sort-versions() {
    if [ "$(uname)" == 'Darwin' ]; then
        gsort --version-sort
    else
        sort --version-sort
    fi
}

# Process arguments
dry_run=false
debug=false

while [[ $# -gt 0 ]]; do
    key="$1"
    case "${key}" in
        -n)
        dry_run=true
        ;;
        -d)
        debug=true
        ;;
        *)
        echo "ERROR: Unknown option: ${key}"
        exit 1
        ;;
    esac
    shift
done


if [[ "${debug}" = true ]]; then
    echo "Debug mode enabled"
    set -x
fi

if [[ "${dry_run}" = true ]]; then
    echo "Dry run, will not publish images"
fi

# Retrieve all the Jenkins versions from Artifactory
all_jenkins_versions="$(curl --disable --fail --silent --show-error --location \
        https://repo.jenkins-ci.org/releases/org/jenkins-ci/main/jenkins-war/maven-metadata.xml \
    | grep '<version>.*</version>')"

latest_lts_version="$(echo "${all_jenkins_versions}" | grep -E -o '[0-9]\.[0-9]+\.[0-9]' | sort-versions | tail -n1)"
latest_weekly_version="$(echo "${all_jenkins_versions}" | grep -E -o '[0-9]\.[0-9]+' | sort-versions | tail -n 1)"

if [[ "${JENKINS_VERSION}" == "${latest_weekly_version}" ]]
then
    LATEST_WEEKLY="true"
else
    LATEST_WEEKLY="false"
fi

if [[ "${JENKINS_VERSION}" == "${latest_lts_version}" ]]
then
    LATEST_LTS="true"
else
    LATEST_LTS="false"
fi

build_opts=("--pull")
metadata_suffix="publish"
if test "${dry_run}" == "true"; then
    build_opts+=("--set=*.output=type=cacheonly")
    metadata_suffix="dry-run"
else
    build_opts+=("--push")
fi

# Save build result metadata
mkdir -p target
BUILD_METADATA_PATH="target/build-result-metadata_${BAKE_TARGET}_${metadata_suffix}.json"
build_opts+=("--metadata-file=${BUILD_METADATA_PATH}")

COMMIT_SHA=$(git rev-parse HEAD)
export COMMIT_SHA JENKINS_VERSION LATEST_WEEKLY LATEST_LTS BUILD_METADATA_PATH

cat <<EOF
Using the following settings:
* JENKINS_REPO: ${JENKINS_REPO}
* JENKINS_VERSION: ${JENKINS_VERSION}
* COMMIT_SHA: ${COMMIT_SHA}
* LATEST_WEEKLY: ${LATEST_WEEKLY}
* LATEST_LTS: ${LATEST_LTS}
* BUILD_METADATA_PATH: ${BUILD_METADATA_PATH}
* BAKE_TARGET: ${BAKE_TARGET}
* BAKE OPTIONS:
$(printf '  %s\n' "${build_opts[@]}")
EOF

echo '* RESOLVED BAKE CONFIG:'
docker buildx bake --file docker-bake.hcl --progress=quiet --print "${BAKE_TARGET}"

if [[ "${CI:-false}" == "false" ]]; then
  read -rp "Confirm? [y/N] " answer

  if [[ ! "${answer}" =~ ^[Yy]$ ]]; then
      exit 0
  fi
fi

docker buildx bake --file docker-bake.hcl "${build_opts[@]}" "${BAKE_TARGET}"


================================================
FILE: .git-blame-ignore-revs
================================================
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-and-understanding-files#ignore-commits-in-the-blame-view
# Format Jenkinsfile: https://github.com/jenkinsci/docker/pull/2003
9ade9569a658c0ed27107915d7597fcc98d7a577


================================================
FILE: .github/CODEOWNERS
================================================
* @jenkinsci/team-docker-packaging
*/debian @ksalerno99
*/rhel @ksalerno99


================================================
FILE: .github/FUNDING.yml
================================================
community_bridge: jenkins
custom: ["https://jenkins.io/donate/#why-donate"]


================================================
FILE: .github/dependabot.yml
================================================
# Per https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:

# GitHub actions
- package-ecosystem: "github-actions"
  labels:
    - "skip-changelog"
  target-branch: master
  directory: "/"
  schedule:
    # Check for updates to GitHub Actions every week
    interval: "weekly"


================================================
FILE: .github/release-drafter.yml
================================================
# https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc

_extends: github:jenkinsci/.github:/.github/release-drafter.yml
# We are using 2-digit weekly versioning here
version-template: $MAJOR.$MINOR
tag-template: $NEXT_MINOR_VERSION
name-template: $NEXT_MINOR_VERSION
template: |
  <!-- Optional: add a release summary here -->
  ## 📦 Jenkins Core updates

  * Update to Jenkins $NEXT_MINOR_VERSION ([changelog](https://www.jenkins.io/changelog/$NEXT_MINOR_VERSION))

  $CHANGES


================================================
FILE: .github/workflows/release-drafter.yml
================================================
# Note: additional setup is required, see https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc

name: Release Drafter

on:
  push:
    branches:
      - "master"
  workflow_dispatch:

jobs:
  update_release_draft:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'jenkinsci'
    steps:
      # https://github.com/release-drafter/release-drafter/issues/871#issuecomment-3686135188
      - name: Wait for 15 seconds to ensure GraphQL consistency
        shell: bash
        run: sleep 15s
      # Drafts your next Release notes as Pull Requests are merged into "master"
      - name: Release Drafter
        uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/updatecli.yaml
================================================
name: updatecli
on:
  # Allow to be run manually
  workflow_dispatch:
  schedule:
    # Run once per week (to avoid alert fatigue)
    - cron: '0 2 * * 1' # Every Monday at 2am UTC
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
jobs:
  updatecli:
    runs-on: ubuntu-latest
    if: github.repository_owner == 'jenkinsci'
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Install Updatecli in the runner
        uses: updatecli/updatecli-action@v2.100.0

      - name: Run Updatecli in Dry Run mode
        run: updatecli diff --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml
        env:
          UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Run Updatecli in Apply mode
        if: github.ref == 'refs/heads/master'
        run: updatecli apply --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml
        env:
          UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
*.tmp
bats/
target/
tests/functions/init.groovy.d/
tests/functions/java_cp/
tests/functions/copy_reference_file.log
tests/**/work-*/
manifest-tool
multiarch/qemu-*
multiarch/Dockerfile-*
/docker.iml
work-pester-jenkins-windows/
/.idea/

/**/windows/**/jenkins.ps1
/**/windows/**/jenkins-plugin-cli.ps1
/**/windows/**/jenkins-support.psm1

build-windows_*.yaml

tests/**/Dockerfile\.*


================================================
FILE: .gitmodules
================================================
[submodule "tests/test_helper/bats-support"]
	path = tests/test_helper/bats-support
	url = https://github.com/ztombol/bats-support
[submodule "tests/test_helper/bats-assert"]
	path = tests/test_helper/bats-assert
	url = https://github.com/ztombol/bats-assert


================================================
FILE: .hadolint.yml
================================================
# Hadolint configuration file
---
# configure ignore rules
# see https://github.com/hadolint/hadolint#rules for a list of available rules.

ignored:
  # Exclusions in this section have been triaged and determined to be false
  # positives.
  - DL3008  # Pin versions in apt-get install
  - DL3018  # Pin versions in apk add
  - DL3033  # Specify version with yum install -y <package>-<version>
  - DL3041  # Specify version with dnf install -y <package>-<version>

  # Here lies technical debt. Exclusions in this section have not yet been
  # triaged. When working on on this section, pick an exclusion to triage, then:
  # - If it is a false positive, add a "# hadolint ignore=<rule>" suppression,
  #   then remove the exclusion from this section.
  # - If it is not a false positive, fix the bug, then remove the exclusion from
  #   this section.
  - DL3006  # Always tag the version of an image explicitly


================================================
FILE: CHANGELOG.md
================================================
Changelog
=========

| See [GitHub releases](https://github.com/jenkinsci/docker/releases) |
| --- |

These release notes represent changes in the controller image content or packaging, but not in the bundled WAR files.
Please refer to the [weekly changelog](https://jenkins.io/changelog/) and [LTS changelog](https://jenkins.io/changelog-stable/) for WAR file changelogs.

## Version scheme

The repository follows the scheme of Jenkins, 2-digit for [Weekly releases](https://jenkins.io/download/weekly/) and 3-digit for [LTS releases](https://jenkins.io/download/lts/).


================================================
FILE: CONTRIBUTING.md
================================================
# Issues and Contributing

Please note that only issues related to this Docker image will be addressed here.

* If you have Docker related issues, please ask in the [Docker user mailing list](https://groups.google.com/forum/#!forum/docker-user).
* If you have Jenkins related issues, please ask in the [Jenkins mailing lists](https://jenkins-ci.org/content/mailing-lists).
* If you are not sure, then this is probably not the place to create an issue and you should use any of the previously mentioned mailing lists.

If after going through the previous checklist you still think you should create an issue here please provide:

* Docker commands that you execute
* Actual result
* Expected outcome
* Have you tried a non-dockerized Jenkins and get the expected outcome?
* Output of `docker version`
* Other relevant information

If you are interested to provide fixes by yourself, you might want to check out the [dedicated documentation](HACKING.adoc).


================================================
FILE: HACKING.adoc
================================================
= Hacking documentation

This document explains how to develop on this repository.

== Requirements

* A Bourne-Again-Shell compatible prompt (bash)
* GNU `make` 3.80+
* Docker with https://github.com/docker/buildx[BuildX] capability
** Docker 20.10+ is recommended as it is usually packaged with Buildx
** Docker 19.03+ is required
** BuildX v0.5.1+ is needed (manual installation of the plugin can be done if you don't have it: https://github.com/docker/buildx)
* https://git-scm.com/[git] 1.6+ (git 2+ is recommended)
* https://stedolan.github.io/jq/[jq] 1.6+
* https://curl.se/[curl] 7+

We recommend https://www.gnu.org/software/parallel/[GNU Parallel] for parallel test execution, but it is not required.

// In case the link breaks, and the bug hasn't been fixed yet:
// On Apple Silicon in native arm64 containers, older versions of libssl in
// debian:buster and ubuntu:20.04 will segfault when connected to some TLS
// servers, for example curl https://dl.yarnpkg.com. The bug is fixed in newer versions
// of libssl in debian:bookworm, ubuntu:21.04 and fedora:35.

Tests currently do not work on Mac M1 due to a link:https://docs.docker.com/docker-for-mac/release-notes/#known-issues[known issue] in 'Docker Desktop 3.4.0'.

== Building

=== Linux

[source,bash]
--
## build all linux platforms
make build

## only build a specific linux image
make build-debian_jdk17 # or build-alpine_jdk17 build-debian_slim_jdk17 build-debian_jdk17 ...

--

== Testing

=== Linux

Tests for Linux images are written using https://github.com/bats-core/bats-core[bats] under the `tests/` directory.

Tests pre-requisites are automatically managed by the `make prepare-test` target (dependency of any `make test*` target)  which:

- Ensures that the `bats` command is installed in the `bats/bin/` directory (along with all the bats project in `./bats/`)
- Ensures that the additional bats helper are installed as git sub-modules in `./tests/test_helper/`

For efficiency, the tests are executed in parallel.

[IMPORTANT]
Due to the parallel execution, each test should be self-contained
and not depend on another test, even inside a given test harness.

Please note that:

- You can disable the parallel execution by setting the environment variable `DISABLE_PARALLEL_TESTS` to the value `true`
- Parallel execution is disabled if the commands `docker` or (GNU) `parallel` are not installed.

You can restrict the execution to only a subset of test harness files. By setting the environment variable `TEST_SUITES`
to the path of the bats test harness file to execute alone.

[source,bash]
--
## Run tests for all linux platforms
make test

## Run tests for a specific linux platform
make test-debian_jdk17 # or test-alpine_jdk17 test-debian_slim_jdk17 test-debian_jdk17 ...

## Run tests for Alpine Linux JDK17 platform in sequential mode
DISABLE_PARALLEL_TESTS=true make test-alpine_jdk17

## Only run the test suite `functions.bats` for the Debian JDK21 platform
TEST_SUITES=./tests/functions.bats make test-debian_jdk21
--

You can also pass extra parameters to `bats` by setting `BATS_FLAGS`.

Example:
[source,bash]
--
## Run tests except those with a "test-type:golden-file" `bats` tag
make test BATS_FLAGS="--filter-tags '\!test-type:golden-file'"

## Add an extra parameter
make test BATS_FLAGS="--filter-tags '\!test-type:golden-file' --verbose-run"
--

See https://bats-core.readthedocs.io/en/stable/usage.html for the list of `bats` options.

=== Golden files

A golden file (sometimes called a snapshot) is a file that contains the expected output of a program or function.
Tests compare the current output of the code against this "golden" reference to detect regressions or unintended changes.
They are treated as contract artifacts, not test fixtures.

Golden files may be updated only when:

* Output behavior is intentionally changed
* A bug fix corrects previously incorrect output
* A new test case is added

Golden updates must be reviewed like code.

If your work implies golden file changes, those changes must be committed:
* In the same commit as the behavior change
* With a commit message explaining why output changed

Golden updates must never be automatic.

==== How to update a golden file

* Reproduce output manually
* Inspect output
* Update the golden file explicitly
* Run tests

To update a golden file, you can use the dedicated ./tests/update-golden-file.sh helper script.

Example if there are new LTS tags:
[source,bash]
--
./tests/update-golden-file.sh expected_tags_latest_lts make tags LATEST_LTS=true
--

Then ensure corresponding tests are passing with:
[source,bash]
--
bats ./tests/tags.bats
--

== Multiarch support

The buildx tool is used to build our multiarch images, this relies on either QEMU for emulating the architecture being built, or a remote builder configured for the required platform(s).

Planned supported architectures:

* amd64
* arm64
* s390x

== Debugging

In order to debug the controller, use the `-e DEBUG=true -p 5005:5005` when starting the container.
Jenkins will be suspended on the startup in such case,
and then it will be possible to attach a debugger from IDE to it.

== Test images publication

You can test the script used for publication in dry-run by setting an existing Jenkins Core version and by passing the `-n` option:

[source,bash]
--
$ export JENKINS_VERSION=2.528.3
$ ./.ci/publish.sh -n
Dry run, will not publish images
Using the following settings:
* JENKINS_REPO: jenkins/jenkins
* JENKINS_VERSION: 2.528.3
* COMMIT_SHA: 1c72a9383191562eb3c44838aeeadad0839c2c92
* LATEST_WEEKLY: false
* LATEST_LTS: true
* BUILD_METADATA_PATH: target/build-result-metadata_linux_dry-run.json
[+] Building 104.6s (59/73)
<...snip...>
--

Note that you can set `BAKE_TARGET` to test the publication of a single target instead of the default "linux" multiarch (heavy) build:

[source,bash]
--
$ export BAKE_TARGET=debian_jdk25
$ ./.ci/publish.sh -n
Using the following settings:
* JENKINS_REPO: jenkins/jenkins
* JENKINS_VERSION: 2.528.3
* COMMIT_SHA: aaf4e7faf887b7ac4879c3bf540ede48220cca9f
* LATEST_WEEKLY: false
* LATEST_LTS: true
* BUILD_METADATA_PATH: target/build-result-metadata_debian_jdk25_dry-run.json
* BAKE TARGET: debian_jdk25
* BUILDX OPTIONS:
  --pull
  --set=*.output=type=cacheonly
  --metadata-file=target/build-result-metadata_debian_jdk25_dry-run.json

* RESOLVED BAKE CONFIG:
{
  "group": {
    "default": {
      "targets": [
        "debian_jdk25"
      ]
    }
  },
  "target": {
    "debian_jdk25": {
      "context": ".",
      "dockerfile": "debian/Dockerfile",
      "args": {
        "COMMIT_SHA": "aaf4e7faf887b7ac4879c3bf540ede48220cca9f",
        "DEBIAN_RELEASE_LINE": "trixie",
        "DEBIAN_VARIANT": "",
        "DEBIAN_VERSION": "20251117",
        "JAVA_VERSION": "25.0.1_8",
        "JENKINS_VERSION": "2.528.3",
        "PLUGIN_CLI_VERSION": "2.14.0",
        "WAR_URL": "https://get.jenkins.io/war-stable/2.528.3/jenkins.war"
      },
      "tags": [
        "docker.io/jenkins/jenkins:2.528.3-jdk25",
        "docker.io/jenkins/jenkins:lts-jdk25",
        "docker.io/jenkins/jenkins:2.528.3-lts-jdk25"
      ],
      "platforms": [
        "linux/amd64",
        "linux/arm64",
        "linux/s390x",
        "linux/ppc64le"
      ]
    }
  }
}
[+] Building 104.6s (59/73)
...
--

You can also pass the `-d` option (debug) to see traces from the script.

=== Using an overridden target repository on Docker Hub

Create a new dedicated target repository in your Docker Hub account, and use it like follows (note the absence of `-d` option):

[source,bash]
--
$ export DOCKERHUB_ORGANISATION=jenkins4eval
$ export DOCKERHUB_REPO=test-jenkins
# The log below will help confirm this override was taken in account:
$ ./.ci/publish.sh
Using the following settings:
* JENKINS_REPO: jenkins4eval/test-jenkins
* JENKINS_VERSION: 2.528.3
* WAR_SHA: bfa31f1e3aacebb5bce3d5076c73df97bf0c0567eeb8d8738f54f6bac48abd74
* COMMIT_SHA: aaf4e7faf887b7ac4879c3bf540ede48220cca9f
* LATEST_WEEKLY: false
* LATEST_LTS: true
* BUILD_METADATA_PATH: target/build-result-metadata_linux_publish.json
* BAKE TARGET: linux
* BUILDX OPTIONS:
  --pull
  --push
  --metadata-file=target/build-result-metadata_linux_publish.json

* RESOLVED BAKE CONFIG:
{
...
--


================================================
FILE: Jenkinsfile
================================================
#!/usr/bin/env groovy

def listOfProperties = []
listOfProperties << buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '5'))

// Only master branch will run on a timer basis
if (env.BRANCH_NAME.trim() == 'master') {
    listOfProperties << pipelineTriggers([cron('''H H/6 * * 0-2,4-6
H 6,21 * * 3''')])
}

properties(listOfProperties)

// Default environment variable set to allow images publication
def envVars = ['PUBLISH=true']

// List of architectures and corresponding ci.jenkins.io agent labels
def architecturesAndCiJioAgentLabels = [
    'amd64': 'docker && amd64',
    'arm64': 'arm64docker',
    // Using qemu
    'ppc64le': 'docker && amd64',
    'riscv64': 'docker && amd64',
    's390x': 'docker && amd64',
]

// Set to true in a replay to simulate a LTS build on ci.jenkins.io
// It will set the environment variables needed for a LTS
// and disable images publication out of caution
def SIMULATE_LTS_BUILD = false

if (SIMULATE_LTS_BUILD) {
    envVars = [
        'PUBLISH=false',
        'TAG_NAME=2.504.3',
        // TODO: replace by the first LTS based on 2.534+ when available
        'JENKINS_VERSION=2.541.1',
        // Filter out golden file based testing
        // To filter out all tests, set BATS_FLAGS="--filter-tags none"
        'BATS_FLAGS=--filter-tags "\\!test-type:golden-file"'
    ]
}

stage('Build') {
    def builds = [:]

    withEnv(envVars) {
        echo '= bake target: linux'

        def windowsImageTypes = [
            'windowsservercore-ltsc2019',
            'windowsservercore-ltsc2022'
        ]
        for (anImageType in windowsImageTypes) {
            def imageType = anImageType
            builds[imageType] = {
                def windowsVersionNumber = imageType.split('-')[1].replace('ltsc', '')
                def windowsLabel = "windows-${windowsVersionNumber}"
                nodeWithTimeout(windowsLabel) {
                    stage('Checkout') {
                        checkout scm
                    }

                    withEnv(["IMAGE_TYPE=${imageType}"]) {
                        if (!infra.isTrusted()) {
                            /* Outside of the trusted.ci environment, we're building and testing
                            * the Dockerfile in this repository, but not publishing to docker hub
                            */
                            stage("Build ${imageType}") {
                                powershell './make.ps1 build -ImageType ${env:IMAGE_TYPE}'
                                archiveArtifacts artifacts: 'build-windows_*.yaml', allowEmptyArchive: true
                            }

                            stage("Test ${imageType}") {
                                def windowsTestStatus = powershell(script: './make.ps1 test -ImageType ${env:IMAGE_TYPE}', returnStatus: true)
                                junit(allowEmptyResults: true, keepLongStdio: true, testResults: 'target/**/junit-results.xml')
                                if (windowsTestStatus > 0) {
                                    // If something bad happened let's clean up the docker images
                                    error('Windows test stage failed.')
                                }
                            }

                        // disable until we get the parallel changes merged in
                        // def branchName = "${env.BRANCH_NAME}"
                        // if (branchName ==~ 'master'){
                        //    stage('Publish Experimental') {
                        //        infra.withDockerCredentials {
                        //            withEnv(['DOCKERHUB_ORGANISATION=jenkins4eval','DOCKERHUB_REPO=jenkins']) {
                        //                powershell './make.ps1 publish'
                        //            }
                        //        }
                        //    }
                        // }
                        } else {
                            // Only publish when a tag triggered the build & the publication is enabled (ie not simulating a LTS)
                            if (env.TAG_NAME && (env.PUBLISH == 'true')) {
                                // Split to ensure any suffix is not taken in account (but allow suffix tags to trigger rebuilds)
                                String jenkins_version = env.TAG_NAME.split('-')[0]
                                // Setting WAR_URL to download war from Artifactory instead of mirrors on publication from trusted.ci.jenkins.io
                                withEnv([
                                    "JENKINS_VERSION=${jenkins_version}",
                                    "WAR_URL=https://repo.jenkins-ci.org/public/org/jenkins-ci/main/jenkins-war/${jenkins_version}/jenkins-war-${jenkins_version}.war"
                                ]) {
                                    stage('Publish') {
                                        infra.withDockerCredentials {
                                            withEnv(['DOCKERHUB_ORGANISATION=jenkins', 'DOCKERHUB_REPO=jenkins']) {
                                                powershell './make.ps1 build -ImageType ${env:IMAGE_TYPE}'
                                                powershell './make.ps1 publish -ImageType ${env:IMAGE_TYPE}'
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if (!infra.isTrusted()) {
            // An up to date list can be obtained with make list-linux
            def images = [
                'alpine_jdk21',
                'alpine_jdk25',
                'debian_jdk21',
                'debian_jdk25',
                'debian-slim_jdk21',
                'debian-slim_jdk25',
                'rhel_jdk21',
                'rhel_jdk25',
            ]
            for (i in images) {
                def imageToBuild = i

                builds[imageToBuild] = {
                    nodeWithTimeout(architecturesAndCiJioAgentLabels["amd64"]) {
                        deleteDir()

                        stage('Checkout') {
                            checkout scm
                        }

                        stage('Static analysis') {
                            sh 'make hadolint shellcheck'
                        }

                        /* Outside of the trusted.ci environment, we're building and testing
                        * the Dockerfile in this repository, but not publishing to docker hub
                        */
                        stage("Build linux-${imageToBuild}") {
                            sh "make build-${imageToBuild}"
                            archiveArtifacts artifacts: 'target/build-result-metadata_*.json', allowEmptyArchive: true
                        }

                        stage("Test linux-${imageToBuild}") {
                            sh 'make prepare-test'
                            try {
                                sh "make test-${imageToBuild}"
                            } catch (err) {
                                error("${err.toString()}")
                            } finally {
                                junit(allowEmptyResults: true, keepLongStdio: true, testResults: 'target/*.xml')
                            }
                        }
                    }
                }
            }
            // Building every other architectures than amd64 on agents with the corresponding labels if available
            architecturesAndCiJioAgentLabels.findAll { arch, _ -> arch != 'amd64' }.each { architecture, labels ->
                builds[architecture] = {
                    nodeWithTimeout(labels) {
                        stage('Checkout') {
                            deleteDir()
                            checkout scm
                        }
                        // sanity check that proves all images build on declared platforms not already built in other stages
                        stage("Multi arch build - ${architecture}") {
                            sh "make docker-init buildarch-${architecture}"
                            archiveArtifacts artifacts: 'target/build-result-metadata_*.json', allowEmptyArchive: true
                        }
                    }
                }
            }
        } else {
            // Only publish when a tag triggered the build
            if (env.TAG_NAME) {
                // Split to ensure any suffix is not taken in account (but allow suffix tags to trigger rebuilds)
                String jenkins_version = env.TAG_NAME.split('-')[0]
                builds['linux'] = {
                    // Setting WAR_URL to download war from Artifactory instead of mirrors on publication from trusted.ci.jenkins.io
                    withEnv([
                        "JENKINS_VERSION=${jenkins_version}",
                        "WAR_URL=https://repo.jenkins-ci.org/public/org/jenkins-ci/main/jenkins-war/${jenkins_version}/jenkins-war-${jenkins_version}.war"
                    ]) {
                        nodeWithTimeout('docker') {
                            stage('Checkout') {
                                checkout scm
                            }

                            stage('Publish') {
                                // Publication is enabled by default, disabled when simulating a LTS
                                if (env.PUBLISH == 'true') {
                                    infra.withDockerCredentials {
                                        sh 'make docker-init'
                                        sh 'make publish'
                                        archiveArtifacts artifacts: 'target/build-result-metadata_*.json', allowEmptyArchive: true
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        parallel builds
    }
}

void nodeWithTimeout(String label, def body) {
    node(label) {
        timeout(time: 60, unit: 'MINUTES') {
            body.call()
        }
    }
}


================================================
FILE: LICENSE.txt
================================================
The MIT License

Copyright (c) 2014-, Michael Neale, Nicolas de Loof, Carlos Sanchez, and a number of other of contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.



================================================
FILE: Makefile
================================================
ROOT_DIR="$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/"

## For Docker <=20.04
export DOCKER_BUILDKIT=1
## For Docker <=20.04
export DOCKER_CLI_EXPERIMENTAL=enabled
## Required to have docker build output always printed on stdout
export BUILDKIT_PROGRESS=plain
## Required to have the commit SHA added as a Docker image label
export COMMIT_SHA=$(shell git rev-parse HEAD)

current_os := $(shell uname -s)
current_arch := $(shell uname -m)

export OS ?= $(shell \
	case "$(current_os)" in \
		(Linux) echo linux ;; \
		(Darwin) echo linux ;; \
		(MINGW*|MSYS*|CYGWIN*) echo windows ;; \
		(*) echo unknown ;; \
	esac)

export ARCH ?= $(shell \
	case $(current_arch) in \
		(x86_64) echo "amd64" ;; \
		(aarch64|arm64) echo "arm64" ;; \
		(s390*|riscv*|ppc64le) echo $(current_arch);; \
		(*) echo "UNKNOWN-CPU";; \
	esac)

all: hadolint shellcheck build test

# Set to 'true' to disable parallel tests
DISABLE_PARALLEL_TESTS ?= false

# Set to the path of a specific test suite to restrict execution only to this
# default is "all test suites in the "tests/" directory
TEST_SUITES ?= $(CURDIR)/tests

##### Macros
## Check the presence of a CLI in the current PATH
check_cli = type "$(1)" >/dev/null 2>&1 || { echo "Error: command '$(1)' required but not found. Exiting." ; exit 1 ; }
## Check if a given image exists in the current manifest docker-bake.hcl
check_image = make --silent list | grep -w '$(1)' >/dev/null 2>&1 || { echo "Error: the image '$(1)' does not exist in manifest for the current platform '$(OS)/$(ARCH)'. Please check the output of 'make list'. Exiting." ; exit 1 ; }
## Base "docker buildx base" command to be reused everywhere
bake_base_cli := docker buildx bake -f docker-bake.hcl --load
## Default bake target
bake_default_target := all

check-reqs:
## Build requirements
	@$(call check_cli,bash)
	@$(call check_cli,git)
	@$(call check_cli,docker)
	@docker info | grep 'buildx:' >/dev/null 2>&1 || { echo "Error: Docker BuildX plugin required but not found. Exiting." ; exit 1 ; }
## Test requirements
	@$(call check_cli,curl)
	@$(call check_cli,jq)

## This function is specific to Jenkins infrastructure and isn't required in other contexts
docker-init: check-reqs
ifeq ($(CI),true)
ifeq ($(wildcard /etc/buildkitd.toml),)
	@echo 'WARNING: /etc/buildkitd.toml not found, using default configuration.'
	docker buildx create --use --bootstrap --driver docker-container
else
	docker buildx create --use --bootstrap --driver docker-container --config /etc/buildkitd.toml
endif
else
	docker buildx create --use --bootstrap --driver docker-container
endif
# There is only an amd64 qemu image
ifeq ($(ARCH),amd64)
	docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
endif

# Lint check on all Dockerfiles
hadolint:
	find . -type f -name 'Dockerfile*' -not -path "./bats/*" -print0 | xargs -0 $(ROOT_DIR)/tools/hadolint

# Shellcheck on all bash scripts
shellcheck:
	@$(ROOT_DIR)/tools/shellcheck -e SC1091 jenkins-support *.sh tests/test_helpers.bash tools/hadolint tools/shellcheck .ci/publish.sh

# Build all targets with the current OS and architecture
build: check-reqs target
	@set -x; $(bake_base_cli) --metadata-file=target/build-result-metadata_$(bake_default_target).json --set '*.platform=$(OS)/$(ARCH)' $(shell make --silent list)

# Build targets depending on the architecture (Linux only, no multiarch for Windows)
buildarch-%: check-reqs target showarch-%
	@set -x; $(bake_base_cli) --metadata-file=target/build-result-metadata_$*.json --set '*.platform=linux/$*' $(shell make --silent listarch-$*)

# Build a specific target with the current OS and architecture
build-%: check-reqs target show-%
	@$(call check_image,$*)
	@set -x; $(bake_base_cli) --metadata-file=target/build-result-metadata_$*.json --set '*.platform=$(OS)/$(ARCH)' '$*'

# Show all targets
show:
	@set -x; make --silent show-$(bake_default_target)

# Show a specific target
show-%:
	@set -x; $(bake_base_cli) --progress=quiet '$*' --print | jq

# Show all targets depending on the architecture
showarch-%:
	@set -x; make --silent show | jq --arg arch "$(OS)/$*" '.target |= with_entries(select(.value.platforms | index($$arch)))'

# List tags of all targets
tags:
	@set -x; make tags-$(bake_default_target)

# List tags of a specific target
tags-%:
	@set -x; make show-$* | jq -r ' .target | to_entries[] | .key as $$name | .value.tags[] | "\(.) (\($$name))"' | LC_ALL=C sort -u

# List all platforms
platforms:
	@set -x; make platforms-$(bake_default_target)

# List platforms of a specific target
platforms-%:
	@set -x; make show-$* | jq -r ' .target | to_entries[] | .key as $$name | .value.platforms[] | "\($$name):\(.)"' | LC_ALL=C sort -u

# Return the list of targets depending on the current OS and architecture
list: check-reqs
	@set -x; make --silent listarch-$(ARCH)

# Return the list of targets of a specific "target" (can be a docker bake group)
list-%: check-reqs
	@set -x; make --silent show-$* | jq -r '.target | keys[]'

# Return the list of targets depending on the architecture (Linux only, no multiarch for Windows)
listarch-%: check-reqs
	@set -x; make --silent showarch-$* | jq -r '.target | keys[]'

# Ensure bats exists in the current folder
bats:
	git clone https://github.com/bats-core/bats-core bats ;\
	cd bats ;\
	git checkout 3bca150ec86275d6d9d5a4fd7d48ab8b6c6f3d87; # v1.13.0

# Ensure all bats submodules are up to date
prepare-test: bats check-reqs target
	git submodule update --init --recursive

# Ensure tests and build metadata "target" folder exist
target:
	mkdir -p target

## Define bats options based on environment
# common flags for all tests
bats_flags := $(TEST_SUITES)
# if DISABLE_PARALLEL_TESTS true, then disable parallel execution
ifneq (true,$(DISABLE_PARALLEL_TESTS))
# If the GNU 'parallel' command line is absent, then disable parallel execution
parallel_cli := $(shell command -v parallel 2>/dev/null)
ifneq (,$(parallel_cli))
# If parallel execution is enabled, then set 2 tests per core available for the Docker Engine
test-%: PARALLEL_JOBS ?= $(shell echo $$(( $(shell docker run --rm alpine grep -c processor /proc/cpuinfo) * 2)))
test-%: bats_flags += --jobs $(PARALLEL_JOBS)
endif
endif
# Optional bats flags (see https://bats-core.readthedocs.io/en/stable/usage.html)
ifneq (,$(BATS_FLAGS))
test-%: bats_flags += $(BATS_FLAGS)
endif
test-%: prepare-test
# Check that the image exists in the manifest
	@$(call check_image,$*)
# Ensure that the image is built
	@make --silent build-$*
# Show bats version
	@bats/bin/bats --version
ifeq ($(CI), true)
# Execute the test harness and write result to a TAP file
	IMAGE=$* bats/bin/bats $(bats_flags) --formatter junit | tee target/junit-results-$*.xml
else
# Execute the test harness
	IMAGE=$* bats/bin/bats $(bats_flags) --timing
endif

# Test targets depending on the current architecture
test: prepare-test
	@make --silent list | while read image; do make --silent "test-$${image}"; done

# Set all required variables and publish all targets
# Calling publish.sh with `-n` (dry-run) arg in case `PUBLISH` is not set to true
publish: target
ifeq ($(PUBLISH),true)
	./.ci/publish.sh
else
	./.ci/publish.sh -n
endif

clean:
	rm -rf tests/test_helper/bats-*; \
	rm -rf bats

.PHONY: hadolint shellcheck check-reqs build clean test list show


================================================
FILE: README.md
================================================
# Official Jenkins Docker image

[![Docker Stars](https://img.shields.io/docker/stars/jenkins/jenkins.svg)](https://hub.docker.com/r/jenkins/jenkins/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jenkins/jenkins.svg)](https://hub.docker.com/r/jenkins/jenkins/)
[![Join the chat at https://gitter.im/jenkinsci/docker](https://badges.gitter.im/jenkinsci/docker.svg)](https://gitter.im/jenkinsci/docker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

The Jenkins Continuous Integration and Delivery server [available on Docker Hub](https://hub.docker.com/r/jenkins/jenkins).

This is a fully functional Jenkins server.
[https://jenkins.io/](https://jenkins.io/).

<img src="https://jenkins.io/sites/default/files/jenkins_logo.png"/>

# Usage

```
docker run -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:lts-jdk21
```

NOTE: read the section [_Connecting agents_](#connecting-agents) below for the role of the `50000` port mapping.
NOTE: read the section [_DNS Configuration_](#dns-configuration) in case you see the message "This Jenkins instance appears to be offline." 

This will store the workspace in `/var/jenkins_home`.
All Jenkins data lives in there - including plugins and configuration.
You will probably want to make that an explicit volume so you can manage it and attach to another container for upgrades :

```
docker run -p 8080:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk21
```

This will automatically create a 'jenkins_home' [docker volume](https://docs.docker.com/storage/volumes/) on the host machine.
Docker volumes retain their content even when the container is stopped, started, or deleted.

NOTE: Avoid using a [bind mount](https://docs.docker.com/storage/bind-mounts/) from a folder on the host machine into `/var/jenkins_home`, as this might result in file permission issues (the user used inside the container might not have rights to the folder on the host machine).
If you _really_ need to bind mount jenkins_home, ensure that the directory on the host is accessible by the jenkins user inside the container (jenkins user - uid 1000) or use `-u some_other_user` parameter with `docker run`.

```
docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:lts-jdk21
```

This will run Jenkins in detached mode with port forwarding and volume added. You can access logs with command 'docker logs CONTAINER_ID' in order to check first login token. ID of container will be returned from output of command above.

Or, directly print the initial admin password using:

```
docker exec <jenkins_container_id_or_name> cat /var/jenkins_home/secrets/initialAdminPassword
```
Replace <jenkins_container_id_or_name> with your actual Jenkins container id or name.

To access Jenkins and complete the initial setup, follow the instructions in the [installation guide](https://www.jenkins.io/doc/book/installing/docker/#setup-wizard).

## Backing up data

If you bind mount in a volume - you can simply back up that directory
(which is jenkins_home) at any time.

Using a bind mount is not recommended since it can lead to permission issues. Treat the jenkins_home directory as you would a database - in Docker you would generally put a database on a volume.

If your volume is inside a container - you can use `docker cp $ID:/var/jenkins_home` command to extract the data, or other options to find where the volume data is.
Note that some symlinks on some OSes may be converted to copies (this can confuse jenkins with lastStableBuild links, etc)

For more info check Docker docs section on [Use volumes](https://docs.docker.com/storage/volumes/)

## Setting the number of executors

You can define the number of executors on the Jenkins built-in node using a groovy script.
By default it is set to 2 executors, but you can extend the image and change it to your desired number of executors (recommended 0 executors on the built-in node) :

`executors.groovy`

```
import jenkins.model.*
Jenkins.instance.setNumExecutors(0) // Recommended to not run builds on the built-in node
```

and `Dockerfile`

```
FROM jenkins/jenkins:lts
COPY --chown=jenkins:jenkins executors.groovy /usr/share/jenkins/ref/init.groovy.d/executors.groovy
```

## Connecting agents

You can run builds on the controller out of the box.
The Jenkins project recommends that no executors be enabled on the controller.

In order to connect agents **through an inbound TCP connection**, map the port: `-p 50000:50000`.
That port will be used when you connect agents to the controller.

If you are only using [SSH (outbound) build agents](https://plugins.jenkins.io/ssh-slaves/), this port is not required, as connections are established from the controller.
If you connect agents using web sockets (since Jenkins 2.217), the TCP agent port is not used either.

## Passing JVM parameters

You might need to customize the JVM running Jenkins, typically to adjust [system properties](https://www.jenkins.io/doc/book/managing/system-properties/) or tweak heap memory settings.
Use the `JAVA_OPTS` or `JENKINS_JAVA_OPTS` environment variables for this purpose :

```
docker run --name myjenkins -p 8080:8080 -p 50000:50000 --restart=on-failure --env JAVA_OPTS=-Dhudson.footerURL=http://mycompany.com jenkins/jenkins:lts-jdk21
```

JVM options specifically for the Jenkins controller should be set through `JENKINS_JAVA_OPTS`, as other tools might also respond to the `JAVA_OPTS` environment variable.

## Configuring logging

Jenkins logging can be configured through a properties file and `java.util.logging.config.file` Java property.
For example:

```
mkdir data
cat > data/log.properties <<EOF
handlers=java.util.logging.ConsoleHandler
jenkins.level=FINEST
java.util.logging.ConsoleHandler.level=FINEST
EOF
docker run --name myjenkins -p 8080:8080 -p 50000:50000 --restart=on-failure --env JAVA_OPTS="-Djava.util.logging.config.file=/var/jenkins_home/log.properties" -v `pwd`/data:/var/jenkins_home jenkins/jenkins:lts-jdk21
```

## Configuring reverse proxy

If you want to install Jenkins behind a reverse proxy with a prefix, example: mysite.com/jenkins, you need to add environment variable `JENKINS_OPTS="--prefix=/jenkins"` and then follow the below procedures to configure your reverse proxy, which will depend if you have Apache or Nginx:

- [Apache](https://www.jenkins.io/doc/book/system-administration/reverse-proxy-configuration-apache/)
- [Nginx](https://www.jenkins.io/doc/book/system-administration/reverse-proxy-configuration-nginx/)

## DNS configuration

If the message "This Jenkins instance appears to be offline." appears on first startup, and the container logs show lines such as `java.net.UnknownHostException: updates.jenkins.io`, your container may be having issues with resolving DNS names.

To potentially solve the issue, start the container specifying a dns server (for example Cloudflare's 1.1.1.1 or Google's 8.8.8.8, or any other DNS server):
```
docker run -p 8080:8080 -p 50000:50000 --restart=on-failure --dns 1.1.1.1 --dns 8.8.8.8 jenkins/jenkins:lts-jdk21
```

## Passing Jenkins launcher parameters

Arguments you pass to docker running the Jenkins image are passed to jenkins launcher, so for example you can run:

```
docker run jenkins/jenkins:lts-jdk21 --version
```

This will show the Jenkins version, the same as when you run Jenkins from an executable war.

You can also define Jenkins arguments via `JENKINS_OPTS`. This is useful for customizing arguments to the jenkins
launcher in a derived Jenkins image. The following sample Dockerfile uses this option
to force use of HTTPS with a certificate included in the image.

```
FROM jenkins/jenkins:lts-jdk21

COPY --chown=jenkins:jenkins certificate.pfx /var/lib/jenkins/certificate.pfx
COPY --chown=jenkins:jenkins https.key /var/lib/jenkins/pk
ENV JENKINS_OPTS="--httpPort=-1 --httpsPort=8083 --httpsKeyStore=/var/lib/jenkins/certificate.pfx --httpsKeyStorePassword=Password12"
EXPOSE 8083
```

You can also change the default agent port for Jenkins by defining `JENKINS_SLAVE_AGENT_PORT` in a sample Dockerfile.

```
FROM jenkins/jenkins:lts-jdk21
ENV JENKINS_SLAVE_AGENT_PORT=50001
```

or as a parameter to docker,

```
docker run --name myjenkins -p 8080:8080 -p 50001:50001 --restart=on-failure --env JENKINS_SLAVE_AGENT_PORT=50001 jenkins/jenkins:lts-jdk21
```

**Note**: This environment variable will be used to set the
[system property](https://www.jenkins.io/doc/book/managing/system-properties/) `jenkins.model.Jenkins.slaveAgentPort`.

> If this property is already set in **JAVA_OPTS** or **JENKINS_JAVA_OPTS**, then the value of
> `JENKINS_SLAVE_AGENT_PORT` will be ignored.

# Installing more tools

You can run your container as root - and install via apt-get, install as part of build steps via jenkins tool installers, or you can create your own Dockerfile to customise, for example:

```
FROM jenkins/jenkins:lts-jdk21
# if we want to install via apt
USER root
RUN apt-get update && apt-get install -y ruby make more-thing-here
# drop back to the regular jenkins user - good practice
USER jenkins
```

In such a derived image, you can customize your jenkins instance with hook scripts or additional plugins.
For this purpose, use `/usr/share/jenkins/ref` as a place to define the default JENKINS_HOME content you
wish the target installation to look like :

```
FROM jenkins/jenkins:lts-jdk21
COPY --chown=jenkins:jenkins custom.groovy /usr/share/jenkins/ref/init.groovy.d/custom.groovy
```

If you need to maintain the entire init.groovy.d directory and have a persistent JENKINS_HOME you may run the docker image with `-e PRE_CLEAR_INIT_GROOVY_D=true`

## Preinstalling plugins

### Install plugins

You can rely on [the plugin manager CLI](https://github.com/jenkinsci/plugin-installation-manager-tool/) to pass a set of plugins to download with their dependencies. This tool will perform downloads from update centers, and internet access is required for the default update centers.

### Setting update centers

During the download, the CLI will use update centers defined by the following environment variables:

- `JENKINS_UC` - Main update center.
  This update center may offer plugin versions depending on the Jenkins LTS Core versions.
  Default value: https://updates.jenkins.io
- `JENKINS_UC_EXPERIMENTAL` - [Experimental Update Center](https://jenkins.io/blog/2013/09/23/experimental-plugins-update-center/).
  This center offers Alpha and Beta versions of plugins.
  Default value: https://updates.jenkins.io/experimental
- `JENKINS_INCREMENTALS_REPO_MIRROR` -
  Defines Maven mirror to be used to download plugins from the
  [Incrementals repo](https://jenkins.io/blog/2018/05/15/incremental-deployment/).
  Default value: https://repo.jenkins-ci.org/incrementals
- `JENKINS_UC_DOWNLOAD` - Download url of the Update Center.
  Default value: `$JENKINS_UC/download`
- `JENKINS_PLUGIN_INFO` - Location of plugin information.
  Default value: https://updates.jenkins.io/current/plugin-versions.json

It is possible to override the environment variables in images.

:exclamation: Note that changing update center variables **will not** change the Update Center being used by Jenkins runtime, it concerns only the plugin manager CLI.

### Installing Custom Plugins

Installing prebuilt, custom plugins can be accomplished by copying the plugin HPI file into `/usr/share/jenkins/ref/plugins/` within the `Dockerfile`:

```
COPY --chown=jenkins:jenkins path/to/custom.hpi /usr/share/jenkins/ref/plugins/
```

### Usage

You can run the CLI manually in Dockerfile:

```Dockerfile
FROM jenkins/jenkins:lts-jdk21
RUN jenkins-plugin-cli --plugins pipeline-model-definition github-branch-source:1.8
```

Furthermore it is possible to pass a file that contains this set of plugins (with or without line breaks).

```Dockerfile
FROM jenkins/jenkins:lts-jdk21
COPY --chown=jenkins:jenkins plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt
```

When jenkins container starts, it will check `JENKINS_HOME` has this reference content, and copy them
there if required. It will not override such files, so if you upgraded some plugins from UI they won't
be reverted on next start.

In case you _do_ want to override, append '.override' to the name of the reference file. E.g. a file named
`/usr/share/jenkins/ref/config.xml.override` will overwrite an existing `config.xml` file in JENKINS_HOME.

Also see [JENKINS-24986](https://issues.jenkins.io/browse/JENKINS-24986)

Here is an example to get the list of plugins from an existing server:

```
JENKINS_HOST=username:password@myhost.com:port
curl -sSL "http://$JENKINS_HOST/pluginManager/api/xml?depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1 \2\n/g'|sed 's/ /:/'
```

Example Output:

```
cucumber-testresult-plugin:0.8.2
pam-auth:1.1
matrix-project:1.4.1
script-security:1.13
...
```

For 2.x-derived images, you may also want to

    RUN echo 2.0 > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state

to indicate that this Jenkins installation is fully configured.
Otherwise a banner will appear prompting the user to install additional plugins,
which may be inappropriate.

### Access logs

To enable Jenkins user access logs from Jenkins home directory inside a docker container, set the `JENKINS_OPTS` environment variable value to `--accessLoggerClassName=winstone.accesslog.SimpleAccessLogger --simpleAccessLogger.format=combined --simpleAccessLogger.file=/var/jenkins_home/logs/access_log`

### Naming convention in tags

The naming convention for the tags on Docker Hub follows the format `<repository_name>:<tag>`, where the repository name is jenkins/jenkins and where the tag specifies the image version.
In the case of the LTS and latest versions, the tags are `lts` and `latest`, respectively.

You can use these tags to pull the corresponding Jenkins images from Docker Hub and run them on your system.
For example, to pull the LTS version of the Jenkins image use this command: `docker pull jenkins/jenkins:lts`

### Docker Compose with Jenkins

To use Docker Compose with Jenkins, you can define a docker-compose.yml file including a Jenkins instance and any other services it depends on.
For example, the following docker-compose.yml file defines a Jenkins controller and a Jenkins SSH agent:

```yaml
services:
  jenkins:
    image: jenkins/jenkins:lts
    ports:
      - "8080:8080"
    volumes:
      - jenkins_home:/var/jenkins_home
  ssh-agent:
    image: jenkins/ssh-agent
volumes:
  jenkins_home:
```

This `docker-compose.yml` file creates two containers: one for Jenkins and one for the Jenkins SSH agent.

The Jenkins container is based on the `jenkins/jenkins:lts` image and exposes the Jenkins web interface on port 8080.
The `jenkins_home` volume is a [named volume](https://docs.docker.com/storage/volumes/) that is created and managed by Docker.

It is mounted at `/var/jenkins_home` in the Jenkins container, and it will persist the Jenkins configuration and data.

The ssh-agent container is based on the `jenkins/ssh-agent` image and runs an SSH server to execute [Jenkins SSH Build Agent](https://plugins.jenkins.io/ssh-slaves/).

To start the Jenkins instance and the other services defined in the `docker-compose.yml` file, run the `docker compose up -d`.

This will pull the necessary images from Docker Hub if they are not already present on your system, and start the services in the background.

You can then access the Jenkins web interface on `http://localhost:8080` on your host system to configure and manage your Jenkins instance (where `localhost` points to the published port by your Docker Engine).

NOTE: read the section [_DNS Configuration_](#dns-configuration) in case you see the message "This Jenkins instance appears to be offline." In that case add the dns configuration to the yaml:
```yaml
services:
  jenkins:
# ... other config
    dns:
      - 1.1.1.1
      - 8.8.8.8
# ... other config
```

### Updating plugins file

The [plugin-installation-manager-tool](https://github.com/jenkinsci/plugin-installation-manager-tool) supports updating the plugin file for you.

Example command:

```command
JENKINS_IMAGE=jenkins/jenkins:lts-jdk21
docker run -it ${JENKINS_IMAGE} bash -c "stty -onlcr && jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt --available-updates --output txt" >  plugins2.txt
mv plugins2.txt plugins.txt
```

## Upgrading

All the data needed is in the /var/jenkins_home directory - so depending on how you manage that - depends on how you upgrade.
Generally - you can copy it out - and then "docker pull" the image again - and you will have the latest LTS - you can then start up with -v pointing to that data (/var/jenkins_home) and everything will be as you left it.

As always - please ensure that you know how to drive docker - especially volume handling!

If you mount the Jenkins home directory to a [Docker named volume](https://docs.docker.com/storage/volumes/), then the upgrade consists of `docker pull` and nothing more.

We recommend using `docker compose`, especially in cases where the user is also running a parallel nginx/apache container as a reverse proxy for the Jenkins container.

### Upgrading plugins

By default, plugins will be upgraded if they haven't been upgraded manually and if the version from the docker image is newer than the version in the container.
Versions installed by the docker image are tracked through a marker file.

To force upgrades of plugins that have been manually upgraded, run the docker image with `-e PLUGINS_FORCE_UPGRADE=true`.

The default behaviour when upgrading from a docker image that didn't write marker files is to leave existing plugins in place.
If you want to upgrade existing plugins without marker you may run the docker image with `-e TRY_UPGRADE_IF_NO_MARKER=true`.
Then plugins will be upgraded if the version provided by the docker image is newer.

# Hacking

If you wish to contribute fixes to this repository, please refer to the [dedicated documentation](HACKING.adoc).

# Security

For information related to the security of this Docker image, please refer to the [dedicated documentation](SECURITY.md).

# Questions?

We're on Gitter, https://gitter.im/jenkinsci/docker


================================================
FILE: SECURITY.md
================================================
# Security Policy

The Jenkins project takes security seriously.
We make every possible effort to ensure users can adequately secure their automation infrastructure.

You can find more information in the [general Security Policy](https://github.com/jenkinsci/.github/blob/master/SECURITY.md), this policy is specific to our Docker images.

## Docker Image Publication

When an image is published, the latest image and the latest available packages are used.

We rely on the base image provider for the security of the system libraries.
The default base image is Debian but multiple other variants are proposed, that could potentially better fit your needs.

## Reporting Security Vulnerabilities

If you have identified a security vulnerability and would like to report it, please be aware of those requirements.

For findings from a **Software Composition Analysis (SCA) scanner report**, all of the following points must be satisfied:
- If the finding is coming from the system (Docker layer):
  - The scan must have been done on the latest version of the image.
Vulnerabilities are discovered in a continuous way, so it is expected that past releases could contain some.
  - The package should have a fixed version provided in the base image that is not yet included in our image.
We rely on the base image provider to propose the corrections.
  - The correction should have existed at the time the image was created.
Normally our update workflow ensures that the latest available versions are used.
- If the finding is coming from the application dependencies:
  - Proof of exploitation or sufficiently good explanation about why you think it's impacting the application.

For all "valid" findings from SCA, your report must contain:
- The path to the library (there are ~2000 components in the ecosystem, we don't want to have to guess)
- The version and variant of the Docker image you scanned.
- The scanner name and version as well.
- The publicly accessible information about the vulnerability (ideally CVE). For private vulnerability database, please provide all the information at your disposal.

The objective is to reduce the number of reports we receive that are not relevant to the security of the project.

For findings from a **manual audit**, the report must contain either reproduction steps or a sufficiently well described proof to demonstrate the impact.

Once the report is ready, please follow the process about [Reporting Security Vulnerabilities](https://jenkins.io/security/reporting/).

We will reject reports that are not satisfying those requirements.

## Vulnerability Management

Once the report is considered legitimate, a new image is published with the latest packages.
In the case the adjustment has to be done in the building process (e.g. in the Dockerfile), the correction will be prioritized and applied as soon as possible.

By default we do not plan to publish advisories for vulnerabilities at the Docker level. 
There may be exceptions.


================================================
FILE: alpine/hotspot/Dockerfile
================================================
ARG ALPINE_TAG=3.23.3

FROM alpine:"${ALPINE_TAG}" AS jre-and-war

ARG JAVA_VERSION=17.0.18_8

SHELL ["/bin/ash", "-o", "pipefail", "-c"]

COPY jdk-download-url.sh /usr/bin/jdk-download-url.sh
COPY jdk-download.sh /usr/bin/jdk-download.sh

RUN apk add --no-cache \
    ca-certificates \
    gnupg \
    jq \
    curl \
    && rm -fr /var/cache/apk/* \
    && /usr/bin/jdk-download.sh alpine

ENV PATH="/opt/jdk-${JAVA_VERSION}/bin:${PATH}"

# Generate smaller java runtime without unneeded files
# for now we include the full module path to maintain compatibility
# while still saving space (approx 200mb from the full distribution)
# hadolint ignore=SC2086
RUN java_major_version="$(jlink --version 2>&1 | cut -c1-2)"; \
    if [ "$java_major_version" = "25" ]; then \
      cp -r "/opt/jdk-${JAVA_VERSION}" /javaruntime; \
    else \
      case "$java_major_version" in \
        "17") options="--compress=2" ;; \
        "21") options="--compress=zip-6" ;; \
        *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \
      esac; \
      jlink \
        --strip-java-debug-attributes \
        ${options} \
        --add-modules ALL-MODULE-PATH \
        --no-man-pages \
        --no-header-files \
        --output /javaruntime; \
    fi

# Jenkins version being bundled in this docker image
ARG JENKINS_VERSION=2.555
# Can be used to customize where jenkins.war get downloaded from
ARG WAR_URL=https://get.jenkins.io/war/${JENKINS_VERSION}/jenkins.war

COPY jenkins.io-2026.key /war/jenkins-key.pub

# Not using ADD as it does not check Last-Modified header
# see https://github.com/docker/docker/issues/8331
RUN curl -fsSL "${WAR_URL}" -o /war/jenkins.war \
  && curl -fsSL "${WAR_URL}.asc" -o /war/jenkins.war.asc \
  && gpg --import /war/jenkins-key.pub \
  && gpg --verify --trust-model direct /war/jenkins.war.asc /war/jenkins.war

FROM alpine:"${ALPINE_TAG}" AS controller

RUN apk add --no-cache \
    bash \
    coreutils \
    curl \
    git \
    git-lfs \
    musl-locales \
    musl-locales-lang \
    openssh-client \
    tini \
    ttf-dejavu \
    tzdata \
    unzip \
  && git lfs install

ENV LANG=C.UTF-8

ARG TARGETARCH
ARG COMMIT_SHA

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG http_port=8080
ARG agent_port=50000
ARG JENKINS_HOME=/var/jenkins_home
ARG REF=/usr/share/jenkins/ref

ENV JENKINS_HOME=$JENKINS_HOME
ENV JENKINS_SLAVE_AGENT_PORT=${agent_port}
ENV REF=$REF

# Jenkins is run with user `jenkins`, uid = 1000
# If you bind mount a volume from the host or a data container,
# ensure you use the same uid
RUN mkdir -p $JENKINS_HOME \
  && chown ${uid}:${gid} $JENKINS_HOME \
  && addgroup -g ${gid} ${group} \
  && adduser -h "$JENKINS_HOME" -u ${uid} -G ${group} -s /bin/bash -D ${user}

# Jenkins home directory is a volume, so configuration and build history
# can be persisted and survive image upgrades
VOLUME $JENKINS_HOME

# $REF (defaults to `/usr/share/jenkins/ref/`) contains all reference configuration we want
# to set on a fresh new installation. Use it to bundle additional plugins
# or config file with your custom jenkins Docker image.
RUN mkdir -p ${REF}/init.groovy.d

ENV JENKINS_UC=https://updates.jenkins.io
ENV JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental
ENV JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals
RUN chown -R ${user} "$JENKINS_HOME" "$REF"

ARG PLUGIN_CLI_VERSION=2.14.0
ARG PLUGIN_CLI_URL=https://github.com/jenkinsci/plugin-installation-manager-tool/releases/download/${PLUGIN_CLI_VERSION}/jenkins-plugin-manager-${PLUGIN_CLI_VERSION}.jar
RUN curl -fsSL ${PLUGIN_CLI_URL} -o /opt/jenkins-plugin-manager.jar \
  && echo "$(curl -fsSL "${PLUGIN_CLI_URL}.sha256")  /opt/jenkins-plugin-manager.jar" >/tmp/jpm_sha \
  && sha256sum -c --strict /tmp/jpm_sha \
  && rm -f /tmp/jpm_sha

# for main web interface:
EXPOSE ${http_port}

# will be used by attached agents:
EXPOSE ${agent_port}

ENV COPY_REFERENCE_FILE_LOG=$JENKINS_HOME/copy_reference_file.log

ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-and-war /javaruntime $JAVA_HOME
COPY --from=jre-and-war /war/jenkins.war /usr/share/jenkins/jenkins.war

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY jenkins-plugin-cli.sh /bin/jenkins-plugin-cli

ARG JENKINS_VERSION=2.555
ENV JENKINS_VERSION=${JENKINS_VERSION}

ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"]

# metadata labels
LABEL \
    org.opencontainers.image.vendor="Jenkins project" \
    org.opencontainers.image.title="Official Jenkins Docker image" \
    org.opencontainers.image.description="The Jenkins Continuous Integration and Delivery server" \
    org.opencontainers.image.version="${JENKINS_VERSION}" \
    org.opencontainers.image.url="https://www.jenkins.io/" \
    org.opencontainers.image.source="https://github.com/jenkinsci/docker" \
    org.opencontainers.image.revision="${COMMIT_SHA}" \
    org.opencontainers.image.licenses="MIT"


================================================
FILE: debian/Dockerfile
================================================
ARG TRIXIE_TAG=20251103

ARG DEBIAN_RELEASE_LINE=trixie
ARG DEBIAN_VERSION=20251117
ARG DEBIAN_VARIANT="-slim"
FROM debian:"${DEBIAN_RELEASE_LINE}-${DEBIAN_VERSION}${DEBIAN_VARIANT}" AS jre-and-war

ARG JAVA_VERSION=17.0.18_8

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

COPY jdk-download-url.sh /usr/bin/jdk-download-url.sh
COPY jdk-download.sh /usr/bin/jdk-download.sh

RUN apt-get update \
  && apt-get install --no-install-recommends -y \
    ca-certificates \
    curl \
    gnupg \
    jq \
  && rm -rf /var/lib/apt/lists/* \
  && /usr/bin/jdk-download.sh

ENV PATH="/opt/jdk-${JAVA_VERSION}/bin:${PATH}"

# Generate smaller java runtime without unneeded files
# for now we include the full module path to maintain compatibility
# while still saving space (approx 200mb from the full distribution)
# hadolint ignore=SC2086
RUN java_major_version="$(jlink --version 2>&1 | cut -c1-2)"; \
    if [ "$java_major_version" = "25" ]; then \
      cp -r "/opt/jdk-${JAVA_VERSION}" /javaruntime; \
    else \
      case "$java_major_version" in \
        "17") options="--compress=2" ;; \
        "21") options="--compress=zip-6" ;; \
        *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \
      esac; \
      jlink \
        --strip-java-debug-attributes \
        ${options} \
        --add-modules ALL-MODULE-PATH \
        --no-man-pages \
        --no-header-files \
        --output /javaruntime; \
    fi

# Jenkins version being bundled in this docker image
ARG JENKINS_VERSION=2.555
# Can be used to customize where jenkins.war get downloaded from
ARG WAR_URL=https://get.jenkins.io/war/${JENKINS_VERSION}/jenkins.war

COPY jenkins.io-2026.key /war/jenkins-key.pub

# Not using ADD as it does not check Last-Modified header
# see https://github.com/docker/docker/issues/8331
RUN curl -fsSL "${WAR_URL}" -o /war/jenkins.war \
  && curl -fsSL "${WAR_URL}.asc" -o /war/jenkins.war.asc \
  && gpg --import /war/jenkins-key.pub \
  && gpg --verify --trust-model direct /war/jenkins.war.asc /war/jenkins.war

FROM debian:"${DEBIAN_RELEASE_LINE}-${DEBIAN_VERSION}${DEBIAN_VARIANT}" AS controller

RUN apt-get update \
  && apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    git \
    libfontconfig1 \
    libfreetype6 \
    procps \
    ssh-client \
    tini \
    unzip \
    tzdata \
  && rm -rf /var/lib/apt/lists/*

# Git LFS is not available from a package manager on all the platforms we support
# Download and unpack the tar.gz distribution
ARG GIT_LFS_VERSION=3.7.1
# hadolint ignore=DL4006
RUN arch=$(uname -m | sed -e 's/x86_64/amd64/g' -e 's/aarch64/arm64/g') \
  && curl -L -s -o git-lfs.tgz "https://github.com/git-lfs/git-lfs/releases/download/v${GIT_LFS_VERSION}/git-lfs-linux-${arch}-v${GIT_LFS_VERSION}.tar.gz" \
  && tar xzf git-lfs.tgz \
  && bash git-lfs-*/install.sh \
  && rm -rf git-lfs*

ENV LANG=C.UTF-8

ARG TARGETARCH
ARG COMMIT_SHA

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG http_port=8080
ARG agent_port=50000
ARG JENKINS_HOME=/var/jenkins_home
ARG REF=/usr/share/jenkins/ref

ENV JENKINS_HOME=$JENKINS_HOME
ENV JENKINS_SLAVE_AGENT_PORT=${agent_port}
ENV REF=$REF

# Jenkins is run with user `jenkins`, uid = 1000
# If you bind mount a volume from the host or a data container,
# ensure you use the same uid
RUN mkdir -p $JENKINS_HOME \
  && chown ${uid}:${gid} $JENKINS_HOME \
  && groupadd -g ${gid} ${group} \
  && useradd -d "$JENKINS_HOME" -u ${uid} -g ${gid} -l -m -s /bin/bash ${user}

# Jenkins home directory is a volume, so configuration and build history
# can be persisted and survive image upgrades
VOLUME $JENKINS_HOME

# $REF (defaults to `/usr/share/jenkins/ref/`) contains all reference configuration we want
# to set on a fresh new installation. Use it to bundle additional plugins
# or config file with your custom jenkins Docker image.
RUN mkdir -p ${REF}/init.groovy.d

ENV JENKINS_UC=https://updates.jenkins.io
ENV JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental
ENV JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals
RUN chown -R ${user} "$JENKINS_HOME" "$REF"

ARG PLUGIN_CLI_VERSION=2.14.0
ARG PLUGIN_CLI_URL=https://github.com/jenkinsci/plugin-installation-manager-tool/releases/download/${PLUGIN_CLI_VERSION}/jenkins-plugin-manager-${PLUGIN_CLI_VERSION}.jar
RUN curl -fsSL ${PLUGIN_CLI_URL} -o /opt/jenkins-plugin-manager.jar \
  && echo "$(curl -fsSL "${PLUGIN_CLI_URL}.sha256")  /opt/jenkins-plugin-manager.jar" >/tmp/jpm_sha \
  && sha256sum -c --strict /tmp/jpm_sha \
  && rm -f /tmp/jpm_sha

# for main web interface:
EXPOSE ${http_port}

# will be used by attached agents:
EXPOSE ${agent_port}

ENV COPY_REFERENCE_FILE_LOG=$JENKINS_HOME/copy_reference_file.log

ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-and-war /javaruntime $JAVA_HOME
COPY --from=jre-and-war /war/jenkins.war /usr/share/jenkins/jenkins.war

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY jenkins-plugin-cli.sh /bin/jenkins-plugin-cli

ARG JENKINS_VERSION=2.555
ENV JENKINS_VERSION=${JENKINS_VERSION}

ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/jenkins.sh"]

# metadata labels
LABEL \
    org.opencontainers.image.vendor="Jenkins project" \
    org.opencontainers.image.title="Official Jenkins Docker image" \
    org.opencontainers.image.description="The Jenkins Continuous Integration and Delivery server" \
    org.opencontainers.image.version="${JENKINS_VERSION}" \
    org.opencontainers.image.url="https://www.jenkins.io/" \
    org.opencontainers.image.source="https://github.com/jenkinsci/docker" \
    org.opencontainers.image.revision="${COMMIT_SHA}" \
    org.opencontainers.image.licenses="MIT"


================================================
FILE: docker-bake.hcl
================================================
## Variables
variable "jdks_to_build" {
  default = [21, 25]
}

variable "windows_version_to_build" {
  default = ["ltsc2019", "ltsc2022"]
}

variable "default_jdk" {
  default = 21
}

variable "JENKINS_VERSION" {
  default = "2.555"
}

variable "WAR_URL" {
  default = ""
}

variable "REGISTRY" {
  default = "docker.io"
}

variable "JENKINS_REPO" {
  default = "jenkins/jenkins"
}

variable "LATEST_WEEKLY" {
  default = "false"
}

variable "LATEST_LTS" {
  default = "false"
}

variable "PLUGIN_CLI_VERSION" {
  default = "2.14.0"
}

variable "COMMIT_SHA" {
  default = ""
}

variable "ALPINE_FULL_TAG" {
  default = "3.23.3"
}

variable "ALPINE_SHORT_TAG" {
  default = regex_replace(ALPINE_FULL_TAG, "\\.\\d+$", "")
}

variable "JAVA17_VERSION" {
  default = "17.0.18_8"
}

variable "JAVA21_VERSION" {
  default = "21.0.10_7"
}

variable "JAVA25_VERSION" {
  default = "25.0.2_10"
}

variable "DEBIAN_RELEASE_LINE" {
  default = "trixie"
}

variable "DEBIAN_VERSION" {
  default = "20251117"
}

variable "RHEL_TAG" {
  default = "9.7-1773204657"
}

variable "RHEL_RELEASE_LINE" {
  default = "ubi9"
}

# Set this value to a specific Windows version to override Windows versions to build returned by windowsversions function
variable "WINDOWS_VERSION_OVERRIDE" {
  default = ""
}

## Internal variables
variable "jdk_versions" {
  default = {
    17 = JAVA17_VERSION
    21 = JAVA21_VERSION
    25 = JAVA25_VERSION
  }
}

variable "debian_variants" {
  default = ["debian", "debian-slim"]
}

variable "current_rhel" {
  default = "rhel-${RHEL_RELEASE_LINE}"
}

## Targets
target "alpine" {
  matrix = {
    jdk = jdks_to_build
  }
  name       = "alpine_jdk${jdk}"
  dockerfile = "alpine/hotspot/Dockerfile"
  context    = "."
  args = {
    JENKINS_VERSION    = JENKINS_VERSION
    WAR_URL            = war_url()
    COMMIT_SHA         = COMMIT_SHA
    PLUGIN_CLI_VERSION = PLUGIN_CLI_VERSION
    JAVA_VERSION       = javaversion(jdk)
    ALPINE_TAG         = ALPINE_FULL_TAG
  }
  tags      = linux_tags("alpine", jdk)
  platforms = platforms("alpine", jdk)
}

target "debian" {
  matrix = {
    jdk     = jdks_to_build
    variant = debian_variants
  }
  name       = "${variant}_jdk${jdk}"
  dockerfile = "debian/Dockerfile"
  context    = "."
  args = {
    JENKINS_VERSION     = JENKINS_VERSION
    WAR_URL             = war_url()
    COMMIT_SHA          = COMMIT_SHA
    PLUGIN_CLI_VERSION  = PLUGIN_CLI_VERSION
    JAVA_VERSION        = javaversion(jdk)
    DEBIAN_RELEASE_LINE = DEBIAN_RELEASE_LINE
    DEBIAN_VERSION      = DEBIAN_VERSION
    DEBIAN_VARIANT      = is_debian_slim(variant) ? "-slim" : ""
  }
  tags      = linux_tags(variant, jdk)
  platforms = platforms(variant, jdk)
}

target "rhel" {
  matrix = {
    jdk = jdks_to_build
  }
  name       = "rhel_jdk${jdk}"
  dockerfile = "rhel/Dockerfile"
  context    = "."
  args = {
    JENKINS_VERSION    = JENKINS_VERSION
    WAR_URL            = war_url()
    COMMIT_SHA         = COMMIT_SHA
    PLUGIN_CLI_VERSION = PLUGIN_CLI_VERSION
    JAVA_VERSION       = javaversion(jdk)
    RHEL_TAG           = RHEL_TAG
    RHEL_RELEASE_LINE  = RHEL_RELEASE_LINE
  }
  tags      = linux_tags(current_rhel, jdk)
  platforms = platforms(current_rhel, jdk)
}

target "windowsservercore" {
  matrix = {
    jdk             = jdks_to_build
    windows_version = windowsversions()
  }
  name       = "windowsservercore-${windows_version}_jdk${jdk}"
  dockerfile = "windows/windowsservercore/hotspot/Dockerfile"
  context    = "."
  args = {
    JENKINS_VERSION    = JENKINS_VERSION
    WAR_URL            = war_url()
    COMMIT_SHA         = COMMIT_SHA
    PLUGIN_CLI_VERSION = PLUGIN_CLI_VERSION
    JAVA_VERSION       = javaversion(jdk)
    JAVA_HOME          = "C:/openjdk-${jdk}"
    WINDOWS_VERSION    = windows_version
  }
  tags      = windows_tags("windowsservercore-${windows_version}", jdk)
  platforms = ["windows/amd64"]
}

## Groups
group "linux" {
  targets = [
    "alpine",
    "debian",
    "rhel",
  ]
}

group "windows" {
  targets = [
    "windowsservercore"
  ]
}

group "all" {
  targets = [
    "linux",
    "windows",
  ]
}

## Common functions
# return true if JENKINS_VERSION is a Weekly (one sequence of digits with a trailing literal '.')
function "is_jenkins_version_weekly" {
  # If JENKINS_VERSION has more than one sequence of digits with a trailing literal '.', this is LTS
  # 2.523 has only one sequence of digits with a trailing literal '.'
  # 2.516.1 has two sequences of digits with a trailing literal '.'
  params = []
  result = length(regexall("[0-9]+[.]", JENKINS_VERSION)) < 2 ? true : false
}

# return a tag prefixed by the Jenkins version
function "_tag_jenkins_version" {
  params = [tag]
  result = notequal(tag, "") ? "${REGISTRY}/${JENKINS_REPO}:${JENKINS_VERSION}-${tag}" : "${REGISTRY}/${JENKINS_REPO}:${JENKINS_VERSION}"
}

# return a tag optionally prefixed by the Jenkins version
function "tag" {
  params = [prepend_jenkins_version, tag]
  result = equal(prepend_jenkins_version, true) ? _tag_jenkins_version(tag) : "${REGISTRY}/${JENKINS_REPO}:${tag}"
}

# return a weekly optionally prefixed by the Jenkins version
function "tag_weekly" {
  params = [prepend_jenkins_version, tag]
  result = equal(LATEST_WEEKLY, "true") ? tag(prepend_jenkins_version, tag) : ""
}

# return a LTS optionally prefixed by the Jenkins version
function "tag_lts" {
  params = [prepend_jenkins_version, tag]
  result = equal(LATEST_LTS, "true") ? tag(prepend_jenkins_version, tag) : ""
}

# return WAR_URL if not empty, get.jenkins.io URL depending on JENKINS_VERSION release line otherwise
function "war_url" {
  params = []
  result = (notequal(WAR_URL, "")
    ? WAR_URL
    : (is_jenkins_version_weekly()
      ? "https://get.jenkins.io/war/${JENKINS_VERSION}/jenkins.war"
  : "https://get.jenkins.io/war-stable/${JENKINS_VERSION}/jenkins.war"))
}

# Return "true" if the jdk passed as parameter is the same as the default jdk, "false" otherwise
function "is_default_jdk" {
  params = [jdk]
  result = equal(default_jdk, jdk) ? true : false
}

# Return the complete Java version corresponding to the jdk passed as parameter
function "javaversion" {
  params = [jdk]
  result = lookup(jdk_versions, jdk, "Unsupported JDK version")
}

# Return an array of platforms depending on the distribution and the jdk
function "platforms" {
  params = [distribution, jdk]
  result = (
    # Alpine
    is_alpine(distribution)
    ? (equal(17, jdk)
      ? ["linux/amd64"]
    : ["linux/amd64", "linux/arm64"])

    # Debian slim
    : is_debian_slim(distribution)
    ? (equal(17, jdk)
      ? ["linux/amd64"]
    : ["linux/amd64", "linux/arm64", "linux/riscv64"])

    # RHEL
    : is_rhel(distribution)
    ? ["linux/amd64", "linux/arm64", "linux/ppc64le"]

    # Default (Debian)
    : ["linux/amd64", "linux/arm64", "linux/s390x", "linux/ppc64le", "linux/riscv64"]
  )
}

# Return an array of tags for linux images depending on the distribution and the jdk
function "linux_tags" {
  params = [distribution, jdk]
  result = (
    ## Debian variants
    is_debian_variant(distribution)
    ? debian_tags(distribution, jdk)

    : [
      ## Always publish explicit jdk tag
      tag(true, "${distribution}-jdk${jdk}"),
      tag_weekly(false, "${distribution}-jdk${jdk}"),
      tag_lts(false, "lts-${distribution}-jdk${jdk}"),

      # Special case for Alpine
      is_alpine(distribution) ? tag_weekly(false, "alpine${ALPINE_SHORT_TAG}-jdk${jdk}") : "",

      # Special case for RHEL
      is_rhel(distribution) ? tag_lts(true, "lts-${distribution}-jdk${jdk}") : "",

      ## Default JDK extra short tags (except for current rhel)
      is_default_jdk(jdk) && !is_rhel(distribution) ? tag(true, distribution) : "",
      is_default_jdk(jdk) && !is_rhel(distribution) ? tag_weekly(false, distribution) : "",
      is_default_jdk(jdk) && !is_rhel(distribution) ? tag_lts(false, "lts-${distribution}") : "",
      is_default_jdk(jdk) && !is_rhel(distribution) ? tag_lts(true, "lts-${distribution}") : "",
    ]
  )
}

# Return an array of tags depending on the agent type, the jdk
# and the flavor and version of Windows passed as parameters (ex: windowsservercore-ltsc2022)
function "windows_tags" {
  params = [distribution, jdk]
  result = [
    ## Always publish explicit jdk tag
    tag(true, "jdk${jdk}-hotspot-${distribution}"),
    tag_weekly(false, "jdk${jdk}-hotspot-${distribution}"),
    tag_lts(false, "lts-jdk${jdk}-hotspot-${distribution}"),

    ## Default JDK extra short tags
    is_default_jdk(jdk) ? tag(true, "hotspot-${distribution}") : "",
    is_default_jdk(jdk) ? tag_weekly(false, distribution) : "",
    is_default_jdk(jdk) ? tag_weekly(true, distribution) : "",
    is_default_jdk(jdk) ? tag_lts(false, "lts-${distribution}") : "",
    is_default_jdk(jdk) ? tag_lts(true, distribution) : "",
  ]
}

# Return if the distribution passed in parameter is Alpine
function "is_alpine" {
  params = [distribution]
  result = equal("alpine", distribution)
}

# Return if the distribution passed in parameter is Alpine
function "is_rhel" {
  params = [distribution]
  result = equal(current_rhel, distribution)
}

# Return if the distribution passed in parameter is a debian variant
function "is_debian_variant" {
  params = [distribution]
  result = contains(debian_variants, distribution)
}

# Return if the variant passed in parameter is the debian slim one
function "is_debian_slim" {
  params = [variant]
  result = equal("debian-slim", variant)
}

# Return text prefixed with "slim-" if the variant passed in parameter is the slim one
# Return only "slim" if the text passed in parameter is empty or "latest"
function "slim_prefix" {
  params = [variant, text]
  result = (is_debian_slim(variant)
    ? (equal("", text) || equal("latest", text) ? "slim" : "slim-${text}")
  : text)
}

# Return text suffixed with "-slim" if the variant passed in parameter is the slim one
# Return only "slim" if the text passed in parameter is empty
function "slim_suffix" {
  params = [variant, text]
  result = (is_debian_slim(variant)
    ? (equal("", text) ? "slim" : "${text}-slim")
  : text)
}

# Return an array of tags for debian images depending on the variant and the jdk passed as parameters
function "debian_tags" {
  params = [variant, jdk]
  result = [
    ## Default tags including jdk
    tag(true, slim_prefix(variant, "jdk${jdk}")),
    tag_weekly(false, slim_prefix(variant, "jdk${jdk}")),
    tag_lts(false, "${slim_suffix(variant, "lts")}-jdk${jdk}"),
    # Tags for debian only
    is_debian_slim(variant) ? "" : tag_weekly(false, slim_prefix(variant, "latest-jdk${jdk}")),
    is_debian_slim(variant) ? "" : tag_lts(true, "${slim_suffix(variant, "lts")}-jdk${jdk}"),

    ## If default jdk, short tags
    is_default_jdk(jdk) ? tag(true, slim_prefix(variant, "")) : "",
    is_default_jdk(jdk) ? tag_weekly(false, slim_prefix(variant, "latest")) : "",
    is_default_jdk(jdk) ? tag_lts(false, slim_suffix(variant, "lts")) : "",
    is_default_jdk(jdk) ? tag_lts(true, slim_suffix(variant, "lts")) : "",
  ]
}

# Return array of Windows version(s) to build
# Can be overridden by setting WINDOWS_VERSION_OVERRIDE to a specific Windows version
# Ex: WINDOWS_VERSION_OVERRIDE=ltsc2025 docker buildx bake windows
function "windowsversions" {
  params = []
  result = notequal(WINDOWS_VERSION_OVERRIDE, "") ? [WINDOWS_VERSION_OVERRIDE] : windows_version_to_build
}


================================================
FILE: jdk-download-url.sh
================================================
#!/bin/sh

# Check if at least one argument was passed to the script
# If one argument was passed and JAVA_VERSION is set, assign the argument to OS
# If two arguments were passed, assign them to JAVA_VERSION and OS respectively
# If three arguments were passed, assign them to JAVA_VERSION, OS and ARCHS respectively
# If not, check if JAVA_VERSION and OS are already set. If they're not set, exit the script with an error message
if [ $# -eq 1 ] && [ -n "$JAVA_VERSION" ]; then
    OS=$1
elif [ $# -eq 2 ]; then
    JAVA_VERSION=$1
    OS=$2
elif [ $# -eq 3 ]; then
    JAVA_VERSION=$1
    OS=$2
    ARCHS=$3
elif [ -z "$JAVA_VERSION" ] && [ -z "$OS" ]; then
    echo "Error: No Java version and OS specified. Please set the JAVA_VERSION and OS environment variables or pass them as arguments." >&2
    exit 1
elif [ -z "$JAVA_VERSION" ]; then
    echo "Error: No Java version specified. Please set the JAVA_VERSION environment variable or pass it as an argument." >&2
    exit 1
elif [ -z "$OS" ]; then
    OS=$1
    if [ -z "$OS" ]; then
        echo "Error: No OS specified. Please set the OS environment variable or pass it as an argument." >&2
        exit 1
    fi
fi

# Check if ARCHS is set. If it's not set, assign the current architecture to it
if [ -z "$ARCHS" ]; then
    ARCHS=$(uname -m | sed -e 's/x86_64/x64/' -e 's/armv7l/arm/')
else
    # Convert ARCHS to an array
    OLD_IFS=$IFS
    IFS=','
    set -- "$ARCHS"
    ARCHS=""
    for arch in "$@"; do
        ARCHS="$ARCHS $arch"
    done
    IFS=$OLD_IFS
fi

# Check if jq and curl are installed
# If they are not installed, exit the script with an error message
if ! command -v jq >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
    echo "jq and curl are required but not installed. Exiting with status 1." >&2
    exit 1
fi

# Replace underscores with plus signs in JAVA_VERSION
ARCHIVE_DIRECTORY=$(echo "$JAVA_VERSION" | tr '_' '+')

# URL encode ARCHIVE_DIRECTORY
ENCODED_ARCHIVE_DIRECTORY=$(echo "$ARCHIVE_DIRECTORY" | xargs -I {} printf %s {} | jq "@uri" -jRr)

# Determine the OS type for the URL
OS_TYPE="linux"
if [ "$OS" = "alpine" ]; then
    OS_TYPE="alpine-linux"
fi
if [ "$OS" = "windows" ]; then
    OS_TYPE="windows"
fi

# Initialize a variable to store the URL for the first architecture
FIRST_ARCH_URL=""

# Loop over the array of architectures
for ARCH in $ARCHS; do
    # Fetch the download URL from the Adoptium API
    URL="https://api.adoptium.net/v3/binary/version/jdk-${ENCODED_ARCHIVE_DIRECTORY}/${OS_TYPE}/${ARCH}/jdk/hotspot/normal/eclipse?project=jdk"

    if ! RESPONSE=$(curl -fsI "$URL"); then
        echo "Error: Failed to fetch the URL for architecture ${ARCH} from ${URL}. Exiting with status 1." >&2
        echo "Response: $RESPONSE" >&2
        exit 1
    fi

    # Extract the redirect URL from the HTTP response
    REDIRECTED_URL=$(echo "$RESPONSE" | grep -i location | awk '{print $2}' | tr -d '\r')

    # If no redirect URL was found, exit the script with an error message
    if [ -z "$REDIRECTED_URL" ]; then
        echo "Error: No redirect URL found for architecture ${ARCH} from ${URL}. Exiting with status 1." >&2
        echo "Response: $RESPONSE" >&2
        exit 1
    fi

    # Use curl to check if the URL is reachable
    # If the URL is not reachable, print an error message and exit the script with status 1
    if ! curl -v -fs "$REDIRECTED_URL" >/dev/null 2>&1; then
        echo "${REDIRECTED_URL}" is not reachable for architecture "${ARCH}". >&2
        exit 1
    fi

    # If FIRST_ARCH_URL is empty, store the current URL
    if [ -z "$FIRST_ARCH_URL" ]; then
        FIRST_ARCH_URL=$REDIRECTED_URL
    fi
done

# If all downloads are successful, print the URL for the first architecture
echo "$FIRST_ARCH_URL"


================================================
FILE: jdk-download.sh
================================================
#!/bin/sh
set -x
# Check if curl and tar are installed
if ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1 ; then
    echo "curl and tar are required but not installed. Exiting with status 1." >&2
    exit 1
fi

# Set the OS to "standard" by default
OS="standard"

# If a second argument is provided, use it as the OS
if [ $# -eq 1 ]; then
    OS=$1
fi

# Call jdk-download-url.sh with JAVA_VERSION and OS as arguments
# The two scripts should be in the same directory.
# That's why we're trying to find the directory of the current script and use it to call the other script.
SCRIPT_DIR=$(cd "$(dirname "$0")" || exit; pwd)
if ! DOWNLOAD_URL=$("${SCRIPT_DIR}"/jdk-download-url.sh "${JAVA_VERSION}" "${OS}"); then
    echo "Error: Failed to fetch the URL. Exiting with status 1." >&2
    exit 1
fi

# Use curl to download the JDK archive from the URL
if ! curl --silent --location --output /tmp/jdk.tar.gz "${DOWNLOAD_URL}"; then
    echo "Error: Failed to download the JDK archive. Exiting with status 1." >&2
    exit 1
fi

# Extract the archive to the /opt/ directory
if ! tar -xzf /tmp/jdk.tar.gz -C /opt/; then
    echo "Error: Failed to extract the JDK archive. Exiting with status 1." >&2
    exit 1
fi

# Get the name of the extracted directory
EXTRACTED_DIR=$(tar -tzf /tmp/jdk.tar.gz | head -n 1 | cut -f1 -d"/")

# Rename the extracted directory to /opt/jdk-${JAVA_VERSION}
if ! mv "/opt/${EXTRACTED_DIR}" "/opt/jdk-${JAVA_VERSION}"; then
    echo "Error: Failed to rename the extracted directory. Exiting with status 1." >&2
    exit 1
fi

# Remove the downloaded archive
if ! rm -f /tmp/jdk.tar.gz; then
    echo "Error: Failed to remove the downloaded archive. Exiting with status 1." >&2
    exit 1
fi


================================================
FILE: jenkins-plugin-cli.ps1
================================================
& java "$env:JAVA_OPTS" -jar C:/ProgramData/Jenkins/jenkins-plugin-manager.jar $args

================================================
FILE: jenkins-plugin-cli.sh
================================================
#!/bin/bash

# read JAVA_OPTS into array to avoid need for eval (and associated vulnerabilities)
java_opts_array=()
while IFS= read -r -d '' item; do
	java_opts_array+=( "$item" )
done < <([[ $JAVA_OPTS ]] && xargs printf '%s\0' <<<"$JAVA_OPTS")

exec java "${java_opts_array[@]}" -jar /opt/jenkins-plugin-manager.jar "$@"


================================================
FILE: jenkins-support
================================================
#!/bin/bash -eu

: "${REF:="/usr/share/jenkins/ref"}"

# compare if version1 < version2
versionLT() {
    local normalized_version1 normalized_version2 first_part_of_1 first_char_other_part_of_1 first_part_of_2

    # Quick check for equality
    if [ "$1" = "$2" ]; then
        return 1
    fi

    # Convert '-' to '.' to ease comparison
    normalized_version1=$(echo "$1" | tr '-' '.')
    normalized_version2=$(echo "$2" | tr '-' '.')
    first_part_of_1=${normalized_version1%%.*}
    other_part_of_1=${normalized_version1#*.}
    first_char_other_part_of_1=${other_part_of_1:0:1}
    first_part_of_2=${normalized_version2%%.*}


    # Security fix backport special case
    # Ex: 3894.vd0f0248b_a_fc4 < 3894.3896.vca_2c931e7935
    # -> normal incrementals version includes a "v" as first char of second part
    # -> security fix backport adds the backport source first part as second part
    # https://github.com/jenkinsci/workflow-cps-plugin/releases/tag/3894.vd0f0248b_a_fc4
    # https://github.com/jenkinsci/workflow-cps-plugin/releases/tag/3894.3896.vca_2c931e7935
    if [[ "$first_part_of_1" = "$first_part_of_2" ]]; then
        # If the second part of $version1 starts with a "v", then $version1 is older
        if [[ "$first_char_other_part_of_1" == "v" ]]; then
            return 0
        fi
    fi

    if [ "$normalized_version1" = "$(printf '%s\n%s\n' "$normalized_version1" "$normalized_version2" | sort --version-sort | head -n1)" ]; then
        return 0
    else
        return 1
    fi
}

# returns a plugin version from a plugin archive
get_plugin_version() {
    local archive; archive=$1
    local version; version=$(unzip -p "$archive" META-INF/MANIFEST.MF | grep "^Plugin-Version: " | sed -e 's#^Plugin-Version: ##')
    version=${version%%[[:space:]]}
    echo "$version"
}

# Copy files from /usr/share/jenkins/ref into $JENKINS_HOME
# So the initial JENKINS-HOME is set with expected content.
# Don't override, as this is just a reference setup, and use from UI
# can then change this, upgrade plugins, etc.
copy_reference_file() {
    f="${1%/}"
    b="${f%.override}"
    rel="${b#"$REF/"}"
    version_marker="${rel}.version_from_image"
    dir=$(dirname "${rel}")
    local action;
    local reason;
    local container_version;
    local image_version;
    local marker_version;
    local log; log=false
    if [[ ${rel} == plugins/*.jpi ]]; then
        container_version=$(get_plugin_version "$JENKINS_HOME/${rel}")
        image_version=$(get_plugin_version "${f}")
        if [[ -e $JENKINS_HOME/${version_marker} ]]; then
            marker_version=$(cat "$JENKINS_HOME/${version_marker}")
            if versionLT "$marker_version" "$container_version"; then
                if ( versionLT "$container_version" "$image_version" && [[ -n $PLUGINS_FORCE_UPGRADE ]]); then
                    action="UPGRADED"
                    reason="Manually upgraded version ($container_version) is older than image version $image_version"
                    log=true
                else
                    action="SKIPPED"
                    reason="Installed version ($container_version) has been manually upgraded from initial version ($marker_version)"
                    log=true
                fi
            else
                if [[ "$image_version" == "$container_version" ]]; then
                    action="SKIPPED"
                    reason="Version from image is the same as the installed version $image_version"
                else
                    if versionLT "$image_version" "$container_version"; then
                        action="SKIPPED"
                        log=true
                        reason="Image version ($image_version) is older than installed version ($container_version)"
                    else
                        action="UPGRADED"
                        log=true
                        reason="Image version ($image_version) is newer than installed version ($container_version)"
                    fi
                fi
            fi
        else
            if [[ -n "$TRY_UPGRADE_IF_NO_MARKER" ]]; then
                if [[ "$image_version" == "$container_version" ]]; then
                    action="SKIPPED"
                    reason="Version from image is the same as the installed version $image_version (no marker found)"
                    # Add marker for next time
                    echo "$image_version" > "$JENKINS_HOME/${version_marker}"
                else
                    if versionLT "$image_version" "$container_version"; then
                        action="SKIPPED"
                        log=true
                        reason="Image version ($image_version) is older than installed version ($container_version) (no marker found)"
                    else
                        action="UPGRADED"
                        log=true
                        reason="Image version ($image_version) is newer than installed version ($container_version) (no marker found)"
                    fi
                fi
            fi
        fi
        if [[ ! -e $JENKINS_HOME/${rel} || "$action" == "UPGRADED" || $f = *.override ]]; then
            action=${action:-"INSTALLED"}
            log=true
            mkdir -p "$JENKINS_HOME/${dir}"
            cp -pr "${f}" "$JENKINS_HOME/${rel}";
            # pin plugins on initial copy
            touch "$JENKINS_HOME/${rel}.pinned"
            echo "$image_version" > "$JENKINS_HOME/${version_marker}"
            reason=${reason:-$image_version}
        else
            action=${action:-"SKIPPED"}
        fi
    else
        if [[ ! -e $JENKINS_HOME/${rel} || $f = *.override ]]
        then
            action="INSTALLED"
            log=true
            mkdir -p "$JENKINS_HOME/${dir}"
            cp -pr "$(realpath "${f}")" "$JENKINS_HOME/${rel}";
        else
            action="SKIPPED"
        fi
    fi
    if [[ -n "$VERBOSE" || "$log" == "true" ]]; then
        if [ -z "$reason" ]; then
            echo "$action $rel" >> "$COPY_REFERENCE_FILE_LOG"
        else
            echo "$action $rel : $reason" >> "$COPY_REFERENCE_FILE_LOG"
        fi
    fi
}


================================================
FILE: jenkins-support.psm1
================================================

# compare if version1 < version2
function Compare-VersionLessThan([string] $version1 = '', [string] $version2 = '') {
    # Quick check for equality
    if($version1 -eq $version2) {
        return $false
    }

    # Convert '-' to '.' to ease comparison
    $normalizedVersion1 = $version1 -replace '-', '.'
    $normalizedVersion2 = $version2 -replace '-', '.'

    $version1Parts = $normalizedVersion1.Split('.')
    $version2Parts = $normalizedVersion2.Split('.')

    # Compare major versions
    if ($version1Parts[0] -lt $version2parts[0]) {
        return $true
    }
    if ($version1Parts[0] -gt $version2parts[0]) {
        return $false
    }

    $maxLength = [Math]::Max($version1Parts.Length, $version2parts.Length)
    # First parts are equal, compare subsequent parts
    for ($i = 1; $i -lt $maxLength; $i++) {
        $version1part = if ($i -lt $version1Parts.Length) { $version1Parts[$i] } else { '0' }
        $version2part = if ($i -lt $version2Parts.Length) { $version2Parts[$i] } else { '0' }

        if ($version1part -eq $version2part) {
            continue
        }

        # Security fix backport special case
        # Ex: 3894.vd0f0248b_a_fc4 < 3894.3896.vca_2c931e7935
        # -> normal incrementals version includes a "v" as first char of second part
        # -> security fix backport adds the backport source first part as second part
        # https://github.com/jenkinsci/workflow-cps-plugin/releases/tag/3894.vd0f0248b_a_fc4
        # https://github.com/jenkinsci/workflow-cps-plugin/releases/tag/3894.3896.vca_2c931e7935
        # If only the nth part of $version1 starts with a "v", then $version1 is older
        if ($version1part.Substring(0,1) -eq 'v' -and $version2part.Substring(0,1) -ne 'v') {
            return $true
        }

        # Try numeric comparison first, fall back to string comparison
        $num1 = 0
        $num2 = 0
        $isNum1 = [int]::TryParse($version1part, [ref]$num1)
        $isNum2 = [int]::TryParse($version2part, [ref]$num2)

        if ($isNum1 -and $isNum2) {
            # Both are numeric, compare as integers
            return ($num1 -lt $num2)
        } else {
            # At least one is not numeric, use string comparison
            return ($version1part -lt $version2part)
        }
    }
}

function Get-EnvOrDefault($name, $def) {
    $entry = Get-ChildItem env: | Where-Object { $_.Name -eq $name } | Select-Object -First 1
    if(($null -ne $entry) -and ![System.String]::IsNullOrWhiteSpace($entry.Value)) {
        return $entry.Value
    }
    return $def
}

function Expand-Zip($archive, $file) {
    # load ZIP methods
    Add-Type -AssemblyName System.IO.Compression.FileSystem

    Write-Verbose "Unzipping $file from $archive"

    $contents = ""

    if(Test-Path $archive) {
        # open ZIP archive for reading
        $zip = [System.IO.Compression.ZipFile]::OpenRead($archive)

        if($null -ne $zip) {
            $entry = $zip.GetEntry($file)
            if($null -ne $entry) {
                $reader = New-Object -TypeName System.IO.StreamReader -ArgumentList $entry.Open()
                $contents = $reader.ReadToEnd()
                $reader.Dispose()
            }

            # close ZIP file
            $zip.Dispose()
        }
    }

    return $contents
}

# returns a plugin version from a plugin archive
function Get-PluginVersion($archive) {
    $archive = $archive.Trim()
    Write-Verbose "Getting plugin version for $archive"
    if(-not (Test-Path $archive)) {
        return ""
    }

    $version = Expand-Zip $archive "META-INF/MANIFEST.MF" | ForEach-Object {$_ -split "`n"} | Select-String -Pattern "^Plugin-Version:\s+" | ForEach-Object {$_ -replace "^Plugin-Version:\s+(.*)", '$1'} | Select-Object -First 1 | Out-String
    return $version.Trim()
}

# Copy files from C:/ProgramData/Jenkins/Reference/ into $JENKINS_HOME
# So the initial JENKINS-HOME is set with expected content.
# Don't override, as this is just a reference setup, and use from UI
# can then change this, upgrade plugins, etc.
function Copy-ReferenceFile($file) {
    $action = ""
    $reason = ""
    $log = $false
    $refDir = Get-EnvOrDefault 'REF' 'C:/ProgramData/Jenkins/Reference'

    if(-not (Test-Path $refDir)) {
        return
    }

    Push-Location $refDir
    $rel = Resolve-Path -Relative -Path $file
    Pop-Location
    $dir = Split-Path -Parent $rel

    if($file -match "plugins[\\/].*\.jpi") {
        $fileName = Split-Path -Leaf $file
        $versionMarker = (Join-Path $env:JENKINS_HOME (Join-Path "plugins" "${fileName}.version_from_image"))
        $containerVersion = Get-PluginVersion (Join-Path $env:JENKINS_HOME $rel)
        $imageVersion = Get-PluginVersion $file
        if(Test-Path $versionMarker) {
            $markerVersion = (Get-Content -Raw $versionMarker).Trim()
            if(Compare-VersionLessThan $markerVersion $containerVersion) {
                if((Compare-VersionLessThan $containerVersion $imageVersion) -and ![System.String]::IsNullOrWhiteSpace($env:PLUGINS_FORCE_UPGRADE)) {
                    $action = "UPGRADED"
                    $reason="Manually upgraded version ($containerVersion) is older than image version $imageVersion"
                    $log=$true
                } else {
                    $action="SKIPPED"
                    $reason="Installed version ($containerVersion) has been manually upgraded from initial version ($markerVersion)"
                    $log=$true
                }
            } else {
                if($imageVersion -eq $containerVersion) {
                    $action = "SKIPPED"
                    $reason = "Version from image is the same as the installed version $imageVersion"
                } else {
                    if(Compare-VersionLessThan $imageVersion $containerVersion) {
                        $action = "SKIPPED"
                        $log = $true
                        $reason = "Image version ($imageVersion) is older than installed version ($containerVersion)"
                    } else {
                        $action="UPGRADED"
                        $log=$true
                        $reason="Image version ($imageVersion) is newer than installed version ($containerVersion)"
                    }
                }
            }
        } else {
            if(![System.String]::IsNullOrWhiteSpace($env:TRY_UPGRADE_IF_NO_MARKER)) {
                if($imageVersion -eq $containerVersion) {
                    $action = "SKIPPED"
                    $reason = "Version from image is the same as the installed version $imageVersion (no marker found)"
                    # Add marker for next time
                    Add-Content -Path $versionMarker -Value $imageVersion
                } else {
                    if(Compare-VersionLessThan $imageVersion $containerVersion) {
                        $action = "SKIPPED"
                        $log = $true
                        $reason = "Image version ($imageVersion) is older than installed version ($containerVersion) (no marker found)"
                    } else {
                        $action = "UPGRADED"
                        $log = $true
                        $reason = "Image version ($imageVersion) is newer than installed version ($containerVersion) (no marker found)"
                    }
                }
            }
        }

        if((-not (Test-Path (Join-Path $env:JENKINS_HOME $rel))) -or ($action -eq "UPGRADED") -or ($file -match "\.override")) {
            if([System.String]::IsNullOrWhiteSpace($action)) {
                $action = "INSTALLED"
            }
            $log=$true

            if(-not (Test-Path (Join-Path $env:JENKINS_HOME $dir))) {
                New-Item -ItemType Directory -Path (Join-Path $env:JENKINS_HOME $dir)
            }
            Copy-Item $file (Join-Path $env:JENKINS_HOME $rel)
            # pin plugins on initial copy
            Write-Output $null >> (Join-Path $env:JENKINS_HOME "${rel}.pinned")
            Add-Content -Path $versionMarker -Value $imageVersion
            if([System.String]::IsNullOrWhiteSpace($reason)) {
                $reason = $imageVersion
            }
        } else {
            if([System.String]::IsNullOrWhiteSpace($action)) {
                $action = "SKIPPED"
            }
        }
    } else {
        if((-not (Test-Path (Join-Path $env:JENKINS_HOME $rel))) -or ($file -match "\.override")) {
            $action = "INSTALLED"
            $log = $true
            if(-not (Test-Path (Join-Path $env:JENKINS_HOME (Split-Path -Parent $rel)))) {
                New-Item -ItemType Directory (Join-Path $env:JENKINS_HOME (Split-Path -Parent $rel))
            }
            Copy-Item $file (Join-Path $env:JENKINS_HOME $rel)
        } else {
            $action="SKIPPED"
        }
    }

    if(![System.String]::IsNullOrWhiteSpace($env:VERBOSE) -or $log) {
        if([System.String]::IsNullOrWhiteSpace($reason)) {
            Add-Content -Path $COPY_REFERENCE_FILE_LOG -Value "$action $rel"
        } else {
            Add-Content -Path $COPY_REFERENCE_FILE_LOG -Value "$action $rel : $reason"
        }
    }
}

================================================
FILE: jenkins.io-2026.key
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBGlJRoMBEADGTw4Jms5rD1Wd0evqpTkNBgAIvCzvsjgGXHevmNIsDmm/niiE
gKJlrl73T9d8GZeoacsAqwGTIq29ZA1jEt1lUZ8YkVxD3VxoL0RBhgMcy3qhiu37
mQN1mzuItob8P2pft5pPqCWQDojXRpnMB/BTHgbtIH3i4chKVLJoCEX/Gw7abDbj
cUpoKMTByd0+Zv2OBtdm7ZOYXHObPmSqRoiYNiCsW3mZRsgN1LkwTl5IwJQ7Xpj8
9J4DK1Y6Fuyxi+QTbZk9Z3inrTx3pbARPd91MylIsOtuXkUFNQkA/ZWnKHTFgWQA
qx//KrsCKLe6r3+CQ4/1R4F7jHjBB01qHrxofEzGo0LB/+QNwf1ISqD7piw20IMt
vhlOqdsF2MQQAeyg8fv4nuLglI9ueh4T5FJabp6oL0QDozx1toa5Q58n0nX8gSBq
3VTd8FkzTTsaihyypWmzbdVPwAAfXhRh7sNAUvALkq4vj/EWjPruQElWyP8DwmiC
Aq8iduFb66oN58vlT1rf3z/jJH3FeByVEHEymz4E9rhBN1oOUQ++ONqCMOZHwnpY
K549A+mHrK12RDQTYjgbi9BH2ktPqPUE37rZDoGN9hzZ9dqG8dMEEz5qVMzsGhuw
nm1d86yQRUzscHwgPELc7xiIuV3taLf2KI4qSHTDmq6nRFxcgKI2LGFfcwARAQAB
tDJKZW5raW5zIFByb2plY3QgPGplbmtpbnNjaS1ib2FyZEBnb29nbGVncm91cHMu
Y29tPokCVwQTAQgAQRYhBF44bq21XwFQTK6Lz3GY9LcUq/xoBQJpSUaDAhsDBQkF
o5qABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEHGY9LcUq/xouboP/1Zd
KxZXkTj20jnBn8MJ9scr17wzGLy2/EaAelbfeIYmsWJ6A7ZuuUw/41dUbTuI3k3D
Ta1Ft0oO5K63sJqvTQzUdas6x3HMsjYSo+YtbRZnMmR/KO4//5Lewm3LPQnCV662
8ZI73T22msQAbyxa8do56dmBT4N/NO6oGFZI6JBFnkiIlXmKDzm3aiEZi//piN3X
PZgtu8wHqpFleJXUbCpk8Db69xTjdXhnFpaYg29VrzvD/0jBEZE47Bekrl6YgjJ8
CKyhaPWZfxYxNeuVRTn+yxlAcDc8o9tboSKnlZ8HSOBPbf36qmLKbD4rPQmTAVgJ
hwBY2mxDUT5hTVom25KeyueIyN4l6OZEoLxcq5GxN85RkU2Zfq1jodpnm/PnF47Y
7qS4zu8bOOeUCFpJXG3kDYo34tkFKk5CT8PJLHdjgLWGvhQeL95ytPvrTLkEj4yk
6SXHH4EcKimgi0c/zotnzv997kGCpoMZoeIXpkhrTJoZvSQqFpeCamFRwl/AfM/l
ppyH905Cm/GcB+W0hQqTsA0wm+6ZQn4fAR/rhqRk4Ka1TuX2ow3OQKlyoA4EgvdI
41MQEw4y9spjH2RgyJpOAgIagidECrFJbqNcyzHUZUxcD7fKMRaiv5LepxVLXZ0/
XDDBGd3AXh6nv2BTDhoE+ZI1suWZAMwvxyoFDDFO
=8CuH
-----END PGP PUBLIC KEY BLOCK-----


================================================
FILE: jenkins.ps1
================================================
Import-Module -Force -DisableNameChecking C:/ProgramData/Jenkins/jenkins-support.psm1

$JENKINS_WAR = Get-EnvOrDefault 'JENKINS_WAR' 'C:/ProgramData/Jenkins/jenkins.war'
$JENKINS_HOME = Get-EnvOrDefault 'JENKINS_HOME' 'C:/ProgramData/Jenkins/JenkinsHome'
$COPY_REFERENCE_FILE_LOG = Get-EnvOrDefault 'COPY_REFERENCE_FILE_LOG' "$($JENKINS_HOME)/copy_reference_file.log"

try {
  [System.IO.File]::OpenWrite($COPY_REFERENCE_FILE_LOG).Close()
} catch {
  Write-Error "Can not write to $COPY_REFERENCE_FILE_LOG. Wrong volume permissions?`n`n$_"
  exit 1
}

Add-Content -Path $COPY_REFERENCE_FILE_LOG -Value "--- Copying files at $(Get-Date)"
Get-ChildItem -Recurse -File -Path 'C:/ProgramData/Jenkins/Reference' | ForEach-Object { Copy-ReferenceFile $_.FullName }
Add-Content -Path $COPY_REFERENCE_FILE_LOG -Value "--- Copied files finished at $(Get-Date)"

# if `docker run` first argument starts with `--` the user is passing jenkins launcher arguments
if(($args.Count -eq 0) -or ($args[0] -match "^--.*")) {

  # read JAVA_OPTS and JENKINS_OPTS into arrays to avoid need for eval (and associated vulnerabilities)
  $java_opts_array = ($env:JAVA_OPTS -split ' ') + ($env:JENKINS_JAVA_OPTS -split ' ')

  $agent_port_property='jenkins.model.Jenkins.slaveAgentPort'
  if(![System.String]::IsNullOrWhiteSpace($env:JENKINS_AGENT_PORT) -and !($java_opts_array -match "$agent_port_property")) {
    $java_opts_array += "-D`"$agent_port_property=$env:JENKINS_AGENT_PORT`""
  }

  if($null -ne $env:DEBUG) {
    $java_opts_array += '-Xdebug'
    $java_opts_array += '-Xrunjdwp:server=y,transport=dt_socket,address=5005,suspend=y'
  }

  $jenkins_opts_array = $env:JENKINS_OPTS -split ' '
  $proc = Start-Process -NoNewWindow -Wait -PassThru -FilePath 'java.exe' -ArgumentList "-D`"user.home=$JENKINS_HOME`" $java_opts_array -jar $JENKINS_WAR $jenkins_opts_array $args"
  if($null -ne $proc) {
    $proc.WaitForExit()
  }
} else {
  # As argument is not jenkins, assume user wants to run their own process, for example a `powershell` shell to explore this image
  Invoke-Expression "$args"
  exit $lastExitCode
}


================================================
FILE: jenkins.sh
================================================
#! /bin/bash -e

: "${JENKINS_WAR:="/usr/share/jenkins/jenkins.war"}"
: "${JENKINS_HOME:="/var/jenkins_home"}"

if [[ -n "${PRE_CLEAR_INIT_GROOVY_D}" ]]; then
  rm -rf "${JENKINS_HOME}/init.groovy.d"
fi

: "${COPY_REFERENCE_FILE_LOG:="${JENKINS_HOME}/copy_reference_file.log"}"
: "${REF:="/usr/share/jenkins/ref"}"

if ! [ -r "${JENKINS_HOME}" ] || ! [ -w "${JENKINS_HOME}" ]; then
        echo "INSTALL WARNING: User: ${USER} missing rw permissions on JENKINS_HOME: ${JENKINS_HOME}"
fi
touch "${COPY_REFERENCE_FILE_LOG}" || { echo "Can not write to ${COPY_REFERENCE_FILE_LOG}. Wrong volume permissions?"; exit 1; }
echo "--- Copying files at $(date)" >> "$COPY_REFERENCE_FILE_LOG"
find "${REF}" \( -type f -o -type l \) -exec bash -c '. /usr/local/bin/jenkins-support; for arg; do copy_reference_file "$arg"; done' _ {} +
echo "--- Copied files finished at $(date)" >> "$COPY_REFERENCE_FILE_LOG"

# if `docker run` first argument start with `--` the user is passing jenkins launcher arguments
if [[ $# -lt 1 ]] || [[ "$1" == "--"* ]]; then

  # shellcheck disable=SC2001
  effective_java_opts=$(sed -e 's/^ $//' <<<"$JAVA_OPTS $JENKINS_JAVA_OPTS")

  # read JAVA_OPTS and JENKINS_OPTS into arrays to avoid need for eval (and associated vulnerabilities)
  java_opts_array=()
  while IFS= read -r -d '' item; do
    java_opts_array+=( "$item" )
  done < <([[ $effective_java_opts ]] && xargs printf '%s\0' <<<"$effective_java_opts")

  readonly agent_port_property='jenkins.model.Jenkins.slaveAgentPort'
  if [ -n "${JENKINS_SLAVE_AGENT_PORT:-}" ] && [[ "${effective_java_opts:-}" != *"${agent_port_property}"* ]]; then
    java_opts_array+=( "-D${agent_port_property}=${JENKINS_SLAVE_AGENT_PORT}" )
  fi

  readonly lifecycle_property='hudson.lifecycle'
  if [[ "${JAVA_OPTS:-}" != *"${lifecycle_property}"* ]]; then
    java_opts_array+=( "-D${lifecycle_property}=hudson.lifecycle.ExitLifecycle" )
  fi

  if [[ "$DEBUG" ]] ; then
    java_opts_array+=( \
      '-Xdebug' \
      '-Xrunjdwp:server=y,transport=dt_socket,address=*:5005,suspend=y' \
    )
  fi

  jenkins_opts_array=( )
  while IFS= read -r -d '' item; do
    jenkins_opts_array+=( "$item" )
  done < <([[ $JENKINS_OPTS ]] && xargs printf '%s\0' <<<"$JENKINS_OPTS")

  exec java -Duser.home="$JENKINS_HOME" "${java_opts_array[@]}" -jar "${JENKINS_WAR}" "${jenkins_opts_array[@]}" "$@"
fi

# As argument is not jenkins, assume user wants to run a different process, for example a `bash` shell to explore this image
exec "$@"


================================================
FILE: make.ps1
================================================
[CmdletBinding()]
Param(
    [Parameter(Position = 1)]
    # Default script target
    [String] $Target = 'build',
    # Jenkins version to include
    [String] $JenkinsVersion = '2.555',
    # Windows flavor and windows version to build
    [String] $ImageType = 'windowsservercore-ltsc2022',
    # Generate a docker compose file even if it already exists
    [switch] $OverwriteDockerComposeFile = $false,
    # Print the build and publish command instead of executing them if set
    [switch] $DryRun = $false,
    # Output debug info for tests: 'empty' (no additional test output), 'debug' (test cmd & stderr output), 'verbose' (test cmd, stderr, stdout output)
    [String] $TestsDebug = ''
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue' # Disable Progress bar for faster downloads

$Repository = 'jenkins'
$Organisation = 'jenkins'

if(![String]::IsNullOrWhiteSpace($env:DOCKERHUB_REPO)) {
    $Repository = $env:DOCKERHUB_REPO
}

if(![String]::IsNullOrWhiteSpace($env:DOCKERHUB_ORGANISATION)) {
    $Organisation = $env:DOCKERHUB_ORGANISATION
}

if(![String]::IsNullOrWhiteSpace($env:JENKINS_VERSION)) {
    $JenkinsVersion = $env:JENKINS_VERSION
}

if(![String]::IsNullOrWhiteSpace($env:IMAGE_TYPE)) {
    $ImageType = $env:IMAGE_TYPE
}

$env:DOCKERHUB_ORGANISATION = "$Organisation"
$env:DOCKERHUB_REPO = "$Repository"
$env:JENKINS_VERSION = "$JenkinsVersion"
$env:COMMIT_SHA = git rev-parse HEAD

# Check for required commands
Function Test-CommandExists {
    Param (
        [String] $command
    )

    $oldPreference = $ErrorActionPreference
    $ErrorActionPreference = 'stop'
    try {
        # Special case to test "docker buildx"
        if ($command.Contains(' ')) {
            Invoke-Expression $command | Out-Null
            Write-Debug "$command exists"
        } else {
            if(Get-Command $command){
                Write-Debug "$command exists"
            }
        }
    }
    Catch {
        "$command does not exist"
    }
    Finally {
        $ErrorActionPreference = $oldPreference
    }
}

function Test-Image {
    param (
        [String] $ImageName
    )

    Write-Host "= TEST: Received ${ImageName} image name"

    $items = $ImageName.split(':')
    $orgRepo = $items[0] -replace 'docker.io/', ''
    $tag = $items[1]

    Write-Host "= TEST: Testing ${tag} tag of ${orgRepo} repository"

    $env:DOCKERHUB_ORG_REPO = $orgRepo
    $env:CONTROLLER_TAG = $tag

    $targetPath = '.\target\{0}' -f $tag
    if (Test-Path $targetPath) {
        Remove-Item -Recurse -Force $targetPath
    }
    New-Item -Path $targetPath -Type Directory | Out-Null
    $configuration.TestResult.OutputPath = '{0}\junit-results.xml' -f $targetPath

    $TestResults = Invoke-Pester -Configuration $configuration
    $failed = $false
    if ($TestResults.FailedCount -gt 0) {
        Write-Host "There were $($TestResults.FailedCount) failed tests in $tag"
        $failed = $true
    } else {
        Write-Host "There were $($TestResults.PassedCount) passed tests out of $($TestResults.TotalCount) in $tag"
    }

    Remove-Item env:\DOCKERHUB_ORG_REPO
    Remove-Item env:\CONTROLLER_TAG

    return $failed
}
function Test-IsLatestJenkinsRelease {
    param (
        [String] $Version
    )

    Write-Host "= PREPARE: Checking if $env:JENKINS_VERSION is latest Weekly or LTS..."

    $metadataUrl = "https://repo.jenkins-ci.org/releases/org/jenkins-ci/main/jenkins-war/maven-metadata.xml"
    try {
        [xml]$metadata = Invoke-WebRequest $metadataUrl -UseBasicParsing
    }
    catch {
        Write-Error "Failed to retrieve Jenkins versions from Artifactory"
        exit 1
    }
    $allVersions = $metadata.metadata.versioning.versions.version

    # Weekly
    $weeklyVersions = $allVersions |
        Where-Object { $_ -match '^\d+\.\d+$' } |
        ForEach-Object { [version]$_ } |
        Sort-Object

    # LTS
    $ltsVersions = $allVersions |
        Where-Object { $_ -match '^\d+\.\d+\.\d+$' } |
        ForEach-Object { [version]$_ } |
        Sort-Object

    $latestWeeklyVersion = $weeklyVersions[-1]
    Write-Host "latest Weekly version: $latestWeeklyVersion"
    $latestLTSVersion    = $ltsVersions[-1]
    Write-Host "latest LTS version: $latestLTSVersion"

    $latest = $false
    if ($Version -eq $latestWeeklyVersion) {
        $latest = $true
    }
    if ($Version -eq $latestLTSVersion) {
        $latest = $true
    }
    if (!$latest) {
        Write-Host "WARNING: $JenkinsVersion is neither the lastest Weekly nor the latest LTS version"
    }
    return $latest
}

function Initialize-DockerComposeFile {
    param (
        [String] $ImageType,
        [String] $DockerComposeFile
    )

    Write-Host "= PREPARE: Docker compose file generation for $ImageType"

    $items = $ImageType.Split('-')
    $windowsFlavor = $items[0]
    $windowsVersion = $items[1]

    # Override the list of Windows versions taken defined in docker-bake.hcl by the version from image type
    $env:WINDOWS_VERSION_OVERRIDE = $windowsVersion

    # Retrieve the targets from docker buildx bake --print output
    # Remove the 'output' section (unsupported by docker compose)
    # For each target name as service key, return a map consisting of:
    # - 'image' set to the first tag value
    # - 'build' set to the content of the bake target
    $yqMainQuery = '.target[] | del(.output) | {(. | key): {"image": .tags[0], "build": .}}'
    # Encapsulate under a top level 'services' map
    $yqServicesQuery = '{"services": .}'

    if ($PSVersionTable.PSVersion.Major -eq 5) {
        $yqMainQuery = $yqMainQuery -replace '"', '\"'
        $yqServicesQuery = $yqServicesQuery -replace '"', '\"'
    }

    # - Use docker buildx bake to output image definitions from the "<windowsFlavor>" bake target
    # - Convert with yq to the format expected by docker compose
    # - Store the result in the docker compose file
    docker buildx bake --progress=quiet --file=docker-bake.hcl $windowsFlavor --print |
        yq --prettyPrint $yqMainQuery |
        yq $yqServicesQuery |
        Out-File -FilePath $DockerComposeFile

    # Remove override
    Remove-Item env:\WINDOWS_VERSION_OVERRIDE
}

Test-CommandExists 'docker'
Test-CommandExists 'docker-compose'
Test-CommandExists 'docker buildx'
Test-CommandExists 'yq'

# Sanity check
yq --version

# Add 'lts-' prefix to LTS tags not including Jenkins version
# Compared to weekly releases, LTS releases include an additional build number in their version
$releaseLine = 'war'
# Determine if the current JENKINS_VERSION corresponds to the latest Weekly or LTS version from Artifactory 
$isJenkinsVersionLatest = Test-IsLatestJenkinsRelease -Version $JenkinsVersion

if ($JenkinsVersion.Split('.').Count -eq 3) {
    $releaseLine = 'war-stable'
    $env:LATEST_LTS = If ($isJenkinsVersionLatest) { "true" } Else { "false" }
} else {
    $env:LATEST_WEEKLY = If ($isJenkinsVersionLatest) { "true" } Else { "false" }
}

# If there is no WAR_URL set, using get.jenkins.io URL depending on the release line
if([String]::IsNullOrWhiteSpace($env:WAR_URL)) {
    $env:WAR_URL = 'https://get.jenkins.io/{0}/{1}/jenkins.war' -f $releaseLine, $JenkinsVersion
}

$dockerComposeFile = 'build-windows_{0}.yaml' -f $ImageType
$baseDockerCmd = 'docker-compose --file={0}' -f $dockerComposeFile
$baseDockerBuildCmd = '{0} build --parallel --pull' -f $baseDockerCmd

# Generate the docker compose file if it doesn't exists or if the parameter OverwriteDockerComposeFile is set
if ((Test-Path $dockerComposeFile) -and -not $OverwriteDockerComposeFile) {
    Write-Host "= PREPARE: The docker compose file '$dockerComposeFile' containing the image definitions already exists."
} else {
    Write-Host "= PREPARE: Initialize the docker compose file '$dockerComposeFile' containing the image definitions."
    Initialize-DockerComposeFile -ImageType $ImageType -DockerComposeFile $dockerComposeFile
}

Write-Host '= PREPARE: List of images and tags to be processed:'
Invoke-Expression "$baseDockerCmd config"

if ($target -eq 'build') {
    Write-Host '= BUILD: Building all images...'

    switch ($DryRun) {
        $true { Write-Host "(dry-run) $baseDockerBuildCmd" }
        $false { Invoke-Expression $baseDockerBuildCmd }
    }

    if ($lastExitCode -ne 0) {
        exit $lastExitCode
    }

    Write-Host '= BUILD: Finished building all images.'
}

if ($target -eq 'test') {
    if ($DryRun) {
        Write-Host '= TEST: (dry-run) test harness skipped'
    } else {
        Write-Host '= TEST: Starting test harness'

        $mod = Get-InstalledModule -Name Pester -MinimumVersion 5.3.0 -MaximumVersion 5.3.3 -ErrorAction SilentlyContinue
        if ($null -eq $mod) {
            Write-Host '= TEST: Pester 5.3.x not found: installing...'
            Install-Module -Force -Name Pester -MaximumVersion 5.3.3 -Scope CurrentUser
        }

        Import-Module Pester
        Write-Host '= TEST: Setting up Pester environment...'
        $configuration = [PesterConfiguration]::Default
        $configuration.Run.PassThru = $true
        $configuration.Run.Path = '.\tests'
        $configuration.Run.Exit = $true
        $configuration.TestResult.Enabled = $true
        $configuration.TestResult.OutputFormat = 'JUnitXml'
        $configuration.Output.Verbosity = 'Diagnostic'
        $configuration.CodeCoverage.Enabled = $false

        Write-Host '= TEST: Testing all images...'
        # Only fail the run afterwards in case of any test failures
        $testFailed = $false
        $imageDefinitions = Invoke-Expression "$baseDockerCmd config" | yq --unwrapScalar --output-format json '.services' | ConvertFrom-Json
        foreach ($imageDefinition in $imageDefinitions.PSObject.Properties) {
            $testFailed = $testFailed -or (Test-Image -ImageName $imageDefinition.Value.image)
        }

        # Fail if any test failures
        if ($testFailed -ne $false) {
            Write-Error '= TEST: Test stage failed'
            exit 1
        } else {
            Write-Host '= TEST: Test stage passed!'
        }
    }
}

if ($target -eq 'publish') {
    Write-Host '= PUBLISH: push all images and tags'
    switch($DryRun) {
        $true { Write-Host "(dry-run) $baseDockerCmd push" }
        $false { Invoke-Expression "$baseDockerCmd push" }
    }

    # Fail if any issues when publishing the docker images
    if ($lastExitCode -ne 0) {
        Write-Error '= PUBLISH: failed!'
        exit 1
    }
}

if ($lastExitCode -ne 0 -and !$DryRun) {
    Write-Error 'Build failed!'
} else {
    Write-Host 'Build finished successfully'
}
exit $lastExitCode


================================================
FILE: rhel/Dockerfile
================================================
ARG RHEL_TAG=9.7-1773204657
ARG RHEL_RELEASE_LINE=ubi9
FROM registry.access.redhat.com/${RHEL_RELEASE_LINE}/ubi:${RHEL_TAG} AS jre-and-war

ARG JAVA_VERSION=17.0.18_8

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

COPY jdk-download-url.sh /usr/bin/jdk-download-url.sh
COPY jdk-download.sh /usr/bin/jdk-download.sh

RUN dnf install --disableplugin=subscription-manager --setopt=install_weak_deps=0 --setopt=tsflags=nodocs --allowerasing -y \
    ca-certificates \
    curl \
    jq \
    && dnf clean --disableplugin=subscription-manager all \
    && /usr/bin/jdk-download.sh

ENV PATH="/opt/jdk-${JAVA_VERSION}/bin:${PATH}"

# Generate smaller java runtime without unneeded files
# for now we include the full module path to maintain compatibility
# while still saving space (approx 200mb from the full distribution)
# hadolint ignore=SC2086
RUN java_major_version="$(jlink --version 2>&1 | cut -c1-2)"; \
    if [ "$java_major_version" = "25" ]; then \
      cp -r "/opt/jdk-${JAVA_VERSION}" /javaruntime; \
    else \
      case "$java_major_version" in \
        "17") options="--compress=2" ;; \
        "21") options="--compress=zip-6" ;; \
        *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \
      esac; \
      jlink \
        --strip-java-debug-attributes \
        ${options} \
        --add-modules ALL-MODULE-PATH \
        --no-man-pages \
        --no-header-files \
        --output /javaruntime; \
    fi

# Jenkins version being bundled in this docker image
ARG JENKINS_VERSION=2.555
# Can be used to customize where jenkins.war get downloaded from
ARG WAR_URL=https://get.jenkins.io/war/${JENKINS_VERSION}/jenkins.war

COPY jenkins.io-2026.key /war/jenkins-key.pub

# Not using ADD as it does not check Last-Modified header
# see https://github.com/docker/docker/issues/8331
RUN curl -fsSL "${WAR_URL}" -o /war/jenkins.war \
  && curl -fsSL "${WAR_URL}.asc" -o /war/jenkins.war.asc \
  && gpg --import /war/jenkins-key.pub \
  && gpg --verify --trust-model direct /war/jenkins.war.asc /war/jenkins.war

FROM registry.access.redhat.com/${RHEL_RELEASE_LINE}/ubi:${RHEL_TAG} AS controller

ENV LANG=C.UTF-8

ARG TARGETARCH
ARG COMMIT_SHA

RUN dnf install --disableplugin=subscription-manager --setopt=install_weak_deps=0 --setopt=tsflags=nodocs -y \
        fontconfig \
        freetype \
        git \
        git-lfs \
        unzip \
        which \
        tzdata \
    && dnf clean --disableplugin=subscription-manager all

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG http_port=8080
ARG agent_port=50000
ARG JENKINS_HOME=/var/jenkins_home
ARG REF=/usr/share/jenkins/ref

ENV JENKINS_HOME=$JENKINS_HOME
ENV JENKINS_SLAVE_AGENT_PORT=${agent_port}
ENV REF=$REF

# Jenkins is run with user `jenkins`, uid = 1000
# If you bind mount a volume from the host or a data container,
# ensure you use the same uid
RUN mkdir -p $JENKINS_HOME \
  && chown ${uid}:${gid} $JENKINS_HOME \
  && groupadd -g ${gid} ${group} \
  && useradd -N -d "$JENKINS_HOME" -u ${uid} -g ${gid} -l -m -s /bin/bash ${user}

# Jenkins home directory is a volume, so configuration and build history
# can be persisted and survive image upgrades
VOLUME $JENKINS_HOME

# $REF (defaults to `/usr/share/jenkins/ref/`) contains all reference configuration we want
# to set on a fresh new installation. Use it to bundle additional plugins
# or config file with your custom jenkins Docker image.
RUN mkdir -p ${REF}/init.groovy.d

# Use tini as subreaper in Docker container to adopt zombie processes
ARG TINI_VERSION=v0.19.0
COPY tini_pub.gpg "${JENKINS_HOME}/tini_pub.gpg"
RUN curl -fsSL "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TARGETARCH}" -o /sbin/tini \
  && curl -fsSL "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TARGETARCH}.asc" -o /sbin/tini.asc \
  && gpg --no-tty --import "${JENKINS_HOME}/tini_pub.gpg" \
  && gpg --verify /sbin/tini.asc \
  && rm -rf /sbin/tini.asc /root/.gnupg \
  && chmod +x /sbin/tini

ENV JENKINS_UC=https://updates.jenkins.io
ENV JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental
ENV JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals
RUN chown -R ${user} "$JENKINS_HOME" "$REF"

ARG PLUGIN_CLI_VERSION=2.14.0
ARG PLUGIN_CLI_URL=https://github.com/jenkinsci/plugin-installation-manager-tool/releases/download/${PLUGIN_CLI_VERSION}/jenkins-plugin-manager-${PLUGIN_CLI_VERSION}.jar
RUN curl -fsSL ${PLUGIN_CLI_URL} -o /opt/jenkins-plugin-manager.jar \
  && echo "$(curl -fsSL "${PLUGIN_CLI_URL}.sha256")  /opt/jenkins-plugin-manager.jar" >/tmp/jpm_sha \
  && sha256sum -c --strict /tmp/jpm_sha \
  && rm -f /tmp/jpm_sha

# for main web interface:
EXPOSE ${http_port}

# will be used by attached agents:
EXPOSE ${agent_port}

ENV COPY_REFERENCE_FILE_LOG=$JENKINS_HOME/copy_reference_file.log

ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-and-war /javaruntime $JAVA_HOME
COPY --from=jre-and-war /war/jenkins.war /usr/share/jenkins/jenkins.war

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY jenkins-plugin-cli.sh /bin/jenkins-plugin-cli

ARG JENKINS_VERSION=2.555
ENV JENKINS_VERSION=${JENKINS_VERSION}

ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"]

# metadata labels
LABEL \
    org.opencontainers.image.vendor="Jenkins project" \
    org.opencontainers.image.title="Official Jenkins Docker image" \
    org.opencontainers.image.description="The Jenkins Continuous Integration and Delivery server" \
    org.opencontainers.image.version="${JENKINS_VERSION}" \
    org.opencontainers.image.url="https://www.jenkins.io/" \
    org.opencontainers.image.source="https://github.com/jenkinsci/docker" \
    org.opencontainers.image.revision="${COMMIT_SHA}" \
    org.opencontainers.image.licenses="MIT"


================================================
FILE: tests/bake.bats
================================================
#!/usr/bin/env bats

# bats file_tags=test-suite:bake
# bats file_tags=test-type:golden-file

load test_helpers

SUT_DESCRIPTION="docker bake"
LTS_JENKINS_VERSION="2.541.1"

@test "[${SUT_DESCRIPTION}: tags] Default tags unchanged" {
  assert_matches_golden expected_tags make --silent tags
}
@test "[${SUT_DESCRIPTION}: tags] Latest weekly tags unchanged" {
  assert_matches_golden expected_tags_latest_weekly make --silent tags LATEST_WEEKLY=true
}
@test "[${SUT_DESCRIPTION}: tags] Latest LTS tags unchanged" {
  assert_matches_golden expected_tags_latest_lts make --silent tags LATEST_LTS=true JENKINS_VERSION="${LTS_JENKINS_VERSION}"
}

@test "[${SUT_DESCRIPTION}: platforms] Platforms per target unchanged" {
  assert_matches_golden expected_platforms make --silent platforms
}


================================================
FILE: tests/functions/.ssh/config
================================================


================================================
FILE: tests/functions/Dockerfile
================================================
FROM bats-jenkins

RUN mkdir -p /usr/share/jenkins/ref/.ssh && touch /usr/share/jenkins/ref/.ssh/config.override
RUN chmod 600 /usr/share/jenkins/ref/.ssh/config.override


================================================
FILE: tests/functions/Dockerfile-windows
================================================
FROM bats-jenkins
# hadolint shell=powershell

RUN mkdir C:/ProgramData/Jenkins/Reference/pester ; echo $null >> C:/ProgramData/Jenkins/Reference/pester/test.override

================================================
FILE: tests/functions.Tests.ps1
================================================
Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1
Import-Module -DisableNameChecking -Force $PSScriptRoot/test_helpers.psm1

$global:SUT_IMAGE=Get-SutImage
$global:SUT_CONTAINER=Get-SutImage
$global:TEST_TAG=$global:SUT_IMAGE.Replace('pester-jenkins-', '')

Describe "[functions > $global:TEST_TAG] build image" {
  BeforeEach {
    Push-Location -StackName 'jenkins' -Path "$PSScriptRoot/.."
  }

  It 'builds image' {
    $exitCode, $stdout, $stderr = Build-Docker $global:SUT_IMAGE
    $exitCode | Should -Be 0
  }

  AfterEach {
    Pop-Location -StackName 'jenkins'
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[functions > $global:TEST_TAG] Check-VersionLessThan" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'exit codes work' {
    docker run --rm $global:SUT_IMAGE "exit -1"
    $LastExitCode | Should -Be -1
  }

  It 'has same version' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '1.0' '1.0')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be -1
  }

  It 'has right side greater' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '1.0' '1.1')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be 0
  }

  It 'has left side greater' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '1.1' '1.0')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be -1
  }
  ## Real world examples from https://github.com/jenkinsci/docker/issues/1456
  It 'has left side greater (commons-lang3-api-plugin)' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '3.12.0.0' '3.12.0-36.vd97de6465d5b_')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be 0
  }
  It 'has left side greater (map-db-plugin)' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '1.0.9.0' '1.0.9-28.vf251ce40855d')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be 0
  }
  It 'has left side greater (role-strategy-plugin, security fix backport)' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '587.v2872c41fa_e51' '587.588.v850a_20a_30162')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be 0
  }
  It 'has left side greater (workflow-cps-plugin, security fix backport)' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '3894.vd0f0248b_a_fc4' '3894.3896.vca_2c931e7935')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be 0
  }
  It 'has left side greater (workflow-cps-plugin, security fix backport of an older release, published later than a newer normal release)' {
    docker run --rm $global:SUT_IMAGE "Import-Module -DisableNameChecking -Force C:/ProgramData/Jenkins/jenkins-support.psm1 ; if(`$(Compare-VersionLessThan '4106.4108.v841a_e1819d4d' '4151.v5406e29e3c90')) { exit 0 } else { exit -1 }"
    $LastExitCode | Should -Be 0
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[functions > $global:TEST_TAG] Copy-ReferenceFile" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'build test image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE $PSScriptRoot/functions
    $exitCode | Should -Be 0
  }

  It 'start container' {
    $exitCode, $stdout, $stderr = Run-Program 'docker' "run -d --name $global:SUT_CONTAINER -P $global:SUT_IMAGE"
    $exitCode | Should -Be 0
  }

  It 'wait for running' {
    # give time to eventually fail to initialize
    Start-Sleep -Seconds 5
    Retry-Command -RetryCount 3 -Delay 1 -ScriptBlock { docker inspect -f "{{.State.Running}}" $global:SUT_CONTAINER ; if($lastExitCode -ne 0) { throw('Docker inspect failed') } } -Verbose | Should -BeTrue
  }

  It 'is initialized' {
    Retry-Command -RetryCount 30 -Delay 5 -ScriptBlock { Test-Url $global:SUT_CONTAINER "/api/json" } -Verbose | Should -BeTrue
  }

  It 'check files in JENKINS_HOME' {
    $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:SUT_CONTAINER powershell -C `"Get-ChildItem `$env:JENKINS_HOME`" | Select-Object -Property 'Name'"
    $exitCode | Should -Be 0
    $stdout | Should -Match "pester"
    $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:SUT_CONTAINER powershell -C `"Get-ChildItem `$env:JENKINS_HOME/pester`" | Select-Object -Property 'Name'"
    $exitCode | Should -Be 0
    $stdout | Should -Match "test.override"
  }

  It 'cleanup container' {
    Cleanup $global:SUT_CONTAINER | Out-Null
  }
}


================================================
FILE: tests/functions.bats
================================================
#!/usr/bin/env bats

# bats file_tags=test-suite:functions

load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load test_helpers

SUT_IMAGE=$(get_sut_image)
SUT_DESCRIPTION="${IMAGE}-functions"

. $BATS_TEST_DIRNAME/../jenkins-support

@test "[${SUT_DESCRIPTION}] versionLT" {
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 1.0 1.0"
  assert_failure
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 1.0 1.1"
  assert_success
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 1.1 1.0"
  assert_failure
  ## Real world examples from https://github.com/jenkinsci/docker/issues/1456
  # commons-lang3-api-plugin/releases
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 3.12.0.0 3.12.0-36.vd97de6465d5b_"
  assert_success
  # map-db-plugin
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 1.0.9.0 1.0.9-28.vf251ce40855d"
  assert_success
  # role-strategy-plugin, security fix backport
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 587.v2872c41fa_e51 587.588.v850a_20a_30162"
  assert_success
  # workflow-cps-plugin, security fix backport
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 3894.vd0f0248b_a_fc4 3894.3896.vca_2c931e7935"
  assert_success
  # workflow-cps-plugin, security fix backport of an older release, published later than a newer normal release
  run docker run --rm $SUT_IMAGE bash -c "source /usr/local/bin/jenkins-support && versionLT 4106.4108.v841a_e1819d4d 4151.v5406e29e3c90"
  assert_success
}

@test "[${SUT_DESCRIPTION}] permissions are propagated from override file" {
  local sut_image="${SUT_IMAGE}-functions-${BATS_TEST_NUMBER}"
  run docker_build_child "${SUT_IMAGE}" "${sut_image}" $BATS_TEST_DIRNAME/functions
  assert_success
  # Create a predefined named volume and fill it with a file in an unexpected file mode
  local volume_name
  volume_name="functions_${BATS_TEST_NUMBER}"
  run bash -c "docker volume rm ${volume_name}; docker volume create ${volume_name}"
  run docker run --rm --volume "${volume_name}:/sut_data" --user=0 "${sut_image}" \
    bash -c "mkdir -p /sut_data/.ssh && touch /sut_data/.ssh/config && chmod 644 /sut_data/.ssh/config && chown -R 1000:1000 /sut_data"
  # replace DOS line endings \r\n
  run bash -c "docker run --rm --volume "${volume_name}:/var/jenkins_home:rw" "${sut_image}" stat -c '%a' /var/jenkins_home/.ssh/config"
  assert_success
  assert_line '600'
  # Cleanup
  run docker volume rm "${volume_name}"
}


================================================
FILE: tests/golden/expected_env_vars_except_hostname.txt
================================================
COPY_REFERENCE_FILE_LOG=/var/jenkins_home/copy_reference_file.log
HOME=/var/jenkins_home
JAVA_HOME=/opt/java/openjdk
JENKINS_HOME=/var/jenkins_home
JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals
JENKINS_SLAVE_AGENT_PORT=50000
JENKINS_UC=https://updates.jenkins.io
JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental
JENKINS_VERSION=2.555
LANG=C.UTF-8
PATH=/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
REF=/usr/share/jenkins/ref
SHLVL=1
_=/usr/bin/env


================================================
FILE: tests/golden/expected_platforms.txt
================================================
alpine_jdk21:linux/amd64
alpine_jdk21:linux/arm64
alpine_jdk25:linux/amd64
alpine_jdk25:linux/arm64
debian-slim_jdk21:linux/amd64
debian-slim_jdk21:linux/arm64
debian-slim_jdk21:linux/riscv64
debian-slim_jdk25:linux/amd64
debian-slim_jdk25:linux/arm64
debian-slim_jdk25:linux/riscv64
debian_jdk21:linux/amd64
debian_jdk21:linux/arm64
debian_jdk21:linux/ppc64le
debian_jdk21:linux/riscv64
debian_jdk21:linux/s390x
debian_jdk25:linux/amd64
debian_jdk25:linux/arm64
debian_jdk25:linux/ppc64le
debian_jdk25:linux/riscv64
debian_jdk25:linux/s390x
rhel_jdk21:linux/amd64
rhel_jdk21:linux/arm64
rhel_jdk21:linux/ppc64le
rhel_jdk25:linux/amd64
rhel_jdk25:linux/arm64
rhel_jdk25:linux/ppc64le
windowsservercore-ltsc2019_jdk21:windows/amd64
windowsservercore-ltsc2019_jdk25:windows/amd64
windowsservercore-ltsc2022_jdk21:windows/amd64
windowsservercore-ltsc2022_jdk25:windows/amd64


================================================
FILE: tests/golden/expected_tags.txt
================================================
docker.io/jenkins/jenkins:2.555 (debian_jdk21)
docker.io/jenkins/jenkins:2.555-alpine (alpine_jdk21)
docker.io/jenkins/jenkins:2.555-alpine-jdk21 (alpine_jdk21)
docker.io/jenkins/jenkins:2.555-alpine-jdk25 (alpine_jdk25)
docker.io/jenkins/jenkins:2.555-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.555-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:2.555-jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:2.555-jdk21-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.555-jdk21-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:2.555-jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:2.555-jdk25-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk25)
docker.io/jenkins/jenkins:2.555-jdk25-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk25)
docker.io/jenkins/jenkins:2.555-rhel-ubi9-jdk21 (rhel_jdk21)
docker.io/jenkins/jenkins:2.555-rhel-ubi9-jdk25 (rhel_jdk25)
docker.io/jenkins/jenkins:2.555-slim (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.555-slim-jdk21 (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.555-slim-jdk25 (debian-slim_jdk25)


================================================
FILE: tests/golden/expected_tags_latest_lts.txt
================================================
docker.io/jenkins/jenkins:2.541.1 (debian_jdk21)
docker.io/jenkins/jenkins:2.541.1-alpine (alpine_jdk21)
docker.io/jenkins/jenkins:2.541.1-alpine-jdk21 (alpine_jdk21)
docker.io/jenkins/jenkins:2.541.1-alpine-jdk25 (alpine_jdk25)
docker.io/jenkins/jenkins:2.541.1-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.541.1-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:2.541.1-jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:2.541.1-jdk21-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.541.1-jdk21-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:2.541.1-jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:2.541.1-jdk25-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk25)
docker.io/jenkins/jenkins:2.541.1-jdk25-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk25)
docker.io/jenkins/jenkins:2.541.1-lts (debian_jdk21)
docker.io/jenkins/jenkins:2.541.1-lts-alpine (alpine_jdk21)
docker.io/jenkins/jenkins:2.541.1-lts-jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:2.541.1-lts-jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:2.541.1-lts-rhel-ubi9-jdk21 (rhel_jdk21)
docker.io/jenkins/jenkins:2.541.1-lts-rhel-ubi9-jdk25 (rhel_jdk25)
docker.io/jenkins/jenkins:2.541.1-lts-slim (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.541.1-rhel-ubi9-jdk21 (rhel_jdk21)
docker.io/jenkins/jenkins:2.541.1-rhel-ubi9-jdk25 (rhel_jdk25)
docker.io/jenkins/jenkins:2.541.1-slim (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.541.1-slim-jdk21 (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.541.1-slim-jdk25 (debian-slim_jdk25)
docker.io/jenkins/jenkins:2.541.1-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.541.1-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:lts (debian_jdk21)
docker.io/jenkins/jenkins:lts-alpine (alpine_jdk21)
docker.io/jenkins/jenkins:lts-alpine-jdk21 (alpine_jdk21)
docker.io/jenkins/jenkins:lts-alpine-jdk25 (alpine_jdk25)
docker.io/jenkins/jenkins:lts-jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:lts-jdk21-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:lts-jdk21-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:lts-jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:lts-jdk25-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk25)
docker.io/jenkins/jenkins:lts-jdk25-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk25)
docker.io/jenkins/jenkins:lts-rhel-ubi9-jdk21 (rhel_jdk21)
docker.io/jenkins/jenkins:lts-rhel-ubi9-jdk25 (rhel_jdk25)
docker.io/jenkins/jenkins:lts-slim (debian-slim_jdk21)
docker.io/jenkins/jenkins:lts-slim-jdk21 (debian-slim_jdk21)
docker.io/jenkins/jenkins:lts-slim-jdk25 (debian-slim_jdk25)
docker.io/jenkins/jenkins:lts-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:lts-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)


================================================
FILE: tests/golden/expected_tags_latest_weekly.txt
================================================
docker.io/jenkins/jenkins:2.555 (debian_jdk21)
docker.io/jenkins/jenkins:2.555-alpine (alpine_jdk21)
docker.io/jenkins/jenkins:2.555-alpine-jdk21 (alpine_jdk21)
docker.io/jenkins/jenkins:2.555-alpine-jdk25 (alpine_jdk25)
docker.io/jenkins/jenkins:2.555-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.555-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:2.555-jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:2.555-jdk21-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.555-jdk21-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:2.555-jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:2.555-jdk25-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk25)
docker.io/jenkins/jenkins:2.555-jdk25-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk25)
docker.io/jenkins/jenkins:2.555-rhel-ubi9-jdk21 (rhel_jdk21)
docker.io/jenkins/jenkins:2.555-rhel-ubi9-jdk25 (rhel_jdk25)
docker.io/jenkins/jenkins:2.555-slim (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.555-slim-jdk21 (debian-slim_jdk21)
docker.io/jenkins/jenkins:2.555-slim-jdk25 (debian-slim_jdk25)
docker.io/jenkins/jenkins:2.555-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:2.555-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:alpine (alpine_jdk21)
docker.io/jenkins/jenkins:alpine-jdk21 (alpine_jdk21)
docker.io/jenkins/jenkins:alpine-jdk25 (alpine_jdk25)
docker.io/jenkins/jenkins:alpine3.23-jdk21 (alpine_jdk21)
docker.io/jenkins/jenkins:alpine3.23-jdk25 (alpine_jdk25)
docker.io/jenkins/jenkins:jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:jdk21-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:jdk21-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)
docker.io/jenkins/jenkins:jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:jdk25-hotspot-windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk25)
docker.io/jenkins/jenkins:jdk25-hotspot-windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk25)
docker.io/jenkins/jenkins:latest (debian_jdk21)
docker.io/jenkins/jenkins:latest-jdk21 (debian_jdk21)
docker.io/jenkins/jenkins:latest-jdk25 (debian_jdk25)
docker.io/jenkins/jenkins:rhel-ubi9-jdk21 (rhel_jdk21)
docker.io/jenkins/jenkins:rhel-ubi9-jdk25 (rhel_jdk25)
docker.io/jenkins/jenkins:slim (debian-slim_jdk21)
docker.io/jenkins/jenkins:slim-jdk21 (debian-slim_jdk21)
docker.io/jenkins/jenkins:slim-jdk25 (debian-slim_jdk25)
docker.io/jenkins/jenkins:windowsservercore-ltsc2019 (windowsservercore-ltsc2019_jdk21)
docker.io/jenkins/jenkins:windowsservercore-ltsc2022 (windowsservercore-ltsc2022_jdk21)


================================================
FILE: tests/jenkinsfile.bats
================================================
#!/usr/bin/env bats

# bats file_tags=test-suite:jenkinsfile

load test_helpers

SUT_DESCRIPTION="Jenkinsfile"

@test "[${SUT_DESCRIPTION}] Default (weekly) Linux targets from docker bake are taken in account in Jenkinsfile" {
  [ "$(get_default_docker_bake_linux_targets)" == "$(get_targets_from_jenkinsfile)" ]
}


================================================
FILE: tests/plugins-cli/Dockerfile
================================================
FROM bats-jenkins

RUN jenkins-plugin-cli --plugins junit:1.6 ant:1.3 mesos:0.13.0 git:latest filesystem_scm:experimental docker-plugin:1.1.6


================================================
FILE: tests/plugins-cli/Dockerfile-windows
================================================
FROM bats-jenkins
# hadolint shell=powershell

RUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --verbose --plugins junit:1.6 ant:1.3 mesos:0.13.0 git:latest filesystem_scm:experimental docker-plugin:1.1.6



================================================
FILE: tests/plugins-cli/custom-war/Dockerfile
================================================
FROM bats-jenkins

# Define a custom location for the war
ENV JENKINS_WAR=/test-custom-dockerfile/my-custom-jenkins.war
WORKDIR /test-custom-dockerfile
# Add there a new weird plugin to assert
COPY --chown=jenkins:jenkins WEB-INF/ WEB-INF/

USER root
RUN chown jenkins:jenkins /test-custom-dockerfile
USER jenkins

# Copy the original jenkins.war to the custom location, add the weird plugin to
# the new custom WAR, and run the jenkins-plugin-cli script.
RUN cp /usr/share/jenkins/jenkins.war $JENKINS_WAR \
  && chown jenkins:jenkins $JENKINS_WAR \
  && jar -uf my-custom-jenkins.war WEB-INF/* \
  && jenkins-plugin-cli --war $JENKINS_WAR --plugins junit:1.6


================================================
FILE: tests/plugins-cli/custom-war/Dockerfile-windows
================================================
FROM bats-jenkins
# hadolint shell=powershell

# Define a custom location for the war
ENV JENKINS_WAR=C:/ProgramData/TestCustomDockerfile/my-custom-jenkins.war
WORKDIR C:/ProgramData/TestCustomDockerfile
# Add there a new weird plugin to assert
COPY WEB-INF/ WEB-INF/

# Copy the original jenkins.war to the custom location 
RUN Copy-Item C:/ProgramData/Jenkins/jenkins.war $env:JENKINS_WAR 

# Add the weird plugin to the new custom war
# hadolint ignore=DL3059
RUN jar -uf my-custom-jenkins.war WEB-INF/* 

# Run the jenkins-plugin-cli script
# hadolint ignore=DL3059
RUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --war $env:JENKINS_WAR --plugins junit:1.6


================================================
FILE: tests/plugins-cli/java-opts/Dockerfile
================================================
FROM bats-jenkins-plugins-cli

ENV JAVA_OPTS="-Djava.opts.test=true -XshowSettings:properties"
RUN jenkins-plugin-cli --version


================================================
FILE: tests/plugins-cli/no-war/Dockerfile
================================================
FROM bats-jenkins

COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
USER root
RUN rm -rf /usr/share/jenkins/jenkins.war
USER jenkins
RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt


================================================
FILE: tests/plugins-cli/no-war/Dockerfile-windows
================================================
FROM bats-jenkins
# hadolint shell=powershell

COPY plugins.txt C:/ProgramData/Jenkins/Reference/plugins.txt
RUN Remove-Item -Force C:/ProgramData/Jenkins/jenkins.war
# hadolint ignore=DL3059
RUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 -f C:/ProgramData/Jenkins/Reference/plugins.txt


================================================
FILE: tests/plugins-cli/no-war/plugins.txt
================================================
# comment line should be skipped

# simple case
ant:1.3

# trailing spaces
junit:1.6   

# leading spaces
  mesos:0.13.0

# leading spaces, and trailing spaces
  git:latest  

# with comments at the end
filesystem_scm:experimental  # comment at the end

# empty line
 
    #  
     # empty line


================================================
FILE: tests/plugins-cli/pluginsfile/Dockerfile
================================================
FROM bats-jenkins

COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt


================================================
FILE: tests/plugins-cli/pluginsfile/Dockerfile-windows
================================================
FROM bats-jenkins
# hadolint shell=powershell

COPY plugins.txt C:/ProgramData/Jenkins/Reference/plugins.txt
RUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --verbose -f C:/ProgramData/Jenkins/Reference/plugins.txt


================================================
FILE: tests/plugins-cli/pluginsfile/plugins.txt
================================================
# comment line should be skipped

# simple case
ant:1.3

# trailing spaces
junit:1.6   

# leading spaces
  mesos:0.13.0

# leading spaces, and trailing spaces
  git:latest  

# with comments at the end
filesystem_scm:experimental  # comment at the end

# empty line
 
    #  
     # empty line

# from url
subversion:::https://updates.jenkins.io/download/plugins/subversion/2.12.1/subversion.hpi


================================================
FILE: tests/plugins-cli/ref/Dockerfile
================================================
FROM bats-jenkins-plugins-cli

RUN rm -rf /usr/share/jenkins/ref ; jenkins-plugin-cli --plugins junit:1.28 ant:1.3


================================================
FILE: tests/plugins-cli/ref/Dockerfile-windows
================================================
FROM bats-jenkins
# hadolint shell=powershell

RUN Remove-Item -Recurse -Force C:/ProgramData/Jenkins/Reference ; C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --verbose --plugins junit:1.28 ant:1.3


================================================
FILE: tests/plugins-cli/update/Dockerfile
================================================
FROM bats-jenkins-plugins-cli

RUN jenkins-plugin-cli --verbose --plugins junit:1.28 ant:1.3


================================================
FILE: tests/plugins-cli/update/Dockerfile-windows
================================================
FROM bats-jenkins-plugins-cli
# hadolint shell=powershell

RUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --verbose --plugins junit:1.28 ant:1.3


================================================
FILE: tests/plugins-cli.Tests.ps1
================================================
Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1
Import-Module -DisableNameChecking -Force $PSScriptRoot/test_helpers.psm1

$global:SUT_IMAGE=Get-SutImage
$global:SUT_CONTAINER=Get-SutImage
$global:TEST_TAG=$global:SUT_IMAGE.Replace('pester-jenkins-', '')

Describe "[plugins-cli > $global:TEST_TAG] build image" {
  BeforeEach {
    Push-Location -StackName 'jenkins' -Path "$PSScriptRoot/.."
  }

  It 'builds image' {
    $exitCode, $stdout, $stderr = Build-Docker $global:SUT_IMAGE
    $exitCode | Should -Be 0
  }

  AfterEach {
    Pop-Location -StackName 'jenkins'
  }
}

Describe "[plugins-cli > $global:TEST_TAG] cleanup container" {
  It 'cleanup' {
    Cleanup $global:SUT_CONTAINER | Out-Null
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] plugins are installed with jenkins-plugin-cli" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli $PSScriptRoot/plugins-cli
    $exitCode | Should -Be 0
    $stdout | Should -Not -Match "Skipping already installed dependency"
  }

  It 'has correct plugins' {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm $global:SUT_IMAGE-plugins-cli gci `$env:JENKINS_HOME/plugins | Select-Object -Property Name"
    $exitCode | Should -Be 0

    $stdout | Should -Match 'junit.jpi'
    $stdout | Should -Match 'junit.jpi.pinned'
    $stdout | Should -Match 'ant.jpi'
    $stdout | Should -Match 'ant.jpi.pinned'
    $stdout | Should -Match 'credentials.jpi'
    $stdout | Should -Match 'credentials.jpi.pinned'
    $stdout | Should -Match 'mesos.jpi'
    $stdout | Should -Match 'mesos.jpi.pinned'
    # optional dependencies
    $stdout | Should -Not -Match 'metrics.jpi'
    $stdout | Should -Not -Match 'metrics.jpi.pinned'
    # plugins bundled but under detached-plugins, so need to be installed
    $stdout | Should -Match 'mailer.jpi'
    $stdout | Should -Match 'mailer.jpi.pinned'
    $stdout | Should -Match 'git.jpi'
    $stdout | Should -Match 'git.jpi.pinned'
    $stdout | Should -Match 'filesystem_scm.jpi'
    $stdout | Should -Match 'filesystem_scm.jpi.pinned'
    $stdout | Should -Match 'docker-plugin.jpi'
    $stdout | Should -Match 'docker-plugin.jpi.pinned'
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] plugins are installed with jenkins-plugin-cli with non-default REF" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli-ref $PSScriptRoot/plugins-cli/ref
    $exitCode | Should -Be 0
    $stdout | Should -Not -Match "Skipping already installed dependency"
  }

  It 'has correct plugins' {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm $global:SUT_IMAGE-plugins-cli-ref -e REF=C:/ProgramData/JenkinsDir/Reference gci C:/ProgramData/JenkinsDir/Reference"
    $exitCode | Should -Be 0

    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm $global:SUT_IMAGE-plugins-cli gci `$env:JENKINS_HOME/plugins | Select-Object -Property Name"
    $exitCode | Should -Be 0

    $stdout | Should -Match 'junit.jpi'
    $stdout | Should -Match 'junit.jpi.pinned'
    $stdout | Should -Match 'ant.jpi'
    $stdout | Should -Match 'ant.jpi.pinned'
    $stdout | Should -Match 'credentials.jpi'
    $stdout | Should -Match 'credentials.jpi.pinned'
    $stdout | Should -Match 'mesos.jpi'
    $stdout | Should -Match 'mesos.jpi.pinned'
    # optional dependencies
    $stdout | Should -Not -Match 'metrics.jpi'
    $stdout | Should -Not -Match 'metrics.jpi.pinned'
    # plugins bundled but under detached-plugins, so need to be installed
    $stdout | Should -Match 'mailer.jpi'
    $stdout | Should -Match 'mailer.jpi.pinned'
    $stdout | Should -Match 'git.jpi'
    $stdout | Should -Match 'git.jpi.pinned'
    $stdout | Should -Match 'filesystem_scm.jpi'
    $stdout | Should -Match 'filesystem_scm.jpi.pinned'
    $stdout | Should -Match 'docker-plugin.jpi'
    $stdout | Should -Match 'docker-plugin.jpi.pinned'
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] plugins are installed with jenkins-plugin-cli from a plugins file" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli $PSScriptRoot/plugins-cli
    $exitCode | Should -Be 0
  }

  It 'builds grandchild image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli-pluginsfile $PSScriptRoot/plugins-cli/pluginsfile
    $exitCode | Should -Be 0
    $stdout | Should -Not -Match "Skipping already installed dependency"
  }

  It 'has correct plugins' {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm $global:SUT_IMAGE-plugins-cli gci `$env:JENKINS_HOME/plugins | Select-Object -Property Name"
    $exitCode | Should -Be 0

    $stdout | Should -Match 'junit.jpi'
    $stdout | Should -Match 'junit.jpi.pinned'
    $stdout | Should -Match 'ant.jpi'
    $stdout | Should -Match 'ant.jpi.pinned'
    $stdout | Should -Match 'credentials.jpi'
    $stdout | Should -Match 'credentials.jpi.pinned'
    $stdout | Should -Match 'mesos.jpi'
    $stdout | Should -Match 'mesos.jpi.pinned'
    # optional dependencies
    $stdout | Should -Not -Match 'metrics.jpi'
    $stdout | Should -Not -Match 'metrics.jpi.pinned'
    # plugins bundled but under detached-plugins, so need to be installed
    $stdout | Should -Match 'mailer.jpi'
    $stdout | Should -Match 'mailer.jpi.pinned'
    $stdout | Should -Match 'git.jpi'
    $stdout | Should -Match 'git.jpi.pinned'
    $stdout | Should -Match 'filesystem_scm.jpi'
    $stdout | Should -Match 'filesystem_scm.jpi.pinned'
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] plugins are installed with jenkins-plugin-cli even when already exist" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli $PSScriptRoot/plugins-cli
    $exitCode | Should -Be 0
  }

  It 'builds grandchild image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli-update $PSScriptRoot/plugins-cli/update --no-cache
    $exitCode | Should -Be 0
  }

  It 'has the correct version of junit' {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm $global:SUT_IMAGE-plugins-cli-update Import-Module -Force -DisableNameChecking C:/ProgramData/Jenkins/jenkins-support.psm1 ; Expand-Zip `$env:JENKINS_HOME/plugins/junit.jpi 'META-INF/MANIFEST.MF'"
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.28'
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] plugins are getting upgraded but not downgraded" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  BeforeAll {
    $dockerVolume = (New-Guid).Guid
    docker volume rm -f $dockerVolume
  }
  It 'builds child image' {
    # Initial execution
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli $PSScriptRoot/plugins-cli
    $exitCode | Should -Be 0
  }

  It 'has correct version of junit and ant plugins' {
    # Image contains junit 1.6 and ant-plugin 1.3
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run -v ${dockerVolume}:C:\ProgramData\Jenkins\JenkinsHome --rm $global:SUT_IMAGE-plugins-cli exit 0"
    $exitCode | Should -Be 0

    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE "junit.jpi" $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.6'
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE "ant.jpi" $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.3'
  }

  It 'upgrades plugins' {
    # Upgrade to new image with different plugins
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-upgrade-plugins $PSScriptRoot/upgrade-plugins
    $exitCode | Should -Be 0
    # Images contains junit 1.28 and ant-plugin 1.2
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run -v ${dockerVolume}:C:\ProgramData\Jenkins\JenkinsHome --rm $global:SUT_IMAGE-upgrade-plugins exit 0"
    $exitCode | Should -Be 0
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'junit.jpi' $dockerVolume
    $exitCode | Should -Be 0
    # Should be updated
    $stdout | Should -Match 'Plugin-Version: 1.28'
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'ant.jpi' $dockerVolume
    $exitCode | Should -Be 0
    # 1.2 is older than the existing 1.3, so keep 1.3
    $stdout | Should -Match 'Plugin-Version: 1.3'
  }
  AfterAll {
    docker volume rm -f $dockerVolume
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] do not upgrade if plugin has been manually updated" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  BeforeAll {
    $dockerVolume = (New-Guid).Guid
    docker volume rm -f $dockerVolume
  }

  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli $PSScriptRoot/plugins-cli
    $exitCode | Should -Be 0
  }

  It 'updates plugin manually and then via plugin-cli' {
    # Image contains junit 1.8 and ant-plugin 1.3
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run -v ${dockerVolume}:C:\ProgramData\Jenkins\JenkinsHome --rm $global:SUT_IMAGE-plugins-cli curl.exe --connect-timeout 20 --retry 5 --retry-delay 0 --retry-max-time 60 -s -f -L https://updates.jenkins.io/download/plugins/junit/1.8/junit.hpi -o C:/ProgramData/Jenkins/JenkinsHome/plugins/junit.jpi"
    $exitCode | Should -Be 0
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'junit.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.8'
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'ant.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.3'

    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-upgrade-plugins $PSScriptRoot/upgrade-plugins
    $exitCode | Should -Be 0

    # Images contains junit 1.28 and ant-plugin 1.2
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run -v ${dockerVolume}:C:\ProgramData\Jenkins\JenkinsHome --rm $global:SUT_IMAGE-upgrade-plugins exit 0"
    $exitCode | Should -Be 0
    # junit shouldn't be upgraded
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'junit.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.8'
    $stdout | Should -Not -Match 'Plugin-Version: 1.28'
    # ant shouldn't be downgraded
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'ant.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.3'
    $stdout | Should -Not -Match 'Plugin-Version: 1.2'
  }
  AfterAll {
    docker volume rm -f $dockerVolume
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] upgrade plugin even if it has been manually updated when PLUGINS_FORCE_UPGRADE=true" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
   BeforeAll {
    $dockerVolume = (New-Guid).Guid
    docker volume rm -f $dockerVolume
  }

  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli $PSScriptRoot/plugins-cli
    $exitCode | Should -Be 0
  }

  It 'upgrades plugins' {
    # Image contains junit 1.6 and ant-plugin 1.3
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run -v ${dockerVolume}:C:\ProgramData\Jenkins\JenkinsHome --rm $global:SUT_IMAGE-plugins-cli curl.exe --connect-timeout 20 --retry 5 --retry-delay 0 --retry-max-time 60 -s -f -L https://updates.jenkins.io/download/plugins/junit/1.8/junit.hpi -o C:/ProgramData/Jenkins/JenkinsHome/plugins/junit.jpi"
    $exitCode | Should -Be 0

    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'junit.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.8'

    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-upgrade-plugins $PSScriptRoot/upgrade-plugins
    $exitCode | Should -Be 0

    # Images contains junit 1.28 and ant-plugin 1.2
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run -e PLUGINS_FORCE_UPGRADE=true -v ${dockerVolume}:C:\ProgramData\Jenkins\JenkinsHome --rm $global:SUT_IMAGE-upgrade-plugins exit 0" true
    $exitCode | Should -Be 0
    # junit should be upgraded
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'junit.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Not -Match 'Plugin-Version: 1.8'
    $stdout | Should -Match 'Plugin-Version: 1.28'
    # ant shouldn't be downgraded
    $exitCode, $stdout, $stderr = Unzip-Manifest $global:SUT_IMAGE 'ant.jpi' $dockerVolume
    $exitCode | Should -Be 0
    $stdout | Should -Match 'Plugin-Version: 1.3'
    $stdout | Should -Not -Match 'Plugin-Version: 1.2'
  }
  AfterAll {
    docker volume rm -f $dockerVolume
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] plugins are installed with jenkins-plugin-cli and no war" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli-no-war $PSScriptRoot/plugins-cli/no-war
    $exitCode | Should -Be 0
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[plugins-cli > $global:TEST_TAG] Use a custom jenkins.war" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'builds child image' {
    $exitCode, $stdout, $stderr = Build-DockerChild $global:SUT_IMAGE-plugins-cli-custom-war $PSScriptRoot/plugins-cli/custom-war --no-cache
    $exitCode | Should -Be 0
  }
}


================================================
FILE: tests/plugins-cli.bats
================================================
#!/usr/bin/env bats

# bats file_tags=test-suite:plugin-cli

load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load test_helpers

SUT_IMAGE=$(get_sut_image)
SUT_DESCRIPTION="${IMAGE}-plugins-cli"

teardown() {
  clean_work_directory "${BATS_TEST_DIRNAME}" "${SUT_IMAGE}"
}

@test "[${SUT_DESCRIPTION}] plugins are installed with jenkins-plugin-cli" {
  local custom_sut_image
  custom_sut_image="$(get_test_image)"
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image}" "${BATS_TEST_DIRNAME}/plugins-cli"
  assert_success
  refute_line --partial 'Skipping already installed dependency'

  run docker run --rm "${custom_sut_image}" ls --color=never -1 /var/jenkins_home/plugins
  assert_success
  assert_line 'junit.jpi'
  assert_line 'junit.jpi.pinned'
  assert_line 'ant.jpi'
  assert_line 'ant.jpi.pinned'
  assert_line 'credentials.jpi'
  assert_line 'credentials.jpi.pinned'
  assert_line 'mesos.jpi'
  assert_line 'mesos.jpi.pinned'
  # optional dependencies
  refute_line 'metrics.jpi'
  refute_line 'metrics.jpi.pinned'
  # plugins bundled but under detached-plugins, so need to be installed
  assert_line 'mailer.jpi'
  assert_line 'mailer.jpi.pinned'
  assert_line 'git.jpi'
  assert_line 'git.jpi.pinned'
  assert_line 'filesystem_scm.jpi'
  assert_line 'filesystem_scm.jpi.pinned'
  assert_line 'docker-plugin.jpi'
  assert_line 'docker-plugin.jpi.pinned'
}

@test "[${SUT_DESCRIPTION}] plugins are installed with jenkins-plugin-cli with non-default REF" {
  local custom_sut_image custom_ref
  custom_sut_image="$(get_test_image)"
  custom_ref=/var/lib/jenkins/ref

  # Build a custom image to validate the build time behavior
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image}" "${BATS_TEST_DIRNAME}/plugins-cli/ref" --build-arg REF="${custom_ref}"
  assert_success
  refute_line --partial 'Skipping already installed dependency'

  volume_name="$(docker volume create)"
  # Start an image with the default entrypoint to test the runtime behavior
  run docker run --volume "${volume_name}:/var/jenkins_home" --rm "${custom_sut_image}" true
  assert_success

  # Check the content of the resulting data volume (expecting installed plugins as present and pinned)
  run bash -c "docker run --rm --volume ${volume_name}:/var/jenkins_home ${custom_sut_image} ls --color=never -1 /var/jenkins_home/plugins \
    | tr -d '\r' `# replace DOS line endings \r\n`"
  assert_success
  assert_line 'junit.jpi'
  assert_line 'junit.jpi.pinned'
  assert_line 'ant.jpi'
  assert_line 'ant.jpi.pinned'
}

@test "[${SUT_DESCRIPTION}] plugins are installed with jenkins-plugin-cli from a plugins file" {
  local custom_sut_image
  custom_sut_image="$(get_test_image)"

  # Then proceed with child
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image}" "${BATS_TEST_DIRNAME}/plugins-cli/pluginsfile"
  assert_success
  refute_line --partial 'Skipping already installed dependency'
  # replace DOS line endings \r\n
  run bash -c "docker run --rm ${custom_sut_image} ls --color=never -1 /var/jenkins_home/plugins | tr -d '\r'"
  assert_success
  assert_line 'junit.jpi'
  assert_line 'junit.jpi.pinned'
  assert_line 'ant.jpi'
  assert_line 'ant.jpi.pinned'
  assert_line 'credentials.jpi'
  assert_line 'credentials.jpi.pinned'
  assert_line 'mesos.jpi'
  assert_line 'mesos.jpi.pinned'
  # optional dependencies
  refute_line 'metrics.jpi'
  refute_line 'metrics.jpi.pinned'
  # plugins bundled but under detached-plugins, so need to be installed
  assert_line 'mailer.jpi'
  assert_line 'mailer.jpi.pinned'
  assert_line 'git.jpi'
  assert_line 'git.jpi.pinned'
  assert_line 'filesystem_scm.jpi'
  assert_line 'filesystem_scm.jpi.pinned'
}

@test "[${SUT_DESCRIPTION}] plugins are getting upgraded but not downgraded" {
  local custom_sut_image_first custom_sut_image_second
  custom_sut_image_first="$(get_test_image)"
  custom_sut_image_second="${custom_sut_image_first}-2"

  # Build first image with junit 1.6 and ant-plugin 1.3
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image_first}" "${BATS_TEST_DIRNAME}/plugins-cli"
  assert_success

  local volume_name
  volume_name="$(docker volume create)"

  # Generates a jenkins home (in the volume) with the plugins junit 1.6 and ant-plugin 1.3 from first image's reference
  run docker run --volume "$volume_name:/var/jenkins_home" --rm "${custom_sut_image_first}" true
  assert_success
  run unzip_manifest junit.jpi "$volume_name"
  assert_line 'Plugin-Version: 1.6'
  run unzip_manifest ant.jpi "$volume_name"
  assert_line 'Plugin-Version: 1.3'

  # Build second image with junit 1.28 and ant 1.2
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image_second}" "${BATS_TEST_DIRNAME}/upgrade-plugins"
  assert_success

  # Execute the second image with the existing jenkins volume: junit plugin should be updated, and ant should NOT be downgraded
  run docker run --volume "$volume_name:/var/jenkins_home" --rm "${custom_sut_image_second}" true
  assert_success
  run unzip_manifest junit.jpi "$volume_name"
  assert_success
  # Should be updated
  assert_line 'Plugin-Version: 1.28'
  run unzip_manifest ant.jpi "$volume_name"
  # 1.2 is older than the existing 1.3, so keep 1.3
  assert_line 'Plugin-Version: 1.3'
}

@test "[${SUT_DESCRIPTION}] do not upgrade if plugin has been manually updated" {
  local custom_sut_image_first custom_sut_image_second
  custom_sut_image_first="$(get_test_image)"
  custom_sut_image_second="${custom_sut_image_first}-2"

  ## Generates an image with the plugin junit 1.6
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image_first}" "${BATS_TEST_DIRNAME}/plugins-cli"
  assert_success

  ## Image contains junit 1.6, which is manually upgraded to 1.8
  local volume_name
  volume_name="$(docker volume create)"
  run docker run --volume "${volume_name}:/var/jenkins_home" --rm "${custom_sut_image_first}" \
    curl --connect-timeout 20 --retry 5 --retry-delay 0 --retry-max-time 60 --silent \
      --fail --location https://updates.jenkins.io/download/plugins/junit/1.8/junit.hpi \
      --output /var/jenkins_home/plugins/junit.jpi
  assert_success
  run unzip_manifest junit.jpi "$volume_name"
  assert_line 'Plugin-Version: 1.8'

  ## Generates an image with the plugin junit 1.28 (upgraded)
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image_second}" "${BATS_TEST_DIRNAME}/upgrade-plugins"
  assert_success

  # The image with junit 1.28 should not upgrade the version 1.8 in the volume (jenkins_home)
  run docker run --volume "${volume_name}:/var/jenkins_home" --rm ${custom_sut_image_second} true
  assert_success
  # junit shouldn't be upgraded
  run unzip_manifest junit.jpi "$volume_name"
  assert_success
  assert_line 'Plugin-Version: 1.8'
  refute_line 'Plugin-Version: 1.28'
}

@test "[${SUT_DESCRIPTION}] upgrade plugin even if it has been manually updated when PLUGINS_FORCE_UPGRADE=true" {
  local custom_sut_image_first custom_sut_image_second
  custom_sut_image_first="$(get_test_image)"
  custom_sut_image_second="${custom_sut_image_first}-2"

  ## Generates an image with the plugin junit 1.6
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image_first}" "${BATS_TEST_DIRNAME}/plugins-cli"
  assert_success

  ## Image contains junit 1.6, which is manually upgraded to 1.8
  local volume_name
  volume_name="$(docker volume create)"
  run docker run --volume "${volume_name}:/var/jenkins_home" --rm "${custom_sut_image_first}" \
    curl --connect-timeout 20 --retry 5 --retry-delay 0 --retry-max-time 60 --silent \
      --fail --location https://updates.jenkins.io/download/plugins/junit/1.8/junit.hpi \
      --output /var/jenkins_home/plugins/junit.jpi
  assert_success
  run unzip_manifest junit.jpi "$volume_name"
  assert_line 'Plugin-Version: 1.8'

  ## Generates an image with the plugin junit 1.28 (upgraded)
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image_second}" "${BATS_TEST_DIRNAME}/upgrade-plugins"
  assert_success

  # The image with junit 1.28 should force-upgrade junit in the volume (jenkins_home)
  run docker run --volume "${volume_name}:/var/jenkins_home" --env PLUGINS_FORCE_UPGRADE=true --rm ${custom_sut_image_second} true
  assert_success
  # junit shouldn't be upgraded
  run unzip_manifest junit.jpi "$volume_name"
  assert_success
  refute_line 'Plugin-Version: 1.8'
  assert_line 'Plugin-Version: 1.28'
}


@test "[${SUT_DESCRIPTION}] plugins are installed with jenkins-plugin-cli and no war" {
  local custom_sut_image
  custom_sut_image="$(get_test_image)"
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image}" "${BATS_TEST_DIRNAME}/plugins-cli/no-war"
  assert_success
}

@test "[${SUT_DESCRIPTION}] Use a custom jenkins.war" {
  local custom_sut_image
  custom_sut_image="$(get_test_image)"
  # Build the image using the right Dockerfile setting a new war with JENKINS_WAR env and with a weird plugin inside
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image}" "${BATS_TEST_DIRNAME}/plugins-cli/custom-war"
  assert_success
}

@test "[${SUT_DESCRIPTION}] JAVA_OPTS environment variable is used with jenkins-plugin-cli" {
  local custom_sut_image
  custom_sut_image="$(get_test_image)"
  run docker_build_child "${SUT_IMAGE}" "${custom_sut_image}" "${BATS_TEST_DIRNAME}/plugins-cli/java-opts"
  assert_success
  # Assert JAVA_OPTS has been used and 'java.opts.test' has been set to JVM
  assert_line --regexp 'java.opts.test.*=.*true'
}


================================================
FILE: tests/runtime.Tests.ps1
================================================
Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1
Import-Module -DisableNameChecking -Force $PSScriptRoot/test_helpers.psm1

$global:SUT_IMAGE=Get-SutImage
$global:SUT_CONTAINER=Get-SutImage
$global:TEST_TAG=$global:SUT_IMAGE.Replace('pester-jenkins-', '')

Describe "[runtime > $global:TEST_TAG] build image" {
  BeforeEach {
    Push-Location -StackName 'jenkins' -Path "$PSScriptRoot/.."
  }

  It 'builds image' {
    $exitCode, $stdout, $stderr = Build-Docker $global:SUT_IMAGE
    $exitCode | Should -Be 0
  }

  AfterEach {
    Pop-Location -StackName 'jenkins'
  }
}

Describe "[runtime > $global:TEST_TAG] cleanup container" {
  It 'cleanup' {
    Cleanup $global:SUT_CONTAINER | Out-Null
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[runtime > $global:TEST_TAG] test multiple JENKINS_OPTS" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It '"--help --version" should return the version, not the help' {
    # need the last line of output
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm -e JENKINS_OPTS=`"--help --version`" --name $global:SUT_CONTAINER -P $global:SUT_IMAGE"
    $exitCode | Should -Be 0
    $stdout -split '`n' | %{$_.Trim()} | Select-Object -Last 1 | Should -Be $env:JENKINS_VERSION
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[runtime > $global:TEST_TAG] test jenkins arguments" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  It 'running --help --version should return the version, not the help' {
    # need the last line of output
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "run --rm --name $global:SUT_CONTAINER -P $global:SUT_IMAGE --help --version"
    $exitCode | Should -Be 0
    $stdout -split '`n' | %{$_.Trim()} | Select-Object -Last 1 | Should -Be $env:JENKINS_VERSION
  }

  It 'version in docker metadata' {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "inspect -f `"{{index .Config.Labels \`"org.opencontainers.image.version\`"}}`" $global:SUT_IMAGE"
    $exitCode | Should -Be 0
    $stdout.Trim() | Should -Match $env:JENKINS_VERSION
  }

  It 'commit SHA in docker metadata' {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "inspect -f `"{{index .Config.Labels \`"org.opencontainers.image.revision\`"}}`" $global:SUT_IMAGE"
    $exitCode | Should -Be 0
    $stdout.Trim() | Should -Match $env:COMMIT_SHA
  }
}

# Only test on Java 21, one JDK is enough to test all versions
Describe "[runtime > $global:TEST_TAG] passing JVM parameters" -Skip:(-not $global:TEST_TAG.Contains('jdk21-')) {
  BeforeAll {
    $tzSetting = '-Duser.timezone=Europe/Madrid'
    $tzRegex = [regex]::Escape("Europe/Madrid")

    $cspSetting = @'
-Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';\"
'@
    $cspRegex = [regex]::Escape("default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';")

    function Start-With-Opts() {
      Param (
        [string] $JAVA_OPTS,
        [string] $JENKINS_JAVA_OPTS
      )

      $cmd = "docker --% run -d --name $global:SUT_CONTAINER -P"
      if ($JAVA_OPTS.length -gt 0) {
        $cmd += " -e JAVA_OPTS=`"$JAVA_OPTS`""
      }
      if ($JENKINS_JAVA_OPTS.length -gt 0) {
        $cmd += " -e JENKINS_JAVA_OPTS=`"$JENKINS_JAVA_OPTS`""
      }
      $cmd += " $global:SUT_IMAGE"

      Invoke-Expression $cmd
      $lastExitCode | Should -Be 0

      # give time to eventually fail to initialize
      Start-Sleep -Seconds 5
      Retry-Command -RetryCount 3 -Delay 1 -ScriptBlock { docker inspect -f "{{.State.Running}}" $global:SUT_CONTAINER ; if($lastExitCode -ne 0) { throw('Docker inspect failed') } } -Verbose | Should -BeTrue

      # it takes a while for jenkins to be up enough
      Retry-Command -RetryCount 30 -Delay 5 -ScriptBlock { Test-Url $global:SUT_CONTAINER "/api/json" } -Verbose | Should -BeTrue
    }

    function Get-Csp-Value() {
      return (Run-In-Script-Console $global:SUT_CONTAINER "System.getProperty('hudson.model.DirectoryBrowserSupport.CSP')")
    }

    function Get-Timezone-Value() {
      return (Run-In-Script-Console $global:SUT_CONTAINER "System.getProperty('user.timezone')")
    }
  }

  It 'passes JAVA_OPTS' {
    Start-With-Opts -JAVA_OPTS "$tzSetting $cspSetting"

    Get-Csp-Value | Should -Match $cspRegex
    Get-Timezone-Value | Should -Match $tzRegex
  }

  It 'passes JENKINS_JAVA_OPTS' {
    Start-With-Opts -JENKINS_JAVA_OPTS "$tzSetting $cspSetting"

    Get-Csp-Value | Should -Match $cspRegex
    Get-Timezone-Value | Should -Match $tzRegex
  }

  It 'JENKINS_JAVA_OPTS overrides JAVA_OPTS' {
    Start-With-Opts -JAVA_OPTS "$tzSetting -Dhudson.model.DirectoryBrowserSupport.CSP=\`"default-src 'self';\`"" -JENKINS_JAVA_OPTS "$cspSetting"

    Get-Csp-Value | Should -Match $cspRegex
    Get-Timezone-Value | Should -Match $tzRegex
  }

  AfterEach {
    Cleanup $global:SUT_CONTAINER | Out-Null
  }
}


================================================
FILE: tests/runtime.bats
================================================
#!/usr/bin/env bats

# bats file_tags=test-suite:runtime

load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load test_helpers

IMAGE=${IMAGE:-debian_jdk17}
SUT_IMAGE=$(get_sut_image)
SUT_DESCRIPTION="${IMAGE}-runtime"

teardown() {
  cleanup "$(get_sut_container_name)"
}

@test "[${SUT_DESCRIPTION}] test version in docker metadata" {
  local version
  version=$(get_jenkins_version)
  assert "${version}" docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version"}}' $SUT_IMAGE
}

@test "[${SUT_DESCRIPTION}] test commit SHA in docker metadata is not empty" {
  run docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision"}}' $SUT_IMAGE
  refute_output ""
}

@test "[${SUT_DESCRIPTION}] test commit SHA in docker metadata" {
  local revision
  revision=$(get_commit_sha)
  assert "${revision}" docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision"}}' $SUT_IMAGE
}

@test "[${SUT_DESCRIPTION}] test multiple JENKINS_OPTS" {
  local container_name version
  # running --help --version should return the version, not the help
  version=$(get_jenkins_version)
  container_name="$(get_sut_container_name)"
  cleanup "${container_name}"
  # need the last line of output
  assert "${version}" docker run --rm --env JENKINS_OPTS="--help --version" --name "${container_name}" -P $SUT_IMAGE | tail -n 1
}

# bats test_tags=test-type:golden-file
@test "[${SUT_DESCRIPTION}] ensure expected environment variables are set" {
  local container_name
  container_name="$(get_sut_container_name)"
  cleanup "${container_name}"

  # Excluding HOSTNAME as its value is variable, and 'container=oci' existing only in RHEL images
  assert_matches_golden expected_env_vars_except_hostname docker run --rm --name "${container_name}" "${SUT_IMAGE}" bash -c "env | sort | grep -v -e HOSTNAME -e container=oci"
}

@test "[${SUT_DESCRIPTION}] test jenkins arguments" {
  local container_name version
  # running --help --version should return the version, not the help
  version=$(get_jenkins_version)
  container_name="$(get_sut_container_name)"
  cleanup "${container_name}"
  # need the last line of output
  assert "${version}" docker run --rm --name "${container_name}" -P $SUT_IMAGE --help --version | tail -n 1
}

@test "[${SUT_DESCRIPTION}] timezones are handled correctly" {
  local timezone1 timezone2 container_name
  container_name="$(get_sut_container_name)"
  cleanup "${container_name}"

  run docker run --rm --name "${container_name}" $SUT_IMAGE bash -c "date +'%Z %z'"
  timezone1="${output}"
  assert_equal "${timezone1}" "UTC +0000"

  run docker run --rm --name "${container_name}" --env "TZ=Europe/Luxembourg" $SUT_IMAGE bash -c "date +'%Z %z'"
  timezone1="${output}"
  run docker run --rm --name "${container_name}" --env "TZ=Australia/Sydney" $SUT_IMAGE bash -c "date +'%Z %z'"
  timezone2="${output}"

  refute [ "${timezone1}" = "${timezone2}" ]
}

@test "[${SUT_DESCRIPTION}] has utf-8 locale" {
  run docker run --rm "${SUT_IMAGE}" locale charmap
  assert_equal "${output}" "UTF-8"
}

# parameters are passed as docker run parameters
start-jenkins-with-jvm-opts() {
  local container_name
  container_name="$(get_sut_container_name)"
  cleanup "${container_name}"

  run docker run --detach --name "${container_name}" --publish-all "$@" $SUT_IMAGE
  assert_success

  # Container is running
  sleep 1  # give time to eventually fail to initialize
  retry 3 1 assert "true" docker inspect -f '{{.State.Running}}' "${container_name}"

  # Jenkins is initialized
  retry 30 5 test_url /api/json
}

get-csp-value() {
  runInScriptConsole "System.getProperty('hudson.model.DirectoryBrowserSupport.CSP')"
}

get-timezone-value() {
  runInScriptConsole "System.getProperty('user.timezone')"
}

runInScriptConsole() {
  SERVER="$(get_jenkins_url)"
  COOKIEJAR="$(mktemp)"
  PASSWORD="$(get_jenkins_password)"
  CRUMB=$(curl -u "admin:$PASSWORD" --cookie-jar "$COOKIEJAR" "$SERVER/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,%22:%22,//crumb)")

  bash -c "curl -fssL -X POST -u \"admin:$PASSWORD\" --cookie \"$COOKIEJAR\" -H \"$CRUMB\" \"$SERVER\"/scriptText -d script=\"$1\" | sed -e 's/Result: //'"
}

# bats test_tags=use:start-jenkins-with-jvm-opts
@test "[${SUT_DESCRIPTION}] passes JAVA_OPTS as JVM options" {
  start-jenkins-with-jvm-opts --env JAVA_OPTS="-Duser.timezone=Europe/Madrid -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';\""

  # JAVA_OPTS are used
  assert "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" get-csp-value
  assert 'Europe/Madrid' get-timezone-value
}

# bats test_tags=use:start-jenkins-with-jvm-opts
@test "[${SUT_DESCRIPTION}] passes JENKINS_JAVA_OPTS as JVM options" {
  start-jenkins-with-jvm-opts --env JENKINS_JAVA_OPTS="-Duser.timezone=Europe/Madrid -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';\""

  # JENKINS_JAVA_OPTS are used
  assert "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" get-csp-value
  assert 'Europe/Madrid' get-timezone-value
}

# bats test_tags=use:start-jenkins-with-jvm-opts
@test "[${SUT_DESCRIPTION}] JENKINS_JAVA_OPTS overrides JAVA_OPTS" {
  start-jenkins-with-jvm-opts \
    --env JAVA_OPTS="-Duser.timezone=Europe/Madrid -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'\"" \
    --env JENKINS_JAVA_OPTS="-Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';\""

  # JAVA_OPTS and JENKINS_JAVA_OPTS are used
  assert "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" get-csp-value
  assert 'Europe/Madrid' get-timezone-value
}

@test "[${SUT_DESCRIPTION}] ensure that 'ps' command is available" {
  command -v ps # Check for binary presence in the current PATH
}


================================================
FILE: tests/test_helpers.bash
================================================
#!/bin/bash
set -euo pipefail

# Assert that $1 is the outputof a command $2
function assert {
    local expected_output=$1
    shift
    local actual_output
    actual_output=$("$@")
    actual_output="${actual_output//[$'\t\r\n']}" # remove newlines
    if ! [ "$actual_output" = "$expected_output" ]; then
        echo "expected: \"$expected_output\""
        echo "actual:   \"$actual_output\""
        false
    fi
}

# Assert that golden file $1 matches the output of a command $2
assert_matches_golden() {
    local golden="$1"
    shift
    local golden_path="tests/golden/${golden}.txt"

    if [[ ! -f "${golden_path}" ]]; then
        echo "Golden file '${golden_path}' does not exist"
        return 1
    fi

    # Run the command passed as arguments and capture its output
    local output
    output="$(mktemp)"
    "$@" > "${output}"

    # Compare with golden file
    diff -u "${golden_path}" <(cat "${output}")
}

# Retry a command $1 times until it succeeds. Wait $2 seconds between retries.
function retry {
    local attempts=$1
    shift
    local delay=$1
    shift
    local i

    for ((i=0; i < attempts; i++)); do
        run "$@"
        # shellcheck disable=SC2154
        if [ "$status" -eq 0 ]; then
            return 0
        fi
        sleep "${delay}"
    done

    # shellcheck disable=SC2154
    echo "Command \"$*\" failed $attempts times. Status: $status. Output: $output" >&2
    false
}

function get_sut_image {
    test -n "${IMAGE:?"[sut_image] Please set the variable 'IMAGE' to the name of the image to test in 'docker-bake.hcl'."}"
    ## Retrieve the SUT image name from buildx
    # Option --print for 'docker buildx bake' prints the JSON configuration on the stdout
    # Option --silent for 'make' suppresses the echoing of command so the output is valid JSON
    # The image name is the 1st of the "tags" array, on the first "image" found
    make --silent show | jq -r '.target."'"${IMAGE}"'".tags[0]'
}

function get_jenkins_version() {
  test -n "${IMAGE:?"[sut_image] Please set the variable 'IMAGE' to the name of the image to test in 'docker-bake.hcl'."}"

  make --silent show | jq -r '.target."'"${IMAGE}"'".args.JENKINS_VERSION'
}

function get_commit_sha() {
  test -n "${IMAGE:?"[sut_image] Please set the variable 'IMAGE' to the name of the image to test in 'docker-bake.hcl'."}"

  make --silent show | jq -r '.target."'"${IMAGE}"'".args.COMMIT_SHA'
}

function get_test_image {
    test -n "${BATS_TEST_NUMBER:?"[get_test_image] Please set the variable BATS_TEST_NUMBER."}"
    test -n "${SUT_DESCRIPTION:?"[get_test_image] Please set the variable SUT_DESCRIPTION."}"
    echo "${SUT_DESCRIPTION}-${BATS_TEST_NUMBER}"
}

function get_sut_container_name {
    echo "$(get_test_image)-container"
}

function docker_build_child {
    local parent=$1; shift
    local tag=$1; shift
    local dir=$1; shift
    local build_opts=("$@")
    local tmp
    tmp=$(mktemp "$dir/Dockerfile.XXXXXX")
    sed -e "s#FROM bats-jenkins.*#FROM ${parent}#g" "$dir/Dockerfile" > "$tmp"
    docker build --tag "$tag" --no-cache "${build_opts[@]}" --file "${tmp}" "${dir}" 2>&1
    rm "$tmp"
}

function get_jenkins_url {
    docker_host="${DOCKER_HOST:-}"
    if [ -z "${docker_host}" ]; then
        DOCKER_IP=localhost
    else
        # shellcheck disable=SC2001
        DOCKER_IP=$(echo "${docker_host}" | sed -e 's|tcp://\(.*\):[0-9]*|\1|')
    fi
    echo "http://$DOCKER_IP:$(docker port "$(get_sut_container_name)" 8080 | cut -d: -f2)"
}

function get_jenkins_password {
    docker exec "$(get_sut_container_name)" cat /var/jenkins_home/secrets/initialAdminPassword
}

function get_targets_from_jenkinsfile {
    sed -n '/def images = \[/,/]/p' Jenkinsfile `# retrieve images array from Jenkinsfile` \
     | grep "'" `# keep only its items` \
     | tr -d "', " `# cleanup output` \
     | sort `# ensure constant output sort`
}

function get_default_docker_bake_linux_targets {
    make --silent show-linux | jq -r '.target | keys[]' | sort
}

function test_url {
    run curl --user "admin:$(get_jenkins_password)" --output /dev/null --silent --head --fail --connect-timeout 30 --max-time 60 "$(get_jenkins_url)$1"
    if [ "$status" -eq 0 ]; then
        true
    else
        echo "URL $(get_jenkins_url)$1 failed" >&2
        echo "output: $output" >&2
        false
    fi
}

function cleanup {
    docker kill "$1" &>/dev/null ||:
    docker rm -fv "$1" &>/dev/null ||:
}

function unzip_manifest {
    local plugin=$1
    local volume_name=$2
    export SUT_IMAGE
    docker run --rm --volume "${volume_name}:/var/jenkins_home" --entrypoint unzip "${SUT_IMAGE}" \
        -p "/var/jenkins_home/plugins/${plugin}" META-INF/MANIFEST.MF | tr -d '\r'
}

function clean_work_directory {
    local workdir=$1
    local sut_image=$2
    rm -rf "${workdir}/upgrade-plugins/work-${sut_image}"
}


================================================
FILE: tests/test_helpers.psm1
================================================
Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1

function Test-CommandExists($command) {
  $oldPreference = $ErrorActionPreference
  $ErrorActionPreference = 'stop'
  $res = $false
  try {
      if(Get-Command $command) {
          $res = $true
      }
  } catch {
      $res = $false
  } finally {
      $ErrorActionPreference=$oldPreference
  }
  return $res
}

# check dependencies
if(-Not (Test-CommandExists docker)) {
    Write-Error "docker is not available"
}

function Retry-Command {
    [CmdletBinding()]
    param (
        [parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [scriptblock] $ScriptBlock,
        [int] $RetryCount = 3,
        [int] $Delay = 30,
        [string] $SuccessMessage = "Command executed successfully!",
        [string] $FailureMessage = "Failed to execute the command"
        )

    process {
        $Attempt = 1
        $Flag = $true

        do {
            try {
                $PreviousPreference = $ErrorActionPreference
                $ErrorActionPreference = 'Stop'
                Invoke-Command -NoNewScope -ScriptBlock $ScriptBlock -OutVariable Result 4>&1
                $ErrorActionPreference = $PreviousPreference

                # flow control will execute the next line only if the command in the scriptblock executed without any errors
                # if an error is thrown, flow control will go to the 'catch' block
                Write-Verbose "$SuccessMessage `n"
                $Flag = $false
            }
            catch {
                if ($Attempt -gt $RetryCount) {
                    Write-Verbose "$FailureMessage! Total retry attempts: $RetryCount"
                    Write-Verbose "[Error Message] $($_.exception.message) `n"
                    $Flag = $false
                } else {
                    Write-Verbose "[$Attempt/$RetryCount] $FailureMessage. Retrying in $Delay seconds..."
                    Start-Sleep -Seconds $Delay
                    $Attempt = $Attempt + 1
                }
            }
        }
        While ($Flag)
    }
}

function Get-SutImage {
    # TODO: don't hardcode Windows flavor
    $DOCKERFILE = 'windows/windowsservercore/hotspot/Dockerfile'
    $IMAGETAG = Get-EnvOrDefault 'CONTROLLER_TAG' ''

    $REAL_DOCKERFILE=Resolve-Path -Path "$PSScriptRoot/../${DOCKERFILE}"

    if(!($DOCKERFILE -match '^(?<os>.+)[\\/](?<flavor>.+)[\\/](?<jvm>.+)[\\/]Dockerfile$') -or !(Test-Path $REAL_DOCKERFILE)) {
        Write-Error "Wrong Dockerfile path format or file does not exist: $DOCKERFILE"
        exit 1
    }

    return "pester-jenkins-$IMAGETAG"
}

function Run-Program($cmd, $params, $verbose=$false) {
    if($verbose) {
        Write-Host "$cmd $params"
    }
    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.CreateNoWindow = $true
    $psi.UseShellExecute = $false
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.WorkingDirectory = (Get-Location)
    $psi.FileName = $cmd
    $psi.Arguments = $params
    $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo = $psi
    [void]$proc.Start()
    $stdout = $proc.StandardOutput.ReadToEnd()
    $stderr = $proc.StandardError.ReadToEnd()
    $proc.WaitForExit()
    if($proc.ExitCode -ne 0) {
        Write-Host "`n`nstdout:`n$stdout`n`nstderr:`n$stderr`n`n"
    }

    return $proc.ExitCode, $stdout, $stderr
}

function Build-Docker($tag) {
    $windowsVersion = '2019'
    if ($tag -match 'ltsc(\d+)$') {
        $windowsVersion = $matches[1]
    }
    $composeParams = '--file=build-windows_windowsservercore-ltsc{0}.yaml build --parallel' -f $windowsVersion
    $exitCode, $stdout, $stderr = Run-Program 'docker-compose' $composeParams
    if($exitCode -ne 0) {
        return $exitCode, $stdout, $stderr
    }
    return(Run-Program 'docker' $('tag {0}:{1} {2}' -f $env:DOCKERHUB_ORG_REPO, $env:CONTROLLER_TAG, $tag))
}

function Build-DockerChild($tag, $dir) {
    Get-Content "$dir/Dockerfile-windows" | ForEach-Object{$_ -replace "FROM bats-jenkins","FROM $(Get-SutImage)" } | Out-File -FilePath "$dir/Dockerfile-windows.tmp" -Encoding ASCII
    return (Run-Program 'docker.exe' "build -t `"$tag`" $args -f `"$dir/Dockerfile-windows.tmp`" `"$dir`"")
}

function Get-JenkinsUrl($Container) {
    $DOCKER_IP=(Get-EnvOrDefault 'DOCKER_HOST' 'localhost') | %{$_ -replace 'tcp://(.*):[0-9]*','$1'} | Select-Object -First 1
    $port = (docker port "$CONTAINER" 8080 | %{$_ -split ':'})[1]
    return "http://$($DOCKER_IP):$($port)"
}

function Get-JenkinsPassword($Container) {
    $res = docker exec $Container powershell.exe -c 'if(Test-Path "C:\ProgramData\Jenkins\JenkinsHome\secrets\initialAdminPassword") { Get-Content "C:\ProgramData\Jenkins\JenkinsHome\secrets\initialAdminPassword" ; exit 0 } else { exit -1 }'
    if($lastExitCode -eq 0) {
        return $res
    }
    return $null
}

function Run-In-Script-Console($Container, $Script) {
    $jenkinsPassword = Get-JenkinsPassword $Container
    $jenkinsUrl = Get-JenkinsUrl $Container
    if($null -ne $jenkinsPassword) {
        $pair = "admin:$($jenkinsPassword)"
        $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
        $basicAuthValue = "Basic $encodedCreds"
        $Headers = @{ Authorization = $basicAuthValue }

        $crumb = (Invoke-RestMethod -Uri $('{0}{1}' -f $jenkinsUrl, '/crumbIssuer/api/json') -Headers $Headers -TimeoutSec 60 -Method Get -SessionVariable session -UseBasicParsing).crumb
        if ($null -ne $crumb) {
            $Headers += @{ "Jenkins-Crumb" = $c
Download .txt
gitextract_gxy0y0_6/

├── .ci/
│   └── publish.sh
├── .git-blame-ignore-revs
├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   ├── release-drafter.yml
│   └── workflows/
│       ├── release-drafter.yml
│       └── updatecli.yaml
├── .gitignore
├── .gitmodules
├── .hadolint.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── HACKING.adoc
├── Jenkinsfile
├── LICENSE.txt
├── Makefile
├── README.md
├── SECURITY.md
├── alpine/
│   └── hotspot/
│       └── Dockerfile
├── debian/
│   └── Dockerfile
├── docker-bake.hcl
├── jdk-download-url.sh
├── jdk-download.sh
├── jenkins-plugin-cli.ps1
├── jenkins-plugin-cli.sh
├── jenkins-support
├── jenkins-support.psm1
├── jenkins.io-2026.key
├── jenkins.ps1
├── jenkins.sh
├── make.ps1
├── rhel/
│   └── Dockerfile
├── tests/
│   ├── bake.bats
│   ├── functions/
│   │   ├── .ssh/
│   │   │   └── config
│   │   ├── Dockerfile
│   │   └── Dockerfile-windows
│   ├── functions.Tests.ps1
│   ├── functions.bats
│   ├── golden/
│   │   ├── expected_env_vars_except_hostname.txt
│   │   ├── expected_platforms.txt
│   │   ├── expected_tags.txt
│   │   ├── expected_tags_latest_lts.txt
│   │   └── expected_tags_latest_weekly.txt
│   ├── jenkinsfile.bats
│   ├── plugins-cli/
│   │   ├── Dockerfile
│   │   ├── Dockerfile-windows
│   │   ├── custom-war/
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile-windows
│   │   │   └── WEB-INF/
│   │   │       └── plugins/
│   │   │           └── my-happy-plugin.hpi
│   │   ├── java-opts/
│   │   │   └── Dockerfile
│   │   ├── no-war/
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile-windows
│   │   │   └── plugins.txt
│   │   ├── pluginsfile/
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile-windows
│   │   │   └── plugins.txt
│   │   ├── ref/
│   │   │   ├── Dockerfile
│   │   │   └── Dockerfile-windows
│   │   └── update/
│   │       ├── Dockerfile
│   │       └── Dockerfile-windows
│   ├── plugins-cli.Tests.ps1
│   ├── plugins-cli.bats
│   ├── runtime.Tests.ps1
│   ├── runtime.bats
│   ├── test_helpers.bash
│   ├── test_helpers.psm1
│   ├── update-golden-file.sh
│   └── upgrade-plugins/
│       ├── Dockerfile
│       └── Dockerfile-windows
├── tini_pub.gpg
├── tools/
│   ├── hadolint
│   └── shellcheck
├── updatecli/
│   ├── scripts/
│   │   └── rhel-latest-tag.sh
│   ├── updatecli.d/
│   │   ├── alpine.yaml
│   │   ├── debian.yaml
│   │   ├── git-lfs.yaml
│   │   ├── hadolint.yaml
│   │   ├── jdk17.yaml
│   │   ├── jdk21.yaml
│   │   ├── jdk25.yaml
│   │   ├── jenkins-version-simulated-lts.yaml
│   │   ├── jenkins-version.yaml
│   │   ├── plugin-installation-manager-tool.yaml
│   │   ├── rhel.yaml
│   │   └── shellcheck.yaml
│   └── values.github-action.yaml
└── windows/
    └── windowsservercore/
        └── hotspot/
            └── Dockerfile
Condensed preview — 88 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (250K chars).
[
  {
    "path": ".ci/publish.sh",
    "chars": 2947,
    "preview": "#!/bin/bash -eu\n\n# Publish any versions of the docker image not yet pushed to ${JENKINS_REPO}\n# Arguments:\n#   -n dry ru"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 247,
    "preview": "# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-and-understanding-files#ignore-commits-"
  },
  {
    "path": ".github/CODEOWNERS",
    "chars": 75,
    "preview": "* @jenkinsci/team-docker-packaging\n*/debian @ksalerno99\n*/rhel @ksalerno99\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 76,
    "preview": "community_bridge: jenkins\ncustom: [\"https://jenkins.io/donate/#why-donate\"]\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 350,
    "preview": "# Per https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates\nversion:"
  },
  {
    "path": ".github/release-drafter.yml",
    "chars": 503,
    "preview": "# https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc\n\n_extends: github:jenkinsci/.github:/.gi"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "chars": 791,
    "preview": "# Note: additional setup is required, see https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc\n"
  },
  {
    "path": ".github/workflows/updatecli.yaml",
    "chars": 1022,
    "preview": "name: updatecli\non:\n  # Allow to be run manually\n  workflow_dispatch:\n  schedule:\n    # Run once per week (to avoid aler"
  },
  {
    "path": ".gitignore",
    "chars": 384,
    "preview": "*.tmp\nbats/\ntarget/\ntests/functions/init.groovy.d/\ntests/functions/java_cp/\ntests/functions/copy_reference_file.log\ntest"
  },
  {
    "path": ".gitmodules",
    "chars": 259,
    "preview": "[submodule \"tests/test_helper/bats-support\"]\n\tpath = tests/test_helper/bats-support\n\turl = https://github.com/ztombol/ba"
  },
  {
    "path": ".hadolint.yml",
    "chars": 912,
    "preview": "# Hadolint configuration file\n---\n# configure ignore rules\n# see https://github.com/hadolint/hadolint#rules for a list o"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 572,
    "preview": "Changelog\n=========\n\n| See [GitHub releases](https://github.com/jenkinsci/docker/releases) |\n| --- |\n\nThese release note"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 955,
    "preview": "# Issues and Contributing\n\nPlease note that only issues related to this Docker image will be addressed here.\n\n* If you h"
  },
  {
    "path": "HACKING.adoc",
    "chars": 8244,
    "preview": "= Hacking documentation\n\nThis document explains how to develop on this repository.\n\n== Requirements\n\n* A Bourne-Again-Sh"
  },
  {
    "path": "Jenkinsfile",
    "chars": 10242,
    "preview": "#!/usr/bin/env groovy\n\ndef listOfProperties = []\nlistOfProperties << buildDiscarder(logRotator(numToKeepStr: '50', artif"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1149,
    "preview": "The MIT License\n\nCopyright (c) 2014-, Michael Neale, Nicolas de Loof, Carlos Sanchez, and a number of other of contribut"
  },
  {
    "path": "Makefile",
    "chars": 7287,
    "preview": "ROOT_DIR=\"$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/\"\n\n## For Docker <=20.04\nexport DOCKER_BUILDKIT=1\n##"
  },
  {
    "path": "README.md",
    "chars": 18395,
    "preview": "# Official Jenkins Docker image\n\n[![Docker Stars](https://img.shields.io/docker/stars/jenkins/jenkins.svg)](https://hub."
  },
  {
    "path": "SECURITY.md",
    "chars": 2981,
    "preview": "# Security Policy\n\nThe Jenkins project takes security seriously.\nWe make every possible effort to ensure users can adequ"
  },
  {
    "path": "alpine/hotspot/Dockerfile",
    "chars": 5023,
    "preview": "ARG ALPINE_TAG=3.23.3\n\nFROM alpine:\"${ALPINE_TAG}\" AS jre-and-war\n\nARG JAVA_VERSION=17.0.18_8\n\nSHELL [\"/bin/ash\", \"-o\", "
  },
  {
    "path": "debian/Dockerfile",
    "chars": 5763,
    "preview": "ARG TRIXIE_TAG=20251103\n\nARG DEBIAN_RELEASE_LINE=trixie\nARG DEBIAN_VERSION=20251117\nARG DEBIAN_VARIANT=\"-slim\"\nFROM debi"
  },
  {
    "path": "docker-bake.hcl",
    "chars": 11384,
    "preview": "## Variables\nvariable \"jdks_to_build\" {\n  default = [21, 25]\n}\n\nvariable \"windows_version_to_build\" {\n  default = [\"ltsc"
  },
  {
    "path": "jdk-download-url.sh",
    "chars": 3761,
    "preview": "#!/bin/sh\n\n# Check if at least one argument was passed to the script\n# If one argument was passed and JAVA_VERSION is se"
  },
  {
    "path": "jdk-download.sh",
    "chars": 1738,
    "preview": "#!/bin/sh\nset -x\n# Check if curl and tar are installed\nif ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/nu"
  },
  {
    "path": "jenkins-plugin-cli.ps1",
    "chars": 84,
    "preview": "& java \"$env:JAVA_OPTS\" -jar C:/ProgramData/Jenkins/jenkins-plugin-manager.jar $args"
  },
  {
    "path": "jenkins-plugin-cli.sh",
    "chars": 323,
    "preview": "#!/bin/bash\n\n# read JAVA_OPTS into array to avoid need for eval (and associated vulnerabilities)\njava_opts_array=()\nwhil"
  },
  {
    "path": "jenkins-support",
    "chars": 6149,
    "preview": "#!/bin/bash -eu\n\n: \"${REF:=\"/usr/share/jenkins/ref\"}\"\n\n# compare if version1 < version2\nversionLT() {\n    local normaliz"
  },
  {
    "path": "jenkins-support.psm1",
    "chars": 9146,
    "preview": "\n# compare if version1 < version2\nfunction Compare-VersionLessThan([string] $version1 = '', [string] $version2 = '') {\n "
  },
  {
    "path": "jenkins.io-2026.key",
    "chars": 1680,
    "preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGlJRoMBEADGTw4Jms5rD1Wd0evqpTkNBgAIvCzvsjgGXHevmNIsDmm/niiE\ngKJlrl73T9d8GZeoa"
  },
  {
    "path": "jenkins.ps1",
    "chars": 2101,
    "preview": "Import-Module -Force -DisableNameChecking C:/ProgramData/Jenkins/jenkins-support.psm1\n\n$JENKINS_WAR = Get-EnvOrDefault '"
  },
  {
    "path": "jenkins.sh",
    "chars": 2490,
    "preview": "#! /bin/bash -e\n\n: \"${JENKINS_WAR:=\"/usr/share/jenkins/jenkins.war\"}\"\n: \"${JENKINS_HOME:=\"/var/jenkins_home\"}\"\n\nif [[ -n"
  },
  {
    "path": "make.ps1",
    "chars": 10910,
    "preview": "[CmdletBinding()]\r\nParam(\r\n    [Parameter(Position = 1)]\r\n    # Default script target\r\n    [String] $Target = 'build',\r\n"
  },
  {
    "path": "rhel/Dockerfile",
    "chars": 5882,
    "preview": "ARG RHEL_TAG=9.7-1773204657\nARG RHEL_RELEASE_LINE=ubi9\nFROM registry.access.redhat.com/${RHEL_RELEASE_LINE}/ubi:${RHEL_T"
  },
  {
    "path": "tests/bake.bats",
    "chars": 784,
    "preview": "#!/usr/bin/env bats\n\n# bats file_tags=test-suite:bake\n# bats file_tags=test-type:golden-file\n\nload test_helpers\n\nSUT_DES"
  },
  {
    "path": "tests/functions/.ssh/config",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/functions/Dockerfile",
    "chars": 171,
    "preview": "FROM bats-jenkins\n\nRUN mkdir -p /usr/share/jenkins/ref/.ssh && touch /usr/share/jenkins/ref/.ssh/config.override\nRUN chm"
  },
  {
    "path": "tests/functions/Dockerfile-windows",
    "chars": 169,
    "preview": "FROM bats-jenkins\r\n# hadolint shell=powershell\r\n\r\nRUN mkdir C:/ProgramData/Jenkins/Reference/pester ; echo $null >> C:/P"
  },
  {
    "path": "tests/functions.Tests.ps1",
    "chars": 5200,
    "preview": "Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1\r\nImport-Module -DisableNameChecking -For"
  },
  {
    "path": "tests/functions.bats",
    "chars": 2715,
    "preview": "#!/usr/bin/env bats\n\n# bats file_tags=test-suite:functions\n\nload 'test_helper/bats-support/load'\nload 'test_helper/bats-"
  },
  {
    "path": "tests/golden/expected_env_vars_except_hostname.txt",
    "chars": 534,
    "preview": "COPY_REFERENCE_FILE_LOG=/var/jenkins_home/copy_reference_file.log\nHOME=/var/jenkins_home\nJAVA_HOME=/opt/java/openjdk\nJEN"
  },
  {
    "path": "tests/golden/expected_platforms.txt",
    "chars": 872,
    "preview": "alpine_jdk21:linux/amd64\nalpine_jdk21:linux/arm64\nalpine_jdk25:linux/amd64\nalpine_jdk25:linux/arm64\ndebian-slim_jdk21:li"
  },
  {
    "path": "tests/golden/expected_tags.txt",
    "chars": 1268,
    "preview": "docker.io/jenkins/jenkins:2.555 (debian_jdk21)\ndocker.io/jenkins/jenkins:2.555-alpine (alpine_jdk21)\ndocker.io/jenkins/j"
  },
  {
    "path": "tests/golden/expected_tags_latest_lts.txt",
    "chars": 3140,
    "preview": "docker.io/jenkins/jenkins:2.541.1 (debian_jdk21)\ndocker.io/jenkins/jenkins:2.541.1-alpine (alpine_jdk21)\ndocker.io/jenki"
  },
  {
    "path": "tests/golden/expected_tags_latest_weekly.txt",
    "chars": 2837,
    "preview": "docker.io/jenkins/jenkins:2.555 (debian_jdk21)\ndocker.io/jenkins/jenkins:2.555-alpine (alpine_jdk21)\ndocker.io/jenkins/j"
  },
  {
    "path": "tests/jenkinsfile.bats",
    "chars": 315,
    "preview": "#!/usr/bin/env bats\n\n# bats file_tags=test-suite:jenkinsfile\n\nload test_helpers\n\nSUT_DESCRIPTION=\"Jenkinsfile\"\n\n@test \"["
  },
  {
    "path": "tests/plugins-cli/Dockerfile",
    "chars": 142,
    "preview": "FROM bats-jenkins\n\nRUN jenkins-plugin-cli --plugins junit:1.6 ant:1.3 mesos:0.13.0 git:latest filesystem_scm:experimenta"
  },
  {
    "path": "tests/plugins-cli/Dockerfile-windows",
    "chars": 208,
    "preview": "FROM bats-jenkins\n# hadolint shell=powershell\n\nRUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --verbose --plugins jun"
  },
  {
    "path": "tests/plugins-cli/custom-war/Dockerfile",
    "chars": 661,
    "preview": "FROM bats-jenkins\n\n# Define a custom location for the war\nENV JENKINS_WAR=/test-custom-dockerfile/my-custom-jenkins.war\n"
  },
  {
    "path": "tests/plugins-cli/custom-war/Dockerfile-windows",
    "chars": 663,
    "preview": "FROM bats-jenkins\n# hadolint shell=powershell\n\n# Define a custom location for the war\nENV JENKINS_WAR=C:/ProgramData/Tes"
  },
  {
    "path": "tests/plugins-cli/java-opts/Dockerfile",
    "chars": 128,
    "preview": "FROM bats-jenkins-plugins-cli\n\nENV JAVA_OPTS=\"-Djava.opts.test=true -XshowSettings:properties\"\nRUN jenkins-plugin-cli --"
  },
  {
    "path": "tests/plugins-cli/no-war/Dockerfile",
    "chars": 197,
    "preview": "FROM bats-jenkins\n\nCOPY plugins.txt /usr/share/jenkins/ref/plugins.txt\nUSER root\nRUN rm -rf /usr/share/jenkins/jenkins.w"
  },
  {
    "path": "tests/plugins-cli/no-war/Dockerfile-windows",
    "chars": 290,
    "preview": "FROM bats-jenkins\n# hadolint shell=powershell\n\nCOPY plugins.txt C:/ProgramData/Jenkins/Reference/plugins.txt\nRUN Remove-"
  },
  {
    "path": "tests/plugins-cli/no-war/plugins.txt",
    "chars": 295,
    "preview": "# comment line should be skipped\n\n# simple case\nant:1.3\n\n# trailing spaces\njunit:1.6   \n\n# leading spaces\n  mesos:0.13.0"
  },
  {
    "path": "tests/plugins-cli/pluginsfile/Dockerfile",
    "chars": 132,
    "preview": "FROM bats-jenkins\n\nCOPY plugins.txt /usr/share/jenkins/ref/plugins.txt\nRUN jenkins-plugin-cli -f /usr/share/jenkins/ref/"
  },
  {
    "path": "tests/plugins-cli/pluginsfile/Dockerfile-windows",
    "chars": 217,
    "preview": "FROM bats-jenkins\n# hadolint shell=powershell\n\nCOPY plugins.txt C:/ProgramData/Jenkins/Reference/plugins.txt\nRUN C:/Prog"
  },
  {
    "path": "tests/plugins-cli/pluginsfile/plugins.txt",
    "chars": 397,
    "preview": "# comment line should be skipped\n\n# simple case\nant:1.3\n\n# trailing spaces\njunit:1.6   \n\n# leading spaces\n  mesos:0.13.0"
  },
  {
    "path": "tests/plugins-cli/ref/Dockerfile",
    "chars": 115,
    "preview": "FROM bats-jenkins-plugins-cli\n\nRUN rm -rf /usr/share/jenkins/ref ; jenkins-plugin-cli --plugins junit:1.28 ant:1.3\n"
  },
  {
    "path": "tests/plugins-cli/ref/Dockerfile-windows",
    "chars": 199,
    "preview": "FROM bats-jenkins\n# hadolint shell=powershell\n\nRUN Remove-Item -Recurse -Force C:/ProgramData/Jenkins/Reference ; C:/Pro"
  },
  {
    "path": "tests/plugins-cli/update/Dockerfile",
    "chars": 93,
    "preview": "FROM bats-jenkins-plugins-cli\n\nRUN jenkins-plugin-cli --verbose --plugins junit:1.28 ant:1.3\n"
  },
  {
    "path": "tests/plugins-cli/update/Dockerfile-windows",
    "chars": 148,
    "preview": "FROM bats-jenkins-plugins-cli\n# hadolint shell=powershell\n\nRUN C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --verbose -"
  },
  {
    "path": "tests/plugins-cli.Tests.ps1",
    "chars": 14555,
    "preview": "Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1\r\nImport-Module -DisableNameChecking -For"
  },
  {
    "path": "tests/plugins-cli.bats",
    "chars": 9395,
    "preview": "#!/usr/bin/env bats\n\n# bats file_tags=test-suite:plugin-cli\n\nload 'test_helper/bats-support/load'\nload 'test_helper/bats"
  },
  {
    "path": "tests/runtime.Tests.ps1",
    "chars": 5046,
    "preview": "Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1\nImport-Module -DisableNameChecking -Forc"
  },
  {
    "path": "tests/runtime.bats",
    "chars": 6123,
    "preview": "#!/usr/bin/env bats\n\n# bats file_tags=test-suite:runtime\n\nload 'test_helper/bats-support/load'\nload 'test_helper/bats-as"
  },
  {
    "path": "tests/test_helpers.bash",
    "chars": 4850,
    "preview": "#!/bin/bash\nset -euo pipefail\n\n# Assert that $1 is the outputof a command $2\nfunction assert {\n    local expected_output"
  },
  {
    "path": "tests/test_helpers.psm1",
    "chars": 7419,
    "preview": "Import-Module -DisableNameChecking -Force $PSScriptRoot/../jenkins-support.psm1\n\nfunction Test-CommandExists($command) {"
  },
  {
    "path": "tests/update-golden-file.sh",
    "chars": 2292,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# This script runs a specified command, captures its output,\n# and compares it ag"
  },
  {
    "path": "tests/upgrade-plugins/Dockerfile",
    "chars": 71,
    "preview": "FROM bats-jenkins\n\nRUN jenkins-plugin-cli --plugins junit:1.28 ant:1.2\n"
  },
  {
    "path": "tests/upgrade-plugins/Dockerfile-windows",
    "chars": 128,
    "preview": "FROM bats-jenkins\n# hadolint shell=powershell\n\nRUN & C:/ProgramData/Jenkins/jenkins-plugin-cli.ps1 --plugins junit:1.28 "
  },
  {
    "path": "tini_pub.gpg",
    "chars": 7152,
    "preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFANDtsBEACpb69Ul0Ko7D4XxRIvPGnDMuGdocb8PxR+EGbnHe0uS2tCbsfj\nTOoWWUrjufrWYxGlK"
  },
  {
    "path": "tools/hadolint",
    "chars": 130,
    "preview": "#!/bin/sh -e\n\nexec docker run --rm \\\n\t-w \"${PWD}\" \\\n\t-v \"${PWD}:${PWD}\" \\\n\tghcr.io/hadolint/hadolint:v2.14.0-debian hado"
  },
  {
    "path": "tools/shellcheck",
    "chars": 117,
    "preview": "#!/bin/sh -e\n\nexec docker run --rm \\\n    -w \"${PWD}\" \\\n    -v \"${PWD}:${PWD}\" \\\n    koalaman/shellcheck:v0.11.0 \"$@\"\n"
  },
  {
    "path": "updatecli/scripts/rhel-latest-tag.sh",
    "chars": 3260,
    "preview": "#!/bin/bash\n\n# This script fetches the latest tag from the Red Hat Container Catalog API for the images of the current R"
  },
  {
    "path": "updatecli/updatecli.d/alpine.yaml",
    "chars": 1959,
    "preview": "---\nname: Bump Alpine version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email:"
  },
  {
    "path": "updatecli/updatecli.d/debian.yaml",
    "chars": 2463,
    "preview": "---\nname: Bump Debian version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email:"
  },
  {
    "path": "updatecli/updatecli.d/git-lfs.yaml",
    "chars": 1167,
    "preview": "---\nname: Bump `git-lfs` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      ema"
  },
  {
    "path": "updatecli/updatecli.d/hadolint.yaml",
    "chars": 1246,
    "preview": "---\nname: Bump hadolint version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      emai"
  },
  {
    "path": "updatecli/updatecli.d/jdk17.yaml",
    "chars": 1817,
    "preview": "---\nname: Bump JDK17 version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: "
  },
  {
    "path": "updatecli/updatecli.d/jdk21.yaml",
    "chars": 1434,
    "preview": "name: Bump JDK21 version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: \"{{ "
  },
  {
    "path": "updatecli/updatecli.d/jdk25.yaml",
    "chars": 1434,
    "preview": "name: Bump JDK25 version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: \"{{ "
  },
  {
    "path": "updatecli/updatecli.d/jenkins-version-simulated-lts.yaml",
    "chars": 2187,
    "preview": "---\nname: Bump simulated LTS `JENKINS_VERSION` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .git"
  },
  {
    "path": "updatecli/updatecli.d/jenkins-version.yaml",
    "chars": 2664,
    "preview": "---\nname: Bump default `JENKINS_VERSION` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.us"
  },
  {
    "path": "updatecli/updatecli.d/plugin-installation-manager-tool.yaml",
    "chars": 1643,
    "preview": "---\nname: Bump `plugin-installation-manager-tool` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ ."
  },
  {
    "path": "updatecli/updatecli.d/rhel.yaml",
    "chars": 1708,
    "preview": "---\nname: Bump RHEL version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: \""
  },
  {
    "path": "updatecli/updatecli.d/shellcheck.yaml",
    "chars": 1238,
    "preview": "---\nname: Bump shellcheck version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      em"
  },
  {
    "path": "updatecli/values.github-action.yaml",
    "chars": 224,
    "preview": "github:\n  user: \"GitHub Actions\"\n  email: \"41898282+github-actions[bot]@users.noreply.github.com\"\n  username: \"github-ac"
  },
  {
    "path": "windows/windowsservercore/hotspot/Dockerfile",
    "chars": 7732,
    "preview": "# escape=`\n# hadolint shell=powershell\n\nARG JAVA_VERSION=17.0.18_8\nARG WINDOWS_VERSION=ltsc2022\n\nFROM mcr.microsoft.com/"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the jenkinsci/docker GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 88 files (229.7 KB), approximately 70.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!