fanchonhe hai 3 días
achega
2cb1fd7bba
Modificáronse 100 ficheiros con 9891 adicións e 0 borrados
  1. 3 0
      jenkins/.babelrc
  2. 7 0
      jenkins/.editorconfig
  3. 4 0
      jenkins/.git-blame-ignore-revs
  4. 42 0
      jenkins/.gitattributes
  5. 128 0
      jenkins/.github/PULL_REQUEST_TEMPLATE.md
  6. 11 0
      jenkins/.github/comment-ops.yml
  7. 25 0
      jenkins/.github/config.yml
  8. 173 0
      jenkins/.github/images/jenkins-welcome.svg
  9. 14 0
      jenkins/.github/release-drafter.yml
  10. 103 0
      jenkins/.github/renovate.json
  11. 31 0
      jenkins/.github/workflows/announce-lts-rc.yml
  12. 72 0
      jenkins/.github/workflows/changelog.yml
  13. 21 0
      jenkins/.github/workflows/label-conflicting-pr.yml
  14. 23 0
      jenkins/.github/workflows/label-lts-prs.yml
  15. 192 0
      jenkins/.github/workflows/publish-release-artifact.yml
  16. 32 0
      jenkins/.github/workflows/require-changelog-label.yml
  17. 41 0
      jenkins/.github/workflows/run-since-updater.yml
  18. 79 0
      jenkins/.gitignore
  19. 1 0
      jenkins/.idea/.name
  20. 39 0
      jenkins/.idea/encodings.xml
  21. 101 0
      jenkins/.idea/icon.svg
  22. 1 0
      jenkins/.jenkins
  23. 7 0
      jenkins/.mvn/extensions.xml
  24. 1 0
      jenkins/.mvn/jvm.config
  25. 2 0
      jenkins/.mvn/maven.config
  26. 104 0
      jenkins/.perltidyrc
  27. 40 0
      jenkins/.prettierignore
  28. 4 0
      jenkins/.prettierrc.json
  29. 22 0
      jenkins/.stylelintrc.js
  30. 2 0
      jenkins/.yarnrc.yml
  31. 19 0
      jenkins/CITATION.cff
  32. 283 0
      jenkins/CONTRIBUTING.md
  33. 252 0
      jenkins/Jenkinsfile
  34. 22 0
      jenkins/LICENSE.txt
  35. 120 0
      jenkins/README.md
  36. 73 0
      jenkins/ath.sh
  37. 363 0
      jenkins/bom/pom.xml
  38. 266 0
      jenkins/cli/pom.xml
  39. 1 0
      jenkins/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties
  40. 572 0
      jenkins/cli/src/main/java/hudson/cli/CLI.java
  41. 58 0
      jenkins/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
  42. 57 0
      jenkins/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java
  43. 193 0
      jenkins/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
  44. 121 0
      jenkins/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
  45. 48 0
      jenkins/cli/src/main/java/hudson/cli/HexDump.java
  46. 27 0
      jenkins/cli/src/main/java/hudson/cli/NoCheckTrustManager.java
  47. 381 0
      jenkins/cli/src/main/java/hudson/cli/PlainCLIProtocol.java
  48. 160 0
      jenkins/cli/src/main/java/hudson/cli/PrivateKeyProvider.java
  49. 133 0
      jenkins/cli/src/main/java/hudson/cli/SSHCLI.java
  50. 562 0
      jenkins/cli/src/main/java/hudson/util/QuotedStringTokenizer.java
  51. 26 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages.properties
  52. 44 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_bg.properties
  53. 28 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_da.properties
  54. 14 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_de.properties
  55. 24 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_es.properties
  56. 14 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_fr.properties
  57. 67 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_it.properties
  58. 40 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_ja.properties
  59. 36 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_pt_BR.properties
  60. 26 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_ru.properties
  61. 26 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_sv_SE.properties
  62. 35 0
      jenkins/cli/src/main/resources/hudson/cli/client/Messages_zh_TW.properties
  63. 26 0
      jenkins/cli/src/test/java/hudson/cli/CLIConnectionFactoryTest.java
  64. 51 0
      jenkins/cli/src/test/java/hudson/cli/HexDumpTest.java
  65. 166 0
      jenkins/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java
  66. 163 0
      jenkins/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
  67. 133 0
      jenkins/cli/src/test/java/hudson/util/QuotedStringTokenizerTest.java
  68. 0 0
      jenkins/cli/src/test/resources/hudson/cli/blank
  69. 12 0
      jenkins/cli/src/test/resources/hudson/cli/dsa
  70. 15 0
      jenkins/cli/src/test/resources/hudson/cli/dsa-password
  71. 8 0
      jenkins/cli/src/test/resources/hudson/cli/ecdsa
  72. 7 0
      jenkins/cli/src/test/resources/hudson/cli/ed25519
  73. 17 0
      jenkins/cli/src/test/resources/hudson/cli/openssh
  74. 16 0
      jenkins/cli/src/test/resources/hudson/cli/openssh-broken
  75. 35 0
      jenkins/cli/src/test/resources/hudson/cli/openssh-multiple-keys
  76. 17 0
      jenkins/cli/src/test/resources/hudson/cli/openssh-pkcs8
  77. 17 0
      jenkins/cli/src/test/resources/hudson/cli/openssh-rfc4716
  78. 15 0
      jenkins/cli/src/test/resources/hudson/cli/rsa
  79. 18 0
      jenkins/cli/src/test/resources/hudson/cli/rsa-password
  80. 35 0
      jenkins/core/move-l10n.groovy
  81. 807 0
      jenkins/core/pom.xml
  82. 551 0
      jenkins/core/report-l10n.rb
  83. 2 0
      jenkins/core/src/filter/resources/hudson/model/hudson-version.properties
  84. 2 0
      jenkins/core/src/filter/resources/jenkins/model/jenkins-version.properties
  85. 6 0
      jenkins/core/src/filter/resources/jenkins/slaves/remoting-info.properties
  86. 57 0
      jenkins/core/src/main/antlr4/hudson/model/labels/LabelExpressionLexer.g4
  87. 56 0
      jenkins/core/src/main/antlr4/hudson/model/labels/LabelExpressionParser.g4
  88. 70 0
      jenkins/core/src/main/antlr4/hudson/scheduler/CrontabLexer.g4
  89. 83 0
      jenkins/core/src/main/antlr4/hudson/scheduler/CrontabParser.g4
  90. 21 0
      jenkins/core/src/main/java/antlr/ANTLRException.java
  91. 49 0
      jenkins/core/src/main/java/hudson/AbortException.java
  92. 73 0
      jenkins/core/src/main/java/hudson/AboutJenkins.java
  93. 166 0
      jenkins/core/src/main/java/hudson/AbstractMarkupText.java
  94. 165 0
      jenkins/core/src/main/java/hudson/BulkChange.java
  95. 699 0
      jenkins/core/src/main/java/hudson/ClassicPluginStrategy.java
  96. 42 0
      jenkins/core/src/main/java/hudson/CloseProofOutputStream.java
  97. 56 0
      jenkins/core/src/main/java/hudson/CopyOnWrite.java
  98. 95 0
      jenkins/core/src/main/java/hudson/DependencyRunner.java
  99. 251 0
      jenkins/core/src/main/java/hudson/DescriptorExtensionList.java
  100. 473 0
      jenkins/core/src/main/java/hudson/EnvVars.java

+ 3 - 0
jenkins/.babelrc

@@ -0,0 +1,3 @@
+{
+  "presets": ["@babel/preset-env"]
+}

+ 7 - 0
jenkins/.editorconfig

@@ -0,0 +1,7 @@
+root = true
+
+[*.{js, scss, css, hbs, svg}]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 4 - 0
jenkins/.git-blame-ignore-revs

@@ -0,0 +1,4 @@
+# Whitespace: https://github.com/jenkinsci/jenkins/pull/6149
+e27b310065b3c036b5fc9d123f1d1d99d3058c00
+# Reformatted: https://github.com/jenkinsci/jenkins/pull/6863
+e3fdfa527e4fefb4b37f04c92a2dd87b8a374b75

+ 42 - 0
jenkins/.gitattributes

@@ -0,0 +1,42 @@
+# Handle line endings automatically for files detected as text
+# and leave all files detected as binary untouched.
+* text=auto
+
+#
+# The above will handle all files NOT found below
+#
+# These files are text and should be normalized (Convert crlf => lf)
+*.css           text
+*.groovy        text
+*.htm           text
+*.html          text
+*.java          text
+*.js            text
+*.json          text
+*.jelly         text
+*.jellytag      text
+*.properties    text
+*.rb            text
+*.sh            text
+*.txt           text
+*.xml           text
+
+# These files are binary and should be left untouched
+# (binary is a macro for -text -diff)
+*.class         binary
+*.gz            binary
+*.tgz           binary
+*.ear           binary
+*.gif           binary
+*.hpi           binary
+*.ico           binary
+*.jar           binary
+*.jpg           binary
+*.jpeg          binary
+*.png           binary
+*.war           binary
+*.zip           binary
+
+# Yarn
+# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
+/.yarn/plugins/** binary

+ 128 - 0
jenkins/.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,128 @@
+<!-- Comment:
+!!! ⚠️ IMPORTANT ⚠️ !!!
+Do not remove any of the sections below, even if they are not applicable to your change. The sections are used by the
+changelog generator and other tools to extract information about the change. If a section is not applicable,
+leave as is. Carefully read the instructions in each section and provide the necessary information.
+
+Pull requests that do not follow the template might be closed without further review.
+-->
+
+<!-- Comment:
+A great PR typically begins with the line below.
+Replace <issue-number> with the issue number.
+-->
+
+Fixes #<issue-number>
+
+<!-- Comment:
+If the issue is not fully described in the issue tracker, add more information here (justification, pull request links, etc.).
+
+ * We do not require an issue for minor improvements.
+ * Major new features should have an issue created.
+-->
+
+### Testing done
+
+<!-- Comment:
+Provide a clear description of how this change was tested.
+At minimum this should include proof that a computer has executed the changed lines.
+Ideally this should include an automated test or an explanation as to why this change has no tests.
+Note that automated test coverage is less than complete, so a successful PR build does not necessarily imply that a computer has executed the changed lines.
+If automated test coverage does not exist for the lines you are changing, you must describe the scenario(s) in which you manually tested the change.
+For frontend changes, include screenshots of the relevant page(s) before and after the change.
+For refactoring and code cleanup changes, exercise the code before and after the change and verify the behavior remains the same.
+-->
+
+### Screenshots (UI changes only)
+
+<!--
+If your change involves a UI change, please include screenshots showing the before and after states.
+-->
+
+#### Before
+
+#### After
+
+### Proposed changelog entries
+
+- human-readable text
+
+<!-- Comment:
+The changelog entry should be in the imperative mood; e.g., write "do this"/"return that" rather than "does this"/"returns that".
+For examples, see: https://www.jenkins.io/changelog/
+
+Do not include the issue in the changelog entry.
+Include the issue in the description of the pull request so that the changelog generator can find it and include it in the generated changelog.
+
+You may add multiple changelog entries if applicable by adding a new entry to the list, e.g.
+- First changelog entry
+- Second changelog entry
+-->
+
+### Proposed changelog category
+
+/label <update-this-with-category>
+
+<!--
+The changelog entry needs to have a category which is selected based on the label.
+If there's no changelog then the label should be `skip-changelog`.
+
+The available categories are:
+* bug - Minor bug. Will be listed after features
+* developer - Changes which impact plugin developers
+* dependencies - Pull requests that update a dependency
+* internal - Internal only change, not user facing
+* into-lts - Changes that are backported to the LTS baseline
+* localization - Updates localization files
+* major-bug - Major bug. Will be highlighted on the top of the changelog
+* major-rfe - Major enhancement. Will be highlighted on the top
+* rfe - Minor enhancement
+* regression-fix - Fixes a regression in one of the previous Jenkins releases
+* removed - Removes a feature or a public API
+* skip-changelog - Should not be shown in the changelog
+
+Non-changelog categories:
+* web-ui - Changes in the web UI
+
+Non-changelog categories require a changelog category but should be used if applicable,
+comma separate to provide multiple categories in the label command.
+-->
+
+### Proposed upgrade guidelines
+
+N/A
+
+<!-- Comment:
+Leave the proposed upgrade guidelines in the pull request with the "N/A" value if no upgrade guidelines are needed.
+The changelog generator relies on the presence of the upgrade guidelines section as part of its data extraction process.
+-->
+
+### Submitter checklist
+
+- [ ] The issue, if it exists, is well-described.
+- [ ] The changelog entries and upgrade guidelines are appropriate for the audience affected by the change (users or developers, depending on the change) and are in the imperative mood (see [examples](https://github.com/jenkins-infra/jenkins.io/blob/master/content/_data/changelogs/weekly.yml)). Fill in the **Proposed upgrade guidelines** section only if there are breaking changes or changes that may require extra steps from users during upgrade.
+- [ ] There is automated testing or an explanation as to why this change has no tests.
+- [ ] New public classes, fields, and methods are annotated with `@Restricted` or have `@since TODO` Javadocs, as appropriate.
+- [ ] New deprecations are annotated with `@Deprecated(since = "TODO")` or `@Deprecated(forRemoval = true, since = "TODO")`, if applicable.
+- [ ] UI changes do not introduce regressions when enforcing the current default rules of [Content Security Policy Plugin](https://plugins.jenkins.io/csp/). In particular, new or substantially changed JavaScript is not defined inline and does not call `eval` to ease future introduction of Content Security Policy (CSP) directives (see [documentation](https://www.jenkins.io/doc/developer/security/csp/)).
+- [ ] For dependency updates, there are links to external changelogs and, if possible, full differentials.
+- [ ] For new APIs and extension points, there is a link to at least one consumer.
+
+### Desired reviewers
+
+@mention
+
+<!-- Comment:
+If you need an accelerated review process by the community (e.g., for critical bugs), mention @jenkinsci/core-pr-reviewers.
+-->
+
+Before the changes are marked as `ready-for-merge`:
+
+### Maintainer checklist
+
+- [ ] There are at least two (2) approvals for the pull request and no outstanding requests for change.
+- [ ] Conversations in the pull request are over, or it is explicit that a reviewer is not blocking the change.
+- [ ] Changelog entries in the pull request title and/or **Proposed changelog entries** are accurate, human-readable, and in the imperative mood.
+- [ ] Proper changelog labels are set so that the changelog can be generated automatically.
+- [ ] If the change needs additional upgrade steps from users, the `upgrade-guide-needed` label is set and there is a **Proposed upgrade guidelines** section in the pull request title (see [example](https://github.com/jenkinsci/jenkins/pull/4387)).
+- [ ] If it would make sense to backport the change to LTS, be a _Bug_ or _Improvement_, and either the issue or pull request must be labeled as `lts-candidate` to be considered.

+ 11 - 0
jenkins/.github/comment-ops.yml

@@ -0,0 +1,11 @@
+commands:
+  close:
+    enabled: true
+  reopen:
+    enabled: true
+  label:
+    enabled: true
+  removeLabel:
+    enabled: true
+  reviewer:
+    enabled: true

+ 25 - 0
jenkins/.github/config.yml

@@ -0,0 +1,25 @@
+newPRWelcomeComment: >
+  Yay, your first pull request towards Jenkins core was created successfully!
+  Thank you so much!
+  <br>
+  <br>
+  A contributor will provide feedback soon.
+  Meanwhile, you can join the [chats](https://matrix.to/#/#jenkins-ci:matrix.org) and [community forums](https://community.jenkins.io/) to connect with other Jenkins users, developers, and maintainers.
+
+firstPRMergeComment: >
+  Congratulations on getting your very first Jenkins core pull request merged 🎉🥳
+  <br>
+  <br>
+  This is a fantastic achievement, and we're thrilled to have you as part of our community!
+  Thank you for your valuable input, and we look forward to seeing more of your contributions in the future!
+  <br>
+  <br>
+  We would like to invite you to join the [community chats](https://matrix.to/#/#jenkins-ci:matrix.org) and [forums](https://community.jenkins.io/) to meet other Jenkins contributors 😊
+  <br>
+  Don't forget to check out the [participation](https://www.jenkins.io/participate/) page to learn more about how to contribute to Jenkins.
+
+  <a href="https://www.jenkins.io/participate/" target="_blank">
+   <picture>
+     <img width="600" src="https://raw.githubusercontent.com/jenkinsci/jenkins/master/.github/images/jenkins-welcome.svg">
+   </picture>
+  </a>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 173 - 0
jenkins/.github/images/jenkins-welcome.svg


+ 14 - 0
jenkins/.github/release-drafter.yml

@@ -0,0 +1,14 @@
+# Configuration for Release Drafter: https://github.com/toolmantim/release-drafter
+_extends: github:jenkinsci/.github:/.github/release-drafter.yml
+# We use the 2-digit versioning in Jenkins weekly releases.
+version-template: $MAJOR.$MINOR
+name-template: $NEXT_MINOR_VERSION
+tag-template: jenkins-$NEXT_MINOR_VERSION
+
+template: |
+  _This is an automatically generated changelog draft for Jenkins weekly releases.
+  See https://www.jenkins.io/changelog/$NEXT_MINOR_VERSION/ for the official changelog for this release._
+
+  $CHANGES
+
+  All contributors: $CONTRIBUTORS

+ 103 - 0
jenkins/.github/renovate.json

@@ -0,0 +1,103 @@
+{
+  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+  "extends": ["github>jenkinsci/renovate-config", "schedule:daily"],
+  "prHourlyLimit": 0,
+  "prConcurrentLimit": 0,
+  "packageRules": [
+    {
+      "description": "Provided by Jetty and should be aligned with the version provided by the version of Jetty we deliver. See: https://github.com/jenkinsci/jenkins/pull/5211",
+      "matchManagers": ["maven"],
+      "enabled": false,
+      "matchPackageNames": [
+        "jakarta.servlet:jakarta.servlet-api",
+        "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api"
+      ]
+    },
+    {
+      "description": "Needs significant testing. See: https://github.com/jenkinsci/jenkins/pull/5112#issuecomment-744429487 and https://github.com/jenkinsci/jenkins/pull/5116#issuecomment-744526638",
+      "matchManagers": ["maven"],
+      "allowedVersions": "<2.5.0",
+      "matchPackageNames": ["org.codehaus.groovy:groovy-all"]
+    },
+    {
+      "description": "Consumed by Groovy and should be updated in lockstep with Groovy. See: https://github.com/jenkinsci/jenkins/pull/5184",
+      "matchManagers": ["maven"],
+      "enabled": false,
+      "matchPackageNames": ["org.fusesource.jansi:jansi"]
+    },
+    {
+      "description": "Contains incompatible API changes and needs compatibility work. See: https://github.com/jenkinsci/jenkins/pull/4224",
+      "matchManagers": ["maven"],
+      "enabled": false,
+      "matchPackageNames": ["org.jfree:jfreechart"]
+    },
+    {
+      "description": "Starting with 7.x, Guice switches from javax.* to jakarta.* bindings. See https://github.com/google/guice/wiki/Guice700",
+      "matchManagers": ["maven"],
+      "allowedVersions": "<7.0.0",
+      "matchPackageNames": ["com.google.inject:guice-bom"]
+    },
+    {
+      "description": "Apache Commons Codec 1.23.0 breaks some Base64 parsing. See https://github.com/jenkinsci/jenkins/issues/27539",
+      "matchManagers": ["maven"],
+      "allowedVersions": "<1.23.0",
+      "matchPackageNames": ["commons-codec:commons-codec"]
+    },
+    {
+      "matchFileNames": ["core/pom.xml", "test/pom.xml", "war/pom.xml"],
+      "matchPackageNames": ["org.jenkins-ci.main:remoting"],
+      "description": "Avoid updating the remoting.minimum.supported.version property but still update latest one by not placing this property in the parent pom.xml",
+      "enabled": false
+    },
+    {
+      "matchPackageNames": ["net.jcip:jcip-annotations"],
+      "matchDatasources": ["maven"],
+      "enabled": false,
+      "description": "maven-metadata.xml is missing for this really old package which is required by renovate"
+    }
+  ],
+  "customManagers": [
+    {
+      "customType": "regex",
+      "managerFilePatterns": ["/pom.xml/"],
+      "matchStrings": ["<node.version>(?<currentValue>.*?)</node.version>"],
+      "depNameTemplate": "node",
+      "datasourceTemplate": "node-version"
+    },
+    {
+      "customType": "regex",
+      "managerFilePatterns": ["/ath.sh/"],
+      "matchStrings": ["export ATH_VERSION=(?<currentValue>.*?)\n"],
+      "depNameTemplate": "jenkins/ath",
+      "datasourceTemplate": "docker",
+      "versioningTemplate": "loose"
+    },
+    {
+      "customType": "regex",
+      "managerFilePatterns": ["/core/src/site/site.xml/"],
+      "matchStrings": ["lit@(?<currentValue>.*?)/"],
+      "depNameTemplate": "lit",
+      "datasourceTemplate": "npm"
+    },
+    {
+      "customType": "regex",
+      "managerFilePatterns": ["/core/src/site/site.xml/"],
+      "matchStrings": ["webcomponentsjs@(?<currentValue>.*?)/"],
+      "depNameTemplate": "@webcomponents/webcomponentsjs",
+      "datasourceTemplate": "npm"
+    },
+    {
+      "customType": "regex",
+      "managerFilePatterns": ["/core/src/site/site.xml/"],
+      "matchStrings": ["<version>(?<currentValue>.*?)</version>"],
+      "depNameTemplate": "org.apache.maven.skins:maven-fluido-skin",
+      "datasourceTemplate": "maven"
+    }
+  ],
+  "lockFileMaintenance": {
+    "enabled": true,
+    "automerge": false,
+    "schedule": ["on the first day of the month"]
+  },
+  "labels": ["dependencies", "skip-changelog"]
+}

+ 31 - 0
jenkins/.github/workflows/announce-lts-rc.yml

@@ -0,0 +1,31 @@
+name: Announce LTS RCs
+
+on:
+  release:
+    types: [prereleased]
+
+jobs:
+  post:
+    runs-on: ubuntu-latest
+    if: ${{ github.repository_owner == 'jenkinsci' }}
+    steps:
+      - name: Post on Discourse
+        uses: roots/discourse-topic-github-release-action@557d74ea05b6cc0c47f555c1d5d28a89d904005b # v1.1.0
+        with:
+          discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
+          discourse-base-url: https://community.jenkins.io/
+          discourse-author-username: jenkins-release-bot
+          discourse-category: 23
+      - name: Post on mailing list
+        uses: dawidd6/action-send-mail@67ce3558d613188179559c61d3565140cf389968 # v22
+        with:
+          server_address: smtp.gmail.com
+          server_port: 465
+          username: ${{secrets.MAIL_USERNAME}}
+          password: ${{secrets.MAIL_PASSWORD}}
+          secure: true
+          subject: ${{ github.event.release.tag_name }} has been released
+          to: jenkinsci-dev@googlegroups.com
+          from: "Jenkins Release Bot <jenkins-release-bot@community.jenkins.io>"
+          html_body: ${{ github.event.release.body }}
+          convert_markdown: true

+ 72 - 0
jenkins/.github/workflows/changelog.yml

@@ -0,0 +1,72 @@
+# Automates creation of changelog drafts using Release Drafter
+# More Info: https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc
+name: Changelog Drafter
+
+on:
+  workflow_dispatch:
+  push:
+    # branches to consider in the event; optional, defaults to all
+    branches:
+      - master
+
+permissions:
+  contents: read
+
+jobs:
+  update_draft_release:
+    permissions:
+      pull-requests: write # to add label to PR (release-drafter/release-drafter)
+      contents: write # to create a github release (release-drafter/release-drafter)
+
+    if: ${{ github.repository_owner == 'jenkinsci' }}
+    runs-on: ubuntu-latest
+    steps:
+      # Drafts your next Release notes as Pull Requests are merged into "master"
+      - name: Generate GitHub Release Draft
+        id: release-drafter
+        uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+      # Generates a YAML changelog file using https://github.com/jenkinsci/jenkins-core-changelog-generator
+      # used by Oleg N in open graph generator experiment for now
+      - name: Generate YAML changelog draft
+        id: jenkins-core-changelog-generator
+        uses: jenkinsci/core-changelog-generator@feb124ed2262f8586ac4561348436afb965812e1 # v2.2.2
+        env:
+          GITHUB_AUTH: "github-actions:${{ secrets.GITHUB_TOKEN }}"
+      - name: Upload Changelog YAML
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+        with:
+          name: changelog.yaml
+          path: changelog.yaml
+
+  jenkins_io_draft:
+    runs-on: ubuntu-latest
+    if: github.repository_owner == 'jenkinsci'
+    steps:
+      - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
+        id: generate-token
+        with:
+          client-id: ${{ secrets.JENKINS_CHANGELOG_UPDATER_APP_ID }}
+          private-key: ${{ secrets.JENKINS_CHANGELOG_UPDATER_PRIVATE_KEY }}
+          owner: jenkins-infra
+          repositories: jenkins.io
+      - name: Check out
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          fetch-depth: 0
+      - name: Publish jenkins.io changelog draft
+        env:
+          GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
+          GITHUB_AUTH: "nobody:${{ steps.generate-token.outputs.token }}"
+          GIT_AUTHOR_NAME: jenkins-infra-changelog-generator
+          GIT_AUTHOR_EMAIL: <86592549+jenkins-infra-changelog-generator[bot]@users.noreply.github.com>
+          GIT_COMMITTER_NAME: jenkins-infra-changelog-generator
+          GIT_COMMITTER_EMAIL: <86592549+jenkins-infra-changelog-generator[bot]@users.noreply.github.com>
+        run: |
+          wget --quiet https://raw.githubusercontent.com/jenkinsci/core-changelog-generator/master/generate-weekly-changelog.sh
+          # Create a Python virtual environment for pip install
+          # See https://github.com/jenkinsci/core-changelog-generator/issues/37
+          python3 -m venv venv
+          source venv/bin/activate
+          bash generate-weekly-changelog.sh

+ 21 - 0
jenkins/.github/workflows/label-conflicting-pr.yml

@@ -0,0 +1,21 @@
+name: "Label conflicting PRs"
+on:
+  push:
+  pull_request_target:
+    types: [synchronize]
+
+permissions:
+  pull-requests: write
+
+jobs:
+  main:
+    if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.repository_owner == 'jenkinsci' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: Label conflicting PRs
+        uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
+        with:
+          dirtyLabel: "unresolved-merge-conflict"
+          repoToken: "${{ secrets.GITHUB_TOKEN }}"
+          commentOnDirty: "Please take a moment and address the merge conflicts of your pull request. Thanks!"
+          continueOnMissingPermissions: true

+ 23 - 0
jenkins/.github/workflows/label-lts-prs.yml

@@ -0,0 +1,23 @@
+name: Label PRs targeting LTS branches
+
+on: [pull_request_target]
+
+permissions:
+  pull-requests: write
+
+jobs:
+  label:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check if PR targets LTS branch
+        if: startsWith(github.event.pull_request.base.ref, 'stable-')
+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            github.rest.issues.addLabels({
+              owner: context.payload.repository.owner.login,
+              repo: context.payload.repository.name,
+              issue_number: context.payload.pull_request.number,
+              labels: ['into-lts']
+            });

+ 192 - 0
jenkins/.github/workflows/publish-release-artifact.yml

@@ -0,0 +1,192 @@
+name: Publish artifact
+
+on:
+  release:
+    types: [published]
+
+permissions:
+  contents: read # to fetch code (actions/checkout)
+
+jobs:
+  determine-version:
+    if: ${{ github.repository_owner == 'jenkinsci' }}
+    runs-on: ubuntu-latest
+    outputs:
+      project-version: ${{ steps.set-version.outputs.project-version }}
+      is-lts: ${{ steps.set-version.outputs.is-lts }}
+      is-rc: ${{ steps.set-version.outputs.is-rc }}
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+      - name: Set up JDK 21
+        uses: actions/setup-java@1d018f9b8b9b505bb578a83b230fabcce01af93b #v 5.0.0
+        with:
+          distribution: "temurin"
+          java-version: 25
+          cache: "maven"
+      - name: Set version
+        id: set-version
+        run: |
+          version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+
+          dot_count=$(echo $version | sed 's/[^.]//g' | awk '{ print length }')
+          if [ "${dot_count}" = '2' ]
+          then
+            is_lts=true
+          else
+            is_lts=false
+          fi
+
+          is_rc=false
+          if [[ ${version} == *"-SNAPSHOT" ]]; then
+            is_rc=true
+          fi
+
+          echo "Version is $version, is_lts: $is_lts, is_rc: $is_rc"
+
+          echo "is-lts=${is_lts}" >> $GITHUB_OUTPUT
+          echo "project-version=$version" >> $GITHUB_OUTPUT
+          echo "is-rc=${is_rc}" >> $GITHUB_OUTPUT
+  war:
+    permissions:
+      contents: write # to upload release asset (softprops/action-gh-release)
+
+    runs-on: ubuntu-latest
+    needs: determine-version
+    if: ${{ needs.determine-version.outputs.is-rc == 'false' }}
+    steps:
+      - name: Fetch war
+        id: fetch-war
+        run: |
+          FILE_NAME=jenkins.war
+          PROJECT_VERSION=${{ needs.determine-version.outputs.project-version }}
+          IS_LTS=${{ needs.determine-version.outputs.is-lts }}
+          echo $PROJECT_VERSION
+          echo "file-name=$FILE_NAME" >> $GITHUB_OUTPUT
+          echo "signature-file-name=${FILE_NAME}.asc" >> $GITHUB_OUTPUT
+
+          REPO=war
+          if [ ${IS_LTS} = 'true' ]
+          then
+            REPO=war-stable
+          fi
+
+          echo $REPO
+
+          wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME}
+          wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME}.asc
+      - name: Upload Release Asset
+        id: upload-war
+        uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          files: |
+            ${{ steps.fetch-war.outputs.file-name }}
+            ${{ steps.fetch-war.outputs.signature-file-name }}
+  debian:
+    permissions:
+      contents: write # to upload release asset (softprops/action-gh-release)
+
+    runs-on: ubuntu-latest
+    needs: determine-version
+    if: ${{ needs.determine-version.outputs.is-rc == 'false' }}
+    steps:
+      - name: Fetch Deb
+        id: fetch-deb
+        if: always()
+        run: |
+          PROJECT_VERSION=${{ needs.determine-version.outputs.project-version }}
+          echo $PROJECT_VERSION
+          FILE_NAME=jenkins_${PROJECT_VERSION}_all.deb
+          IS_LTS=${{ needs.determine-version.outputs.is-lts }}
+          echo "file-name=$FILE_NAME" >> $GITHUB_OUTPUT
+
+          REPO=debian
+          if [ ${IS_LTS} = 'true' ]
+          then
+            REPO=debian-stable
+          fi
+
+          echo $REPO
+
+          wget -q https://get.jenkins.io/${REPO}/${FILE_NAME}
+      - name: Upload Release Asset
+        id: upload-deb
+        if: always()
+        uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          files: ${{ steps.fetch-deb.outputs.file-name }}
+  rpm:
+    permissions:
+      contents: write # to upload release asset (softprops/action-gh-release)
+
+    runs-on: ubuntu-latest
+    needs: determine-version
+    if: ${{ needs.determine-version.outputs.is-rc == 'false' }}
+    steps:
+      - name: Fetch RPM
+        id: fetch-rpm
+        if: always()
+        run: |
+          PROJECT_VERSION=${{ needs.determine-version.outputs.project-version }}
+          echo $PROJECT_VERSION
+          FILE_NAME=jenkins-${PROJECT_VERSION}-1.noarch.rpm
+          IS_LTS=${{ needs.determine-version.outputs.is-lts }}
+
+          echo "file-name=$FILE_NAME" >> $GITHUB_OUTPUT
+
+          REPO=rpm
+          if [ ${IS_LTS} = 'true' ]
+          then
+            REPO=rpm-stable
+          fi
+
+          echo $REPO
+
+          wget -q https://get.jenkins.io/${REPO}/${FILE_NAME}
+      - name: Upload Release Asset
+        id: upload-rpm
+        if: always()
+        uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          files: ${{ steps.fetch-rpm.outputs.file-name }}
+  windows:
+    permissions:
+      contents: write # to upload release asset (softprops/action-gh-release)
+
+    runs-on: ubuntu-latest
+    needs: determine-version
+    if: ${{ needs.determine-version.outputs.is-rc == 'false' }}
+    steps:
+      - name: Fetch MSI
+        id: fetch-msi
+        if: always()
+        run: |
+          PROJECT_VERSION=${{ needs.determine-version.outputs.project-version }}
+          echo $PROJECT_VERSION
+          FILE_NAME=jenkins.msi
+          IS_LTS=${{ needs.determine-version.outputs.is-lts }}
+
+          echo "file-name=$FILE_NAME" >> $GITHUB_OUTPUT
+
+          REPO=windows
+          if [ ${IS_LTS} = 'true' ]
+          then
+            REPO=windows-stable
+          fi
+
+          echo $REPO
+
+          wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME}
+      - name: Upload Release Asset
+        id: upload-msi
+        if: always()
+        uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          files: ${{ steps.fetch-msi.outputs.file-name }}

+ 32 - 0
jenkins/.github/workflows/require-changelog-label.yml

@@ -0,0 +1,32 @@
+name: Require Changelog Label
+on:
+  pull_request_target:
+    types: [opened, labeled, unlabeled, synchronize]
+    branches:
+      - "master"
+jobs:
+  label:
+    if: ${{ github.repository_owner == 'jenkinsci' }}
+    runs-on: ubuntu-latest
+    permissions:
+      issues: write
+      pull-requests: write
+    steps:
+      - uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5.6.0
+        with:
+          mode: minimum
+          count: 1
+          add_comment: true
+          message: "Missing required label for changelog. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}.\n\nYou can add the required label by adding a comment with the following text: `/label <category>`"
+          labels: |
+            bug
+            developer
+            dependencies
+            internal
+            localization
+            major-bug
+            major-rfe
+            rfe
+            regression-fix
+            removed
+            skip-changelog

+ 41 - 0
jenkins/.github/workflows/run-since-updater.yml

@@ -0,0 +1,41 @@
+name: Run update-since-todo.py
+
+on:
+  schedule:
+    - cron: "0 16 * * THU"
+  workflow_dispatch:
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  since_updater:
+    runs-on: ubuntu-latest
+    if: ${{ github.repository_owner == 'jenkinsci' }}
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          fetch-depth: 0
+      - name: Run update-since-todo.py
+        run: |
+          body=$(./update-since-todo.py)
+
+          {
+            echo 'PROGRESS<<EOF'
+            echo "${body}"
+            echo 'EOF'
+          } >> $GITHUB_OUTPUT
+        id: run_script
+        shell: bash
+      - name: Create Pull Request
+        uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          commit-message: Fill in since annotations
+          title: Fill in since annotations
+          body: ${{ steps.run_script.outputs.PROGRESS }}
+          base: master
+          labels: skip-changelog
+          branch: actions/update-since-todo
+          delete-branch: true

+ 79 - 0
jenkins/.gitignore

@@ -0,0 +1,79 @@
+target
+work
+
+# IntelliJ project files 
+*.iml
+*.iws
+*.ipr
+/.idea/*
+!/.idea/icon.svg
+!/.idea/.name
+!/.idea/encodings.xml
+out
+
+# Eclipse and VSCode project files
+.settings
+.classpath
+.project
+.factorypath
+build
+
+# VSCode workspace file
+*.code-workspace
+
+# vim
+*~
+*.swp
+
+# ctags
+tags
+
+# OS X
+.DS_Store
+
+# mvn versions:set
+pom.xml.versionsBackup
+
+war/images/16x16
+war/images/24x24
+war/images/32x32
+war/images/48x48
+jenkins.war
+RPMS
+SRPMS
+debian/binary
+debian/debian/jenkins
+debian/debian/jenkins.substvars
+debian/debian/files
+debian/build-stamp
+debian/configure-stamp
+*.debhelper
+*.debhelper.log
+jenkins_*.build
+jenkins_*.changes
+*.deb
+*.pkg
+*.zip
+push-build.sh
+node_modules/
+yarn-error.log
+.java-version
+.checkstyle
+
+/rebel.xml
+junit.xml
+
+# Yarn
+# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/sdks
+!.yarn/versions
+
+# Node
+node/
+node_modules/
+
+# Generated JavaScript Bundles
+war/src/main/webapp/jsbundles/

+ 1 - 0
jenkins/.idea/.name

@@ -0,0 +1 @@
+jenkins

+ 39 - 0
jenkins/.idea/encodings.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
+    <file url="file://$PROJECT_DIR$/bom/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/bom/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/bom/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/cli/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/cli/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/cli/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/cli/src/test/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/cli/src/test/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/core/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/core/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/core/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/core/src/test/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/core/src/test/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/coverage/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/coverage/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/coverage/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/test/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/test/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/test/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/test/src/test/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/test/src/test/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/war/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/war/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/war/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/war/src/test/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/websocket/jetty12-ee9/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/websocket/jetty12-ee9/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/websocket/jetty12-ee9/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/websocket/spi/src/filter/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/websocket/spi/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/websocket/spi/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 101 - 0
jenkins/.idea/icon.svg


+ 1 - 0
jenkins/.jenkins

@@ -0,0 +1 @@
+This file is used as a marker for the test harness to discover the root directory of Jenkins.

+ 7 - 0
jenkins/.mvn/extensions.xml

@@ -0,0 +1,7 @@
+<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 https://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
+  <extension>
+    <groupId>io.jenkins.tools.incrementals</groupId>
+    <artifactId>git-changelist-maven-extension</artifactId>
+    <version>1.13</version>
+  </extension>
+</extensions>

+ 1 - 0
jenkins/.mvn/jvm.config

@@ -0,0 +1 @@
+-Xmx1400m

+ 2 - 0
jenkins/.mvn/maven.config

@@ -0,0 +1,2 @@
+-Pconsume-incrementals
+-Pmight-produce-incrementals

+ 104 - 0
jenkins/.perltidyrc

@@ -0,0 +1,104 @@
+#################################################
+# Categories outlined in this config
+# are based on the categories in the
+# perltidy man page:
+# http://perltidy.sourceforge.net/perltidy.html
+#################################################
+
+###################################
+# I/O Control
+###################################
+
+# Send all errors to standard output rather than a file.
+--standard-error-output
+
+# All non-critical warnings will be reported as errors.
+--warning-output
+
+###################################
+# Basic Options
+###################################
+
+# Maximum number of characters per line.
+--maximum-line-length=80
+
+###################################
+# Code Indentation Control
+###################################
+
+# The number of spaces to indent a line when a new block starts.
+--indent-columns=4
+
+# If a line continues, it should be indented 2 spaces.
+--continuation-indentation=4
+
+# If a comment is longer than the maximum line length, break it up for
+# readability.
+--outdent-long-comments
+
+# If a quoted string is longer than the maximum line length, do not break it up
+# for readability.
+--no-outdent-long-quotes
+
+###################################
+# Whitespace Control
+###################################
+
+# Stack opening braces in order to avoid having a brace sitting by itself on
+# a line.
+--stack-opening-tokens
+
+# Stack closing tokens in order to avoid having a brace sitting by itself on
+# a line.
+--stack-closing-tokens
+
+# Spaces between parentheses e.g. if ((my $len_tab = length($tabstr)) > 0) {
+--paren-tightness=2
+
+# Spaces between brackets e.g. $width = $col[$j + $k] - $col[$j];
+--square-bracket-tightness=2
+
+# Spaces between braces in expression e.g.  $obj->{$parsed_sql->{'table'}[0]};
+--brace-tightness=2
+
+# Spaces between braches with blocks of code
+# e.g. %bf = map { $_ => -M $_ } grep { /\.deb$/ } dirents '.';
+--block-brace-tightness=0
+
+# Do not add spaces between semicolons within for loops.
+--nospace-for-semicolon
+
+###################################
+# Comment Controls
+###################################
+
+# Indent comments to be at the same level as the code.
+--indent-block-comments
+
+###################################
+# Line Break Control
+###################################
+
+# The else is on the same line as the brace.
+--cuddled-else
+
+# Create a break after -> and period if a break is required.
+--want-break-after='-> .'
+
+###################################
+# Blank Line Control
+###################################
+
+# Do not force blank lines before full line comments.
+--noblanks-before-comments
+
+# Do not force blank lines before blocks starting with for, foreach, while,
+# until, and if,unless.
+--noblanks-before-blocks
+
+###################################
+# Vertical Alignment
+###################################
+
+# Turn off vertical alignment.
+-novalign

+ 40 - 0
jenkins/.prettierignore

@@ -0,0 +1,40 @@
+# Only scan Jenkins source areas, not arbitrary top-level work directories
+/*/
+!/.github/
+!/.idea/
+!/.mvn/
+!/bom/
+!/cli/
+!/core/
+!/coverage/
+!/docs/
+!/src/
+!/test/
+!/war/
+!/websocket/
+
+# generated / build files
+.pnp.cjs
+.pnp.loader.mjs
+target/
+work/
+node/
+
+.git
+
+# libraries / external deps / generated files
+src/main/js/plugin-setup-wizard/bootstrap-detached.js
+war/src/main/webapp/jsbundles/
+src/main/scss/_bootstrap.scss
+
+# test files that we don't need formatted
+test/src/test/resources
+test/jmh-report.json
+
+# doesn't work, see https://github.com/prettier/prettier/issues/5340
+*.hbs
+
+.yarn
+
+# Incorrectly flagging forwarding slashes in regex
+.github/renovate.json

+ 4 - 0
jenkins/.prettierrc.json

@@ -0,0 +1,4 @@
+{
+  "endOfLine": "auto",
+  "htmlWhitespaceSensitivity": "ignore"
+}

+ 22 - 0
jenkins/.stylelintrc.js

@@ -0,0 +1,22 @@
+module.exports = {
+  extends: "stylelint-config-standard-scss",
+  ignoreFiles: ["src/main/scss/_bootstrap.scss"],
+  rules: {
+    "no-descending-specificity": null,
+    "selector-class-pattern": "[a-z]",
+    "selector-id-pattern": "[a-z]",
+    "custom-property-pattern": "[a-z]",
+    "value-keyword-case": [
+      "lower",
+      {
+        camelCaseSvgKeywords: true,
+      },
+    ],
+    "property-no-vendor-prefix": null,
+    "alpha-value-notation": "number",
+    "number-max-precision": 5,
+    "no-duplicate-selectors": null,
+    "hue-degree-notation": "number",
+    "scss/operator-no-newline-after": null,
+  },
+};

+ 2 - 0
jenkins/.yarnrc.yml

@@ -0,0 +1,2 @@
+enableGlobalCache: false
+nodeLinker: node-modules

+ 19 - 0
jenkins/CITATION.cff

@@ -0,0 +1,19 @@
+cff-version: 1.2.0
+title: "Jenkins"
+type: software
+repository-code: "https://github.com/jenkinsci/jenkins"
+url: "https://jenkins.io/"
+authors:
+  - name: "Jenkins contributors"
+    affiliation: "Jenkins project"
+abstract: >-
+  In a nutshell, Jenkins is the leading open-source automation server.
+  Built with Java, it provides over 2,000 plugins to support automating virtually anything,
+  so that humans can spend their time doing things machines cannot.
+keywords:
+  - jenkins
+  - continuous-integration
+  - continuous-delivery
+  - pipelines
+  - automation
+license: MIT

+ 283 - 0
jenkins/CONTRIBUTING.md

@@ -0,0 +1,283 @@
+# Contributing to Jenkins
+
+This page provides information about contributing code to the Jenkins core codebase.
+
+:exclamation: There's a lot more to the Jenkins project than just code. For more information on the ways that you can contribute to the Jenkins project, see [Participate](https://www.jenkins.io/participate/).
+
+## Getting started
+
+1. Fork the repository on GitHub
+2. Clone the forked repository to your machine
+3. Install the necessary development tools. In order to develop Jenkins, you need the following:
+   - Java Development Kit (JDK) 21 or 25.
+     In the Jenkins project we usually use [Eclipse Temurin](https://adoptium.net/) or [OpenJDK](https://openjdk.org/), but you can use other JDKs as well.
+   - Apache Maven 3.9.6 or above. You can [download Maven here](https://maven.apache.org/download.cgi).
+     In the Jenkins project we usually use the most recent Maven release.
+   - Any IDE which supports importing Maven projects.
+4. Set up your development environment as described in [Preparing for Plugin Development](https://www.jenkins.io/doc/developer/tutorial/prepare/)
+
+If you want to contribute to Jenkins, or just learn about the project,
+you can start by fixing some easier issues.
+In the Jenkins issue tracker we mark such issues as `good first issue`.
+You can find them by using this query (check the link) for [good first issues](https://github.com/jenkinsci/jenkins/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22).
+
+## Building and Debugging
+
+The Jenkins core build flow is built around Maven.
+You can read a description of the [building and debugging process here](https://www.jenkins.io/doc/developer/building/).
+
+### Building the WAR file
+
+If you want simply to build the `jenkins.war` file as fast as possible without tests, run:
+
+```sh
+mvn -am -pl war,bom -Pquick-build clean install
+```
+
+The WAR file will be created in `war/target/jenkins.war`.
+
+> [!NOTE]
+> `quick-build` sets `maven.test.skip`, so test classes are not compiled and are not present
+> afterwards. To build something you can then run tests against, drop the profile or pass
+> `-Dmaven.test.skip=false`.
+
+`clean` deliberately leaves `node/`, `node_modules/` and `.yarn/` in place, so the node
+toolchain and the downloaded packages survive between builds. Pass `-Dclean.node` to delete
+those as well.
+
+If you are only changing backend code and `war/src/main/webapp/jsbundles` is already populated
+from an earlier build, `-Dskip.frontend=true` skips the node installation, `yarn install` and
+`yarn build`.
+
+After that, you can start Jenkins using Java CLI ([guide](https://www.jenkins.io/doc/book/installing/war-file/#run-the-war-file)).
+If you want to debug the WAR file without using Maven plugins,
+You can run the executable with [Remote Debug Flags](https://stackoverflow.com/questions/975271/remote-debugging-a-java-application)
+and then attach IDE Debugger to it.
+
+### Launching a development instance
+
+To launch a development instance, after [building the WAR file](#building-the-war-file), run:
+
+```sh
+mvn -pl war hpi:run
+```
+
+This forks a JVM that serves `war/target/jenkins` under Winstone, the same way `jenkins.war` is
+served in production. Jenkins is available on <http://localhost:8080/jenkins/> and uses
+`war/work` as its `JENKINS_HOME`; pass `-Dport=`, `-Dhost=` or `-DjenkinsHome=` to change that.
+
+Edits to `war/src/main/webapp`, and to Jelly views and their sibling `.properties` files under
+`core/src/main/resources`, are picked up on the next page load. Edits to a `Messages.properties`
+take effect when you restart the development instance — no repackaging needed, but the bundle is
+cached for the life of the JVM. Changes to Java code still need a rebuild.
+
+Because Jenkins now runs in its own JVM, starting Maven under a debugger no longer attaches to
+Jenkins. Pass `-Dmaven.hpi.debug=true` to have the forked JVM suspend and wait for a debugger, or
+give the option a JVM debug string of your own; see
+[maven-hpi-plugin#862](https://github.com/jenkinsci/maven-hpi-plugin/pull/862).
+
+### Running the Yarn frontend build
+
+> [!TIP]
+> If you already have Node.js installed, you do not need to change your path. Start using Yarn by enabling [Corepack](https://yarnpkg.com/corepack) with `corepack enable`, if it isn't already; this will add the `yarn` binary to your path.
+
+To run the Yarn frontend build, after [building the WAR file](#building-the-war-file), add the downloaded versions of Node and Yarn to your path:
+
+```sh
+export PATH=$PWD/node:$PWD/node/node_modules/corepack/shims:$PATH
+```
+
+Then you can run Yarn with e.g.
+
+```sh
+yarn
+```
+
+### Building frontend assets
+
+To work on the `war` module frontend assets, two processes are needed at the same time:
+
+On one terminal, start a development server:
+
+```sh
+mvn -pl war hpi:run
+```
+
+Open another terminal and start a [webpack](https://webpack.js.org/) dev server, after [optionally adding Node and Yarn to your path](#running-the-yarn-frontend-build):
+
+```sh
+yarn start
+```
+
+`yarn start` writes to `war/src/main/webapp/jsbundles`, which the development server serves
+directly, so a browser reload is enough to pick up the rebuilt bundles.
+
+### Linting
+
+For linting we use a number of tools:
+
+- [checkstyle](https://checkstyle.sourceforge.io/)
+- [eslint](https://eslint.org/)
+- [prettier](https://prettier.io/)
+- [spotless](https://github.com/diffplug/spotless)
+- [stylelint](https://stylelint.io/)
+
+These are all configured to run as part of the Maven build, although they will be skipped if you are building with the `quick-build` profile.
+
+To automatically fix backend issues, run:
+
+```sh
+mvn spotless:apply
+```
+
+To view frontend issues, after [optionally adding Node and Yarn to your path](#running-the-yarn-frontend-build), run:
+
+```sh
+yarn lint
+```
+
+To fix frontend issues, after [optionally adding Node and Yarn to your path](#running-the-yarn-frontend-build), run:
+
+```sh
+yarn lint:fix
+```
+
+## Testing changes
+
+Jenkins core includes unit and functional tests as a part of the repository.
+
+Functional tests (`test` module) take a while to run, even on server-grade machines.
+Most of the tests will be launched by the continuous integration instance,
+so there is no strict need to run full test suites before proposing a pull request.
+
+There are 3 profiles for tests:
+
+- `light-test` - runs only unit tests, no functional tests
+- `smoke-test` - runs unit tests + a number of functional tests
+- `all-tests` - runs all tests, with re-run (default)
+
+In addition to the included tests, you can also find extra integration and UI
+tests in the [Acceptance Test Harness (ATH)](https://github.com/jenkinsci/acceptance-test-harness) repository.
+If you propose complex UI changes, you should create new ATH tests for them.
+
+## Proposing Changes
+
+The Jenkins project source code repositories are hosted at GitHub.
+All proposed changes are submitted, and code reviewed, using a [GitHub pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) process.
+
+To submit a pull request:
+
+1. Commit your changes and push them to your fork on GitHub.
+   It is a good practice is to create branches instead of pushing to master.
+2. In the GitHub Web UI, click the _New Pull Request_ button.
+3. Select `jenkinsci` as _base fork_ and `master` as `base`, then click _Create Pull Request_.
+   - We integrate all changes into the master branch towards the Weekly releases.
+   - After that, the changes may be backported to the current LTS baseline by the LTS Team.
+     Read more about the [backporting process](https://www.jenkins.io/download/lts/).
+4. Fill in the Pull Request description according to the [proposed template](.github/PULL_REQUEST_TEMPLATE.md).
+5. Click _Create Pull Request_.
+6. Wait for CI results/reviews, process the feedback.
+   - If you do not get feedback after 3 days, feel free to ping `@jenkinsci/core-pr-reviewers` in the comments.
+   - Usually we merge pull requests after 2 approvals from reviewers, no requested changes, and having waited some more time to give others an opportunity to provide their feedback.
+     See [this page](/docs/MAINTAINERS.adoc) for more information about our review process.
+
+Once your Pull Request is ready to be merged,
+the repository maintainers will integrate it, prepare changelogs, and
+ensure it gets released in one of upcoming Weekly releases.
+There is no additional action required from pull request authors at this point.
+
+### Pull request management
+
+The Jenkins project uses a well-defined set of labels to mark the status and content of pull requests.
+The complete list of labels can be found at https://github.com/jenkinsci/jenkins/labels.
+These labels are defined as follows:
+
+- `needs-docs` marks a pull request as lacking documentation, either for developers (e.g., Javadoc) or users (e.g., changes to the [Jenkins handbook](https://www.jenkins.io/doc/book/)).
+  For such pull requests to be approved and merged, the corresponding changes to the documentation should be proposed.
+  If those changes belong to a separate repository (e.g., `jenkins-infra/jenkins.io`), a secondary pull request should be created in draft state in the other repository and reviewed in tandem with the primary pull request that proposes the code change.
+- `needs-fix` marks a pull request which has pending requests for change that have not yet been addressed.
+  Such pull requests will not be merged until the code has been fixed and the tests pass.
+- `needs-justification` marks a pull request where the reasoning is unclear, incomplete or not entirely cogent.
+  To properly evaluate the solution provided in a pull request, maintainers must be able to understand the high-level problem that the pull request attempts to solve.
+  While the context might be obvious to the author, it is not always apparent to reviewers and maintainers.
+  The use of design documents, high-level tracking epics, [minimal reproducible examples (MREs)](https://en.wikipedia.org/wiki/Minimal_reproducible_example), etc. is strongly encouraged.
+- `needs-more-review` marks a pull request as lacking a sufficient number of reviews from subject-matter expert(s) (SME), either because the changes are complex and not sufficiently explained or because there is a lack of consensus regarding the proposed solution.
+- `on-hold` marks a pull request that depends on another event and cannot be merged until the completion of that event.
+  When the dependent task has been completed, the pull request will be ready for merge.
+- `proposed-for-close` marks a pull request where there is either no consensus on the next steps or where the next steps have not been taken and an extended period of time has elapsed.
+  Such pull requests are typically closed approximately one week after the label has been applied.
+  They can always be reopened once consensus has been reached on the next steps or when action is taken regarding these next steps.
+- `ready-for-merge` marks a pull request that has met the acceptance criteria, as defined elsewhere in this document.
+  If there is no negative feedback, such pull requests are typically merged within approximately 24 hours.
+- `stalled` marks a pull request that is off to a promising start but requires additional effort to reach completion - effort that appears to have been abandoned.
+  If the original author lacks the time and interest to continue the original effort, we suggest that someone else pick up where the original author left off to drive the effort to completion.
+- `work-in-progress` marks a pull request that remains under active development.
+  Such pull requests are not ready for final review.
+
+To ensure that pull requests are processed efficiently, the `ready-for-merge`, `stalled`, and `proposed-for-close` labels are subject to time constraints.
+
+A pull request labeled with `ready-for-merge` is merged after approximately 24 hours if there is no negative feedback.
+
+If a pull request has remained incomplete with no activity for over a month, we will make this explicit by labeling the PR as `stalled`.
+
+If a pull request labelled as `stalled` remains inactive for yet another month, we will label it as `proposed-for-close` in order to maintain an orderly PR queue.
+Approximately one week after this label is applied to a pull request, it will be closed.
+
+While contributors are strongly encouraged to drive PRs to completion on their own, we recognize that in some situations the help of a maintainer can be valuable.
+Yet also, we recognize that some contributors prefer not to receive unsolicited changes.
+When opening a pull request, enabling the _Allow edits by maintainers_ option indicates that you accept that maintainers may push new commits into your pull request branch.
+As some maintainers are willing to fix typographical errors and merge conflicts while reviewing pull requests, accepting edits from maintainers can speed up the integration of your pull request.
+
+## IntelliJ suggestion
+
+In case you are using IntelliJ, please adjust the default setting in respect to whitespace fixes on save.
+The setting can be found in Settings -> Editor -> General -> On Save -> Remove trailing spaces on: `Modified lines`
+This will help minimize the diff, which makes reviewing PRs easier.
+
+We also do not recommend `*` imports in the production code.
+Please disable them in Settings > Editor > Codestyle > Java by setting _Class count to use import with '\*'_ and Names count to use import with '\*'\_ to a high value, e.g. 100.
+
+The addition of `@{jenkins.addOpens}` to `argLine` exposes a bug in IntelliJ IDEA.
+A patch has been proposed in [JetBrains/intellij-community#1976](https://github.com/JetBrains/intellij-community/pull/1976).
+Pending the merge and release of this patch, IntelliJ IDEA users should work around the problem as follows:
+
+1. Go to **Settings** > **Build, Execution, Deployment** > **Build Tools** > **Maven** > **Running Tests**.
+2. Under "Pass to JUnit process [the] following `maven-surefire-plugin` and `maven-failsafe-plugin` settings", uncheck `argLine`.
+
+Failure to work around the problem as described above will result in a `could not open '{jenkins.addOpens}'` failure when running tests in IntelliJ IDEA.
+
+### Code formatting for frontend files
+
+Install the [Prettier plugin](https://www.jetbrains.com/help/idea/prettier.html).
+Follow the instructions on the above JetBrains page to configure it how you wish. 'On code reformatting' is a good option.
+
+## Copyright
+
+The Jenkins core is licensed under [MIT license](./LICENSE.txt), with a few exceptions in bundled classes.
+We consider all contributions as MIT unless it's explicitly stated otherwise.
+MIT-incompatible code contributions will be rejected.
+Contributions under MIT-compatible licenses may also be rejected if they are not ultimately necessary.
+
+We **Do NOT** require pull request submitters to sign the [contributor agreement](https://www.jenkins.io/project/governance/#cla)
+as long as the code is licensed under MIT, and merged by one of the contributors with the signed agreement.
+
+We still encourage people to sign the contributor agreement if they intend to submit more than a few pull requests.
+Signing is also a mandatory prerequisite for getting merge/push permissions to core repositories
+and for joining teams like the [Jenkins Security Team](https://www.jenkins.io/security/#team).
+
+## Continuous Integration
+
+The Jenkins project has a Continuous Integration server... powered by Jenkins, of course.
+It is located at [ci.jenkins.io](https://ci.jenkins.io/).
+
+The Jenkins project uses [Jenkins Pipeline](https://www.jenkins.io/doc/book/pipeline/) to run builds.
+The code for the core build flow is stored in the [Jenkinsfile](./Jenkinsfile) in the repository root.
+If you want to update that build flow (e.g. "add more checks"),
+just submit a pull request.
+
+# Links
+
+- [Jenkins Contribution Landing Page](https://www.jenkins.io/participate/)
+- [Jenkins Chat Channels](https://www.jenkins.io/chat/)
+- [Beginners Guide To Contributing](https://www.jenkins.io/participate/)
+- [List of good first issues in core](https://github.com/jenkinsci/jenkins/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22)

+ 252 - 0
jenkins/Jenkinsfile

@@ -0,0 +1,252 @@
+#!/usr/bin/env groovy
+
+/*
+ * This Jenkinsfile is intended to run on https://ci.jenkins.io and may fail anywhere else.
+ * It makes assumptions about plugins being installed, labels mapping to nodes that can build what is needed, etc.
+ */
+
+def failFast = false
+def numBuildToKeep = env.CHANGE_ID ? '10' : '50'
+
+properties([
+  buildDiscarder(logRotator(numToKeepStr: numBuildToKeep, artifactNumToKeepStr: '3')),
+  disableConcurrentBuilds(abortPrevious: true)
+])
+
+def axes = [
+  platforms: ['linux', 'windows'],
+  jdks: [21, 25],
+]
+
+stage('Record build') {
+  retry(conditions: [kubernetesAgent(handleNonKubernetes: true), nonresumable()], count: 2) {
+    node('maven-21-nonspot') {
+      infra.checkoutSCM()
+
+      /*
+       * Record the primary build for this CI job.
+       */
+      withCredentials([string(credentialsId: 'launchable-jenkins-jenkins', variable: 'LAUNCHABLE_TOKEN')]) {
+        /*
+         * TODO Add the commits of the transitive closure of the Jenkins WAR under test to this build.
+         */
+        // Replace URL encoded characters with '-' because Launchable rejects '%2F' in build name
+        def launchableName = env.BUILD_TAG.replaceAll('(%[0-9A-Fa-f]{2})+', '-')
+        sh "launchable verify && launchable record build --name ${launchableName} --source jenkinsci/jenkins=."
+        axes.values().combinations {
+          def (platform, jdk) = it
+          if (platform == 'windows' && jdk != axes.jdks.last()) {
+            return // unnecessary use of hardware
+          }
+          def sessionFile = "launchable-session-${platform}-jdk${jdk}.txt"
+          sh "launchable record session --build ${launchableName} --flavor platform=${platform} --flavor jdk=${jdk} >${sessionFile}"
+          stash name: sessionFile, includes: sessionFile
+        }
+      }
+
+      /*
+       * Record commits for use in downstream CI jobs that may consume this artifact.
+       */
+      withCredentials([string(credentialsId: 'launchable-jenkins-acceptance-test-harness', variable: 'LAUNCHABLE_TOKEN')]) {
+        sh 'launchable verify && launchable record commit'
+      }
+      withCredentials([string(credentialsId: 'launchable-jenkins-bom', variable: 'LAUNCHABLE_TOKEN')]) {
+        sh 'launchable verify && launchable record commit'
+      }
+    }
+  }
+}
+
+def builds = [:]
+
+axes.values().combinations {
+  def (platform, jdk) = it
+  if (platform == 'windows' && jdk != axes.jdks.last()) {
+    return // unnecessary use of hardware
+  }
+  builds["${platform}-jdk${jdk}"] = {
+    // see https://github.com/jenkins-infra/documentation/blob/master/ci.adoc#node-labels for information on what node types are available
+    def agentContainerLabel = 'maven-' + jdk
+    if (platform == 'windows') {
+      agentContainerLabel += '-windows'
+    }
+    agentContainerLabel += '-nonspot'
+    retry(conditions: [kubernetesAgent(handleNonKubernetes: true), nonresumable()], count: 2) {
+      node(agentContainerLabel) {
+        // First stage is actually checking out the source. Since we're using Multibranch
+        // currently, we can use "checkout scm".
+        stage("${platform.capitalize()} - JDK ${jdk} - Checkout") {
+          infra.checkoutSCM()
+        }
+
+        def tmpDir = pwd(tmp: true)
+        def changelistF = "${tmpDir}/changelist"
+        def m2repo = "${tmpDir}/m2repo"
+        def session
+
+        // Now run the actual build.
+        stage("${platform.capitalize()} - JDK ${jdk} - Build / Test") {
+          timeout(time: 6, unit: 'HOURS') {
+            dir(tmpDir) {
+              def sessionFile = "launchable-session-${platform}-jdk${jdk}.txt"
+              unstash sessionFile
+              session = readFile(sessionFile).trim()
+            }
+            def mavenOptions = [
+              '-Pdebug',
+              '-Penable-jacoco',
+              '--update-snapshots',
+              "-Dmaven.repo.local=$m2repo",
+              '-Dmaven.test.failure.ignore',
+              '-DforkCount=2',
+              '-Dspotbugs.failOnError=false',
+              '-Dcheckstyle.failOnViolation=false',
+              '-Dset.changelist',
+              'help:evaluate',
+              '-Dexpression=changelist',
+              "-Doutput=$changelistF",
+              'clean',
+              'install',
+            ]
+            if (env.CHANGE_ID && !pullRequest.labels.contains('full-test')) {
+              def excludesFile
+              def target = platform == 'windows' ? '30%' : '100%'
+              withCredentials([string(credentialsId: 'launchable-jenkins-jenkins', variable: 'LAUNCHABLE_TOKEN')]) {
+                if (isUnix()) {
+                  excludesFile = "${tmpDir}/excludes.txt"
+                  sh "launchable verify && launchable subset --session ${session} --target ${target} --get-tests-from-previous-sessions --output-exclusion-rules maven >${excludesFile}"
+                } else {
+                  excludesFile = "${tmpDir}\\excludes.txt"
+                  bat "launchable verify && launchable subset --session ${session} --target ${target}% --get-tests-from-previous-sessions --output-exclusion-rules maven >${excludesFile}"
+                }
+              }
+              mavenOptions.add(0, "-Dsurefire.excludesFile=${excludesFile}")
+            }
+            withChecks(name: 'Tests', includeStage: true) {
+              realtimeJUnit(healthScaleFactor: 20.0, testResults: '*/target/surefire-reports/*.xml,target/vitest-reports/*.xml') {
+                infra.runMaven(mavenOptions, jdk)
+                if (isUnix()) {
+                  sh 'git add . && git diff --exit-code HEAD'
+                }
+              }
+            }
+          }
+        }
+
+        // Once we've built, archive the artifacts and the test results.
+        stage("${platform.capitalize()} - JDK ${jdk} - Publish") {
+          archiveArtifacts allowEmptyArchive: true, artifacts: '**/target/surefire-reports/*.dumpstream'
+          // cli and war have been migrated to JUnit 5
+          if (failFast && currentBuild.result == 'UNSTABLE') {
+            error 'There were test failures; halting early'
+          }
+          if (platform == 'linux' && jdk == axes['jdks'][0]) {
+            def folders = env.JOB_NAME.split('/')
+            if (folders.length > 1) {
+              discoverGitReferenceBuild(scm: folders[1])
+            }
+            recordCoverage(tools: [[parser: 'JACOCO', pattern: 'coverage/target/site/jacoco-aggregate/jacoco.xml']],
+            sourceCodeRetention: 'MODIFIED', sourceDirectories: [[path: 'core/src/main/java']])
+
+            echo "Recording static analysis results for '${platform.capitalize()}'"
+            recordIssues(
+                enabledForFailure: true,
+                tools: [java()],
+                filters: [excludeFile('.*Assert.java')],
+                sourceCodeEncoding: 'UTF-8',
+                skipBlames: true,
+                trendChartType: 'TOOLS_ONLY'
+                )
+            recordIssues(
+                enabledForFailure: true,
+                tools: [javaDoc()],
+                filters: [excludeFile('.*Assert.java')],
+                sourceCodeEncoding: 'UTF-8',
+                skipBlames: true,
+                trendChartType: 'TOOLS_ONLY',
+                qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]]
+            )
+            recordIssues([tool: spotBugs(pattern: '**/target/spotbugsXml.xml'),
+              sourceCodeEncoding: 'UTF-8',
+              skipBlames: true,
+              trendChartType: 'TOOLS_ONLY',
+              qualityGates: [[threshold: 1, type: 'NEW', unstable: true]]])
+            recordIssues([tool: checkStyle(pattern: '**/target/checkstyle-result.xml'),
+              sourceCodeEncoding: 'UTF-8',
+              skipBlames: true,
+              trendChartType: 'TOOLS_ONLY',
+              qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]]])
+            recordIssues([tool: esLint(pattern: '**/target/eslint-warnings.xml'),
+              sourceCodeEncoding: 'UTF-8',
+              skipBlames: true,
+              trendChartType: 'TOOLS_ONLY',
+              qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]]])
+            recordIssues([tool: styleLint(pattern: '**/target/stylelint-warnings.xml'),
+              sourceCodeEncoding: 'UTF-8',
+              skipBlames: true,
+              trendChartType: 'TOOLS_ONLY',
+              qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]]])
+            if (failFast && currentBuild.result == 'UNSTABLE') {
+              error 'Static analysis quality gates not passed; halting early'
+            }
+            def changelist = readFile(changelistF)
+            dir(m2repo) {
+              archiveArtifacts(
+                  artifacts: "**/*$changelist/*$changelist*",
+                  excludes: '**/*.lastUpdated,**/jenkins-coverage*/,**/jenkins-test*/',
+                  allowEmptyArchive: true, // in case we forgot to reincrementalify
+                  fingerprint: true
+                  )
+            }
+          }
+          withCredentials([string(credentialsId: 'launchable-jenkins-jenkins', variable: 'LAUNCHABLE_TOKEN')]) {
+            if (isUnix()) {
+              sh "launchable verify && launchable record tests --session ${session} --flavor platform=${platform} --flavor jdk=${jdk} maven './**/target/surefire-reports'"
+            } else {
+              bat "launchable verify && launchable record tests --session ${session} --flavor platform=${platform} --flavor jdk=${jdk} maven ./**/target/surefire-reports"
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+def athAxes = [
+  platforms: ['linux'],
+  jdks: [25],
+  browsers: ['firefox'],
+]
+athAxes.values().combinations {
+  def (platform, jdk, browser) = it
+  builds["ath-${platform}-jdk${jdk}-${browser}"] = {
+    retry(conditions: [agent(), nonresumable()], count: 2) {
+      node('docker-highmem') {
+        // Just to be safe
+        deleteDir()
+        checkout scm
+
+        withChecks(name: 'Tests', includeStage: true) {
+          infra.withArtifactCachingProxy {
+            sh "bash ath.sh ${jdk} ${browser}"
+          }
+          junit testResults: 'target/ath-reports/TEST-*.xml', testDataPublishers: [[$class: 'AttachmentPublisher']]
+        }
+        /*
+         * Currently disabled, as the fact that this is a manually created subset will confuse Launchable,
+         * which expects this to be a full build. When we implement subsetting, this can be re-enabled using
+         * Launchable's subset rather than our own.
+         */
+        /*
+         withCredentials([string(credentialsId: 'launchable-jenkins-acceptance-test-harness', variable: 'LAUNCHABLE_TOKEN')]) {
+         sh "launchable verify && launchable record tests --no-build --flavor platform=${platform} --flavor jdk=${jdk} --flavor browser=${browser} maven './target/ath-reports'"
+         }
+         */
+      }
+    }
+  }
+}
+
+builds.failFast = failFast
+parallel builds
+infra.maybePublishIncrementals()

+ 22 - 0
jenkins/LICENSE.txt

@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 120 - 0
jenkins/README.md

@@ -0,0 +1,120 @@
+<a href="https://jenkins.io">
+    <img width="400" src="https://www.jenkins.io/images/jenkins-logo-title-dark.svg" alt="Jenkins logo"> 
+</a>
+
+[![Jenkins Regular Release](https://img.shields.io/endpoint?url=https%3A%2F%2Fwww.jenkins.io%2Fchangelog%2Fbadge.json)](https://www.jenkins.io/changelog)
+[![Jenkins LTS Release](https://img.shields.io/endpoint?url=https%3A%2F%2Fwww.jenkins.io%2Fchangelog-stable%2Fbadge.json)](https://www.jenkins.io/changelog-stable)
+[![Docker Pulls](https://img.shields.io/docker/pulls/jenkins/jenkins.svg)](https://hub.docker.com/r/jenkins/jenkins/)
+[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3538/badge)](https://bestpractices.coreinfrastructure.org/projects/3538)
+[![Reproducible Builds](https://img.shields.io/badge/Reproducible_Builds-ok-green)](https://maven.apache.org/guides/mini/guide-reproducible-builds.html)
+[![Gitter](https://img.shields.io/gitter/room/jenkinsci/jenkins)](https://matrix.to/#/#jenkinsci_jenkins:gitter.im)
+
+---
+
+# Table of Contents
+
+- [About](#about)
+- [What to Use Jenkins for and When to Use It](#what-to-use-jenkins-for-and-when-to-use-it)
+- [Downloads](#downloads)
+- [Getting Started (Development)](#getting-started-development)
+- [Source](#source)
+- [Contributing to Jenkins](#contributing-to-jenkins)
+- [News and Website](#news-and-website)
+- [Governance](#governance)
+- [Adopters](#adopters)
+- [License](#license)
+
+---
+
+# About
+
+In a nutshell, Jenkins is the leading open-source automation server.
+Built with Java, it provides over 2,000 [plugins](https://plugins.jenkins.io/) to support automating virtually anything,
+so that humans can spend their time doing things machines cannot.
+
+# What to Use Jenkins for and When to Use It
+
+Use Jenkins to automate your development workflow, so you can focus on work that matters most. Jenkins is commonly used for:
+
+- Building projects
+- Running tests to detect bugs and other issues as soon as they are introduced
+- Static code analysis
+- Deployment
+
+Execute repetitive tasks, save time, and optimize your development process with Jenkins.
+
+# Downloads
+
+The Jenkins project provides official distributions as WAR files, Docker images, native packages and installers for platforms including several Linux distributions and Windows.
+See the [Downloads](https://www.jenkins.io/download) page for references.
+
+For all distributions Jenkins offers two release lines:
+
+- [Weekly](https://www.jenkins.io/download/weekly/) -
+  Frequent releases which include all new features, improvements, and bug fixes.
+- [Long-Term Support (LTS)](https://www.jenkins.io/download/lts/) -
+  Older release line which gets periodically updated via bug fix backports.
+
+Latest releases:
+
+[![Jenkins Regular Release](https://img.shields.io/endpoint?url=https%3A%2F%2Fwww.jenkins.io%2Fchangelog%2Fbadge.json)](https://www.jenkins.io/changelog)
+[![Jenkins LTS Release](https://img.shields.io/endpoint?url=https%3A%2F%2Fwww.jenkins.io%2Fchangelog-stable%2Fbadge.json)](https://www.jenkins.io/changelog-stable)
+
+# Getting Started (Development)
+
+For more information on setting up your development environment, contributing, and working with Jenkins internals, check the [contributing guide](CONTRIBUTING.md) and the [Jenkins Developer Documentation](https://www.jenkins.io/doc/developer/).
+
+# Source
+
+Our latest and greatest source of Jenkins can be found on [GitHub](https://github.com/jenkinsci/jenkins). Fork us!
+
+# Contributing to Jenkins
+
+New to open source or Jenkins? Here’s how to get started:
+
+- Read the [Contribution Guidelines](CONTRIBUTING.md)
+- Check our [good first issues](https://github.com/jenkinsci/jenkins/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22)
+- Join our [Gitter chat](https://matrix.to/#/#jenkinsci_newcomer-contributors:gitter.im) for questions and help
+
+For more information about participating in the community and contributing to the Jenkins project,
+see [this page](https://www.jenkins.io/participate/).
+
+Documentation for Jenkins core maintainers is in the [maintainers guidelines](docs/MAINTAINERS.adoc).
+
+# News and Website
+
+All information about Jenkins can be found on our [official website](https://www.jenkins.io/), including documentation, blog posts, plugin listings, community updates, and more.
+
+Stay up-to-date with the latest Jenkins news, tutorials, and release notes:
+
+- [Jenkins Blog](https://www.jenkins.io/blog/)
+- [Documentation](https://www.jenkins.io/doc/)
+- [Plugins Index](https://plugins.jenkins.io/)
+- [Events](https://www.jenkins.io/events/)
+
+Follow Jenkins on social media to stay connected with the community:
+
+- [Twitter / X](https://x.com/jenkinsci)
+- [YouTube](https://www.youtube.com/@jenkinscicd)
+- [LinkedIn](https://www.linkedin.com/company/jenkins-project/)
+
+# Governance
+
+The Jenkins project is governed by an open source community.
+To learn more about the governance structure, project leadership, and how decisions are made, visit the [Governance Page](https://www.jenkins.io/project/governance/).
+
+# Adopters
+
+Jenkins is trusted by **millions of users** and adopted by **thousands of companies** around the world — from startups to enterprises — to automate their software delivery pipelines.
+
+Explore the [Adopters Page](https://www.jenkins.io/project/adopters/) and https://stories.jenkins.io to see:
+
+- Companies and organizations using Jenkins
+- Success stories and case studies
+- How Jenkins is used in different industries
+
+> If your company uses Jenkins and you'd like to be featured, feel free to [submit your story](https://www.jenkins.io/project/adopters/contributing/#share-your-story)!
+
+# License
+
+Jenkins is **licensed** under the **[MIT License](LICENSE.txt)**.

+ 73 - 0
jenkins/ath.sh

@@ -0,0 +1,73 @@
+#!/usr/bin/bash
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+cd "$(dirname "$0")"
+
+# https://github.com/jenkinsci/acceptance-test-harness/releases
+export ATH_VERSION=6724.va_7c5c4b_fcf9c
+
+if [[ $# -eq 0 ]]; then
+	export JDK=21
+	export BROWSER=firefox
+else
+	export JDK=$1
+	export BROWSER=$2
+fi
+
+MVN='mvn -B -ntp -Pquick-build -am -pl war package'
+if [[ -n ${MAVEN_SETTINGS-} ]]; then
+	MVN="${MVN} -s ${MAVEN_SETTINGS}"
+fi
+
+[[ -f war/target/jenkins.war ]] || $MVN
+
+mkdir -p target/ath-reports
+chmod a+rwx target/ath-reports
+
+curl \
+	--fail \
+	--silent \
+	--show-error \
+	--output /tmp/ath.yml \
+	--location "https://raw.githubusercontent.com/jenkinsci/acceptance-test-harness/refs/tags/${ATH_VERSION}/docker-compose.yml"
+
+sed -i -e "s/jenkins\/ath:latest/jenkins\/ath:${ATH_VERSION}/g" /tmp/ath.yml
+
+# obtain the groupId to grant to access the docker socket to run tests needing docker
+if [[ -z ${DOCKER_GID:-} ]]; then
+	DOCKER_GID=$(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ubuntu:noble stat -c %g /var/run/docker.sock) || exit 1
+	export DOCKER_GID
+fi
+
+trap 'docker-compose --file /tmp/ath.yml kill && docker-compose --file /tmp/ath.yml down' EXIT
+
+exec docker-compose \
+	--file /tmp/ath.yml \
+	run \
+	--env JDK \
+	--env ATH_VERSION \
+	--env BROWSER \
+	--name mvn \
+	--no-TTY \
+	--rm \
+	--volume "$(pwd)"/war/target/jenkins.war:/jenkins.war:ro \
+	--volume "$(pwd)"/target/ath-reports:/reports:rw \
+	mvn \
+	bash <<-'INSIDE'
+		set -o errexit
+		set -o nounset
+		set -o pipefail
+		set -o xtrace
+		cd
+		set-java.sh "${JDK}"
+		env | sort
+		git clone --branch "${ATH_VERSION}" --depth 1 https://github.com/jenkinsci/acceptance-test-harness
+		cd acceptance-test-harness
+		run.sh "remote-webdriver-${BROWSER}" /jenkins.war \
+			-Dmaven.test.failure.ignore \
+			-DforkCount=1 \
+			-Dgroups=org.jenkinsci.test.acceptance.junit.SmokeTest
+		cp --verbose target/surefire-reports/TEST-*.xml /reports
+	INSIDE

+ 363 - 0
jenkins/bom/pom.xml

@@ -0,0 +1,363 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+The MIT License
+
+Copyright (c) 2019, 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.jenkins-ci.main</groupId>
+    <artifactId>jenkins-parent</artifactId>
+    <version>${revision}${changelist}</version>
+  </parent>
+
+  <artifactId>jenkins-bom</artifactId>
+  <packaging>pom</packaging>
+
+  <name>Jenkins BOM</name>
+  <description>The module contains dependencies that are used by a specific Jenkins version</description>
+
+  <properties>
+    <commons-fileupload2.version>2.0.0-M5</commons-fileupload2.version>
+    <groovy.version>2.4.21</groovy.version>
+    <jelly.version>1.1-jenkins-20260418</jelly.version>
+    <stapler.version>2123.v539a_775c45c3</stapler.version>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>com.google.inject</groupId>
+        <artifactId>guice-bom</artifactId>
+        <version>6.0.0</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-bom</artifactId>
+        <version>2.0.20</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-framework-bom</artifactId>
+        <version>7.0.9</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <!-- https://docs.spring.io/spring-security/reference/6.3/getting-spring-security.html#getting-maven-no-boot -->
+        <groupId>org.springframework.security</groupId>
+        <artifactId>spring-security-bom</artifactId>
+        <version>7.1.1</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <!-- JENKINS-21160: Remoting also depends on args4j; please update accordingly -->
+        <groupId>args4j</groupId>
+        <artifactId>args4j</artifactId>
+        <version>2.37</version>
+      </dependency>
+      <dependency>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-annotations</artifactId>
+        <version>${spotbugs-annotations.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>33.7.2-jre</version>
+      </dependency>
+      <dependency>
+        <groupId>com.sun.xml.txw2</groupId>
+        <artifactId>txw2</artifactId>
+        <version>20110809</version>
+      </dependency>
+      <dependency>
+        <groupId>com.thoughtworks.xstream</groupId>
+        <artifactId>xstream</artifactId>
+        <version>1.4.21</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-beanutils</groupId>
+        <artifactId>commons-beanutils</artifactId>
+        <version>1.11.0</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-codec</groupId>
+        <artifactId>commons-codec</artifactId>
+        <version>1.22.1</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-collections</groupId>
+        <artifactId>commons-collections</artifactId>
+        <version>3.2.2</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-io</groupId>
+        <artifactId>commons-io</artifactId>
+        <version>2.22.0</version>
+      </dependency>
+      <dependency>
+        <groupId>jakarta.servlet</groupId>
+        <artifactId>jakarta.servlet-api</artifactId>
+        <version>5.0.0</version>
+      </dependency>
+      <dependency>
+        <groupId>jakarta.servlet.jsp.jstl</groupId>
+        <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
+        <version>2.0.0</version>
+      </dependency>
+      <dependency>
+        <groupId>jaxen</groupId>
+        <artifactId>jaxen</artifactId>
+        <version>2.0.6</version>
+      </dependency>
+      <dependency>
+        <groupId>net.java.dev.jna</groupId>
+        <artifactId>jna</artifactId>
+        <version>5.19.1</version>
+      </dependency>
+      <dependency>
+        <groupId>net.java.sezpoz</groupId>
+        <artifactId>sezpoz</artifactId>
+        <version>1.17</version>
+      </dependency>
+      <dependency>
+        <groupId>net.jcip</groupId>
+        <artifactId>jcip-annotations</artifactId>
+        <version>1.0</version>
+      </dependency>
+      <dependency>
+        <groupId>org.antlr</groupId>
+        <artifactId>antlr4-runtime</artifactId>
+        <version>${antlr.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.ant</groupId>
+        <artifactId>ant</artifactId>
+        <version>1.10.18</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2-core</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2-distribution</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2-jakarta-servlet5</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2-javax</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-fileupload2-portlet</artifactId>
+        <version>${commons-fileupload2.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.codehaus.groovy</groupId>
+        <artifactId>groovy-all</artifactId>
+        <version>${groovy.version}</version>
+      </dependency>
+      <dependency>
+        <!-- Groovy shell uses this, but it doesn't declare the dependency -->
+        <groupId>org.fusesource.jansi</groupId>
+        <artifactId>jansi</artifactId>
+        <version>1.11</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>annotation-indexer</artifactId>
+        <version>1.213.v92788b_1f0a_21</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>commons-jelly</artifactId>
+        <version>${jelly.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>commons-jelly-tags-fmt</artifactId>
+        <version>${jelly.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>commons-jelly-tags-xml</artifactId>
+        <version>${jelly.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>commons-jexl</artifactId>
+        <version>1.1-jenkins-20111212</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>crypto-util</artifactId>
+        <version>1.11</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>memory-monitor</artifactId>
+        <version>1.13</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>symbol-annotation</artifactId>
+        <version>1.26</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>task-reactor</artifactId>
+        <version>1.10</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci</groupId>
+        <artifactId>version-number</artifactId>
+        <version>1.14</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jenkins-ci.main</groupId>
+        <artifactId>remoting</artifactId>
+        <version>${remoting.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jfree</groupId>
+        <artifactId>jfreechart</artifactId>
+        <version>1.0.19</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jvnet.hudson</groupId>
+        <artifactId>commons-jelly-tags-define</artifactId>
+        <version>${jelly.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jvnet.localizer</groupId>
+        <artifactId>localizer</artifactId>
+        <version>1.31</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jvnet.winp</groupId>
+        <artifactId>winp</artifactId>
+        <version>1.31</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke</groupId>
+        <artifactId>access-modifier-annotation</artifactId>
+        <version>${access-modifier.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke</groupId>
+        <artifactId>windows-package-checker</artifactId>
+        <version>1.2</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke.metainf-services</groupId>
+        <artifactId>metainf-services</artifactId>
+        <version>1.11</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke.stapler</groupId>
+        <artifactId>json-lib</artifactId>
+        <version>2.4-jenkins-15</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke.stapler</groupId>
+        <artifactId>stapler</artifactId>
+        <version>${stapler.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke.stapler</groupId>
+        <artifactId>stapler-adjunct-codemirror</artifactId>
+        <version>1.3</version>
+      </dependency>
+      <dependency>
+        <groupId>org.kohsuke.stapler</groupId>
+        <artifactId>stapler-groovy</artifactId>
+        <version>${stapler.version}</version>
+      </dependency>
+      <!-- Override the outdated managed dependency on asm in guice-parent -->
+      <dependency>
+        <groupId>org.ow2.asm</groupId>
+        <artifactId>asm</artifactId>
+        <version>9.11.1</version>
+      </dependency>
+      <dependency>
+        <!-- provided by jcl-over-slf4j -->
+        <groupId>commons-logging</groupId>
+        <artifactId>commons-logging</artifactId>
+        <version>1.4.0</version>
+        <scope>provided</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <plugins>
+      <!-- override the default to keep the dependencyManagement intact -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>flatten-maven-plugin</artifactId>
+        <configuration>
+          <updatePomFile>true</updatePomFile>
+          <outputDirectory>${project.build.directory}</outputDirectory>
+          <flattenedPomFilename>${project.artifactId}-${project.version}.pom</flattenedPomFilename>
+        </configuration>
+        <executions>
+          <execution>
+            <id>flatten</id>
+            <goals>
+              <goal>flatten</goal>
+            </goals>
+            <phase>process-resources</phase>
+            <configuration>
+              <flattenMode>bom</flattenMode>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>

+ 266 - 0
jenkins/cli/pom.xml

@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.jenkins-ci.main</groupId>
+    <artifactId>jenkins-parent</artifactId>
+    <version>${revision}${changelist}</version>
+  </parent>
+
+  <artifactId>cli</artifactId>
+
+  <name>Jenkins cli</name>
+  <description>Command line interface for Jenkins</description>
+  <url>https://github.com/jenkinsci/jenkins</url>
+
+  <properties>
+    <mina-sshd.version>2.20.0</mina-sshd.version>
+    <!-- Filled in by jacoco-maven-plugin -->
+    <jacocoSurefireArgs />
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.jenkins-ci.main</groupId>
+        <artifactId>jenkins-bom</artifactId>
+        <version>${project.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <dependencies>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sshd</groupId>
+      <artifactId>sshd-common</artifactId>
+      <version>${mina-sshd.version}</version>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sshd</groupId>
+      <artifactId>sshd-core</artifactId>
+      <version>${mina-sshd.version}</version>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk18on</artifactId>
+      <version>1.86</version>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish.tyrus.bundles</groupId>
+      <artifactId>tyrus-standalone-client-jdk</artifactId>
+      <version>2.2.2</version>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>annotation-indexer</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.jvnet.localizer</groupId>
+      <artifactId>localizer</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-jdk14</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>com.github.spotbugs</groupId>
+      <artifactId>spotbugs-annotations</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+      <exclusions>
+        <exclusion>
+          <groupId>com.google.code.findbugs</groupId>
+          <artifactId>jsr305</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke</groupId>
+      <artifactId>access-modifier-annotation</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <annotationProcessorPaths>
+            <!-- Indexes @Restricted; version resolves from dependencyManagement (jenkins-bom) -->
+            <path>
+              <groupId>org.jenkins-ci</groupId>
+              <artifactId>annotation-indexer</artifactId>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <configuration>
+          <argLine>@{jacocoSurefireArgs}</argLine>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>3.6.2</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <phase>package</phase>
+            <configuration>
+              <createDependencyReducedPom>false</createDependencyReducedPom>
+              <relocations>
+                <relocation>
+                  <pattern>org</pattern>
+                  <shadedPattern>io.jenkins.cli.shaded.org</shadedPattern>
+                </relocation>
+                <relocation>
+                  <pattern>net</pattern>
+                  <shadedPattern>io.jenkins.cli.shaded.net</shadedPattern>
+                </relocation>
+                <relocation>
+                  <pattern>com</pattern>
+                  <shadedPattern>io.jenkins.cli.shaded.com</shadedPattern>
+                </relocation>
+                <relocation>
+                  <pattern>jakarta</pattern>
+                  <shadedPattern>io.jenkins.cli.shaded.jakarta</shadedPattern>
+                </relocation>
+              </relocations>
+              <transformers>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <mainClass>hudson.cli.CLI</mainClass>
+                  <manifestEntries>
+                    <Jenkins-CLI-Version>${project.version}</Jenkins-CLI-Version>
+                  </manifestEntries>
+                </transformer>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+              </transformers>
+              <filters>
+                <filter>
+                  <!-- signature might cause some trouble -->
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                    <!--
+                      Multi-release variants are dead code in this uber jar: the shaded manifest does not declare
+                      Multi-Release: true and the versioned classes are not relocated, so the JVM can never load
+                      them. BouncyCastle et al. run on their base classes.
+                    -->
+                    <exclude>META-INF/versions/**</exclude>
+                    <!--
+                      annotation-indexer declares an annotation processor. Relocating it leaves this
+                      uber jar advertising io.jenkins.cli.shaded...AnnotationProcessorImpl, so javac
+                      runs it in every module that compiles against the CLI. It can never match
+                      anything, since the annotations it looks for are relocated too, but it does
+                      defeat compile avoidance. A runtime CLI jar has no compile-time processor.
+                    -->
+                    <exclude>META-INF/services/javax.annotation.processing.Processor</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.jvnet.localizer</groupId>
+        <artifactId>localizer-maven-plugin</artifactId>
+        <!-- version specified in grandparent pom -->
+        <executions>
+          <execution>
+            <goals>
+              <goal>generate</goal>
+            </goals>
+            <configuration>
+              <fileMask>Messages.properties</fileMask>
+              <outputDirectory>target/generated-sources/localizer</outputDirectory>
+              <accessModifierAnnotations>true</accessModifierAnnotations>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <executions>
+          <execution>
+            <id>add-source</id>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <sources>
+                <source>${project.build.directory}/generated-sources/localizer</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <profiles>
+    <profile>
+      <id>enable-jacoco</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.jacoco</groupId>
+            <artifactId>jacoco-maven-plugin</artifactId>
+            <!-- Version specified in parent POM -->
+            <executions>
+              <execution>
+                <goals>
+                  <goal>prepare-agent</goal>
+                </goals>
+                <configuration>
+                  <propertyName>jacocoSurefireArgs</propertyName>
+                  <includes>
+                    <include>hudson/*</include>
+                    <include>hudson/**/*</include>
+                    <include>jenkins/*</include>
+                    <include>jenkins/**/*</include>
+                    <include>org/jenkins/**/*</include>
+                  </includes>
+                  <excludes>
+                    <exclude>**/Messages.class</exclude>
+                  </excludes>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>

+ 1 - 0
jenkins/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties

@@ -0,0 +1 @@
+version=${project.version}

+ 572 - 0
jenkins/cli/src/main/java/hudson/cli/CLI.java

@@ -0,0 +1,572 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, 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.
+ */
+
+package hudson.cli;
+
+import static java.util.logging.Level.FINE;
+import static java.util.logging.Level.parse;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.cli.client.Messages;
+import jakarta.websocket.ClientEndpointConfig;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.HandshakeResponse;
+import jakarta.websocket.Session;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import org.glassfish.tyrus.client.ClientManager;
+import org.glassfish.tyrus.client.ClientProperties;
+import org.glassfish.tyrus.client.SslEngineConfigurator;
+import org.glassfish.tyrus.client.exception.DeploymentHandshakeException;
+import org.glassfish.tyrus.container.jdk.client.JdkClientContainer;
+
+/**
+ * CLI entry point to Jenkins.
+ */
+@SuppressFBWarnings(value = "CRLF_INJECTION_LOGS", justification = "We don't care about this behavior")
+public class CLI {
+
+    private CLI() {}
+
+    /**
+     * Make sure the connection is open against Jenkins server.
+     *
+     * @param c The open connection.
+     * @throws IOException in case of communication problem.
+     * @throws NotTalkingToJenkinsException when connection is not made to Jenkins service.
+     */
+    /*package*/ static void verifyJenkinsConnection(URLConnection c) throws IOException {
+        if (c.getHeaderField("X-Hudson") == null && c.getHeaderField("X-Jenkins") == null)
+            throw new NotTalkingToJenkinsException(c);
+    }
+
+    /*package*/ static final class NotTalkingToJenkinsException extends IOException {
+        NotTalkingToJenkinsException(String s) {
+            super(s);
+        }
+
+        NotTalkingToJenkinsException(URLConnection c) {
+            super("There's no Jenkins running at " + c.getURL().toString());
+        }
+    }
+
+    public static void main(final String[] _args) throws Exception {
+        try {
+            System.exit(_main(_args));
+        } catch (NotTalkingToJenkinsException ex) {
+            System.err.println(ex.getMessage());
+            System.exit(3);
+        } catch (Throwable t) {
+            // if the CLI main thread die, make sure to kill the JVM.
+            t.printStackTrace();
+            System.exit(-1);
+        }
+    }
+
+    private enum Mode { HTTP, SSH, WEB_SOCKET }
+
+    public static int _main(String[] _args) throws Exception {
+        List<String> args = Arrays.asList(_args);
+        PrivateKeyProvider provider = new PrivateKeyProvider();
+
+        String url = System.getenv("JENKINS_URL");
+
+        if (url == null)
+            url = System.getenv("HUDSON_URL");
+
+        boolean noKeyAuth = false;
+
+        Mode mode = null;
+
+        String user = null;
+        String auth = null;
+        String bearer = null;
+
+        String userIdEnv = System.getenv("JENKINS_USER_ID");
+        String tokenEnv = System.getenv("JENKINS_API_TOKEN");
+
+        boolean strictHostKey = false;
+        boolean noCertificateCheck = false;
+
+        while (!args.isEmpty()) {
+            String head = args.get(0);
+            switch (head) {
+                case "-version" -> {
+                    System.out.println("Version: " + computeVersion());
+                    return 0;
+                }
+                case "-http" -> {
+                    if (mode != null) {
+                        printUsage("-http clashes with previously defined mode " + mode);
+                        return -1;
+                    }
+                    mode = Mode.HTTP;
+                    args = args.subList(1, args.size());
+                    continue;
+                }
+                case "-ssh" -> {
+                    if (mode != null) {
+                        printUsage("-ssh clashes with previously defined mode " + mode);
+                        return -1;
+                    }
+                    mode = Mode.SSH;
+                    args = args.subList(1, args.size());
+                    continue;
+                }
+                case "-webSocket" -> {
+                    if (mode != null) {
+                        printUsage("-webSocket clashes with previously defined mode " + mode);
+                        return -1;
+                    }
+                    mode = Mode.WEB_SOCKET;
+                    args = args.subList(1, args.size());
+                    continue;
+                }
+                case "-remoting" -> {
+                    printUsage("-remoting mode is no longer supported");
+                    return -1;
+                }
+                default -> {
+                    // continue
+                }
+            }
+            if (head.equals("-s") && args.size() >= 2) {
+                url = args.get(1);
+                args = args.subList(2, args.size());
+                continue;
+            }
+            if (head.equals("-noCertificateCheck")) {
+                LOGGER.info("Skipping HTTPS certificate checks altogether. Note that this is not secure at all.");
+                noCertificateCheck = true;
+                args = args.subList(1, args.size());
+                continue;
+            }
+            if (head.equals("-noKeyAuth")) {
+                noKeyAuth = true;
+                args = args.subList(1, args.size());
+                continue;
+            }
+            if (head.equals("-i") && args.size() >= 2) {
+                File f = getFileFromArguments(args);
+                if (!f.exists()) {
+                    printUsage(Messages.CLI_NoSuchFileExists(f));
+                    return -1;
+                }
+
+                provider.readFrom(f);
+
+                args = args.subList(2, args.size());
+                continue;
+            }
+            if (head.equals("-strictHostKey")) {
+                strictHostKey = true;
+                args = args.subList(1, args.size());
+                continue;
+            }
+            if (head.equals("-user") && args.size() >= 2) {
+                user = args.get(1);
+                args = args.subList(2, args.size());
+                continue;
+            }
+            if (head.equals("-auth") && args.size() >= 2) {
+                auth = args.get(1);
+                args = args.subList(2, args.size());
+                continue;
+            }
+            if (head.equals("-bearer") && args.size() >= 2) {
+                bearer = args.get(1);
+                args = args.subList(2, args.size());
+                continue;
+            }
+            if (head.equals("-logger") && args.size() >= 2) {
+                Level level = parse(args.get(1));
+                for (Handler h : Logger.getLogger("").getHandlers()) {
+                    h.setLevel(level);
+                }
+                for (Logger logger : new Logger[] {LOGGER, FullDuplexHttpStream.LOGGER, PlainCLIProtocol.LOGGER, Logger.getLogger("org.apache.sshd")}) { // perhaps also Channel
+                    logger.setLevel(level);
+                }
+                args = args.subList(2, args.size());
+                continue;
+            }
+            break;
+        }
+
+        if (url == null) {
+            printUsage(Messages.CLI_NoURL());
+            return -1;
+        }
+
+        if (auth != null && bearer != null) {
+            LOGGER.warning("-auth and -bearer are mutually exclusive");
+        }
+
+        if (auth == null && bearer == null) {
+            // -auth option not set
+            if ((userIdEnv != null && !userIdEnv.isBlank()) && (tokenEnv != null && !tokenEnv.isBlank())) {
+                auth = userIdEnv.concat(":").concat(tokenEnv);
+            } else if ((userIdEnv != null && !userIdEnv.isBlank()) || (tokenEnv != null && !tokenEnv.isBlank())) {
+                printUsage(Messages.CLI_BadAuth());
+                return -1;
+            } // Otherwise, none credentials were set
+
+        }
+
+        if (!url.endsWith("/")) {
+            url += '/';
+        }
+
+        if (args.isEmpty())
+            args = List.of("help"); // default to help
+
+        if (mode == null) {
+            mode = Mode.WEB_SOCKET;
+        }
+
+        LOGGER.log(FINE, "using connection mode {0}", mode);
+
+        if (user != null && auth != null) {
+            LOGGER.warning("-user and -auth are mutually exclusive");
+        }
+
+        if (mode == Mode.SSH) {
+            if (user == null) {
+                // TODO SshCliAuthenticator already autodetects the user based on public key; why cannot AsynchronousCommand.getCurrentUser do the same?
+                LOGGER.warning("-user required when using -ssh");
+                return -1;
+            }
+            if (!noKeyAuth && !provider.hasKeys()) {
+                provider.readFromDefaultLocations();
+            }
+            return SSHCLI.sshConnection(url, user, args, provider, strictHostKey);
+        }
+
+        if (strictHostKey) {
+            LOGGER.warning("-strictHostKey meaningful only with -ssh");
+        }
+
+        if (noKeyAuth) {
+            LOGGER.warning("-noKeyAuth meaningful only with -ssh");
+        }
+
+        if (user != null) {
+            LOGGER.warning("Warning: -user ignored unless using -ssh");
+        }
+
+        CLIConnectionFactory factory = new CLIConnectionFactory().noCertificateCheck(noCertificateCheck);
+        String userInfo = new URL(url).getUserInfo();
+        if (userInfo != null) {
+            factory = factory.basicAuth(userInfo);
+        } else if (auth != null) {
+            factory = factory.basicAuth(auth.startsWith("@") ? readAuthFromFile(auth).trim() : auth);
+        } else if (bearer != null) {
+            factory = factory.bearerAuth(bearer.startsWith("@") ? readAuthFromFile(bearer).trim() : bearer);
+        }
+
+
+        if (mode == Mode.HTTP) {
+            return plainHttpConnection(url, args, factory);
+        }
+
+        if (mode == Mode.WEB_SOCKET) {
+            return webSocketConnection(url, args, factory);
+        }
+
+        throw new AssertionError();
+    }
+
+    @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "User provided value for running the program.")
+    private static String readAuthFromFile(String auth) throws IOException {
+        Path path;
+        try {
+            path = Paths.get(auth.substring(1));
+        } catch (InvalidPathException e) {
+            throw new IOException(e);
+        }
+        return Files.readString(path, Charset.defaultCharset());
+    }
+
+    @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "User provided value for running the program.")
+    private static File getFileFromArguments(List<String> args) {
+        return new File(args.get(1));
+    }
+
+    private static int webSocketConnection(String url, List<String> args, CLIConnectionFactory factory) throws Exception {
+        LOGGER.fine(() -> "Trying to connect to " + url + " via plain protocol over WebSocket");
+        class CLIEndpoint extends Endpoint {
+            @Override
+            public void onOpen(Session session, EndpointConfig config) {}
+        }
+
+        class Authenticator extends ClientEndpointConfig.Configurator {
+            HandshakeResponse hr;
+
+            @Override
+            public void beforeRequest(Map<String, List<String>> headers) {
+                if (factory.authorization != null) {
+                    headers.put("Authorization", List.of(factory.authorization));
+                }
+            }
+
+            @Override
+            public void afterResponse(HandshakeResponse hr) {
+                this.hr = hr;
+            }
+        }
+
+        var authenticator = new Authenticator();
+
+        ClientManager client = ClientManager.createClient(JdkClientContainer.class.getName()); // ~ ContainerProvider.getWebSocketContainer()
+        client.getProperties().put(ClientProperties.REDIRECT_ENABLED, true); // https://tyrus-project.github.io/documentation/1.13.1/index/tyrus-proprietary-config.html#d0e1775
+        if (factory.noCertificateCheck) {
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, new TrustManager[] {new NoCheckTrustManager()}, new SecureRandom());
+            SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator(sslContext);
+            sslEngineConfigurator.setHostnameVerifier((s, sslSession) -> true);
+            client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator);
+        }
+        Session session;
+        try {
+            session = client.connectToServer(new CLIEndpoint(), ClientEndpointConfig.Builder.create().configurator(authenticator).build(), URI.create(url.replaceFirst("^http", "ws") + "cli/ws"));
+        } catch (DeploymentHandshakeException x) {
+            System.err.println("CLI handshake failed with status code " + x.getHttpStatusCode());
+            if (authenticator.hr != null) {
+                for (var entry : authenticator.hr.getHeaders().entrySet()) {
+                    // org.glassfish.tyrus.core.Utils.parseHeaderValue improperly splits values like Date at commas, so undo that:
+                    System.err.println(entry.getKey() + ": " + String.join(", ", entry.getValue()));
+                }
+                // UpgradeResponse.getReasonPhrase is useless since Jetty generates it from the code,
+                // and the body is not accessible at all.
+            }
+            return 15; // compare CLICommand.main
+        }
+        PlainCLIProtocol.Output out = new PlainCLIProtocol.Output() {
+            @Override
+            public void send(byte[] data) throws IOException {
+                session.getBasicRemote().sendBinary(ByteBuffer.wrap(data));
+            }
+
+            @Override
+            public void close() throws IOException {
+                session.close();
+            }
+        };
+        try (ClientSideImpl connection = new ClientSideImpl(out)) {
+            session.addMessageHandler(InputStream.class, is -> {
+                try {
+                    connection.handle(new DataInputStream(is));
+                } catch (IOException x) {
+                    LOGGER.log(Level.WARNING, null, x);
+                }
+            });
+            connection.start(args);
+            return connection.exit();
+        }
+    }
+
+    private static int plainHttpConnection(String url, List<String> args, CLIConnectionFactory factory)
+            throws GeneralSecurityException, IOException, InterruptedException {
+        LOGGER.log(FINE, "Trying to connect to {0} via plain protocol over HTTP", url);
+        if (factory.noCertificateCheck) {
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, new TrustManager[] {new NoCheckTrustManager()}, new SecureRandom());
+            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
+            HttpsURLConnection.setDefaultHostnameVerifier((s, sslSession) -> true);
+        }
+        FullDuplexHttpStream streams = new FullDuplexHttpStream(new URL(url), "cli?remoting=false", factory.authorization);
+        try (ClientSideImpl connection = new ClientSideImpl(new PlainCLIProtocol.FramedOutput(streams.getOutputStream()))) {
+            connection.start(args);
+            InputStream is = streams.getInputStream();
+            if (is.read() != 0) { // cf. FullDuplexHttpService
+                throw new IOException("expected to see initial zero byte; perhaps you are connecting to an old server which does not support -http?");
+            }
+            new PlainCLIProtocol.FramedReader(connection, is).start();
+            new Thread("ping") { // JENKINS-46659
+                @Override
+                public void run() {
+                    try {
+                        Thread.sleep(PING_INTERVAL);
+                        while (!connection.complete) {
+                            LOGGER.fine("sending ping");
+                            connection.sendEncoding(Charset.defaultCharset().name()); // no-op at this point
+                            Thread.sleep(PING_INTERVAL);
+                        }
+                    } catch (IOException | InterruptedException x) {
+                        LOGGER.log(Level.WARNING, null, x);
+                    }
+                }
+
+            }.start();
+            return connection.exit();
+        }
+    }
+
+    private static final class ClientSideImpl extends PlainCLIProtocol.ClientSide {
+
+        volatile boolean complete;
+        private int exit = -1;
+
+        ClientSideImpl(PlainCLIProtocol.Output out) {
+            super(out);
+        }
+
+        void start(List<String> args) throws IOException {
+            for (String arg : args) {
+                sendArg(arg);
+            }
+            sendEncoding(Charset.defaultCharset().name());
+            sendLocale(Locale.getDefault().toString());
+            sendStart();
+            new Thread("input reader") {
+                @Override
+                public void run() {
+                    try {
+                        final OutputStream stdin = streamStdin();
+                        byte[] buf = new byte[60_000]; // less than 64Kb frame size for WS
+                        while (!complete) {
+                            int len = System.in.read(buf);
+                            if (len == -1) {
+                                break;
+                            } else {
+                                stdin.write(buf, 0, len);
+                            }
+                        }
+                        sendEndStdin();
+                    } catch (IOException x) {
+                        LOGGER.log(Level.WARNING, null, x);
+                    }
+                }
+            }.start();
+        }
+
+        @Override
+        protected synchronized void onExit(int code) {
+            this.exit = code;
+            finished();
+        }
+
+        @Override
+        protected void onStdout(byte[] chunk) throws IOException {
+            System.out.write(chunk);
+        }
+
+        @Override
+        protected void onStderr(byte[] chunk) throws IOException {
+            System.err.write(chunk);
+        }
+
+        @Override
+        protected void handleClose() {
+            finished();
+        }
+
+        private synchronized void finished() {
+            complete = true;
+            notifyAll();
+        }
+
+        synchronized int exit() throws InterruptedException {
+            while (!complete) {
+                wait();
+            }
+            return exit;
+        }
+
+    }
+
+    private static String computeVersion() {
+        Properties props = new Properties();
+        try (InputStream is = CLI.class.getResourceAsStream("/jenkins/cli/jenkins-cli-version.properties")) {
+            if (is != null) {
+                props.load(is);
+            }
+        } catch (IOException e) {
+            e.printStackTrace(); // if the version properties is missing, that's OK.
+        }
+        return props.getProperty("version", "?");
+    }
+
+    /**
+     * Loads an SSH private key (RSA, DSA, ECDSA or Ed25519) into a {@link KeyPair}.
+     */
+    public static KeyPair loadKey(File f, String passwd) throws IOException, GeneralSecurityException {
+        return PrivateKeyProvider.loadKey(f, passwd);
+    }
+
+    public static KeyPair loadKey(File f) throws IOException, GeneralSecurityException {
+        return loadKey(f, null);
+    }
+
+    /**
+     * Loads an SSH private key (RSA, DSA, ECDSA or Ed25519) into a {@link KeyPair}.
+     */
+    public static KeyPair loadKey(String pemString, String passwd) throws IOException, GeneralSecurityException {
+        return PrivateKeyProvider.loadKey(pemString, passwd);
+    }
+
+    public static KeyPair loadKey(String pemString) throws IOException, GeneralSecurityException {
+        return loadKey(pemString, null);
+    }
+
+    /** For access from {@code HelpCommand}. */
+    static String usage() {
+        return Messages.CLI_Usage();
+    }
+
+    private static void printUsage(String msg) {
+        if (msg != null)   System.out.println(msg);
+        System.err.println(usage());
+    }
+
+    static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
+
+    private static final int PING_INTERVAL = Integer.getInteger(CLI.class.getName() + ".pingInterval", 3_000); // JENKINS-59267
+}

+ 58 - 0
jenkins/cli/src/main/java/hudson/cli/CLIConnectionFactory.java

@@ -0,0 +1,58 @@
+package hudson.cli;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Fluent-API to instantiate {@link CLI}.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class CLIConnectionFactory {
+    String authorization;
+    boolean noCertificateCheck;
+
+    /**
+     * For CLI connection that goes through HTTP, sometimes you need
+     * to pass in the custom authentication header (before Jenkins even get to authenticate
+     * the CLI channel.) This method lets you specify the value of this header.
+     */
+    public CLIConnectionFactory authorization(String value) {
+        this.authorization = value;
+        return this;
+    }
+
+    /**
+     * Skip TLS certificate and hostname verification checks.
+     *
+     * @since 2.444
+     */
+    public CLIConnectionFactory noCertificateCheck(boolean value) {
+        this.noCertificateCheck = value;
+        return this;
+    }
+
+    /**
+     * Convenience method to call {@link #authorization} with the HTTP basic authentication.
+     * Currently unused.
+     */
+    public CLIConnectionFactory basicAuth(String username, String password) {
+        return basicAuth(username + ':' + password);
+    }
+
+    /**
+     * Convenience method to call {@link #authorization} with the HTTP basic authentication.
+     * Cf. {@code BasicHeaderApiTokenAuthenticator}.
+     */
+    public CLIConnectionFactory basicAuth(String userInfo) {
+        return authorization("Basic " + Base64.getEncoder().encodeToString(userInfo.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    /**
+     * Convenience method to call {@link #authorization} with the HTTP bearer authentication.
+     * Cf. {@code BasicHeaderApiTokenAuthenticator}.
+     */
+    public CLIConnectionFactory bearerAuth(String bearerToken) {
+        return authorization("Bearer " + bearerToken);
+    }
+}

+ 57 - 0
jenkins/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java

@@ -0,0 +1,57 @@
+package hudson.cli;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.io.PrintWriter;
+import java.io.StreamCorruptedException;
+import java.io.StringWriter;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * Signals a {@link StreamCorruptedException} with some additional diagnostic information.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class DiagnosedStreamCorruptionException extends StreamCorruptedException {
+    private final Exception diagnoseFailure;
+    private final byte[] readBack;
+    private final byte[] readAhead;
+
+    DiagnosedStreamCorruptionException(Exception cause, Exception diagnoseFailure, byte[] readBack, byte[] readAhead) {
+        initCause(cause);
+        this.diagnoseFailure = diagnoseFailure;
+        this.readBack = readBack;
+        this.readAhead = readAhead;
+    }
+
+    public Exception getDiagnoseFailure() {
+        return diagnoseFailure;
+    }
+
+    public byte[] getReadBack() {
+        return readBack;
+    }
+
+    public byte[] getReadAhead() {
+        return readAhead;
+    }
+
+    @Override
+    @SuppressFBWarnings(value = "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE", justification = "Jenkins handles this issue differently or doesn't care about it")
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append(super.toString()).append("\n");
+        buf.append("Read back: ").append(HexDump.toHex(readBack)).append('\n');
+        buf.append("Read ahead: ").append(HexDump.toHex(readAhead));
+        if (diagnoseFailure != null) {
+            StringWriter w = new StringWriter();
+            PrintWriter p = new PrintWriter(w);
+            diagnoseFailure.printStackTrace(p);
+            p.flush();
+
+            buf.append("\nDiagnosis problem:\n    ");
+            buf.append(w.toString().trim().replace("\n", "\n    "));
+        }
+        return buf.toString();
+    }
+}

+ 193 - 0
jenkins/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java

@@ -0,0 +1,193 @@
+package hudson.cli;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * Filter input stream that records the content as it's read, so that it can be reported
+ * in case of a catastrophic stream corruption problem.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class FlightRecorderInputStream extends InputStream {
+
+    /**
+     * Size (in bytes) of the flight recorder ring buffer used for debugging remoting issues.
+     * @since 2.41
+     */
+    static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024);
+
+    private final InputStream source;
+    private ByteArrayRingBuffer recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
+
+    FlightRecorderInputStream(InputStream source) {
+        this.source = source;
+    }
+
+    /**
+     * Rewinds the record buffer and forget everything that was recorded.
+     */
+    public void clear() {
+        recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
+    }
+
+    /**
+     * Gets the recorded content.
+     */
+    public byte[] getRecord() {
+        return recorder.toByteArray();
+    }
+
+    /**
+     * Creates a {@link DiagnosedStreamCorruptionException} based on the recorded content plus read ahead.
+     * The caller is responsible for throwing the exception.
+     */
+    public DiagnosedStreamCorruptionException analyzeCrash(Exception problem, String diagnosisName) {
+        final ByteArrayOutputStream readAhead = new ByteArrayOutputStream();
+        final IOException[] error = new IOException[1];
+
+        Thread diagnosisThread = new Thread(diagnosisName + " stream corruption diagnosis thread") {
+            @Override
+            public void run() {
+                int b;
+                try {
+                    // not all InputStream will look for the thread interrupt flag, so check that explicitly to be defensive
+                    while (!Thread.interrupted() && (b = source.read()) != -1) {
+                        readAhead.write(b);
+                    }
+                } catch (IOException e) {
+                    error[0] = e;
+                }
+            }
+        };
+
+        // wait up to 1 sec to grab as much data as possible
+        diagnosisThread.start();
+        try {
+            diagnosisThread.join(1000);
+        } catch (InterruptedException ignored) {
+            // we are only waiting for a fixed amount of time, so we'll pretend like we were in a busy loop
+            Thread.currentThread().interrupt();
+            // fall through
+        }
+
+        IOException diagnosisProblem = error[0]; // capture the error, if any, before we kill the thread
+        if (diagnosisThread.isAlive())
+            diagnosisThread.interrupt();    // if it's not dead, kill
+
+        return new DiagnosedStreamCorruptionException(problem, diagnosisProblem, getRecord(), readAhead.toByteArray());
+
+    }
+
+    @Override
+    public int read() throws IOException {
+        int i = source.read();
+        if (i >= 0)
+            recorder.write(i);
+        return i;
+    }
+
+    @Override
+    public int read(@NonNull byte[] b, int off, int len) throws IOException {
+        len = source.read(b, off, len);
+        if (len > 0)
+            recorder.write(b, off, len);
+        return len;
+    }
+
+    /**
+     * To record the bytes we've skipped, convert the call to read.
+     */
+    @Override
+    public long skip(long n) throws IOException {
+        byte[] buf = new byte[(int) Math.min(n, 64 * 1024)];
+        return read(buf, 0, buf.length);
+    }
+
+    @Override
+    public int available() throws IOException {
+        return source.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+        source.close();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    // http://stackoverflow.com/a/3651696/12916
+    private static class ByteArrayRingBuffer extends OutputStream {
+
+        byte[] data;
+
+        int capacity, pos = 0;
+
+        boolean filled = false;
+
+        ByteArrayRingBuffer(int capacity) {
+            data = new byte[capacity];
+            this.capacity = capacity;
+        }
+
+        @Override
+        public synchronized void write(int b) {
+            if (pos == capacity) {
+                filled = true;
+                pos = 0;
+            }
+            data[pos++] = (byte) b;
+        }
+
+        public synchronized byte[] toByteArray() {
+            if (!filled) {
+                return Arrays.copyOf(data, pos);
+            }
+            byte[] ret = new byte[capacity];
+            System.arraycopy(data, pos, ret, 0, capacity - pos);
+            System.arraycopy(data, 0, ret, capacity - pos, pos);
+            return ret;
+        }
+
+        /** @author @roadrunner2 */
+        @Override public synchronized void write(@NonNull byte[] buf, int off, int len) {
+            // no point in trying to copy more than capacity; this also simplifies logic below
+            if (len > capacity) {
+                off += len - capacity;
+                len = capacity;
+            }
+
+            // copy to buffer, but no farther than the end
+            int num = Math.min(len, capacity - pos);
+            if (num > 0) {
+                System.arraycopy(buf, off, data, pos, num);
+                off += num;
+                len -= num;
+                pos += num;
+            }
+
+            // wrap around if necessary
+            if (pos == capacity) {
+                filled = true;
+                pos = 0;
+            }
+
+            // copy anything still left
+            if (len > 0) {
+                System.arraycopy(buf, off, data, pos, len);
+                pos += len;
+            }
+        }
+
+    }
+
+}

+ 121 - 0
jenkins/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java

@@ -0,0 +1,121 @@
+package hudson.cli;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Creates a capacity-unlimited bi-directional {@link InputStream}/{@link OutputStream} pair over
+ * HTTP, which is a request/response protocol.
+ * {@code FullDuplexHttpService} is the counterpart on the server side.
+ * @author Kohsuke Kawaguchi
+ */
+public class FullDuplexHttpStream {
+    private final URL base;
+
+    private final OutputStream output;
+    private final InputStream input;
+
+    /**
+     * Get data from the server.
+     * An initial zero byte is used as a handshake which you should expect and ignore.
+     */
+    public InputStream getInputStream() {
+        return input;
+    }
+
+    /**
+     * Upload data to the server.
+     * You will need to write to this and {@link OutputStream#flush} it to establish a connection.
+     */
+    public OutputStream getOutputStream() {
+        return output;
+    }
+
+    /**
+     * @param base the base URL of Jenkins.
+     * @param relativeTarget
+     *      The endpoint that we are making requests to.
+     * @param authorization
+     *      The value of the authorization header.
+     */
+    public FullDuplexHttpStream(URL base, String relativeTarget, String authorization) throws IOException {
+        if (!base.toString().endsWith("/")) {
+            throw new IllegalArgumentException(base.toString());
+        }
+        if (relativeTarget.startsWith("/")) {
+            throw new IllegalArgumentException(relativeTarget);
+        }
+
+        this.base = tryToResolveRedirects(base, authorization);
+
+        URL target = new URL(this.base, relativeTarget);
+
+        UUID uuid = UUID.randomUUID(); // so that the server can correlate those two connections
+
+        // server->client
+        LOGGER.fine("establishing download side");
+        HttpURLConnection con = openHttpConnection(target);
+        con.setDoOutput(true); // request POST to avoid caching
+        con.setRequestMethod("POST");
+        con.addRequestProperty("Session", uuid.toString());
+        con.addRequestProperty("Side", "download");
+        if (authorization != null) {
+            con.addRequestProperty("Authorization", authorization);
+        }
+        con.getOutputStream().close();
+        input = con.getInputStream();
+        // make sure we hit the right URL; no need for CLI.verifyJenkinsConnection here
+        if (con.getHeaderField("Hudson-Duplex") == null) {
+            throw new CLI.NotTalkingToJenkinsException("There's no Jenkins running at " + target + ", or is not serving the HTTP Duplex transport");
+        }
+        LOGGER.fine("established download side"); // calling getResponseCode or getHeaderFields fails
+
+        // client->server uses chunked encoded POST for unlimited capacity.
+        LOGGER.fine("establishing upload side");
+        con = openHttpConnection(target);
+        con.setDoOutput(true); // request POST
+        con.setRequestMethod("POST");
+        con.setChunkedStreamingMode(0);
+        con.setRequestProperty("Content-type", "application/octet-stream");
+        con.addRequestProperty("Session", uuid.toString());
+        con.addRequestProperty("Side", "upload");
+        if (authorization != null) {
+            con.addRequestProperty("Authorization", authorization);
+        }
+        output = con.getOutputStream();
+        LOGGER.fine("established upload side");
+    }
+
+    @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD", justification = "Client-side code doesn't involve SSRF.")
+    private HttpURLConnection openHttpConnection(URL target) throws IOException {
+        return (HttpURLConnection) target.openConnection();
+    }
+
+    // As this transport mode is using POST, it is necessary to resolve possible redirections using GET first.
+    private URL tryToResolveRedirects(URL base, String authorization) {
+        try {
+            HttpURLConnection con = openHttpConnection(base);
+            if (authorization != null) {
+                con.addRequestProperty("Authorization", authorization);
+            }
+            con.getInputStream().close();
+            base = con.getURL();
+        } catch (Exception ex) {
+            // If the exception is not real (permission for CLI connection but not for UI) do not inform user.
+            // Otherwise, the error will be reported during the exchange.
+            LOGGER.log(Level.FINE, "Failed to resolve potential redirects", ex);
+        }
+        return base;
+    }
+
+    static final int BLOCK_SIZE = 1024;
+    static final Logger LOGGER = Logger.getLogger(FullDuplexHttpStream.class.getName());
+
+}

+ 48 - 0
jenkins/cli/src/main/java/hudson/cli/HexDump.java

@@ -0,0 +1,48 @@
+package hudson.cli;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+class HexDump {
+    private static final String CODE = "0123456789abcdef";
+
+    public static String toHex(byte[] buf) {
+        return toHex(buf, 0, buf.length);
+    }
+
+    public static String toHex(byte[] buf, int start, int len) {
+        StringBuilder r = new StringBuilder(len * 2);
+        boolean inText = false;
+        for (int i = 0; i < len; i++) {
+            byte b = buf[start + i];
+            if (b >= 0x20 && b <= 0x7e) {
+                if (!inText) {
+                    inText = true;
+                    r.append('\'');
+                }
+                r.append((char) b);
+            } else {
+                if (inText) {
+                    r.append("' ");
+                    inText = false;
+                }
+                r.append("0x");
+                r.append(CODE.charAt((b >> 4) & 15));
+                r.append(CODE.charAt(b & 15));
+                if (i < len - 1) {
+                    if (b == 10) {
+                        r.append('\n');
+                    } else {
+                        r.append(' ');
+                    }
+                }
+            }
+        }
+        if (inText) {
+            r.append('\'');
+        }
+        return r.toString();
+    }
+}

+ 27 - 0
jenkins/cli/src/main/java/hudson/cli/NoCheckTrustManager.java

@@ -0,0 +1,27 @@
+package hudson.cli;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class NoCheckTrustManager implements X509TrustManager {
+    @Override
+    @SuppressFBWarnings(value = "WEAK_TRUST_MANAGER", justification = "User set parameter to skip verifier.")
+    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+    }
+
+    @Override
+    @SuppressFBWarnings(value = "WEAK_TRUST_MANAGER", justification = "User set parameter to skip verifier.")
+    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+    }
+
+    @Override
+    @SuppressFBWarnings(value = "WEAK_TRUST_MANAGER", justification = "User set parameter to skip verifier.")
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}

+ 381 - 0
jenkins/cli/src/main/java/hudson/cli/PlainCLIProtocol.java

@@ -0,0 +1,381 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 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.
+ */
+
+package hudson.cli;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ReadPendingException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.BoundedInputStream;
+import org.apache.commons.io.input.CountingInputStream;
+
+/**
+ * CLI protocol working over a plain socket-like connection, without SSH or Remoting.
+ * Each side consists of frames starting with an {@code int} length,
+ * then a {@code byte} opcode, then any opcode-specific data.
+ * The length does not count the length field itself nor the opcode, so it is nonnegative.
+ */
+class PlainCLIProtocol {
+
+    static final Logger LOGGER = Logger.getLogger(PlainCLIProtocol.class.getName());
+
+    /** One-byte operation to send to the other side. */
+    private enum Op {
+        /** UTF-8 command name or argument. */
+        ARG(true),
+        /** UTF-8 locale identifier. */
+        LOCALE(true),
+        /** UTF-8 client encoding. */
+        ENCODING(true),
+        /** Start running command. */
+        START(true),
+        /** Exit code, as int. */
+        EXIT(false),
+        /** Chunk of stdin, as int length followed by bytes. */
+        STDIN(true),
+        /** EOF on stdin. */
+        END_STDIN(true),
+        /** Chunk of stdout. */
+        STDOUT(false),
+        /** Chunk of stderr. */
+        STDERR(false);
+        /** True if sent from the client to the server; false if sent from the server to the client. */
+        final boolean clientSide;
+
+        Op(boolean clientSide) {
+            this.clientSide = clientSide;
+        }
+    }
+
+    interface Output extends Closeable {
+        void send(byte[] data) throws IOException;
+    }
+
+    static final class FramedOutput implements Output {
+
+        private final DataOutputStream dos;
+
+        FramedOutput(OutputStream os) {
+            dos = new DataOutputStream(os);
+        }
+
+        @Override
+        public void send(byte[] data) throws IOException {
+            dos.writeInt(data.length - 1); // not counting the opcode
+            dos.write(data);
+            dos.flush();
+        }
+
+        @Override
+        public void close() throws IOException {
+            dos.close();
+        }
+
+    }
+
+    static final class FramedReader extends Thread {
+
+        private final EitherSide side;
+        private final CountingInputStream cis;
+        private final FlightRecorderInputStream flightRecorder;
+        private final DataInputStream dis;
+
+        FramedReader(EitherSide side, InputStream is) {
+            super("PlainCLIProtocol"); // TODO set distinctive Thread.name
+            this.side = side;
+            cis = new CountingInputStream(is);
+            flightRecorder = new FlightRecorderInputStream(cis);
+            dis = new DataInputStream(flightRecorder);
+        }
+
+        @Override
+        public void run() {
+            try {
+                while (true) {
+                    LOGGER.finest("reading frame");
+                    int framelen;
+                    try {
+                        framelen = dis.readInt();
+                    } catch (EOFException x) {
+                        side.handleClose();
+                        break; // TODO verify that we hit EOF immediately, not partway into framelen
+                    }
+                    if (framelen < 0) {
+                        throw new IOException("corrupt stream: negative frame length");
+                    }
+                    LOGGER.finest("read frame length " + framelen);
+                    long start = cis.getByteCount();
+                    try {
+                        side.handle(new DataInputStream(new BoundedInputStream(dis, /* op byte not counted */framelen + 1)));
+                    } catch (ProtocolException x) {
+                        LOGGER.log(Level.WARNING, null, x);
+                        // but read another frame
+                    } finally {
+                        long actuallyRead = cis.getByteCount() - start;
+                        long unread = framelen + 1 - actuallyRead;
+                        if (unread > 0) {
+                            LOGGER.warning(() -> "Did not read " + unread + " bytes");
+                            IOUtils.skipFully(dis, unread);
+                        }
+                    }
+                }
+            } catch (ClosedChannelException x) {
+                LOGGER.log(Level.FINE, null, x);
+            } catch (IOException x) {
+                LOGGER.log(Level.WARNING, null, flightRecorder.analyzeCrash(x, "broken stream"));
+            } catch (ReadPendingException x) {
+                // in case trick in CLIAction does not work
+                LOGGER.log(Level.FINE, null, x);
+            } catch (RuntimeException x) {
+                LOGGER.log(Level.WARNING, null, x);
+            } finally {
+                side.handleClose();
+            }
+        }
+
+    }
+
+    private static final class ProtocolException extends IOException {
+        ProtocolException(String message) {
+            super(message);
+        }
+    }
+
+    abstract static class EitherSide implements Closeable {
+
+        private final Output out;
+
+        protected EitherSide(Output out) {
+            this.out = out;
+        }
+
+        protected abstract void handleClose();
+
+        final void handle(DataInputStream dis) throws IOException {
+            byte b = dis.readByte();
+            if (b < 0) { // i.e., >127
+                throw new IOException("corrupt stream: negative operation code");
+            }
+            if (b >= Op.values().length) {
+                throw new ProtocolException("unknown operation #" + b);
+            }
+            Op op = Op.values()[b];
+            LOGGER.finest(() -> "handling frame with " + op);
+            if (!handle(op, dis)) {
+                throw new ProtocolException("unhandled: " + op);
+            }
+        }
+
+        protected abstract boolean handle(Op op, DataInputStream dis) throws IOException;
+
+        protected final synchronized void send(Op op) throws IOException {
+            send(op, new byte[0], 0, 0);
+        }
+
+        protected final synchronized void send(Op op, int v) throws IOException {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(4);
+            new DataOutputStream(baos).writeInt(v);
+            send(op, baos.toByteArray());
+        }
+
+        protected final synchronized void send(Op op, byte[] chunk, int off, int len) throws IOException {
+            byte[] data = new byte[len + 1];
+            data[0] = (byte) op.ordinal();
+            System.arraycopy(chunk, off, data, 1, len);
+            out.send(data);
+        }
+
+        protected final void send(Op op, byte[] chunk) throws IOException {
+            send(op, chunk, 0, chunk.length);
+        }
+
+        protected final void send(Op op, String text) throws IOException {
+            ByteArrayOutputStream buf = new ByteArrayOutputStream();
+            new DataOutputStream(buf).writeUTF(text);
+            send(op, buf.toByteArray());
+        }
+
+        protected final OutputStream stream(final Op op) {
+            return new OutputStream() {
+                @Override
+                public void write(int b) throws IOException {
+                    send(op, new byte[] {(byte) b});
+                }
+
+                @Override
+                public void write(@NonNull byte[] b, int off, int len) throws IOException {
+                    send(op, b, off, len);
+                }
+
+                @Override
+                public void write(@NonNull byte[] b) throws IOException {
+                    send(op, b);
+                }
+            };
+        }
+
+        @Override
+        public synchronized void close() throws IOException {
+            out.close();
+        }
+
+    }
+
+    abstract static class ServerSide extends EitherSide {
+
+        ServerSide(Output out) {
+            super(out);
+        }
+
+        @Override
+        protected final boolean handle(Op op, DataInputStream dis) throws IOException {
+            assert op.clientSide;
+            return switch (op) {
+                case ARG -> {
+                    onArg(dis.readUTF());
+                    yield true;
+                }
+                case LOCALE -> {
+                    onLocale(dis.readUTF());
+                    yield true;
+                }
+                case ENCODING -> {
+                    onEncoding(dis.readUTF());
+                    yield true;
+                }
+                case START -> {
+                    onStart();
+                    yield true;
+                }
+                case STDIN -> {
+                    onStdin(dis.readAllBytes());
+                    yield true;
+                }
+                case END_STDIN -> {
+                    onEndStdin();
+                    yield true;
+                }
+                default -> false;
+            };
+        }
+
+        protected abstract void onArg(String text);
+
+        protected abstract void onLocale(String text);
+
+        protected abstract void onEncoding(String text);
+
+        protected abstract void onStart();
+
+        protected abstract void onStdin(byte[] chunk) throws IOException;
+
+        protected abstract void onEndStdin() throws IOException;
+
+        public final void sendExit(int code) throws IOException {
+            send(Op.EXIT, code);
+        }
+
+        public final OutputStream streamStdout() {
+            return stream(Op.STDOUT);
+        }
+
+        public final OutputStream streamStderr() {
+            return stream(Op.STDERR);
+        }
+
+    }
+
+    abstract static class ClientSide extends EitherSide {
+
+        ClientSide(Output out) {
+            super(out);
+        }
+
+        @Override
+        protected boolean handle(Op op, DataInputStream dis) throws IOException {
+            assert !op.clientSide;
+            return switch (op) {
+                case EXIT -> {
+                    onExit(dis.readInt());
+                    yield true;
+                }
+                case STDOUT -> {
+                    onStdout(dis.readAllBytes());
+                    yield true;
+                }
+                case STDERR -> {
+                    onStderr(dis.readAllBytes());
+                    yield true;
+                }
+                default -> false;
+            };
+        }
+
+        protected abstract void onExit(int code);
+
+        // TODO more efficient to change signature to InputStream, then use IOUtils.copy
+        protected abstract void onStdout(byte[] chunk) throws IOException;
+
+        protected abstract void onStderr(byte[] chunk) throws IOException;
+
+        public final void sendArg(String text) throws IOException {
+            send(Op.ARG, text);
+        }
+
+        public final void sendLocale(String text) throws IOException {
+            send(Op.LOCALE, text);
+        }
+
+        public final void sendEncoding(String text) throws IOException {
+            send(Op.ENCODING, text);
+        }
+
+        public final void sendStart() throws IOException {
+            send(Op.START);
+        }
+
+        public final OutputStream streamStdin() {
+            return stream(Op.STDIN);
+        }
+
+        public final void sendEndStdin() throws IOException {
+            send(Op.END_STDIN);
+        }
+
+    }
+
+    private PlainCLIProtocol() {}
+
+}

+ 160 - 0
jenkins/cli/src/main/java/hudson/cli/PrivateKeyProvider.java

@@ -0,0 +1,160 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2014 Red Hat, 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.
+ */
+
+package hudson.cli;
+
+import static java.util.logging.Level.FINE;
+
+import java.io.ByteArrayInputStream;
+import java.io.Console;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.stream.StreamSupport;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.util.io.resource.PathResource;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * Read an SSH private key (RSA, DSA, ECDSA or Ed25519) from file(s) asking for password interactively.
+ *
+ * @author ogondza
+ * @since 1.556
+ */
+public class PrivateKeyProvider {
+
+    private final List<KeyPair> privateKeys = new ArrayList<>();
+
+    /**
+     * Get keys read so far.
+     *
+     * @return Possibly empty list. Never null.
+     */
+    public List<KeyPair> getKeys() {
+        return Collections.unmodifiableList(privateKeys);
+    }
+
+    public boolean hasKeys() {
+        return !privateKeys.isEmpty();
+    }
+
+    /**
+     * Read keys from default keyFiles
+     *
+     * {@code .ssh/id_rsa}, {@code .ssh/id_ecdsa}, {@code .ssh/id_ed25519}, {@code .ssh/id_dsa} and {@code .ssh/identity}.
+     *
+     * @return true if some key was read successfully.
+     */
+    public boolean readFromDefaultLocations() {
+        final File home = new File(System.getProperty("user.home"));
+
+        boolean read = false;
+        for (String path : new String[] {".ssh/id_rsa", ".ssh/id_ecdsa", ".ssh/id_ed25519", ".ssh/id_dsa", ".ssh/identity"}) {
+            final File key = new File(home, path);
+            if (!key.exists()) continue;
+
+            try {
+
+                readFrom(key);
+                read = true;
+            } catch (IOException | GeneralSecurityException e) {
+
+                LOGGER.log(FINE, "Failed to load " + key, e);
+            }
+        }
+
+        return read;
+    }
+
+    /**
+     * Read key from keyFile.
+     */
+    public void readFrom(File keyFile) throws IOException, GeneralSecurityException {
+        final String password = isPemEncrypted(keyFile)
+                ? askForPasswd(keyFile.getCanonicalPath())
+                : null
+        ;
+        privateKeys.add(loadKey(keyFile, password));
+    }
+
+    private static boolean isPemEncrypted(File f) throws IOException {
+        //simple check if the file is encrypted
+        return readPemFile(f).contains("4,ENCRYPTED");
+    }
+
+    private static String askForPasswd(String filePath) {
+        Console cons = System.console();
+        String passwd = null;
+        if (cons != null) {
+            char[] p = cons.readPassword("%s", "Enter passphrase for " + filePath + ":");
+            passwd = String.valueOf(p);
+        }
+        return passwd;
+    }
+
+    public static KeyPair loadKey(File f, String passwd) throws IOException, GeneralSecurityException {
+        return loadKey(readPemFile(f), passwd);
+    }
+
+    private static String readPemFile(File f) throws IOException {
+        try (InputStream is = Files.newInputStream(f.toPath());
+             DataInputStream dis = new DataInputStream(is)) {
+            byte[] bytes = new byte[(int) f.length()];
+            dis.readFully(bytes);
+            return new String(bytes, StandardCharsets.UTF_8);
+        } catch (InvalidPathException e) {
+            throw new IOException(e);
+        }
+    }
+
+    public static KeyPair loadKey(String pemString, String passwd) throws IOException, GeneralSecurityException {
+        Iterable<KeyPair> itr = SecurityUtils.loadKeyPairIdentities(null,
+                new PathResource(Paths.get("key")),
+                new ByteArrayInputStream(pemString.getBytes(StandardCharsets.UTF_8)),
+                FilePasswordProvider.of(passwd));
+        long numLoaded = itr == null ? 0 : StreamSupport.stream(itr.spliterator(), false).count();
+        if (numLoaded <= 0) {
+            throw new InvalidKeyException("Unsupported private key file format: key");
+        }
+        if (numLoaded != 1) {
+            throw new InvalidKeySpecException("Multiple private key pairs N/A: key");
+        }
+        return itr.iterator().next();
+    }
+
+    private static final Logger LOGGER = Logger.getLogger(PrivateKeyProvider.class.getName());
+}

+ 133 - 0
jenkins/cli/src/main/java/hudson/cli/SSHCLI.java

@@ -0,0 +1,133 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 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.
+ */
+
+package hudson.cli;
+
+import static java.util.logging.Level.FINE;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.util.QuotedStringTokenizer;
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.KeyPair;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.future.ConnectFuture;
+import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.future.WaitableFuture;
+import org.apache.sshd.common.util.io.input.NoCloseInputStream;
+import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * Implements SSH connection mode of {@link CLI}.
+ * In a separate class to avoid any class loading of {@code sshd-core} when not using {@code -ssh} mode.
+ * That allows the {@code test} module to pick up a version of {@code sshd-core} from the {@code sshd} module via {@code jenkins-war}
+ * that may not match the version being used from the {@code cli} module and may not be compatible.
+ */
+class SSHCLI {
+
+    static int sshConnection(String jenkinsUrl, String user, List<String> args, PrivateKeyProvider provider, final boolean strictHostKey) throws IOException {
+        Logger.getLogger(SecurityUtils.class.getName()).setLevel(Level.WARNING); // suppress: BouncyCastle not registered, using the default JCE provider
+        URL url = new URL(jenkinsUrl + "login");
+        URLConnection conn = openConnection(url);
+        CLI.verifyJenkinsConnection(conn);
+        String endpointDescription = conn.getHeaderField("X-SSH-Endpoint");
+
+        if (endpointDescription == null) {
+            CLI.LOGGER.warning("No header 'X-SSH-Endpoint' returned by Jenkins");
+            return -1;
+        }
+
+        CLI.LOGGER.log(FINE, "Connecting via SSH to: {0}", endpointDescription);
+
+        int sshPort = Integer.parseInt(endpointDescription.split(":")[1]);
+        String sshHost = endpointDescription.split(":")[0];
+
+        StringBuilder command = new StringBuilder();
+
+        for (String arg : args) {
+            command.append(QuotedStringTokenizer.quote(arg));
+            command.append(' ');
+        }
+
+        try (SshClient client = SshClient.setUpDefaultClient()) {
+
+            KnownHostsServerKeyVerifier verifier = new DefaultKnownHostsServerKeyVerifier((clientSession, remoteAddress, serverKey) -> {
+                CLI.LOGGER.log(Level.WARNING, "Unknown host key for {0}", remoteAddress.toString());
+                return !strictHostKey;
+            }, true);
+
+            client.setServerKeyVerifier(verifier);
+            client.start();
+
+            ConnectFuture cf = client.connect(user, sshHost, sshPort);
+            cf.await();
+            try (ClientSession session = cf.getSession()) {
+                for (KeyPair pair : provider.getKeys()) {
+                    CLI.LOGGER.log(FINE, "Offering {0} private key", pair.getPrivate().getAlgorithm());
+                    session.addPublicKeyIdentity(pair);
+                }
+                session.auth().verify(10000L);
+
+                try (ClientChannel channel = session.createExecChannel(command.toString())) {
+                    channel.setIn(new NoCloseInputStream(System.in));
+                    channel.setOut(new NoCloseOutputStream(System.out));
+                    channel.setErr(new NoCloseOutputStream(System.err));
+                    WaitableFuture wf = channel.open();
+                    wf.await();
+
+                    Set<ClientChannelEvent> waitMask = channel.waitFor(List.of(ClientChannelEvent.CLOSED), 0L);
+
+                    if (waitMask.contains(ClientChannelEvent.TIMEOUT)) {
+                        throw new SocketTimeoutException("Failed to retrieve command result in time: " + command);
+                    }
+
+                    Integer exitStatus = channel.getExitStatus();
+                    return exitStatus;
+
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD", justification = "Client-side code doesn't involve SSRF.")
+    private static URLConnection openConnection(URL url) throws IOException {
+        return url.openConnection();
+    }
+
+    private SSHCLI() {}
+
+}

+ 562 - 0
jenkins/cli/src/main/java/hudson/util/QuotedStringTokenizer.java

@@ -0,0 +1,562 @@
+/*
+ * (C) Copyright 2004-2005 Mort Bay Consulting Pty. Ltd.
+ *
+ *   Parts of this code was taken from the Jetty project, which can be
+ *   found at http://www.mortbay.org/jetty
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ========================================================================
+// Copyright 2004-2005 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+
+package hudson.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+/* ------------------------------------------------------------ */
+/** StringTokenizer with Quoting support.
+ *
+ * This class is a copy of the java.util.StringTokenizer API and
+ * the behaviour is the same, except that single and double quoted
+ * string values are recognized.
+ * Delimiters within quotes are not considered delimiters.
+ * Quotes can be escaped with '\'.
+ *
+ * @see java.util.StringTokenizer
+ * @author Greg Wilkins (gregw)
+ */
+public class QuotedStringTokenizer
+    extends StringTokenizer
+{
+    private static final String __delim = " \t\n\r";
+    private String _string;
+    private String _delim = __delim;
+    private boolean _returnQuotes = false;
+    private boolean _returnDelimiters = false;
+    private StringBuilder _token;
+    private boolean _hasToken = false;
+    private int _i = 0;
+    private int _lastStart = 0;
+    private boolean _double = true;
+    private boolean _single = true;
+
+    public static String[] tokenize(String str) {
+        return new QuotedStringTokenizer(str).toArray();
+    }
+
+    public static String[] tokenize(String str, String delimiters) {
+        return new QuotedStringTokenizer(str, delimiters).toArray();
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     *
+     * @param str
+     *      String to tokenize.
+     * @param delim
+     *      List of delimiter characters as string. Can be null, to default to ' \t\n\r'
+     * @param returnDelimiters
+     *      If true, {@link #nextToken()} will include the delimiters, not just tokenized
+     *      tokens.
+     * @param returnQuotes
+     *      If true, {@link #nextToken()} will include the quotation characters when they are present.
+     */
+    public QuotedStringTokenizer(String str,
+                                 String delim,
+                                 boolean returnDelimiters,
+                                 boolean returnQuotes)
+    {
+        super("");
+        _string = str;
+        if (delim != null)
+            _delim = delim;
+        _returnDelimiters = returnDelimiters;
+        _returnQuotes = returnQuotes;
+
+        if (_delim.indexOf('\'') >= 0 ||
+            _delim.indexOf('"') >= 0)
+            throw new Error("Can't use quotes as delimiters: " + _delim);
+
+        _token = new StringBuilder(_string.length() > 1024 ? 512 : _string.length() / 2);
+    }
+
+    /* ------------------------------------------------------------ */
+    public QuotedStringTokenizer(String str,
+                                 String delim,
+                                 boolean returnDelimiters)
+    {
+        this(str, delim, returnDelimiters, false);
+    }
+
+    /* ------------------------------------------------------------ */
+    public QuotedStringTokenizer(String str,
+                                 String delim)
+    {
+        this(str, delim, false, false);
+    }
+
+    /* ------------------------------------------------------------ */
+    public QuotedStringTokenizer(String str)
+    {
+        this(str, null, false, false);
+    }
+
+    public String[] toArray() {
+        List<String> r = new ArrayList<>();
+        while (hasMoreTokens())
+            r.add(nextToken());
+        return r.toArray(new String[r.size()]);
+    }
+
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public boolean hasMoreTokens()
+    {
+        // Already found a token
+        if (_hasToken)
+            return true;
+
+        _lastStart = _i;
+
+        int state = 0;
+        boolean escape = false;
+        while (_i < _string.length())
+        {
+            char c = _string.charAt(_i++);
+
+            switch (state)
+            {
+              case 0: // Start
+                  if (_delim.indexOf(c) >= 0)
+                  {
+                      if (_returnDelimiters)
+                      {
+                          _token.append(c);
+                          return _hasToken = true;
+                      }
+                  }
+                  else if (c == '\'' && _single)
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      state = 2;
+                  }
+                  else if (c == '\"' && _double)
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      state = 3;
+                  }
+                  else
+                  {
+                      _token.append(c);
+                      _hasToken = true;
+                      state = 1;
+                  }
+                  continue;
+
+              case 1: // Token
+                  _hasToken = true;
+                  if (escape)
+                  {
+                      escape = false;
+                      if (ESCAPABLE_CHARS.indexOf(c) < 0)
+                          _token.append('\\');
+                      _token.append(c);
+                  }
+                  else if (_delim.indexOf(c) >= 0)
+                  {
+                      if (_returnDelimiters)
+                          _i--;
+                      return _hasToken;
+                  }
+                  else if (c == '\'' && _single)
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      state = 2;
+                  }
+                  else if (c == '\"' && _double)
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      state = 3;
+                  }
+                  else if (c == '\\')
+                  {
+                      escape = true;
+                  }
+                  else
+                      _token.append(c);
+                  continue;
+
+
+              case 2: // Single Quote
+                  _hasToken = true;
+                  if (escape)
+                  {
+                      escape = false;
+                      if (ESCAPABLE_CHARS.indexOf(c) < 0)
+                          _token.append('\\');
+                      _token.append(c);
+                  }
+                  else if (c == '\'')
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      state = 1;
+                  }
+                  else if (c == '\\')
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      escape = true;
+                  }
+                  else
+                      _token.append(c);
+                  continue;
+
+
+              case 3: // Double Quote
+                  _hasToken = true;
+                  if (escape)
+                  {
+                      escape = false;
+                      if (ESCAPABLE_CHARS.indexOf(c) < 0)
+                          _token.append('\\');
+                      _token.append(c);
+                  }
+                  else if (c == '\"')
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      state = 1;
+                  }
+                  else if (c == '\\')
+                  {
+                      if (_returnQuotes)
+                          _token.append(c);
+                      escape = true;
+                  }
+                  else
+                      _token.append(c);
+                  continue;
+
+              default:
+                  break;
+            }
+        }
+
+        return _hasToken;
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public String nextToken()
+        throws NoSuchElementException
+    {
+        if (!hasMoreTokens() || _token == null)
+            throw new NoSuchElementException();
+        String t = _token.toString();
+        _token.setLength(0);
+        _hasToken = false;
+        return t;
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public String nextToken(String delim)
+        throws NoSuchElementException
+    {
+        _delim = delim;
+        _i = _lastStart;
+        _token.setLength(0);
+        _hasToken = false;
+        return nextToken();
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public boolean hasMoreElements()
+    {
+        return hasMoreTokens();
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public Object nextElement()
+        throws NoSuchElementException
+    {
+        return nextToken();
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Not implemented.
+     */
+    @Override
+    public int countTokens()
+    {
+        return -1;
+    }
+
+
+    /* ------------------------------------------------------------ */
+    /** Quote a string.
+     * The string is quoted only if quoting is required due to
+     * embedded delimiters, quote characters or the
+     * empty string.
+     * @param s The string to quote.
+     * @return quoted string
+     */
+    public static String quote(String s, String delim)
+    {
+        if (s == null)
+            return null;
+        if (s.isEmpty())
+            return "\"\"";
+
+
+        for (int i = 0; i < s.length(); i++)
+        {
+            char c = s.charAt(i);
+            if (c == '\\' || c == '"' || c == '\'' || Character.isWhitespace(c) || delim.indexOf(c) >= 0)
+            {
+                StringBuffer b = new StringBuffer(s.length() + 8);
+                quote(b, s);
+                return b.toString();
+            }
+        }
+
+        return s;
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Quote a string.
+     * The string is always quoted, even when quoting is not required.
+     * Use {@link #quote(String, String)} to quote only when required due to
+     * embedded delimiters, quote characters or the empty string.
+     * @param s The string to quote.
+     * @return quoted string
+     */
+    public static String quote(String s)
+    {
+        if (s == null)
+            return null;
+        if (s.isEmpty())
+            return "\"\"";
+
+        StringBuffer b = new StringBuffer(s.length() + 8);
+        quote(b, s);
+        return b.toString();
+
+    }
+
+
+    /* ------------------------------------------------------------ */
+    /** Quote a string into a StringBuffer.
+     * The characters ", \, \n, \r, \t, \f and \b are escaped
+     * @param buf The StringBuffer
+     * @param s The String to quote.
+     */
+    public static void quote(StringBuffer buf, String s)
+    {
+        synchronized (buf)
+        {
+            buf.append('"');
+            for (int i = 0; i < s.length(); i++)
+            {
+                char c = s.charAt(i);
+                switch (c)
+                {
+                    case '"':
+                        buf.append("\\\"");
+                        continue;
+                    case '\\':
+                        buf.append("\\\\");
+                        continue;
+                    case '\n':
+                        buf.append("\\n");
+                        continue;
+                    case '\r':
+                        buf.append("\\r");
+                        continue;
+                    case '\t':
+                        buf.append("\\t");
+                        continue;
+                    case '\f':
+                        buf.append("\\f");
+                        continue;
+                    case '\b':
+                        buf.append("\\b");
+                        continue;
+
+                    default:
+                        buf.append(c);
+                }
+            }
+            buf.append('"');
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Unquote a string.
+     * @param s The string to unquote.
+     * @return unquoted string
+     */
+    public static String unquote(String s)
+    {
+        if (s == null)
+            return null;
+        if (s.length() < 2)
+            return s;
+
+        char first = s.charAt(0);
+        char last = s.charAt(s.length() - 1);
+        if (first != last || (first != '"' && first != '\''))
+            return s;
+
+        StringBuilder b = new StringBuilder(s.length() - 2);
+        boolean escape = false;
+        for (int i = 1; i < s.length() - 1; i++)
+        {
+            char c = s.charAt(i);
+
+            if (escape)
+            {
+                escape = false;
+                switch (c)
+                {
+                    case 'n':
+                        b.append('\n');
+                        break;
+                    case 'r':
+                        b.append('\r');
+                        break;
+                    case 't':
+                        b.append('\t');
+                        break;
+                    case 'f':
+                        b.append('\f');
+                        break;
+                    case 'b':
+                        b.append('\b');
+                        break;
+                    case 'u':
+                        if (i + 4 >= s.length() - 1)
+                            throw new IllegalArgumentException("Incomplete Unicode escape sequence in " + s);
+                        b.append((char) (
+                                (parseHexDigit(s.charAt(++i)) << 12) +
+                                (parseHexDigit(s.charAt(++i)) << 8) +
+                                (parseHexDigit(s.charAt(++i)) << 4) +
+                                parseHexDigit(s.charAt(++i))
+                                )
+                        );
+                        break;
+                    default:
+                        b.append(c);
+                }
+            }
+            else if (c == '\\')
+            {
+                escape = true;
+            }
+            else
+                b.append(c);
+        }
+
+        return b.toString();
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * @return handle double quotes if true
+     */
+    public boolean getDouble()
+    {
+        return _double;
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * @param d handle double quotes if true
+     */
+    public void setDouble(boolean d)
+    {
+        _double = d;
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * @return handle single quotes if true
+     */
+    public boolean getSingle()
+    {
+        return _single;
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * @param single handle single quotes if true
+     */
+    public void setSingle(boolean single)
+    {
+        _single = single;
+    }
+
+    /**
+     * @param b An ASCII encoded character 0-9 a-f A-F
+     * @return The byte value of the character 0-16.
+     */
+    public static byte convertHexDigit(byte b)
+    {
+        if (b >= '0' && b <= '9') return (byte) (b - '0');
+        if (b >= 'a' && b <= 'f') return (byte) (b - 'a' + 10);
+        if (b >= 'A' && b <= 'F') return (byte) (b - 'A' + 10);
+        return 0;
+    }
+
+    private static int parseHexDigit(char c) {
+        int digit = Character.digit(c, 16);
+        if (digit < 0)
+            throw new IllegalArgumentException("Invalid hex digit '" + c + "' in Unicode escape sequence");
+        return digit;
+    }
+
+    /**
+     * Characters that can be escaped with \.
+     *
+     * Others, like, say, \W will be left alone instead of becoming just W.
+     * This is important to keep Hudson behave on Windows, which uses '\' as
+     * the directory separator.
+     */
+    private static final String ESCAPABLE_CHARS = "\\\"' ";
+}

+ 26 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages.properties

@@ -0,0 +1,26 @@
+CLI.Usage=Jenkins CLI\n\
+  Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  Options:\n\
+  \ -s URL              : the server URL (defaults to the JENKINS_URL env var)\n\
+  \ -webSocket          : connect using WebSocket (the default; works well with most reverse proxies; requires Jetty)\n\
+  \ -http               : use a pair of HTTP(S) connections rather than WebSocket\n\
+  \ -ssh                : use SSH protocol rather than WebSocket (requires -user; SSH port must be open on server)\n\
+  \ -i KEY              : SSH private key file used for authentication (for use with -ssh)\n\
+  \ -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\
+  \ -noKeyAuth          : do not try to load the SSH authentication private key. Conflicts with -i\n\
+  \ -user               : specify user (for use with -ssh; must have registered a public key)\n\
+  \ -strictHostKey      : request strict host key checking (for use with -ssh)\n\
+  \ -logger FINE        : enable detailed logging from the client\n\
+  \ -auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);\n\
+  \                                 for use with -http.\n\
+  \                                 Passing credentials by file is recommended.\n\
+  \                                 See https://www.jenkins.io/redirect/cli-http-connection-mode for more info and options.\n\
+  \ -bearer [ TOKEN | @FILE ]     : specify authentication using a bearer token (or load the token from file);\n\
+  \                                 for use with -http. Mutually exclusive with -auth.\n\
+  \                                 Passing credentials by file is recommended.\n\
+  \n\
+  The available commands depend on the server. Run the ''help'' command to \
+  see the list.
+CLI.NoURL=Neither -s nor the JENKINS_URL env var is specified.
+CLI.NoSuchFileExists=No such file exists: {0}
+CLI.BadAuth=The JENKINS_USER_ID and JENKINS_API_TOKEN env vars should be both set or left empty. 

+ 44 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_bg.properties

@@ -0,0 +1,44 @@
+# The MIT License
+#
+# Bulgarian translation: Copyright (c) 2015, 2016, Alexander Shopov <ash@kambanaria.org>
+#
+# 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.
+
+CLI.Usage=\
+ Програма на командния ред на Jenkins\n\
+  Употреба: java -jar jenkins-cli.jar [-s АДРЕС] КОМАНДА [ОПЦИЯ…] АРГУМЕНТ…\n\
+  Опции:\n\
+   -s АДРЕС     : адресът на сървъра (стандартно се взима от променливата на\n\
+                  средата „JENKINS_URL“)\n\
+   -i КЛЮЧ      : частен ключ за SSH за идентификация\n\
+   -p ХОСТ:ПОРТ : хост и порт за сървър-посредник по HTTP за тунел по HTTPS.\n\
+                  За информация: https://www.jenkins.io/redirect/cli-https-proxy-tunnel\n\
+   -noCertificateCheck : без проверка на сертификата за HTTPS (ВНИМАНИЕ!)\n\
+   -noKeyAuth   : без идентификация с частен ключ за SSH, опцията е\n\
+                  несъвместима с опцията „-i“\n\n\
+  Наличните команди зависят от версията на сървъра. За извеждане на списъка\n\
+  им използвайте командата „help“.\n
+CLI.NoURL=\
+ Адресът не е зададен нито чрез опцията „-s“, нито чрез променливата на\
+ средата „JENKINS_URL“.
+CLI.VersionMismatch=\
+ Версиите не съвпадат. Тази програма за команден ред не работи с този сървър\
+ Jenkins.
+CLI.NoSuchFileExists=\
+ Такъв файл не съществува: {0}

+ 28 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_da.properties

@@ -0,0 +1,28 @@
+# The MIT License
+#
+# Copyright (c) 2004-2010, Sun Microsystems, Inc. Kohsuke Kawaguchi. Knud Poulsen.
+#
+# 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.
+
+CLI.VersionMismatch=Versionskonflikt. CLI''en fungerer ikke med denne Hudson server
+CLI.Usage=Jenkins_ CLI\n\
+Brug: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+Tilvalg:\n\
+De tilgængelige kommandoer afhænger af serveren. Kør 'help' kommandoen for at se listen.
+CLI.NoURL=Hverken -s eller JENKINS_URL miljøvariablen er defineret

+ 14 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_de.properties

@@ -0,0 +1,14 @@
+#Tue Mar 05 22:28:46 CET 2013
+CLI.Usage=Jenkins Kommandozeilenschnittstelle (Jenkins CLI)\n\
+  Verwendung: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  Optionen:\n\
+  -s URL              : URL des Jenkins-Servers (Wert der Umgebungsvariable JENKINS_URL ist der Vorgabewert)\n\
+  -i KEY              : Datei mit privatem SSH-Schlüssel zur Authentisierung\n\
+  -p HOST\:PORT       : HTTP-Proxy-Host und -Port für HTTPS-Proxy-Tunnel. Siehe https://www.jenkins.io/redirect/cli-https-proxy-tunnel\n\
+  -noCertificateCheck : Überspringt die Zertifikatsprüfung bei HTTPS. Bitte mit Vorsicht einsetzen.\n\
+  -noKeyAuth          : Überspringt die Authentifizierung mit einem privaten SSH-Schlüssel. Nicht kombinierbar mit -i\n\
+  \n\
+  Die verfügbaren Kommandos hängen vom kontaktierten Server ab. Verwenden Sie das Kommando help, um eine Liste aller verfügbaren Kommandos anzuzeigen.
+CLI.NoURL=Weder die Option -s noch eine Umgebungsvariable JENKINS_URL wurde spezifiziert.
+CLI.NoSuchFileExists=Diese Datei existiert nicht {0}
+CLI.VersionMismatch=Versionskonflikt: Diese Version von Jenkins CLI ist nicht mit dem kontaktierten Jenkins-Server kompatibel.

+ 24 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_es.properties

@@ -0,0 +1,24 @@
+CLI.Usage=Jenkins CLI\n\
+  Uso: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  Opciones:\n\
+   -s URL       : dirección web (por defecto se usa la variable JENKINS_URL)\n\
+   -http        : usa un protocolo plano sobre HTTP(S) (es el uso por defecto; excluyente con -ssh y -remoting)\n\
+   -ssh         : usa el protocolo SSH (requiere -user; el puerto SSH debe estar abierto en el servidor y el usuario debe tener registrada su clave publica)\n\
+   -remoting    : usa el protocolo deprecado de Remoting (siempre que esté habilitado en el servidor; sólo para compatibilidad con comandos heredados o legacy)\n\
+   -i KEY       : clave privada SSH usada para autenticación (usado con -ssh o -remoting)\n\
+   -p HOST:PORT : host y puerto para el uso de proxy HTTPS. Ver https://www.jenkins.io/redirect/cli-https-proxy-tunnel\n\
+   -noCertificateCheck : elude por completo la verificación del certificado HTTPS. Usar con precaución\n\
+   -noKeyAuth   : intenta no cargar la clave privada de autenticación SSH. Presenta conflicto con -i\n\
+   -user        : especifica el usuario (se usa con -ssh)\n\
+   -strictHostKey : solicita la comprobación estricta de la clave del host (se usa con -ssh)\n\
+   -logger FINE : habilita logs detallados en el cliente\n\
+   -auth [ USER:SECRET | @FILE ] : especifica el usuario y contraseña o API token (o los carga de un fichero);\n\
+                                   se usa con -http o -remoting pero sólo si el puerto del agente JNLP está deshabilitado.\n\
+                                   No es necesario si las variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN se encuentran configuradas.\n\
+  \n\
+  La lista de comandos disponibles depende del servidor. Ejecuta\n\
+  el comando ''help'' para ver la lista completa.
+CLI.NoURL=No se ha especificado el parámetro -s ni la variable JENKINS_URL
+CLI.VersionMismatch=La versión no coincide. Esta consola "CLI" no se puede usar en este Jenkins
+CLI.NoSuchFileExists=El fichero {0} no existe.
+CLI.BadAuth=Ambas variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN deberían estar configuradas o mantenerse vacías. 

+ 14 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_fr.properties

@@ -0,0 +1,14 @@
+CLI.Usage=Jenkins CLI\n\
+  Utilisation: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  Options:\n\
+   -s URL       : l''URL du serveur (par défaut : variable d environnement JENKINS_URL)\n\
+   -i KEY       : fichier de la clé privée SSH à utiliser pour l''authentification\n\
+   -p HOST:PORT : hôte et port des proxys HTTP et HTTPS. Voir https://www.jenkins.io/redirect/cli-https-proxy-tunnel\n\
+   -noCertificateCheck : contourne entiérement la vérification des certificats HTTPS. A utiliser avec précaution\n\
+   -noKeyAuth   : ne charge pas la clé privée d''authentification SSH. En conflit avec -i\n\
+  \n\
+  Les commandes disponibles dépendent du serveur. Lancer la commande ''help'' pour\n\
+  obtenir la liste.
+CLI.NoURL=Erreur, ni -s ou la variable d''environnement JENKINS_URL ne sont renseignées.
+CLI.VersionMismatch=Conflit de versions. Cet outil ne peut fonctionner avec le serveur Jenkins renseigné.
+CLI.NoSuchFileExists=Ce fichier n''existe pas: {0}

+ 67 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_it.properties

@@ -0,0 +1,67 @@
+# The MIT License
+#
+# Italian localization plugin for Jenkins
+# Copyright © 2020 Alessandro Menti
+#
+# 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.
+
+CLI.BadAuth=Le variabili d''ambiente JENKINS_USER_ID e JENKINS_API_TOKEN \
+  devono essere entrambe impostate o lasciate vuote.
+CLI.NoSuchFileExists=File non esistente: {0}
+CLI.NoURL=Non sono stati specificati né -s né la variabile d''ambiente \
+  JENKINS_URL.
+CLI.Usage=Interfaccia a riga di comando di Jenkins\n\
+  Uso: java -jar jenkins-cli.jar [-s URL] comando [opzioni...] argomenti...\n\
+  Opzioni:\n\
+  \ -s URL              : l''URL del server (impostazione predefinita: la \
+  variabile d''ambiente JENKINS_URL)\n\
+  \ -http               : utilizza un protocollo interfaccia della riga di \
+  comando in testo semplice su HTTP(S) (impostazione predefinita; mutualmente \
+  esclusiva con -ssh)\n\
+  \ -webSocket          : come -http, ma utilizza websocket (funziona meglio \
+  con la maggior parte dei reverse proxy)\n\
+  \ -ssh                : utilizza il protocollo SSH (richiede -user; la \
+  porta SSH deve essere aperta sul server e l''utente deve aver registrato \
+  una chiave pubblica)\n\
+  \ -i KEY              : file chiave privata SSH utilizzato per \
+  l''autenticazione (per l''utilizzo con -ssh)\n\
+  \ -noCertificateCheck : omette completamente la verifica dei certificati \
+  HTTPS. Utilizzare con cautela\n\
+  \ -noKeyAuth          : non tenta di caricare la chiave privata per \
+  l''autenticazione SSH. In conflitto con -i\n\
+  \ -user               : specifica l''utente (per l''utilizzo con -ssh)\n\
+  \ -strictHostKey      : richiede la modalità strict per la verifica delle \
+  chiavi host (per l''utilizzo con -ssh)\n\
+  \ -logger FINE        : abilita la registrazione dettagliata degli eventi \
+  da parte del client\n\
+  \ -auth [ UTENTE:SEGRETO | @FILE ] : specifica il nome utente e la password \
+  o il token API (o li carica entrambi da un file);\n\
+  \                                    per l''utilizzo con -http.\n\
+  \                                    È raccomandato fornire le credenziali \
+  in un file.\n\
+  \                                    Si veda \
+  https://www.jenkins.io/redirect/cli-http-connection-mode per ulteriori \
+  informazioni e opzioni.\n\
+  \ -bearer [ TOKEN | @FILE ]        : specifica di utilizzare \
+  l'autenticazione tramite un bearer token (o carica il token da\n\
+  \                                    un file); per l''utilizzo con -http. \
+  L''opzione è mutualmente esclusiva con -auth.\n\
+  \n\
+  I comandi disponibili dipendono dal server. Eseguire il comando ''help'' per \
+  visualizzarne l''elenco.

+ 40 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_ja.properties

@@ -0,0 +1,40 @@
+# The MIT License
+#
+# Copyright (c) 2004-2010, Sun Microsystems, 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.
+
+# Neither -s nor the JENKINS_URL env var is specified.
+CLI.NoURL=-sも環境変数JENKINS_URLも指定されていません。
+# Version mismatch. This CLI cannot work with this Hudson server
+CLI.VersionMismatch=バージョンミスマッチです。このCLIはHudsonサーバでは動きません。
+# Hudson CLI\n\ 
+# Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ 
+# Options:\n\ 
+# \ -s URL : specify the server URL (defaults to the HUDSON_URL env var)\n\ 
+# \n\ 
+# The available commands depend on the server. Run the 'help' command to\n\ 
+# see the list.
+CLI.Usage=Jenkins CLI\n\
+  使用方法: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  オプション:\n\
+  \ -s URL : サーバのURLを指定してください(デフォルトは環境変数JENKINS_URLです)。\n\
+  \n\
+  利用可能なコマンドはサーバに依存します。そのリストをみるには'help'コマンドを実行してください。
+CLI.NoSuchFileExists=ファイルが存在しません。: {0}

+ 36 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_pt_BR.properties

@@ -0,0 +1,36 @@
+# The MIT License
+#
+# Copyright (c) 2004-2010, Sun Microsystems, Inc., Reginaldo L. Russinholi, Cleiber Silva, Fernando Boaglio
+#
+# 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.
+
+CLI.Usage=Jenkins CLI\n\
+  Uso: java -jar jenkins-cli.jar [-s URL] comando [opções...] parâmetros...\n\
+  Opções:\n\
+   -s URL       : a URL do servidor (por padrão a variável de ambiente JENKINS_URL é usada)\n\
+   -i KEY       : arquivo contendo a chave SSH privada usada para autenticação\n\
+   -p HOST:PORT : host e porta do proxy HTTP para tunelamento de proxy HTTPS. Veja https://www.jenkins.io/redirect/cli-https-proxy-tunnel\n\
+   -noCertificateCheck : ignora completamente a validação dos certificados HTTPS. Use com cautela\n\
+   -noKeyAuth   : não tenta carregar a chave privada para autenticação SSH. Conflita com -i\n\
+  \n\
+  Os comandos disponíveis dependem do servidor. Execute o comando 'help' para\n\
+  ver a lista.
+CLI.NoURL=Não foi especificado nem '-s' e nem a variável de ambiente JENKINS_URL
+CLI.NoSuchFileExists=O arquivo não existe: {0}
+CLI.BadAuth=As variáveis de ambiente JENKINS_USER_ID e JENKINS_API_TOKEN precisam ser ambas configuradas ou deixadas vazias.

+ 26 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_ru.properties

@@ -0,0 +1,26 @@
+CLI.Usage=Jenkins CLI\n\
+  Использование: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  Параметры:\n\
+  \ -s URL              : URL сервера (по умолчанию используется переменная окружения JENKINS_URL)\n\
+  \ -webSocket          : Подключиться с помощью WebSocket (по умолчанию; хорошо работает с большинством обратных прокси-серверов; требует Jetty)\n\
+  \ -http               : Использовать пару соединений HTTP(S), а не WebSocket\n\
+  \ -ssh                : Использовать протокол SSH вместо WebSocket (требуется -user; порт SSH должен быть открыт на сервере)\n\
+  \ -i KEY              : Файл закрытого ключа SSH, используемый для аутентификации (для использования с -ssh)\n\
+  \ -noCertificateCheck : Полностью обойти проверку сертификата HTTPS. Используйте с осторожностью\n\
+  \ -noKeyAuth          : Не пытайтесь загрузить закрытый ключ аутентификации SSH. Конфликты с -i\n\
+  \ -user               : Указать пользователя (для использования с -ssh; необходимо зарегистрировать открытый ключ)\n\
+  \ -strictHostKey      : Запрос строгой проверки ключей хоста (для использования с -ssh)\n\
+  \ -logger FINE        : Включить подробное ведение журнала с клиента\n\
+  \ -auth [ USER:SECRET | @FILE ] : Указать имя пользователя и пароль или API-токен (или загрузить оба из файла);\n\
+  \                                 для использования с -http.\n\
+  \                                 Рекомендуется передавать учетные данные по файлам.\n\
+  \                                 Более подробную информацию и варианты смотрите на сайте https://www.jenkins.io/redirect/cli-http-connection-mode.\n\
+  \ -bearer [ TOKEN | @FILE ]     : Указать аутентификацию с использованием токена (или загрузить токен из файла);\n\
+  \                                 Для использования с -http. Взаимоисключающий вариант с -auth.\n\
+  \                                 Рекомендуется передавать учетные данные через файл.\n\
+  \n\
+  Доступные команды зависят от сервера. Выполните команду ''help'', чтобы \
+  посмотреть список.
+CLI.NoURL=Не указаны ни -s, ни переменная окружения JENKINS_URL.
+CLI.NoSuchFileExists=Не существует такого файла: {0}
+CLI.BadAuth=Параметры окружения JENKINS_USER_ID и JENKINS_API_TOKEN должны быть оба установлены или оставлены пустыми.

+ 26 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_sv_SE.properties

@@ -0,0 +1,26 @@
+CLI.Usage=Jenkins kommandoradsgränssnitt\n\
+  Användning: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+  Alternativ:\n\
+  \ -s URL              : serverns webbadress (standard är miljövariabeln JENKINS_URL)\n\
+  \ -webSocket          : anslut med WebSocket (standard; fungerar med de flesta omvända proxys; kräver Jetty)\n\
+  \ -http               : använd ett par HTTP(S)-anslutningar istället för WebSocket\n\
+  \ -ssh                : använd SSH-protokoll istället för WebSocket (kräver -user; SSH-porten måste vara öppen på servern)\n\
+  \ -i KEY              : privat SSH-nyckel som används för autentisering (används med -ssh)\n\
+  \ -noCertificateCheck : kringgå kontroll av HTTPS-certifikat helt och hållet. Används varsamt\n\
+  \ -noKeyAuth          : försök inte läsa privata SSH-autentiseringsnyckeln. Ligger i konflikt med -i\n\
+  \ -user               : specificera användare (används med -ssh; måste ha registrerats med en offentlig nyckel)\n\
+  \ -strictHostKey      : begär strikt kontroll av värdnyckel (används med -ssh)\n\
+  \ -logger FINE        : aktivera detaljerad loggning från klienten\n\
+  \ -auth [ USER:SECRET | @FILE ] : specificera användare och antingen lösenord eller API-token (eller läs in båda från en fil);\n\
+  \                                 används med -http.\n\
+  \                                 Det rekommenderas att bifoga inloggningsinformation via fil.\n\
+  \                                 Se https://www.jenkins.io/redirect/cli-http-connection-mode för mer information och alternativ.\n\
+  \ -bearer [ TOKEN | @FILE ]     : specificera autentisering med en ägartoken (eller läs in token från en fil);\n\
+  \                                 används med -http. Kan inte användas samtidigt med -auth.\n\
+  \                                 Det rekommenderas att bifoga inloggningsinformation via fil.\n\
+  \n\
+  Tillgängliga kommandon beror på servern. Kör kommandot ''help'' för att \
+  se listan.
+CLI.NoURL=Varken -s eller miljövariabeln JENKINS_URL har specificerats.
+CLI.NoSuchFileExists=Det finns ingen sådan fil: {0}
+CLI.BadAuth=Miljövariablerna JENKINS_USER_ID och JENKINS_API_TOKEN bör anges eller lämnas tomma. 

+ 35 - 0
jenkins/cli/src/main/resources/hudson/cli/client/Messages_zh_TW.properties

@@ -0,0 +1,35 @@
+# The MIT License
+#
+# Copyright (c) 2013, Sun Microsystems, Inc., Kohsuke Kawaguchi, Pei-Tang Huang,
+# Chunghwa Telecom Co., Ltd., and a number of other of contributers
+#
+# 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.
+
+CLI.NoURL=沒有指定 -s 參數或是 JENKINS_URL 環境變數。
+CLI.VersionMismatch=版本不符。CLI 無法在本 Jenkins 伺服器上運作。
+CLI.Usage=Jenkins CLI\n\
+  使用方式: java -jar jenkins-cli.jar [-s URL] 指令 [選項...] 參數...\n\
+  選項:\n\
+   -s URL       : 伺服器 URL (預設值為 JENKINS_URL 環境變數)\n\
+   -i KEY       : 驗證用的 SSH 私鑰檔\n\
+   -p HOST:PORT : 建 HTTPS Proxy Tunnel 的 HTTP 代理伺服器主機及連接埠。請參考 https://www.jenkins.io/redirect/cli-https-proxy-tunnel\n\
+   -noCertificateCheck : 完全略過 HTTPS 憑證檢查。請小心使用\n\
+  \n\
+  可用的指令取決於伺服器。執行 'help' 指令可以查看完整清單。
+CLI.NoSuchFileExists=檔案不存在: {0}

+ 26 - 0
jenkins/cli/src/test/java/hudson/cli/CLIConnectionFactoryTest.java

@@ -0,0 +1,26 @@
+package hudson.cli;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class CLIConnectionFactoryTest {
+
+    private CLIConnectionFactory cliFactory;
+
+    @BeforeEach
+    void setUp() {
+        cliFactory = new CLIConnectionFactory();
+    }
+
+    @Test
+    void testBearerFromToken() {
+        assertEquals("Bearer some-token", cliFactory.bearerAuth("some-token").authorization);
+    }
+
+    @Test
+    void testBasicFromUserAndPass() {
+        assertEquals("Basic c29tZTpwYXNz", cliFactory.basicAuth("some:pass").authorization);
+    }
+}

+ 51 - 0
jenkins/cli/src/test/java/hudson/cli/HexDumpTest.java

@@ -0,0 +1,51 @@
+package hudson.cli;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@Execution(ExecutionMode.CONCURRENT)
+class HexDumpTest {
+
+    @DisplayName("Test HexDump.toHex(byte[] buf)")
+    @ParameterizedTest(name = "{index} => expected: {0}, buf: {1}")
+    @MethodSource("testToHex1Sources")
+    void testToHex1(String expected, byte[] buf) {
+        assertEquals(expected, HexDump.toHex(buf));
+    }
+
+    static Stream<Arguments> testToHex1Sources() {
+        return Stream.of(
+            arguments("'fooBar'", new byte[] {'f', 'o', 'o', 'B', 'a', 'r'}),
+            arguments("0xc3", new byte[] {(byte) 'Ã'}),
+            arguments("0xac '100'", new byte[] {(byte) '€', '1', '0', '0'}),
+            arguments("'1' 0xf7 '2'", new byte[] {'1', (byte) '÷', '2'}),
+            arguments("'foo' 0x0a\n'Bar'", new byte[] {'f', 'o', 'o', '\n', 'B', 'a', 'r'})
+        );
+    }
+
+    @DisplayName("Test HexDump.toHex(byte[] buf, int start, int len)")
+    @ParameterizedTest(name = "{index} => expected: {0}, buf: {1}, start: {2}, len: {3}")
+    @MethodSource("testToHex2Sources")
+    void testToHex2(String expected, byte[] buf, int start, int len) {
+        assertEquals(expected, HexDump.toHex(buf, start, len));
+    }
+
+    static Stream<Arguments> testToHex2Sources() {
+        return Stream.of(
+            arguments("'ooBa'", new byte[] {'f', 'o', 'o', 'B', 'a', 'r'}, 1, 4),
+            arguments("0xc3", new byte[] {(byte) 'Ã'}, 0, 1),
+            arguments("0xac '10'", new byte[] {(byte) '€', '1', '0', '0'}, 0, 3),
+            arguments("0xf7 '2'", new byte[] {'1', (byte) '÷', '2'}, 1, 2),
+            arguments("'Bar'", new byte[] {'f', 'o', 'o', '\n', 'B', 'a', 'r'}, 4, 3),
+            arguments("", new byte[] {'f', 'o', 'o', 'B', 'a', 'r'}, 0, 0)
+        );
+    }
+}

+ 166 - 0
jenkins/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java

@@ -0,0 +1,166 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 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.
+ */
+
+package hudson.cli;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.charset.Charset;
+import org.junit.jupiter.api.Test;
+
+class PlainCLIProtocolTest {
+
+    @Test
+    void ignoreUnknownOperations() throws Exception {
+        final PipedOutputStream upload = new PipedOutputStream();
+        final PipedOutputStream download = new PipedOutputStream();
+        class Client extends PlainCLIProtocol.ClientSide {
+            int code = -1;
+            final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+
+            Client() {
+                super(new PlainCLIProtocol.FramedOutput(upload));
+            }
+
+            @Override
+            protected synchronized void onExit(int code) {
+                this.code = code;
+                notifyAll();
+            }
+
+            @Override
+            protected void onStdout(byte[] chunk) throws IOException {
+                stdout.write(chunk);
+            }
+
+            @Override
+            protected void onStderr(byte[] chunk) throws IOException {}
+
+            @Override
+            protected void handleClose() {}
+
+            void send() throws IOException {
+                sendArg("command");
+                sendStart();
+                streamStdin().write("hello".getBytes(Charset.defaultCharset()));
+            }
+
+            void newop() throws IOException {
+                DataOutputStream dos = new DataOutputStream(upload);
+                dos.writeInt(0);
+                dos.writeByte(99);
+                dos.flush();
+            }
+        }
+
+        class Server extends PlainCLIProtocol.ServerSide {
+            String arg;
+            boolean started;
+            final ByteArrayOutputStream stdin = new ByteArrayOutputStream();
+
+            Server() throws IOException {
+                super(new PlainCLIProtocol.FramedOutput(download));
+            }
+
+            @Override
+            protected void onArg(String text) {
+                arg = text;
+            }
+
+            @Override
+            protected void onLocale(String text) {}
+
+            @Override
+            protected void onEncoding(String text) {}
+
+            @Override
+            protected synchronized void onStart() {
+                started = true;
+                notifyAll();
+            }
+
+            @Override
+            protected void onStdin(byte[] chunk) throws IOException {
+                /* To inject a race condition:
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException x) {
+                    throw new IOException(x);
+                }
+                */
+                stdin.write(chunk);
+            }
+
+            @Override
+            protected void onEndStdin() throws IOException {}
+
+            @Override
+            protected void handleClose() {}
+
+            void send() throws IOException {
+                streamStdout().write("goodbye".getBytes(Charset.defaultCharset()));
+                sendExit(2);
+            }
+
+            void newop() throws IOException {
+                DataOutputStream dos = new DataOutputStream(download);
+                dos.writeInt(0);
+                dos.writeByte(99);
+                dos.flush();
+            }
+        }
+
+        Client client = new Client();
+        Server server = new Server();
+        new PlainCLIProtocol.FramedReader(client, new PipedInputStream(download)).start();
+        new PlainCLIProtocol.FramedReader(server, new PipedInputStream(upload)).start();
+        client.send();
+        client.newop();
+        synchronized (server) {
+            while (!server.started) {
+                server.wait();
+            }
+        }
+        server.newop();
+        server.send();
+        synchronized (client) {
+            while (client.code == -1) {
+                client.wait();
+            }
+        }
+        while (server.stdin.size() == 0) {
+            Thread.sleep(100);
+        }
+        assertEquals("hello", server.stdin.toString(Charset.defaultCharset()));
+        assertEquals("command", server.arg);
+        assertEquals("goodbye", client.stdout.toString(Charset.defaultCharset()));
+        assertEquals(2, client.code);
+    }
+
+}

+ 163 - 0
jenkins/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java

@@ -0,0 +1,163 @@
+package hudson.cli;
+
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Objects;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+
+/**
+keys were generated with ssh-keygen from OpenSSH_7.9p1, LibreSSL 2.7.3
+*/
+@Execution(ExecutionMode.CONCURRENT)
+class PrivateKeyProviderTest {
+
+    /**
+    key command: ssh-keygen -f dsa -t dsa -b 1024 -m PEM
+    */
+    @Test
+    void loadKeyDSA() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("dsa");
+        assertKeyPairNotNull(file, null);
+    }
+
+    /**
+     * Asserts the keyPair private and public are not null.
+     * @param file the file to load the key from
+     * @param password the password
+     */
+    private void assertKeyPairNotNull(File file, String password) throws IOException, GeneralSecurityException {
+        KeyPair keyPair = PrivateKeyProvider.loadKey(file, password);
+        assertNotNull(keyPair);
+        assertNotNull(keyPair.getPrivate());
+        assertNotNull(keyPair.getPublic());
+    }
+
+    private File resourceAsFile(String name) {
+        URL url = Objects.requireNonNull(getClass().getResource(name), "Missing test resource " + name);
+        try {
+            return new File(url.toURI());
+        } catch (URISyntaxException e) {
+            throw new AssertionError("Invalid URI for test resource " + name + ": " + url, e);
+        }
+    }
+
+    /**
+    key command: ssh-keygen -f dsa-password -t dsa -b 1024 -m PEM -P password
+    */
+    @Test
+    void loadKeyDSAPassword() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("dsa-password");
+        String password = "password";
+        assertKeyPairNotNull(file, password);
+    }
+
+    /**
+    key command: ssh-keygen -f rsa -t rsa -b 1024 -m PEM
+    */
+    @Test
+    void loadKeyRSA() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("rsa");
+        assertKeyPairNotNull(file, null);
+    }
+
+    /**
+    key command: ssh-keygen -f rsa-password -t rsa -b 1024 -m PEM -P password
+    */
+    @Test
+    void loadKeyRSAPassword() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("rsa-password");
+        String password = "password";
+        assertKeyPairNotNull(file, password);
+    }
+
+    /**
+    key command: ssh-keygen -f openssh -t rsa -b 1024
+    */
+    @Test
+    void loadKeyOpenSSH() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("openssh");
+        assertKeyPairNotNull(file, null);
+    }
+
+    /**
+     key command: ssh-keygen -f openssh-unsupported -t rsa -b 1024 -m PKCS8 -P password
+     */
+    @Test
+    void loadKeyOpenSSHPKCS8() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("openssh-pkcs8");
+        String password = "password";
+        assertKeyPairNotNull(file, password);
+    }
+
+    /**
+     key command: ssh-keygen -f openssh-unsupported -t rsa -b 1024 -m RFC4716 -p password
+     */
+    @Test
+    void loadKeyOpenSSHRFC4716() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("openssh-rfc4716");
+        String password = "password";
+        assertKeyPairNotNull(file, password);
+    }
+
+    /**
+     key command: ssh-keygen -f openssh-unsupported -t rsa -b 1024 -m RFC4716 -p password
+     Copy pasted the same key twice
+     */
+    @Test
+    void loadKeyOpenSSHMultipleKeys() {
+        File file = resourceAsFile("openssh-multiple-keys");
+        String password = "password";
+        assertThrows(InvalidKeySpecException.class, () -> PrivateKeyProvider.loadKey(file, password));
+    }
+
+    /**
+     * Uses a blank file
+     */
+    @Test
+    void loadBlankKey() {
+        File file = resourceAsFile("blank");
+        String password = "password";
+        assertThrows(InvalidKeyException.class, () -> PrivateKeyProvider.loadKey(file, password));
+    }
+
+    /**
+    key command: ssh-keygen -f openssh -t rsa -b 1024
+    in this key we remove some lines to break the key.
+    */
+    @Test
+    void loadKeyBroken() {
+        File file = resourceAsFile("openssh-broken");
+        String password = "password";
+        assertThrows(IllegalArgumentException.class, () -> PrivateKeyProvider.loadKey(file, password));
+    }
+
+    /**
+    key command: ssh-keygen -t ecdsa -b 256 -N "" -C "" -f ecdsa
+    */
+    @Test
+    void loadKeyECDSA() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("ecdsa");
+        assertKeyPairNotNull(file, null);
+    }
+
+    /**
+    key command: ssh-keygen -t ed25519 -N "" -C "" -f ed25519
+    */
+    @Test
+    void loadKeyED25519() throws IOException, GeneralSecurityException {
+        File file = resourceAsFile("ed25519");
+        assertKeyPairNotNull(file, null);
+    }
+}

+ 133 - 0
jenkins/cli/src/test/java/hudson/util/QuotedStringTokenizerTest.java

@@ -0,0 +1,133 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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.
+ */
+
+package hudson.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+class QuotedStringTokenizerTest {
+
+    @Test
+    void test1() {
+        check("foo bar",
+              "foo", "bar");
+    }
+
+    @Test
+    void test2() {
+        check("foo \"bar zot\"",
+              "foo", "bar zot");
+    }
+
+    @Test
+    void test3() {
+        check("foo bar=\"quote zot\"",
+              "foo", "bar=quote zot");
+    }
+
+    @Test
+    void test4() {
+        check("foo\\\"",
+              "foo\"");
+    }
+
+    @Test
+    void test5() {
+        check("foo\\ bar",
+              "foo bar");
+    }
+
+    @Test
+    void test6() {
+        check("foo\\\\ bar",
+              "foo\\", "bar");
+    }
+
+    // see http://www.nabble.com/Error-parsing-%22-in-msbuild-task-to20535754.html
+    @Test
+    void test7() {
+        check("foo=\"bar\\zot\"",
+              "foo=bar\\zot");
+    }
+
+    @Test
+    void testHasMoreToken() {
+        QuotedStringTokenizer tokenizer = new QuotedStringTokenizer("");
+        assertFalse(tokenizer.hasMoreTokens());
+        tokenizer = new QuotedStringTokenizer("one");
+        assertTrue(tokenizer.hasMoreTokens());
+    }
+
+    @Test
+    void testUnquoteUnicodeEscapeAscii() {
+        String src = "\"" + "\\" + "u0041" + "\"";
+        assertEquals("A", QuotedStringTokenizer.unquote(src));
+    }
+
+    @Test
+    void testUnquoteUnicodeEscapeNonAscii() {
+        String src = "\"" + "\\" + "u263A" + "\"";
+        assertEquals(0x263A, QuotedStringTokenizer.unquote(src).charAt(0));
+    }
+
+    @Test
+    void testUnquoteTruncatedUnicodeEscape() {
+        String src = "\"" + "\\" + "u004" + "\"";
+        assertThrows(IllegalArgumentException.class, () -> QuotedStringTokenizer.unquote(src));
+    }
+
+    @Test
+    void testUnquoteInvalidUnicodeEscape() {
+        String src = "\"" + "\\" + "u00Q1" + "\"";
+        assertThrows(IllegalArgumentException.class, () -> QuotedStringTokenizer.unquote(src));
+    }
+
+    // Pins the differing contracts of the two quote overloads: the single-argument
+    // quote(String) always quotes, while quote(String, String) quotes only when required.
+    @Test
+    void quoteSingleArgAlwaysQuotes() {
+        assertEquals("\"abc\"", QuotedStringTokenizer.quote("abc"));
+    }
+
+    @Test
+    void quoteTwoArgReturnsInputWhenQuotingNotRequired() {
+        assertEquals("abc", QuotedStringTokenizer.quote("abc", ""));
+    }
+
+    private void check(String src, String... expected) {
+        String[] r = QuotedStringTokenizer.tokenize(src);
+        System.out.println(Arrays.asList(r));
+        assertArrayEquals(expected, r);
+    }
+}

+ 0 - 0
jenkins/cli/src/test/resources/hudson/cli/blank


+ 12 - 0
jenkins/cli/src/test/resources/hudson/cli/dsa

@@ -0,0 +1,12 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBvAIBAAKBgQCUlgM7pckNS/AU9w80QDlw304vdyK6u6cWXW7F1PFYMhCrsC7C
+ZcbSKo2mTPHk6P77z3zceSAHeYxkXx34N2HYWCth+79N3VIz/EZC/IaN0sea1teF
+XYLvdnDWazDcRdq+3d4iLAL+46QX6tIaEmUh9SFd4kz7P0rwPHz7SP7+MwIVAPnJ
+QmOwaMbyvb0Q0/OjtXF1orn5AoGBAIP6gOIhNErjgnyzHSfAaR/F2wNFW/TR5HFH
+GEnsVuxTXE00mjqZJaDznviDoVtWF/HlKnR6OcnVr7fm61PXKogeLeJ/zgNPWYCe
+7b45iO38Ztnmimmo9GbzTcDYOVDhNhgZEOUWes9Cdrku8yB3Ugf6nN6GY17LoQ9s
+EDk1Fa8TAoGAD3cFbpbnnw0sHaKMRopCPJKgmCtvzJFgf+xGUZRgEPbeB+1UnG2Z
+T8xPyPF4Zwyo0Mf/b0gxjynLpvzLrr3TMQjqjL4vGE1UIx638ff1Jw08tRRCfBIr
+GpIA5AIzZhxRE6aZREN5wqvnWnlEN+s6mOhcQ8gtNOObHaKuGdU+jAwCFQCvo83Q
+shI+la9gd69CWzJx5GJkJw==
+-----END DSA PRIVATE KEY-----

+ 15 - 0
jenkins/cli/src/test/resources/hudson/cli/dsa-password

@@ -0,0 +1,15 @@
+-----BEGIN DSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,CC014AB7E7C376D246B02422C40ED26A
+
+VjIeNQw7EVgiMDJVglpma1CIvRlfzhBBR6r0i5QsVEPTNGLNNUvbqMxUATud8ql0
+Dc84nDj47qbp+jfsvtSfau32hONynT91mNZiCTPUMGFQqiBDMXUMYBv64NXNFi51
++QpI+bW85KLJyRZSmxmBHH31s+6buWwzpYQ2ImwA/Zzkn/1+Evts+VYWEAwXZNBR
+rlpouBiY7XIP3hifU6vBwkH1Dr5Qff0hq13b4T74X2cL2cmWwmPjWDm8nXgDW2Of
+B+5+w2vCp/QDBUpc8LsxNqUK2/B7KA22wqpmQhD7dY4orzaHQWGKwojSd4LwfX6r
+3NEeptKAAn4TusyREypO67g7xUDT0BfZmCNUg6J9dLPWGusx8IvpK/hABzo5Etn/
+CnBRMc1QaDUASHRgzCkadM1DpKmyVQFlMLXCRJaS+51CHGVVGFILcgPsCcrzFL0r
+HOzDbBymhU9Mfw1AygdKNMyZ8NdDsdZ1H3fkkZu7sMgt3PRaYZG+wNImpkdnQDoN
+V3VqTZ5orF4Lo4OqxOuEd7dakpdpyidRvQQc5GaOZuG+2X/NlKhzdGEjt0iubMnX
+S94J+KPJUeVfPphtA2Lb8w==
+-----END DSA PRIVATE KEY-----

+ 8 - 0
jenkins/cli/src/test/resources/hudson/cli/ecdsa

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSmYgxT7E9XwGYmr8cd++EvpaNX3e1/
+T5w30hcO7xNk4n7xDsfZk8jOvg1ZbsfItBB5yjwRy7QTyXt/2ankp/E7AAAAoG6/A51uvw
+OdAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKZiDFPsT1fAZiav
+xx374S+lo1fd7X9PnDfSFw7vE2TifvEOx9mTyM6+DVlux8i0EHnKPBHLtBPJe3/ZqeSn8T
+sAAAAhAJgBcc/Ez07nhpmLlsHhySD5zFeW/mHizkk/CIrvjkqSAAAAAAECAwQFBgc=
+-----END OPENSSH PRIVATE KEY-----

+ 7 - 0
jenkins/cli/src/test/resources/hudson/cli/ed25519

@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACD6KfM1gIePqB5IPHvD0vjniUUYE6hb6c6GsxVrPZg4ngAAAIiLNylbizcp
+WwAAAAtzc2gtZWQyNTUxOQAAACD6KfM1gIePqB5IPHvD0vjniUUYE6hb6c6GsxVrPZg4ng
+AAAEAA/Q+bSWkxT9WZacetaaN/KBJvrDRlHOuxKQghG49hKvop8zWAh4+oHkg8e8PS+OeJ
+RRgTqFvpzoazFWs9mDieAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----

+ 17 - 0
jenkins/cli/src/test/resources/hudson/cli/openssh

@@ -0,0 +1,17 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bk
+xjv/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnM
+fgCEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAIYX2d44V9neOEAAAAH
+c3NoLXJzYQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bkxj
+v/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnMfg
+CEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAADAQABAAAAgBRXdq7kj/
+iR+WIEs7uifSMwuPGDjtxksg2Uj09kSGRLFmZdu4EWtvUh0uV0J37vbfBSkubU3fAvrP99
+bRxUHhD5Z444BIyht8jlBetfoJOBSE/TQJ/69xguSmHB8XH8/WUqEaNZ2F+q0AAkRt5CTs
+lkML/YJI1mPzy+0ny6tS8hAAAAQQD6N3CByknj5WrDIJQCce+zbhbftnN4RM6OBuaHv4mm
+y0qIAH7i6ZHFqHlr1OnCPzqtzIt4McoyIDEf+9eH3ktKAAAAQQD8uQ/jcs27tMPwb2GYyU
+vSNZ9g225seV+Y1dU+GSk8zuqmkN1Cbc0dmJW8hqncYrbDuFbRH3kPiLTLO3Aifn2xAAAA
+QQDsirzo7Ox+f2SC0TCu6+wbar6f617IhWRlUyPDNa18+ju+BHdw1890sRSpany5RbJekq
+KoumXchRmzl8vZPoVPAAAAHWluaWZjQFRoZS10b3hpYy1hdmVuZ2VyLmxvY2FsAQIDBAU=
+
+-----END OPENSSH PRIVATE KEY-----

+ 16 - 0
jenkins/cli/src/test/resources/hudson/cli/openssh-broken

@@ -0,0 +1,16 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bk
+xjv/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnM
+fgCEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAIYX2d44V9neOEAAAAH
+c3NoLXJzYQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bkxj
+v/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnMfg
+CEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAADAQABAAAAgBRXdq7kj/
+iR+WIEs7uifSMwuPGDjtxksg2Uj09kSGRLFmZdu4EWtvUh0uV0J37vbfBSkubU3fAvrP99
+bRxUHhD5Z444BIyht8jlBetfoJOBSE/TQJ/69xguSmHB8XH8/WUqEaNZ2F+q0AAkRt5CTs
+y0qIAH7i6ZHFqHlr1OnCPzqtzIt4McoyIDEf+9eH3ktKAAAAQQD8uQ/jcs27tMPwb2GYyU
+vSNZ9g225seV+Y1dU+GSk8zuqmkN1Cbc0dmJW8hqncYrbDuFbRH3kPiLTLO3Aifn2xAAAA
+QQDsirzo7Ox+f2SC0TCu6+wbar6f617IhWRlUyPDNa18+ju+BHdw1890sRSpany5RbJekq
+KoumXchRmzl8vZPoVPAAAAHWluaWZjQFRoZS10b3hpYy1hdmVuZ2VyLmxvY2FsAQIDBAU=
+
+-----END OPENSSH PRIVATE KEY-----

+ 35 - 0
jenkins/cli/src/test/resources/hudson/cli/openssh-multiple-keys

@@ -0,0 +1,35 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB7+Wg6qP
+jwYDqZ6ghEjcxsAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDDQX8IscJ6
+Axj5nni6lv4uh6w+CyNfsItKdLBj2KaohA7fcgpL2DrjcYo0Z9W74jbVRSW9mcCp/6Kdfl
+JumpnUjpx7QBlUcnKIakpFuIHJl8HcHDLusy/R1RB1H/ShXqkHfPT3JmSbo1iwrGS4UFSq
+S/KZsfKNph4x/vbyizUlPQAAAiAhHAwqGb+KrmywoeqT/QkWflUH/aRy7iZM9rYx7ROh2K
+FY143YePB66yCczropqQPFn4hgf2wxSEsvMLKnBy44ruUl7w8O/mmsO993DkXmtVTHdipz
+vWHZLtSFe2IB1IFAoGGcOeGFIBKQ6DfIVWPaPRFF+jmpVpn19qYpkxmp6uBq8guViXmoTV
+eM/r46ipHYQ57BVopYnUQrxa13p7PZQRmrfNapfAgJpvvJxHjO9RFUeJndWNKVbCXblc24
+TTQ5EUxlLrTsRhH5ICdTSvPnm50I8yjiIZVOXNkPn8UOrj2yVaU2LDkFnbzB4EJIEtdmVb
+iavgaTO/TURIS5b7oa21rVnYVacUmhZyBRgo/abu49ovvTyyNGk+MIgtmgbq8NZunBP/Te
+HpaeTgZm9uMOFy7ni/S9raMYzKun8dCoNaEES/WIFBdAtw3SY2eLXYVRVKI1dk2q3swhiL
+t+M81BfSzCrBzqENyrRSvbMIV0fI6Kvc8Kq5aNIPw5JIFTNYc0c8JWyxf72Is4PffyCrK7
+7qGrXiU/fys1j+LCQUg7UOLaMnYetY+JnN7Z284J59YsvFnyjoZnCJeS6Ewy6s8alLDi3k
+da3VASzohO4j1mD1HWVYG/eEWMxcX5TkyxaMcXkpFkR/lEXZXMr0y+1mgICefspMsi+OSf
+1xrbdd5CNbELWRD88BOuIKFhdThx9C2A65AKGpCt7UkFfNB2pCm6
+-----END OPENSSH PRIVATE KEY-----
+
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB7+Wg6qP
+jwYDqZ6ghEjcxsAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDDQX8IscJ6
+Axj5nni6lv4uh6w+CyNfsItKdLBj2KaohA7fcgpL2DrjcYo0Z9W74jbVRSW9mcCp/6Kdfl
+JumpnUjpx7QBlUcnKIakpFuIHJl8HcHDLusy/R1RB1H/ShXqkHfPT3JmSbo1iwrGS4UFSq
+S/KZsfKNph4x/vbyizUlPQAAAiAhHAwqGb+KrmywoeqT/QkWflUH/aRy7iZM9rYx7ROh2K
+FY143YePB66yCczropqQPFn4hgf2wxSEsvMLKnBy44ruUl7w8O/mmsO993DkXmtVTHdipz
+vWHZLtSFe2IB1IFAoGGcOeGFIBKQ6DfIVWPaPRFF+jmpVpn19qYpkxmp6uBq8guViXmoTV
+eM/r46ipHYQ57BVopYnUQrxa13p7PZQRmrfNapfAgJpvvJxHjO9RFUeJndWNKVbCXblc24
+TTQ5EUxlLrTsRhH5ICdTSvPnm50I8yjiIZVOXNkPn8UOrj2yVaU2LDkFnbzB4EJIEtdmVb
+iavgaTO/TURIS5b7oa21rVnYVacUmhZyBRgo/abu49ovvTyyNGk+MIgtmgbq8NZunBP/Te
+HpaeTgZm9uMOFy7ni/S9raMYzKun8dCoNaEES/WIFBdAtw3SY2eLXYVRVKI1dk2q3swhiL
+t+M81BfSzCrBzqENyrRSvbMIV0fI6Kvc8Kq5aNIPw5JIFTNYc0c8JWyxf72Is4PffyCrK7
+7qGrXiU/fys1j+LCQUg7UOLaMnYetY+JnN7Z284J59YsvFnyjoZnCJeS6Ewy6s8alLDi3k
+da3VASzohO4j1mD1HWVYG/eEWMxcX5TkyxaMcXkpFkR/lEXZXMr0y+1mgICefspMsi+OSf
+1xrbdd5CNbELWRD88BOuIKFhdThx9C2A65AKGpCt7UkFfNB2pCm6
+-----END OPENSSH PRIVATE KEY-----

+ 17 - 0
jenkins/cli/src/test/resources/hudson/cli/openssh-pkcs8

@@ -0,0 +1,17 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDvGXdAqe
+A3cfETZNuKc2qEAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDNj316M4aw
+DJHITn0XzcGapC6+vkXOLSwD/hK8qC6cp/FPEPrOo20jxRiTrd00/M6SbfeJR+wUyZl4V5
+mz8RPKGjVFtktZ5jQJZ3QY4Jqtpc5iMxQfMXmcV0UBJaom8zc7vHSdsH2XrM+xt9Tvrob3
+wQIMjLbwj7cjcHh/1b7iywAAAiDP15QQ8FKnRgbnbRgwuOoEpAqMEt0z7l4CJE6WAQ8hCU
+YyvDFs6c9LPftxa0BlS/INKC3hblqs35X24iU8XzKWTZL3ac99/AfC4Mru9NhpOme1Xdi6
+ZWpjbakqyGHeBX9ERnxcGb/hlKsWErdsMttKFgOm5WvnlOb/Rywt74zIJdq6/LTWvwjgQi
+mF5yZSsJuZiFaq9fASul1gLFikhdxVPGHtI+pzDCviC2UkPdI8jVnqBEzDl7ewjZdcHykX
+hZW+LxbxjKRxrFPSn8prxPqRwAbM4bWoeyh0Y1F+DUaWS9LQjMyG2eHM5RCJdjb8V3NNpH
+vOFD213akcrA7X6lQFlbI5wbVgfMQ6Zo4EGlSA8f9U9fMuBhL24Opbb1d7PFaJLUnw/mJl
+BlqHS2LMaTz5k/dhwVKZfaDLq6Q1RJ/0toJN6LtlQjz5DU7tmUwNmt5/CYtkjMHUQWffk9
+WQ7hPSmKiO2m07zfmwuE2Hdihbb5o2imDuBbTz129Dr5OyqhMcmAxg4GAIZBCjdlUItaAP
+y9ieAVgG4gPYn1tYYa8259Gjug+v8rWzvjdRuUW21gv+quQ+yx1Q1/dErxP8m6VU6pdE7b
+3TgQH+sxdXZpOksYLpuDBZ6KNdkDtQjWCCfkDCXm/4Pr5ljyu5n56YDBYyzbxq+Amsc02S
+iC8i6JQjGsnvJzuGMbmrr1qE99vbRnwWJAeZsjh3U4ResfZ5mAgs
+-----END OPENSSH PRIVATE KEY-----

+ 17 - 0
jenkins/cli/src/test/resources/hudson/cli/openssh-rfc4716

@@ -0,0 +1,17 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB7+Wg6qP
+jwYDqZ6ghEjcxsAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDDQX8IscJ6
+Axj5nni6lv4uh6w+CyNfsItKdLBj2KaohA7fcgpL2DrjcYo0Z9W74jbVRSW9mcCp/6Kdfl
+JumpnUjpx7QBlUcnKIakpFuIHJl8HcHDLusy/R1RB1H/ShXqkHfPT3JmSbo1iwrGS4UFSq
+S/KZsfKNph4x/vbyizUlPQAAAiAhHAwqGb+KrmywoeqT/QkWflUH/aRy7iZM9rYx7ROh2K
+FY143YePB66yCczropqQPFn4hgf2wxSEsvMLKnBy44ruUl7w8O/mmsO993DkXmtVTHdipz
+vWHZLtSFe2IB1IFAoGGcOeGFIBKQ6DfIVWPaPRFF+jmpVpn19qYpkxmp6uBq8guViXmoTV
+eM/r46ipHYQ57BVopYnUQrxa13p7PZQRmrfNapfAgJpvvJxHjO9RFUeJndWNKVbCXblc24
+TTQ5EUxlLrTsRhH5ICdTSvPnm50I8yjiIZVOXNkPn8UOrj2yVaU2LDkFnbzB4EJIEtdmVb
+iavgaTO/TURIS5b7oa21rVnYVacUmhZyBRgo/abu49ovvTyyNGk+MIgtmgbq8NZunBP/Te
+HpaeTgZm9uMOFy7ni/S9raMYzKun8dCoNaEES/WIFBdAtw3SY2eLXYVRVKI1dk2q3swhiL
+t+M81BfSzCrBzqENyrRSvbMIV0fI6Kvc8Kq5aNIPw5JIFTNYc0c8JWyxf72Is4PffyCrK7
+7qGrXiU/fys1j+LCQUg7UOLaMnYetY+JnN7Z284J59YsvFnyjoZnCJeS6Ewy6s8alLDi3k
+da3VASzohO4j1mD1HWVYG/eEWMxcX5TkyxaMcXkpFkR/lEXZXMr0y+1mgICefspMsi+OSf
+1xrbdd5CNbELWRD88BOuIKFhdThx9C2A65AKGpCt7UkFfNB2pCm6
+-----END OPENSSH PRIVATE KEY-----

+ 15 - 0
jenkins/cli/src/test/resources/hudson/cli/rsa

@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDcPcvFMiF7OsfaV9MuHQytEOffnyyFraVyYJrgRsdMsFMJcUVJ
+JY2oo2OdMF42CzstSe1fasAZnxalkGBvSdfH6PphgIhBcVj1eCgPR/76q2OyuRk7
+GE5R8GDlQSCsFsV658zyoBXELVFL9HvhXOw/0u7h60h/ertIoYawz44e3wIDAQAB
+AoGBAKTP7bQ07o88DqCbRmJkxL6iPxK+F+A1cPDl0CBzduMxtAIF7LZvTtHa60mP
+D4Fb6D3c67CSvwytW5IsN64wUTNaB6pOXmVnscgsXzNXz4UGsxsEOFUcYkEGJZ8M
+EMKDSqaCI43EMR7nJi/UNrJyj37Axn4cU3bgnDP5DAxO+AK5AkEA7kWA7zw86AoR
+vuSSWEINNSglzTRLZjciDX2b93kh97voRV0q/CHIoSvB3sLU0R/oLpOjaUhPFWrM
+JfI9GQemNQJBAOyg3MHBQhAd1rhXzNkW62903ICjlNGLOTemi+E11iIfGyA0bhLH
+DJQfR+Tx/VLQEVbAMU1pkUg33GRck4SPA0MCQHbmuDCqHrqsS6624VCppW2hWzvL
+nNSlLpkM1YfpKso1OvNiStEHCtdivpwrHYg+I98aTbF8I/rMEJPfDh4vcwECQQCv
+LBbAyNSjIbPHHBhlzXXVOOnTwUV2Kl7dN8ntmvE+qVBncujZtck2DkIm1o32NFnh
+or3c1P3cPJ5HHdGHHGgJAkBdbOMa8VIFBliHr22akFD43gakZ/7ymRNTXA1uj6xt
+NO/1mklbaj4u41UQZU9JjsqebOMGnIc88KMFh6wdfaed
+-----END RSA PRIVATE KEY-----

+ 18 - 0
jenkins/cli/src/test/resources/hudson/cli/rsa-password

@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,B937F90AFDFDE86CD9023373F5079F8F
+
+N6ohIfZ+nmqodFzFlFCG6gBXEGIBOl/ojaV+GSHpCJRKJs6lgGSwVmSHESqiKpAI
+ZNQpWEURIVXRpScAuqB+BNQyNjnlFwoo47aBkfDFR2DTY6y+21Z1Fll9ZExABgRJ
+IsMoRVjVbd4eJ1njln73b290EUBNu4ej14pPFsHqSdHQ4atPXw3Alph0NuFYz1CI
+Jg/0vUFFX1u2UjLYAHRU7EKJUk027H1GjZdlV7kcogMRblINokGK0rpmuize6fE9
+WHzEkeGb7qwU4fJ1Sa396TwA5sU6K7xPqV+sArWevavVC4xi0yKoSMmzR2ps5rg+
+XDNBnscNo0dTxnR/Dku2fsqiTsvXZ/LQDWTyTkC0Sx/gYKmfmYfmOpFh6pW0PRUF
+h8Cxm/XkAZV90pK+SrwKA/Pj2vi5nJvAsQlHD+5f7GYLMiB2pWJdbTuXjg0MJjvP
+3TMZm7SqOMe7G4iyXKmlg7Hri2jMPkzvmoGMQZk8bFoX87579w4s0bCdP23lcI2H
+6F7OHTHnPbPc302vN2NWARuqP6XJGRlwJJaNkL8/cvMh42UeVZEZ5WwacUW5g0IN
+IE+K/L5EemBVI2whRsgBBxbyKymBMTolKDhLbGicxAb7E1jr3UXsRv17LaoaPMDN
+TbCjhRkXiYAWXzR/BVwG2vhCI9geCXbLXAVhp2U/z6+KDAVXUIf2Hwz1v1g+fabV
+DGQBzCobfJv9ML9/oO6+KP/dgAA/kBMsLNmQTAR9NslDqdbVujNJ0fSvqaEG1lbq
+7oc36BIyjJgPEehE0kCfzAeVRHq73N8lfbZaz5cnvdMjCXwMubcIL1kBfUpv5eSg
+-----END RSA PRIVATE KEY-----

+ 35 - 0
jenkins/core/move-l10n.groovy

@@ -0,0 +1,35 @@
+// Usage: groovy move-l10n.groovy hudson/model/OldClass/old-view jenkins/model/NewClass/new-view 'Some\ Translatable\ Text'
+// (The new view may be given as '-' to simply delete the key.)
+
+def oldview = args[0]
+def newview = args[1]
+def key = args[2]
+
+def scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+def resDir = new File(scriptDir, 'src/main/resources')
+
+def basename = new File(resDir, oldview).name
+for (p in new File(resDir, oldview).parentFile.listFiles()) {
+    def n = p.name
+    if (n == "${basename}.properties" || n.startsWith("${basename}_") && n.endsWith(".properties")) {
+        def lines = p.readLines('ISO-8859-1')
+        // TODO does not handle multiline values correctly
+        def matches = lines.findAll({it.startsWith("${key}=")})
+        if (!matches.isEmpty()) {
+            lines.removeAll(matches)
+            p.withWriter('ISO-8859-1') {out ->
+                lines.each {line -> out.writeLine(line)}
+            }
+            if (newview == '-') {
+                println("deleting ${matches.size()} matches from ${n}")
+            } else {
+                def nue = new File(resDir, newview + n.substring(basename.length()))
+                println("moving ${matches.size()} matches from ${n} to ${nue.name}")
+                // TODO if the original lacked a trailing newline, this will corrupt previously final key
+                nue.withWriterAppend('ISO-8859-1') {out ->
+                        matches.each {line -> out.writeLine(line)}
+                }
+            }
+        }
+    }
+}

+ 807 - 0
jenkins/core/pom.xml

@@ -0,0 +1,807 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+The MIT License
+
+Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
+Daniel Dyer, Erik Ramfelt, Stephen Connolly, Tom Huybrechts, Alan Harder
+
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.jenkins-ci.main</groupId>
+    <artifactId>jenkins-parent</artifactId>
+    <version>${revision}${changelist}</version>
+  </parent>
+
+  <artifactId>jenkins-core</artifactId>
+
+  <name>Jenkins core</name>
+  <description>Jenkins core code and view files to render HTML.</description>
+  <url>https://github.com/jenkinsci/jenkins</url>
+
+  <properties>
+    <!-- Minimum Remoting version, which is tested for API compatibility, duplicated so that renovate only updates the latest remoting version property -->
+    <remoting.minimum.supported.version>3256.3258.v858f3c9a_f69d</remoting.minimum.supported.version>
+    <!-- Filled in by jacoco-maven-plugin -->
+    <jacocoSurefireArgs />
+    <!-- Not managed by jenkins-bom, so referenced from annotationProcessorPaths via this property -->
+    <core-annotation-processors.version>59.ve181f58722c6</core-annotation-processors.version>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.jenkins-ci.main</groupId>
+        <artifactId>jenkins-bom</artifactId>
+        <version>${project.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.xmlunit</groupId>
+        <artifactId>xmlunit-bom</artifactId>
+        <version>2.14.0</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>remoting</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>args4j</groupId>
+      <artifactId>args4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.github.spotbugs</groupId>
+      <artifactId>spotbugs-annotations</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>com.google.code.findbugs</groupId>
+          <artifactId>jsr305</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>com.google.errorprone</groupId>
+          <artifactId>error_prone_annotations</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.google.j2objc</groupId>
+          <artifactId>j2objc-annotations</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.checkerframework</groupId>
+          <artifactId>checker-qual</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+      <exclusions>
+        <exclusion>
+          <!-- Provided by spring-aop via spring-security-web -->
+          <groupId>aopalliance</groupId>
+          <artifactId>aopalliance</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.sun.xml.txw2</groupId>
+      <artifactId>txw2</artifactId>
+      <exclusions>
+        <!-- StAX is now bundled in the JRE -->
+        <exclusion>
+          <groupId>javax.xml.stream</groupId>
+          <artifactId>stax-api</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.thoughtworks.xstream</groupId>
+      <artifactId>xstream</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>xmlpull</groupId>
+          <artifactId>xmlpull</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>xpp3</groupId>
+          <artifactId>xpp3_min</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>commons-beanutils</groupId>
+      <artifactId>commons-beanutils</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+    </dependency>
+    <dependency>
+      <!-- Jenkins core does not use this directly, but it remains a transitive dependency through BeanUtils, Jelly, and JSON-lib, and plugins rely on core providing it. -->
+      <groupId>commons-collections</groupId>
+      <artifactId>commons-collections</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.jenkins.tools</groupId>
+      <artifactId>bridge-method-annotation</artifactId>
+      <version>${bridge-method-injector.version}</version>
+    </dependency>
+    <dependency>
+      <!-- needed by Jelly -->
+      <groupId>jakarta.servlet.jsp.jstl</groupId>
+      <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>jakarta.el</groupId>
+          <artifactId>jakarta.el-api</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>jakarta.xml.bind</groupId>
+          <artifactId>jakarta.xml.bind-api</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>jaxen</groupId>
+      <artifactId>jaxen</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>dom4j</groupId>
+          <artifactId>dom4j</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>jdom</groupId>
+          <artifactId>jdom</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>xerces</groupId>
+          <artifactId>xercesImpl</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>xml-apis</groupId>
+          <artifactId>xml-apis</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>xom</groupId>
+          <artifactId>xom</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <!-- Groovy shell uses this, but it uses an optional dependency. Not included into BOM. -->
+      <groupId>jline</groupId>
+      <artifactId>jline</artifactId>
+      <version>2.14.6</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.java.dev.jna</groupId>
+      <artifactId>jna</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.java.sezpoz</groupId>
+      <artifactId>sezpoz</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.jcip</groupId>
+      <artifactId>jcip-annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.antlr</groupId>
+      <artifactId>antlr4-runtime</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.ant</groupId>
+      <artifactId>ant</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-fileupload2-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-fileupload2-jakarta-servlet5</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.groovy</groupId>
+      <artifactId>groovy-all</artifactId>
+    </dependency>
+    <dependency>
+      <!-- Groovy shell uses this, but it doesn't declare the dependency -->
+      <groupId>org.fusesource.jansi</groupId>
+      <artifactId>jansi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>annotation-indexer</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>commons-jelly-tags-fmt</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>commons-jelly-tags-xml</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>jaxen</groupId>
+          <artifactId>jaxen</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.dom4j</groupId>
+          <artifactId>dom4j</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>commons-jexl</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>crypto-util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>memory-monitor</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>symbol-annotation</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>task-reactor</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>version-number</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci.main</groupId>
+      <artifactId>websocket-spi</artifactId>
+      <version>${project.version}</version>
+      <exclusions>
+        <!-- Provided by Jetty/Winstone -->
+        <exclusion>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.jfree</groupId>
+      <artifactId>jfreechart</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jvnet.hudson</groupId>
+      <artifactId>commons-jelly-tags-define</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jvnet.localizer</groupId>
+      <artifactId>localizer</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jvnet.winp</groupId>
+      <artifactId>winp</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke</groupId>
+      <artifactId>access-modifier-annotation</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke</groupId>
+      <artifactId>windows-package-checker</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke.metainf-services</groupId>
+      <artifactId>metainf-services</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke.stapler</groupId>
+      <artifactId>json-lib</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke.stapler</groupId>
+      <artifactId>stapler</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke.stapler</groupId>
+      <artifactId>stapler-adjunct-codemirror</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kohsuke.stapler</groupId>
+      <artifactId>stapler-groovy</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>commons-jelly</groupId>
+          <artifactId>commons-jelly</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>commons-jexl</groupId>
+          <artifactId>commons-jexl</artifactId>
+        </exclusion>
+        <exclusion>
+          <!-- we bundle groovy-all -->
+          <groupId>org.codehaus.groovy</groupId>
+          <artifactId>groovy</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.jvnet.hudson</groupId>
+          <artifactId>commons-jexl</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>log4j-over-slf4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-web</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>io.micrometer</groupId>
+          <artifactId>micrometer-observation</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.springframework</groupId>
+          <artifactId>spring-jcl</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <!--
+        still including the xpp3 driver to ensure backwards compatibilty
+        for other plugins that may be depending on it. Not included into BOM
+      -->
+      <groupId>xpp3</groupId>
+      <artifactId>xpp3</artifactId>
+      <version>1.1.4c</version>
+    </dependency>
+    <dependency>
+      <groupId>jakarta.servlet</groupId>
+      <artifactId>jakarta.servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <!-- Also listed in annotationProcessorPaths above; kept as a dependency so that
+           jenkins-core continues to ship META-INF/services/javax.annotation.processing.Processor
+           for plugin builds. -->
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>core-annotation-processors</artifactId>
+      <version>${core-annotation-processors.version}</version>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <!-- offline profiler API to put in the classpath if we need it -->
+    <!--dependency>
+      <groupId>com.yourkit.api</groupId>
+      <artifactId>yjp</artifactId>
+      <version>dontcare</version>
+      <scope>system</scope>
+      <systemPath>/usr/local/yjp/lib/yjp.jar</systemPath>
+    </dependency-->
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.jenkins-ci</groupId>
+      <artifactId>test-annotations</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-jdk14</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xmlunit</groupId>
+      <artifactId>xmlunit-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xmlunit</groupId>
+      <artifactId>xmlunit-matchers</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <!--
+            Declare the annotation processors explicitly rather than letting javac discover them by
+            scanning the whole compile classpath.
+          -->
+          <annotationProcessorPaths>
+            <path>
+              <groupId>net.java.sezpoz</groupId>
+              <artifactId>sezpoz</artifactId>
+            </path>
+            <path>
+              <groupId>org.jenkins-ci</groupId>
+              <artifactId>annotation-indexer</artifactId>
+            </path>
+            <path>
+              <groupId>org.jenkins-ci</groupId>
+              <artifactId>core-annotation-processors</artifactId>
+              <version>${core-annotation-processors.version}</version>
+            </path>
+            <path>
+              <groupId>org.kohsuke</groupId>
+              <artifactId>windows-package-checker</artifactId>
+            </path>
+            <path>
+              <groupId>org.kohsuke.metainf-services</groupId>
+              <artifactId>metainf-services</artifactId>
+            </path>
+            <path>
+              <groupId>org.kohsuke.stapler</groupId>
+              <artifactId>stapler</artifactId>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <!-- Version specified in parent POM -->
+        <executions>
+          <execution>
+            <id>enforce-banned-dependencies</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <bannedDependencies>
+                  <excludes combine.children="append">
+                    <!-- Library plugins -->
+                    <exclude>com.fasterxml.jackson.*</exclude>
+                    <exclude>com.github.ben-manes.caffeine:caffeine</exclude>
+                    <exclude>com.github.jnr:jnr-posix</exclude>
+                    <exclude>com.github.mwiede:jsch</exclude>
+                    <exclude>com.google.code.gson:gson</exclude>
+                    <exclude>com.jayway.jsonpath:json-path</exclude>
+                    <exclude>commons-httpclient:commons-httpclient</exclude>
+                    <exclude>com.sun.activation:javax.activation</exclude>
+                    <exclude>com.sun.mail:javax.mail</exclude>
+                    <exclude>com.sun.xml.bind:jaxb-impl</exclude>
+                    <exclude>io.jsonwebtoken</exclude>
+                    <!-- Used in unit tests, so exclude from compile and runtime scope only -->
+                    <exclude>jakarta.activation:jakarta.activation-api:*:jar:compile</exclude>
+                    <exclude>jakarta.activation:jakarta.activation-api:*:jar:runtime</exclude>
+                    <exclude>jakarta.mail:jakarta.mail-api</exclude>
+                    <exclude>javax.activation:javax.activation-api</exclude>
+                    <exclude>javax.mail:javax.mail-api</exclude>
+                    <exclude>javax.xml.bind:jaxb-api</exclude>
+                    <exclude>joda-time:joda-time</exclude>
+                    <!-- Used in unit tests, so exclude from compile and runtime scope only -->
+                    <exclude>net.bytebuddy:byte-buddy:*:jar:compile</exclude>
+                    <exclude>net.bytebuddy:byte-buddy:*:jar:runtime</exclude>
+                    <exclude>net.i2p.crypto:eddsa</exclude>
+                    <exclude>net.minidev</exclude>
+                    <exclude>org.apache.commons:commons-lang3</exclude>
+                    <exclude>org.apache.commons:commons-text</exclude>
+                    <exclude>org.apache.httpcomponents</exclude>
+                    <exclude>org.bouncycastle</exclude>
+                    <exclude>org.eclipse.angus:angus-activation</exclude>
+                    <exclude>org.eclipse.angus:angus-mail</exclude>
+                    <exclude>org.glassfish.jersey.*</exclude>
+                    <exclude>org.json:json</exclude>
+                    <exclude>org.ow2.asm</exclude>
+                    <exclude>org.yaml:snakeyaml</exclude>
+                  </excludes>
+                </bannedDependencies>
+              </rules>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <executions>
+          <execution>
+            <id>add-source</id>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <sources>
+                <!-- The antlr4 sources are found automatically by the maven compiler,
+                but listing them explicitly avoids a missing classpath entry when importing the maven project in eclipse. -->
+                <source>${project.build.directory}/generated-sources/antlr4</source>
+                <source>${project.build.directory}/generated-sources/localizer</source>
+                <source>${project.build.directory}/generated-sources/taglib-interface</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.jenkins-ci.tools</groupId>
+        <artifactId>maven-hpi-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <executions>
+          <execution>
+            <goals>
+              <goal>generate-taglib-interface</goal>
+              <goal>record-core-location</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>io.jenkins.tools</groupId>
+        <artifactId>bridge-method-injector</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>process</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.jvnet.localizer</groupId>
+        <artifactId>localizer-maven-plugin</artifactId>
+        <!-- version specified in grandparent pom -->
+        <executions>
+          <execution>
+            <goals>
+              <goal>generate</goal>
+            </goals>
+            <configuration>
+              <fileMask>Messages.properties</fileMask>
+              <outputDirectory>target/generated-sources/localizer</outputDirectory>
+              <accessModifierAnnotations>true</accessModifierAnnotations>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.kohsuke</groupId>
+        <artifactId>access-modifier-checker</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.antlr</groupId>
+        <artifactId>antlr4-maven-plugin</artifactId>
+        <!-- version specified in grandparent pom -->
+        <executions>
+          <execution>
+            <id>antlr</id>
+            <goals>
+              <goal>antlr4</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <executions>
+          <execution>
+            <goals>
+              <goal>properties</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>winsw</id>
+            <goals>
+              <!-- we use copy as this is a dependency from outside the reactor -->
+              <goal>copy</goal>
+            </goals>
+            <phase>generate-resources</phase>
+            <configuration>
+              <artifactItems>
+                <artifactItem>
+                  <groupId>com.sun.winsw</groupId>
+                  <artifactId>winsw</artifactId>
+                  <version>2.9.0</version>
+                  <classifier>net4</classifier>
+                  <type>exe</type>
+                  <outputDirectory>${project.build.outputDirectory}/windows-service</outputDirectory>
+                  <destFileName>jenkins.exe</destFileName>
+                </artifactItem>
+              </artifactItems>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <executions>
+          <execution>
+            <!--
+              License headers make up the majority of the bytes of the localization and Jelly view files
+              (the license is retained in the source files and in the LICENSE.txt shipped in the war, just
+              as it is not retained in compiled class files), so strip comments from the packaged jar.
+            -->
+            <id>strip-license-comments</id>
+            <goals>
+              <goal>run</goal>
+            </goals>
+            <phase>prepare-package</phase>
+            <configuration>
+              <skip>${strip.license.comments.skip}</skip>
+              <target>
+                <!-- Only the leading comment block, to leave any semantically interesting comments elsewhere in the file untouched -->
+                <replaceregexp encoding="UTF-8" match="\A(?:[ \t]*(?:[#!].*)?\r?\n)+" replace="">
+                  <fileset dir="${project.build.outputDirectory}" includes="**/*.properties" />
+                </replaceregexp>
+                <!-- Only license comments, to leave any semantically interesting comments untouched -->
+                <replaceregexp encoding="UTF-8" flags="g" match="&lt;!--(?:(?!--&gt;)[\s\S])*?The MIT License[\s\S]*?--&gt;" replace="">
+                  <fileset dir="${project.build.outputDirectory}" includes="**/*.jelly" />
+                </replaceregexp>
+              </target>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <configuration>
+          <!-- Make sure to keep the directives in test/pom.xml and war/pom.xml in sync with these. -->
+          <argLine>@{jacocoSurefireArgs} --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED -javaagent:"${org.mockito:mockito-core:jar}"</argLine>
+          <reuseForks>false</reuseForks>
+        </configuration>
+      </plugin>
+      <plugin>
+        <!-- set main class -->
+        <artifactId>maven-jar-plugin</artifactId>
+        <!-- version specified in grandparent pom -->
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>hudson.Main</mainClass>
+              <addClasspath>true</addClasspath>
+              <classpathPrefix />
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <reporting>
+    <plugins>
+      <plugin>
+        <!-- generate Jelly tag lib documentation -->
+        <groupId>io.jenkins.tools.maven</groupId>
+        <artifactId>stapler-maven-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <configuration>
+          <patterns>
+            <pattern>/lib/.*</pattern>
+          </patterns>
+        </configuration>
+      </plugin>
+      <plugin>
+        <!-- skip slow dependency analysis -->
+        <artifactId>maven-project-info-reports-plugin</artifactId>
+        <!-- Version specified in grandparent POM -->
+        <configuration>
+          <dependencyLocationsEnabled>false</dependencyLocationsEnabled>
+        </configuration>
+      </plugin>
+    </plugins>
+  </reporting>
+
+  <profiles>
+    <profile>
+      <id>debug</id>
+      <activation>
+        <activeByDefault>true</activeByDefault>
+      </activation>
+    </profile>
+    <profile>
+      <id>enable-jacoco</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.jacoco</groupId>
+            <artifactId>jacoco-maven-plugin</artifactId>
+            <!-- Version specified in parent POM -->
+            <executions>
+              <execution>
+                <goals>
+                  <goal>prepare-agent</goal>
+                </goals>
+                <configuration>
+                  <propertyName>jacocoSurefireArgs</propertyName>
+                  <includes>
+                    <include>hudson/*</include>
+                    <include>hudson/**/*</include>
+                    <include>jenkins/*</include>
+                    <include>jenkins/**/*</include>
+                    <include>org/jenkins/**/*</include>
+                  </includes>
+                  <excludes>
+                    <exclude>**/Messages.class</exclude>
+                  </excludes>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>

+ 551 - 0
jenkins/core/report-l10n.rb

@@ -0,0 +1,551 @@
+#!/usr/bin/env ruby
+require 'set'
+
+list = Set.new
+Dir.glob("src/**/*.properties").each do |f|
+  f = File.basename(f)
+  if f =~ /_(.._..)\.properties$/
+    list << $1
+  elsif f =~ /_(..)\.properties$/
+    list << $1
+  end
+end
+
+# from https://github.com/ged/linguistics/blob/master/lib/linguistics/iso639.rb
+module ISO639
+
+	# Hash of ISO639 2- and 3-letter language codes
+	LANGUAGE_CODES = {}
+
+    # Read everything after the __END__
+    _, data = File.read(__FILE__).split( /^__END__$/, 2 )
+
+	# To read the files, please note that one line of text contains one
+	# entry. An alpha-3 (bibliographic) code, an alpha-3 (terminologic)
+	# code (when given), an alpha-2 code (when given), an English name,
+	# and a French name of a language are all separated by pipe (|)
+	# characters. If one of these elements is not applicable to the entry,
+	# the field is left empty, i.e., a pipe (|) character immediately
+	# follows the preceding entry. The Line terminator is the LF character.
+
+	# bib_alpha3|term_alpha3|alpha2|eng_name|fre_name
+	# E.g., "eng||en|English|anglais"
+	data.lines do |line|
+		next unless line =~ /\|/ # Skip non-language lines
+		bib_alpha3, term_alpha3, alpha2, eng_name, fre_name = line.chomp.split( '|', 5 )
+		entry      = {
+			:eng_name => eng_name,
+			:fre_name => fre_name,
+			:codes    => [ bib_alpha3, alpha2, term_alpha3 ].reject {|item| item.empty? }
+		}
+		$stderr.puts "  adding language code entry %p from line: %p" %
+			[ entry, line ] if $DEBUG
+
+		LANGUAGE_CODES[ bib_alpha3.to_sym ] = entry
+		LANGUAGE_CODES[ alpha2.to_sym ] = entry if alpha2!=""
+	end
+
+end # module Linguistics::ISO639
+
+
+list = list.map do |l|
+  p,v = l.split("_",2)
+  
+  p ||= l
+
+  e = ISO639::LANGUAGE_CODES[p.to_sym]
+  if e then
+    e[:eng_name]+(v ? " (#{v})" : "")
+  else
+    v
+  end
+end
+
+list.sort.each do |x| puts x end
+
+__END__
+aar||aa|Afar|afar
+abk||ab|Abkhazian|abkhaze
+ace|||Achinese|aceh
+ach|||Acoli|acoli
+ada|||Adangme|adangme
+ady|||Adyghe; Adygei|adyghé
+afa|||Afro-Asiatic languages|afro-asiatiques, langues
+afh|||Afrihili|afrihili
+afr||af|Afrikaans|afrikaans
+ain|||Ainu|aïnou
+aka||ak|Akan|akan
+akk|||Akkadian|akkadien
+alb|sqi|sq|Albanian|albanais
+ale|||Aleut|aléoute
+alg|||Algonquian languages|algonquines, langues
+alt|||Southern Altai|altai du Sud
+amh||am|Amharic|amharique
+ang|||English, Old (ca.450-1100)|anglo-saxon (ca.450-1100)
+anp|||Angika|angika
+apa|||Apache languages|apaches, langues
+ara||ar|Arabic|arabe
+arc|||Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)|araméen d'empire (700-300 BCE)
+arg||an|Aragonese|aragonais
+arm|hye|hy|Armenian|arménien
+arn|||Mapudungun; Mapuche|mapudungun; mapuche; mapuce
+arp|||Arapaho|arapaho
+art|||Artificial languages|artificielles, langues
+arw|||Arawak|arawak
+asm||as|Assamese|assamais
+ast|||Asturian; Bable; Leonese; Asturleonese|asturien; bable; léonais; asturoléonais
+ath|||Athapascan languages|athapascanes, langues
+aus|||Australian languages|australiennes, langues
+ava||av|Avaric|avar
+ave||ae|Avestan|avestique
+awa|||Awadhi|awadhi
+aym||ay|Aymara|aymara
+aze||az|Azerbaijani|azéri
+bad|||Banda languages|banda, langues
+bai|||Bamileke languages|bamiléké, langues
+bak||ba|Bashkir|bachkir
+bal|||Baluchi|baloutchi
+bam||bm|Bambara|bambara
+ban|||Balinese|balinais
+baq|eus|eu|Basque|basque
+bas|||Basa|basa
+bat|||Baltic languages|baltes, langues
+bej|||Beja; Bedawiyet|bedja
+bel||be|Belarusian|biélorusse
+bem|||Bemba|bemba
+ben||bn|Bengali|bengali
+ber|||Berber languages|berbères, langues
+bho|||Bhojpuri|bhojpuri
+bih||bh|Bihari languages|langues biharis
+bik|||Bikol|bikol
+bin|||Bini; Edo|bini; edo
+bis||bi|Bislama|bichlamar
+bla|||Siksika|blackfoot
+bnt|||Bantu (Other)|bantoues, autres langues
+bos||bs|Bosnian|bosniaque
+bra|||Braj|braj
+bre||br|Breton|breton
+btk|||Batak languages|batak, langues
+bua|||Buriat|bouriate
+bug|||Buginese|bugi
+bul||bg|Bulgarian|bulgare
+bur|mya|my|Burmese|birman
+byn|||Blin; Bilin|blin; bilen
+cad|||Caddo|caddo
+cai|||Central American Indian languages|amérindiennes de L'Amérique centrale, langues
+car|||Galibi Carib|karib; galibi; carib
+cat||ca|Catalan; Valencian|catalan; valencien
+cau|||Caucasian languages|caucasiennes, langues
+ceb|||Cebuano|cebuano
+cel|||Celtic languages|celtiques, langues; celtes, langues
+cha||ch|Chamorro|chamorro
+chb|||Chibcha|chibcha
+che||ce|Chechen|tchétchène
+chg|||Chagatai|djaghataï
+chi|zho|zh|Chinese|chinois
+chk|||Chuukese|chuuk
+chm|||Mari|mari
+chn|||Chinook jargon|chinook, jargon
+cho|||Choctaw|choctaw
+chp|||Chipewyan; Dene Suline|chipewyan
+chr|||Cherokee|cherokee
+chu||cu|Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic|slavon d'église; vieux slave; slavon liturgique; vieux bulgare
+chv||cv|Chuvash|tchouvache
+chy|||Cheyenne|cheyenne
+cmc|||Chamic languages|chames, langues
+cop|||Coptic|copte
+cor||kw|Cornish|cornique
+cos||co|Corsican|corse
+cpe|||Creoles and pidgins, English based|créoles et pidgins basés sur l'anglais
+cpf|||Creoles and pidgins, French-based |créoles et pidgins basés sur le français
+cpp|||Creoles and pidgins, Portuguese-based |créoles et pidgins basés sur le portugais
+cre||cr|Cree|cree
+crh|||Crimean Tatar; Crimean Turkish|tatar de Crimé
+crp|||Creoles and pidgins |créoles et pidgins
+csb|||Kashubian|kachoube
+cus|||Cushitic languages|couchitiques, langues
+cze|ces|cs|Czech|tchèque
+dak|||Dakota|dakota
+dan||da|Danish|danois
+dar|||Dargwa|dargwa
+day|||Land Dayak languages|dayak, langues
+del|||Delaware|delaware
+den|||Slave (Athapascan)|esclave (athapascan)
+dgr|||Dogrib|dogrib
+din|||Dinka|dinka
+div||dv|Divehi; Dhivehi; Maldivian|maldivien
+doi|||Dogri|dogri
+dra|||Dravidian languages|dravidiennes, langues
+dsb|||Lower Sorbian|bas-sorabe
+dua|||Duala|douala
+dum|||Dutch, Middle (ca.1050-1350)|néerlandais moyen (ca. 1050-1350)
+dut|nld|nl|Dutch; Flemish|néerlandais; flamand
+dyu|||Dyula|dioula
+dzo||dz|Dzongkha|dzongkha
+efi|||Efik|efik
+egy|||Egyptian (Ancient)|égyptien
+eka|||Ekajuk|ekajuk
+elx|||Elamite|élamite
+eng||en|English|anglais
+enm|||English, Middle (1100-1500)|anglais moyen (1100-1500)
+epo||eo|Esperanto|espéranto
+est||et|Estonian|estonien
+ewe||ee|Ewe|éwé
+ewo|||Ewondo|éwondo
+fan|||Fang|fang
+fao||fo|Faroese|féroïen
+fat|||Fanti|fanti
+fij||fj|Fijian|fidjien
+fil|||Filipino; Pilipino|filipino; pilipino
+fin||fi|Finnish|finnois
+fiu|||Finno-Ugrian languages|finno-ougriennes, langues
+fon|||Fon|fon
+fre|fra|fr|French|français
+frm|||French, Middle (ca.1400-1600)|français moyen (1400-1600)
+fro|||French, Old (842-ca.1400)|français ancien (842-ca.1400)
+frr|||Northern Frisian|frison septentrional
+frs|||Eastern Frisian|frison oriental
+fry||fy|Western Frisian|frison occidental
+ful||ff|Fulah|peul
+fur|||Friulian|frioulan
+gaa|||Ga|ga
+gay|||Gayo|gayo
+gba|||Gbaya|gbaya
+gem|||Germanic languages|germaniques, langues
+geo|kat|ka|Georgian|géorgien
+ger|deu|de|German|allemand
+gez|||Geez|guèze
+gil|||Gilbertese|kiribati
+gla||gd|Gaelic; Scottish Gaelic|gaélique; gaélique écossais
+gle||ga|Irish|irlandais
+glg||gl|Galician|galicien
+glv||gv|Manx|manx; mannois
+gmh|||German, Middle High (ca.1050-1500)|allemand, moyen haut (ca. 1050-1500)
+goh|||German, Old High (ca.750-1050)|allemand, vieux haut (ca. 750-1050)
+gon|||Gondi|gond
+gor|||Gorontalo|gorontalo
+got|||Gothic|gothique
+grb|||Grebo|grebo
+grc|||Greek, Ancient (to 1453)|grec ancien (jusqu'à 1453)
+gre|ell|el|Greek, Modern (1453-)|grec moderne (après 1453)
+grn||gn|Guarani|guarani
+gsw|||Swiss German; Alemannic; Alsatian|suisse alémanique; alémanique; alsacien
+guj||gu|Gujarati|goudjrati
+gwi|||Gwich'in|gwich'in
+hai|||Haida|haida
+hat||ht|Haitian; Haitian Creole|haïtien; créole haïtien
+hau||ha|Hausa|haoussa
+haw|||Hawaiian|hawaïen
+heb||he|Hebrew|hébreu
+her||hz|Herero|herero
+hil|||Hiligaynon|hiligaynon
+him|||Himachali languages; Western Pahari languages|langues himachalis; langues paharis occidentales
+hin||hi|Hindi|hindi
+hit|||Hittite|hittite
+hmn|||Hmong|hmong
+hmo||ho|Hiri Motu|hiri motu
+hrv||hr|Croatian|croate
+hsb|||Upper Sorbian|haut-sorabe
+hun||hu|Hungarian|hongrois
+hup|||Hupa|hupa
+iba|||Iban|iban
+ibo||ig|Igbo|igbo
+ice|isl|is|Icelandic|islandais
+ido||io|Ido|ido
+iii||ii|Sichuan Yi; Nuosu|yi de Sichuan
+ijo|||Ijo languages|ijo, langues
+iku||iu|Inuktitut|inuktitut
+ile||ie|Interlingue; Occidental|interlingue
+ilo|||Iloko|ilocano
+ina||ia|Interlingua (International Auxiliary Language Association)|interlingua (langue auxiliaire internationale)
+inc|||Indic languages|indo-aryennes, langues
+ind||id|Indonesian|indonésien
+ine|||Indo-European languages|indo-européennes, langues
+inh|||Ingush|ingouche
+ipk||ik|Inupiaq|inupiaq
+ira|||Iranian languages|iraniennes, langues
+iro|||Iroquoian languages|iroquoises, langues
+ita||it|Italian|italien
+jav||jv|Javanese|javanais
+jbo|||Lojban|lojban
+jpn||ja|Japanese|japonais
+jpr|||Judeo-Persian|judéo-persan
+jrb|||Judeo-Arabic|judéo-arabe
+kaa|||Kara-Kalpak|karakalpak
+kab|||Kabyle|kabyle
+kac|||Kachin; Jingpho|kachin; jingpho
+kal||kl|Kalaallisut; Greenlandic|groenlandais
+kam|||Kamba|kamba
+kan||kn|Kannada|kannada
+kar|||Karen languages|karen, langues
+kas||ks|Kashmiri|kashmiri
+kau||kr|Kanuri|kanouri
+kaw|||Kawi|kawi
+kaz||kk|Kazakh|kazakh
+kbd|||Kabardian|kabardien
+kha|||Khasi|khasi
+khi|||Khoisan languages|khoïsan, langues
+khm||km|Central Khmer|khmer central
+kho|||Khotanese; Sakan|khotanais; sakan
+kik||ki|Kikuyu; Gikuyu|kikuyu
+kin||rw|Kinyarwanda|rwanda
+kir||ky|Kirghiz; Kyrgyz|kirghiz
+kmb|||Kimbundu|kimbundu
+kok|||Konkani|konkani
+kom||kv|Komi|kom
+kon||kg|Kongo|kongo
+kor||ko|Korean|coréen
+kos|||Kosraean|kosrae
+kpe|||Kpelle|kpellé
+krc|||Karachay-Balkar|karatchai balkar
+krl|||Karelian|carélien
+kro|||Kru languages|krou, langues
+kru|||Kurukh|kurukh
+kua||kj|Kuanyama; Kwanyama|kuanyama; kwanyama
+kum|||Kumyk|koumyk
+kur||ku|Kurdish|kurde
+kut|||Kutenai|kutenai
+lad|||Ladino|judéo-espagnol
+lah|||Lahnda|lahnda
+lam|||Lamba|lamba
+lao||lo|Lao|lao
+lat||la|Latin|latin
+lav||lv|Latvian|letton
+lez|||Lezghian|lezghien
+lim||li|Limburgan; Limburger; Limburgish|limbourgeois
+lin||ln|Lingala|lingala
+lit||lt|Lithuanian|lituanien
+lol|||Mongo|mongo
+loz|||Lozi|lozi
+ltz||lb|Luxembourgish; Letzeburgesch|luxembourgeois
+lua|||Luba-Lulua|luba-lulua
+lub||lu|Luba-Katanga|luba-katanga
+lug||lg|Ganda|ganda
+lui|||Luiseno|luiseno
+lun|||Lunda|lunda
+luo|||Luo (Kenya and Tanzania)|luo (Kenya et Tanzanie)
+lus|||Lushai|lushai
+mac|mkd|mk|Macedonian|macédonien
+mad|||Madurese|madourais
+mag|||Magahi|magahi
+mah||mh|Marshallese|marshall
+mai|||Maithili|maithili
+mak|||Makasar|makassar
+mal||ml|Malayalam|malayalam
+man|||Mandingo|mandingue
+mao|mri|mi|Maori|maori
+map|||Austronesian languages|austronésiennes, langues
+mar||mr|Marathi|marathe
+mas|||Masai|massaï
+may|msa|ms|Malay|malais
+mdf|||Moksha|moksa
+mdr|||Mandar|mandar
+men|||Mende|mendé
+mga|||Irish, Middle (900-1200)|irlandais moyen (900-1200)
+mic|||Mi'kmaq; Micmac|mi'kmaq; micmac
+min|||Minangkabau|minangkabau
+mis|||Uncoded languages|langues non codées
+mkh|||Mon-Khmer languages|môn-khmer, langues
+mlg||mg|Malagasy|malgache
+mlt||mt|Maltese|maltais
+mnc|||Manchu|mandchou
+mni|||Manipuri|manipuri
+mno|||Manobo languages|manobo, langues
+moh|||Mohawk|mohawk
+mon||mn|Mongolian|mongol
+mos|||Mossi|moré
+mul|||Multiple languages|multilingue
+mun|||Munda languages|mounda, langues
+mus|||Creek|muskogee
+mwl|||Mirandese|mirandais
+mwr|||Marwari|marvari
+myn|||Mayan languages|maya, langues
+myv|||Erzya|erza
+nah|||Nahuatl languages|nahuatl, langues
+nai|||North American Indian languages|nord-amérindiennes, langues
+nap|||Neapolitan|napolitain
+nau||na|Nauru|nauruan
+nav||nv|Navajo; Navaho|navaho
+nbl||nr|Ndebele, South; South Ndebele|ndébélé du Sud
+nde||nd|Ndebele, North; North Ndebele|ndébélé du Nord
+ndo||ng|Ndonga|ndonga
+nds|||Low German; Low Saxon; German, Low; Saxon, Low|bas allemand; bas saxon; allemand, bas; saxon, bas
+nep||ne|Nepali|népalais
+new|||Nepal Bhasa; Newari|nepal bhasa; newari
+nia|||Nias|nias
+nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues
+niu|||Niuean|niué
+nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien
+nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål
+nog|||Nogai|nogaï; nogay
+non|||Norse, Old|norrois, vieux
+nor||no|Norwegian|norvégien
+nqo|||N'Ko|n'ko
+nso|||Pedi; Sepedi; Northern Sotho|pedi; sepedi; sotho du Nord
+nub|||Nubian languages|nubiennes, langues
+nwc|||Classical Newari; Old Newari; Classical Nepal Bhasa|newari classique
+nya||ny|Chichewa; Chewa; Nyanja|chichewa; chewa; nyanja
+nym|||Nyamwezi|nyamwezi
+nyn|||Nyankole|nyankolé
+nyo|||Nyoro|nyoro
+nzi|||Nzima|nzema
+oci||oc|Occitan (post 1500); Provençal|occitan (après 1500); provençal
+oji||oj|Ojibwa|ojibwa
+ori||or|Oriya|oriya
+orm||om|Oromo|galla
+osa|||Osage|osage
+oss||os|Ossetian; Ossetic|ossète
+ota|||Turkish, Ottoman (1500-1928)|turc ottoman (1500-1928)
+oto|||Otomian languages|otomi, langues
+paa|||Papuan languages|papoues, langues
+pag|||Pangasinan|pangasinan
+pal|||Pahlavi|pahlavi
+pam|||Pampanga; Kapampangan|pampangan
+pan||pa|Panjabi; Punjabi|pendjabi
+pap|||Papiamento|papiamento
+pau|||Palauan|palau
+peo|||Persian, Old (ca.600-400 B.C.)|perse, vieux (ca. 600-400 av. J.-C.)
+per|fas|fa|Persian|persan
+phi|||Philippine languages|philippines, langues
+phn|||Phoenician|phénicien
+pli||pi|Pali|pali
+pol||pl|Polish|polonais
+pon|||Pohnpeian|pohnpei
+por||pt|Portuguese|portugais
+pra|||Prakrit languages|prâkrit, langues
+pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
+pus||ps|Pushto; Pashto|pachto
+qaa-qtz|||Reserved for local use|réservée à l'usage local
+que||qu|Quechua|quechua
+raj|||Rajasthani|rajasthani
+rap|||Rapanui|rapanui
+rar|||Rarotongan; Cook Islands Maori|rarotonga; maori des îles Cook
+roa|||Romance languages|romanes, langues
+roh||rm|Romansh|romanche
+rom|||Romany|tsigane
+rum|ron|ro|Romanian; Moldavian; Moldovan|roumain; moldave
+run||rn|Rundi|rundi
+rup|||Aromanian; Arumanian; Macedo-Romanian|aroumain; macédo-roumain
+rus||ru|Russian|russe
+sad|||Sandawe|sandawe
+sag||sg|Sango|sango
+sah|||Yakut|iakoute
+sai|||South American Indian (Other)|indiennes d'Amérique du Sud, autres langues
+sal|||Salishan languages|salishennes, langues
+sam|||Samaritan Aramaic|samaritain
+san||sa|Sanskrit|sanskrit
+sas|||Sasak|sasak
+sat|||Santali|santal
+scn|||Sicilian|sicilien
+sco|||Scots|écossais
+sel|||Selkup|selkoupe
+sem|||Semitic languages|sémitiques, langues
+sga|||Irish, Old (to 900)|irlandais ancien (jusqu'à 900)
+sgn|||Sign Languages|langues des signes
+shn|||Shan|chan
+sid|||Sidamo|sidamo
+sin||si|Sinhala; Sinhalese|singhalais
+sio|||Siouan languages|sioux, langues
+sit|||Sino-Tibetan languages|sino-tibétaines, langues
+sla|||Slavic languages|slaves, langues
+slo|slk|sk|Slovak|slovaque
+slv||sl|Slovenian|slovène
+sma|||Southern Sami|sami du Sud
+sme||se|Northern Sami|sami du Nord
+smi|||Sami languages|sames, langues
+smj|||Lule Sami|sami de Lule
+smn|||Inari Sami|sami d'Inari
+smo||sm|Samoan|samoan
+sms|||Skolt Sami|sami skolt
+sna||sn|Shona|shona
+snd||sd|Sindhi|sindhi
+snk|||Soninke|soninké
+sog|||Sogdian|sogdien
+som||so|Somali|somali
+son|||Songhai languages|songhai, langues
+sot||st|Sotho, Southern|sotho du Sud
+spa||es|Spanish; Castilian|espagnol; castillan
+srd||sc|Sardinian|sarde
+srn|||Sranan Tongo|sranan tongo
+srp||sr|Serbian|serbe
+srr|||Serer|sérère
+ssa|||Nilo-Saharan languages|nilo-sahariennes, langues
+ssw||ss|Swati|swati
+suk|||Sukuma|sukuma
+sun||su|Sundanese|soundanais
+sus|||Susu|soussou
+sux|||Sumerian|sumérien
+swa||sw|Swahili|swahili
+swe||sv|Swedish|suédois
+syc|||Classical Syriac|syriaque classique
+syr|||Syriac|syriaque
+tah||ty|Tahitian|tahitien
+tai|||Tai languages|tai, langues
+tam||ta|Tamil|tamoul
+tat||tt|Tatar|tatar
+tel||te|Telugu|télougou
+tem|||Timne|temne
+ter|||Tereno|tereno
+tet|||Tetum|tetum
+tgk||tg|Tajik|tadjik
+tgl||tl|Tagalog|tagalog
+tha||th|Thai|thaï
+tib|bod|bo|Tibetan|tibétain
+tig|||Tigre|tigré
+tir||ti|Tigrinya|tigrigna
+tiv|||Tiv|tiv
+tkl|||Tokelau|tokelau
+tlh|||Klingon; tlhIngan-Hol|klingon
+tli|||Tlingit|tlingit
+tmh|||Tamashek|tamacheq
+tog|||Tonga (Nyasa)|tonga (Nyasa)
+ton||to|Tonga (Tonga Islands)|tongan (Îles Tonga)
+tpi|||Tok Pisin|tok pisin
+tsi|||Tsimshian|tsimshian
+tsn||tn|Tswana|tswana
+tso||ts|Tsonga|tsonga
+tuk||tk|Turkmen|turkmène
+tum|||Tumbuka|tumbuka
+tup|||Tupi languages|tupi, langues
+tur||tr|Turkish|turc
+tut|||Altaic languages|altaïques, langues
+tvl|||Tuvalu|tuvalu
+twi||tw|Twi|twi
+tyv|||Tuvinian|touva
+udm|||Udmurt|oudmourte
+uga|||Ugaritic|ougaritique
+uig||ug|Uighur; Uyghur|ouïgour
+ukr||uk|Ukrainian|ukrainien
+umb|||Umbundu|umbundu
+und|||Undetermined|indéterminée
+urd||ur|Urdu|ourdou
+uzb||uz|Uzbek|ouszbek
+vai|||Vai|vaï
+ven||ve|Venda|venda
+vie||vi|Vietnamese|vietnamien
+vol||vo|Volapük|volapük
+vot|||Votic|vote
+wak|||Wakashan languages|wakashanes, langues
+wal|||Walamo|walamo
+war|||Waray|waray
+was|||Washo|washo
+wel|cym|cy|Welsh|gallois
+wen|||Sorbian languages|sorabes, langues
+wln||wa|Walloon|wallon
+wol||wo|Wolof|wolof
+xal|||Kalmyk; Oirat|kalmouk; oïrat
+xho||xh|Xhosa|xhosa
+yao|||Yao|yao
+yap|||Yapese|yapois
+yid||yi|Yiddish|yiddish
+yor||yo|Yoruba|yoruba
+ypk|||Yupik languages|yupik, langues
+zap|||Zapotec|zapotèque
+zbl|||Blissymbols; Blissymbolics; Bliss|symboles Bliss; Bliss
+zen|||Zenaga|zenaga
+zha||za|Zhuang; Chuang|zhuang; chuang
+znd|||Zande languages|zandé, langues
+zul||zu|Zulu|zoulou
+zun|||Zuni|zuni
+zxx|||No linguistic content; Not applicable|pas de contenu linguistique; non applicable
+zza|||Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki|zaza; dimili; dimli; kirdki; kirmanjki; zazaki

+ 2 - 0
jenkins/core/src/filter/resources/hudson/model/hudson-version.properties

@@ -0,0 +1,2 @@
+# TODO unused?
+version=${project.version}

+ 2 - 0
jenkins/core/src/filter/resources/jenkins/model/jenkins-version.properties

@@ -0,0 +1,2 @@
+version=${project.version}
+changelog.url=${changelog.url}

+ 6 - 0
jenkins/core/src/filter/resources/jenkins/slaves/remoting-info.properties

@@ -0,0 +1,6 @@
+# Remoting version, which is embedded into the core
+# This version MAY differ from what is really classloaded (see the "Pluggable Core Components", JENKINS-41196)
+remoting.embedded.version=${remoting.version}
+
+# Minimum Remoting version on external agents which is supported by the core
+remoting.minimum.supported.version=${remoting.minimum.supported.version}

+ 57 - 0
jenkins/core/src/main/antlr4/hudson/model/labels/LabelExpressionLexer.g4

@@ -0,0 +1,57 @@
+lexer grammar LabelExpressionLexer;
+
+AND
+   : '&&'
+   ;
+
+OR
+   : '||'
+   ;
+
+NOT
+   : '!'
+   ;
+
+IMPLIES
+   : '->'
+   ;
+
+IFF
+   : '<->'
+   ;
+
+LPAREN
+   : '('
+   ;
+
+RPAREN
+   : ')'
+   ;
+
+fragment IDENTIFIER_PART
+   : ~ ('&' | '|' | '!' | '<' | '>' | '(' | ')' | ' ' | '\t' | '"' | '\'' | '-')
+   ;
+
+ATOM
+/*
+    the real check of valid identifier happens in LabelAtom.get()
+
+    https://www.antlr2.org/doc/lexer.html#usingexplicit
+    If we are seeing currently a '-', we check that the next char is not a '>' which will be a IMPLIES.
+    Otherwise the ATOM and the IMPLIES will collide and expr like a->b will just be parsed as ATOM (without spaces)
+*/
+   
+   : (
+   { _input.LA(2) != '>' }? '-' | IDENTIFIER_PART)+
+   ;
+
+WS
+   : (' ' | '\t')+ -> skip
+   ;
+
+STRINGLITERAL
+   : '"' ('\\' ('b' | 't' | 'n' | 'f' | 'r' | '"' | '\'' | '\\') /* escape */
+   
+   | ~ ('\\' | '"' | '\r' | '\n'))* '"'
+   ;
+

+ 56 - 0
jenkins/core/src/main/antlr4/hudson/model/labels/LabelExpressionParser.g4

@@ -0,0 +1,56 @@
+parser grammar LabelExpressionParser;
+
+@ header
+{
+import hudson.model.Label;
+}
+
+options { tokenVocab = LabelExpressionLexer; }
+// order of precedence is as per http://en.wikipedia.org/wiki/Logical_connective#Order_of_precedence
+
+expr returns[Label l]
+   : term1
+   { $l=$term1.ctx.l; } EOF
+   ;
+
+term1 returns[Label l] locals[Label r]
+   : term2
+   { $l=$term2.ctx.l; } (IFF term2
+   { $r=$term2.ctx.l; $l=$l.iff($r); })*
+   ;
+   // (a->b)->c != a->(b->c) (for example in case of a=F,b=T,c=F) so don't allow chaining
+   
+term2 returns[Label l] locals[Label r]
+   : term3
+   { $l=$term3.ctx.l; } (IMPLIES term3
+   { $r=$term3.ctx.l; $l=$l.implies($r); })?
+   ;
+
+term3 returns[Label l] locals[Label r]
+   : term4
+   { $l=$term4.ctx.l; } (OR term4
+   { $r=$term4.ctx.l; $l=$l.or($r); })*
+   ;
+
+term4 returns[Label l] locals[Label r]
+   : term5
+   { $l=$term5.ctx.l; } (AND term5
+   { $r=$term5.ctx.l; $l=$l.and($r); })*
+   ;
+
+term5 returns[Label l]
+   : term6
+   { $l=$term6.ctx.l; }
+   | NOT term6
+   { $l=$term6.ctx.l; $l=$l.not(); }
+   ;
+
+term6 returns[Label l]
+   : LPAREN term1 RPAREN
+   { $l=$term1.ctx.l ; $l=$l.paren(); }
+   | ATOM
+   { $l=LabelAtom.get($ATOM.getText()); }
+   | STRINGLITERAL
+   { $l=LabelAtom.get(hudson.util.QuotedStringTokenizer.unquote($STRINGLITERAL.getText())); }
+   ;
+

+ 70 - 0
jenkins/core/src/main/antlr4/hudson/scheduler/CrontabLexer.g4

@@ -0,0 +1,70 @@
+lexer grammar CrontabLexer;
+
+TOKEN
+   : ('0' .. '9')+
+   ;
+
+WS
+   : (' ' | '\t')+
+   ;
+
+MINUS
+   : '-'
+   ;
+
+STAR
+   : '*'
+   ;
+
+DIV
+   : '/'
+   ;
+
+OR
+   : ','
+   ;
+
+AT
+   : '@'
+   ;
+
+H
+   : 'H'
+   ;
+
+LPAREN
+   : '('
+   ;
+
+RPAREN
+   : ')'
+   ;
+
+YEARLY
+   : 'yearly'
+   ;
+
+ANNUALLY
+   : 'annually'
+   ;
+
+MONTHLY
+   : 'monthly'
+   ;
+
+WEEKLY
+   : 'weekly'
+   ;
+
+DAILY
+   : 'daily'
+   ;
+
+MIDNIGHT
+   : 'midnight'
+   ;
+
+HOURLY
+   : 'hourly'
+   ;
+

+ 83 - 0
jenkins/core/src/main/antlr4/hudson/scheduler/CrontabParser.g4

@@ -0,0 +1,83 @@
+parser grammar CrontabParser;
+
+
+options { tokenVocab = CrontabLexer; superClass = BaseParser; }
+startRule[CronTab table]
+   : expr[0]
+   { $table.bits[0]=$expr.ctx.bits; } WS expr[1]
+   { $table.bits[1]=$expr.ctx.bits; } WS expr[2]
+   { $table.bits[2]=$expr.ctx.bits; } WS expr[3]
+   { $table.bits[3]=$expr.ctx.bits; } WS expr[4]
+   { $table.dayOfWeek=(int)$expr.ctx.bits; } EOF
+   | (AT ('yearly'
+   {
+           $table.set("H H H H *",getHashForTokens());
+         } | 'annually'
+   {
+           $table.set("H H H H *",getHashForTokens());
+         } | 'monthly'
+   {
+           $table.set("H H H * *",getHashForTokens());
+         } | 'weekly'
+   {
+           $table.set("H H * * H",getHashForTokens());
+         } | 'daily'
+   {
+           $table.set("H H * * *",getHashForTokens());
+         } | 'midnight'
+   {
+           $table.set("H H(0-2) * * *",getHashForTokens());
+         } | 'hourly'
+   {
+           $table.set("H * * * *",getHashForTokens());
+         }))
+   ;
+
+expr[int field] returns[long bits=0] locals[long lhs, long rhs=0]
+   : term[field]
+   { $lhs = $term.ctx.bits; } (OR expr[field]
+   { $rhs = $expr.ctx.bits; })?
+   {
+  $bits = $lhs|$rhs;
+ }
+   ;
+
+term[int field] returns[long bits=0] locals[int d=NO_STEP, int s, int e]
+   : token
+   { $s=$token.ctx.value; } MINUS token
+   { $e=$token.ctx.value; } (DIV token
+   { $d=$token.ctx.value; })?
+   {
+     $bits = doRange($s,$e,$d,$field);
+   }
+   | token
+   {
+     rangeCheck($token.ctx.value,$field);
+     $bits = 1L<<$token.ctx.value;
+   }
+   | STAR (DIV token
+   { $d=$token.ctx.value; })?
+   {
+     $bits = doRange($d,$field);
+   }
+   | 'H' LPAREN token
+   { $s=$token.ctx.value; } MINUS token
+   { $e=$token.ctx.value; } RPAREN (DIV token
+   { $d=$token.ctx.value; })?
+   {
+     $bits = doHash($s,$e,$d,$field);
+   }
+   | 'H' (DIV token
+   { $d=$token.ctx.value; })?
+   {
+     $bits = doHash($d,$field);
+   }
+   ;
+
+token returns[int value=0]
+   : TOKEN
+   {
+     $value = Integer.parseInt($TOKEN.getText());
+   }
+   ;
+

+ 21 - 0
jenkins/core/src/main/java/antlr/ANTLRException.java

@@ -0,0 +1,21 @@
+package antlr;
+
+/**
+ * This class is for binary compatibility for older plugins that import {@link ANTLRException}.
+ *
+ * @deprecated use {@link IllegalArgumentException}
+ */
+@Deprecated
+public class ANTLRException extends IllegalArgumentException {
+    public ANTLRException(String message) {
+        super(message);
+    }
+
+    public ANTLRException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ANTLRException(Throwable cause) {
+        super(cause);
+    }
+}

+ 49 - 0
jenkins/core/src/main/java/hudson/AbortException.java

@@ -0,0 +1,49 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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.
+ */
+
+package hudson;
+
+import java.io.IOException;
+
+/**
+ * Signals a failure where the error was anticipated and diagnosed.
+ * When this exception is caught,
+ * the stack trace will not be printed, and the build will be marked as a failure.
+ *
+ * @author Kohsuke Kawaguchi
+*/
+public class AbortException extends IOException {
+    public AbortException() {
+    }
+
+    /**
+     * When this exception is caught, the specified message will be reported.
+     * @since 1.298
+     */
+    public AbortException(String message) {
+        super(message);
+    }
+
+    private static final long serialVersionUID = 1L;
+}

+ 73 - 0
jenkins/core/src/main/java/hudson/AboutJenkins.java

@@ -0,0 +1,73 @@
+package hudson;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.model.ManagementLink;
+import hudson.security.Permission;
+import java.net.URL;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * Show "About Jenkins" link.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@Extension @Symbol("about")
+public class AboutJenkins extends ManagementLink {
+    @Override
+    public String getIconFileName() {
+        return "symbol-jenkins";
+    }
+
+    @Override
+    public String getUrlName() {
+        return "about";
+    }
+
+    @Override
+    public String getDisplayName() {
+        return Messages.AboutJenkins_DisplayName();
+    }
+
+    @Override
+    public String getDescription() {
+        return Messages.AboutJenkins_Description();
+    }
+
+    @Restricted(NoExternalUse.class)
+    public URL getLicensesURL() {
+        return AboutJenkins.class.getResource("/META-INF/licenses.xml");
+    }
+
+    @NonNull
+    @Override
+    public Permission getRequiredPermission() {
+        return Jenkins.READ;
+    }
+
+    @Override
+    public boolean hasRequiredPermission() {
+        return Jenkins.get().hasAnyPermission(Jenkins.MANAGE_AND_SYSTEM_READ);
+    }
+
+    @NonNull
+    @Override
+    public Category getCategory() {
+        return Category.STATUS;
+    }
+
+    /**
+     * Used to display subpages for plugin information.
+     * @return the plugin with the given short name.
+     */
+    @CheckForNull
+    @Restricted(DoNotUse.class)
+    public PluginWrapper getPlugin(String shortName) {
+        Plugin plugin = Jenkins.get().getPlugin(shortName);
+        return plugin != null ? plugin.getWrapper() : null;
+    }
+}

+ 166 - 0
jenkins/core/src/main/java/hudson/AbstractMarkupText.java

@@ -0,0 +1,166 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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.
+ */
+
+package hudson;
+
+import hudson.MarkupText.SubText;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Common part between {@link MarkupText} and {@link MarkupText.SubText}.
+ *
+ * <p>
+ * See {@link MarkupText} for more discussion about what this class represents.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.250
+ */
+public abstract class AbstractMarkupText {
+    /*package*/ AbstractMarkupText() {} // limit who can subclass this type.
+
+
+    /**
+     * Returns the plain text portion of this {@link MarkupText} without
+     * any markup, nor any escape.
+     */
+    public abstract String getText();
+
+    public char charAt(int idx) {
+        return getText().charAt(idx);
+    }
+
+    /**
+     * Length of the plain text.
+     */
+    public final int length() {
+        return getText().length();
+    }
+
+    /**
+     * Returns a subtext.
+     *
+     * @param end
+     *      If negative, -N means "trim the last N-1 chars". That is, (s,-1) is the same as (s,length)
+     */
+    public abstract MarkupText.SubText subText(int start, int end);
+
+    /**
+     * Adds a start tag and end tag at the specified position.
+     *
+     * <p>
+     * For example, if the text was "abc", then {@code addMarkup(1,2,"<b>","</b>")}
+     * would generate {@code "a<b>b</b>c"}
+     */
+    public abstract void addMarkup(int startPos, int endPos, String startTag, String endTag);
+
+    /**
+     * Inserts an A tag that surrounds the given position.
+     *
+     * @since 1.349
+     */
+    public void addHyperlink(int startPos, int endPos, String url) {
+        addMarkup(startPos, endPos, "<a href='" + Functions.htmlAttributeEscape(url) + "'>", "</a>");
+    }
+
+    /**
+     * Inserts an A tag that surrounds the given position.
+     * But this hyperlink is less visible.
+     *
+     * @since 1.395
+     */
+    public void addHyperlinkLowKey(int startPos, int endPos, String url) {
+        addMarkup(startPos, endPos, "<a class='lowkey' href='" + Functions.htmlAttributeEscape(url) + "'>", "</a>");
+    }
+
+    /**
+     * Hides the given text.
+     */
+    public void hide(int startPos, int endPos) {
+        addMarkup(startPos, endPos, "<span style='display:none'>", "</span>");
+    }
+
+    /**
+     * Adds a start tag and end tag around the entire text
+     */
+    public final void wrapBy(String startTag, String endTag) {
+        addMarkup(0, length(), startTag, endTag);
+    }
+
+    /**
+     * Find the first occurrence of the given pattern in this text, or null.
+     *
+     * @since 1.349
+     */
+    public MarkupText.SubText findToken(Pattern pattern) {
+        String text = getText();
+        Matcher m = pattern.matcher(text);
+
+        if (m.find())
+            return createSubText(m);
+
+        return null;
+    }
+
+    /**
+     * Find all "tokens" that match the given pattern in this text.
+     *
+     * <p>
+     * A token is like a substring, except that it's aware of word boundaries.
+     * For example, while "bc" is a string of "abc", calling {@code findTokens}
+     * with "bc" as a pattern on string "abc" won't match anything.
+     *
+     * <p>
+     * This method is convenient for finding keywords that follow a certain syntax
+     * from natural text. You can then use {@link MarkupText.SubText#surroundWith(String,String)}
+     * to put mark up around such text.
+     */
+    public List<MarkupText.SubText> findTokens(Pattern pattern) {
+        String text = getText();
+        Matcher m = pattern.matcher(text);
+        List<SubText> r = new ArrayList<>();
+
+        while (m.find()) {
+            int idx = m.start();
+            if (idx > 0) {
+                char ch = text.charAt(idx - 1);
+                if (Character.isLetter(ch) || Character.isDigit(ch))
+                    continue;   // not at a word boundary
+            }
+            idx = m.end();
+            if (idx < text.length()) {
+                char ch = text.charAt(idx);
+                if (Character.isLetter(ch) || Character.isDigit(ch))
+                    continue;   // not at a word boundary
+            }
+            r.add(createSubText(m));
+        }
+
+        return r;
+    }
+
+    protected abstract SubText createSubText(Matcher m);
+}

+ 165 - 0
jenkins/core/src/main/java/hudson/BulkChange.java

@@ -0,0 +1,165 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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.
+ */
+
+package hudson;
+
+import hudson.model.Saveable;
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Transaction-like object that can be used to make a bunch of changes to an object, and defer the
+ * {@link Saveable#save()} until the end.
+ *
+ * <p>
+ * The usage of {@link BulkChange} needs to follow a specific closure-like pattern, namely:
+ *
+ * <pre>
+ * try (BulkChange bc = new BulkChange(someObject)) {
+ *    ... make changes to 'someObject'
+ *    bc.commit();
+ * }
+ * </pre>
+ *
+ * <p>
+ * Use of this method is optional. If {@link BulkChange} is not used, individual mutator
+ * will perform the save operation, and things will just run somewhat slower.
+ *
+ *
+ * <h2>Cooperation from {@link Saveable}</h2>
+ * <p>
+ * For this class to work as intended, {@link Saveable} implementations need to co-operate.
+ * Namely,
+ *
+ * <ol>
+ * <li>
+ * Mutator methods should invoke {@code this.save()} so that if the method is called outside
+ * a {@link BulkChange}, the result will be saved immediately.
+ *
+ * <li>
+ * In the {@code save()} method implementation, use {@link #contains(Saveable)} and
+ * only perform the actual I/O operation when this method returns false.
+ * </ol>
+ *
+ * <p>
+ * See {@link jenkins.model.Jenkins#save()} as an example if you are not sure how to implement {@link Saveable}.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.249
+ */
+public class BulkChange implements Closeable {
+    private final Saveable saveable;
+    public final Exception allocator;
+    private final BulkChange parent;
+
+    private boolean completed;
+
+    public BulkChange(Saveable saveable) {
+        this.parent = current();
+        this.saveable = saveable;
+        // remember who allocated this object in case
+        // someone forgot to call save() at the end.
+        allocator = new Exception();
+
+        // in effect at construction
+        INSCOPE.set(this);
+    }
+
+    /**
+     * Saves the accumulated changes.
+     */
+    public void commit() throws IOException {
+        if (completed)   return;
+        completed = true;
+
+        // move this object out of the scope first before save, or otherwise the save() method will do nothing.
+        pop();
+        saveable.save();
+    }
+
+    /**
+     * Alias for {@link #abort()} to make {@link BulkChange} auto-closeable.
+     */
+    @Override
+    public void close() {
+        abort();
+    }
+
+    /**
+     * Exits the scope of {@link BulkChange} without saving the changes.
+     *
+     * <p>
+     * This can be used when a bulk change fails in the middle.
+     * Note that unlike a real transaction, this will not roll back the state of the object.
+     *
+     * <p>
+     * The abort method can be called after the commit method, in which case this method does nothing.
+     * This is so that {@link BulkChange} can be used naturally in the try/finally block.
+     */
+    public void abort() {
+        if (completed)   return;
+        completed = true;
+        pop();
+    }
+
+    private void pop() {
+        if (current() != this)
+            throw new AssertionError("Trying to save BulkChange that's not in scope");
+        INSCOPE.set(parent);
+    }
+
+    /**
+     * {@link BulkChange}s that are effective currently.
+     */
+    private static final ThreadLocal<BulkChange> INSCOPE = new ThreadLocal<>();
+
+    /**
+     * Gets the {@link BulkChange} instance currently in scope for the current thread.
+     */
+    public static BulkChange current() {
+        return INSCOPE.get();
+    }
+
+    /**
+     * Checks if the given {@link Saveable} is currently in the bulk change.
+     *
+     * <p>
+     * The expected usage is from the {@link Saveable#save()} implementation to check
+     * if the actual persistence should happen now or not.
+     */
+    public static boolean contains(Saveable s) {
+        for (BulkChange b = current(); b != null; b = b.parent)
+            if (b.saveable == s || b.saveable == ALL)
+                return true;
+        return false;
+    }
+
+    /**
+     * Magic {@link Saveable} instance that can make {@link BulkChange} veto
+     * all the save operations by making the {@link #contains(Saveable)} method return
+     * true for everything.
+     */
+    public static final Saveable ALL = () -> {
+    };
+}

+ 699 - 0
jenkins/core/src/main/java/hudson/ClassicPluginStrategy.java

@@ -0,0 +1,699 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jean-Baptiste Quenot, Tom Huybrechts
+ *
+ * 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.
+ */
+
+package hudson;
+
+import static org.apache.commons.io.FilenameUtils.getBaseName;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.Plugin.DummyImpl;
+import hudson.PluginWrapper.Dependency;
+import hudson.model.Hudson;
+import hudson.util.CyclicGraphDetector;
+import hudson.util.CyclicGraphDetector.CycleDetectedException;
+import hudson.util.DelegatingClassLoader;
+import hudson.util.IOUtils;
+import hudson.util.MaskingClassLoader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.ClassLoaderReflectionToolkit;
+import jenkins.ExtensionFilter;
+import jenkins.plugins.DetachedPluginsUtil;
+import jenkins.util.URLClassLoader2;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.taskdefs.Expand;
+import org.apache.tools.ant.taskdefs.Zip;
+import org.apache.tools.ant.types.FileSet;
+import org.apache.tools.ant.types.PatternSet;
+import org.apache.tools.ant.types.Resource;
+import org.apache.tools.ant.types.ZipFileSet;
+import org.apache.tools.ant.types.resources.MappedResourceCollection;
+import org.apache.tools.ant.util.GlobPatternMapper;
+import org.apache.tools.zip.ZipEntry;
+import org.apache.tools.zip.ZipExtraField;
+import org.apache.tools.zip.ZipOutputStream;
+
+public class ClassicPluginStrategy implements PluginStrategy {
+
+    private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
+
+    /**
+     * Filter for jar files.
+     */
+    private static final FilenameFilter JAR_FILTER = (dir, name) -> name.endsWith(".jar");
+
+    private final PluginManager pluginManager;
+
+    public ClassicPluginStrategy(PluginManager pluginManager) {
+        this.pluginManager = pluginManager;
+    }
+
+    @Override public String getShortName(File archive) throws IOException {
+        Manifest manifest;
+        if (!archive.exists()) {
+            throw new FileNotFoundException("Failed to load " + archive + ". The file does not exist");
+        } else if (!archive.isFile()) {
+            throw new FileNotFoundException("Failed to load " + archive + ". It is not a file");
+        }
+
+        if (isLinked(archive)) {
+            manifest = loadLinkedManifest(archive);
+        } else {
+            try (JarFile jf = new JarFile(archive, false)) {
+                manifest = jf.getManifest();
+            } catch (IOException ex) {
+                // Mention file name in the exception
+                throw new IOException("Failed to load " + archive, ex);
+            }
+        }
+        return PluginWrapper.computeShortName(manifest, archive.getName());
+    }
+
+    private static boolean isLinked(File archive) {
+        return archive.getName().endsWith(".hpl") || archive.getName().endsWith(".jpl");
+    }
+
+    private static Manifest loadLinkedManifest(File archive) throws IOException {
+            // resolve the .hpl file to the location of the manifest file
+            try {
+                // Locate the manifest
+                String firstLine;
+                try (InputStream manifestHeaderInput = Files.newInputStream(archive.toPath())) {
+                    firstLine = IOUtils.readFirstLine(manifestHeaderInput, "UTF-8");
+                } catch (InvalidPathException e) {
+                    throw new IOException(e);
+                }
+                //noinspection StatementWithEmptyBody
+                if (firstLine.startsWith("Manifest-Version:")) {
+                    // this is the manifest already
+                } else {
+                    // indirection
+                    archive = resolve(archive, firstLine);
+                }
+
+                // Read the manifest
+                try (InputStream manifestInput = Files.newInputStream(archive.toPath())) {
+                    return new Manifest(manifestInput);
+                } catch (InvalidPathException e) {
+                    throw new IOException(e);
+                }
+            } catch (IOException e) {
+                throw new IOException("Failed to load " + archive, e);
+            }
+    }
+
+    @Override public PluginWrapper createPluginWrapper(File archive) throws IOException {
+        final Manifest manifest;
+
+        URL baseResourceURL;
+        File expandDir = null;
+        // if .hpi, this is the directory where war is expanded
+
+        boolean isLinked = isLinked(archive);
+        if (isLinked) {
+            manifest = loadLinkedManifest(archive);
+        } else {
+            if (archive.isDirectory()) { // already expanded
+                expandDir = archive;
+            } else {
+                File f = pluginManager.getWorkDir();
+                expandDir =  new File(f == null ? archive.getParentFile() : f, getBaseName(archive.getName()));
+                explode(archive, expandDir);
+            }
+
+            File manifestFile = new File(expandDir, PluginWrapper.MANIFEST_FILENAME);
+            if (!manifestFile.exists()) {
+                throw new IOException(
+                        "Plugin installation failed. No manifest at "
+                                + manifestFile);
+            }
+            try (InputStream fin = Files.newInputStream(manifestFile.toPath())) {
+                manifest = new Manifest(fin);
+            } catch (InvalidPathException e) {
+                throw new IOException(e);
+            }
+            String canonicalName = manifest.getMainAttributes().getValue("Short-Name") + ".jpi";
+            if (!archive.getName().equals(canonicalName)) {
+                LOGGER.warning(() -> "encountered " + archive + " under a nonstandard name; expected " + canonicalName);
+            }
+        }
+
+        final Attributes atts = manifest.getMainAttributes();
+
+        // TODO: define a mechanism to hide classes
+        // String export = manifest.getMainAttributes().getValue("Export");
+
+        List<File> paths = new ArrayList<>();
+        if (isLinked) {
+            parseClassPath(manifest, archive, paths, "Libraries", ",");
+            parseClassPath(manifest, archive, paths, "Class-Path", " +"); // backward compatibility
+
+            baseResourceURL = resolve(archive, atts.getValue("Resource-Path")).toURI().toURL();
+        } else {
+            File classes = new File(expandDir, "WEB-INF/classes");
+            if (classes.exists()) { // should not normally happen, due to createClassJarFromWebInfClasses
+                LOGGER.log(Level.WARNING, "Deprecated unpacked classes directory found in {0}", classes);
+                paths.add(classes);
+            }
+            File lib = new File(expandDir, "WEB-INF/lib");
+            File[] libs = lib.listFiles(JAR_FILTER);
+            if (libs != null)
+                paths.addAll(Arrays.asList(libs));
+
+            baseResourceURL = expandDir.toPath().toUri().toURL();
+
+        }
+        File disableFile = new File(archive.getPath() + ".disabled");
+        if (disableFile.exists()) {
+            LOGGER.info("Plugin " + archive.getName() + " is disabled");
+        }
+        if (paths.isEmpty()) {
+            LOGGER.info("No classpaths found for plugin " + archive.getName());
+        }
+
+        // compute dependencies
+        List<PluginWrapper.Dependency> dependencies = new ArrayList<>();
+        List<PluginWrapper.Dependency> optionalDependencies = new ArrayList<>();
+        String v = atts.getValue("Plugin-Dependencies");
+        if (v != null) {
+            for (String s : v.split(",")) {
+                PluginWrapper.Dependency d = new PluginWrapper.Dependency(s);
+                if (d.optional) {
+                    optionalDependencies.add(d);
+                } else {
+                    dependencies.add(d);
+                }
+            }
+        }
+
+        fix(atts, optionalDependencies);
+
+        ClassLoader dependencyLoader = new DependencyClassLoader(
+                getClass().getClassLoader(), archive, Util.join(dependencies, optionalDependencies), pluginManager);
+        dependencyLoader = getBaseClassLoader(atts, dependencyLoader);
+
+        return new PluginWrapper(pluginManager, archive, manifest, baseResourceURL,
+                createClassLoader(computeClassLoaderName(manifest, archive), paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies);
+    }
+
+    private static String computeClassLoaderName(Manifest mf, File archive) {
+        return "PluginClassLoader for " + PluginWrapper.computeShortName(mf, archive.getName());
+    }
+
+    private void fix(Attributes atts, List<PluginWrapper.Dependency> optionalDependencies) {
+        String pluginName = atts.getValue("Short-Name");
+
+        String jenkinsVersion = atts.getValue("Jenkins-Version");
+        if (jenkinsVersion == null)
+            jenkinsVersion = atts.getValue("Hudson-Version");
+
+        for (Dependency d : DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion)) {
+            LOGGER.fine(() -> "implied dep " + pluginName + " → " + d.shortName);
+            pluginManager.considerDetachedPlugin(d.shortName, pluginName);
+            optionalDependencies.add(d);
+        }
+    }
+
+    /**
+     * @see DetachedPluginsUtil#getImpliedDependencies(String, String)
+     *
+     * @deprecated since 2.163
+     */
+    @Deprecated
+    @NonNull
+    public static List<PluginWrapper.Dependency> getImpliedDependencies(String pluginName, String jenkinsVersion) {
+        return DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion);
+    }
+
+    /**
+     * @deprecated since 2.459 use {@link #createClassLoader(String, List, ClassLoader, Attributes)}
+     */
+    @Deprecated(since = "2.459")
+    protected ClassLoader createClassLoader(List<File> paths, ClassLoader parent) throws IOException {
+        return createClassLoader(paths, parent, null);
+    }
+
+    /**
+     * @deprecated since 2.459 use {@link #createClassLoader(String, List, ClassLoader, Attributes)}
+     */
+    @Deprecated(since = "2.459")
+    protected ClassLoader createClassLoader(List<File> paths, ClassLoader parent, Attributes atts) throws IOException {
+        // generate a legacy id so at least we can track to something
+        return createClassLoader("unidentified-" + UUID.randomUUID(), paths, parent, atts);
+    }
+
+    /**
+     * Creates a  classloader that can load all the specified jar files and delegate to the given parent.
+     * @since 2.459
+     */
+    protected ClassLoader createClassLoader(String name, List<File> paths, ClassLoader parent, Attributes atts) throws IOException {
+        boolean usePluginFirstClassLoader =
+                atts != null && Boolean.parseBoolean(atts.getValue("PluginFirstClassLoader"));
+
+        List<URL> urls = new ArrayList<>();
+        for (File path : paths) {
+            if (path.getName().startsWith("jenkins-test-harness")) {
+                throw new IllegalStateException("Refusing to load the Jenkins test harness in production (via "
+                        + atts.getValue("Short-Name") + ")");
+            }
+            urls.add(path.toURI().toURL());
+        }
+        URLClassLoader2 classLoader;
+        if (usePluginFirstClassLoader) {
+            classLoader = new PluginFirstClassLoader2(name, urls.toArray(new URL[0]), parent);
+        } else {
+            classLoader = new URLClassLoader2(name, urls.toArray(new URL[0]), parent);
+        }
+        return classLoader;
+    }
+
+    /**
+     * Computes the classloader that takes the class masking into account.
+     *
+     * <p>
+     * This mechanism allows plugins to have their own versions for libraries that core bundles.
+     */
+    private ClassLoader getBaseClassLoader(Attributes atts, ClassLoader base) {
+        String masked = atts.getValue("Mask-Classes");
+        if (masked != null)
+            base = new MaskingClassLoader(base, masked.trim().split("[ \t\r\n]+"));
+        return base;
+    }
+
+    @Override
+    public void initializeComponents(PluginWrapper plugin) {
+    }
+
+    @Override
+    public <T> List<ExtensionComponent<T>> findComponents(Class<T> type, Hudson hudson) {
+
+        List<ExtensionFinder> finders;
+        if (type == ExtensionFinder.class) {
+            // Avoid infinite recursion of using ExtensionFinders to find ExtensionFinders
+            finders = List.of(new ExtensionFinder.Sezpoz());
+        } else {
+            finders = hudson.getExtensionList(ExtensionFinder.class);
+        }
+
+        /*
+         * See ExtensionFinder#scout(Class, Hudson) for the dead lock issue and what this does.
+         */
+        if (LOGGER.isLoggable(Level.FINER))
+            LOGGER.log(Level.FINER, "Scout-loading ExtensionList: " + type, new Throwable());
+        for (ExtensionFinder finder : finders) {
+            finder.scout(type, hudson);
+        }
+
+        List<ExtensionComponent<T>> r = new ArrayList<>();
+        for (ExtensionFinder finder : finders) {
+            try {
+                r.addAll(finder.find(type, hudson));
+            } catch (AbstractMethodError e) {
+                // backward compatibility
+                for (T t : finder.findExtensions(type, hudson))
+                    r.add(new ExtensionComponent<>(t));
+            }
+        }
+
+        List<ExtensionComponent<T>> filtered = new ArrayList<>();
+        for (ExtensionComponent<T> e : r) {
+            if (ExtensionFilter.isAllowed(type, e))
+                filtered.add(e);
+        }
+
+        return filtered;
+    }
+
+    @Override
+    public void load(PluginWrapper wrapper) throws IOException {
+        // override the context classloader. This no longer makes sense,
+        // but it is left for the backward compatibility
+        ClassLoader old = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(wrapper.classLoader);
+        try {
+            String className = wrapper.getPluginClass();
+            if (className == null) {
+                // use the default dummy instance
+                wrapper.setPlugin(new DummyImpl());
+            } else {
+                try {
+                    Class<?> clazz = wrapper.classLoader.loadClass(className);
+                    Object o = clazz.getDeclaredConstructor().newInstance();
+                    if (!(o instanceof Plugin)) {
+                        throw new IOException(className + " doesn't extend from hudson.Plugin");
+                    }
+                    wrapper.setPlugin((Plugin) o);
+                } catch (LinkageError | ClassNotFoundException e) {
+                    throw new IOException("Unable to load " + className + " from " + wrapper.getShortName(), e);
+                } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
+                    throw new IOException("Unable to create instance of " + className + " from " + wrapper.getShortName(), e);
+                }
+            }
+
+            // initialize plugin
+            try {
+                Plugin plugin = wrapper.getPluginOrFail();
+                plugin.setServletContext(pluginManager.context);
+                startPlugin(wrapper);
+            } catch (Throwable t) {
+                // gracefully handle any error in plugin.
+                throw new IOException("Failed to initialize", t);
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(old);
+        }
+    }
+
+    public void startPlugin(PluginWrapper plugin) throws Exception {
+        plugin.getPluginOrFail().start();
+    }
+
+    @Override
+    public void updateDependency(PluginWrapper depender, PluginWrapper dependee) {
+        DependencyClassLoader classLoader = findAncestorDependencyClassLoader(depender.classLoader);
+        if (classLoader != null) {
+            classLoader.updateTransitiveDependencies();
+            LOGGER.log(Level.INFO, "Updated dependency of {0}", depender.getShortName());
+        }
+    }
+
+    private DependencyClassLoader findAncestorDependencyClassLoader(ClassLoader classLoader)
+    {
+        for (; classLoader != null; classLoader = classLoader.getParent()) {
+            if (classLoader instanceof DependencyClassLoader) {
+                return (DependencyClassLoader) classLoader;
+            }
+        }
+        return null;
+    }
+
+    @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Administrator action installing a plugin, which could do far worse.")
+    private static File resolve(File base, String relative) {
+        File rel = new File(relative);
+        if (rel.isAbsolute())
+            return rel;
+        else
+            return new File(base.getParentFile(), relative);
+    }
+
+    private static void parseClassPath(Manifest manifest, File archive, List<File> paths, String attributeName, String separator) throws IOException {
+        String classPath = manifest.getMainAttributes().getValue(attributeName);
+        if (classPath == null) return; // attribute not found
+        for (String s : classPath.split(separator)) {
+            File file = resolve(archive, s);
+            if (file.getName().contains("*")) {
+                // handle wildcard
+                FileSet fs = new FileSet();
+                File dir = file.getParentFile();
+                fs.setDir(dir);
+                fs.setIncludes(file.getName());
+                for (String included : fs.getDirectoryScanner(new Project()).getIncludedFiles()) {
+                    paths.add(new File(dir, included));
+                }
+            } else {
+                if (!file.exists())
+                    throw new IOException("No such file: " + file);
+                paths.add(file);
+            }
+        }
+    }
+
+    /**
+     * Explodes the plugin into a directory, if necessary.
+     */
+    private static void explode(File archive, File destDir) throws IOException {
+        Util.createDirectories(Util.fileToPath(destDir));
+
+        // timestamp check
+        File explodeTime = new File(destDir, ".timestamp2");
+        if (explodeTime.exists() && explodeTime.lastModified() == archive.lastModified())
+            return; // no need to expand
+
+        // delete the contents so that old files won't interfere with new files
+        Util.deleteRecursive(destDir);
+
+        try {
+            Project prj = new Project();
+            unzipExceptClasses(archive, destDir, prj);
+            createClassJarFromWebInfClasses(archive, destDir, prj);
+        } catch (BuildException x) {
+            throw new IOException("Failed to expand " + archive, x);
+        }
+
+        try {
+            new FilePath(explodeTime).touch(archive.lastModified());
+        } catch (InterruptedException e) {
+            throw new AssertionError(e); // impossible
+        }
+    }
+
+    /**
+     * Repackage classes directory into a jar file to make it remoting friendly.
+     * The remoting layer can cache jar files but not class files.
+     */
+    private static void createClassJarFromWebInfClasses(File archive, File destDir, Project prj) throws IOException {
+        File classesJar = new File(destDir, "WEB-INF/lib/classes.jar");
+
+        ZipFileSet zfs = new ZipFileSet();
+        zfs.setProject(prj);
+        zfs.setSrc(archive);
+        zfs.setIncludes("WEB-INF/classes/");
+
+        MappedResourceCollection mapper = new MappedResourceCollection();
+        mapper.add(zfs);
+
+        GlobPatternMapper gm = new GlobPatternMapper();
+        gm.setFrom("WEB-INF/classes/*");
+        gm.setTo("*");
+        mapper.add(gm);
+
+        final long dirTime = archive.lastModified();
+        // this ZipOutputStream is reused and not created for each directory
+        try (OutputStream nos = OutputStream.nullOutputStream(); ZipOutputStream wrappedZOut = new ZipOutputStream(nos) {
+            @Override
+            public void putNextEntry(ZipEntry ze) throws IOException {
+                ze.setTime(dirTime + 1999);   // roundup
+                super.putNextEntry(ze);
+            }
+        }) {
+            Zip z = new Zip() {
+                /**
+                 * Forces the fixed timestamp for directories to make sure
+                 * classes.jar always get a consistent checksum.
+                 */
+                @Override
+                protected void zipDir(Resource dir, ZipOutputStream zOut, String vPath,
+                                      int mode, ZipExtraField[] extra)
+                    throws IOException {
+                    // use wrappedZOut instead of zOut
+                    super.zipDir(dir, wrappedZOut, vPath, mode, extra);
+                }
+            };
+            z.setProject(prj);
+            z.setTaskType("zip");
+            Util.createDirectories(Util.fileToPath(classesJar.getParentFile()));
+            z.setDestFile(classesJar);
+            z.add(mapper);
+            z.execute();
+        }
+        if (classesJar.isFile()) {
+            LOGGER.log(Level.WARNING, "Created {0}; update plugin to a version created with a newer harness", classesJar);
+        }
+    }
+
+    private static void unzipExceptClasses(File archive, File destDir, Project prj) {
+        Expand e = new Expand();
+        e.setProject(prj);
+        e.setTaskType("unzip");
+        e.setSrc(archive);
+        e.setDest(destDir);
+        PatternSet p = new PatternSet();
+        p.setExcludes("WEB-INF/classes/");
+        e.addPatternset(p);
+        e.execute();
+    }
+
+    /**
+     * Used to load classes from dependency plugins.
+     */
+    static final class DependencyClassLoader extends DelegatingClassLoader {
+        /**
+         * This classloader is created for this plugin. Useful during debugging.
+         */
+        private final File _for;
+
+        private List<Dependency> dependencies;
+
+        private final PluginManager pluginManager;
+
+        /**
+         * Topologically sorted list of transitive dependencies. Lazily initialized via double-checked locking.
+         */
+        private volatile List<PluginWrapper> transitiveDependencies;
+
+        DependencyClassLoader(ClassLoader parent, File archive, List<Dependency> dependencies, PluginManager pluginManager) {
+            super("dependency ClassLoader for " + archive.getPath(), parent);
+            this._for = archive;
+            this.dependencies = List.copyOf(dependencies);
+            this.pluginManager = pluginManager;
+        }
+
+        private void updateTransitiveDependencies() {
+            // This will be recalculated at the next time.
+            transitiveDependencies = null;
+        }
+
+        private List<PluginWrapper> getTransitiveDependencies() {
+          List<PluginWrapper> localTransitiveDependencies = transitiveDependencies;
+          if (localTransitiveDependencies == null) {
+            synchronized (this) {
+              localTransitiveDependencies = transitiveDependencies;
+              if (localTransitiveDependencies == null) {
+                CyclicGraphDetector<PluginWrapper> cgd = new CyclicGraphDetector<>() {
+                    @Override
+                    protected List<PluginWrapper> getEdges(PluginWrapper pw) {
+                        List<PluginWrapper> dep = new ArrayList<>();
+                        for (Dependency d : pw.getDependencies()) {
+                            PluginWrapper p = pluginManager.getPlugin(d.shortName);
+                            if (p != null && p.isActive())
+                                dep.add(p);
+                        }
+                        return dep;
+                    }
+                };
+
+                try {
+                    for (Dependency d : dependencies) {
+                        PluginWrapper p = pluginManager.getPlugin(d.shortName);
+                        if (p != null && p.isActive())
+                            cgd.run(Set.of(p));
+                    }
+                } catch (CycleDetectedException e) {
+                    throw new AssertionError(e);    // such error should have been reported earlier
+                }
+
+                transitiveDependencies = localTransitiveDependencies = cgd.getSorted();
+              }
+            }
+          }
+          return localTransitiveDependencies;
+        }
+
+        @Override
+        protected Class<?> findClass(String name) throws ClassNotFoundException {
+            if (PluginManager.FAST_LOOKUP) {
+                for (PluginWrapper pw : getTransitiveDependencies()) {
+                    try {
+                        return ClassLoaderReflectionToolkit.loadClass(pw.classLoader, name);
+                    } catch (ClassNotFoundException ignored) {
+                        //not found. try next
+                    }
+                }
+            } else {
+                for (Dependency dep : dependencies) {
+                    PluginWrapper p = pluginManager.getPlugin(dep.shortName);
+                    if (p != null) {
+                        try {
+                            return p.classLoader.loadClass(name);
+                        } catch (ClassNotFoundException ignored) {
+                            // OK, try next
+                        }
+                    }
+                }
+            }
+
+            throw new ClassNotFoundException(name);
+        }
+
+        @Override
+        @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS",
+                            justification = "Should not produce network overheads since the URL is local. JENKINS-53793 is a follow-up")
+        protected Enumeration<URL> findResources(String name) throws IOException {
+            HashSet<URL> result = new HashSet<>();
+
+            if (PluginManager.FAST_LOOKUP) {
+                    for (PluginWrapper pw : getTransitiveDependencies()) {
+                        Enumeration<URL> urls = ClassLoaderReflectionToolkit._findResources(pw.classLoader, name);
+                        while (urls != null && urls.hasMoreElements())
+                            result.add(urls.nextElement());
+                    }
+            } else {
+                for (Dependency dep : dependencies) {
+                    PluginWrapper p = pluginManager.getPlugin(dep.shortName);
+                    if (p != null) {
+                        Enumeration<URL> urls = p.classLoader.getResources(name);
+                        while (urls != null && urls.hasMoreElements())
+                            result.add(urls.nextElement());
+                    }
+                }
+            }
+
+            return Collections.enumeration(result);
+        }
+
+        @Override
+        protected URL findResource(String name) {
+            if (PluginManager.FAST_LOOKUP) {
+                    for (PluginWrapper pw : getTransitiveDependencies()) {
+                        URL url = ClassLoaderReflectionToolkit._findResource(pw.classLoader, name);
+                        if (url != null)    return url;
+                    }
+            } else {
+                for (Dependency dep : dependencies) {
+                    PluginWrapper p = pluginManager.getPlugin(dep.shortName);
+                    if (p != null) {
+                        URL url = p.classLoader.getResource(name);
+                        if (url != null)
+                            return url;
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 42 - 0
jenkins/core/src/main/java/hudson/CloseProofOutputStream.java

@@ -0,0 +1,42 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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.
+ */
+
+package hudson;
+
+import hudson.util.DelegatingOutputStream;
+import java.io.OutputStream;
+
+/**
+ * {@link OutputStream} that blocks {@link #close()} method.
+ * @author Kohsuke Kawaguchi
+ */
+public class CloseProofOutputStream extends DelegatingOutputStream {
+    public CloseProofOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    @Override
+    public void close() {
+    }
+}

+ 56 - 0
jenkins/core/src/main/java/hudson/CopyOnWrite.java

@@ -0,0 +1,56 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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.
+ */
+
+package hudson;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Represents fields that are protected for concurrency by the copy-on-write semantics.
+ *
+ * <p>
+ * Fields marked by this annotation always holds on to an immutable collection.
+ * A change to the collection must be done by first creating a new collection
+ * object, making changes, then replacing the reference atomically.
+ *
+ * <p>
+ * This allows code to access the field without synchronization, and
+ * greatly reduces the risk of dead-lock bugs.
+ *
+ * <p>
+ * The field marked with this annotation usually needs to be marked as
+ * {@code volatile}.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@Retention(SOURCE)
+@Documented
+@Target(FIELD)
+public @interface CopyOnWrite {
+}

+ 95 - 0
jenkins/core/src/main/java/hudson/DependencyRunner.java

@@ -0,0 +1,95 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
+ * Brian Westrich, Jean-Baptiste Quenot
+ *
+ * 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.
+ */
+
+package hudson;
+
+import hudson.model.AbstractProject;
+import hudson.security.ACL;
+import hudson.security.ACLContext;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
+
+/**
+ * Runs a job on all projects in the order of dependencies
+ */
+public class DependencyRunner implements Runnable {
+
+    private static final Logger LOGGER = Logger.getLogger(DependencyRunner.class.getName());
+
+    ProjectRunnable runnable;
+
+    List<AbstractProject> polledProjects = new ArrayList<>();
+
+    public DependencyRunner(ProjectRunnable runnable) {
+        this.runnable = runnable;
+    }
+
+    @Override
+    public void run() {
+        try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
+            Set<AbstractProject> topLevelProjects = new HashSet<>();
+            // Get all top-level projects
+            LOGGER.fine("assembling top level projects");
+            for (AbstractProject p : Jenkins.get().allItems(AbstractProject.class))
+                if (p.getUpstreamProjects().isEmpty()) {
+                    LOGGER.fine("adding top level project " + p.getName());
+                    topLevelProjects.add(p);
+                } else {
+                    LOGGER.fine("skipping project since not a top level project: " + p.getName());
+                }
+            populate(topLevelProjects);
+            for (AbstractProject p : polledProjects) {
+                    LOGGER.fine("running project in correct dependency order: " + p.getName());
+                runnable.run(p);
+            }
+        }
+    }
+
+    private void populate(Collection<? extends AbstractProject> projectList) {
+        for (AbstractProject<?, ?> p : projectList) {
+            if (polledProjects.contains(p)) {
+                // Project will be readded at the queue, so that we always use
+                // the longest path
+                LOGGER.fine("removing project " + p.getName() + " for re-add");
+                polledProjects.remove(p);
+            }
+
+            LOGGER.fine("adding project " + p.getName());
+            polledProjects.add(p);
+
+            // Add all downstream dependencies
+            populate(p.getDownstreamProjects());
+        }
+    }
+
+    public interface ProjectRunnable {
+        void run(AbstractProject p);
+    }
+}

+ 251 - 0
jenkins/core/src/main/java/hudson/DescriptorExtensionList.java

@@ -0,0 +1,251 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, 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.
+ */
+
+package hudson;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.model.Describable;
+import hudson.model.Descriptor;
+import hudson.model.Descriptor.FormException;
+import hudson.model.Hudson;
+import hudson.model.ViewDescriptor;
+import hudson.slaves.NodeDescriptor;
+import hudson.tasks.Publisher;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.ExtensionComponentSet;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONException;
+import net.sf.json.JSONObject;
+import org.kohsuke.stapler.Stapler;
+
+/**
+ * {@link ExtensionList} for holding a set of {@link Descriptor}s, which is a group of descriptors for
+ * the same extension point.
+ *
+ * Use {@link jenkins.model.Jenkins#getDescriptorList(Class)} to obtain instances.
+ *
+ * @param <D>
+ *      Represents the descriptor type. This is {@code Descriptor<T>} normally but often there are subtypes
+ *      of descriptors, like {@link ViewDescriptor}, {@link NodeDescriptor}, etc, and this parameter points
+ *      to those for better type safety of users.
+ *
+ *      The actual value of 'D' is not necessary for the operation of this code, so it's purely for convenience
+ *      of the users of this class.
+ *
+ * @since 1.286
+ */
+public class DescriptorExtensionList<T extends Describable<T>, D extends Descriptor<T>> extends ExtensionList<D> {
+    /**
+     * Creates a new instance.
+     */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public static <T extends Describable<T>, D extends Descriptor<T>>
+    DescriptorExtensionList<T, D> createDescriptorList(Jenkins jenkins, Class<T> describableType) {
+        if (describableType == Publisher.class) {
+            return (DescriptorExtensionList) new Publisher.DescriptorExtensionListImpl(jenkins);
+        }
+        return new DescriptorExtensionList<>(jenkins, describableType);
+    }
+
+    /**
+     * @deprecated as of 1.416
+     *      Use {@link #create(Jenkins, Class)}
+     */
+    @Deprecated
+    public static <T extends Describable<T>, D extends Descriptor<T>>
+    DescriptorExtensionList<T, D> createDescriptorList(Hudson hudson, Class<T> describableType) {
+        return (DescriptorExtensionList) createDescriptorList((Jenkins) hudson, describableType);
+    }
+
+    /**
+     * Type of the {@link Describable} that this extension list retains.
+     */
+    private final Class<T> describableType;
+
+    /**
+     * @deprecated as of 1.416
+     *      Use {@link #DescriptorExtensionList(Jenkins, Class)}
+     */
+    @Deprecated
+    protected DescriptorExtensionList(Hudson hudson, Class<T> describableType) {
+        this((Jenkins) hudson, describableType);
+    }
+
+    protected DescriptorExtensionList(Jenkins jenkins, Class<T> describableType) {
+        super(jenkins, (Class) Descriptor.class);
+        this.describableType = describableType;
+    }
+
+    /**
+     * Finds the descriptor that has the matching fully-qualified class name.
+     *
+     * @param fqcn
+     *      Fully qualified name of the descriptor, not the describable.
+     * @deprecated {@link Descriptor#getId} is supposed to be used for new code, not the descriptor class name.
+     */
+    @Deprecated
+    public D find(String fqcn) {
+        return Descriptor.find(this, fqcn);
+    }
+
+    /**
+     * Finds the descriptor that describes the given type.
+     * That is, if this method returns d, {@code d.clazz==type}
+     */
+    public D find(Class<? extends T> type) {
+        for (D d : this)
+            if (d.clazz == type)
+                return d;
+        return null;
+    }
+
+    /**
+     * Creates a new instance of a {@link Describable} from the structured form submission data posted by a radio button group.
+     * @param config Submitted configuration for Radio List
+     * @return New instance.
+     *         {@code null} if none was selected in the radio list or if the value is filtered by a {@link hudson.model.DescriptorVisibilityFilter}
+     * @throws FormException Data submission error
+     * @since 1.312
+     */
+    @CheckForNull
+    public T newInstanceFromRadioList(JSONObject config) throws FormException {
+        if (config.isNullObject())
+            return null;    // none was selected
+        int idx = config.getInt("value");
+        return get(idx).newInstance(Stapler.getCurrentRequest2(), config);
+    }
+
+    /**
+     * Creates a new instance of a {@link Describable} from the structured form submission data posted by a radio list.
+     * @since 1.312
+     * @param name Name of the form field
+     * @return Created instance.
+     *         {@code null} if none was selected in the radio list or if the value is filtered by a {@link hudson.model.DescriptorVisibilityFilter}
+     * @throws FormException Data submission error
+     */
+    @CheckForNull
+    public T newInstanceFromRadioList(@NonNull JSONObject parent, @NonNull String name) throws FormException {
+        try {
+            return newInstanceFromRadioList(parent.getJSONObject(name));
+        } catch (JSONException ex) {
+            throw new FormException(ex, name);
+        }
+    }
+
+    /**
+     * Finds a descriptor by their {@link Descriptor#getId()}.
+     *
+     * If none is found, null is returned.
+     */
+    public @CheckForNull D findByName(String id) {
+        for (D d : this)
+            if (d.getId().equals(id))
+                return d;
+        return null;
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public boolean add(D d) {
+        boolean r = super.add(d);
+        getDescriptorExtensionList().add(d);
+        return r;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        getDescriptorExtensionList().remove(o);
+        return super.remove(o);
+    }
+
+    /**
+     * {@link #load()} in the descriptor is not a real load activity, so locking against "this" is enough.
+     */
+    @Override
+    protected Object getLoadLock() {
+        // Get a lock for the singleton extension list to prevent deadlocks (JENKINS-55361)
+        return getDescriptorExtensionList().getLoadLock();
+    }
+
+    /**
+     * Loading the descriptors in this case means filtering the descriptor from the master {@link ExtensionList}.
+     */
+    @Override
+    protected List<ExtensionComponent<D>> load() {
+        if (jenkins == null) {
+            // Should never happen on the real instance
+            LOGGER.log(Level.WARNING, "Cannot load extension components, because Jenkins instance has not been assigned yet");
+            return Collections.emptyList();
+        }
+        return _load(getDescriptorExtensionList().getComponents());
+    }
+
+    @Override
+    protected Collection<ExtensionComponent<D>> load(ExtensionComponentSet delta) {
+        return _load(delta.find(Descriptor.class));
+    }
+
+    private List<ExtensionComponent<D>> _load(Iterable<ExtensionComponent<Descriptor>> set) {
+        List<ExtensionComponent<D>> r = new ArrayList<>();
+        for (ExtensionComponent<Descriptor> c : set) {
+            Descriptor d = c.getInstance();
+            try {
+                if (d.getT() == describableType)
+                    r.add((ExtensionComponent) c);
+            } catch (IllegalStateException e) {
+                LOGGER.log(Level.SEVERE, d.getClass() + " doesn't extend Descriptor with a type parameter", e); // skip this one
+            }
+        }
+        return r;
+    }
+
+    private ExtensionList<Descriptor> getDescriptorExtensionList() {
+        return ExtensionList.lookup(Descriptor.class);
+    }
+
+    /**
+     * @deprecated Now always empty.
+     */
+    @Deprecated
+    public static Iterable<Descriptor> listLegacyInstances() {
+        return List.of();
+    }
+
+    /**
+     * @deprecated No longer does anything.
+     */
+    @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "irrelevant now")
+    @Deprecated
+    public static void clearLegacyInstances() {
+    }
+
+    private static final Logger LOGGER = Logger.getLogger(DescriptorExtensionList.class.getName());
+}

+ 473 - 0
jenkins/core/src/main/java/hudson/EnvVars.java

@@ -0,0 +1,473 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Red Hat, 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.
+ */
+
+package hudson;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.remoting.VirtualChannel;
+import hudson.util.CyclicGraphDetector;
+import hudson.util.CyclicGraphDetector.CycleDetectedException;
+import hudson.util.VariableResolver;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.logging.Logger;
+import jenkins.security.MasterToSlaveCallable;
+
+/**
+ * Environment variables.
+ *
+ * <p>
+ * While all the platforms I tested (Linux 2.6, Solaris, and Windows XP) have the case sensitive
+ * environment variable table, Windows batch script handles environment variable in the case preserving
+ * but case <b>insensitive</b> way (that is, cmd.exe can get both FOO and foo as environment variables
+ * when it's launched, and the "set" command will display it accordingly, but "echo %foo%" results in
+ * echoing the value of "FOO", not "foo" &mdash; this is presumably caused by the behavior of the underlying
+ * Win32 API {@code GetEnvironmentVariable} acting in case insensitive way.) Windows users are also
+ * used to write environment variable case-insensitively (like %Path% vs %PATH%), and you can see many
+ * documents on the web that claims Windows environment variables are case insensitive.
+ *
+ * <p>
+ * So for a consistent cross platform behavior, it creates the least confusion to make the table
+ * case insensitive but case preserving.
+ *
+ * <p>
+ * In Jenkins, often we need to build up "environment variable overrides"
+ * on the controller, then to execute the process on agents. This causes a problem
+ * when working with variables like {@code PATH}. So to make this work,
+ * we introduce a special convention {@code PATH+FOO} &mdash; all entries
+ * that starts with {@code PATH+} are merged and prepended to the inherited
+ * {@code PATH} variable, on the process where a new process is executed.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class EnvVars extends TreeMap<String, String> {
+    private static final long serialVersionUID = 4320331661987259022L;
+    private static Logger LOGGER = Logger.getLogger(EnvVars.class.getName());
+    /**
+     * If this {@link EnvVars} object represents the whole environment variable set,
+     * not just a partial list used for overriding later, then we need to know
+     * the platform for which this env vars are targeted for, or else we won't know
+     * how to merge variables properly.
+     *
+     * <p>
+     * So this property remembers that information.
+     */
+    private Platform platform;
+
+    /**
+     * Gets the platform for which these env vars targeted.
+     * @since 2.144
+     * @return The platform.
+     */
+    public @CheckForNull Platform getPlatform() {
+        return platform;
+    }
+
+    /**
+     * Sets the platform for which these env vars target.
+     * @since 2.144
+     * @param platform the platform to set.
+     */
+    public void setPlatform(@NonNull Platform platform) {
+        this.platform = platform;
+    }
+
+    public EnvVars() {
+        super(String.CASE_INSENSITIVE_ORDER);
+    }
+
+    public EnvVars(@NonNull Map<String, String> m) {
+        this();
+        putAll(m);
+
+        // because of the backward compatibility, some parts of Jenkins passes
+        // EnvVars as Map<String,String> so downcasting is safer.
+        if (m instanceof EnvVars lhs) {
+            this.platform = lhs.platform;
+        }
+    }
+
+    @SuppressWarnings("CopyConstructorMissesField") // does not set #platform, see its Javadoc
+    public EnvVars(@NonNull EnvVars m) {
+        // this constructor is so that in future we can get rid of the downcasting.
+        this((Map) m);
+    }
+
+    /**
+     * Builds an environment variables from an array of the form {@code "key","value","key","value"...}
+     */
+    public EnvVars(String... keyValuePairs) {
+        this();
+        if (keyValuePairs.length % 2 != 0)
+            throw new IllegalArgumentException(Arrays.asList(keyValuePairs).toString());
+        for (int i = 0; i < keyValuePairs.length; i += 2)
+            put(keyValuePairs[i], keyValuePairs[i + 1]);
+    }
+
+    /**
+     * Overrides the current entry by the given entry.
+     *
+     * <p>
+     * Handles {@code PATH+XYZ} notation.
+     */
+    public void override(String key, String value) {
+        if (value == null || value.isEmpty()) {
+            remove(key);
+            return;
+        }
+
+        int idx = key.indexOf('+');
+        if (idx > 0) {
+            String realKey = key.substring(0, idx);
+            String v = get(realKey);
+            if (v == null) v = value;
+            else {
+                // we might be handling environment variables for a agent that can have different path separator
+                // than the controller, so the following is an attempt to get it right.
+                // it's still more error prone that I'd like.
+                char ch = platform == null ? File.pathSeparatorChar : platform.pathSeparator;
+                v = value + ch + v;
+            }
+            put(realKey, v);
+            return;
+        }
+
+        put(key, value);
+    }
+
+    /**
+     * Overrides all values in the map by the given map.
+     * See {@link #override(String, String)}.
+     * @return this
+     */
+    public EnvVars overrideAll(Map<String, String> all) {
+        for (Map.Entry<String, String> e : all.entrySet()) {
+            override(e.getKey(), e.getValue());
+        }
+        return this;
+    }
+
+    /**
+     * Calculates the order to override variables.
+     *
+     * Sort variables with topological sort with their reference graph.
+     *
+     * This is package accessible for testing purpose.
+     */
+    static class OverrideOrderCalculator {
+        /**
+         * Extract variables referred directly from a variable.
+         */
+        private static class TraceResolver implements VariableResolver<String> {
+            private final Comparator<? super String> comparator;
+            public Set<String> referredVariables;
+
+            TraceResolver(Comparator<? super String> comparator) {
+                this.comparator = comparator;
+                clear();
+            }
+
+            public void clear() {
+                referredVariables = new TreeSet<>(comparator);
+            }
+
+            @Override
+            public String resolve(String name) {
+                referredVariables.add(name);
+                return "";
+            }
+        }
+
+        private static class VariableReferenceSorter extends CyclicGraphDetector<String> {
+            // map from a variable to a set of variables that variable refers.
+            private final Map<String, Set<String>> refereeSetMap;
+
+            VariableReferenceSorter(Map<String, Set<String>> refereeSetMap) {
+                this.refereeSetMap = refereeSetMap;
+            }
+
+            @Override
+            protected Iterable<? extends String> getEdges(String n) {
+                // return variables referred from the variable.
+                if (!refereeSetMap.containsKey(n)) {
+                    // there is a case a non-existing variable is referred...
+                    return Collections.emptySet();
+                }
+                return refereeSetMap.get(n);
+            }
+        }
+
+        private final Comparator<? super String> comparator;
+
+        @NonNull
+        private final EnvVars target;
+        @NonNull
+        private final Map<String, String> overrides;
+
+        private Map<String, Set<String>> refereeSetMap;
+        private List<String> orderedVariableNames;
+
+        OverrideOrderCalculator(@NonNull EnvVars target, @NonNull Map<String, String> overrides) {
+            comparator = target.comparator();
+            this.target = target;
+            this.overrides = overrides;
+            scan();
+        }
+
+        public List<String> getOrderedVariableNames() {
+            return orderedVariableNames;
+        }
+
+        // Cut the reference to the variable in a cycle.
+        private void cutCycleAt(String referee, List<String> cycle) {
+            // cycle contains variables in referrer-to-referee order.
+            // This should not be negative, for the first and last one is same.
+            int refererIndex = cycle.lastIndexOf(referee) - 1;
+
+            assert refererIndex >= 0;
+            String referrer = cycle.get(refererIndex);
+            boolean removed = refereeSetMap.get(referrer).remove(referee);
+            assert removed;
+            LOGGER.warning(String.format("Cyclic reference detected: %s", String.join(" -> ", cycle)));
+            LOGGER.warning(String.format("Cut the reference %s -> %s", referrer, referee));
+        }
+
+        // Cut the variable reference in a cycle.
+        private void cutCycle(List<String> cycle) {
+            // if an existing variable is contained in that cycle,
+            // cut the cycle with that variable:
+            // existing:
+            //   PATH=/usr/bin
+            // overriding:
+            //   PATH1=/usr/local/bin:${PATH}
+            //   PATH=/opt/something/bin:${PATH1}
+            // then consider reference PATH1 -> PATH can be ignored.
+            for (String referee : cycle) {
+                if (target.containsKey(referee)) {
+                    cutCycleAt(referee, cycle);
+                    return;
+                }
+            }
+
+            // if not, cut the reference to the first one.
+            cutCycleAt(cycle.getFirst(), cycle);
+        }
+
+        /**
+         * Scan all variables and list all referring variables.
+         */
+        public void scan() {
+            refereeSetMap = new TreeMap<>(comparator);
+            List<String> extendingVariableNames = new ArrayList<>();
+
+            TraceResolver resolver = new TraceResolver(comparator);
+
+            for (Map.Entry<String, String> entry : overrides.entrySet()) {
+                if (entry.getKey().indexOf('+') > 0) {
+                    // XYZ+AAA variables should be always processed in last.
+                    extendingVariableNames.add(entry.getKey());
+                    continue;
+                }
+                resolver.clear();
+                Util.replaceMacro(entry.getValue(), resolver);
+
+                // Variables directly referred from the current scanning variable.
+                Set<String> refereeSet = resolver.referredVariables;
+                // Ignore self reference.
+                refereeSet.remove(entry.getKey());
+                refereeSetMap.put(entry.getKey(), refereeSet);
+            }
+
+            VariableReferenceSorter sorter;
+            while (true) {
+                sorter = new VariableReferenceSorter(refereeSetMap);
+                try {
+                    sorter.run(refereeSetMap.keySet());
+                } catch (CycleDetectedException e) {
+                    // cyclic reference found.
+                    // cut the cycle and retry.
+                    @SuppressWarnings("unchecked")
+                    List<String> cycle = e.cycle;
+                    cutCycle(cycle);
+                    continue;
+                }
+                break;
+            }
+
+            // When A refers B, the last appearance of B always comes after
+            // the last appearance of A.
+            List<String> reversedDuplicatedOrder = new ArrayList<>(sorter.getSorted());
+            Collections.reverse(reversedDuplicatedOrder);
+
+            orderedVariableNames = new ArrayList<>(overrides.size());
+            for (String key : reversedDuplicatedOrder) {
+                if (overrides.containsKey(key) && !orderedVariableNames.contains(key)) {
+                    orderedVariableNames.add(key);
+                }
+            }
+            Collections.reverse(orderedVariableNames);
+            orderedVariableNames.addAll(extendingVariableNames);
+        }
+    }
+
+
+    /**
+     * Overrides all values in the map by the given map. Expressions in values will be expanded.
+     * See {@link #override(String, String)}.
+     * @return {@code this}
+     */
+    public EnvVars overrideExpandingAll(@NonNull Map<String, String> all) {
+        for (String key : new OverrideOrderCalculator(this, all).getOrderedVariableNames()) {
+            override(key, expand(all.get(key)));
+        }
+        return this;
+    }
+
+    /**
+     * Resolves environment variables against each other.
+     */
+    public static void resolve(Map<String, String> env) {
+        for (Map.Entry<String, String> entry : env.entrySet()) {
+            entry.setValue(Util.replaceMacro(entry.getValue(), env));
+        }
+    }
+
+    /**
+     * Convenience message
+     * @since 1.485
+     **/
+    public String get(String key, String defaultValue) {
+        String v = get(key);
+        if (v == null)    v = defaultValue;
+        return v;
+    }
+
+    @Override
+    public String put(String key, String value) {
+        if (value == null)    throw new IllegalArgumentException("Null value not allowed as an environment variable: " + key);
+        return super.put(key, value);
+    }
+
+    /**
+     * Add a key/value but only if the value is not-null. Otherwise no-op.
+     * @since 1.556
+     */
+    public void putIfNotNull(String key, String value) {
+        if (value != null)
+            put(key, value);
+    }
+
+    /**
+     * Add entire map but filter null values out.
+     * @since 2.214
+     */
+    public void putAllNonNull(Map<String, String> map) {
+        map.forEach(this::putIfNotNull);
+    }
+
+
+    /**
+     * Takes a string that looks like "a=b" and adds that to this map.
+     */
+    public void addLine(String line) {
+        int sep = line.indexOf('=');
+        if (sep > 0) {
+            put(line.substring(0, sep), line.substring(sep + 1));
+        }
+    }
+
+    /**
+     * Expands the variables in the given string by using environment variables represented in 'this'.
+     */
+    public String expand(String s) {
+        return Util.replaceMacro(s, this);
+    }
+
+    /**
+     * Creates a magic cookie that can be used as the model environment variable
+     * when we later kill the processes.
+     */
+    public static EnvVars createCookie() {
+        return new EnvVars("HUDSON_COOKIE", UUID.randomUUID().toString());
+    }
+
+    /**
+     * Obtains the environment variables of a remote peer.
+     *
+     * @param channel
+     *      Can be null, in which case the map indicating "N/A" will be returned.
+     * @return
+     *      A fresh copy that can be owned and modified by the caller.
+     */
+    public static EnvVars getRemote(VirtualChannel channel) throws IOException, InterruptedException {
+        if (channel == null)
+            return new EnvVars("N/A", "N/A");
+        return channel.call(new GetEnvVars());
+    }
+
+    private static final class GetEnvVars extends MasterToSlaveCallable<EnvVars, RuntimeException> {
+        @Override
+        public EnvVars call() {
+            return new EnvVars(EnvVars.masterEnvVars);
+        }
+
+        private static final long serialVersionUID = 1L;
+    }
+
+    /**
+     * Environmental variables that we've inherited.
+     *
+     * <p>
+     * Despite what the name might imply, this is the environment variable
+     * of the current JVM process. And therefore, it is the Jenkins controller's
+     * environment variables only when you access this from the controller.
+     *
+     * <p>
+     * If you access this field from agents, then this is the environment
+     * variable of the agent.
+     */
+    public static final Map<String, String> masterEnvVars = initMaster();
+
+    private static EnvVars initMaster() {
+        EnvVars vars = new EnvVars(System.getenv());
+        vars.platform = Platform.current();
+        if (Main.isUnitTest || Main.isDevelopmentMode)
+            // if unit test is launched with maven debug switch,
+            // we need to prevent forked Maven processes from seeing it, or else
+            // they'll hang
+            vars.remove("MAVEN_OPTS");
+        return vars;
+    }
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio