Full Code of jenkinsci/docker-ssh-agent for AI

master b8c9b018fcc1 cached
45 files
134.2 KB
39.8k tokens
1 requests
Download .txt
Repository: jenkinsci/docker-ssh-agent
Branch: master
Commit: b8c9b018fcc1
Files: 45
Total size: 134.2 KB

Directory structure:
gitextract_vtv8hsk6/

├── .dockerignore
├── .gitattributes
├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   ├── release-drafter.yml
│   └── workflows/
│       ├── release-drafter.yml
│       └── updatecli.yaml
├── .gitignore
├── .gitmodules
├── CreateProfile.psm1
├── Jenkinsfile
├── LICENSE
├── Makefile
├── README.md
├── alpine/
│   └── Dockerfile
├── build.ps1
├── debian/
│   └── Dockerfile
├── docker-bake.hcl
├── jdk-download-url.sh
├── jdk-download.sh
├── setup-sshd
├── setup-sshd.ps1
├── tests/
│   ├── golden/
│   │   └── expected_tags.txt
│   ├── keys.bash
│   ├── sshAgent.Tests.ps1
│   ├── tags.bats
│   ├── test_helpers.bash
│   ├── test_helpers.psm1
│   ├── tests.bats
│   └── update-golden-file.sh
├── updatecli/
│   ├── updatecli.d/
│   │   ├── alpine.yaml
│   │   ├── bats.yaml
│   │   ├── debian.yaml
│   │   ├── git-lfs.yml
│   │   ├── git-windows.yml
│   │   ├── jdk17.yaml
│   │   ├── jdk21.yaml
│   │   ├── jdk25.yaml
│   │   ├── openssh.yml
│   │   └── pester.yaml
│   ├── values.github-action.yaml
│   └── values.temurin.yaml
└── windows/
    ├── nanoserver/
    │   └── Dockerfile
    └── windowsservercore/
        └── Dockerfile

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

================================================
FILE: .dockerignore
================================================
tests
README.md
.git
.gitignore
.gitattributes
.DS_Store


================================================
FILE: .gitattributes
================================================
# Force checkout as Unix endline style
text eol=lf


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


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


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: docker
  directory: "/alpine"
  schedule:
    interval: weekly
  open-pull-requests-limit: 10
- package-ecosystem: docker
  directory: "/debian"
  schedule:
    interval: weekly
  open-pull-requests-limit: 10
- package-ecosystem: docker
  directory: "/windows/nanoserver"
  schedule:
    interval: weekly
  open-pull-requests-limit: 10
- package-ecosystem: docker
  directory: "/windows/windowsservercore"
  schedule:
    interval: weekly
  open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
  target-branch: master
  directory: "/"
  schedule:
    # Check for updates to GitHub Actions every week
    interval: "weekly"
  labels:
    - github-action
    - dependencies
    - skip-changelog


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

_extends: github:jenkinsci/.github:/.github/release-drafter.yml
# Semantic versioning: https://semver.org/
version-template: $MAJOR.$MINOR.$PATCH
tag-template: $NEXT_MINOR_VERSION
name-template: $NEXT_MINOR_VERSION


================================================
FILE: .github/workflows/release-drafter.yml
================================================
# Automates creation of Release Drafts using Release Drafter
# Note: additional setup is required, see https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc

name: Release Drafter (Changelog)

on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  update_release_draft:
    runs-on: ubuntu-latest
    steps:
      # Drafts your next Release notes as Pull Requests are merged into the default branch
      - uses: release-drafter/release-drafter@v7.3.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/updatecli.yaml
================================================
name: updatecli
on:
  # Allow to be run manually
  workflow_dispatch:
  schedule:
    - cron: '0 1 * * *' # Once a day at 01:00am UTC
  push:
  pull_request:
jobs:
  updatecli:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Install Updatecli in the runner
        uses: updatecli/updatecli-action@v3.2.0

      - name: Run Updatecli in Dry Run mode
        run: updatecli diff --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml --values ./updatecli/values.temurin.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 --values ./updatecli/values.temurin.yaml
        env:
          UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
.DS_Store
bats-core/
bats/
target/
/.vscode/
build-windows.yaml


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


================================================
FILE: CreateProfile.psm1
================================================
# Based on code developed by  Josh Rickard (@MS_dministrator) and Thom Schumacher (@driberif)
# Location: https://gist.github.com/crshnbrn66/7e81bf20408c05ddb2b4fdf4498477d8

#function to register a native method
function Register-NativeMethod {
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]$dll,

        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1)]
        [string]
        $methodSignature
    )

    $script:nativeMethods += [PSCustomObject]@{ Dll = $dll; Signature = $methodSignature; }
}

#function to add native method
function Add-NativeMethods {
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param($typeName = 'NativeMethods')

    $nativeMethodsCode = $script:nativeMethods | ForEach-Object { "
        [DllImport(`"$($_.Dll)`")]
        public static extern $($_.Signature);
    " }

    Add-Type @"
        using System;
        using System.Text;
        using System.Runtime.InteropServices;
        public static class $typeName {
            $nativeMethodsCode
        }
"@
}

#Main function to create the new user profile
function New-UserWithProfile {
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]$UserName,

        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1)]
        [string]$Description = ''
    )

    Write-Verbose "Creating local user $Username";

    try {
        net user $UserName /ADD /ACTIVE:YES /EXPIRES:NEVER /FULLNAME:"$Description" /PASSWORDCHG:NO /PASSWORDREQ:NO
        net localgroup Administrators /add $UserName
    } catch {
        Write-Error $_.Exception.Message;
        break;
    }

    $localUser = New-Object System.Security.Principal.NTAccount($UserName)

    $methodName = 'UserEnvCP'
    $script:nativeMethods = @();

    if (-not ([System.Management.Automation.PSTypeName]$MethodName).Type) {
        Register-NativeMethod "userenv.dll" "int CreateProfile([MarshalAs(UnmanagedType.LPWStr)] string pszUserSid,`
         [MarshalAs(UnmanagedType.LPWStr)] string pszUserName,`
         [Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath, uint cchProfilePath)";

        Add-NativeMethods -typeName $MethodName;
    }

    $userSID = $localUser.Translate([System.Security.Principal.SecurityIdentifier])
    $sb = New-Object System.Text.StringBuilder(260)
    $pathLen = $sb.Capacity

    Write-Verbose "Creating user profile for $UserName";

    try {
        [UserEnvCP]::CreateProfile($userSID.Value, $UserName, $sb, $pathLen) | Out-Null;
    } catch {
        Write-Error $_.Exception.Message;
        break;
    }

    $profilePath = $sb.ToString()
    Write-Verbose "Profile created at $profilePath"
    if(-not (Test-Path (Join-Path $profilePath "NTUSER.DAT"))) {
        Copy-Item "C:\Users\Default\NTUSER.DAT" $profilePath
    }
}

================================================
FILE: Jenkinsfile
================================================
final String cronExpr = env.BRANCH_IS_PRIMARY ? '@daily' : ''

properties([
    buildDiscarder(logRotator(numToKeepStr: '10')),
    disableConcurrentBuilds(abortPrevious: true),
    pipelineTriggers([cron(cronExpr)]),
])

def agentSelector(String imageType, retryCounter) {
    def platform
    switch (imageType) {
        // nanoserver-ltsc2019 and windowservercore-ltsc2019
        case ~/.*2019/:
            platform = 'windows-2019'
            break

        // nanoserver-ltsc2022 and windowservercore-ltsc2022
        case ~/.*2022/:
            platform = 'windows-2022'
            break

        // All other Windows images
        case ~/(nanoserver|windowsservercore).*/:
            platform = 'windows-2025'
            break

        // Linux
        default:
            // Need Docker and a LOT of memory for faster builds (due to multi archs)
            platform = 'docker-highmem'
            break
    }

    // Defined in https://github.com/jenkins-infra/pipeline-library/blob/master/vars/infra.groovy
    return infra.getBuildAgentLabel([
        useContainerAgent: false,
        platform: platform,
        spotRetryCounter: retryCounter
    ])
}

// Specify parallel stages
def parallelStages = [failFast: false]
[
    'linux',
    'nanoserver-ltsc2019',
    'nanoserver-ltsc2022',
    'windowsservercore-ltsc2019',
    'windowsservercore-ltsc2022'
].each { imageType ->
    parallelStages[imageType] = {
        withEnv([
          "IMAGE_TYPE=${imageType}", 
          "REGISTRY_ORG=${infra.isTrusted() ? 'jenkins' : 'jenkins4eval'}",
        ]) {
            int retryCounter = 0
            retry(count: 2, conditions: [agent(), nonresumable()]) {
                // Use local variable to manage concurrency and increment BEFORE spinning up any agent
                final String resolvedAgentLabel = agentSelector(imageType, retryCounter)
                retryCounter++
                node(resolvedAgentLabel) {
                    timeout(time: 60, unit: 'MINUTES') {
                        checkout scm
                        if (imageType == "linux") {
                            stage('Prepare Docker') {
                                sh 'make docker-init'
                            }
                        }
                        // This function is defined in the jenkins-infra/pipeline-library
                        if (infra.isTrusted()) {
                            // trusted.ci.jenkins.io builds (e.g. publication to DockerHub)
                            stage('Deploy to DockerHub') {
                                withEnv([
                                    "ON_TAG=true",
                                    "VERSION=${env.TAG_NAME}",
                                ]) {
                                    // This function is defined in the jenkins-infra/pipeline-library
                                    infra.withDockerCredentials {
                                        if (isUnix()) {
                                            sh 'make publish'
                                        } else {
                                            powershell '& ./build.ps1 build'
                                            powershell '& ./build.ps1 publish'
                                        }
                                    }
                                }
                            }
                        } else {
                            // ci.jenkins.io builds (e.g. no publication)
                            stage('Build') {
                                if (isUnix()) {
                                    sh 'make build'
                                } else {
                                    powershell '& ./build.ps1 build'
                                    archiveArtifacts artifacts: 'build-windows.yaml', allowEmptyArchive: true
                                }
                            }
                            stage('Test') {
                                if (isUnix()) {
                                    sh 'make test'
                                } else {
                                    powershell '& ./build.ps1 test'
                                }
                                junit(allowEmptyResults: true, keepLongStdio: true, testResults: 'target/**/junit-results.xml')
                            }
                            // If the tests are passing for Linux AMD64, then we can build all the CPU architectures
                            if (isUnix()) {
                                stage('Multi-Arch Build') {

                                    sh 'make every-build'
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

// Execute parallel stages
parallel parallelStages
// // vim: ft=groovy


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2015-2019 Jenkins project 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:=$(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

current_arch := $(shell uname -m)
export ARCH ?= $(shell case $(current_arch) in (x86_64) echo "amd64" ;; (i386) echo "386";; (aarch64|arm64) echo "arm64" ;; (armv6*) echo "arm/v6";; (armv7*) echo "arm/v7";; (s390*|riscv*|ppc64le) echo $(current_arch);; (*) echo "UNKNOWN-CPU";; esac)

IMAGE_NAME:=jenkins4eval/ssh-agent

# 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 platform 'linux/$(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 --file docker-bake.hcl
bake_cli := $(bake_base_cli) --load

.PHONY: build
.PHONY: test test-alpine test-debian

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
	docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

build: check-reqs
	@set -x; $(bake_cli) $(shell make --silent list) --set '*.platform=linux/$(ARCH)'

build-%:
	@$(call check_image,$*)
	@set -x; $(bake_cli) '$*' --set '*.platform=linux/$(ARCH)'

every-build: check-reqs
	@set -x; $(bake_base_cli) linux

show:
	@$(bake_base_cli) --progress=quiet linux --print | jq

tags:
	@make show | jq -r '.target[].tags[]' | LC_ALL=C sort

list: check-reqs
	@set -x; make --silent show | jq -r '.target | path(.. | select(.platforms[] | contains("linux/$(ARCH)"))?) | add'

bats:
	git clone --branch v1.13.0 https://github.com/bats-core/bats-core bats

prepare-test: bats check-reqs
	git submodule update --init --recursive
	mkdir -p target

publish:
	@set -x; $(bake_base_cli) linux --push

## 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
test-%: prepare-test
# Check that the image exists in the manifest
	@$(call check_image,$*)
# Ensure that the image is built
	@make --silent build-$*
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)
endif

test: prepare-test
	@make --silent list | while read image; do make --silent "test-$${image}"; done


================================================
FILE: README.md
================================================
# Docker image for Jenkins agents connected over SSH

[![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)
[![GitHub stars](https://img.shields.io/github/stars/jenkinsci/docker-ssh-agent?label=GitHub%20stars)](https://github.com/jenkinsci/docker-ssh-agent)
[![Docker Pulls](https://img.shields.io/docker/pulls/jenkins/ssh-agent.svg)](https://hub.docker.com/r/jenkins/ssh-agent/)
[![GitHub release](https://img.shields.io/github/release/jenkinsci/docker-ssh-agent.svg?label=changelog)](https://github.com/jenkinsci/docker-ssh-agent/releases)

A [Jenkins](https://jenkins.io) agent image which allows using SSH to establish the connection.
It can be used together with the [SSH Build Agents plugin](https://plugins.jenkins.io/ssh-slaves) or other similar plugins.

See [Jenkins Distributed builds](https://wiki.jenkins-ci.org/display/JENKINS/Distributed+builds) for more info.

## Running

### Running with the SSH Build Agents plugin

To run a Docker container

```bash
docker run -d --rm --name=agent --publish 2200:22 -e "JENKINS_AGENT_SSH_PUBKEY=<public_key>" jenkins/ssh-agent
```

 - `-d`: To start a container in detached mode, use the `-d` option. Containers started in detached mode exit when the root process used to run the container exits, unless you also specify the --rm option.
 - `--rm`: If you use -d with --rm, the container is removed when it exits or when the daemon exits, whichever happens first.
 - `--name`: Assigns a name to the container. If you do not specify a name, Docker generates a random name.
 - `--publish 2200:22`: Publishes the host port 2200 to the agent container port 22 (SSH) to allow connection from the host with `ssh jenkins@localhost -p 2200`

Please note none of these options are mandatory, they are just examples.

You will then be able to connect this agent using the [SSH Build Agents plugin](https://plugins.jenkins.io/ssh-slaves) as "jenkins" with the matching private key.

When using the Linux image, you have to set the value of the `Remote root directory` to `/home/jenkins/agent` in the agent configuration UI.

![Remote root directory with a Linux agent](docs/ssh-plugin-remote-root-directory-linux.png "Remote root directory with a Linux agent")

When using the Windows image, you have to set the value of the `Remote root directory` to `C:/Users/jenkins/Work` in the agent configuration UI.

![Remote root directory with a Windows agent](docs/ssh-plugin-remote-root-directory-windows.png "Remote root directory with a Windows agent")

If you intend to use another directory than `/home/jenkins/agent` under Linux or `C:/Users/jenkins/Work` under Windows, don't forget to add it as a data volume.

```bash
docker run -v docker-volume-for-jenkins-ssh-agent:/home/jenkins/agent:rw jenkins/ssh-agent "<public key>"
```

### How to use this image with Docker Plugin

To use this image with [Docker Plugin](https://plugins.jenkins.io/docker-plugin), you need to pass the public SSH key using environment variable `JENKINS_AGENT_SSH_PUBKEY` and not as a startup argument.

In _Environment_ field of the Docker Template (advanced section), just add:

    JENKINS_AGENT_SSH_PUBKEY=<YOUR PUBLIC SSH KEY HERE>

Don't put quotes around the public key.

Please note that you have to set the value of the `Remote File System Root` to `/home/jenkins/agent` in the Docker Agent Template configuration UI.

![Remote File System Root](docs/docker-plugin-remote-filesystem-root.png "Remote File System Root directory")

If you intend to use another directory than `/home/jenkins/agent`, don't forget to add it as a data volume.

![Docker Volumes mounts](docs/docker-plugin-volumes.png "Docker Volumes mounts")

You should be all set.

## Extending the image
Should you need to extend the image, you could use something along those lines:

```Dockerfile
FROM jenkins/ssh-agent:debian-jdk17 as ssh-agent
# [...]
COPY --chown=jenkins mykey "${JENKINS_AGENT_HOME}"/.ssh/mykey
# [...]
```

## Configurations

The image has several supported configurations, which can be accessed via the following tags:

`${IMAGE_VERSION}` can be found on the [releases](https://github.com/jenkinsci/docker-ssh-agent/releases) page.

List of tags can be consulted at https://github.com/jenkinsci/docker-ssh-agent/tree/master/tests/golden

## Host keys

Host keys are generated with `ssh-keygen -A` in `setup-httpd`. Host keys reside inside the container. When the container is recreated, different host keys are also generated. Jenkins master may need to re-trust new host keys.

We can preserve host keys in mounted volume.

Steps:
1. Copy host keys from the mounted volume to /etc/ssh/
2. Run ssh-keygen -A to generate host keys
3. Copy host keys from /etc/ssh/ to the mounted volume
4. Run setup-sshd

Entry point example:

```
#!/bin/bash

# /mnt/agent is the mounted volume
mkdir -p /mnt/agent/host_keys/

cp -u /mnt/agent/host_keys/ssh_host*_key* /etc/ssh/

ssh-keygen -A

cp -u /etc/ssh/ssh_host*_key* /mnt/agent/host_keys/

setup-ssd "$@"
```

## Building instructions

### Pre-requisites

Should you want to build this image on your machine (before submitting a pull request for example), please have a look at the pre-requisites:

* A GNU/Linux machine with [Docker](https://docs.docker.com/engine/install/), a macOS machine with [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/), or a Windows machine with [Docker for Windows](https://docs.docker.com/docker-for-windows/) installed
* Docker BuildX plugin [installed](https://github.com/docker/buildx#installing) on older versions of Docker (from `19.03`). Docker Buildx is included in recent versions of Docker Desktop for Windows, macOS, and Linux. Docker Linux packages also include Docker Buildx when installed using the DEB or RPM packages.
* [GNU Make](https://www.gnu.org/software/make/) [installed](https://command-not-found.com/make)
* jq [installed](https://command-not-found.com/jq)
* yq [installed](https://github.com/mikefarah/yq) (for Windows)
* [GNU Bash](https://www.gnu.org/software/bash/) [installed](https://command-not-found.com/bash)
* git [installed](https://command-not-found.com/git)
* curl [installed](https://command-not-found.com/curl)

### Building

#### Target images

If you want to see the target images that will be built, you can issue the following command:

```bash
make list
alpine_jdk11
alpine_jdk17
debian_jdk11
debian_jdk17
```

#### Building a specific image

If you want to build a specific image, you can issue the following command:

```bash
make build-<OS>_<JDK_VERSION>
```

That would give for JDK 17 on Alpine Linux:

```bash
make build-alpine_jdk17
```

#### Building images supported by your current architecture

Then, you can build the images supported by your current architecture by running:

```bash
make build
```

#### Testing all images

If you want to test these images, you can run:

```bash
make test
```
#### Testing a specific image

If you want to test a specific image, you can run:

```bash
make test-<OS>_<JDK_VERSION>
```

That would give for JDK 17 on Alpine Linux:

```bash
make test-alpine_jdk17
```

#### Building all images

You can build all images (even those unsupported by your current architecture) by running:

```bash
make every-build
```

#### Other `make` targets

`show` gives us a detailed view of the images that will be built, with the tags, platforms, and Dockerfiles.

```bash
make show
{
  "group": {
    "default": {
      "targets": [
        "alpine_jdk17",
        "alpine_jdk11",
        "debian_jdk11",
        "debian_jdk17",
      ]
    }
  },
  "target": {
    "alpine_jdk11": {
      "context": ".",
      "dockerfile": "alpine/Dockerfile",
      "tags": [
        "docker.io/jenkins/ssh-agent:alpine-jdk11",
        "docker.io/jenkins/ssh-agent:latest-alpine-jdk11"
      ],
      "platforms": [
        "linux/amd64"
      ],
      "output": [
        "type=docker"
      ]
    },
    [...]
```

`bats` is a dependency target. It will update the [`bats` submodule](https://github.com/bats-core/bats-core) and run the tests.

```bash
make bats
make: 'bats' is up to date.
```

`publish` allows the publication of all images targeted by 'linux' to a registry.

`docker-init` is dedicated to Jenkins infrastructure for initializing docker and isn't required in other contexts.

### Building and testing on Windows

#### Building all images

Run `.\build.ps1` to launch the build of the images corresponding to the "windows" target of docker-bake.hcl.

Internally, the first time you'll run this script and if there is no build-windows.yaml file in your repository, it will use a combination of `docker buildx bake` and `yq` to generate a  build-windows.yaml docker compose file containing all Windows image definitions from docker-bake.hcl. Then it will run `docker compose` on this file to build these images.

You can modify this docker compose file as you want, then rerun `.\build.ps1`.
It won't regenerate the docker compose file from docker-bake.hcl unless you add the `-OverwriteDockerComposeFile` build.ps1 parameter:  `.\build.ps1 -OverwriteDockerComposeFile`.

Note: you can generate this docker compose file from docker-bake.hcl yourself with the following command (require `docker buildx` and `yq`):

```console
# - Use docker buildx bake to output image definitions from the "windows" bake target
# - Convert with yq to the format expected by docker compose
# - Store the result in the docker compose file

$ docker buildx bake --progress=plain --file=docker-bake.hcl windows --print `
    | yq --prettyPrint '.target[] | del(.output) | {(. | key): {\"image\": .tags[0], \"build\": .}}' | yq '{\"services\": .}' `
    | Out-File -FilePath build-windows.yaml
```

Note that you don't need build.ps1 to build (or to publish) your images from this docker compose file, you can use `docker compose --file=build-windows.yaml build`.

#### Testing all images

Run `.\build.ps1 test` if you also want to run the tests harness suit.

Run `.\build.ps1 test -TestsDebug 'debug'` to also get commands & stderr of tests, displayed on top of them.
You can set it to `'verbose'` to also get stdout of every test command.

Note that instead of passing `-TestsDebug` parameter to build.ps1, you can set the  $env:TESTS_DEBUG environment variable to the desired value.

Also note that contrary to the Linux part, you have to build the images before testing them.

#### Dry run

Add the `-DryRun` parameter to print out any build, publish or tests commands instead of executing them: `.\build.ps1 test -DryRun`

#### Building and testing a specific image

You can build (and test) only one image type by setting `-ImageType` to a combination of Windows flavors ("nanoserver" & "windowsservercore") and Windows versions ("ltsc2019", "ltsc2022").

Ex: `.\build.ps1 -ImageType 'nanoserver-ltsc2019'`

## Changelog

See [GitHub Releases](https://github.com/jenkinsci/docker-ssh-agent/releases/latest).
Note that the changelogs and release tags were introduced in Dec 2019, and there are no entries for previous releases.
Please consult with the commit history if needed.


================================================
FILE: alpine/Dockerfile
================================================
# MIT License
#
# Copyright (c) 2019-2022 Fabio Kruger and other 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.

ARG JAVA_VERSION=17.0.19_10
ARG ALPINE_TAG=3.23.4
FROM alpine:"${ALPINE_TAG}" AS jre-build

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

# This Build ARG is populated by Docker
# Ref. https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
ARG TARGETPLATFORM

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

ARG JAVA_VERSION=17.0.19_10
# hadolint ignore=DL3018
RUN apk add --no-cache \
    ca-certificates \
    jq \
    curl \
  && /usr/bin/local/jdk-download.sh alpine

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

RUN case "$(jlink --version 2>&1)" in \
    "17."*) set -- "--compress=2" --add-modules ALL-MODULE-PATH ;; \
    "21."*) set -- "--compress=zip-6" --add-modules ALL-MODULE-PATH ;; \
    # JDK 25 switches to a minimal module set for smaller images
    "25"*) set -- "--compress=zip-6" --add-modules "java.base,java.logging,java.xml,java.management,java.net.http,jdk.crypto.ec" ;; \
    *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \
  esac; \
  jlink \
    --strip-java-debug-attributes \
    "$@" \
    --no-man-pages \
    --no-header-files \
    --output /javaruntime

# Downloading git-lfs from this intermediate stage as there is no wget/curl in the final one
ARG GIT_LFS_VERSION=3.7.1
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"

FROM alpine:"${ALPINE_TAG}" AS build

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG JENKINS_AGENT_HOME=/home/${user}

ENV JENKINS_AGENT_HOME=${JENKINS_AGENT_HOME}

ARG AGENT_WORKDIR="${JENKINS_AGENT_HOME}"/agent
# Persist agent workdir path through an environment variable for people extending the image
ENV AGENT_WORKDIR=${AGENT_WORKDIR}

RUN addgroup -g "${gid}" "${group}" \
    # Set the home directory (h), set user and group id (u, G), set the shell, don't ask for password (D)
    && adduser -h "${JENKINS_AGENT_HOME}" -u "${uid}" -G "${group}" -s /bin/bash -D "${user}" \
    # Unblock user
    && passwd -u "${user}" \
    # Prepare subdirectories
    && mkdir -p "${JENKINS_AGENT_HOME}/.ssh/" "${JENKINS_AGENT_HOME}/.jenkins/" "${AGENT_WORKDIR}" \
    && chown -R "${uid}":"${gid}" "${JENKINS_AGENT_HOME}" "${AGENT_WORKDIR}"


RUN apk add --no-cache \
        bash \
        git \
        less \
        musl-locales \
        netcat-openbsd \
        openssh \
        patch \
    # Cleanup SSH host keys if any
    && rm -f /etc/ssh/ssh_host*_key*

# Retrieve git-lfs from the build stage and install it
COPY --from=jre-build git-lfs.tgz git-lfs.tgz
RUN tar xzf git-lfs.tgz \
  && bash git-lfs-*/install.sh \
  && rm -rf git-lfs*

# setup SSH server
RUN sed -i /etc/ssh/sshd_config \
        -e 's/#PermitRootLogin.*/PermitRootLogin no/' \
        -e 's/#PasswordAuthentication.*/PasswordAuthentication no/' \
        -e 's/#SyslogFacility.*/SyslogFacility AUTH/' \
        -e 's/#LogLevel.*/LogLevel INFO/' \
        -e 's/#PermitUserEnvironment.*/PermitUserEnvironment yes/' \
    && mkdir /var/run/sshd

# Install JDK

ENV JAVA_HOME=/opt/java/openjdk
COPY --from=jre-build /javaruntime "$JAVA_HOME"
ENV PATH="${JAVA_HOME}/bin:${PATH}"

# VOLUME directive must happen after setting up permissions and content
VOLUME "${AGENT_WORKDIR}" "${JENKINS_AGENT_HOME}"/.jenkins "/tmp" "/run" "/var/run"
WORKDIR "${JENKINS_AGENT_HOME}"

# Alpine's ssh doesn't use $PATH defined in /etc/environment, so we define `$PATH` in `~/.ssh/environment`
# The file path has been created earlier in the file by `mkdir -p` and we also have configured sshd so that it will
# allow environment variables to be sourced (see `sed` command related to `PermitUserEnvironment`)
RUN echo "PATH=${PATH}" >> ${JENKINS_AGENT_HOME}/.ssh/environment
COPY setup-sshd /usr/local/bin/setup-sshd

EXPOSE 22

ENTRYPOINT ["setup-sshd"]

LABEL \
    org.opencontainers.image.vendor="Jenkins project" \
    org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" \
    org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" \
    org.opencontainers.image.url="https://www.jenkins.io/" \
    org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" \
    org.opencontainers.image.licenses="MIT"


================================================
FILE: build.ps1
================================================
[CmdletBinding()]
Param(
    [Parameter(Position=1)]
    # Default build.ps1 target
    [String] $Target = 'build',
    # Image version
    [String] $VersionTag = '0.0.1',
    # Windows flavor and windows version to build
    [String] $ImageType = 'nanoserver-ltsc2019',
    # 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,
    # Pester version to install and use for tests
    [String] $PesterVersion = '5.7.1',
    # Output debug info for tests: 'empty' (no additional test output), 'debug' (test cmd & stderr outputed), 'verbose' (test cmd, stderr, stdout outputed)
    [String] $TestsDebug = ''
)

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

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

$Repository = 'ssh-agent'
$Organisation = 'jenkins'

if(![String]::IsNullOrWhiteSpace($env:TESTS_DEBUG)) {
    $TestsDebug = $env:TESTS_DEBUG
}
$env:TESTS_DEBUG = $TestsDebug

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

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

if(![String]::IsNullOrWhiteSpace($env:VERSION)) {
    $VersionTag = $env:VERSION
}

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

# Ensure constant env vars used in the docker compose file are defined
$env:DOCKERHUB_ORGANISATION = "$Organisation"
$env:DOCKERHUB_REPO = "$Repository"
$env:VERSION = "$VersionTag"

# Check for required commands
Function Test-CommandExists {
    # From https://devblogs.microsoft.com/scripting/use-a-powershell-function-to-see-if-a-command-exists/
    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 (
        $ImageNameAndJavaVersion
    )

    # Ex: docker.io/jenkins/ssh-agent:windowsservercore-ltsc2019-jdk21|21.0.3_9
    $items = $ImageNameAndJavaVersion.Split('|')
    $imageName = $items[0] -replace 'docker.io/', ''
    $javaVersion = $items[1]
    $imageNameItems = $imageName.Split(':')
    $imageTag = $imageNameItems[1]

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

    $env:IMAGE_NAME = $ImageName
    $env:JAVA_VERSION = "$javaVersion"

    $targetPath = '.\target\{0}' -f $imageTag
    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 out of $($TestResults.TotalCount) in ${ImageName}"
        $failed = $true
    } else {
        Write-Host "There were $($TestResults.PassedCount) passed tests in ${ImageName}"
    }
    Remove-Item env:\IMAGE_NAME
    Remove-Item env:\JAVA_VERSION

    return $failed
}

function Initialize-DockerComposeFile {
    $baseDockerBakeCmd = 'docker buildx bake --progress=plain --file=docker-bake.hcl'

    $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\": .}'''

    # - 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
    $generateDockerComposeFileCmd = ' {0} {1} --print' -f $baseDockerBakeCmd, $windowsFlavor + `
        ' | yq --prettyPrint {0} | yq {1}' -f $yqMainQuery, $yqServicesQuery + `
        ' | Out-File -FilePath {0}' -f $dockerComposeFile

    Write-Host "= PREPARE: Docker compose file generation command`n$generateDockerComposeFileCmd"

    Invoke-Expression $generateDockerComposeFileCmd

    # Remove override
    Remove-Item env:\WINDOWS_VERSION_OVERRIDE
}

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

# 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
}

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 }
    }
    Write-Host '= BUILD: Finished building all images.'

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

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

        $mod = Get-InstalledModule -Name Pester -MinimumVersion $PesterVersion -MaximumVersion $PesterVersion -ErrorAction SilentlyContinue
        if($null -eq $mod) {
            Write-Host "= TEST: Pester $PesterVersion not found: installing..."
            Install-Module -Force -Name Pester -MaximumVersion $PesterVersion -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 ('{0}|{1}' -f $imageDefinition.Value.image, $imageDefinition.Value.build.args.JAVA_VERSION))
        }

        # Fail if any test failures
        if($testFailed -ne $false) {
            Write-Error '= TEST: stage failed!'
            exit 1
        } else {
            Write-Host '= 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 publising the docker images
    if($lastExitCode -ne 0) {
        Write-Error '= PUBLISH: failed!'
        exit 1
    }
}

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


================================================
FILE: debian/Dockerfile
================================================
# The MIT License
#
#  Copyright (c) 2015-2024, CloudBees, Inc. and other Jenkins 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.
ARG DEBIAN_RELEASE=trixie-20260505
FROM debian:"${DEBIAN_RELEASE}"-slim AS jre-build

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

# This Build ARG is populated by Docker
# Ref. https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
ARG TARGETPLATFORM

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

ARG JAVA_VERSION=17.0.19_10
# hadolint ignore=DL3008
RUN set -x; apt-get update \
  && apt-get install --no-install-recommends -y \
    ca-certificates \
    jq \
    curl \
  && /usr/bin/local/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)
RUN case "$(jlink --version 2>&1)" in \
    "17."*) set -- "--compress=2" --add-modules ALL-MODULE-PATH ;; \
    "21."*) set -- "--compress=zip-6" --add-modules ALL-MODULE-PATH ;; \
    # JDK 25 switches to a minimal module set for smaller images
    "25"*) set -- "--compress=zip-6" --add-modules "java.base,java.logging,java.xml,java.management,java.net.http,jdk.crypto.ec" ;; \
    *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \
  esac; \
  jlink \
    --strip-java-debug-attributes \
    "$@" \
    --no-man-pages \
    --no-header-files \
    --output /javaruntime

# Downloading git-lfs from this intermediate stage as there is no wget/curl in the final one
ARG GIT_LFS_VERSION=3.7.1
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"

FROM debian:"${DEBIAN_RELEASE}"

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG JENKINS_AGENT_HOME=/home/${user}

ENV JENKINS_AGENT_HOME=${JENKINS_AGENT_HOME}
ARG AGENT_WORKDIR="${JENKINS_AGENT_HOME}/agent"
# Persist agent workdir path through an environment variable for people extending the image
ENV AGENT_WORKDIR=${AGENT_WORKDIR}

RUN groupadd -g ${gid} ${group} \
    && useradd -d "${JENKINS_AGENT_HOME}" -u "${uid}" -g "${gid}" -m -s /bin/bash "${user}" \
    # Prepare subdirectories
    && mkdir -p "${JENKINS_AGENT_HOME}/.ssh/" "${AGENT_WORKDIR}" "${JENKINS_AGENT_HOME}/.jenkins" \
    # Make sure that user 'jenkins' own these directories and their content
    && chown -R "${uid}":"${gid}" "${JENKINS_AGENT_HOME}" "${AGENT_WORKDIR}"

RUN apt-get update \
    && apt-get install --no-install-recommends -y \
        ca-certificates \
        git \
        less \
        netcat-traditional \
        openssh-server \
        patch \
    # Cleanup APT cache
    && rm -rf /var/lib/apt/lists/* \
    # Cleanup SSH host keys if any
    && rm -f /etc/ssh/ssh_host*_key*

# Retrieve git-lfs from the build stage and install it
COPY --from=jre-build git-lfs.tgz git-lfs.tgz
RUN tar xzf git-lfs.tgz \
  && bash git-lfs-*/install.sh \
  && rm -rf git-lfs*

# setup SSH server
RUN sed -i /etc/ssh/sshd_config \
        -e 's/#PermitRootLogin.*/PermitRootLogin no/' \
        -e 's/#RSAAuthentication.*/RSAAuthentication yes/'  \
        -e 's/#PasswordAuthentication.*/PasswordAuthentication no/' \
        -e 's/#SyslogFacility.*/SyslogFacility AUTH/' \
        -e 's/#LogLevel.*/LogLevel INFO/' && \
    mkdir -p /var/run/sshd && \
    sed -i /etc/pam.d/sshd \
        -e 's/\(session\s*\)required\(\s*pam_loginuid.so\)/\1optional\2/' \
        -e '/pam_motd/s/^/#/'

# VOLUME directive must happen after setting up permissions and content
VOLUME "${AGENT_WORKDIR}" "${JENKINS_AGENT_HOME}"/.jenkins "/tmp" "/run" "/var/run"
WORKDIR "${JENKINS_AGENT_HOME}"

ENV LANG='C.UTF-8' LC_ALL='C.UTF-8'

ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-build /javaruntime $JAVA_HOME

RUN echo "PATH=${PATH}" >> /etc/environment
COPY setup-sshd /usr/local/bin/setup-sshd

EXPOSE 22

ENTRYPOINT ["setup-sshd"]

LABEL \
    org.opencontainers.image.vendor="Jenkins project" \
    org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" \
    org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" \
    org.opencontainers.image.url="https://www.jenkins.io/" \
    org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" \
    org.opencontainers.image.licenses="MIT"


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

variable "default_jdk" {
  default = 21
}

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

variable "JENKINS_REPO" {
  default = "jenkins/ssh-agent"
}

variable "ON_TAG" {
  default = "false"
}

variable "VERSION" {
  default = ""
}

variable "ALPINE_FULL_TAG" {
  default = "3.23.4"
}

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

variable "JAVA17_VERSION" {
  default = "17.0.19_10"
}

variable "JAVA21_VERSION" {
  default = "21.0.11_10"
}

variable "JAVA25_VERSION" {
  default = "25.0.3_9"
}

variable "DEBIAN_RELEASE" {
  default = "trixie-20260505"
}

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

## Targets
target "alpine" {
  matrix = {
    jdk = jdks_to_build
  }
  name       = "alpine_${jdk}"
  dockerfile = "alpine/Dockerfile"
  context    = "."
  args = {
    ALPINE_TAG   = ALPINE_FULL_TAG
    JAVA_VERSION = "${javaversion(jdk)}"
  }
  tags = [
    # If there is a tag, add versioned tags suffixed by the jdk
    equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-alpine-jdk${jdk}" : "",
    equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-alpine${ALPINE_SHORT_TAG}-jdk${jdk}" : "",
    # If the jdk is the default one, add Alpine short tags
    is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:alpine" : "",
    is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:alpine${ALPINE_SHORT_TAG}" : "",
    is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:latest-alpine${ALPINE_SHORT_TAG}" : "",
    "${REGISTRY}/${JENKINS_REPO}:alpine-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:latest-alpine-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:alpine${ALPINE_SHORT_TAG}-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:latest-alpine${ALPINE_SHORT_TAG}-jdk${jdk}",
  ]
  platforms = alpine_platforms(jdk)
}

target "debian" {
  matrix = {
    jdk = jdks_to_build
  }
  name       = "debian_${jdk}"
  dockerfile = "debian/Dockerfile"
  context    = "."
  args = {
    DEBIAN_RELEASE = DEBIAN_RELEASE
    JAVA_VERSION   = "${javaversion(jdk)}"
  }
  tags = [
    # If there is a tag, add versioned tag suffixed by the jdk
    equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-jdk${jdk}" : "",
    # If there is a tag and if the jdk is the default one, add versioned short tag
    equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}" : "") : "",
    # If the jdk is the default one, add latest short tag
    is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:latest" : "",
    "${REGISTRY}/${JENKINS_REPO}:trixie-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:debian-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:latest-trixie-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:latest-debian-jdk${jdk}",
    "${REGISTRY}/${JENKINS_REPO}:latest-jdk${jdk}",
  ]
  platforms = debian_platforms(jdk)
}

target "nanoserver" {
  matrix = {
    jdk             = jdks_to_build
    windows_version = windowsversions("nanoserver")
  }
  name       = "nanoserver-${windows_version}_jdk${jdk}"
  dockerfile = "windows/nanoserver/Dockerfile"
  context    = "."
  args = {
    JAVA_HOME             = "C:/openjdk-${jdk}"
    JAVA_VERSION          = "${replace(javaversion(jdk), "_", "+")}"
    TOOLS_WINDOWS_VERSION = "${toolsversion(windows_version)}"
    WINDOWS_VERSION_TAG   = windows_version
  }
  tags = [
    # If there is a tag, add versioned tag suffixed by the jdk
    equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-nanoserver-${windows_version}-jdk${jdk}" : "",
    # If there is a tag and if the jdk is the default one, add versioned and short tags
    equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-nanoserver-${windows_version}" : "") : "",
    equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:nanoserver-${windows_version}" : "") : "",
    "${REGISTRY}/${JENKINS_REPO}:nanoserver-${windows_version}-jdk${jdk}",
  ]
  platforms = ["windows/amd64"]
}

target "windowsservercore" {
  matrix = {
    jdk             = jdks_to_build
    windows_version = windowsversions("windowsservercore")
  }
  name       = "windowsservercore-${windows_version}_jdk${jdk}"
  dockerfile = "windows/windowsservercore/Dockerfile"
  context    = "."
  args = {
    JAVA_HOME             = "C:/openjdk-${jdk}"
    JAVA_VERSION          = "${replace(javaversion(jdk), "_", "+")}"
    TOOLS_WINDOWS_VERSION = "${toolsversion(windows_version)}"
    WINDOWS_VERSION_TAG   = windows_version
  }
  tags = [
    # If there is a tag, add versioned tag suffixed by the jdk
    equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-windowsservercore-${windows_version}-jdk${jdk}" : "",
    # If there is a tag and if the jdk is the default one, add versioned and short tags
    equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-windowsservercore-${windows_version}" : "") : "",
    equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:windowsservercore-${windows_version}" : "") : "",
    "${REGISTRY}/${JENKINS_REPO}:windowsservercore-${windows_version}-jdk${jdk}",
  ]
  platforms = ["windows/amd64"]
}

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

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

group "linux-arm64" {
  targets = [
    "debian",
    "alpine_jdk21",
  ]
}

group "linux-s390x" {
  targets = [
    "debian_jdk21"
  ]
}

group "linux-ppc64le" {
  targets = [
    "debian"
  ]
}

## Common functions
# 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 = (equal(17, jdk)
    ? "${JAVA17_VERSION}"
    : equal(21, jdk)
    ? "${JAVA21_VERSION}"
  : "${JAVA25_VERSION}")
}

## Specific functions
# Return an array of Alpine platforms to use depending on the jdk passed as parameter
function "alpine_platforms" {
  params = [jdk]
  result = (equal(17, jdk)
    ? ["linux/amd64"]
  : ["linux/amd64", "linux/arm64"])
}

# Return an array of Debian platforms to use depending on the jdk passed as parameter
function "debian_platforms" {
  params = [jdk]
  result = (equal(17, jdk)
    ? ["linux/amd64", "linux/arm64", "linux/ppc64le"]
  : ["linux/amd64", "linux/arm64", "linux/ppc64le", "linux/s390x", "linux/riscv64"])
}

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

# Return the Windows version to use as base image for the Windows version passed as parameter
# There is no mcr.microsoft.com/powershell ltsc2019 base image, using a "1809" instead
function "toolsversion" {
  params = [version]
  result = (equal("ltsc2019", version)
    ? "1809"
  : version)
}


================================================
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: setup-sshd
================================================
#!/usr/bin/env bash

set -ex

# The MIT License
#
#  Copyright (c) 2015, CloudBees, Inc.
#
#  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.

# Usage:
#  docker run jenkins/ssh-agent <public key>
# or
#  docker run -e "JENKINS_AGENT_SSH_PUBKEY=<public key>" jenkins/ssh-agent

write_key() {
  local ID_GROUP

  # As user, group, uid, gid and JENKINS_AGENT_HOME can be overridden at build,
  # we need to find the values for JENKINS_AGENT_HOME
  # ID_GROUP contains the user:group of JENKINS_AGENT_HOME directory
  ID_GROUP=$(stat -c '%U:%G' "${JENKINS_AGENT_HOME}")

  mkdir -p "${JENKINS_AGENT_HOME}/.ssh"
  echo "$1" > "${JENKINS_AGENT_HOME}/.ssh/authorized_keys"
  chown -Rf "${ID_GROUP}" "${JENKINS_AGENT_HOME}/.ssh"
  chmod 0700 -R "${JENKINS_AGENT_HOME}/.ssh"
}

if [[ ${JENKINS_AGENT_SSH_PUBKEY} == ssh-* ]]; then
  write_key "${JENKINS_AGENT_SSH_PUBKEY}"
fi
if [[ ${JENKINS_SLAVE_SSH_PUBKEY} == ssh-* ]]; then
  write_key "${JENKINS_SLAVE_SSH_PUBKEY}"
fi

# ensure variables passed to docker container are also exposed to ssh sessions
env | grep _ >> /etc/environment

if [[ $# -gt 0 ]]; then
  echo "${0##*/} params: $@"

  if [[ $1 == ssh-* ]]; then
    echo "Authorizing ssh pubkey found in params."
    write_key "$1"
    shift 1
  elif [[ "$@" == "/usr/sbin/sshd -D -p 22" ]]; then
    # neutralize default jenkins docker-plugin command
    # we will run sshd at the end anyway
    echo "Ignoring provided sshd command."

    # if unquoted (4 tokens) shift extra 3
    [[ "$2" == "-D" ]] && shift 3

    shift 1
  else
    echo "Executing params: '$@'"
    exec "$@"
  fi
fi

# generate host keys if not present
ssh-keygen -A

# do not detach (-D), log to stderr (-e), passthrough other arguments
exec /usr/sbin/sshd -D -e "${@}"


================================================
FILE: setup-sshd.ps1
================================================
# The MIT License
#
#  Copyright (c) 2019-2020, Alex Earl
#
#  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.

# Usage:
#  docker run jenkins/ssh-agent <public key>
# or
#  docker run -e "JENKINS_AGENT_SSH_PUBKEY=<public key>" jenkins/ssh-agent
# or
#  docker run -e "JENKINS_AGENT_SSH_PUBKEY=<public key>" -e "JENKINS_AGENT_SSH_KNOWNHOST_0=<known host entry>" -e "JENKINS_AGENT_SSH_KNOWNHOST_n=<known host entry>" jenkins/ssh-agent

[CmdletBinding()]
Param(
    [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
    [string] $Cmd
)

function Get-SSHDir {
    return Join-Path "C:/Users/$env:JENKINS_AGENT_USER" '.ssh'
}

function Check-SSHDir {
    $sshDir = Get-SSHDir
    if(-not (Test-Path $sshDir)) {
        New-Item -Type Directory -Path $sshDir | Out-Null
        icacls.exe $sshDir /setowner $env:JENKINS_AGENT_USER | Out-Null
        icacls.exe $sshDir /grant $('{0}:(CI)(OI)(F)' -f $env:JENKINS_AGENT_USER) /grant "administrators:(CI)(OI)(F)" | Out-Null
        icacls.exe $sshDir /inheritance:r | Out-Null
    }
}

function Write-Key($Key) {
    # this writes the key and sets the permissions correctly for pubkey auth
    $authorizedKeys = Join-Path (Get-SSHDir) 'authorized_keys'
    Set-Content -Path $authorizedKeys -Value "$Key" -Encoding UTF8

    icacls.exe $authorizedKeys /setowner $env:JENKINS_AGENT_USER | Out-Null
}

function Write-HostKey($Key) {
    # this writes the key and sets the permissions
    $knownHosts = Join-Path (Get-SSHDir) 'known_hosts'
    Set-Content -Path $knownHosts -Value "$Key" -Encoding UTF8

    icacls.exe $knownHosts /setowner $env:JENKINS_AGENT_USER | Out-Null
}

# Give the user Full Access to the home directory
icacls.exe "C:/Users/$env:JENKINS_AGENT_USER" /grant "${env:JENKINS_AGENT_USER}:(CI)(OI)(F)" | Out-Null

# check the .ssh dir permissions
Check-SSHDir

if($env:JENKINS_AGENT_SSH_PUBKEY -match "^ssh-.*") {
    Write-Key $env:JENKINS_AGENT_SSH_PUBKEY
}

$index = 0
$knownHostKeyVar = Get-ChildItem -Path "env:JENKINS_AGENT_SSH_KNOWNHOST_$index" -ErrorAction 'SilentlyContinue'
while($null -ne $knownHostKeyVar) {
    Write-HostKey $knownHostKeyVar.Value
    $index++
    $knownHostKeyVar = Get-ChildItem env: -Name "JENKINS_AGENT_SSH_KNOWNHOST_$index"
}

# ensure variables passed to docker container are also exposed to ssh sessions
Get-ChildItem env: | ForEach-Object { setx /m $_.Name $_.Value | Out-Null }

if(![System.String]::IsNullOrWhiteSpace($Cmd)) {
    Write-Host "$($MyInvocation.MyCommand.Name) param: '$Cmd'"
    if($Cmd -match "^ssh-.*") {
        Write-Host "Authorizing ssh pubkey found in params."
        Write-Key $Cmd
    } elseif($Cmd -match "^/usr/sbin/sshd") {
        # neutralize default jenkins docker-plugin command
        # we will run sshd at the end anyway
        Write-Host "Ignoring provided (linux) sshd command."
    } else {
        Write-Host "Executing param: $Cmd"
        & $Cmd
        exit
    }
}

Start-Service sshd

# dump network information
ipconfig
netstat -a

# aside from forwarding ssh logs, this keeps the container open
Get-Content -Path "C:\ProgramData\ssh\logs\sshd.log" -Wait


================================================
FILE: tests/golden/expected_tags.txt
================================================
docker.io/jenkins/ssh-agent:alpine
docker.io/jenkins/ssh-agent:alpine-jdk17
docker.io/jenkins/ssh-agent:alpine-jdk21
docker.io/jenkins/ssh-agent:alpine-jdk25
docker.io/jenkins/ssh-agent:alpine3.23
docker.io/jenkins/ssh-agent:alpine3.23-jdk17
docker.io/jenkins/ssh-agent:alpine3.23-jdk21
docker.io/jenkins/ssh-agent:alpine3.23-jdk25
docker.io/jenkins/ssh-agent:debian-jdk17
docker.io/jenkins/ssh-agent:debian-jdk21
docker.io/jenkins/ssh-agent:debian-jdk25
docker.io/jenkins/ssh-agent:jdk17
docker.io/jenkins/ssh-agent:jdk21
docker.io/jenkins/ssh-agent:jdk25
docker.io/jenkins/ssh-agent:latest
docker.io/jenkins/ssh-agent:latest-alpine-jdk17
docker.io/jenkins/ssh-agent:latest-alpine-jdk21
docker.io/jenkins/ssh-agent:latest-alpine-jdk25
docker.io/jenkins/ssh-agent:latest-alpine3.23
docker.io/jenkins/ssh-agent:latest-alpine3.23-jdk17
docker.io/jenkins/ssh-agent:latest-alpine3.23-jdk21
docker.io/jenkins/ssh-agent:latest-alpine3.23-jdk25
docker.io/jenkins/ssh-agent:latest-debian-jdk17
docker.io/jenkins/ssh-agent:latest-debian-jdk21
docker.io/jenkins/ssh-agent:latest-debian-jdk25
docker.io/jenkins/ssh-agent:latest-jdk17
docker.io/jenkins/ssh-agent:latest-jdk21
docker.io/jenkins/ssh-agent:latest-jdk25
docker.io/jenkins/ssh-agent:latest-trixie-jdk17
docker.io/jenkins/ssh-agent:latest-trixie-jdk21
docker.io/jenkins/ssh-agent:latest-trixie-jdk25
docker.io/jenkins/ssh-agent:trixie-jdk17
docker.io/jenkins/ssh-agent:trixie-jdk21
docker.io/jenkins/ssh-agent:trixie-jdk25


================================================
FILE: tests/keys.bash
================================================
PUBLIC_SSH_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAvnRN27LdPPQq2OH3GiFFGWX/SH5TCPVePLR21ngMFV8nAthXgYrFkRi/t+Wafe3ByTu2XYUDlXHKGIPIoAKo4gz5dIjUFfoac1ZuCDIbEiqPEjkk4tkfc2qr/BnIZsOYQi4Mbu+Z40VZEsAQU7eBinnZaHE1qGMHjS1xfrRtp2rdeO1EBz92FJ8dfnkUnohTXo3qPVSFGIPbh7UKEoKcyCosRO1P41iWD1rVsH1SLLXYAh2t49L7IPiplg09Dep6H47LyQVbxU9eXY8yMtUrRuwEk9IUX/IqpxNhk5hngHPP3JjsP0hyyrYSPkZlbs3izd9kk3y09Wn/ElHidiEk0Q=="
PRIVATE_SSH_KEY=$(cat <<EOF
-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQEAvnRN27LdPPQq2OH3GiFFGWX/SH5TCPVePLR21ngMFV8nAthX
gYrFkRi/t+Wafe3ByTu2XYUDlXHKGIPIoAKo4gz5dIjUFfoac1ZuCDIbEiqPEjkk
4tkfc2qr/BnIZsOYQi4Mbu+Z40VZEsAQU7eBinnZaHE1qGMHjS1xfrRtp2rdeO1E
Bz92FJ8dfnkUnohTXo3qPVSFGIPbh7UKEoKcyCosRO1P41iWD1rVsH1SLLXYAh2t
49L7IPiplg09Dep6H47LyQVbxU9eXY8yMtUrRuwEk9IUX/IqpxNhk5hngHPP3Jjs
P0hyyrYSPkZlbs3izd9kk3y09Wn/ElHidiEk0QIBJQKCAQEAlUZmiZoHWUnAt9Oz
1jXAiYdLi9ih8kPGZu5PTia9XNvgTlaJxmXZHrKIbYpyK1l8NfCIBBwlZ0tZNc8S
3kdGGPVpkrBu4MryIwxkFELyn4kkB104lh/MiuTnqeqx1AEWeQ9V2mjEuQzXHIiy
2dUEqs40x3tTkdETwa3/AnG9upCsS8DpUmBa50hHvkc8pfmDrCbDAB7QjrgxAv7N
TjZQz1BslDnqULBs0weqD/YG60Vxdbu8ULHcMKYHmlk06a2lxF2A+CbvC+eLyD5B
+YHsD2CnpNhmBxLXfjnKuMhT6ybtop1hZW4zy0jLsyvAgM/kSb/iH9XJ17nfdlMm
NChQcQKBgQDvKs+81jDhoP+fZXi7bnVwlo2UzuTXNkUO1fLCFHWpJXMXu4wY6iMY
klEjXmN68Ijj0n3Enw7yM4/HBcnvRlw78zbDbKxwz5WRVc8w4/Ct4z8TX9Il1srR
Qa9vPhju8KazY1XxNMidMJmcR6cjG7glzKorE9faHc9aIskPP93y1wKBgQDL288f
tk0F/RcikCnfq8Ligm3GkZfP7lyf0T9lXHg0Qe9d3esvVHe02blMGm0vgsKy4Aip
jlyyM8ExI5yF2zUbOqLxDhWWqL6EnlYXEI4s5h/4AJOPrERGdOU/Ix7G312mqcmi
FlRVug8II64O7IgVU6pWyckOSMf6llyH/ItYlwKBgDotAhktLnwSZ7EmhSasKmd+
kSQyU1bxhmtkeVHNoBRjDiheDVIrHUsqgnBjEUdq8N14Y8gLA6KymJgx1yxdOQep
3ONtdg2aRvnWmi58olPPfguhr6hW12NVKqxbNn9PSyS3TEGXN7eIXLdPswiKM7Yq
3Ui/ozUOK4SgrXJpey07AoGAG4xoGQrMI2dj/cB0XH7+qPzeZvEUg+Hw11OgyIIe
FOZQx37al7F39dg7ooAcl7e5ch5GXBooM8HN/7i0SXCmT8mnUQHnPd9zsQ56ViTU
8U+Hx5FgDH8QJTJkKyBr8Vx0cHfPI73UC5WvARmUD9rGSBI5nQaC9BesUkuro6yB
iIMCgYAnlf3vd9/s8izGoHH1K2MJgGQT06Wn4ESjKpqqayqiXHccHGgeXeAiONa1
uiWcmBF4XtMTVXUGcS6DCm/jf/4JDI8B1eJCVQKLbZXZbENWnptDtj098NTt9NdV
TUwLP4n7pK4J2sCIs6fRD5kEYms4BnddXeRuI2fGZHGH70Ci/Q==
-----END RSA PRIVATE KEY-----
EOF
)


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

$global:IMAGE_NAME = Get-EnvOrDefault 'IMAGE_NAME' '' # Ex: jenkins4eval/ssh-agent:nanoserver-ltsc2019-jdk17
$global:JAVA_VERSION = Get-EnvOrDefault 'JAVA_VERSION' ''

Write-Host "= TESTS: Preparing $global:IMAGE_NAME with Java $global:JAVA_VERSION"

$imageItems = $global:IMAGE_NAME.Split(':')
$global:IMAGE_TAG = $imageItems[1]

$items = $global:IMAGE_TAG.Split('-')
# Remove the 'jdk' prefix
$global:JAVAMAJORVERSION = $items[2].Remove(0,3)
$global:WINDOWSFLAVOR = $items[0]
$global:WINDOWSVERSIONTAG = $items[1]
$global:TOOLSWINDOWSVERSION = $items[1]
# There are no eclipse-temurin:*-ltsc2019 or mcr.microsoft.com/powershell:*-ltsc2019 docker images unfortunately, only "1809" ones
if ($items[1] -eq 'ltsc2019') {
    $global:TOOLSWINDOWSVERSION = '1809'
}

# TODO: make this name unique for concurency
$global:CONTAINERNAME = 'pester-jenkins-ssh-agent-{0}' -f $global:IMAGE_TAG

$global:CONTAINERSHELL = 'powershell.exe'
if($global:WINDOWSFLAVOR -eq 'nanoserver') {
    $global:CONTAINERSHELL = 'pwsh.exe'
}

$global:PUBLIC_SSH_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAvnRN27LdPPQq2OH3GiFFGWX/SH5TCPVePLR21ngMFV8nAthXgYrFkRi/t+Wafe3ByTu2XYUDlXHKGIPIoAKo4gz5dIjUFfoac1ZuCDIbEiqPEjkk4tkfc2qr/BnIZsOYQi4Mbu+Z40VZEsAQU7eBinnZaHE1qGMHjS1xfrRtp2rdeO1EBz92FJ8dfnkUnohTXo3qPVSFGIPbh7UKEoKcyCosRO1P41iWD1rVsH1SLLXYAh2t49L7IPiplg09Dep6H47LyQVbxU9eXY8yMtUrRuwEk9IUX/IqpxNhk5hngHPP3JjsP0hyyrYSPkZlbs3izd9kk3y09Wn/ElHidiEk0Q=='
$global:PRIVATE_SSH_KEY = @"
-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQEAvnRN27LdPPQq2OH3GiFFGWX/SH5TCPVePLR21ngMFV8nAthX
gYrFkRi/t+Wafe3ByTu2XYUDlXHKGIPIoAKo4gz5dIjUFfoac1ZuCDIbEiqPEjkk
4tkfc2qr/BnIZsOYQi4Mbu+Z40VZEsAQU7eBinnZaHE1qGMHjS1xfrRtp2rdeO1E
Bz92FJ8dfnkUnohTXo3qPVSFGIPbh7UKEoKcyCosRO1P41iWD1rVsH1SLLXYAh2t
49L7IPiplg09Dep6H47LyQVbxU9eXY8yMtUrRuwEk9IUX/IqpxNhk5hngHPP3Jjs
P0hyyrYSPkZlbs3izd9kk3y09Wn/ElHidiEk0QIBJQKCAQEAlUZmiZoHWUnAt9Oz
1jXAiYdLi9ih8kPGZu5PTia9XNvgTlaJxmXZHrKIbYpyK1l8NfCIBBwlZ0tZNc8S
3kdGGPVpkrBu4MryIwxkFELyn4kkB104lh/MiuTnqeqx1AEWeQ9V2mjEuQzXHIiy
2dUEqs40x3tTkdETwa3/AnG9upCsS8DpUmBa50hHvkc8pfmDrCbDAB7QjrgxAv7N
TjZQz1BslDnqULBs0weqD/YG60Vxdbu8ULHcMKYHmlk06a2lxF2A+CbvC+eLyD5B
+YHsD2CnpNhmBxLXfjnKuMhT6ybtop1hZW4zy0jLsyvAgM/kSb/iH9XJ17nfdlMm
NChQcQKBgQDvKs+81jDhoP+fZXi7bnVwlo2UzuTXNkUO1fLCFHWpJXMXu4wY6iMY
klEjXmN68Ijj0n3Enw7yM4/HBcnvRlw78zbDbKxwz5WRVc8w4/Ct4z8TX9Il1srR
Qa9vPhju8KazY1XxNMidMJmcR6cjG7glzKorE9faHc9aIskPP93y1wKBgQDL288f
tk0F/RcikCnfq8Ligm3GkZfP7lyf0T9lXHg0Qe9d3esvVHe02blMGm0vgsKy4Aip
jlyyM8ExI5yF2zUbOqLxDhWWqL6EnlYXEI4s5h/4AJOPrERGdOU/Ix7G312mqcmi
FlRVug8II64O7IgVU6pWyckOSMf6llyH/ItYlwKBgDotAhktLnwSZ7EmhSasKmd+
kSQyU1bxhmtkeVHNoBRjDiheDVIrHUsqgnBjEUdq8N14Y8gLA6KymJgx1yxdOQep
3ONtdg2aRvnWmi58olPPfguhr6hW12NVKqxbNn9PSyS3TEGXN7eIXLdPswiKM7Yq
3Ui/ozUOK4SgrXJpey07AoGAG4xoGQrMI2dj/cB0XH7+qPzeZvEUg+Hw11OgyIIe
FOZQx37al7F39dg7ooAcl7e5ch5GXBooM8HN/7i0SXCmT8mnUQHnPd9zsQ56ViTU
8U+Hx5FgDH8QJTJkKyBr8Vx0cHfPI73UC5WvARmUD9rGSBI5nQaC9BesUkuro6yB
iIMCgYAnlf3vd9/s8izGoHH1K2MJgGQT06Wn4ESjKpqqayqiXHccHGgeXeAiONa1
uiWcmBF4XtMTVXUGcS6DCm/jf/4JDI8B1eJCVQKLbZXZbENWnptDtj098NTt9NdV
TUwLP4n7pK4J2sCIs6fRD5kEYms4BnddXeRuI2fGZHGH70Ci/Q==
-----END RSA PRIVATE KEY-----
"@

$global:GITLFSVERSION = '3.7.1'

Cleanup($global:CONTAINERNAME)

Describe "[$global:IMAGE_TAG] image has setup-sshd.ps1 in the correct location" {
    BeforeAll {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:CONTAINERSHELL`""
        $exitCode | Should -Be 0
        Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue
    }

    It 'has setup-sshd.ps1 in C:/ProgramData/Jenkins' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"if(Test-Path C:/ProgramData/Jenkins/setup-sshd.ps1) { exit 0 } else { exit 1}`""
        $exitCode | Should -Be 0
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
    }
}

Describe "[$global:IMAGE_TAG] image has no pre-existing SSH host keys" {
    BeforeAll {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:CONTAINERSHELL`""
        $exitCode | Should -Be 0
        Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue
    }

    It 'has has no SSH host key present in C:\ProgramData\ssh' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"if(Test-Path C:/ProgramData/ssh/ssh_host*_key*) { exit 0 } else { exit 1 }`""
        $exitCode | Should -Be 1
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
    }
}

Describe "[$global:IMAGE_TAG] checking image metadata" {
    It 'has correct volumes' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "inspect --format '{{.Config.Volumes}}' $global:IMAGE_NAME"
        $exitCode | Should -Be 0

        $stdout | Should -Match 'C:/Users/jenkins/AppData/Local/Temp'
        $stdout | Should -Match 'C:/Users/jenkins/Work'
    }

    It 'has the source GitHub URL in docker metadata' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "inspect --format=`"{{index .Config.Labels \`"org.opencontainers.image.source\`"}}`" $global:IMAGE_NAME"
        $exitCode | Should -Be 0
        $stdout.Trim() | Should -Match 'https://github.com/jenkinsci/docker-ssh-agent'
    }
}

Describe "[$global:IMAGE_TAG] image has correct version of java and git-lfs installed and in the PATH" {
    BeforeAll {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:PUBLIC_SSH_KEY`""
        $exitCode | Should -Be 0
        Is-ContainerRunning $global:CONTAINERNAME
    }

    It 'has java installed and in the path' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"if(`$null -eq (Get-Command java.exe -ErrorAction SilentlyContinue)) { exit -1 } else { exit 0 }`""
        $exitCode | Should -Be 0

        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"`$version = java -version 2>&1 ; Write-Host `$version`""
        $r = [regex] "^openjdk version `"(?<major>\d+)"
        $m = $r.Match($stdout)
        $m | Should -Not -Be $null
        $m.Groups['major'].ToString() | Should -Be "$global:JAVAMAJORVERSION"
    }

    It 'has git-lfs (and thus git) installed and in the path' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"`& git lfs env`""
        $exitCode | Should -Be 0
        $r = [regex] "^git-lfs/(?<version>\d+\.\d+\.\d+)"
        $m = $r.Match($stdout)
        $m | Should -Not -Be $null
        $m.Groups['version'].ToString() | Should -Be "$global:GITLFSVERSION"
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
    }
}

Describe "[$global:IMAGE_TAG] create agent container with pubkey as argument" {
    BeforeAll {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:PUBLIC_SSH_KEY`""
        $exitCode | Should -Be 0
        Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue
    }

    It 'runs commands via ssh' {
        $exitCode, $stdout, $stderr = Run-ThruSSH $global:CONTAINERNAME "$global:PRIVATE_SSH_KEY" "$global:CONTAINERSHELL -NoLogo -C `"Write-Host 'f00'`""
        $exitCode | Should -Be 0
        $stdout | Should -Match 'f00'
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
    }
}

Describe "[$global:IMAGE_TAG] create agent container with pubkey as envvar" {
    BeforeAll {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:PUBLIC_SSH_KEY`""
        $exitCode | Should -Be 0
        Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue
    }

    It 'runs commands via ssh' {
        $exitCode, $stdout, $stderr = Run-ThruSSH $global:CONTAINERNAME "$global:PRIVATE_SSH_KEY" "$global:CONTAINERSHELL -NoLogo -C `"Write-Host 'f00'`""
        $exitCode | Should -Be 0
        $stdout | Should -Match 'f00'
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
    }
}


$global:DOCKER_PLUGIN_DEFAULT_ARG="/usr/sbin/sshd -D -p 22"
Describe "[$global:IMAGE_TAG] create agent container like docker-plugin with '$global:DOCKER_PLUGIN_DEFAULT_ARG' as argument" {
    BeforeAll {
        [string]::IsNullOrWhiteSpace($global:DOCKER_PLUGIN_DEFAULT_ARG) | Should -BeFalse
        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all --env=`"JENKINS_AGENT_SSH_PUBKEY=$global:PUBLIC_SSH_KEY`" `"$global:IMAGE_NAME`" `"$global:DOCKER_PLUGIN_DEFAULT_ARG`""
        $exitCode | Should -Be 0
        Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue
    }

    It 'runs commands via ssh' {
        $exitCode, $stdout, $stderr = Run-ThruSSH $global:CONTAINERNAME "$global:PRIVATE_SSH_KEY" "$global:CONTAINERSHELL -NoLogo -C `"Write-Host 'f00'`""
        $exitCode | Should -Be 0
        $stdout | Should -Match 'f00'
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
    }
}

Describe "[$global:IMAGE_TAG] image can be built" {
    It 'builds image' {
        $exitCode, $stdout, $stderr = Run-Program 'docker' "build --build-arg `"WINDOWS_VERSION_TAG=${global:WINDOWSVERSIONTAG}`" --build-arg `"TOOLS_WINDOWS_VERSION=${global:TOOLSWINDOWSVERSION}`" --build-arg `"JAVA_VERSION=${global:JAVA_VERSION}`" --build-arg `"JAVA_HOME=C:\openjdk-${global:JAVAMAJORVERSION}`" --tag=${global:IMAGE_TAG} --file ./windows/${global:WINDOWSFLAVOR}/Dockerfile ."
        $exitCode | Should -Be 0
    }
}

Describe "[$global:IMAGE_TAG] image can be built with custom build args" {
    BeforeAll {
        Push-Location -StackName 'agent' -Path "$PSScriptRoot/.."
    }

    It 'uses build args correctly' {
        $TEST_USER = 'testuser'
        $TEST_JAW = 'C:/hamster'
        $CUSTOM_IMAGE_NAME = "custom-${IMAGE_NAME}"

        $exitCode, $stdout, $stderr = Run-Program 'docker' "build --build-arg `"WINDOWS_VERSION_TAG=${global:WINDOWSVERSIONTAG}`" --build-arg `"TOOLS_WINDOWS_VERSION=${global:TOOLSWINDOWSVERSION}`" --build-arg `"JAVA_VERSION=${global:JAVA_VERSION}`" --build-arg `"JAVA_HOME=C:\openjdk-${global:JAVAMAJORVERSION}`" --build-arg `"user=$TEST_USER`" --build-arg `"JENKINS_AGENT_WORK=$TEST_JAW`" --tag=$CUSTOM_IMAGE_NAME --file ./windows/${global:WINDOWSFLAVOR}/Dockerfile ."
        $exitCode | Should -Be 0

        $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=$global:CONTAINERNAME --publish-all $CUSTOM_IMAGE_NAME $global:CONTAINERSHELL"
        $exitCode | Should -Be 0
        Is-ContainerRunning "$global:CONTAINERNAME" | Should -BeTrue

        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME net user $TEST_USER"
        $exitCode | Should -Be 0
        $stdout | Should -Match "User name\s*$TEST_USER"

        $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"(Get-ChildItem env:\ | Where-Object { `$_.Name -eq 'JENKINS_AGENT_WORK' }).Value`""
        $exitCode | Should -Be 0
        $stdout.Trim() | Should -Match "$TEST_JAW"
    }

    AfterAll {
        Cleanup($global:CONTAINERNAME)
        Pop-Location -StackName 'agent'
    }
}


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

load test_helpers

SUT_DESCRIPTION="tags"

@test "[${SUT_DESCRIPTION}] Default tags unchanged" {
  assert_matches_golden expected_tags make --silent tags
}


================================================
FILE: tests/test_helpers.bash
================================================
#!/usr/bin/env bash

set -eu

# check dependencies
(
    type docker &>/dev/null || ( echo "docker is not available"; exit 1 )
    type curl &>/dev/null || ( echo "curl is not available"; exit 1 )
    type ssh &>/dev/null || ( echo "ssh is not available"; exit 1 )
)>&2

function printMessage {
  echo "# ${@}" >&3
}

# Assert that $1 is the output of a command $2
function assert_run_cmd_output_equal {
    local expected_output
    local actual_output
    expected_output="${1}"
    shift
    run "${@}"
    assert_output "${expected_output}"
}

# 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}")
}

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]"
}

# Retry a command $1 times until it succeeds. Wait $2 seconds between retries.
# Command is passed as the "rest" of arguments: $3 $4 $5 ...
function retry {
    local attempts
    local delay
    local i
    attempts="${1}"
    shift
    delay="${1}"
    shift

    for ((i=0; i < attempts; i++)); do
        run "${@}"
        if assert_success; then
            return 0
        fi
        sleep "${delay}"
    done

    printMessage "Command '${BATS_RUN_COMMAND}' failed ${attempts} times. Status: ${status}. Output: ${output}"

    false
}

# return the published port for given container port $1
function get_port {
  local agent_container_name="${1}"
  local port="${2}"
  docker port "${agent_container_name}" "${port}" | cut -d: -f2
}

# run a given command through ssh on the test container.
# Use the $status, $output and $lines variables to make assertions
function run_through_ssh {
  local agent_container_name="${1}"
  shift 1
  SSH_PORT=$(get_port "${agent_container_name}" 22)
	if [[ "${SSH_PORT}" = "" ]]; then
		printMessage "failed to get SSH port"
		false
	else
		TMP_PRIV_KEY_FILE=$(mktemp "${BATS_TMPDIR}"/bats_private_ssh_key_XXXXXXX)
		echo "${PRIVATE_SSH_KEY}" > "${TMP_PRIV_KEY_FILE}" \
			&& chmod 0600 "${TMP_PRIV_KEY_FILE}"

		echo "[DEBUG] *** Running ssh command"
		run ssh -i "${TMP_PRIV_KEY_FILE}" \
			-o LogLevel=quiet \
			-o UserKnownHostsFile=/dev/null \
			-o StrictHostKeyChecking=no \
			-l jenkins \
			127.0.0.1 \
			-p "${SSH_PORT}" \
			"${@}"
    echo "[DEBUG] *** Command was: ${BATS_RUN_COMMAND}"

		rm -f "${TMP_PRIV_KEY_FILE}"
	fi
}

function clean_test_container {
  local agent_container=$1
  docker kill "${agent_container}" &>/dev/null ||:
  docker rm --force --volumes "${agent_container}" &>/dev/null ||:
}

function is_agent_container_running {
  local agent_container=$1
  # 30s is considered enough for the SSH server to start, even under constraint
	retry 15 2 assert_run_cmd_output_equal healthy docker inspect -f '{{.State.Health.Status}}' "${agent_container}"
}


================================================
FILE: tests/test_helpers.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 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 Retry-Command {
    [CmdletBinding()]
    param (
        [parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [scriptblock] $ScriptBlock,
        [int] $RetryCount = 3,
        [int] $Delay = 30,
        [string] $SuccessMessage = 'Command executed successfuly!',
        [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 Cleanup($name='') {
    if([System.String]::IsNullOrWhiteSpace($name)) {
        $name = Get-EnvOrDefault 'IMAGE_NAME' ''
    }

    if(![System.String]::IsNullOrWhiteSpace($name)) {
        docker kill "$name" 2>&1 | Out-Null
        docker rm -fv "$name" 2>&1 | Out-Null
    }
}

function CleanupNetwork($name) {
    docker network rm $name 2>&1 | Out-Null
}

function Is-ContainerRunning($container) {
    Start-Sleep -Seconds 5
    return Retry-Command -RetryCount 10 -Delay 2 -ScriptBlock {
        $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "inspect --format `"{{.State.Running}}`" $container"
        if(($exitCode -ne 0) -or (-not $stdout.Contains('true')) ) {
            throw('Exit code incorrect, or invalid value for running state')
        }
        return $true
    }
}

function Run-Program($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(($env:TESTS_DEBUG -eq 'debug') -or ($env:TESTS_DEBUG -eq 'verbose')) {
        Write-Host -ForegroundColor DarkBlue "[cmd] $cmd $params"
        if ($env:TESTS_DEBUG -eq 'verbose') { Write-Host -ForegroundColor DarkGray "[stdout] $stdout" }
        if($proc.ExitCode -ne 0){
            Write-Host -ForegroundColor DarkRed "[stderr] $stderr"
        }
    }
    return $proc.ExitCode, $stdout, $stderr
}

# return the published port for given container port $1
function Get-Port($container, $port=22) {
    $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "port $container $port"
    return ($stdout -split ":" | Select-Object -Skip 1).Trim()
}

# run a given command through ssh on the test container.
function Run-ThruSSH($container, $privateKeyVal, $cmd) {
    $SSH_PORT = Get-Port $container 22
    if([System.String]::IsNullOrWhiteSpace($SSH_PORT)) {
        Write-Error 'Failed to get SSH port'
        return -1, $null, $null
    } else {
        $TMP_PRIV_KEY_FILE = New-TemporaryFile
        Set-Content -Path $TMP_PRIV_KEY_FILE -Value "$privateKeyVal"

        $exitCode, $stdout, $stderr = Run-Program 'ssh.exe' "-v -i `"${TMP_PRIV_KEY_FILE}`" -o LogLevel=quiet -o UserKnownHostsFile=NUL -o StrictHostKeyChecking=no -l jenkins localhost -p $SSH_PORT $cmd"
        Remove-Item -Force $TMP_PRIV_KEY_FILE

        return $exitCode, $stdout, $stderr
    }
}


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

load test_helpers
load 'test_helper/bats-support/load' # this is required by bats-assert!
load 'test_helper/bats-assert/load'
load keys

IMAGE=${IMAGE:-debian_jdk11}
SUT_IMAGE=$(get_sut_image)

ARCH=${ARCH:-x86_64}
AGENT_CONTAINER=bats-jenkins-ssh-agent

# TODO: uncomment when git-lfs version is the same across all images
# GIT_LFS_VERSION='3.7.1'

# About the health CMD: the netcat command (`nc`) needs the options `-w1` to return 1s after reaches EOF. It's a portable option of `nc` (on BSD, Debian, Windows, busybox).
# Of course, to reach EOF, you need to provide something to the stding: it's the reason of the `echo` piped command
docker_run_opts=('--detach' '--publish-all' '--health-cmd=echo | nc -w1 localhost 22' '--health-start-period=2s' '--health-interval=2s' '--health-retries=10' '--health-timeout=2s' "${SUT_IMAGE}")

@test "[${SUT_IMAGE}] test label in docker metadata" {
  local expected_source="https://github.com/jenkinsci/docker-ssh-agent"

  local actual_source
  actual_source=$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.source"}}' "${SUT_IMAGE}")

  assert_equal "${expected_source}" "${actual_source}"
}

@test "[${SUT_IMAGE}] checking image metadata" {
  local VOLUMES_MAP
  VOLUMES_MAP="$(docker inspect -f '{{.Config.Volumes}}' "${SUT_IMAGE}")"

  echo "${VOLUMES_MAP}" | grep '/tmp'
  echo "${VOLUMES_MAP}" | grep '/home/jenkins'
  echo "${VOLUMES_MAP}" | grep '/run'
  echo "${VOLUMES_MAP}" | grep '/var/run'
}

@test "[${SUT_IMAGE}] image has bash and java installed and in the PATH" {
  local test_container_name=${AGENT_CONTAINER}-bash-java
  clean_test_container "${test_container_name}"
  docker run --name="${test_container_name}" --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}"

  run docker exec "${test_container_name}" which bash
  assert_success
  run docker exec "${test_container_name}" bash --version
  assert_success
  run docker exec "${test_container_name}" which java
  assert_success

  run docker exec "${test_container_name}" sh -c "java -version"
  assert_success

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] image has no pre-existing SSH host keys" {
  local test_container_name=${AGENT_CONTAINER}-ssh-hostkeys
  clean_test_container "${test_container_name}"
  docker run --name="${test_container_name}" --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}"

  run docker exec "${test_container_name}" ls -l /etc/ssh/ssh_host*_key*
  assert_failure

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] create agent container with pubkey as argument" {
  local test_container_name=${AGENT_CONTAINER}-pubkey-arg
  clean_test_container "${test_container_name}"
  docker run --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}"

  is_agent_container_running "${test_container_name}"

  run_through_ssh "${test_container_name}" echo f00
  assert_success
  assert_equal "${output}" "f00"

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] create agent container with pubkey as environment variable (legacy environment variable)" {
  local test_container_name=${AGENT_CONTAINER}-pubkey-legacy-env
  clean_test_container "${test_container_name}"
  docker run --env="JENKINS_SLAVE_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}"

  is_agent_container_running "${test_container_name}"

  run_through_ssh "${test_container_name}" echo f00
  assert_success
  assert_equal "${output}" "f00"

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] create agent container with pubkey as environment variable (JENKINS_AGENT_SSH_PUBKEY)" {
  local test_container_name=${AGENT_CONTAINER}-pubkey-env
  clean_test_container "${test_container_name}"
  docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}"

  is_agent_container_running "${test_container_name}"

  run_through_ssh "${test_container_name}" echo f00
  assert_success
  assert_equal "${output}" "f00"

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] Run Java in a SSH connection" {
  local test_container_name=${AGENT_CONTAINER}-java-in-ssh
  clean_test_container "${test_container_name}"
  docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}"

  is_agent_container_running "${test_container_name}"

  run_through_ssh "${test_container_name}" java -version
  assert_success
  assert_output --regexp '^openjdk version \"[[:digit:]]+'

  clean_test_container "${test_container_name}"
}

DOCKER_PLUGIN_DEFAULT_ARG="/usr/sbin/sshd -D -p 22"
@test "[${SUT_IMAGE}] create agent container like docker-plugin with '${DOCKER_PLUGIN_DEFAULT_ARG}' (unquoted) as argument" {
  [ -n "$DOCKER_PLUGIN_DEFAULT_ARG" ]

  local test_container_name=${AGENT_CONTAINER}-docker-plugin
  clean_test_container "${test_container_name}"
  docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" ${DOCKER_PLUGIN_DEFAULT_ARG}

  is_agent_container_running "${test_container_name}"

  run_through_ssh "${test_container_name}" echo f00
  assert_success
  assert_equal "${output}" "f00"

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] create agent container with '${DOCKER_PLUGIN_DEFAULT_ARG}' (quoted) as argument" {
  [ -n "$DOCKER_PLUGIN_DEFAULT_ARG" ]

  local test_container_name=${AGENT_CONTAINER}-docker-plugin-quoted
  clean_test_container "${test_container_name}"
  docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" "${DOCKER_PLUGIN_DEFAULT_ARG}"

  is_agent_container_running "${test_container_name}"

  run_through_ssh "${test_container_name}" echo f00
  assert_success
  assert_equal "${output}" "f00"

  clean_test_container "${test_container_name}"
}

@test "[${SUT_IMAGE}] use build args correctly" {
  cd "${BATS_TEST_DIRNAME}"/.. || false

	local TEST_USER=test-user
	local TEST_GROUP=test-group
	local TEST_UID=2000
	local TEST_GID=3000
	local TEST_JAH=/home/something

	local sut_image="${SUT_IMAGE}-tests-${BATS_TEST_NUMBER}"

  # false positive detecting platform
  # shellcheck disable=SC2140
  docker buildx bake \
    --set "${IMAGE}".args.user="${TEST_USER}" \
    --set "${IMAGE}".args.group="${TEST_GROUP}" \
    --set "${IMAGE}".args.uid="${TEST_UID}" \
    --set "${IMAGE}".args.gid="${TEST_GID}" \
    --set "${IMAGE}".args.JENKINS_AGENT_HOME="${TEST_JAH}" \
    --set "${IMAGE}".platform="linux/${ARCH}" \
    --set "${IMAGE}".tags="${sut_image}" \
      --load `# Image should be loaded on the Docker engine`\
      "${IMAGE}"

  local test_container_name=${AGENT_CONTAINER}-build-args
  clean_test_container "${test_container_name}"
  docker run --detach --name="${test_container_name}" --publish-all "${sut_image}" "${PUBLIC_SSH_KEY}"

  run docker exec "${test_container_name}" sh -c "id -u -n ${TEST_USER}"
  assert_line --index 0 "${TEST_USER}"
  run docker exec "${test_container_name}" sh -c "id -g -n ${TEST_USER}"
  assert_line --index 0 "${TEST_GROUP}"
  run docker exec "${test_container_name}" sh -c "id -u ${TEST_USER}"
  assert_line --index 0 "${TEST_UID}"
  run docker exec "${test_container_name}" sh -c "id -g ${TEST_USER}"
  assert_line --index 0 "${TEST_GID}"
  run docker exec "${test_container_name}" sh -c 'stat -c "%U:%G" "${JENKINS_AGENT_HOME}"'
  assert_line --index 0 "${TEST_USER}:${TEST_GROUP}"

  clean_test_container "${test_container_name}"
}

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

@test "[${SUT_IMAGE}] the default 'jenkins' user is allowed to write in the default agent directory" {
  run docker run --user=jenkins --entrypoint='' --rm "${SUT_IMAGE}" sh -c 'touch "${AGENT_WORKDIR}"/test.txt'
  assert_success
}

@test "[${SUT_IMAGE}] image has required tools installed and present in the PATH, can clone a repo and list large files" {
  local test_container_name=${AGENT_CONTAINER}-bash-java
  clean_test_container "${test_container_name}"
  docker run --name="${test_container_name}" --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}"

  run docker exec "${test_container_name}" sh -c "command -v ssh"
  assert_success
  run docker exec "${test_container_name}" ssh -V
  assert_success

  run docker exec "${test_container_name}" sh -c "command -v git"
  assert_success
  run docker exec "${test_container_name}" git --version
  assert_success

  run docker exec "${test_container_name}" sh -c "command -v less"
  assert_success
  run docker exec "${test_container_name}" less -V
  assert_success
  run docker exec "${test_container_name}" sh -c "command -v patch"
  assert_success
  run docker exec "${test_container_name}" patch --version
  assert_success

  run docker exec "${test_container_name}" git clone https://github.com/jenkinsci/docker-ssh-agent.git
  assert_success

  run docker exec "${test_container_name}" git lfs env
  assert_success
  # TODO: replace assert_success with assert_output when git-lfs version is the same across all images
  # assert_output --partial "${GIT_LFS_VERSION}"

  clean_test_container "${test_container_name}"
}


================================================
FILE: tests/update-golden-file.sh
================================================
#!/usr/bin/env bash
set -euo pipefail

# This script runs a specified command, captures its output,
# and compares it against a "golden file" representing the expected output.
# If the output differs, the script shows a diff and allows the user to update the golden file.
# If the output matches, it reports that the golden file is up-to-date.
#
# Usage:
#   ./update-golden-file.sh <test-name> <command...>
#
# Arguments:
#   <test-name>    Name of the test, used to determine the golden file path.
#                  The corresponding golden file will be stored as:
#                     golden/<test-name>.txt
#
#   <command...>   Command to run, whose stdout will be compared to the golden file.
#                  This can include arguments, e.g.:
#                     ./update-golden-file.sh expected_tags make tags
#
# Notes:
#   - Requires Bash 4+ for `BASH_SOURCE` handling.
#   - The script is safe to run from any directory; golden files are always relative to the script's own location.

if [[ $# -lt 2 ]]; then
  echo "Usage: $0 <test-name> <command...>"
  echo "Example:"
  echo "  $0 expected_tags make tags"
  exit 1
fi

name="$1"
shift

# Ensure golden folder is always relative to this script
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
golden_file="${name}.txt"
golden_path="${script_dir}/golden/${golden_file}"
tmp="$(mktemp)"

echo
echo "Golden file path:"
echo "  ${golden_path}"
echo
echo "Running command:"
echo "  $*"
echo

"$@" > "${tmp}"

action="create"
if [[ -f "${golden_path}" ]]; then
    if diff -u "${golden_path}" "${tmp}" > /dev/null; then
        echo "Golden file '${golden_file}' is already up-to-date."
        rm "${tmp}"
        exit 0
    fi
    echo "Diff against existing golden file '${golden_file}':"
    diff -u "${golden_path}" "${tmp}" || true
    action="update"
else
    echo "Golden file '${golden_file}' does not exist yet."
fi

echo
echo "Golden file to ${action}: '${golden_file}'"
read -rp "Proceed? [y/N] " answer

if [[ "${answer}" =~ ^[Yy]$ ]]; then
    mkdir -p "$(dirname "${golden_path}")"
    mv "${tmp}" "${golden_path}"
    echo "Golden file '${golden_file}' ${action}d."
else
    rm "${tmp}"
    echo "Aborted. Golden file '${golden_file}' unchanged."
fi


================================================
FILE: updatecli/updatecli.d/alpine.yaml
================================================
---
name: Bump Alpine version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  latestVersion:
    kind: githubrelease
    name: "Get the latest Alpine Linux version"
    spec:
      owner: "alpinelinux"
      repository: "aports" # Its release process follows Alpine's
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      versionfilter:
        kind: semver
        pattern: "~3"
    transformers:
      - trimprefix: "v"

conditions:
  testDockerfileArg:
    name: "Does the Dockerfile have an ARG instruction for the Alpine Linux version?"
    kind: dockerfile
    disablesourceinput: true
    spec:
      file: alpine/Dockerfile
      instruction:
        keyword: "ARG"
        matcher: "ALPINE_TAG"
  testDockerImageExists:
    name: "Does the Docker Image exist on the Docker Hub?"
    kind: dockerimage
    sourceid: latestVersion
    spec:
      image: "alpine"
      # tag come from the source
      architecture: amd64

targets:
  updateDockerfile:
    name: "Update the value of the base image (ARG ALPINE_TAG) in the Dockerfile"
    kind: dockerfile
    spec:
      file: alpine/Dockerfile
      instruction:
        keyword: "ARG"
        matcher: "ALPINE_TAG"
    scmid: default
  updateDockerBake:
    name: "Update the value of the base image (ARG ALPINE_TAG) in the docker-bake.hcl"
    kind: hcl
    spec:
      file: docker-bake.hcl
      path: variable.ALPINE_FULL_TAG.default
    scmid: default
actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump Alpine Linux Version to {{ source "latestVersion" }}
    spec:
      labels:
        - dependencies


================================================
FILE: updatecli/updatecli.d/bats.yaml
================================================
---
name: Bump `bats` version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  lastVersion:
    kind: githubrelease
    spec:
      owner: bats-core
      repository: bats-core
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      versionfilter:
        kind: semver

targets:
  updateMakefile:
    name: "Updates `bats` version in the Makefile"
    kind: file
    spec:
      file: Makefile
      matchpattern: >-
        git clone --branch (.*) https://github.com/bats-core/bats-core bats
      replacepattern: >-
        git clone --branch {{ source "lastVersion" }} https://github.com/bats-core/bats-core bats
    scmid: default

actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: 'chore(tests): Bump `bats` version to {{ source "lastVersion" }}'
    spec:
      labels:
        - chore # Because bats is only used for testing
        - bats


================================================
FILE: updatecli/updatecli.d/debian.yaml
================================================
---
name: Bump Debian Trixie version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  trixieLatestVersion:
    kind: dockerimage
    name: "Get latest Debian Trixie Linux version"
    spec:
      image: "debian"
      tagfilter: "trixie-*"
      versionfilter:
        kind: regex
        pattern: >-
          trixie-\d+$

conditions:
  checkArchitecturesAvailability:
    kind: dockerimage
    name: Check if container image is available for all architectures
    sourceid: trixieLatestVersion
    spec:
      image: "debian"
      architectures:
        - linux/amd64
        - linux/arm64
        - linux/s390x
        - linux/ppc64le

targets:
  updateDockerfile:
    name: "Update value of base image (ARG DEBIAN_RELEASE) in Dockerfile"
    kind: dockerfile
    sourceid: trixieLatestVersion
    spec:
      file: debian/Dockerfile
      instruction:
        keyword: "ARG"
        matcher: "DEBIAN_RELEASE"
    scmid: default
  updateDockerBake:
    name: "Update default value of variable DEBIAN_RELEASE in docker-bake.hcl"
    kind: hcl
    sourceid: trixieLatestVersion
    spec:
      file: docker-bake.hcl
      path: variable.DEBIAN_RELEASE.default
    scmid: default

actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump Debian Trixie Linux version to {{ source "trixieLatestVersion" }}
    spec:
      labels:
        - dependencies


================================================
FILE: updatecli/updatecli.d/git-lfs.yml
================================================
name: Bump `git-lfs` version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  lastVersion:
    kind: githubrelease
    name: Get latest `git-lfs` version
    spec:
      owner: git-lfs
      repository: git-lfs
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      versionfilter:
        kind: semver
    transformers:
      - trimprefix: "v"

targets:
  setGitLfsVersion:
    name: Update `git-lfs` version in Dockerfiles
    kind: dockerfile
    spec:
      files:
        - alpine/Dockerfile
        - debian/Dockerfile
        - windows/nanoserver/Dockerfile
        - windows/windowsservercore/Dockerfile
      instruction:
        keyword: ARG
        matcher: GIT_LFS_VERSION
    scmid: default
  setGitLfsVersionLinuxTests:
    name: Update `git-lfs` version in Linux tests
    kind: file
    spec:
      file: tests/tests.bats
      matchpattern: >
        GIT_LFS_VERSION=(.*)
      replacepattern: >
        GIT_LFS_VERSION='{{ source "lastVersion" }}'
    scmid: default
  setGitLfsVersionWindowsTests:
    name: Update `git-lfs` version in Windows tests
    kind: file
    spec:
      file: tests/sshAgent.Tests.ps1
      matchpattern: >
        global:GITLFSVERSION =(.*)
      replacepattern: >
        global:GITLFSVERSION = '{{ source "lastVersion" }}'
    scmid: default

actions:
  default:
    kind: github/pullrequest
    title: Bump `git-lfs` version to {{ source "lastVersion" }}
    scmid: default
    spec:
      labels:
        - enhancement
        - git-lfs


================================================
FILE: updatecli/updatecli.d/git-windows.yml
================================================
---
name: Bump Git version on Windows

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  lastVersion:
    kind: githubrelease
    name: Get the latest Git version
    spec:
      owner: "git-for-windows"
      repository: "git"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      versionfilter:
        kind: regex
        ## Latest stable v{x.y.z}.windows.<patch>
        pattern: 'v(\d*)\.(\d*)\.(\d*)\.windows\.(\d*)$'
    transformers:
      - trimprefix: "v"

targets:
  # Nanoserver
  setGitVersionWindowsNanoserver:
    name: Update the Git Windows version for Windows Nanoserver
    transformers:
      - findsubmatch:
          pattern: '(.*).windows\.(\d*)$'
          captureindex: 1
    kind: dockerfile
    spec:
      file: windows/nanoserver/Dockerfile
      instruction:
        keyword: ARG
        matcher: GIT_VERSION
    scmid: default
  setGitPackagePatchWindowsNanoserver:
    name: Update the Git Package Windows patch for Windows Nanoserver
    transformers:
      - findsubmatch:
          pattern: '(.*).windows\.(\d*)$'
          captureindex: 2
    kind: dockerfile
    spec:
      file: windows/nanoserver/Dockerfile
      instruction:
        keyword: ARG
        matcher: GIT_PATCH_VERSION
    scmid: default
  # Windows Server Core
  setGitVersionWindowsServer:
    name: Update the Git Windows version for Windows Server Core
    transformers:
      - findsubmatch:
          pattern: '(.*).windows\.(\d*)$'
          captureindex: 1
    kind: dockerfile
    spec:
      file: windows/windowsservercore/Dockerfile
      instruction:
        keyword: ARG
        matcher: GIT_VERSION
    scmid: default
  setGitPackagePatchWindowsServer:
    name: Update the Git Package Windows patch for Windows Server Core
    transformers:
      - findsubmatch:
          pattern: '(.*).windows\.(\d*)$'
          captureindex: 2
    kind: dockerfile
    spec:
      file: windows/windowsservercore/Dockerfile
      instruction:
        keyword: ARG
        matcher: GIT_PATCH_VERSION
    scmid: default

actions:
  default:
    kind: github/pullrequest
    title: Bump Git version on Windows to {{ source "lastVersion" }}
    scmid: default
    spec:
      labels:
        - enhancement
        - git
        - windows


================================================
FILE: updatecli/updatecli.d/jdk17.yaml
================================================
---
name: Bump Temurin's JDK17 version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  jdk17LastVersion:
    kind: temurin
    name: Get the latest Adoptium JDK17 version
    spec:
      featureversion: 17
    transformers:
      - trimprefix: "jdk-"

conditions:
  checkTemurinAllReleases:
    name: Check if the "<lastVersion>" is available for all platforms
    kind: temurin
    spec:
      featureversion: 17
      platforms:
        - alpine-linux/x64
        - linux/x64
        - linux/aarch64
        - linux/ppc64le
        - linux/s390x
        - linux/arm
        - windows/x64

targets:
  setJDK17VersionDockerBake:
    name: "Bump JDK17 version for Linux images in the docker-bake.hcl file"
    kind: hcl
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: docker-bake.hcl
      path: variable.JAVA17_VERSION.default
    scmid: default
  setJDK17VersionAlpine:
    name: "Bump JDK17 default ARG version on Alpine Dockerfile"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: alpine/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default
  setJDK17VersionDebian:
    name: "Bump JDK17 default ARG version on Debian Dockerfile"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: debian/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default

actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump JDK17 version to {{ source "jdk17LastVersion" }}
    spec:
      labels:
        - dependencies
        - jdk17


================================================
FILE: updatecli/updatecli.d/jdk21.yaml
================================================
---
name: Bump Temurin's JDK21 version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"
  temurin21-binaries:
    kind: "github"
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "adoptium"
      repository: "temurin21-binaries"
      token: '{{ requiredEnv .github.token }}'
      branch: "main"

sources:
  jdk21LastVersion:
    kind: temurin
    name: Get the latest Adoptium JDK21 version
    spec:
      featureversion: 21
    transformers:
      - trimprefix: "jdk-"

conditions:
  checkTemurinAllReleases:
    name: Check if the "<lastTemurin21Version>" is available for all platforms
    kind: temurin
    spec:
      featureversion: 21
      platforms:
        - alpine-linux/x64
        - alpine-linux/aarch64
        - linux/x64
        - linux/aarch64
        - linux/ppc64le
        - linux/s390x
        - windows/x64

targets:
  setJDK21VersionDockerBake:
    name: "Bump JDK21 version for Linux images in the docker-bake.hcl file"
    kind: hcl
    sourceid: jdk21LastVersion
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: docker-bake.hcl
      path: variable.JAVA21_VERSION.default
    scmid: default

actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump JDK21 version to {{ source "jdk21LastVersion" }}
    spec:
      labels:
        - dependencies
        - jdk21


================================================
FILE: updatecli/updatecli.d/jdk25.yaml
================================================
---
name: Bump JDK25 version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"
  temurin25-binaries:
    kind: "github"
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "adoptium"
      repository: "temurin25-binaries"
      token: '{{ requiredEnv .github.token }}'
      branch: "main"

sources:
  latestJDK25Version:
    kind: temurin
    name: Get the latest Adoptium JDK25 version via the API
    spec:
      featureversion: 25
    transformers:
      - trimprefix: "jdk-"

# Architectures must match those of the targets in docker-bake.hcl
conditions:
  checkTemurinAllReleases:
    name: Check if the "<latestJDK25Version>" is available for all platforms
    kind: temurin
    sourceid: latestJDK25Version
    spec:
      featureversion: 25
      platforms:
        - alpine-linux/x64
        - alpine-linux/aarch64
        - linux/x64
        - linux/aarch64
        - linux/ppc64le
        - linux/s390x
        - windows/x64

targets:
  setJDK25VersionDockerBake:
    name: "Bump JDK25 version in the docker-bake.hcl file"
    kind: hcl
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: docker-bake.hcl
      path: variable.JAVA25_VERSION.default
    scmid: default

actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump JDK25 version to {{ source "latestJDK25Version" }}
    spec:
      labels:
        - dependencies
        - jdk25


================================================
FILE: updatecli/updatecli.d/openssh.yml
================================================
name: Bump OpenSSH version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  lastVersion:
    kind: githubrelease
    name: Get the latest OpenSSH version
    spec:
      owner: PowerShell
      repository: Win32-OpenSSH
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"

targets:
  setOpenSSHVersionNanoserver:
    name: Update the OpenSSH version for Windows Nanoserver
    kind: dockerfile
    spec:
      file: windows/nanoserver/Dockerfile
      instruction:
        keyword: ARG
        matcher: OPENSSH_VERSION
    scmid: default
  setOpenSSHVersionWindowsServerCore:
    name: Update the OpenSSH version for Windows Server Core
    kind: dockerfile
    spec:
      file: windows/windowsservercore/Dockerfile
      instruction:
        keyword: ARG
        matcher: OPENSSH_VERSION
    scmid: default

actions:
  default:
    kind: github/pullrequest
    title: Bump OpenSSH version to {{ source "lastVersion" }}
    scmid: default
    spec:
      labels:
        - enhancement
        - openssh
        - windows


================================================
FILE: updatecli/updatecli.d/pester.yaml
================================================
name: Bump `pester` version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  lastVersion:
    kind: githubrelease
    name: Get latest `pester` version
    spec:
      owner: pester
      repository: pester
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      versionfilter:
        kind: semver
    transformers:
      - trimprefix: "v"

targets:
  setGitLfsVersion:
    name: Update `pester` version in build.ps1
    kind: file
    spec:
      file: build.ps1
      matchpattern: >
        PesterVersion = (.*)
      replacepattern: >
        PesterVersion = '{{ source "lastVersion" }}',
    scmid: default

actions:
  default:
    kind: github/pullrequest
    title: Bump `pester` version to {{ source "lastVersion" }}
    scmid: default
    spec:
      labels:
        - dependencies
        - pester


================================================
FILE: updatecli/values.github-action.yaml
================================================
github:
  user: "GitHub Actions"
  email: "41898282+github-actions[bot]@users.noreply.github.com"
  username: "github-actions"
  token: "UPDATECLI_GITHUB_TOKEN"
  branch: "master"
  owner: "jenkinsci"
  repository: "docker-ssh-agent"


================================================
FILE: updatecli/values.temurin.yaml
================================================
temurin:
  version_pattern: "^jdk-[17|21].(\\d*).(\\d*).(\\d*)(.(\\d*))\\+(\\d*)?$"


================================================
FILE: windows/nanoserver/Dockerfile
================================================
# escape=`

# The MIT License
#
#  Copyright (c) 2019-2020, Alex Earl and other Jenkins 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.

ARG WINDOWS_VERSION_TAG
ARG TOOLS_WINDOWS_VERSION
FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}" AS jdk-core

# $ProgressPreference: https://github.com/PowerShell/PowerShell/issues/2138#issuecomment-251261324
SHELL ["powershell.exe", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

ARG JAVA_VERSION
RUN New-Item -ItemType Directory -Path C:\temp | Out-Null ; `
    $javaMajorVersion = $env:JAVA_VERSION.substring(0,2) ; `
    $msiUrl = 'https://api.adoptium.net/v3/installer/version/jdk-{0}/windows/x64/jdk/hotspot/normal/eclipse?project=jdk' -f $env:JAVA_VERSION ; `
    Invoke-WebRequest $msiUrl -OutFile 'C:\temp\jdk.msi' ; `
    $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList '/i', 'C:\temp\jdk.msi', '/L*V', 'C:\temp\OpenJDK.log', '/quiet', 'ADDLOCAL=FeatureEnvironment,FeatureJarFileRunWith,FeatureJavaHome',  "INSTALLDIR=C:\openjdk-${javaMajorVersion}" -Wait -Passthru ; `
    $proc.WaitForExit() ; `
    Remove-Item -Path C:\temp -Recurse | Out-Null

FROM mcr.microsoft.com/powershell:nanoserver-"${TOOLS_WINDOWS_VERSION}" AS pwsh-source

FROM mcr.microsoft.com/windows/nanoserver:"${WINDOWS_VERSION_TAG}"

ARG JAVA_HOME
ENV PSHOME="C:\Program Files\PowerShell"
ENV PATH="C:\Windows\system32;C:\Windows;${PSHOME};"

# The nanoserver image is nice and small, but we need a couple of things to get SSH working
COPY --from=jdk-core /windows/system32/netapi32.dll /windows/system32/netapi32.dll
COPY --from=jdk-core /windows/system32/whoami.exe /windows/system32/whoami.exe
COPY --from=jdk-core $JAVA_HOME $JAVA_HOME
COPY --from=pwsh-source $PSHOME $PSHOME

SHELL ["pwsh.exe", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
USER ContainerAdministrator

# Backward compatibility with version <= 5.x: create a symlink to the "new" JAVA_HOME
RUN $javaMajorVersion = $env:JAVA_HOME.Substring($env:JAVA_HOME.Length - 2); `
    New-Item -Path "C:\jdk-${javaMajorVersion}" -ItemType SymbolicLink -Value "${env:JAVA_HOME}"

# Install git
ARG GIT_VERSION=2.54.0
ARG GIT_PATCH_VERSION=1
RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
    # The patch "windows.1" always have a different URL than the subsequent patch (ZIP filename is different)
    if($env:GIT_PATCH_VERSION -eq 1) { $url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION); } `
    else {$url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}.{1}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION)} ; `
    Write-Host "Retrieving $url..." ; `
    Invoke-WebRequest $url -OutFile 'mingit.zip' -UseBasicParsing ; `
    Expand-Archive mingit.zip -DestinationPath c:\mingit ; `
    Remove-Item mingit.zip -Force

# Add java & git to PATH
ENV ProgramFiles="C:\Program Files"
ENV WindowsPATH="C:\Windows\system32;C:\Windows"
ENV PATH="${WindowsPATH};${ProgramFiles}\PowerShell;${JAVA_HOME}\bin;C:\mingit\cmd"

# Install git-lfs
ARG GIT_LFS_VERSION=3.7.1
RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
    $url = $('https://github.com/git-lfs/git-lfs/releases/download/v{0}/git-lfs-windows-amd64-v{0}.zip' -f $env:GIT_LFS_VERSION) ; `
    Write-Host "Retrieving $url..." ; `
    Invoke-WebRequest $url -OutFile 'GitLfs.zip' -UseBasicParsing ; `
    Expand-Archive GitLfs.zip -DestinationPath c:\mingit\mingw64\bin ; `
    $gitLfsFolder = 'c:\mingit\mingw64\bin\git-lfs-{0}' -f $env:GIT_LFS_VERSION ; `
    Move-Item -Path "${gitLfsFolder}\git-lfs.exe" -Destination c:\mingit\mingw64\bin\ ; `
    Remove-Item -Path $gitLfsFolder -Recurse -Force ; `
    Remove-Item GitLfs.zip -Force ; `
    & C:\mingit\cmd\git.exe lfs install

ARG user=jenkins
ARG JENKINS_AGENT_WORK="C:/Users/${user}/Work"
ENV JENKINS_AGENT_USER=${user}
ENV JENKINS_AGENT_WORK=${JENKINS_AGENT_WORK}

# Setup SSH server
ARG OPENSSH_VERSION=10.0.0.0p2-Preview
RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
    $url = 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/{0}/OpenSSH-Win64.zip' -f $env:OPENSSH_VERSION ; `
    Write-Host "Retrieving $url..." ; `
    Invoke-WebRequest -Uri $url -OutFile C:/openssh.zip -UseBasicParsing ; `
    Expand-Archive c:/openssh.zip 'C:/Program Files' ; `
    Remove-Item C:/openssh.zip ; `
    $env:PATH = '{0};{1}' -f $env:PATH,'C:\Program Files\OpenSSH-Win64' ; `
    if(!(Test-Path 'C:\ProgramData\ssh')) { New-Item -Type Directory -Path 'C:\ProgramData\ssh' | Out-Null } ; `
    icacls 'C:\ProgramData\ssh' /inheritance:d ; `
    icacls 'C:\ProgramData\ssh' /remove 'CREATOR OWNER' ; `
    & 'C:/Program Files/OpenSSH-Win64/Install-SSHd.ps1' ; `
    Copy-Item 'C:\Program Files\OpenSSH-Win64\sshd_config_default' 'C:\ProgramData\ssh\sshd_config' ; `
    $content = Get-Content -Path "C:\ProgramData\ssh\sshd_config" ; `
    $content | ForEach-Object { $_ -replace '#PermitRootLogin.*','PermitRootLogin no' `
                        -replace '#PasswordAuthentication.*','PasswordAuthentication no' `
                        -replace '#PermitEmptyPasswords.*','PermitEmptyPasswords no' `
                        -replace '#PubkeyAuthentication.*','PubkeyAuthentication yes' `
                        -replace '#SyslogFacility.*','SyslogFacility LOCAL0' `
                        -replace '#LogLevel.*','LogLevel INFO' `
                        -replace 'Match Group administrators','' `
                        -replace '(\s*)AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','' `
                } | `
    Set-Content -Path "C:\ProgramData\ssh\sshd_config" ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'ChallengeResponseAuthentication no' ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'HostKeyAgent \\.\pipe\openssh-ssh-agent' ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value ('Match User {0}' -f $env:JENKINS_AGENT_USER) ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value ('       AuthorizedKeysFile C:/Users/{0}/.ssh/authorized_keys' -f $env:JENKINS_AGENT_USER) ; `
    New-Item -Path HKLM:\SOFTWARE -Name OpenSSH -Force | Out-Null ; `
    New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value 'C:\Program Files\Powershell\pwsh.exe' -PropertyType string -Force | Out-Null ; `
    Remove-Item -Path "C:\ProgramData\ssh\ssh_host*_key*"

COPY CreateProfile.psm1 C:/

# Create user and user directory
RUN Import-Module -Force C:/CreateProfile.psm1 ; `
    New-UserWithProfile -UserName $env:JENKINS_AGENT_USER -Description 'Jenkins Agent User' ; `
    Remove-Item -Force C:/CreateProfile.psm1

VOLUME "${JENKINS_AGENT_WORK}" "C:/Users/${user}/AppData/Local/Temp"
WORKDIR "${JENKINS_AGENT_WORK}"

COPY setup-sshd.ps1 C:/ProgramData/Jenkins/setup-sshd.ps1

EXPOSE 22

LABEL `
    org.opencontainers.image.vendor="Jenkins project" `
    org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" `
    org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" `
    org.opencontainers.image.url="https://www.jenkins.io/" `
    org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" `
    org.opencontainers.image.licenses="MIT"

ENTRYPOINT ["pwsh.exe", "-NoExit", "-Command", "& C:/ProgramData/Jenkins/setup-sshd.ps1"]


================================================
FILE: windows/windowsservercore/Dockerfile
================================================
# escape=`

# The MIT License
#
#  Copyright (c) 2019-2020, Alex Earl
#
#  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.

ARG WINDOWS_VERSION_TAG
ARG TOOLS_WINDOWS_VERSION
FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}" AS jdk-core

# $ProgressPreference: https://github.com/PowerShell/PowerShell/issues/2138#issuecomment-251261324
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

ARG JAVA_VERSION
RUN New-Item -ItemType Directory -Path C:\temp | Out-Null ; `
    $javaMajorVersion = $env:JAVA_VERSION.substring(0,2) ; `
    $msiUrl = 'https://api.adoptium.net/v3/installer/version/jdk-{0}/windows/x64/jdk/hotspot/normal/eclipse?project=jdk' -f $env:JAVA_VERSION ; `
    Invoke-WebRequest $msiUrl -OutFile 'C:\temp\jdk.msi' ; `
    $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList '/i', 'C:\temp\jdk.msi', '/L*V', 'C:\temp\OpenJDK.log', '/quiet', 'ADDLOCAL=FeatureEnvironment,FeatureJarFileRunWith,FeatureJavaHome',  "INSTALLDIR=C:\openjdk-${javaMajorVersion}" -Wait -Passthru ; `
    $proc.WaitForExit() ; `
    Remove-Item -Path C:\temp -Recurse | Out-Null

FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}"

ARG JAVA_HOME
ENV JAVA_HOME=${JAVA_HOME}

COPY --from=jdk-core $JAVA_HOME $JAVA_HOME

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

ARG user=jenkins
ARG JENKINS_AGENT_WORK="C:/Users/${user}/Work"
ENV JENKINS_AGENT_USER=${user}
ENV JENKINS_AGENT_WORK=${JENKINS_AGENT_WORK}

USER ContainerAdministrator

# Install git
ARG GIT_VERSION=2.54.0
ARG GIT_PATCH_VERSION=1
RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
    # The patch "windows.1" always have a different URL than the subsequent patch (ZIP filename is different)
    if($env:GIT_PATCH_VERSION -eq 1) { $url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION); } `
    else {$url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}.{1}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION)} ; `
    Write-Host "Retrieving $url..." ; `
    Invoke-WebRequest $url -OutFile 'mingit.zip' -UseBasicParsing ; `
    Expand-Archive mingit.zip -DestinationPath c:\mingit ; `
    Remove-Item mingit.zip -Force

# Install git-lfs
ARG GIT_LFS_VERSION=3.7.1
RUN $CurrentPath = (Get-Itemproperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path).Path ; `
    $NewPath = $CurrentPath + ';{0}\bin;C:\mingit\cmd' -f $env:JAVA_HOME ; `
    Set-ItemProperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path -Value $NewPath ; `
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
    $url = $('https://github.com/git-lfs/git-lfs/releases/download/v{0}/git-lfs-windows-amd64-v{0}.zip' -f $env:GIT_LFS_VERSION) ; `
    Write-Host "Retrieving $url..." ; `
    Invoke-WebRequest $url -OutFile 'GitLfs.zip' -UseBasicParsing ; `
    Expand-Archive GitLfs.zip -DestinationPath c:\mingit\mingw64\bin ; `
    $gitLfsFolder = 'c:\mingit\mingw64\bin\git-lfs-{0}' -f $env:GIT_LFS_VERSION ; `
    Move-Item -Path "${gitLfsFolder}\git-lfs.exe" -Destination c:\mingit\mingw64\bin\ ; `
    Remove-Item -Path $gitLfsFolder -Recurse -Force ; `
    Remove-Item GitLfs.zip -Force ; `
    & C:\mingit\cmd\git.exe lfs install

# Setup SSH server
ARG OPENSSH_VERSION=10.0.0.0p2-Preview
RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
    $url = 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/{0}/OpenSSH-Win64.zip' -f $env:OPENSSH_VERSION ; `
    Write-Host "Retrieving $url..." ; `
    Invoke-WebRequest -Uri $url -OutFile C:/openssh.zip -UseBasicParsing ; `
    Expand-Archive c:/openssh.zip 'C:/Program Files' ; `
    Remove-Item C:/openssh.zip ; `
    $env:PATH = '{0};{1}' -f $env:PATH,'C:\Program Files\OpenSSH-Win64' ; `
    & 'C:/Program Files/OpenSSH-Win64/install-sshd.ps1' ; `
    if(!(Test-Path 'C:\ProgramData\ssh')) { New-Item -Type Directory -Path 'C:\ProgramData\ssh' | Out-Null } ; `
    Copy-Item 'C:\Program Files\OpenSSH-Win64\sshd_config_default' 'C:\ProgramData\ssh\sshd_config' ; `
    $content = Get-Content -Path "C:\ProgramData\ssh\sshd_config" ; `
    $content | ForEach-Object { $_ -replace '#PermitRootLogin.*','PermitRootLogin no' `
                        -replace '#PasswordAuthentication.*','PasswordAuthentication no' `
                        -replace '#PermitEmptyPasswords.*','PermitEmptyPasswords no' `
                        -replace '#PubkeyAuthentication.*','PubkeyAuthentication yes' `
                        -replace '#SyslogFacility.*','SyslogFacility LOCAL0' `
                        -replace '#LogLevel.*','LogLevel INFO' `
                        -replace 'Match Group administrators','' `
                        -replace '(\s*)AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','' `
                } | `
    Set-Content -Path "C:\ProgramData\ssh\sshd_config" ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'ChallengeResponseAuthentication no' ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'HostKeyAgent \\.\pipe\openssh-ssh-agent' ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value ('Match User {0}' -f $env:JENKINS_AGENT_USER) ; `
    Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value ('       AuthorizedKeysFile C:/Users/{0}/.ssh/authorized_keys' -f $env:JENKINS_AGENT_USER) ; `
    New-Item -Path HKLM:\SOFTWARE -Name OpenSSH -Force | Out-Null ; `
    New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType string -Force | Out-Null ; `
    Remove-Item -Path "C:\ProgramData\ssh\ssh_host*_key*"

COPY CreateProfile.psm1 C:/

# Create user and user directory
RUN Import-Module -Force C:/CreateProfile.psm1 ; `
    New-UserWithProfile -UserName $env:JENKINS_AGENT_USER -Description 'Jenkins Agent User' ; `
    Remove-Item -Force C:/CreateProfile.psm1

VOLUME "${JENKINS_AGENT_WORK}" "C:/Users/${user}/AppData/Local/Temp"
WORKDIR "${JENKINS_AGENT_WORK}"

COPY setup-sshd.ps1 C:/ProgramData/Jenkins/setup-sshd.ps1

EXPOSE 22

LABEL `
    org.opencontainers.image.vendor="Jenkins project" `
    org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" `
    org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" `
    org.opencontainers.image.url="https://www.jenkins.io/" `
    org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" `
    org.opencontainers.image.licenses="MIT"

ENTRYPOINT ["powershell.exe", "-NoExit", "-Command", "& C:/ProgramData/Jenkins/setup-sshd.ps1"]
Download .txt
gitextract_vtv8hsk6/

├── .dockerignore
├── .gitattributes
├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   ├── release-drafter.yml
│   └── workflows/
│       ├── release-drafter.yml
│       └── updatecli.yaml
├── .gitignore
├── .gitmodules
├── CreateProfile.psm1
├── Jenkinsfile
├── LICENSE
├── Makefile
├── README.md
├── alpine/
│   └── Dockerfile
├── build.ps1
├── debian/
│   └── Dockerfile
├── docker-bake.hcl
├── jdk-download-url.sh
├── jdk-download.sh
├── setup-sshd
├── setup-sshd.ps1
├── tests/
│   ├── golden/
│   │   └── expected_tags.txt
│   ├── keys.bash
│   ├── sshAgent.Tests.ps1
│   ├── tags.bats
│   ├── test_helpers.bash
│   ├── test_helpers.psm1
│   ├── tests.bats
│   └── update-golden-file.sh
├── updatecli/
│   ├── updatecli.d/
│   │   ├── alpine.yaml
│   │   ├── bats.yaml
│   │   ├── debian.yaml
│   │   ├── git-lfs.yml
│   │   ├── git-windows.yml
│   │   ├── jdk17.yaml
│   │   ├── jdk21.yaml
│   │   ├── jdk25.yaml
│   │   ├── openssh.yml
│   │   └── pester.yaml
│   ├── values.github-action.yaml
│   └── values.temurin.yaml
└── windows/
    ├── nanoserver/
    │   └── Dockerfile
    └── windowsservercore/
        └── Dockerfile
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (146K chars).
[
  {
    "path": ".dockerignore",
    "chars": 57,
    "preview": "tests\nREADME.md\n.git\n.gitignore\n.gitattributes\n.DS_Store\n"
  },
  {
    "path": ".gitattributes",
    "chars": 51,
    "preview": "# Force checkout as Unix endline style\ntext eol=lf\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "chars": 35,
    "preview": "* @jenkinsci/team-docker-packaging\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 76,
    "preview": "community_bridge: jenkins\ncustom: [\"https://jenkins.io/donate/#why-donate\"]\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 748,
    "preview": "version: 2\nupdates:\n- package-ecosystem: docker\n  directory: \"/alpine\"\n  schedule:\n    interval: weekly\n  open-pull-requ"
  },
  {
    "path": ".github/release-drafter.yml",
    "chars": 296,
    "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": 551,
    "preview": "# Automates creation of Release Drafts using Release Drafter\n# Note: additional setup is required, see https://github.co"
  },
  {
    "path": ".github/workflows/updatecli.yaml",
    "chars": 950,
    "preview": "name: updatecli\non:\n  # Allow to be run manually\n  workflow_dispatch:\n  schedule:\n    - cron: '0 1 * * *' # Once a day a"
  },
  {
    "path": ".gitignore",
    "chars": 64,
    "preview": ".DS_Store\nbats-core/\nbats/\ntarget/\n/.vscode/\nbuild-windows.yaml\n"
  },
  {
    "path": ".gitmodules",
    "chars": 271,
    "preview": "[submodule \"tests/test_helper/bats-support\"]\n\tpath = tests/test_helper/bats-support\n\turl = https://github.com/bats-core/"
  },
  {
    "path": "CreateProfile.psm1",
    "chars": 3426,
    "preview": "# Based on code developed by  Josh Rickard (@MS_dministrator) and Thom Schumacher (@driberif)\r\n# Location: https://gist."
  },
  {
    "path": "Jenkinsfile",
    "chars": 4869,
    "preview": "final String cronExpr = env.BRANCH_IS_PRIMARY ? '@daily' : ''\n\nproperties([\n    buildDiscarder(logRotator(numToKeepStr: "
  },
  {
    "path": "LICENSE",
    "chars": 1090,
    "preview": "MIT License\n\nCopyright (c) 2015-2019 Jenkins project contributors\n\nPermission is hereby granted, free of charge, to any "
  },
  {
    "path": "Makefile",
    "chars": 4195,
    "preview": "ROOT:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))\n\n## For Docker <=20.04\nexport DOCKER_BUILDKIT=1\n## For D"
  },
  {
    "path": "README.md",
    "chars": 11218,
    "preview": "# Docker image for Jenkins agents connected over SSH\n\n[![Join the chat at https://gitter.im/jenkinsci/docker](https://ba"
  },
  {
    "path": "alpine/Dockerfile",
    "chars": 5577,
    "preview": "# MIT License\n#\n# Copyright (c) 2019-2022 Fabio Kruger and other contributors\n#\n# Permission is hereby granted, free of "
  },
  {
    "path": "build.ps1",
    "chars": 8683,
    "preview": "[CmdletBinding()]\nParam(\n    [Parameter(Position=1)]\n    # Default build.ps1 target\n    [String] $Target = 'build',\n    "
  },
  {
    "path": "debian/Dockerfile",
    "chars": 5681,
    "preview": "# The MIT License\n#\n#  Copyright (c) 2015-2024, CloudBees, Inc. and other Jenkins contributors\n#\n#  Permission is hereby"
  },
  {
    "path": "docker-bake.hcl",
    "chars": 7420,
    "preview": "## Variables\nvariable \"jdks_to_build\" {\n  default = [17, 21, 25]\n}\n\nvariable \"default_jdk\" {\n  default = 21\n}\n\nvariable "
  },
  {
    "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": "setup-sshd",
    "chars": 2763,
    "preview": "#!/usr/bin/env bash\n\nset -ex\n\n# The MIT License\n#\n#  Copyright (c) 2015, CloudBees, Inc.\n#\n#  Permission is hereby grant"
  },
  {
    "path": "setup-sshd.ps1",
    "chars": 4132,
    "preview": "# The MIT License\n#\n#  Copyright (c) 2019-2020, Alex Earl\n#\n#  Permission is hereby granted, free of charge, to any pers"
  },
  {
    "path": "tests/golden/expected_tags.txt",
    "chars": 1472,
    "preview": "docker.io/jenkins/ssh-agent:alpine\ndocker.io/jenkins/ssh-agent:alpine-jdk17\ndocker.io/jenkins/ssh-agent:alpine-jdk21\ndoc"
  },
  {
    "path": "tests/keys.bash",
    "chars": 2107,
    "preview": "PUBLIC_SSH_KEY=\"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAvnRN27LdPPQq2OH3GiFFGWX/SH5TCPVePLR21ngMFV8nAthXgYrFkRi/t+Wafe3ByTu2"
  },
  {
    "path": "tests/sshAgent.Tests.ps1",
    "chars": 11616,
    "preview": "Import-Module -DisableNameChecking -Force $PSScriptRoot/test_helpers.psm1\n\n$global:IMAGE_NAME = Get-EnvOrDefault 'IMAGE_"
  },
  {
    "path": "tests/tags.bats",
    "chars": 177,
    "preview": "#!/usr/bin/env bats\n\nload test_helpers\n\nSUT_DESCRIPTION=\"tags\"\n\n@test \"[${SUT_DESCRIPTION}] Default tags unchanged\" {\n  "
  },
  {
    "path": "tests/test_helpers.bash",
    "chars": 3606,
    "preview": "#!/usr/bin/env bash\n\nset -eu\n\n# check dependencies\n(\n    type docker &>/dev/null || ( echo \"docker is not available\"; ex"
  },
  {
    "path": "tests/test_helpers.psm1",
    "chars": 5213,
    "preview": "function Test-CommandExists($command) {\r\n    $oldPreference = $ErrorActionPreference\r\n    $ErrorActionPreference = 'stop"
  },
  {
    "path": "tests/tests.bats",
    "chars": 9395,
    "preview": "#!/usr/bin/env bats\n\nload test_helpers\nload 'test_helper/bats-support/load' # this is required by bats-assert!\nload 'tes"
  },
  {
    "path": "tests/update-golden-file.sh",
    "chars": 2238,
    "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": "updatecli/updatecli.d/alpine.yaml",
    "chars": 1922,
    "preview": "---\nname: Bump Alpine version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email:"
  },
  {
    "path": "updatecli/updatecli.d/bats.yaml",
    "chars": 1195,
    "preview": "---\nname: Bump `bats` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email:"
  },
  {
    "path": "updatecli/updatecli.d/debian.yaml",
    "chars": 1660,
    "preview": "---\nname: Bump Debian Trixie version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n     "
  },
  {
    "path": "updatecli/updatecli.d/git-lfs.yml",
    "chars": 1807,
    "preview": "name: Bump `git-lfs` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: "
  },
  {
    "path": "updatecli/updatecli.d/git-windows.yml",
    "chars": 2560,
    "preview": "---\nname: Bump Git version on Windows\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n    "
  },
  {
    "path": "updatecli/updatecli.d/jdk17.yaml",
    "chars": 1999,
    "preview": "---\nname: Bump Temurin's JDK17 version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n   "
  },
  {
    "path": "updatecli/updatecli.d/jdk21.yaml",
    "chars": 1674,
    "preview": "---\nname: Bump Temurin's JDK21 version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n   "
  },
  {
    "path": "updatecli/updatecli.d/jdk25.yaml",
    "chars": 1730,
    "preview": "---\nname: Bump JDK25 version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: "
  },
  {
    "path": "updatecli/updatecli.d/openssh.yml",
    "chars": 1339,
    "preview": "name: Bump OpenSSH version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: \"{"
  },
  {
    "path": "updatecli/updatecli.d/pester.yaml",
    "chars": 1128,
    "preview": "name: Bump `pester` version\n\nscms:\n  default:\n    kind: github\n    spec:\n      user: \"{{ .github.user }}\"\n      email: \""
  },
  {
    "path": "updatecli/values.github-action.yaml",
    "chars": 234,
    "preview": "github:\n  user: \"GitHub Actions\"\n  email: \"41898282+github-actions[bot]@users.noreply.github.com\"\n  username: \"github-ac"
  },
  {
    "path": "updatecli/values.temurin.yaml",
    "chars": 84,
    "preview": "temurin:\n  version_pattern: \"^jdk-[17|21].(\\\\d*).(\\\\d*).(\\\\d*)(.(\\\\d*))\\\\+(\\\\d*)?$\"\n"
  },
  {
    "path": "windows/nanoserver/Dockerfile",
    "chars": 8650,
    "preview": "# escape=`\n\n# The MIT License\n#\n#  Copyright (c) 2019-2020, Alex Earl and other Jenkins Contributors\n#\n#  Permission is "
  },
  {
    "path": "windows/windowsservercore/Dockerfile",
    "chars": 7964,
    "preview": "# escape=`\n\n# The MIT License\n#\n#  Copyright (c) 2019-2020, Alex Earl\n#\n#  Permission is hereby granted, free of charge,"
  }
]

About this extraction

This page contains the full source code of the jenkinsci/docker-ssh-agent GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (134.2 KB), approximately 39.8k 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!