diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..6cbd86e12
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @jenkinsci/github-plugin-developers
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..dbae4a465
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: maven
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+ target-branch: master
+ labels:
+ - dependencies
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 000000000..dfb30bd7b
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,5 @@
+# https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc
+_extends: .github
+version-template: $MAJOR.$MINOR.$PATCH
+tag-template: v$NEXT_PATCH_VERSION
+name-template: v$NEXT_PATCH_VERSION
diff --git a/.github/workflows/jenkins-security-scan.yml b/.github/workflows/jenkins-security-scan.yml
new file mode 100644
index 000000000..c7b41fc29
--- /dev/null
+++ b/.github/workflows/jenkins-security-scan.yml
@@ -0,0 +1,21 @@
+name: Jenkins Security Scan
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ types: [ opened, synchronize, reopened ]
+ workflow_dispatch:
+
+permissions:
+ security-events: write
+ contents: read
+ actions: read
+
+jobs:
+ security-scan:
+ uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2
+ with:
+ java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate.
+ # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default.
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
new file mode 100644
index 000000000..1f8a181b6
--- /dev/null
+++ b/.github/workflows/release-drafter.yml
@@ -0,0 +1,17 @@
+# Note: additional setup is required, see https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc
+
+name: Release Drafter
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ update_release_draft:
+ runs-on: ubuntu-latest
+ steps:
+ # Drafts your next Release notes as Pull Requests are merged into the default branch
+ - uses: release-drafter/release-drafter@v6
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 99b3c61f0..41dfd3e40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ target
# autogenerated resources
src/main/webapp/css/*
+.vscode/
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 000000000..4e0774d51
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,7 @@
+
+
+ io.jenkins.tools.incrementals
+ git-changelist-maven-extension
+ 1.8
+
+
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 000000000..2a0299c48
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1,2 @@
+-Pconsume-incrementals
+-Pmight-produce-incrementals
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100755
index 000000000..d58dfb70b
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# 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.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..50a4d7db2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: java
+jdk: oraclejdk8
+before_install:
+ - pip install --user codecov
+install: travis_wait mvn install
+after_success:
+ - codecov
+cache:
+ directories:
+ - $HOME/.m2
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5a161fff9..82d635c21 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,43 +1,43 @@
# Functional contribution
-We are welcome for any contribution. But every new feature implemented in this plugin should:
-
-- Be useful enough for lot of people (should not cover only your professional case)
-- Should not break existing use cases and should avoid breaking the backward compatibility in existing APIs.
- - If the compatibility break is required, it should be well justified.
- [Guide](https://wiki.eclipse.org/Evolving_Java-based_APIs_2)
- and [jenkins solutions](https://wiki.jenkins-ci.org/display/JENKINS/Hint+on+retaining+backward+compatibility) can help to retain the backward compatibility
-- Should be easily maintained (so maintainers need some time to think about architecture of implementation)
-- Have at least one test for positive use case
-
-This plugin is used by lot of people, so it should be stable enough. Please ensure your change is compatible at least with the last LTS line.
-Any core dependency upgrade must be justified
+We welcome any contribution. But every new feature implemented in this plugin should:
+
+- Be useful enough for many people (should cover more than just your professional case).
+- Should not break existing use cases and should avoid breaking backward compatibility in existing APIs.
+ - If a compatibility break is required, it should be well justified.
+ [Guide](https://wiki.eclipse.org/Evolving_Java-based_APIs_2)
+ and [jenkins solutions](https://wiki.jenkins-ci.org/display/JENKINS/Hint+on+retaining+backward+compatibility) can help to retain backward compatibility.
+- Should be easily maintained (so maintainers need some time to think about architecture of implementation).
+- Have at least one test for positive use case.
+
+This plugin is used by many people, so it should be stable. Please ensure your change is compatible at least with the last LTS line.
+Any core dependency upgrade must be justified.
# Code Style Guidelines
-Most of rules is checked with help of the *maven-checkstyle-plugin* during the `validate` phase.
+Most of rules is checked with help of the *maven-checkstyle-plugin* during the `validate` phase.
Checkstyle rules are more important than this document.
## Resulting from long experience
-* To the largest extent possible, all fields shall be private. Use an IDE to generate the getters and setters.
-* If a class has more than one `volatile` member field, it is probable that there are subtle race conditions. Please consider where appropriate encapsulation of the multiple fields into an immutable value object replace the multiple `volatile` member fields with a single `volatile` reference to the value object (or perhaps better yet an `AtomicReference` to allow for `compareAndSet` - if compare-and-set logic is appropriate).
-* If it is `Serializable` it shall have a `serialVersionUID` field. Unless code has shipped to users, the initial value of the `serialVersionUID` field shall be `1L`.
+- To the largest extent possible, all fields should be private. Use an IDE to generate the getters and setters.
+- If a class has more than one `volatile` member field, it is probable that there are subtle race conditions. Please consider, where appropriate, encapsulation of multiple fields into an immutable value object. That is, to replace multiple `volatile` member fields with a single `volatile` reference to the value object (or perhaps better yet an `AtomicReference` to allow for `compareAndSet` - if compare-and-set logic is appropriate).
+- If it is `Serializable`, it should have a `serialVersionUID` field. Unless code has shipped to users, the initial value of the `serialVersionUID` field should be `1L`.
## Indentation
1. **Use spaces.** Tabs are banned.
-2. **Java blocks are 4 spaces.** JavaScript blocks as for Java. **XML nesting is 4 spaces**
+2. **Java blocks are 4 spaces.** JavaScript blocks as for Java. **XML nesting is 4 spaces**.
## Field Naming Conventions
-1. "hungarian"-style notation is banned (i.e. instance variable names preceded by an 'm', etc)
-2. If the field is `static final` then it shall be named in `ALL_CAPS_WITH_UNDERSCORES`.
+1. "hungarian"-style notation is banned (e.g. instance variable names preceded by an 'm', etc.).
+2. If the field is `static final`, then it should be named as `ALL_CAPS_WITH_UNDERSCORES`.
3. Start variable names with a lowercase letter and use camelCase rather than under_scores.
-4. Spelling and abreviations: If the word is widely used in the JVM runtime, stick with the spelling/abreviation in the JVM runtime, e.g. `color` over `colour`, `sync` over `synch`, `async` over `asynch`, etc.
+4. Spelling and abbreviations: If the word is widely used in the JVM runtime, stick with the spelling/abbreviation in the JVM runtime, e.g. `color` over `colour`, `sync` over `synch`, `async` over `asynch`, etc.
5. It is acceptable to use `i`, `j`, `k` for loop indices and iterators. If you need more than three, you are likely doing something wrong and as such you shall either use full descriptive names or refactor.
6. It is acceptable to use `e` for the exception in a `try...catch` block.
-7. You shall never use `l` (i.e. lower case `L`) as a variable name.
+7. Never use `l` (i.e. lower case `L`) as a variable name.
## Line Length
@@ -45,46 +45,47 @@ To the greatest extent possible, please wrap lines to ensure that they do not ex
## Maven POM file layout
-* The `pom.xml` file shall use the sequencing of elements as defined by the `mvn tidy:pom` command (after any indenting fix-up).
-* If you are introducing a property to the `pom.xml` the property must be used in at least two distinct places in the model or a comment justifying the use of a property shall be provided.
-* If the `` is in the groupId `org.apache.maven.plugins` you shall omit the ``.
-* All `` entries shall have an explicit version defined unless inherited from the parent.
+- The `pom.xml` file should use the sequencing of elements as defined by the `mvn tidy:pom` command (after any indenting fix-up).
+- If you are introducing a property to the `pom.xml`, the property must be used in at least two distinct places in the model, or a comment justifying the use of a property should be provided.
+- If the `` is in the groupId `org.apache.maven.plugins`, you should omit the ``.
+- All `` entries should have an explicit version defined unless inherited from the parent.
## Java code style
### Imports
-* For code in `src/main`:
- - `*` imports are banned.
- - `static` imports are preferred until not mislead.
-* For code in `src/test`:
- - `*` imports of anything other than JUnit classes and Hamcrest matchers are banned.
+- For code in `src/main`:
+ - `*` imports are banned.
+ - `static` imports are preferred until not mislead.
+- For code in `src/test`:
+ - `*` imports of anything other than JUnit classes and Hamcrest matchers are banned.
### Annotation placement
-* Annotations on classes, interfaces, annotations, enums, methods, fields and local variables shall be on the lines immediately preceding the line where modifier(s) (e.g. `public` / `protected` / `private` / `final`, etc) would be appropriate.
-* Annotations on method arguments shall, to the largest extent possible, be on the same line as the method argument (and, if present, before the `final` modifier)
+- Annotations on classes, interfaces, annotations, enums, methods, fields and local variables should be on the lines immediately preceding the line where modifier(s) (e.g. `public` / `protected` / `private` / `final`, etc) would be appropriate.
+- Annotations on method arguments should, to the largest extent possible, be on the same line as the method argument (and, if present, before the `final` modifier).
### Javadoc
-* Each class shall have a Javadoc comment.
-* Unless the method is `private`, it shall have a Javadoc comment.
-* Getters and Setters shall have a Javadoc comment. The following is prefered
- ```
+- Each class should have a Javadoc comment.
+- Unless the method is `private`, it should have a Javadoc comment.
+- Getters and Setters should have a Javadoc comment. The following is prefered:
+
+ ```java
/**
* The count of widgets
*/
private int widgetCount;
-
+
/**
* Returns the count of widgets.
*
- * @return the count of widgets.
+ * @return the count of widgets.
*/
public int getWidgetCount() {
return widgetCount;
}
-
+
/**
* Sets the count of widgets.
*
@@ -94,43 +95,44 @@ To the greatest extent possible, please wrap lines to ensure that they do not ex
this.widgetCount = widgetCount;
}
```
-* When adding a new class / interface / etc, it shall have a `@since` doc comment. The version shall be `FIXME` (or `TODO`) to indicate that the person merging the change should replace the `FIXME` with the next release version number. The fields and methods within a class/interface (but not nested classes) will be assumed to have the `@since` annotation of their class/interface unless a different `@since` annotation is present.
+
+- When adding a new class / interface / etc, it should have a `@since` doc comment. The version should be `FIXME` (or `TODO`) to indicate that the person merging the change should replace the `FIXME` with the next release version number. The fields and methods within a class/interface (but not nested classes) will be assumed to have the `@since` annotation of their class/interface unless a different `@since` annotation is present.
### IDE Configuration
-* Eclipse, by and large the IDE defaults are acceptable with the following changes:
- - Tab policy to `Spaces only`
- - Indent statements within `switch` body
- - Maximum line width `120`
- - Line wrapping, ensure all to `wrap where necessary`
- - Organize imports alphabetically, no grouping
-* NetBeans, by and large the IDE defaults are acceptable with the following changes:
- - Tabs and Indents
- + Change Right Margin to `120`
- + Indent case statements in switch
- - Wrapping
- + Change all the `Never` values to `If Long`
- + Select the checkbox for Wrap After Assignement Operators
-* IntelliJ, by and large the IDE defaults are acceptable with the following changes:
- - Wrapping and Braces
- + Change `Do not wrap` to `Wrap if long`
- + Change `Do not force` to `Always`
- - Javadoc
- + Disable generating `` on empty lines
- - Imports
- + Class count to use import with '*': `9999`
- + Names count to use static import with '*': `99999`
- + Import Layout
- * import all other imports
- * blank line
- * import static all other imports
-
+- Eclipse: by and large the IDE defaults are acceptable with the following changes:
+ - Tab policy to `Spaces only`.
+ - Indent statements within `switch` body.
+ - Maximum line width `120`.
+ - Line wrapping, ensure all to `wrap where necessary`.
+ - Organize imports alphabetically, no grouping.
+- NetBeans: by and large the IDE defaults are acceptable with the following changes:
+ - Tabs and Indents:
+ - Change Right Margin to `120`.
+ - Indent case statements in switch.
+ - Wrapping:
+ - Change all the `Never` values to `If Long`.
+ - Select the checkbox for Wrap After Assignment Operators.
+- IntelliJ: by and large the IDE defaults are acceptable with the following changes:
+ - Wrapping and Braces:
+ - Change `Do not wrap` to `Wrap if long`.
+ - Change `Do not force` to `Always`.
+ - Javadoc:
+ - Disable generating `` on empty lines.
+ - Imports:
+ - Class count to use import with '*': `9999`.
+ - Names count to use static import with '*': `99999`.
+ - Import Layout:
+ - import all other imports.
+ - blank line.
+ - import static all other imports.
+
## Issues
-This project uses [Jenkins Jira issue tracker](https://issues.jenkins-ci.org)
-with [github-plugin](https://issues.jenkins-ci.org/browse/JENKINS/component/15896) component.
-
-## Links
+This project uses the [Jenkins Jira issue tracker](https://issues.jenkins.io/)
+with the [github-plugin](https://issues.jenkins.io/browse/JENKINS/component/15896) component.
+
+## Links
-- https://wiki.jenkins-ci.org/display/JENKINS/contributing
-- https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins
+- https://www.jenkins.io/participate/
+- https://www.jenkins.io/doc/developer/
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 000000000..739042f72
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,4 @@
+buildPlugin(useContainerAgent: true, configurations: [
+ [platform: 'linux', jdk: 21],
+ [platform: 'windows', jdk: 17],
+])
diff --git a/README.md b/README.md
index e17706856..2bdb9ff06 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,256 @@
-Jenkins Github Plugin
-=====================
+# GitHub Plugin
-[](http://sonar.lanwen.ru/dashboard/index?id=com.coravy.hudson.plugins.github:github)
+[](https://codecov.io/gh/jenkinsci/github-plugin)
[](LICENSE)
-[](http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin)
+This plugin integrates Jenkins with [GitHub](http://github.com/)
+projects.The plugin currently has three major functionalities:
-Development
-===========
+- Create hyperlinks between your Jenkins projects and GitHub
+- Trigger a job when you push to the repository by groking HTTP POSTs
+ from post-receive hook and optionally auto-managing the hook setup.
+- Report build status result back to github as [Commit
+ Status](https://github.com/blog/1227-commit-status-api) ([documented
+ on
+ SO](https://stackoverflow.com/questions/14274293/show-current-state-of-jenkins-build-on-github-repo/26910986#26910986))
+- Base features for other plugins
+
+## Hyperlinks between changes
+
+The GitHub plugin decorates Jenkins "Changes" pages to create links to
+your GitHub commit and issue pages. It adds a sidebar link that links
+back to the GitHub project page.
+
+
+
+
+When creating a job, specify that is connects to git. Under "GitHub
+project", put in: git@github.com:*Person*/*Project*.git Under "Source
+Code Management" select Git, and put in
+git@github.com:*Person*/*Project*.git
+
+## GitHub hook trigger for GITScm polling
+
+This feature enables builds after [post-receive hooks in your GitHub
+repositories](https://help.github.com/post-receive-hooks/). This trigger
+only kicks git-plugin internal polling algo for every incoming event
+against matched repo.
+
+> This trigger was previously named as "Build when a change is pushed to GitHub"
+
+## Usage
+
+To be able to use this feature different mode are available :
+* manual mode : the url have to be added manually in each project
+* automatic mode : Jenkins register automatically the webhook for every project
+
+### Manual Mode
+
+In this mode, you'll be responsible for registering the hook URLs to
+GitHub. Click the
+
+icon (under Manage Jenkins \> Configure System \> GitHub) to see the URL
+in Jenkins that receives the post-commit POSTs — but in general the URL
+is of the form `$JENKINS_BASE_URL/github-webhook/` — for example:
+`https://ci.example.com/jenkins/github-webhook/`.
+
+Once you have the URL, and have added it as a webhook to the relevant
+GitHub repositories, continue to **Step 3**.
+
+### Automatic Mode (Jenkins manages hooks for jobs by itself)
+
+In this mode, Jenkins will automatically add/remove hook URLs to GitHub
+based on the project configuration in the background. You'll specify
+GitHub OAuth token so that Jenkins can login as you to do this.
+
+**Step 1.** Go to the global configuration and add GitHub Server Config.
+
+
+
+**Step 2.1.** Create your personal access token in GitHub.
+
+Plugin can help you to do it with all required scopes. Go to
+**Advanced** -\> **Manage Additional GitHub Actions** -\> **Convert
+Login and Password to token**
+
+
+
+> *Two-Factor Authentication*
+>
+> Auto-creating token doesn't work with [GitHub
+> 2FA](https://help.github.com/articles/about-two-factor-authentication/)
+>
+> You can create **"Secret text"** credentials with token in corresponding
+> domain with login and password directly, or from username and password
+> credentials.
+
+**Step 2.2.** Select previously created "Secret Text" credentials with
+GitHub OAuth token.
+
+*Required scopes for token*
+
+To be able manage hooks your token should have **admin:org\_hook**
+scope.
+
+*GitHub Enterprise*
+
+You can also redefine GitHub url by clicking on **Custom GitHub API
+URL** checkbox.
+Note that credentials are filtered by entered GH url with help of domain
+requirements. So you can create credentials in different domains and see
+only credentials that matched by predefined domains.
+
+
+
+**Step 3.** Once that configuration is done, go to the project config of
+each job you want triggered automatically and simply check "GitHub hook trigger for GITScm polling"
+under "Build Triggers". With this, every new
+push to the repository automatically triggers a new build.
+
+Note that there's only one URL and it receives all post-receive POSTs
+for all your repositories. The server side of this URL is smart enough
+to figure out which projects need to be triggered, based on the
+submission.
+
+## Security Implications
+
+This plugin requires that you have an HTTP URL reachable from GitHub,
+which means it's reachable from the whole internet. So it is implemented
+carefully with the possible malicious fake post-receive POSTS in mind.
+To cope with this, upon receiving a POST, Jenkins will talk to GitHub to
+ensure the push was actually made.
+
+## Jenkins inside a firewall
+
+In case your Jenkins run inside the firewall and not directly reachable
+from the internet, this plugin lets you specify an arbitrary endpoint
+URL as an override in the automatic mode. The plugin will assume that
+you've set up reverse proxy or some other means so that the POST from
+GitHub will be routed to the Jenkins.
+
+## Trouble-shooting hooks
+
+If you set this up but build aren't triggered, check the following
+things:
+
+- Click the "admin" button on the GitHub repository in question and
+ make sure post-receive hooks are there.
+ - If it's not there, make sure you have proper credential set in
+ the Jenkins system config page.
+- Also, [enable
+ logging](https://wiki.jenkins.io/display/JENKINS/Logging) for the
+ class names
+ - `com.cloudbees.jenkins.GitHubPushTrigger`
+ - `org.jenkinsci.plugins.github.webhook.WebhookManager`
+ - `com.cloudbees.jenkins.GitHubWebHook`
+ and you'll see the log of Jenkins trying to install a
+ post-receive hook.
+- Click "Test hook" button from the GitHub UI and see if Jenkins
+ receive a payload.
+
+## Using cache to GitHub requests
+
+Each **GitHub Server Config** creates own GitHub client to interact with
+api. By default it uses cache (with **20MB** limit) to speedup process
+of fetching data and reduce rate-limit consuming. You can change cache
+limit value in "Advanced" section of this config item. If you set 0,
+then this feature will be disabled for this (and only this) config.
+
+Additional info:
+
+- This plugin now serves only hooks from github as main feature. Then
+ it starts using git-plugin to fetch sources.
+- It works both public and Enterprise GitHub
+- Plugin have some
+ [limitations](https://stackoverflow.com/questions/16323749/jenkins-github-plugin-inverse-branches)
+
+## Possible Issues between Jenkins and GitHub
+
+### Windows:
+
+- In windows, Jenkins will use the the SSH key of the user it is
+ running as, which is located in the %USERPROFILE%\\.ssh folder ( on
+ XP, that would be C:\\Documents and Settings\\USERNAME\\.ssh, and on
+ 7 it would be C:\\Users\\USERNAME\\.ssh). Therefore, you need to
+ force Jenkins to run as the user that has the SSH key configured. To
+ do that, right click on My Computer, and hit "Manage". Click on
+ "Services". Go to Jenkins, right click, and select "Properties".
+ Under the "Log On" tab, choose the user Jenkins will run as, and put
+ in the username and password (it requires one). Then restart the
+ Jenkins service by right clicking on Jenkins (in the services
+ window), and hit "Restart".
+- Jenkins does not support passphrases for SSH keys. Therefore, if you
+ set one while running the initial GitHub configuration, rerun it and
+ don't set one.
+
+## Pipeline examples
+
+### Setting commit status
+
+This code will set commit status for custom repo with configured context
+and message (you can also define same way backref)
+
+```groovy
+void setBuildStatus(String message, String state) {
+ step([
+ $class: "GitHubCommitStatusSetter",
+ reposSource: [$class: "ManuallyEnteredRepositorySource", url: "https://github.com/my-org/my-repo"],
+ contextSource: [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/build-status"],
+ errorHandlers: [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]],
+ statusResultSource: [ $class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]] ]
+ ]);
+}
+
+setBuildStatus("Build complete", "SUCCESS");
+```
+
+More complex example (can be used with multiple scm sources in pipeline)
+
+```groovy
+def getRepoURL() {
+ sh "git config --get remote.origin.url > .git/remote-url"
+ return readFile(".git/remote-url").trim()
+}
+
+def getCommitSha() {
+ sh "git rev-parse HEAD > .git/current-commit"
+ return readFile(".git/current-commit").trim()
+}
+
+def updateGithubCommitStatus(build) {
+ // workaround https://issues.jenkins-ci.org/browse/JENKINS-38674
+ repoUrl = getRepoURL()
+ commitSha = getCommitSha()
+
+ step([
+ $class: 'GitHubCommitStatusSetter',
+ reposSource: [$class: "ManuallyEnteredRepositorySource", url: repoUrl],
+ commitShaSource: [$class: "ManuallyEnteredShaSource", sha: commitSha],
+ errorHandlers: [[$class: 'ShallowAnyErrorHandler']],
+ statusResultSource: [
+ $class: 'ConditionalStatusResultSource',
+ results: [
+ [$class: 'BetterThanOrEqualBuildResult', result: 'SUCCESS', state: 'SUCCESS', message: build.description],
+ [$class: 'BetterThanOrEqualBuildResult', result: 'FAILURE', state: 'FAILURE', message: build.description],
+ [$class: 'AnyBuildResult', state: 'FAILURE', message: 'Loophole']
+ ]
+ ]
+ ])
+}
+```
+
+## Change Log
+
+[GitHub Releases](https://github.com/jenkinsci/github-plugin/releases)
+
+## Development
Start the local Jenkins instance:
mvn hpi:run
-Jenkins Plugin Maven goals
---------------------------
+## Jenkins Plugin Maven goals
hpi:create Creates a skeleton of a new plugin.
@@ -28,8 +263,7 @@ Jenkins Plugin Maven goals
hpi:upload Posts the hpi file to java.net. Used during the release.
-How to install
---------------
+## How to install
Run
@@ -42,10 +276,9 @@ To install:
1. copy the resulting ./target/rdoc.hpi file to the $JENKINS_HOME/plugins directory. Don't forget to restart Jenkins afterwards.
-2. or use the plugin management console (http://example.com:8080/pluginManager/advanced) to upload the hpi file. You have to restart Jenkins in order to find the pluing in the installed plugins list.
+2. or use the plugin management console (https://example.com:8080/pluginManager/advanced) to upload the hpi file. You have to restart Jenkins in order to find the plugin in the installed plugins list.
-Plugin releases
----------------
+## Plugin releases
mvn release:prepare release:perform -Dusername=juretta -Dpassword=******
diff --git a/codecov.yml b/codecov.yml
index e67465776..8a4b8e4c7 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,2 +1,2 @@
codecov:
- token: 9f11e1c0-2bd1-48d1-910e-24f8cf20cc4f
+ token: secret:eB8EFoOdXjvV5BGCkR+nCxMxNWJZqjpnfqPhrzFs6skp+IqoITDObS95TQwCvpUDISWyi3SeoJSrbbPubPUPWtgHjVIDg86fXQARSadlv5E=
diff --git a/docs/images/changes-2.png b/docs/images/changes-2.png
new file mode 100644
index 000000000..e55e4e9b2
Binary files /dev/null and b/docs/images/changes-2.png differ
diff --git a/docs/images/changes.png b/docs/images/changes.png
new file mode 100644
index 000000000..bc8e951cd
Binary files /dev/null and b/docs/images/changes.png differ
diff --git a/docs/images/ghserver-config.png b/docs/images/ghserver-config.png
new file mode 100644
index 000000000..471151457
Binary files /dev/null and b/docs/images/ghserver-config.png differ
diff --git a/docs/images/help_16.svg b/docs/images/help_16.svg
new file mode 100644
index 000000000..f904f3b28
--- /dev/null
+++ b/docs/images/help_16.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/images/manage-token.png b/docs/images/manage-token.png
new file mode 100644
index 000000000..6e506bec3
Binary files /dev/null and b/docs/images/manage-token.png differ
diff --git a/docs/images/secret-text.png b/docs/images/secret-text.png
new file mode 100644
index 000000000..5109c4f70
Binary files /dev/null and b/docs/images/secret-text.png differ
diff --git a/mvnw b/mvnw
new file mode 100755
index 000000000..19529ddf8
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 000000000..249bdf382
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
old mode 100644
new mode 100755
index c59097de6..edb5325a8
--- a/pom.xml
+++ b/pom.xml
@@ -5,20 +5,21 @@
org.jenkins-ci.pluginsplugin
- 2.6
+ 5.28
+ com.coravy.hudson.plugins.githubgithub
- 1.20.1-SNAPSHOT
+ ${revision}${changelist}hpiGitHub plugin
- http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin
+ https://github.com/jenkinsci/github-pluginMIT License
- http://www.opensource.org/licenses/mit-license.php
+ https://www.opensource.org/licenses/mit-license.phprepo
@@ -35,10 +36,10 @@
- scm:git:git://github.com/jenkinsci/github-plugin.git
- scm:git:git@github.com:jenkinsci/github-plugin.git
- https://github.com/jenkinsci/github-plugin
- HEAD
+ scm:git:https://github.com/${gitHubRepo}.git
+ scm:git:git@github.com:${gitHubRepo}.git
+ https://github.com/${gitHubRepo}
+ ${scmTag}JIRA
@@ -46,13 +47,15 @@
- 1.580
- 1.580
+ 1.45.1
+ -SNAPSHOT
+ jenkinsci/${project.artifactId}-plugin
+
+ 2.504
+ ${jenkins.baseline}.3false
- true
- 3.0.2
- 1
- 7
+ false
+ v@{project.version}
@@ -61,7 +64,7 @@
https://repo.jenkins-ci.org/public/
-
+
repo.jenkins-ci.org
@@ -71,161 +74,155 @@
- org.apache.commons
- commons-lang3
- 3.4
+ io.jenkins.plugins
+ commons-lang3-api
-
- org.slf4j
- slf4j-jdk14
- ${slf4jVersion}
+ io.jenkins.plugins
+ okhttp-api
-
- com.squareup.okhttp
- okhttp-urlconnection
- 2.7.5
- false
+ org.jenkins-ci.plugins
+ github-apiorg.jenkins-ci.plugins
- github-api
- 1.69
+ gitorg.jenkins-ci.plugins
- git
- 2.4.0
+ scm-apiorg.jenkins-ci.pluginscredentials
- 1.22org.jenkins-ci.pluginsplain-credentials
- 1.1org.jenkins-ci.pluginstoken-macro
- 1.11
-
+
+ org.jenkins-ci.plugins
+ display-url-api
+
+
+ org.jenkins-ci.modulesinstance-identity
- 1.3
- provided
+
+
+
+ io.jenkins.plugins
+ caffeine-api
+
+
- com.jayway.restassured
- rest-assured
- 2.4.0
+ org.jenkins-ci.plugins
+ apache-httpcomponents-client-4-apitest
- org.hamcrest
- hamcrest-all
- 1.3
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupitertest
+
- junit
- junit
- 4.12
+ org.jenkins-ci.plugins
+ matrix-authtest
- org.mockito
- mockito-core
- 1.10.19
+ io.jenkins
+ configuration-as-codetest
- org.jenkins-ci.plugins.workflow
- workflow-job
- 1.4
+ io.jenkins.configuration-as-code
+ test-harnesstestorg.jenkins-ci.plugins.workflowworkflow-cps
- 1.4
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-durable-task-steptest
- com.tngtech.java
- junit-dataprovider
- 1.10.0
+ org.jenkins-ci.plugins.workflow
+ workflow-jobtest
- com.github.tomakehurst
- wiremock
- 1.57
+ org.wiremock
+ wiremock-standalone
+ 3.12.1test
- standalone
-
-
- org.mortbay.jetty
- jetty
-
-
- com.google.guava
- guava
-
-
- org.apache.httpcomponents
- httpclient
-
-
- xmlunit
- xmlunit
-
-
- com.jayway.jsonpath
- json-path
-
-
- net.sf.jopt-simple
- jopt-simple
-
-
- xml-apis
- xml-apis
- 1.4.01
+ io.rest-assured
+ rest-assured
+ 5.3.2test
+
+
+
+ io.jenkins.tools.bom
+ bom-${jenkins.baseline}.x
+ 5804.v80587a_38d937
+ import
+ pom
+
+
+
+ org.jenkins-ci.plugins
+ credentials
+ 1480.v2246fd131e83
+
+
+
+
nl.geodienstencentrum.mavensass-maven-plugin
- 2.14
+ 3.7.2
@@ -240,7 +237,7 @@
maven-checkstyle-plugin
- 2.16
+ 3.6.0checkstyle
@@ -251,7 +248,6 @@
- UTF-8truetruefalse
@@ -263,38 +259,6 @@
-
-
- org.codehaus.mojo
- findbugs-maven-plugin
- ${findbugs-maven-plugin.version}
-
- Max
- Low
- true
- false
-
-
-
-
- check
-
-
-
-
-
-
- maven-release-plugin
-
- v@{project.version}
- forked-path
- false
- clean install
- deploy
- ${arguments}
- jenkins-release,${releaseProfiles}
-
-
diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java
index 7544ca6e2..027083192 100644
--- a/src/main/java/com/cloudbees/jenkins/Cleaner.java
+++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java
@@ -1,7 +1,7 @@
package com.cloudbees.jenkins;
import hudson.Extension;
-import hudson.model.Job;
+import hudson.model.Item;
import hudson.model.PeriodicWork;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.github.GitHubPlugin;
@@ -28,19 +28,19 @@
public class Cleaner extends PeriodicWork {
/**
* Queue contains repo names prepared to cleanup.
- * After configure method on job, trigger calls {@link #onStop(Job)}
+ * After configure method on job, trigger calls {@link #onStop(Item)}
* which converts to repo names with help of contributors.
*
* This queue is thread-safe, so any thread can write or
* fetch names to this queue without additional sync
*/
- private final Queue cleanQueue = new ConcurrentLinkedQueue();
+ private final Queue cleanQueue = new ConcurrentLinkedQueue<>();
/**
* Called when a {@link GitHubPushTrigger} is about to be removed.
*/
- /* package */ void onStop(Job, ?> job) {
- cleanQueue.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job));
+ /* package */ void onStop(Item item) {
+ cleanQueue.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(item));
}
@Override
@@ -61,8 +61,7 @@ protected void doRun() throws Exception {
URL url = GitHubPlugin.configuration().getHookUrl();
- List jobs = Jenkins.getInstance().getAllItems(Job.class);
- List aliveRepos = from(jobs)
+ List aliveRepos = from(Jenkins.get().allItems(Item.class))
.filter(isAlive()) // live repos
.transformAndConcat(associatedNames()).toList();
diff --git a/src/main/java/com/cloudbees/jenkins/Credential.java b/src/main/java/com/cloudbees/jenkins/Credential.java
index d5b801a7b..99e766119 100644
--- a/src/main/java/com/cloudbees/jenkins/Credential.java
+++ b/src/main/java/com/cloudbees/jenkins/Credential.java
@@ -7,7 +7,7 @@
import org.kohsuke.github.GitHub;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.CheckForNull;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
import java.io.IOException;
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java
index a0e662024..9d7663e51 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java
@@ -32,12 +32,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_DisplayName;
-import static com.google.common.base.Objects.firstNonNull;
import static hudson.model.Result.FAILURE;
import static hudson.model.Result.SUCCESS;
import static hudson.model.Result.UNSTABLE;
@@ -93,17 +91,17 @@ public void setStatusMessage(ExpandableMessage statusMessage) {
/**
* @since 1.10
*/
- @Nonnull
+ @NonNull
public String getResultOnFailure() {
return resultOnFailure != null ? resultOnFailure : getDefaultResultOnFailure().toString();
}
- @Nonnull
+ @NonNull
public static Result getDefaultResultOnFailure() {
return FAILURE;
}
- @Nonnull
+ @NonNull
/*package*/ Result getEffectiveResultOnFailure() {
return Result.fromString(trimToEmpty(resultOnFailure));
}
@@ -125,7 +123,7 @@ public void perform(@NonNull Run, ?> build,
setter.setContextSource(new DefaultCommitContextSource());
- String content = firstNonNull(statusMessage, DEFAULT_MESSAGE).getContent();
+ String content = (statusMessage != null ? statusMessage : DEFAULT_MESSAGE).getContent();
if (isNotBlank(content)) {
setter.setStatusResultSource(new ConditionalStatusResultSource(
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java b/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java
index 3fe337618..1604e5e87 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java
@@ -4,6 +4,7 @@
import java.io.File;
import java.io.IOException;
+import java.util.Objects;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
@@ -37,5 +38,19 @@ public GitHubPushCause(File pollingLog, String pusher) throws IOException {
public String getShortDescription() {
return format("Started by GitHub push by %s", trimToEmpty(pushedBy));
}
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof GitHubPushCause
+ && Objects.equals(this.pushedBy, ((GitHubPushCause) o).pushedBy)
+ && super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = super.hashCode();
+ hash = 89 * hash + Objects.hash(this.pushedBy);
+ return hash;
+ }
}
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java
index 15d2b421f..4cae5f049 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java
@@ -2,6 +2,7 @@
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.Util;
import hudson.XmlFile;
@@ -18,10 +19,10 @@
import hudson.util.NamingThreadFactory;
import hudson.util.SequentialExecutionQueue;
import hudson.util.StreamTaskListener;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn;
+import jenkins.scm.api.SCMEvent;
+import jenkins.triggers.SCMTriggerItem;
import jenkins.triggers.SCMTriggerItem.SCMTriggerItems;
import org.apache.commons.jelly.XMLOutput;
import org.jenkinsci.plugins.github.GitHubPlugin;
@@ -29,12 +30,17 @@
import org.jenkinsci.plugins.github.config.GitHubPluginConfig;
import org.jenkinsci.plugins.github.internal.GHPluginConfigException;
import org.jenkinsci.plugins.github.migration.Migrator;
+import org.jenkinsci.Symbol;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.Stapler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.inject.Inject;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import jakarta.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
@@ -45,9 +51,13 @@
import java.util.Collections;
import java.util.Date;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.commons.lang3.Validate.notNull;
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.asParameterizedJobMixIn;
/**
@@ -66,26 +76,53 @@ public GitHubPushTrigger() {
*/
@Deprecated
public void onPost() {
- onPost("");
+ onPost(GitHubTriggerEvent.create()
+ .build()
+ );
}
/**
* Called when a POST is made.
*/
public void onPost(String triggeredByUser) {
- final String pushBy = triggeredByUser;
+ onPost(GitHubTriggerEvent.create()
+ .withOrigin(SCMEvent.originOf(Stapler.getCurrentRequest2()))
+ .withTriggeredByUser(triggeredByUser)
+ .build()
+ );
+ }
+
+ /**
+ * Called when a POST is made.
+ */
+ public void onPost(final GitHubTriggerEvent event) {
+ if (Objects.isNull(job)) {
+ return; // nothing to do
+ }
+
+ Job, ?> currentJob = notNull(job, "Job can't be null");
+
+ final String pushBy = event.getTriggeredByUser();
DescriptorImpl d = getDescriptor();
d.checkThreadPoolSizeAndUpdateIfNecessary();
d.queue.execute(new Runnable() {
private boolean runPolling() {
try {
- StreamTaskListener listener = new StreamTaskListener(getLogFile());
+ StreamTaskListener listener = new StreamTaskListener(getLogFileForJob(currentJob));
try {
PrintStream logger = listener.getLogger();
+
long start = System.currentTimeMillis();
logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date()));
- boolean result = SCMTriggerItems.asSCMTriggerItem(job).poll(listener).hasChanges();
+ if (event.getOrigin() != null) {
+ logger.format("Started by event from %s on %tc%n", event.getOrigin(), event.getTimestamp());
+ }
+ SCMTriggerItem item = SCMTriggerItems.asSCMTriggerItem(currentJob);
+ if (null == item) {
+ throw new IllegalStateException("Job is not an SCMTriggerItem: " + currentJob);
+ }
+ boolean result = item.poll(listener).hasChanges();
logger.println("Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start));
if (result) {
logger.println("Changes found");
@@ -114,16 +151,18 @@ public void run() {
if (runPolling()) {
GitHubPushCause cause;
try {
- cause = new GitHubPushCause(getLogFile(), pushBy);
+ cause = new GitHubPushCause(getLogFileForJob(currentJob), pushBy);
} catch (IOException e) {
LOGGER.warn("Failed to parse the polling log", e);
cause = new GitHubPushCause(pushBy);
}
- if (asParameterizedJobMixIn(job).scheduleBuild(cause)) {
- LOGGER.info("SCM changes detected in " + job.getFullName()
- + ". Triggering #" + job.getNextBuildNumber());
+
+ if (asParameterizedJobMixIn(currentJob).scheduleBuild(cause)) {
+ LOGGER.info("SCM changes detected in " + currentJob.getFullName()
+ + ". Triggering #" + currentJob.getNextBuildNumber());
} else {
- LOGGER.info("SCM changes detected in " + job.getFullName() + ". Job is already in the queue");
+ LOGGER.info("SCM changes detected in " + currentJob.getFullName()
+ + ". Job is already in the queue");
}
}
}
@@ -134,6 +173,17 @@ public void run() {
* Returns the file that records the last/current polling activity.
*/
public File getLogFile() {
+ try {
+ return getLogFileForJob(notNull(job, "Job can't be null!"));
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Returns the file that records the last/current polling activity.
+ */
+ private File getLogFileForJob(@NonNull Job job) throws IOException {
return new File(job.getRootDir(), "github-polling.log");
}
@@ -213,7 +263,7 @@ public String getUrlName() {
}
public String getLog() throws IOException {
- return Util.loadFile(getLogFile());
+ return Util.loadFile(getLogFileForJob(Objects.requireNonNull(job)));
}
/**
@@ -221,13 +271,22 @@ public String getLog() throws IOException {
*
* @since 1.350
*/
+ @SuppressFBWarnings(
+ value = "RV_RETURN_VALUE_IGNORED",
+ justification =
+ "method signature does not permit plumbing through the return value")
public void writeLogTo(XMLOutput out) throws IOException {
- new AnnotatedLargeText(getLogFile(), Charsets.UTF_8, true, this)
+ new AnnotatedLargeText(
+ getLogFileForJob(Objects.requireNonNull(job)),
+ Charsets.UTF_8,
+ true,
+ this)
.writeHtmlTo(0, out.asWriter());
}
}
@Extension
+ @Symbol("githubPush")
public static class DescriptorImpl extends TriggerDescriptor {
private final transient SequentialExecutionQueue queue =
new SequentialExecutionQueue(Executors.newSingleThreadExecutor(threadFactory()));
@@ -273,7 +332,7 @@ public boolean isApplicable(Item item) {
@Override
public String getDisplayName() {
- return "Build when a change is pushed to GitHub";
+ return "GitHub hook trigger for GITScm polling";
}
/**
@@ -339,11 +398,11 @@ public void clearCredentials() {
}
/**
- * @deprecated use {@link GitHubPluginConfig#isOverrideHookURL()}
+ * @deprecated use {@link GitHubPluginConfig#isOverrideHookUrl()}
*/
@Deprecated
public boolean hasOverrideURL() {
- return GitHubPlugin.configuration().isOverrideHookURL();
+ return GitHubPlugin.configuration().isOverrideHookUrl();
}
/**
@@ -368,19 +427,24 @@ private static ThreadFactory threadFactory() {
}
/**
- * Checks that repo defined in this job is not in administrative monitor as failed to be registered.
+ * Checks that repo defined in this item is not in administrative monitor as failed to be registered.
* If that so, shows warning with some instructions
*
- * @param job - to check against. Should be not null and have at least one repo defined
+ * @param item - to check against. Should be not null and have at least one repo defined
*
* @return warning or empty string
- * @since TODO
+ * @since 1.17.0
*/
@SuppressWarnings("unused")
- public FormValidation doCheckHookRegistered(@AncestorInPath Job, ?> job) {
- Preconditions.checkNotNull(job, "Job can't be null if wants to check hook in monitor");
+ @Restricted(NoExternalUse.class) // invoked from Stapler
+ public FormValidation doCheckHookRegistered(@AncestorInPath Item item) {
+ Preconditions.checkNotNull(item, "Item can't be null if wants to check hook in monitor");
+
+ if (!item.hasPermission(Item.CONFIGURE)) {
+ return FormValidation.ok();
+ }
- Collection repos = GitHubRepositoryNameContributor.parseAssociatedNames(job);
+ Collection repos = GitHubRepositoryNameContributor.parseAssociatedNames(item);
for (GitHubRepositoryName repo : repos) {
if (monitor.isProblemWith(repo)) {
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java
index 658d52460..5cdb857b3 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java
@@ -12,12 +12,13 @@
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.kohsuke.github.GHCommitPointer;
import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -45,22 +46,22 @@ public class GitHubRepositoryName {
* from URLs that include a '.git' suffix, removing the suffix from the
* repository name.
*/
- Pattern.compile("git@(.+):([^/]+)/([^/]+)\\.git"),
- Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)\\.git"),
- Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)\\.git"),
- Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)\\.git"),
- Pattern.compile("ssh://(?:git@)?([^/]+)/([^/]+)/([^/]+)\\.git"),
+ Pattern.compile(".+@(.+):([^/]+)/([^/]+)\\.git(?:/)?"),
+ Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
+ Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
+ Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
+ Pattern.compile("(?:git\\+)?ssh://(?:.+@)?([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
/**
* The second set of patterns extract the host, owner and repository names
* from all other URLs. Note that these patterns must be processed *after*
* the first set, to avoid any '.git' suffix that may be present being included
* in the repository name.
*/
- Pattern.compile("git@(.+):([^/]+)/([^/]+)/?"),
+ Pattern.compile(".+@(.+):([^/]+)/([^/]+)/?"),
Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"),
Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"),
Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"),
- Pattern.compile("ssh://(?:git@)?([^/]+)/([^/]+)/([^/]+)/?")
+ Pattern.compile("(?:git\\+)?ssh://(?:.+@)?([^/]+)/([^/]+)/([^/]+)/?"),
};
/**
@@ -82,7 +83,7 @@ public static GitHubRepositoryName create(String url) {
return ret;
}
}
- LOGGER.warn("Could not match URL {}", url);
+ LOGGER.debug("Could not match URL {}", url);
return null;
}
@@ -180,7 +181,15 @@ public GHRepository resolveOne() {
* Does this repository match the repository referenced in the given {@link GHCommitPointer}?
*/
public boolean matches(GHCommitPointer commit) {
- return userName.equals(commit.getUser().getLogin())
+ final GHUser user;
+ try {
+ user = commit.getUser();
+ } catch (IOException ex) {
+ LOGGER.debug("Failed to extract user from commit " + commit, ex);
+ return false;
+ }
+
+ return userName.equals(user.getLogin())
&& repositoryName.equals(commit.getRepository().getName())
&& host.equals(commit.getRepository().getHtmlUrl().getHost());
}
@@ -213,7 +222,7 @@ public String toString() {
private static Function toGHRepository(final GitHubRepositoryName repoName) {
return new NullSafeFunction() {
@Override
- protected GHRepository applyNullSafe(@Nonnull GitHub gitHub) {
+ protected GHRepository applyNullSafe(@NonNull GitHub gitHub) {
try {
return gitHub.getRepository(format("%s/%s", repoName.getUserName(), repoName.getRepositoryName()));
} catch (IOException e) {
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java
index 948072527..572a77631 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java
@@ -7,6 +7,7 @@
import hudson.Util;
import hudson.model.AbstractProject;
import hudson.model.EnvironmentContributor;
+import hudson.model.Item;
import hudson.model.Job;
import hudson.model.TaskListener;
import hudson.plugins.git.GitSCM;
@@ -36,41 +37,57 @@ public abstract class GitHubRepositoryNameContributor implements ExtensionPoint
* Looks at the definition of {@link AbstractProject} and list up the related github repositories,
* then puts them into the collection.
*
- * @deprecated Use {@link #parseAssociatedNames(Job, Collection)}
+ * @deprecated Use {@link #parseAssociatedNames(Item, Collection)}
*/
@Deprecated
public void parseAssociatedNames(AbstractProject, ?> job, Collection result) {
- parseAssociatedNames((Job) job, result);
+ parseAssociatedNames((Item) job, result);
}
/**
* Looks at the definition of {@link Job} and list up the related github repositories,
* then puts them into the collection.
+ * @deprecated Use {@link #parseAssociatedNames(Item, Collection)}
*/
+ @Deprecated
public /*abstract*/ void parseAssociatedNames(Job, ?> job, Collection result) {
- if (overriddenMethodHasDeprecatedSignature(job)) {
- parseAssociatedNames((AbstractProject) job, result);
- } else {
- throw new AbstractMethodError("you must override the new overload of parseAssociatedNames");
- }
+ parseAssociatedNames((Item) job, result);
}
/**
- * To select backward compatible method with old extensions
- * with overridden {@link #parseAssociatedNames(AbstractProject, Collection)}
- *
- * @param job - parameter to check for old class
- *
- * @return true if overridden deprecated method
+ * Looks at the definition of {@link Item} and list up the related github repositories,
+ * then puts them into the collection.
+ * @param item the item.
+ * @param result the collection to add repository names to
+ * @since 1.25.0
*/
- private boolean overriddenMethodHasDeprecatedSignature(Job, ?> job) {
- return Util.isOverridden(
+ @SuppressWarnings("deprecation")
+ public /*abstract*/ void parseAssociatedNames(Item item, Collection result) {
+ if (Util.isOverridden(
+ GitHubRepositoryNameContributor.class,
+ getClass(),
+ "parseAssociatedNames",
+ Job.class,
+ Collection.class
+ )) {
+ // if this impl is legacy, it cannot contribute to non-jobs, so not an error
+ if (item instanceof Job) {
+ parseAssociatedNames((Job, ?>) item, result);
+ }
+ } else if (Util.isOverridden(
GitHubRepositoryNameContributor.class,
getClass(),
"parseAssociatedNames",
AbstractProject.class,
Collection.class
- ) && job instanceof AbstractProject;
+ )) {
+ // if this impl is legacy, it cannot contribute to non-projects, so not an error
+ if (item instanceof AbstractProject) {
+ parseAssociatedNames((AbstractProject, ?>) item, result);
+ }
+ } else {
+ throw new AbstractMethodError("you must override the new overload of parseAssociatedNames");
+ }
}
public static ExtensionList all() {
@@ -82,13 +99,21 @@ public static ExtensionList all() {
*/
@Deprecated
public static Collection parseAssociatedNames(AbstractProject, ?> job) {
- return parseAssociatedNames((Job) job);
+ return parseAssociatedNames((Item) job);
}
+ /**
+ * @deprecated Use {@link #parseAssociatedNames(Item)}
+ */
+ @Deprecated
public static Collection parseAssociatedNames(Job, ?> job) {
+ return parseAssociatedNames((Item) job);
+ }
+
+ public static Collection parseAssociatedNames(Item item) {
Set names = new HashSet();
for (GitHubRepositoryNameContributor c : all()) {
- c.parseAssociatedNames(job, names);
+ c.parseAssociatedNames(item, names);
}
return names;
}
@@ -99,11 +124,11 @@ public static Collection parseAssociatedNames(Job, ?> jo
@Extension
public static class FromSCM extends GitHubRepositoryNameContributor {
@Override
- public void parseAssociatedNames(Job, ?> job, Collection result) {
- SCMTriggerItem item = SCMTriggerItems.asSCMTriggerItem(job);
- EnvVars envVars = buildEnv(job);
- if (item != null) {
- for (SCM scm : item.getSCMs()) {
+ public void parseAssociatedNames(Item item, Collection result) {
+ SCMTriggerItem triggerItem = SCMTriggerItems.asSCMTriggerItem(item);
+ EnvVars envVars = item instanceof Job ? buildEnv((Job) item) : new EnvVars();
+ if (triggerItem != null) {
+ for (SCM scm : triggerItem.getSCMs()) {
addRepositories(scm, envVars, result);
}
}
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java
index c5a746ee7..f30ff9136 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java
@@ -11,6 +11,7 @@
import hudson.tasks.Builder;
import jenkins.tasks.SimpleBuildStep;
import org.jenkinsci.plugins.github.common.ExpandableMessage;
+import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource;
import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler;
import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult;
import org.jenkinsci.plugins.github.status.GitHubCommitStatusSetter;
@@ -26,7 +27,6 @@
import java.io.IOException;
import java.util.Collections;
-import static com.google.common.base.Objects.firstNonNull;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.jenkinsci.plugins.github.status.sources.misc.AnyBuildResult.onAnyResult;
@@ -35,6 +35,7 @@ public class GitHubSetCommitStatusBuilder extends Builder implements SimpleBuild
private static final ExpandableMessage DEFAULT_MESSAGE = new ExpandableMessage("");
private ExpandableMessage statusMessage = DEFAULT_MESSAGE;
+ private GitHubStatusContextSource contextSource = new DefaultCommitContextSource();
@DataBoundConstructor
public GitHubSetCommitStatusBuilder() {
@@ -47,6 +48,14 @@ public ExpandableMessage getStatusMessage() {
return statusMessage;
}
+ /**
+ * @return Context provider
+ * @since 1.24.0
+ */
+ public GitHubStatusContextSource getContextSource() {
+ return contextSource;
+ }
+
/**
* @since 1.14.1
*/
@@ -55,6 +64,14 @@ public void setStatusMessage(ExpandableMessage statusMessage) {
this.statusMessage = statusMessage;
}
+ /**
+ * @since 1.24.0
+ */
+ @DataBoundSetter
+ public void setContextSource(GitHubStatusContextSource contextSource) {
+ this.contextSource = contextSource;
+ }
+
@Override
public void perform(@NonNull Run, ?> build,
@NonNull FilePath workspace,
@@ -64,14 +81,14 @@ public void perform(@NonNull Run, ?> build,
GitHubCommitStatusSetter setter = new GitHubCommitStatusSetter();
setter.setReposSource(new AnyDefinedRepositorySource());
setter.setCommitShaSource(new BuildDataRevisionShaSource());
- setter.setContextSource(new DefaultCommitContextSource());
+ setter.setContextSource(contextSource);
setter.setErrorHandlers(Collections.singletonList(new ShallowAnyErrorHandler()));
setter.setStatusResultSource(new ConditionalStatusResultSource(
Collections.singletonList(
onAnyResult(
GHCommitState.PENDING,
- defaultIfEmpty(firstNonNull(statusMessage, DEFAULT_MESSAGE).getContent(),
+ defaultIfEmpty((statusMessage != null ? statusMessage : DEFAULT_MESSAGE).getContent(),
Messages.CommitNotifier_Pending(build.getDisplayName()))
)
)));
@@ -79,6 +96,14 @@ public void perform(@NonNull Run, ?> build,
setter.perform(build, workspace, launcher, listener);
}
+
+ public Object readResolve() {
+ if (getContextSource() == null) {
+ setContextSource(new DefaultCommitContextSource());
+ }
+ return this;
+ }
+
@Extension
public static class Descriptor extends BuildStepDescriptor {
@Override
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java
index 1908b934d..9d44eb838 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java
@@ -3,7 +3,7 @@
import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractProject;
-import hudson.model.Job;
+import hudson.model.Item;
import hudson.triggers.Trigger;
import jenkins.model.ParameterizedJobMixIn;
@@ -15,6 +15,7 @@
* and triggers a build.
*
* @author aaronwalker
+ * @deprecated not used any more
*/
public interface GitHubTrigger {
@@ -46,9 +47,9 @@ public interface GitHubTrigger {
@Extension
class GitHubRepositoryNameContributorImpl extends GitHubRepositoryNameContributor {
@Override
- public void parseAssociatedNames(Job, ?> job, Collection result) {
- if (job instanceof ParameterizedJobMixIn.ParameterizedJob) {
- ParameterizedJobMixIn.ParameterizedJob p = (ParameterizedJobMixIn.ParameterizedJob) job;
+ public void parseAssociatedNames(Item item, Collection result) {
+ if (item instanceof ParameterizedJobMixIn.ParameterizedJob) {
+ ParameterizedJobMixIn.ParameterizedJob p = (ParameterizedJobMixIn.ParameterizedJob) item;
// TODO use standard method in 1.621+
for (GitHubTrigger ght : Util.filter(p.getTriggers().values(), GitHubTrigger.class)) {
result.addAll(ght.getGitHubRepositories());
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java b/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java
new file mode 100644
index 000000000..fdae66124
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java
@@ -0,0 +1,125 @@
+package com.cloudbees.jenkins;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jenkins.scm.api.SCMEvent;
+
+/**
+ * Encapsulates an event for {@link GitHubPushTrigger}.
+ *
+ * @since 1.26.0
+ */
+public class GitHubTriggerEvent {
+
+ /**
+ * The timestamp of the event (or if unavailable when the event was received)
+ */
+ private final long timestamp;
+ /**
+ * The origin of the event (see {@link SCMEvent#originOf(HttpServletRequest)})
+ */
+ private final String origin;
+ /**
+ * The user that the event was provided by.
+ */
+ private final String triggeredByUser;
+
+ private GitHubTriggerEvent(long timestamp, String origin, String triggeredByUser) {
+ this.timestamp = timestamp;
+ this.origin = origin;
+ this.triggeredByUser = triggeredByUser;
+ }
+
+ public static Builder create() {
+ return new Builder();
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getOrigin() {
+ return origin;
+ }
+
+ public String getTriggeredByUser() {
+ return triggeredByUser;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ GitHubTriggerEvent that = (GitHubTriggerEvent) o;
+
+ if (timestamp != that.timestamp) {
+ return false;
+ }
+ if (origin != null ? !origin.equals(that.origin) : that.origin != null) {
+ return false;
+ }
+ return triggeredByUser != null ? triggeredByUser.equals(that.triggeredByUser) : that.triggeredByUser == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (timestamp ^ (timestamp >>> 32));
+ result = 31 * result + (origin != null ? origin.hashCode() : 0);
+ result = 31 * result + (triggeredByUser != null ? triggeredByUser.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "GitHubTriggerEvent{"
+ + "timestamp=" + timestamp
+ + ", origin='" + origin + '\''
+ + ", triggeredByUser='" + triggeredByUser + '\''
+ + '}';
+ }
+
+ /**
+ * Builder for {@link GitHubTriggerEvent} instances..
+ */
+ public static class Builder {
+ private long timestamp;
+ private String origin;
+ private String triggeredByUser;
+
+ private Builder() {
+ timestamp = System.currentTimeMillis();
+ }
+
+ public Builder withTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ public Builder withOrigin(String origin) {
+ this.origin = origin;
+ return this;
+ }
+
+ public Builder withTriggeredByUser(String triggeredByUser) {
+ this.triggeredByUser = triggeredByUser;
+ return this;
+ }
+
+ public GitHubTriggerEvent build() {
+ return new GitHubTriggerEvent(timestamp, origin, triggeredByUser);
+ }
+
+ @Override
+ public String toString() {
+ return "GitHubTriggerEvent.Builder{"
+ + "timestamp=" + timestamp
+ + ", origin='" + origin + '\''
+ + ", triggeredByUser='" + triggeredByUser + '\''
+ + '}';
+ }
+ }
+}
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java
index dd494795c..887a1a366 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java
@@ -3,23 +3,29 @@
import com.google.common.base.Function;
import hudson.Extension;
import hudson.ExtensionPoint;
+import hudson.model.Item;
import hudson.model.Job;
import hudson.model.RootAction;
import hudson.model.UnprotectedRootAction;
import hudson.util.SequentialExecutionQueue;
import jenkins.model.Jenkins;
+import jenkins.scm.api.SCMEvent;
import org.apache.commons.lang3.Validate;
import org.jenkinsci.plugins.github.GitHubPlugin;
+import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github.internal.GHPluginConfigException;
import org.jenkinsci.plugins.github.webhook.GHEventHeader;
import org.jenkinsci.plugins.github.webhook.GHEventPayload;
import org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GHEvent;
+import org.kohsuke.stapler.Stapler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.net.URL;
import java.util.List;
@@ -46,6 +52,12 @@ public class GitHubWebHook implements UnprotectedRootAction {
// headers used for testing the endpoint configuration
public static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation";
public static final String X_INSTANCE_IDENTITY = "X-Instance-Identity";
+ /**
+ * X-GitHub-Delivery: A globally unique identifier (GUID) to identify the event.
+ * @see Delivery
+ * headers
+ */
+ public static final String X_GITHUB_DELIVERY = "X-GitHub-Delivery";
private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting);
@@ -70,21 +82,36 @@ public String getUrlName() {
* {@code GitHubWebHook.get().registerHookFor(job);}
*
* @param job not null project to register hook for
+ * @deprecated use {@link #registerHookFor(Item)}
*/
+ @Deprecated
public void registerHookFor(Job job) {
reRegisterHookForJob().apply(job);
}
+ /**
+ * If any wants to auto-register hook, then should call this method
+ * Example code:
+ * {@code GitHubWebHook.get().registerHookFor(item);}
+ *
+ * @param item not null item to register hook for
+ * @since 1.25.0
+ */
+ public void registerHookFor(Item item) {
+ reRegisterHookForJob().apply(item);
+ }
+
/**
* Calls {@link #registerHookFor(Job)} for every project which have subscriber
*
* @return list of jobs which jenkins tried to register hook
*/
- public List reRegisterAllHooks() {
- return from(getJenkinsInstance().getAllItems(Job.class))
+ public List reRegisterAllHooks() {
+ return from(getJenkinsInstance().getAllItems(Item.class))
.filter(isBuildable())
.filter(isAlive())
- .transform(reRegisterHookForJob()).toList();
+ .transform(reRegisterHookForJob())
+ .toList();
}
/**
@@ -95,17 +122,21 @@ public List reRegisterAllHooks() {
*/
@SuppressWarnings("unused")
@RequirePostWithGHHookPayload
- public void doIndex(@Nonnull @GHEventHeader GHEvent event, @Nonnull @GHEventPayload String payload) {
+ public void doIndex(@NonNull @GHEventHeader GHEvent event, @NonNull @GHEventPayload String payload) {
+ var currentRequest = Stapler.getCurrentRequest2();
+ String eventGuid = currentRequest.getHeader(X_GITHUB_DELIVERY);
+ GHSubscriberEvent subscriberEvent =
+ new GHSubscriberEvent(eventGuid, SCMEvent.originOf(currentRequest), event, payload);
from(GHEventsSubscriber.all())
.filter(isInterestedIn(event))
- .transform(processEvent(event, payload)).toList();
+ .transform(processEvent(subscriberEvent)).toList();
}
- private Function reRegisterHookForJob() {
- return new Function() {
+ private Function reRegisterHookForJob() {
+ return new Function() {
@Override
- public Job apply(Job job) {
- LOGGER.debug("Calling registerHooks() for {}", notNull(job, "Job can't be null").getFullName());
+ public T apply(T job) {
+ LOGGER.debug("Calling registerHooks() for {}", notNull(job, "Item can't be null").getFullName());
// We should handle wrong url of self defined hook url here in any case with try-catch :(
URL hookUrl;
@@ -126,7 +157,7 @@ public static GitHubWebHook get() {
return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class);
}
- @Nonnull
+ @NonNull
public static Jenkins getJenkinsInstance() throws IllegalStateException {
Jenkins instance = Jenkins.getInstance();
Validate.validState(instance != null, "Jenkins has not been started, or was already shut down");
@@ -137,7 +168,11 @@ public static Jenkins getJenkinsInstance() throws IllegalStateException {
* Other plugins may be interested in listening for these updates.
*
* @since 1.8
+ * @deprecated working theory is that this API is not required any more with the {@link SCMEvent} based API,
+ * if wrong, please raise a JIRA
*/
+ @Deprecated
+ @Restricted(NoExternalUse.class)
public abstract static class Listener implements ExtensionPoint {
/**
diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java
index b102a5ed4..39191f388 100644
--- a/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java
+++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java
@@ -3,12 +3,14 @@
import hudson.Extension;
import hudson.security.csrf.CrumbExclusion;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+
@Extension
public class GitHubWebHookCrumbExclusion extends CrumbExclusion {
@@ -16,11 +18,16 @@ public class GitHubWebHookCrumbExclusion extends CrumbExclusion {
public boolean process(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
throws IOException, ServletException {
String pathInfo = req.getPathInfo();
- if (pathInfo != null && pathInfo.equals(getExclusionPath())) {
- chain.doFilter(req, resp);
- return true;
+ if (isEmpty(pathInfo)) {
+ return false;
+ }
+ // GitHub will not follow redirects https://github.com/isaacs/github/issues/574
+ pathInfo = pathInfo.endsWith("/") ? pathInfo : pathInfo + '/';
+ if (!pathInfo.equals(getExclusionPath())) {
+ return false;
}
- return false;
+ chain.doFilter(req, resp);
+ return true;
}
public String getExclusionPath() {
diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java
index b92c5f4b4..662b714cb 100644
--- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java
+++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java
@@ -10,9 +10,9 @@
import java.util.Collections;
/**
- * Add the Github Logo/Icon to the sidebar.
+ * Add the GitHub Logo/Icon to the sidebar.
*
- * @author Stefan Saasen
+ * @author Stefan Saasen
*/
public final class GithubLinkAction implements Action {
@@ -29,7 +29,7 @@ public String getDisplayName() {
@Override
public String getIconFileName() {
- return "/plugin/github/logov3.png";
+ return "symbol-logo-github plugin-github";
}
@Override
diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java
index 388901f02..d96acee40 100644
--- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java
+++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java
@@ -7,9 +7,20 @@
import hudson.plugins.git.GitChangeSet;
import hudson.scm.ChangeLogAnnotator;
import hudson.scm.ChangeLogSet.Entry;
+import org.apache.commons.lang3.StringUtils;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.CheckReturnValue;
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+import static hudson.Functions.htmlAttributeEscape;
import static java.lang.String.format;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
import java.util.regex.Pattern;
/**
@@ -17,15 +28,22 @@
*
* It's based on the TracLinkAnnotator.
*
- *
- * @author Stefan Saasen
- * @todo Change the annotator to use GithubUrl instead of the String url.
+ * TODO Change the annotator to use GithubUrl instead of the String url.
* Knowledge about the github url structure should be encapsulated in
* GithubUrl.
+ *
+ * @author Stefan Saasen
*/
@Extension
public class GithubLinkAnnotator extends ChangeLogAnnotator {
+ private static final Set ALLOWED_URI_SCHEMES = new HashSet();
+
+ static {
+ ALLOWED_URI_SCHEMES.addAll(
+ Arrays.asList("http", "https"));
+ }
+
@Override
public void annotate(Run, ?> build, Entry change, MarkupText text) {
final GithubProjectProperty p = build.getParent().getProperty(
@@ -38,15 +56,18 @@ public void annotate(Run, ?> build, Entry change, MarkupText text) {
void annotate(final GithubUrl url, final MarkupText text, final Entry change) {
final String base = url.baseUrl();
+ boolean isValid = verifyUrl(base);
+ if (!isValid) {
+ throw new IllegalArgumentException("The provided GitHub URL is not valid");
+ }
for (LinkMarkup markup : MARKUPS) {
markup.process(text, base);
}
-
if (change instanceof GitChangeSet) {
GitChangeSet cs = (GitChangeSet) change;
final String id = cs.getId();
text.wrapBy("", format(" (commit: %s)",
- url.commitId(id),
+ htmlAttributeEscape(url.commitId(id)),
id.substring(0, Math.min(id.length(), 7))));
}
}
@@ -66,7 +87,7 @@ private static final class LinkMarkup {
void process(MarkupText text, String url) {
for (SubText st : text.findTokens(pattern)) {
- st.surroundWith("", "");
+ st.surroundWith("", "");
}
}
@@ -77,5 +98,35 @@ void process(MarkupText text, String url) {
private static final LinkMarkup[] MARKUPS = new LinkMarkup[]{new LinkMarkup(
"(?:C|c)lose(?:s?)\\s(?
+ * @author Stefan Saasen
*/
public final class GithubProjectProperty extends JobProperty> {
@@ -88,7 +89,7 @@ public void setDisplayName(String displayName) {
* @return display name or full job name if field is not defined
* @since 1.14.1
*/
- public static String displayNameFor(@Nonnull Job, ?> job) {
+ public static String displayNameFor(@NonNull Job, ?> job) {
GithubProjectProperty ghProp = job.getProperty(GithubProjectProperty.class);
if (ghProp != null && isNotBlank(ghProp.getDisplayName())) {
return ghProp.getDisplayName();
@@ -98,6 +99,7 @@ public static String displayNameFor(@Nonnull Job, ?> job) {
}
@Extension
+ @Symbol("githubProjectProperty")
public static final class DescriptorImpl extends JobPropertyDescriptor {
/**
* Used to hide property configuration under checkbox,
@@ -114,7 +116,9 @@ public String getDisplayName() {
}
@Override
- public JobProperty> newInstance(StaplerRequest req, JSONObject formData) throws FormException {
+ public JobProperty> newInstance(@NonNull StaplerRequest2 req,
+ JSONObject formData) throws Descriptor.FormException {
+
GithubProjectProperty tpp = req.bindJSON(
GithubProjectProperty.class,
formData.getJSONObject(GITHUB_PROJECT_BLOCK_NAME)
@@ -135,5 +139,5 @@ public JobProperty> newInstance(StaplerRequest req, JSONObject formData) throw
}
- private static final Logger LOGGER = Logger.getLogger(GitHubPushTrigger.class.getName());
+ private static final Logger LOGGER = Logger.getLogger(GithubProjectProperty.class.getName());
}
diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java b/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java
index b331adcb3..50e9ad9ed 100644
--- a/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java
+++ b/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java
@@ -1,9 +1,9 @@
package com.coravy.hudson.plugins.github;
-import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.StringUtils;
/**
- * @author Stefan Saasen
+ * @author Stefan Saasen
*/
public final class GithubUrl {
diff --git a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java
index 2ab3aea20..4a45fbd2a 100644
--- a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java
+++ b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java
@@ -1,16 +1,20 @@
package org.jenkinsci.plugins.github;
import hudson.Plugin;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
import org.jenkinsci.plugins.github.config.GitHubPluginConfig;
import org.jenkinsci.plugins.github.migration.Migrator;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
* Main entry point for this plugin
- *
+ *
* Launches migration from old config versions
* Contains helper method to get global plugin configuration - {@link #configuration()}
*
@@ -19,23 +23,23 @@
public class GitHubPlugin extends Plugin {
/**
* Launched before plugin starts
- * Adds alias for {@link GitHubPlugin} to simplify resulting xml
+ * Adds alias for {@link GitHubPlugin} to simplify resulting xml.
*/
- public static void init() {
+ @Initializer(before = InitMilestone.SYSTEM_CONFIG_LOADED)
+ @Restricted(DoNotUse.class)
+ public static void addXStreamAliases() {
Migrator.enableCompatibilityAliases();
Migrator.enableAliases();
}
- @Override
- public void start() throws Exception {
- init();
- }
-
/**
- * Launches migration after plugin already initialized
+ * Launches migration after all extensions have been augmented as we need to ensure that the credentials plugin
+ * has been initialized.
+ * We need ensure that migrator will run after xstream aliases will be added.
+ * @see JENKINS-36446
*/
- @Override
- public void postInitialize() throws Exception {
+ @Initializer(after = InitMilestone.EXTENSIONS_AUGMENTED, before = InitMilestone.JOB_LOADED)
+ public static void runMigrator() throws Exception {
new Migrator().migrate();
}
@@ -44,7 +48,7 @@ public void postInitialize() throws Exception {
*
* @return configuration of plugin
*/
- @Nonnull
+ @NonNull
public static GitHubPluginConfig configuration() {
return defaultIfNull(
GitHubPluginConfig.all().get(GitHubPluginConfig.class),
diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java b/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java
index 80e76534f..52eeb6fef 100644
--- a/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java
+++ b/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java
@@ -3,7 +3,7 @@
import com.cloudbees.jenkins.GitHubRepositoryName;
import org.kohsuke.stapler.AnnotationHandler;
import org.kohsuke.stapler.InjectedParameter;
-import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerRequest2;
import org.slf4j.Logger;
import java.lang.annotation.Documented;
@@ -21,7 +21,7 @@
*
* @author lanwen (Merkushev Kirill)
* @see Web Method
- * @since TODO
+ * @since 1.17.0
*/
@Retention(RUNTIME)
@Target(PARAMETER)
@@ -37,8 +37,8 @@ class PayloadHandler extends AnnotationHandler {
* @return {@link GitHubRepositoryName} extracted from request or null on any problem
*/
@Override
- public GitHubRepositoryName parse(StaplerRequest req, GHRepoName a, Class type, String param) {
- String repo = notNull(req, "Why StaplerRequest is null?").getParameter(param);
+ public GitHubRepositoryName parse(StaplerRequest2 req, GHRepoName a, Class type, String param) {
+ String repo = notNull(req, "Why StaplerRequest2 is null?").getParameter(param);
LOGGER.trace("Repo url in method {}", repo);
return GitHubRepositoryName.create(repo);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java
new file mode 100644
index 000000000..794f3db04
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java
@@ -0,0 +1,216 @@
+package org.jenkinsci.plugins.github.admin;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Ticker;
+import com.google.common.annotations.VisibleForTesting;
+
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.model.AdministrativeMonitor;
+import hudson.model.Item;
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.github.Messages;
+import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
+import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.github.GHEvent;
+import org.kohsuke.stapler.HttpResponse;
+import org.kohsuke.stapler.WebMethod;
+import org.kohsuke.stapler.json.JsonHttpResponse;
+import org.kohsuke.stapler.verb.GET;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import net.sf.json.JSONObject;
+
+@SuppressWarnings("unused")
+@Extension
+public class GitHubDuplicateEventsMonitor extends AdministrativeMonitor {
+
+ @VisibleForTesting
+ static final String LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID = GitHubDuplicateEventsMonitor.class.getName()
+ + ".last-duplicate";
+
+ @Override
+ public String getDisplayName() {
+ return Messages.duplicate_events_administrative_monitor_displayname();
+ }
+
+ public String getDescription() {
+ return Messages.duplicate_events_administrative_monitor_description();
+ }
+
+ public String getBlurb() {
+ return Messages.duplicate_events_administrative_monitor_blurb(
+ LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID, this.getLastDuplicateUrl());
+ }
+
+ @VisibleForTesting
+ String getLastDuplicateUrl() {
+ return this.getUrl() + "/" + "last-duplicate.json";
+ }
+
+ @Override
+ public boolean isActivated() {
+ return ExtensionList.lookupSingleton(DuplicateEventsSubscriber.class).isDuplicateEventSeen();
+ }
+
+ @Override
+ public boolean hasRequiredPermission() {
+ return Jenkins.get().hasPermission(Jenkins.SYSTEM_READ);
+ }
+
+ @Override
+ public void checkRequiredPermission() {
+ Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
+ }
+
+ @GET
+ @WebMethod(name = "last-duplicate.json")
+ public HttpResponse doGetLastDuplicatePayload() {
+ Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
+ JSONObject data;
+ var lastDuplicate = ExtensionList.lookupSingleton(DuplicateEventsSubscriber.class).getLastDuplicate();
+ if (lastDuplicate != null) {
+ data = JSONObject.fromObject(lastDuplicate.ghSubscriberEvent().getPayload());
+ } else {
+ data = getLastDuplicateNoEventPayload();
+ }
+ return new JsonHttpResponse(data, 200);
+ }
+
+ @VisibleForTesting
+ static JSONObject getLastDuplicateNoEventPayload() {
+ return new JSONObject().accumulate("payload", "No duplicate events seen yet");
+ }
+
+ /**
+ * Tracks duplicate {@link GHEvent} triggering actions in Jenkins.
+ * Events are tracked for 10 minutes, with the last detected duplicate reference retained for up to 24 hours
+ * (see {@link #isDuplicateEventSeen}).
+ *
+ * Duplicates are stored in-memory only, so a controller restart clears all entries as if none existed.
+ * Persistent storage is omitted for simplicity, since webhook misconfigurations would likely cause new duplicates.
+ */
+ @Extension
+ public static final class DuplicateEventsSubscriber extends GHEventsSubscriber {
+
+ private static final Logger LOGGER = Logger.getLogger(DuplicateEventsSubscriber.class.getName());
+
+ private Ticker ticker = Ticker.systemTicker();
+ /**
+ * Caches GitHub event GUIDs for 10 minutes to track recent events to detect duplicates.
+ *
+ * Only the keys (event GUIDs) are relevant, as Caffeine automatically handles expiration based
+ * on insertion time; the value is irrelevant, we put {@link #DUMMY}, as Caffeine doesn't provide any
+ * Set structures.
+ *
+ * Maximum cache size is set to 24k so it doesn't grow unbound (approx. 1MB). Each key takes 36 bytes, and
+ * timestamp (assuming caffeine internally keeps long) takes 8 bytes; total of 44 bytes
+ * per entry. So the maximum memory consumed by this cache is 24k * 44 = 1056k = 1.056 MB.
+ */
+ private final Cache eventTracker = Caffeine.newBuilder()
+ .maximumSize(24_000L)
+ .expireAfterWrite(Duration.ofMinutes(10))
+ .ticker(() -> ticker.read())
+ .build();
+ private static final Object DUMMY = new Object();
+
+ private volatile TrackedDuplicateEvent lastDuplicate;
+ public record TrackedDuplicateEvent(
+ String eventGuid, Instant lastUpdated, GHSubscriberEvent ghSubscriberEvent) { }
+ private static final Duration TWENTY_FOUR_HOURS = Duration.ofHours(24);
+
+ @VisibleForTesting
+ @Restricted(NoExternalUse.class)
+ void setTicker(Ticker testTicker) {
+ ticker = testTicker;
+ }
+
+ /**
+ * This subscriber is not applicable to any item
+ *
+ * @param item ignored
+ * @return always false
+ */
+ @Override
+ protected boolean isApplicable(@Nullable Item item) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Subscribes to events that trigger actions in Jenkins, such as repository scans or builds.
+ *
+ * The {@link GHEvent} enum defines about 63 events, but not all are relevant to Jenkins.
+ * Tracking unnecessary events increases memory usage, and they occur more frequently than those triggering any
+ * work.
+ *
+ *
+ * Documentation reference (also referenced in {@link GHEvent})
+ */
+ @Override
+ protected Set events() {
+ return Set.of(
+ GHEvent.CHECK_RUN, // associated with GitHub action Re-run button to trigger build
+ GHEvent.CHECK_SUITE, // associated with GitHub action Re-run button to trigger build
+ GHEvent.CREATE, // branch or tag creation
+ GHEvent.DELETE, // branch or tag deletion
+ GHEvent.PULL_REQUEST, // PR creation (also PR close or merge)
+ GHEvent.PUSH // commit push
+ );
+ }
+
+ @Override
+ protected void onEvent(final GHSubscriberEvent event) {
+ String eventGuid = event.getEventGuid();
+ LOGGER.fine(() -> "Received event with GUID: " + eventGuid);
+ if (eventGuid == null) {
+ return;
+ }
+ if (eventTracker.getIfPresent(eventGuid) != null) {
+ lastDuplicate = new TrackedDuplicateEvent(eventGuid, getNow(), event);
+ }
+ eventTracker.put(eventGuid, DUMMY);
+ }
+
+ /**
+ * Checks if a duplicate event was recorded in the past 24 hours.
+ *
+ * Events are not stored for 24 hours—only the most recent duplicate is checked within this timeframe.
+ *
+ * @return {@code true} if a duplicate was seen in the last 24 hours, {@code false} otherwise.
+ */
+ public boolean isDuplicateEventSeen() {
+ return lastDuplicate != null
+ && Duration.between(lastDuplicate.lastUpdated(), getNow()).compareTo(TWENTY_FOUR_HOURS) < 0;
+ }
+
+ private Instant getNow() {
+ return Instant.ofEpochSecond(0L, ticker.read());
+ }
+
+ public TrackedDuplicateEvent getLastDuplicate() {
+ return lastDuplicate;
+ }
+
+ /**
+ * Caffeine expired keys are not removed immediately. Method returns the non-expired keys;
+ * required for the tests.
+ */
+ @VisibleForTesting
+ @Restricted(NoExternalUse.class)
+ Set getPresentEventKeys() {
+ return eventTracker.asMap().keySet().stream()
+ .filter(key -> eventTracker.getIfPresent(key) != null)
+ .collect(Collectors.toSet());
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java
index e35e72524..33dad11a9 100644
--- a/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java
+++ b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java
@@ -15,13 +15,13 @@
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
-import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
-import javax.inject.Inject;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import jakarta.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.List;
@@ -32,7 +32,7 @@
/**
* Administrative monitor to track problems of registering/removing hooks for GH.
- * Holds non-savable map of repo->message and persisted list of ignored projects.
+ * Holds non-savable map of repo->message and persisted list of ignored projects.
* Anyone can register new problem with {@link #registerProblem(GitHubRepositoryName, Throwable)} and check
* repo for problems with {@link #isProblemWith(GitHubRepositoryName)}
*
@@ -40,7 +40,7 @@
* is visible if any problem or ignored repo is registered
*
* @author lanwen (Merkushev Kirill)
- * @since TODO
+ * @since 1.17.0
*/
@Extension
public class GitHubHookRegisterProblemMonitor extends AdministrativeMonitor implements Saveable {
@@ -64,7 +64,7 @@ public GitHubHookRegisterProblemMonitor() {
}
/**
- * @return Immutable copy of map with repo->problem message content
+ * @return Immutable copy of map with repo->problem message content
*/
public Map getProblems() {
return ImmutableMap.copyOf(problems);
@@ -143,11 +143,11 @@ public boolean isActivated() {
}
/**
- * Depending on whether the user said "yes" or "no", send him to the right place.
+ * Depending on whether the user said "yes" or "no", send them to the right place.
*/
@RequirePOST
@RequireAdminRights
- public HttpResponse doAct(StaplerRequest req) throws IOException {
+ public HttpResponse doAct(StaplerRequest2 req) throws IOException {
if (req.hasParameter("no")) {
disable(true);
return HttpResponses.redirectViaContextPath("/manage");
@@ -166,7 +166,7 @@ public HttpResponse doAct(StaplerRequest req) throws IOException {
@ValidateRepoName
@RequireAdminRights
@RespondWithRedirect
- public void doIgnore(@Nonnull @GHRepoName GitHubRepositoryName repo) {
+ public void doIgnore(@NonNull @GHRepoName GitHubRepositoryName repo) {
if (!ignored.contains(repo)) {
ignored.add(repo);
}
@@ -183,7 +183,7 @@ public void doIgnore(@Nonnull @GHRepoName GitHubRepositoryName repo) {
@ValidateRepoName
@RequireAdminRights
@RespondWithRedirect
- public void doDisignore(@Nonnull @GHRepoName GitHubRepositoryName repo) {
+ public void doDisignore(@NonNull @GHRepoName GitHubRepositoryName repo) {
ignored.remove(repo);
}
@@ -236,7 +236,7 @@ public static class GitHubHookRegisterProblemManagementLink extends ManagementLi
public String getIconFileName() {
return monitor.getProblems().isEmpty() && monitor.ignored.isEmpty()
? null
- : "/plugin/github/img/logo.svg";
+ : "symbol-logo-github plugin-github";
}
@Override
@@ -253,5 +253,11 @@ public String getDescription() {
public String getDisplayName() {
return Messages.hooks_problem_administrative_monitor_displayname();
}
+
+ // TODO: Override `getCategory` instead using `Category.TROUBLESHOOTING` when minimum core version is 2.226+,
+ // TODO: see https://github.com/jenkinsci/jenkins/commit/6de7e5fc7f6fb2e2e4cb342461788f97e3dfd8f6.
+ protected String getCategoryName() {
+ return "TROUBLESHOOTING";
+ }
}
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java b/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java
index e1f7f01cb..953a2fae0 100644
--- a/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java
+++ b/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java
@@ -1,11 +1,12 @@
package org.jenkinsci.plugins.github.admin;
import jenkins.model.Jenkins;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.interceptor.Interceptor;
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import jakarta.servlet.ServletException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
@@ -28,8 +29,8 @@
class Processor extends Interceptor {
@Override
- public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments)
- throws IllegalAccessException, InvocationTargetException {
+ public Object invoke(StaplerRequest2 request, StaplerResponse2 response, Object instance, Object[] arguments)
+ throws IllegalAccessException, InvocationTargetException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
return target.invoke(request, response, instance, arguments);
diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java b/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java
index 70dc5b7ba..f0be54946 100644
--- a/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java
+++ b/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java
@@ -1,11 +1,12 @@
package org.jenkinsci.plugins.github.admin;
import org.kohsuke.stapler.HttpRedirect;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.interceptor.Interceptor;
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import jakarta.servlet.ServletException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
@@ -29,8 +30,8 @@
class Processor extends Interceptor {
@Override
- public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments)
- throws IllegalAccessException, InvocationTargetException {
+ public Object invoke(StaplerRequest2 request, StaplerResponse2 response, Object instance, Object[] arguments)
+ throws IllegalAccessException, InvocationTargetException, ServletException {
target.invoke(request, response, instance, arguments);
throw new InvocationTargetException(new HttpRedirect("."));
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java b/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java
index e68a44700..b4977e418 100644
--- a/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java
+++ b/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java
@@ -1,11 +1,12 @@
package org.jenkinsci.plugins.github.admin;
import com.cloudbees.jenkins.GitHubRepositoryName;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.interceptor.Interceptor;
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import jakarta.servlet.ServletException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
@@ -15,7 +16,7 @@
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
import static org.kohsuke.stapler.HttpResponses.errorWithoutStack;
@@ -33,8 +34,8 @@
class Processor extends Interceptor {
@Override
- public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments)
- throws IllegalAccessException, InvocationTargetException {
+ public Object invoke(StaplerRequest2 request, StaplerResponse2 response, Object instance, Object[] arguments)
+ throws IllegalAccessException, InvocationTargetException, ServletException {
if (!from(newArrayList(arguments)).firstMatch(instanceOf(GitHubRepositoryName.class)).isPresent()) {
throw new InvocationTargetException(
diff --git a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java
index 71fec736e..b155a57c3 100644
--- a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java
+++ b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java
@@ -5,7 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
@@ -53,7 +53,7 @@ public CombineErrorHandler withHandlers(List extends ErrorHandler> handlers) {
* @return true if exception handled or rethrows it
*/
@Override
- public boolean handle(Exception e, @Nonnull Run, ?> run, @Nonnull TaskListener listener) {
+ public boolean handle(Exception e, @NonNull Run, ?> run, @NonNull TaskListener listener) {
LOG.debug("Exception in {} will be processed with {} handlers",
run.getParent().getName(), handlers.size(), e);
try {
diff --git a/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java
index 65c4104f1..235caa1db 100644
--- a/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java
+++ b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java
@@ -3,7 +3,7 @@
import hudson.model.Run;
import hudson.model.TaskListener;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
/**
* So you can implement bunch of {@link ErrorHandler}s and log, rethrow, ignore exception.
@@ -26,5 +26,5 @@ public interface ErrorHandler {
* @return true if exception handled successfully
* @throws Exception you can rethrow exception of any type
*/
- boolean handle(Exception e, @Nonnull Run, ?> run, @Nonnull TaskListener listener) throws Exception;
+ boolean handle(Exception e, @NonNull Run, ?> run, @NonNull TaskListener listener) throws Exception;
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java
index 90ccae4ba..44ee71060 100644
--- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java
+++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java
@@ -1,12 +1,17 @@
package org.jenkinsci.plugins.github.config;
import com.cloudbees.jenkins.GitHubWebHook;
+import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.BulkChange;
import hudson.Extension;
+import hudson.Util;
import hudson.XmlFile;
import hudson.model.Descriptor;
-import hudson.model.Job;
+import hudson.model.Item;
+import hudson.security.Permission;
import hudson.util.FormValidation;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
@@ -17,13 +22,17 @@
import org.jenkinsci.plugins.github.Messages;
import org.jenkinsci.plugins.github.internal.GHPluginConfigException;
import org.jenkinsci.plugins.github.migration.Migrator;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.github.GitHub;
+import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
-import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.interceptor.RequirePOST;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.inject.Inject;
+import jakarta.inject.Inject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
@@ -32,9 +41,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import static com.google.common.base.Charsets.UTF_8;
import static java.lang.String.format;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub;
@@ -57,12 +68,13 @@ public class GitHubPluginConfig extends GlobalConfiguration {
* Helps to avoid null in {@link GitHubPlugin#configuration()}
*/
public static final GitHubPluginConfig EMPTY_CONFIG =
- new GitHubPluginConfig(Collections.emptyList());
+ new GitHubPluginConfig(Collections.emptyList());
- private List configs = new ArrayList();
+ private List configs = new ArrayList<>();
private URL hookUrl;
-
- private transient boolean overrideHookUrl;
+ @Deprecated
+ private transient HookSecretConfig hookSecretConfig;
+ private List hookSecretConfigs;
/**
* Used to get current instance identity.
@@ -73,6 +85,7 @@ public class GitHubPluginConfig extends GlobalConfiguration {
private transient InstanceIdentity identity;
public GitHubPluginConfig() {
+ getConfigFile().getXStream().alias("github-server-config", GitHubServerConfig.class);
load();
}
@@ -80,7 +93,18 @@ public GitHubPluginConfig(List configs) {
this.configs = configs;
}
+ private Object readResolve() {
+ if (hookSecretConfig != null) {
+ if (Util.fixEmpty(hookSecretConfig.getCredentialsId()) != null) {
+ setHookSecretConfig(hookSecretConfig);
+ }
+ hookSecretConfig = null;
+ }
+ return this;
+ }
+
@SuppressWarnings("unused")
+ @DataBoundSetter
public void setConfigs(List configs) {
this.configs = configs;
}
@@ -93,16 +117,18 @@ public boolean isManageHooks() {
return from(getConfigs()).filter(allowedToManageHooks()).first().isPresent();
}
- public void setHookUrl(URL hookUrl) {
- if (overrideHookUrl) {
- this.hookUrl = hookUrl;
- } else {
+ @DataBoundSetter
+ public void setHookUrl(String hookUrl) {
+ if (isEmpty(hookUrl)) {
this.hookUrl = null;
+ } else {
+ this.hookUrl = parseHookUrl(hookUrl);
}
}
+ @DataBoundSetter
+ @Deprecated
public void setOverrideHookUrl(boolean overrideHookUrl) {
- this.overrideHookUrl = overrideHookUrl;
}
/**
@@ -117,19 +143,30 @@ public URL getHookUrl() throws GHPluginConfigException {
}
}
- public boolean isOverrideHookURL() {
+ @SuppressWarnings("unused")
+ public boolean isOverrideHookUrl() {
return hookUrl != null;
}
+ @Deprecated
+ public boolean isOverrideHookURL() {
+ return isOverrideHookUrl();
+ }
+
/**
* Filters all stored configs against given predicate then
* logs in as the given user and returns the non null connection objects
*/
public Iterable findGithubConfig(Predicate match) {
+ Function loginFunction = loginToGithub();
+ if (Objects.isNull(loginFunction)) {
+ return Collections.emptyList();
+ }
+
// try all the credentials since we don't know which one would work
return from(getConfigs())
.filter(match)
- .transform(loginToGithub())
+ .transform(loginFunction)
.filter(Predicates.notNull());
}
@@ -155,17 +192,36 @@ protected XmlFile getConfigFile() {
}
@Override
- public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
+ public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException {
try {
- req.bindJSON(this, json);
+ BulkChange bc = new BulkChange(this);
+ try {
+ if (json.has("configs")) {
+ setConfigs(req.bindJSONToList(GitHubServerConfig.class, json.get("configs")));
+ } else {
+ setConfigs(Collections.emptyList());
+ }
+ if (json.has("hookSecretConfigs")) {
+ setHookSecretConfigs(req.bindJSONToList(HookSecretConfig.class, json.get("hookSecretConfigs")));
+ } else {
+ setHookSecretConfigs(Collections.emptyList());
+ }
+ if (json.optBoolean("isOverrideHookUrl", false) && (json.has("hookUrl"))) {
+ setHookUrl(json.optString("hookUrl"));
+ } else {
+ setHookUrl(null);
+ }
+ req.bindJSON(this, json);
+ clearRedundantCaches(configs);
+ } finally {
+ bc.commit();
+ }
} catch (Exception e) {
LOGGER.debug("Problem while submitting form for GitHub Plugin ({})", e.getMessage(), e);
LOGGER.trace("GH form data: {}", json.toString());
throw new FormException(
format("Malformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration");
}
- save();
- clearRedundantCaches(configs);
return true;
}
@@ -175,19 +231,24 @@ public String getDisplayName() {
}
@SuppressWarnings("unused")
+ @RequirePOST
public FormValidation doReRegister() {
+ Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE);
if (!GitHubPlugin.configuration().isManageHooks()) {
- return FormValidation.warning("Works only when Jenkins manages hooks (one ore more creds specified)");
+ return FormValidation.warning("Works only when Jenkins manages hooks (one or more creds specified)");
}
- List registered = GitHubWebHook.get().reRegisterAllHooks();
+ List registered = GitHubWebHook.get().reRegisterAllHooks();
- LOGGER.info("Called registerHooks() for {} jobs", registered.size());
- return FormValidation.ok("Called re-register hooks for %s jobs", registered.size());
+ LOGGER.info("Called registerHooks() for {} items", registered.size());
+ return FormValidation.ok("Called re-register hooks for %s items", registered.size());
}
+ @RequirePOST
+ @Restricted(DoNotUse.class) // WebOnly
@SuppressWarnings("unused")
public FormValidation doCheckHookUrl(@QueryParameter String value) {
+ Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE);
try {
HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection();
con.setRequestMethod("POST");
@@ -198,8 +259,8 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) {
}
String v = con.getHeaderField(GitHubWebHook.X_INSTANCE_IDENTITY);
if (v == null) {
- // people might be running clever apps that's not Jenkins, and that's OK
- return FormValidation.warning("It doesn't look like %s is talking to any Jenkins. "
+ // people might be running clever apps that aren't Jenkins, and that's OK
+ return FormValidation.warning("It doesn't look like %s is talking to Jenkins. "
+ "Are you running your own app?", value);
}
RSAPublicKey key = identity.getPublic();
@@ -211,7 +272,7 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) {
return FormValidation.ok();
} catch (IOException e) {
- return FormValidation.error(e, "Failed to test a connection to %s", value);
+ return FormValidation.error(e, "Connection test for %s failed", value);
}
}
@@ -244,4 +305,43 @@ private static void validateConfig(boolean state, String message) {
throw new GHPluginConfigException(message);
}
}
+
+ @Deprecated
+ public HookSecretConfig getHookSecretConfig() {
+ return hookSecretConfigs != null && !hookSecretConfigs.isEmpty()
+ ? hookSecretConfigs.get(0)
+ : new HookSecretConfig(null);
+ }
+
+ @Deprecated
+ public void setHookSecretConfig(HookSecretConfig hookSecretConfig) {
+ setHookSecretConfigs(hookSecretConfig.getCredentialsId() != null
+ ? Collections.singletonList(hookSecretConfig)
+ : null);
+ }
+
+ public List getHookSecretConfigs() {
+ return hookSecretConfigs != null
+ ? Collections.unmodifiableList(new ArrayList<>(hookSecretConfigs))
+ : Collections.emptyList();
+ }
+
+ @DataBoundSetter
+ public void setHookSecretConfigs(List hookSecretConfigs) {
+ this.hookSecretConfigs = hookSecretConfigs != null ? new ArrayList<>(hookSecretConfigs) : null;
+ }
+
+ private URL parseHookUrl(String hookUrl) {
+ try {
+ return new URL(hookUrl);
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ }
+
+ @NonNull
+ @Override
+ public Permission getRequiredGlobalConfigPagePermission() {
+ return Jenkins.MANAGE;
+ }
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java
index 73dc50ce9..9cb92a5d5 100644
--- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java
+++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java
@@ -1,41 +1,43 @@
package org.jenkinsci.plugins.github.config;
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
-import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.google.common.base.Function;
+import com.google.common.base.Optional;
import com.google.common.base.Predicate;
-import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.google.common.base.Supplier;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
+import hudson.Util;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.security.ACL;
+import hudson.security.Permission;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
import jenkins.model.Jenkins;
+import jenkins.scm.api.SCMName;
+import org.apache.commons.lang3.StringUtils;
import org.jenkinsci.plugins.github.internal.GitHubLoginFunction;
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.jenkinsci.plugins.github.util.misc.NullSafePredicate;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
-import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.github.GitHub;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.interceptor.RequirePOST;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nonnull;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collections;
-
-import static com.cloudbees.plugins.credentials.CredentialsMatchers.firstOrDefault;
-import static com.cloudbees.plugins.credentials.CredentialsMatchers.withId;
-import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials;
+import static com.cloudbees.plugins.credentials.CredentialsProvider.findCredentialByIdInItemGroup;
import static com.cloudbees.plugins.credentials.domains.URIRequirementBuilder.fromUri;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
@@ -49,20 +51,37 @@
* @author lanwen (Merkushev Kirill)
* @since 1.13.0
*/
-@XStreamAlias("github-server-config")
public class GitHubServerConfig extends AbstractDescribableImpl {
private static final Logger LOGGER = LoggerFactory.getLogger(GitHubServerConfig.class);
+ /**
+ * Common prefixes that we should remove when inferring a {@link #name}.
+ *
+ * @since 1.28.0
+ */
+ private static final String[] COMMON_PREFIX_HOSTNAMES = {
+ "git.",
+ "github.",
+ "vcs.",
+ "scm.",
+ "source."
+ };
/**
* Because of {@link GitHub} hide this const from external use we need to store it here
*/
public static final String GITHUB_URL = "https://api.github.com";
+ /**
+ * The name to display for the public GitHub service.
+ *
+ * @since 1.28.0
+ */
+ private static final String PUBLIC_GITHUB_NAME = "GitHub";
+
/**
* Used as default token value if no any creds found by given credsId.
*/
private static final String UNKNOWN_TOKEN = "UNKNOWN_TOKEN";
-
/**
* Default value in MB for client cache size
*
@@ -70,6 +89,11 @@ public class GitHubServerConfig extends AbstractDescribableImpl loginToGithub() {
}
/**
- * Tries to find {@link StringCredentials} by id and returns token from it.
- * Returns {@link #UNKNOWN_TOKEN} if no any creds found with this id.
+ * Extracts token from secret found by {@link #secretFor(String)}
+ * Returns {@link #UNKNOWN_TOKEN} if no any creds secret found with this id.
*
* @param credentialsId id to find creds
*
* @return token from creds or default non empty string
*/
- @Nonnull
+ @NonNull
public static String tokenFor(String credentialsId) {
- StringCredentialsImpl unkn = new StringCredentialsImpl(null, null, null, Secret.fromString(UNKNOWN_TOKEN));
- return firstOrDefault(
- lookupCredentials(StringCredentials.class,
- Jenkins.getInstance(), ACL.SYSTEM,
- Collections.emptyList()),
- withId(credentialsId), unkn).getSecret().getPlainText();
+ return secretFor(credentialsId).or(new Supplier() {
+ @Override
+ public Secret get() {
+ return Secret.fromString(UNKNOWN_TOKEN);
+ }
+ }).getPlainText();
+ }
+
+ /**
+ * Tries to find {@link StringCredentials} by id and returns secret from it.
+ *
+ * @param credentialsId id to find creds
+ *
+ * @return secret from creds or empty optional
+ */
+ @NonNull
+ public static Optional secretFor(String credentialsId) {
+ if (credentialsId == null) {
+ return Optional.absent();
+ }
+ var creds = findCredentialByIdInItemGroup(credentialsId, StringCredentials.class, null, null, null);
+ if (creds == null) {
+ return Optional.absent();
+ }
+ return Optional.of(creds.getSecret());
}
/**
@@ -224,7 +305,7 @@ public static String tokenFor(String credentialsId) {
public static Predicate withHost(final String host) {
return new NullSafePredicate() {
@Override
- protected boolean applyNullSafe(@Nonnull GitHubServerConfig github) {
+ protected boolean applyNullSafe(@NonNull GitHubServerConfig github) {
return defaultIfEmpty(github.getApiUrl(), GITHUB_URL).contains(host);
}
};
@@ -252,24 +333,35 @@ public String getDisplayName() {
return "GitHub Server";
}
+ @NonNull
+ @Override
+ public Permission getRequiredGlobalConfigPagePermission() {
+ return Jenkins.MANAGE;
+ }
+
@SuppressWarnings("unused")
- public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl) {
- if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
- return new ListBoxModel();
+ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl,
+ @QueryParameter String credentialsId) {
+ if (!Jenkins.getInstance().hasPermission(Jenkins.MANAGE)) {
+ return new StandardListBoxModel().includeCurrentValue(credentialsId);
}
return new StandardListBoxModel()
- .withEmptySelection()
- .withAll(lookupCredentials(
- StringCredentials.class,
+ .includeEmptyValue()
+ .includeMatchingAs(ACL.SYSTEM,
Jenkins.getInstance(),
- ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build())
+ StringCredentials.class,
+ fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(),
+ CredentialsMatchers.always()
);
}
+ @RequirePOST
+ @Restricted(DoNotUse.class) // WebOnly
@SuppressWarnings("unused")
public FormValidation doVerifyCredentials(
@QueryParameter String apiUrl,
@QueryParameter String credentialsId) throws IOException {
+ Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE);
GitHubServerConfig config = new GitHubServerConfig(credentialsId);
config.setApiUrl(apiUrl);
@@ -314,11 +406,13 @@ public FormValidation doCheckApiUrl(@QueryParameter String value) {
*/
private static class ClientCacheFunction extends NullSafeFunction {
@Override
- protected GitHub applyNullSafe(@Nonnull GitHubServerConfig github) {
+ protected GitHub applyNullSafe(@NonNull GitHubServerConfig github) {
if (github.getCachedClient() == null) {
github.setCachedClient(new GitHubLoginFunction().apply(github));
}
return github.getCachedClient();
}
}
+
+
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java
index a3e957475..38cbb73ed 100644
--- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java
+++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java
@@ -1,5 +1,6 @@
package org.jenkinsci.plugins.github.config;
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
@@ -23,14 +24,16 @@
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.interceptor.RequirePOST;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.IOException;
import java.net.URI;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.firstOrNull;
@@ -41,7 +44,6 @@
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isEmpty;
-import static org.apache.commons.lang3.Validate.notNull;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL;
import static org.kohsuke.github.GHAuthorization.AMIN_HOOK;
import static org.kohsuke.github.GHAuthorization.REPO;
@@ -89,24 +91,34 @@ public String getDisplayName() {
}
@SuppressWarnings("unused")
- public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl) {
- if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
- return new ListBoxModel();
+ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl, @QueryParameter String credentialsId) {
+ if (!Jenkins.getInstance().hasPermission(Jenkins.MANAGE)) {
+ return new StandardUsernameListBoxModel().includeCurrentValue(credentialsId);
}
return new StandardUsernameListBoxModel()
- .withEmptySelection()
- .withAll(lookupCredentials(
- StandardUsernamePasswordCredentials.class,
- Jenkins.getInstance(),
- ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build())
+ .includeEmptyValue()
+ .includeMatchingAs(
+ ACL.SYSTEM,
+ Jenkins.getInstance(),
+ StandardUsernamePasswordCredentials.class,
+ fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(),
+ CredentialsMatchers.always()
+ )
+ .includeMatchingAs(
+ Jenkins.getAuthentication(),
+ Jenkins.getInstance(),
+ StandardUsernamePasswordCredentials.class,
+ fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(),
+ CredentialsMatchers.always()
);
}
@SuppressWarnings("unused")
+ @RequirePOST
public FormValidation doCreateTokenByCredentials(
@QueryParameter String apiUrl,
@QueryParameter String credentialsId) {
-
+ Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE);
if (isEmpty(credentialsId)) {
return FormValidation.error("Please specify credentials to create token");
}
@@ -114,15 +126,29 @@ public FormValidation doCreateTokenByCredentials(
StandardUsernamePasswordCredentials creds = firstOrNull(lookupCredentials(
StandardUsernamePasswordCredentials.class,
Jenkins.getInstance(),
- ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()),
+ ACL.SYSTEM,
+ fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()),
withId(credentialsId));
+ if (creds == null) {
+ // perhaps they selected a personal credential for conversion
+ creds = firstOrNull(lookupCredentials(
+ StandardUsernamePasswordCredentials.class,
+ Jenkins.getInstance(),
+ Jenkins.getAuthentication(),
+ fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()),
+ withId(credentialsId));
+ }
GHAuthorization token;
+ if (Objects.isNull(creds)) {
+ return FormValidation.error("Can't create GH token - credentials are null.");
+ }
+
try {
token = createToken(
- notNull(creds, "Why selected creds is null?").getUsername(),
- creds.getPassword().getPlainText(),
+ creds.getUsername(),
+ Secret.toString(creds.getPassword()),
defaultIfBlank(apiUrl, GITHUB_URL)
);
} catch (IOException e) {
@@ -136,11 +162,12 @@ ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()),
}
@SuppressWarnings("unused")
+ @RequirePOST
public FormValidation doCreateTokenByPassword(
@QueryParameter String apiUrl,
@QueryParameter String login,
@QueryParameter String password) {
-
+ Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE);
try {
GHAuthorization token = createToken(login, password, defaultIfBlank(apiUrl, GITHUB_URL));
StandardCredentials credentials = createCredentials(apiUrl, token.getToken(), login);
@@ -163,8 +190,8 @@ public FormValidation doCreateTokenByPassword(
* @return personal token with requested scope
* @throws IOException when can't create token with given creds
*/
- public GHAuthorization createToken(@Nonnull String username,
- @Nonnull String password,
+ public GHAuthorization createToken(@NonNull String username,
+ @NonNull String password,
@Nullable String apiUrl) throws IOException {
GitHub gitHub = new GitHubBuilder()
.withEndpoint(defaultIfBlank(apiUrl, GITHUB_URL))
@@ -209,7 +236,7 @@ public StandardCredentials createCredentials(@Nullable String serverAPIUrl, Stri
*
* @return saved creds
*/
- private StandardCredentials createCredentials(@Nonnull String serverAPIUrl,
+ private StandardCredentials createCredentials(@NonNull String serverAPIUrl,
final StandardCredentials credentials) {
URI serverUri = URI.create(defaultIfBlank(serverAPIUrl, GITHUB_URL));
diff --git a/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java
new file mode 100644
index 000000000..9db733af7
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java
@@ -0,0 +1,155 @@
+package org.jenkinsci.plugins.github.config;
+
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
+import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
+import com.cloudbees.plugins.credentials.domains.DomainRequirement;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Descriptor;
+import hudson.security.ACL;
+import hudson.security.Permission;
+import hudson.util.ListBoxModel;
+import hudson.util.Secret;
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.github.webhook.SignatureAlgorithm;
+import org.jenkinsci.plugins.plaincredentials.StringCredentials;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Collections;
+import org.kohsuke.stapler.QueryParameter;
+
+/**
+ * Manages storing/retrieval of the shared secret for the hook.
+ */
+public class HookSecretConfig extends AbstractDescribableImpl {
+
+ private String credentialsId;
+ private SignatureAlgorithm signatureAlgorithm;
+
+ @DataBoundConstructor
+ public HookSecretConfig(String credentialsId, String signatureAlgorithm) {
+ this.credentialsId = credentialsId;
+ this.signatureAlgorithm = parseSignatureAlgorithm(signatureAlgorithm);
+ }
+
+ /**
+ * Legacy constructor for backwards compatibility.
+ */
+ public HookSecretConfig(String credentialsId) {
+ this(credentialsId, null);
+ }
+
+ /**
+ * Gets the currently used secret being used for payload verification.
+ *
+ * @return Current secret, null if not set.
+ */
+ @Nullable
+ public Secret getHookSecret() {
+ return GitHubServerConfig.secretFor(credentialsId).orNull();
+ }
+
+ public String getCredentialsId() {
+ return credentialsId;
+ }
+
+ /**
+ * Gets the signature algorithm to use for webhook validation.
+ *
+ * @return the configured signature algorithm, defaults to SHA-256
+ * @since 1.45.0
+ */
+ public SignatureAlgorithm getSignatureAlgorithm() {
+ return signatureAlgorithm != null ? signatureAlgorithm : SignatureAlgorithm.getDefault();
+ }
+
+ /**
+ * Gets the signature algorithm name for UI binding.
+ *
+ * @return the algorithm name as string (e.g., "SHA256", "SHA1")
+ * @since 1.45.0
+ */
+ public String getSignatureAlgorithmName() {
+ return getSignatureAlgorithm().name();
+ }
+
+ /**
+ * @param credentialsId a new ID
+ * @deprecated rather treat this field as final and use {@link GitHubPluginConfig#setHookSecretConfigs}
+ */
+ @Deprecated
+ public void setCredentialsId(String credentialsId) {
+ this.credentialsId = credentialsId;
+ }
+
+ /**
+ * Ensures backwards compatibility during deserialization.
+ * Sets default algorithm to SHA-256 for existing configurations.
+ */
+ private Object readResolve() {
+ if (signatureAlgorithm == null) {
+ signatureAlgorithm = SignatureAlgorithm.getDefault();
+ }
+ return this;
+ }
+
+ /**
+ * Parses signature algorithm from UI string input.
+ */
+ private SignatureAlgorithm parseSignatureAlgorithm(String algorithmName) {
+ if (algorithmName == null || algorithmName.trim().isEmpty()) {
+ return SignatureAlgorithm.getDefault();
+ }
+
+ try {
+ return SignatureAlgorithm.valueOf(algorithmName.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // Default to SHA-256 for invalid input
+ return SignatureAlgorithm.getDefault();
+ }
+ }
+
+ @Extension
+ public static class DescriptorImpl extends Descriptor {
+
+ @Override
+ public String getDisplayName() {
+ return "Hook secret configuration";
+ }
+
+ /**
+ * Provides dropdown items for signature algorithm selection.
+ */
+ public ListBoxModel doFillSignatureAlgorithmItems() {
+ ListBoxModel items = new ListBoxModel();
+ items.add("SHA-256 (Recommended)", "SHA256");
+ items.add("SHA-1 (Legacy)", "SHA1");
+ return items;
+ }
+
+ @SuppressWarnings("unused")
+ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String credentialsId) {
+ if (!Jenkins.getInstance().hasPermission(Jenkins.MANAGE)) {
+ return new StandardListBoxModel().includeCurrentValue(credentialsId);
+ }
+
+ return new StandardListBoxModel()
+ .includeEmptyValue()
+ .includeMatchingAs(
+ ACL.SYSTEM,
+ Jenkins.getInstance(),
+ StringCredentials.class,
+ Collections.emptyList(),
+ CredentialsMatchers.always()
+ );
+ }
+
+ @NonNull
+ @Override
+ public Permission getRequiredGlobalConfigPagePermission() {
+ return Jenkins.MANAGE;
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java
index bdef0e98c..155d8c826 100644
--- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java
@@ -4,16 +4,23 @@
import com.google.common.base.Predicate;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
+import hudson.model.Item;
import hudson.model.Job;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
import jenkins.model.Jenkins;
+import jenkins.scm.api.SCMEvent;
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.jenkinsci.plugins.github.util.misc.NullSafePredicate;
import org.kohsuke.github.GHEvent;
+import org.kohsuke.stapler.Stapler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
import java.util.Collections;
import java.util.Set;
@@ -32,6 +39,8 @@
*/
public abstract class GHEventsSubscriber implements ExtensionPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(GHEventsSubscriber.class);
+ @CheckForNull
+ private transient Boolean hasIsApplicableItem;
/**
* Should return true only if this subscriber interested in {@link #events()} set for this project
@@ -39,9 +48,63 @@ public abstract class GHEventsSubscriber implements ExtensionPoint {
*
* @param project to check
*
- * @return true to provide events to register and subscribe for this project
+ * @return {@code true} to provide events to register and subscribe for this project
+ * @deprecated override {@link #isApplicable(Item)} instead.
+ */
+ @Deprecated
+ protected boolean isApplicable(@Nullable Job, ?> project) {
+ if (checkIsApplicableItem()) {
+ return isApplicable((Item) project);
+ }
+ // a legacy implementation which should not have been calling super.isApplicable(Job)
+ throw new AbstractMethodError("you must override the new overload of isApplicable");
+ }
+
+ /**
+ * Should return true only if this subscriber interested in {@link #events()} set for this project
+ * Don't call it directly, use {@link #isApplicableFor} static function
+ *
+ * @param item to check
+ *
+ * @return {@code true} to provide events to register and subscribe for this item
+ * @since 1.25.0
+ */
+ protected abstract boolean isApplicable(@Nullable Item item);
+
+ /**
+ * Call {@link #isApplicable(Item)} with safety for calling to legacy implementations before the abstract method
+ * was switched from {@link #isApplicable(Job)}.
+ * @param item to check.
+ * @return {@code true} to provide events to register and subscribe for this item
*/
- protected abstract boolean isApplicable(@Nullable Job, ?> project);
+ @SuppressWarnings("deprecation")
+ private boolean safeIsApplicable(@Nullable Item item) {
+ return checkIsApplicableItem() ? isApplicable(item) : item instanceof Job && isApplicable((Job, ?>) item);
+ }
+
+ private boolean checkIsApplicableItem() {
+ if (hasIsApplicableItem == null) {
+ boolean implemented = false;
+ // cannot use Util.isOverridden because method is protected and isOverridden only checks public methods
+ Class> clazz = getClass();
+ while (clazz != null && clazz != GHEventsSubscriber.class) {
+ try {
+ Method isApplicable = clazz.getDeclaredMethod("isApplicable", Item.class);
+ if (isApplicable.getDeclaringClass() != GHEventsSubscriber.class) {
+ // ok this is the first method we have found that could be an override
+ // if somebody overrode an inherited method with and `abstract` then we don't have the method
+ implemented = !Modifier.isAbstract(isApplicable.getModifiers());
+ break;
+ }
+ } catch (NoSuchMethodException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ // idempotent so no need for synchronization
+ this.hasIsApplicableItem = implemented;
+ }
+ return hasIsApplicableItem;
+ }
/**
* Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)}
@@ -53,17 +116,32 @@ public abstract class GHEventsSubscriber implements ExtensionPoint {
/**
* This method called when root action receives webhook from GH and this extension is interested in such
- * events (provided by {@link #events()} method). By default do nothing and can be overrided to implement any
+ * events (provided by {@link #events()} method). By default do nothing and can be overridden to implement any
* parse logic
- * Don't call it directly, use {@link #processEvent(GHEvent, String)} static function
+ * Don't call it directly, use {@link #processEvent(GHSubscriberEvent)} static function
*
* @param event gh-event (as of PUSH, ISSUE...). One of returned by {@link #events()} method. Never null.
* @param payload payload of gh-event. Never blank. Can be parsed with help of GitHub#parseEventPayload
+ * @deprecated override {@link #onEvent(GHSubscriberEvent)} instead.
*/
+ @Deprecated
protected void onEvent(GHEvent event, String payload) {
// do nothing by default
}
+ /**
+ * This method called when root action receives webhook from GH and this extension is interested in such
+ * events (provided by {@link #events()} method). By default do nothing and can be overridden to implement any
+ * parse logic
+ * Don't call it directly, use {@link #processEvent(GHSubscriberEvent)} static function
+ *
+ * @param event the event.
+ * @since 1.26.0
+ */
+ protected void onEvent(GHSubscriberEvent event) {
+ onEvent(event.getGHEvent(), event.getPayload());
+ }
+
/**
* @return All subscriber extensions
*/
@@ -79,7 +157,7 @@ public static ExtensionList all() {
public static Function> extractEvents() {
return new NullSafeFunction>() {
@Override
- protected Set applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
+ protected Set applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
return defaultIfNull(subscriber.events(), Collections.emptySet());
}
};
@@ -92,12 +170,27 @@ protected Set applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
*
* @return predicate to use in iterable filtering
* @see #isApplicable
+ * @deprecated use {@link #isApplicableFor(Item)}.
*/
+ @Deprecated
public static Predicate isApplicableFor(final Job, ?> project) {
+ return isApplicableFor((Item) project);
+ }
+
+ /**
+ * Helps to filter only GHEventsSubscribers that can return TRUE on given item
+ *
+ * @param item to check every GHEventsSubscriber for being applicable
+ *
+ * @return predicate to use in iterable filtering
+ * @see #isApplicable
+ * @since 1.25.0
+ */
+ public static Predicate isApplicableFor(final Item item) {
return new NullSafePredicate() {
@Override
- protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
- return subscriber.isApplicable(project);
+ protected boolean applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
+ return subscriber.safeIsApplicable(item);
}
};
}
@@ -112,26 +205,40 @@ protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
public static Predicate isInterestedIn(final GHEvent event) {
return new NullSafePredicate() {
@Override
- protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
+ protected boolean applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
return defaultIfNull(subscriber.events(), emptySet()).contains(event);
}
};
}
/**
- * Function which calls {@link #onEvent(GHEvent, String)} for every subscriber on apply
+ * Function which calls {@link #onEvent(GHSubscriberEvent)} for every subscriber on apply
*
* @param event from hook. Applied only with event from {@link #events()} set
* @param payload string content of hook from GH. Never blank
*
* @return function to process {@link GHEventsSubscriber} list. Returns null on apply.
+ * @deprecated use {@link #processEvent(GHSubscriberEvent)}
*/
+ @Deprecated
public static Function processEvent(final GHEvent event, final String payload) {
+ return processEvent(new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest2()), event, payload));
+ }
+
+ /**
+ * Function which calls {@link #onEvent(GHSubscriberEvent)} for every subscriber on apply
+ *
+ * @param event the event
+ *
+ * @return function to process {@link GHEventsSubscriber} list. Returns null on apply.
+ * @since 1.26.0
+ */
+ public static Function processEvent(final GHSubscriberEvent event) {
return new NullSafeFunction() {
@Override
- protected Void applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
+ protected Void applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
try {
- subscriber.onEvent(event, payload);
+ subscriber.onEvent(event);
} catch (Throwable t) {
LOGGER.error("Subscriber {} failed to process {} hook, skipping...",
subscriber.getClass().getName(), event, t);
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java b/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java
new file mode 100644
index 000000000..bde28d6f1
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java
@@ -0,0 +1,62 @@
+package org.jenkinsci.plugins.github.extension;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jenkins.scm.api.SCMEvent;
+import org.kohsuke.github.GHEvent;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * An event for a {@link GHEventsSubscriber}.
+ *
+ * @since 1.26.0
+ */
+public class GHSubscriberEvent extends SCMEvent {
+ /**
+ * The type of event.
+ */
+ private final GHEvent ghEvent;
+
+ private final String eventGuid;
+
+ /**
+ * @deprecated use {@link #GHSubscriberEvent(String, String, GHEvent, String)} instead.
+ */
+ @Deprecated
+ public GHSubscriberEvent(@CheckForNull String origin, @NonNull GHEvent ghEvent, @NonNull String payload) {
+ this(null, origin, ghEvent, payload);
+ }
+
+ /**
+ * Constructs a new {@link GHSubscriberEvent}.
+ * @param eventGuid the globally unique identifier (GUID) to identify the event; value of
+ * request header {@link com.cloudbees.jenkins.GitHubWebHook#X_GITHUB_DELIVERY}.
+ * @param origin the origin (see {@link SCMEvent#originOf(HttpServletRequest)}) or {@code null}.
+ * @param ghEvent the type of event received from GitHub.
+ * @param payload the event payload.
+ */
+ public GHSubscriberEvent(
+ @CheckForNull String eventGuid,
+ @CheckForNull String origin,
+ @NonNull GHEvent ghEvent,
+ @NonNull String payload) {
+ super(Type.UPDATED, payload, origin);
+ this.ghEvent = ghEvent;
+ this.eventGuid = eventGuid;
+ }
+
+ /**
+ * Gets the type of event received.
+ *
+ * @return the type of event received.
+ */
+ public GHEvent getGHEvent() {
+ return ghEvent;
+ }
+
+ @CheckForNull
+ public String getEventGuid() {
+ return eventGuid;
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java
index 325261387..5b118fa1c 100644
--- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java
@@ -5,7 +5,7 @@
import hudson.model.Run;
import hudson.model.TaskListener;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
/**
@@ -23,6 +23,6 @@ public abstract class GitHubCommitShaSource extends AbstractDescribableImpl run, @Nonnull TaskListener listener)
+ public abstract String get(@NonNull Run, ?> run, @NonNull TaskListener listener)
throws IOException, InterruptedException;
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java
index fa21c9bd9..c231297f7 100644
--- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java
@@ -6,7 +6,7 @@
import hudson.model.TaskListener;
import org.kohsuke.github.GHRepository;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
/**
@@ -23,5 +23,5 @@ public abstract class GitHubReposSource extends AbstractDescribableImpl repos(@Nonnull Run, ?> run, @Nonnull TaskListener listener);
+ public abstract List repos(@NonNull Run, ?> run, @NonNull TaskListener listener);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusBackrefSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusBackrefSource.java
new file mode 100644
index 000000000..92130eed7
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusBackrefSource.java
@@ -0,0 +1,25 @@
+package org.jenkinsci.plugins.github.extension.status;
+
+import hudson.ExtensionPoint;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+
+/**
+ * Extension point to provide backref for the status, i.e. to the build or to the test report.
+ *
+ * @author pupssman (Kalinin Ivan)
+ * @since 1.21.2
+ */
+public abstract class GitHubStatusBackrefSource extends AbstractDescribableImpl
+ implements ExtensionPoint {
+
+ /**
+ * @param run actual run
+ * @param listener build listener
+ *
+ * @return URL that points to the status source, i.e. test result page
+ */
+ public abstract String get(Run, ?> run, TaskListener listener);
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java
index f359f1810..bc307d6c7 100644
--- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java
@@ -5,7 +5,7 @@
import hudson.model.Run;
import hudson.model.TaskListener;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
/**
* Extension point to provide context of the state. For example `integration-tests` or `build`
@@ -22,5 +22,5 @@ public abstract class GitHubStatusContextSource extends AbstractDescribableImpl<
*
* @return simple short string to represent context of this state
*/
- public abstract String context(@Nonnull Run, ?> run, @Nonnull TaskListener listener);
+ public abstract String context(@NonNull Run, ?> run, @NonNull TaskListener listener);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java
index 81a14b811..620864120 100644
--- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java
@@ -6,7 +6,7 @@
import hudson.model.TaskListener;
import org.kohsuke.github.GHCommitState;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
/**
@@ -24,7 +24,7 @@ public abstract class GitHubStatusResultSource extends AbstractDescribableImpl run, @Nonnull TaskListener listener)
+ public abstract StatusResult get(@NonNull Run, ?> run, @NonNull TaskListener listener)
throws IOException, InterruptedException;
/**
diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java
index c1486b331..cfc9dc624 100644
--- a/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java
+++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java
@@ -10,7 +10,7 @@
import org.kohsuke.github.GHCommitState;
import org.kohsuke.stapler.DataBoundSetter;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
/**
* This extension point allows to define when and what to send as state and message.
@@ -56,7 +56,7 @@ public String getMessage() {
*
* @return true if matches
*/
- public abstract boolean matches(@Nonnull Run, ?> run);
+ public abstract boolean matches(@NonNull Run, ?> run);
/**
* Should be extended to and marked as {@link hudson.Extension} to be in list
diff --git a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java
index 1610fe48c..7ea4b69a3 100644
--- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java
+++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java
@@ -4,7 +4,8 @@
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.hash.Hashing;
-import com.squareup.okhttp.Cache;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import okhttp3.Cache;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
@@ -13,9 +14,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Path;
import java.util.List;
@@ -132,7 +133,7 @@ private static void deleteEveryIn(DirectoryStream caches) {
*/
private static class WithEnabledCache extends NullSafePredicate {
@Override
- protected boolean applyNullSafe(@Nonnull GitHubServerConfig config) {
+ protected boolean applyNullSafe(@NonNull GitHubServerConfig config) {
return config.getClientCacheSize() > 0;
}
}
@@ -145,12 +146,11 @@ private static class ToCacheDir extends NullSafeFunction 0, "Cache can't be with size <= 0");
Path cacheDir = getBaseCacheDir().resolve(hashed(config));
-
- return new Cache(cacheDir.toFile(), config.getClientCacheSize() * MB);
+ return new Cache(cacheDir.toFile(), (long) config.getClientCacheSize() * MB);
}
/**
@@ -160,8 +160,8 @@ protected Cache applyNullSafe(@Nonnull GitHubServerConfig config) {
*/
private static String hashed(GitHubServerConfig config) {
return Hashing.murmur3_32().newHasher()
- .putString(trimToEmpty(config.getApiUrl()))
- .putString(trimToEmpty(config.getCredentialsId())).hash().toString();
+ .putString(trimToEmpty(config.getApiUrl()), StandardCharsets.UTF_8)
+ .putString(trimToEmpty(config.getCredentialsId()), StandardCharsets.UTF_8).hash().toString();
}
}
@@ -170,8 +170,8 @@ private static String hashed(GitHubServerConfig config) {
*/
private static class CacheToName extends NullSafeFunction {
@Override
- protected String applyNullSafe(@Nonnull Cache cache) {
- return cache.getDirectory().getName();
+ protected String applyNullSafe(@NonNull Cache cache) {
+ return cache.directory().getName();
}
}
@@ -181,7 +181,7 @@ protected String applyNullSafe(@Nonnull Cache cache) {
private static class NotInCachesFilter implements DirectoryStream.Filter {
private final Set activeCacheNames;
- public NotInCachesFilter(Set activeCacheNames) {
+ NotInCachesFilter(Set activeCacheNames) {
this.activeCacheNames = activeCacheNames;
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java
index 01e14947d..ecee2d33b 100644
--- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java
+++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java
@@ -1,9 +1,9 @@
package org.jenkinsci.plugins.github.internal;
import com.cloudbees.jenkins.GitHubWebHook;
-import com.squareup.okhttp.Cache;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.OkUrlFactory;
+import io.jenkins.plugins.okhttp.api.JenkinsOkHttpClient;
+import okhttp3.Cache;
+import okhttp3.OkHttpClient;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
@@ -11,15 +11,14 @@
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
-import org.kohsuke.github.HttpConnector;
import org.kohsuke.github.RateLimitHandler;
+import org.kohsuke.github.extras.okhttp3.OkHttpConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
-import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
@@ -46,6 +45,7 @@
@Restricted(NoExternalUse.class)
public class GitHubLoginFunction extends NullSafeFunction {
+ private static final OkHttpClient BASECLIENT = JenkinsOkHttpClient.newClientBuilder(new OkHttpClient()).build();
private static final Logger LOGGER = LoggerFactory.getLogger(GitHubLoginFunction.class);
/**
@@ -58,7 +58,7 @@ public class GitHubLoginFunction extends NullSafeFunction 0) {
Cache cache = toCacheDir().apply(config);
- client.setCache(cache);
- }
-
- return new OkHttpConnector(new OkUrlFactory(client));
- }
-
- /**
- * Copy-paste due to class loading issues
- *
- * @see org.kohsuke.github.extras.OkHttpConnector
- */
- private static class OkHttpConnector implements HttpConnector {
- private final OkUrlFactory urlFactory;
-
- private OkHttpConnector(OkUrlFactory urlFactory) {
- this.urlFactory = urlFactory;
+ builder.cache(cache);
}
- @Override
- public HttpURLConnection connect(URL url) throws IOException {
- return urlFactory.open(url);
- }
+ return new OkHttpConnector(builder.build());
}
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java
index 370babe1f..9ed3ca0da 100644
--- a/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java
+++ b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java
@@ -54,7 +54,7 @@ public void migrate() throws IOException {
if (descriptor.getDeprecatedHookUrl() != null) {
LOGGER.warn("Migration for old GitHub Plugin hook url started");
GitHubPlugin.configuration().setOverrideHookUrl(true);
- GitHubPlugin.configuration().setHookUrl(descriptor.getDeprecatedHookUrl());
+ GitHubPlugin.configuration().setHookUrl(descriptor.getDeprecatedHookUrl().toString());
descriptor.clearDeprecatedHookUrl();
descriptor.save();
GitHubPlugin.configuration().save();
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java
index d479933cb..0d1d79bd0 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java
@@ -14,11 +14,13 @@
import org.jenkinsci.plugins.github.common.CombineErrorHandler;
import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource;
import org.jenkinsci.plugins.github.extension.status.GitHubReposSource;
+import org.jenkinsci.plugins.github.extension.status.GitHubStatusBackrefSource;
import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource;
import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource;
import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler;
import org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource;
import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource;
+import org.jenkinsci.plugins.github.status.sources.BuildRefBackrefSource;
import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource;
import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource;
import org.kohsuke.github.GHCommitState;
@@ -26,7 +28,7 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
@@ -44,6 +46,7 @@ public class GitHubCommitStatusSetter extends Notifier implements SimpleBuildSte
private GitHubReposSource reposSource = new AnyDefinedRepositorySource();
private GitHubStatusContextSource contextSource = new DefaultCommitContextSource();
private GitHubStatusResultSource statusResultSource = new DefaultStatusResultSource();
+ private GitHubStatusBackrefSource statusBackrefSource = new BuildRefBackrefSource();
private List errorHandlers = new ArrayList<>();
@DataBoundConstructor
@@ -70,6 +73,11 @@ public void setStatusResultSource(GitHubStatusResultSource statusResultSource) {
this.statusResultSource = statusResultSource;
}
+ @DataBoundSetter
+ public void setStatusBackrefSource(GitHubStatusBackrefSource statusBackrefSource) {
+ this.statusBackrefSource = statusBackrefSource;
+ }
+
@DataBoundSetter
public void setErrorHandlers(List errorHandlers) {
this.errorHandlers = errorHandlers;
@@ -103,6 +111,13 @@ public GitHubStatusResultSource getStatusResultSource() {
return statusResultSource;
}
+ /**
+ * @return backref provider
+ */
+ public GitHubStatusBackrefSource getStatusBackrefSource() {
+ return statusBackrefSource;
+ }
+
/**
* @return error handlers
*/
@@ -114,20 +129,29 @@ public List getErrorHandlers() {
* Gets info from the providers and updates commit status
*/
@Override
- public void perform(@Nonnull Run, ?> run, @Nonnull FilePath workspace, @Nonnull Launcher launcher,
- @Nonnull TaskListener listener) {
+ public void perform(@NonNull Run, ?> run, @NonNull FilePath workspace, @NonNull Launcher launcher,
+ @NonNull TaskListener listener) {
try {
String sha = getCommitShaSource().get(run, listener);
List repos = getReposSource().repos(run, listener);
String contextName = getContextSource().context(run, listener);
- String backref = run.getAbsoluteUrl();
+ String backref = getStatusBackrefSource().get(run, listener);
GitHubStatusResultSource.StatusResult result = getStatusResultSource().get(run, listener);
String message = result.getMsg();
GHCommitState state = result.getState();
+ listener.getLogger().printf(
+ "[%s] %s on repos %s (sha:%7.7s) with context:%s%n",
+ getDescriptor().getDisplayName(),
+ state,
+ repos,
+ sha,
+ contextName
+ );
+
for (GHRepository repo : repos) {
listener.getLogger().println(
GitHubCommitNotifier_SettingCommitStatus(repo.getHtmlUrl() + "/commit/" + sha)
@@ -146,6 +170,13 @@ public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
+ public Object readResolve() {
+ if (getStatusBackrefSource() == null) {
+ setStatusBackrefSource(new BuildRefBackrefSource());
+ }
+ return this;
+ }
+
@Extension
public static class GitHubCommitStatusSetterDescr extends BuildStepDescriptor {
@@ -156,7 +187,7 @@ public boolean isApplicable(Class extends AbstractProject> jobType) {
@Override
public String getDisplayName() {
- return "Set status for GitHub commit [universal]";
+ return "Set GitHub commit status (universal)";
}
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java
index 1400f9822..348f4084c 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java
@@ -9,7 +9,7 @@
import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import static hudson.model.Result.FAILURE;
import static hudson.model.Result.UNSTABLE;
@@ -40,7 +40,7 @@ public String getResult() {
* @return true as of it terminating handler
*/
@Override
- public boolean handle(Exception e, @Nonnull Run, ?> run, @Nonnull TaskListener listener) {
+ public boolean handle(Exception e, @NonNull Run, ?> run, @NonNull TaskListener listener) {
Result toSet = Result.fromString(trimToEmpty(result));
listener.error("[GitHub Commit Status Setter] - %s, setting build result to %s", e.getMessage(), toSet);
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java
index ed389b7dc..4fb544526 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java
@@ -7,7 +7,7 @@
import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
/**
* Just logs message to the build console and do nothing after it
@@ -25,7 +25,7 @@ public ShallowAnyErrorHandler() {
* @return true as of its terminating handler
*/
@Override
- public boolean handle(Exception e, @Nonnull Run, ?> run, @Nonnull TaskListener listener) {
+ public boolean handle(Exception e, @NonNull Run, ?> run, @NonNull TaskListener listener) {
listener.error("[GitHub Commit Status Setter] Failed to update commit state on GitHub. "
+ "Ignoring exception [%s]", e.getMessage());
return true;
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java
index d6e1d1029..b0333d88b 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java
@@ -10,8 +10,10 @@
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.kohsuke.github.GHRepository;
import org.kohsuke.stapler.DataBoundConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Collection;
import java.util.List;
@@ -25,6 +27,8 @@
*/
public class AnyDefinedRepositorySource extends GitHubReposSource {
+ private static final Logger LOG = LoggerFactory.getLogger(AnyDefinedRepositorySource.class);
+
@DataBoundConstructor
public AnyDefinedRepositorySource() {
}
@@ -33,12 +37,15 @@ public AnyDefinedRepositorySource() {
* @return all repositories which can be found by repo-contributors
*/
@Override
- public List repos(@Nonnull Run, ?> run, @Nonnull TaskListener listener) {
+ public List repos(@NonNull Run, ?> run, @NonNull TaskListener listener) {
final Collection names = GitHubRepositoryNameContributor
.parseAssociatedNames(run.getParent());
+
+ LOG.trace("repositories source=repo-name-contributor value={}", names);
+
return from(names).transformAndConcat(new NullSafeFunction>() {
@Override
- protected Iterable applyNullSafe(@Nonnull GitHubRepositoryName name) {
+ protected Iterable applyNullSafe(@NonNull GitHubRepositoryName name) {
return name.resolve();
}
}).toList();
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java
index 126122b67..bdec8c467 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java
@@ -9,7 +9,7 @@
import org.jenkinsci.plugins.github.util.BuildDataHelper;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
/**
@@ -28,7 +28,7 @@ public BuildDataRevisionShaSource() {
* @return sha from git's scm build data action
*/
@Override
- public String get(@Nonnull Run, ?> run, @Nonnull TaskListener listener) throws IOException {
+ public String get(@NonNull Run, ?> run, @NonNull TaskListener listener) throws IOException {
return ObjectId.toString(BuildDataHelper.getCommitSHA1(run));
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource.java
new file mode 100644
index 000000000..9f4bbdbc8
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource.java
@@ -0,0 +1,39 @@
+package org.jenkinsci.plugins.github.status.sources;
+
+import hudson.Extension;
+import hudson.model.Descriptor;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;
+import org.jenkinsci.plugins.github.extension.status.GitHubStatusBackrefSource;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+/**
+ * Gets backref from Run URL.
+ *
+ * @author pupssman (Kalinin Ivan)
+ * @since 1.22.1
+ */
+public class BuildRefBackrefSource extends GitHubStatusBackrefSource {
+
+ @DataBoundConstructor
+ public BuildRefBackrefSource() {
+ }
+
+ /**
+ * Returns absolute URL of the Run
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public String get(Run, ?> run, TaskListener listener) {
+ return DisplayURLProvider.get().getRunURL(run);
+ }
+
+ @Extension
+ public static class BuildRefBackrefSourceDescriptor extends Descriptor {
+ @Override
+ public String getDisplayName() {
+ return "Backref to the build";
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java
index 268ee604b..2c7cd6cb5 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java
@@ -11,7 +11,7 @@
import org.kohsuke.github.GHCommitState;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -34,7 +34,7 @@ public ConditionalStatusResultSource(List results) {
this.results = results;
}
- @Nonnull
+ @NonNull
public List getResults() {
return defaultIfNull(results, Collections.emptyList());
}
@@ -46,7 +46,7 @@ public List getResults() {
* @return first matched result or pending state with warn msg
*/
@Override
- public StatusResult get(@Nonnull Run, ?> run, @Nonnull TaskListener listener)
+ public StatusResult get(@NonNull Run, ?> run, @NonNull TaskListener listener)
throws IOException, InterruptedException {
for (ConditionalResult conditionalResult : getResults()) {
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java
index fbd1d3ccb..ee4a38694 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java
@@ -7,7 +7,7 @@
import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor;
@@ -28,7 +28,7 @@ public DefaultCommitContextSource() {
* @see com.coravy.hudson.plugins.github.GithubProjectProperty#displayNameFor(hudson.model.Job)
*/
@Override
- public String context(@Nonnull Run, ?> run, @Nonnull TaskListener listener) {
+ public String context(@NonNull Run, ?> run, @NonNull TaskListener listener) {
return displayNameFor(run.getParent());
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java
index c33971aff..e1a1176f7 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java
@@ -10,7 +10,7 @@
import org.kohsuke.github.GHCommitState;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import static hudson.model.Result.FAILURE;
@@ -34,7 +34,7 @@ public DefaultStatusResultSource() {
}
@Override
- public StatusResult get(@Nonnull Run, ?> run, @Nonnull TaskListener listener) throws IOException,
+ public StatusResult get(@NonNull Run, ?> run, @NonNull TaskListener listener) throws IOException,
InterruptedException {
// We do not use `build.getDurationString()` because it appends 'and counting' (build is still running)
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource.java
new file mode 100644
index 000000000..ba6c7de01
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource.java
@@ -0,0 +1,57 @@
+package org.jenkinsci.plugins.github.status.sources;
+
+import org.jenkinsci.plugins.github.common.ExpandableMessage;
+import org.jenkinsci.plugins.github.extension.status.GitHubStatusBackrefSource;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import hudson.Extension;
+import hudson.model.Descriptor;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+
+/**
+ * Allows to manually enter backref, with env/token expansion.
+ *
+ * @author pupssman (Kalinin Ivan)
+ * @since 1.21.2
+ *
+ */
+public class ManuallyEnteredBackrefSource extends GitHubStatusBackrefSource {
+ private static final Logger LOG = LoggerFactory.getLogger(ManuallyEnteredBackrefSource.class);
+
+ private String backref;
+
+ @DataBoundConstructor
+ public ManuallyEnteredBackrefSource(String backref) {
+ this.backref = backref;
+ }
+
+ public String getBackref() {
+ return backref;
+ }
+
+ /**
+ * Just returns what user entered. Expands env vars and token macro
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public String get(Run, ?> run, TaskListener listener) {
+ try {
+ return new ExpandableMessage(backref).expandAll(run, listener);
+ } catch (Exception e) {
+ LOG.debug("Can't expand backref, returning as is", e);
+ return backref;
+ }
+ }
+
+ @Extension
+ public static class ManuallyEnteredBackrefSourceDescriptor extends Descriptor {
+ @Override
+ public String getDisplayName() {
+ return "Manually entered backref";
+ }
+ }
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java
index ee28e2dd7..ae7768918 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java
@@ -10,7 +10,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
/**
* Allows to manually enter context
@@ -36,7 +36,7 @@ public String getContext() {
* Just returns what user entered. Expands env vars and token macro
*/
@Override
- public String context(@Nonnull Run, ?> run, @Nonnull TaskListener listener) {
+ public String context(@NonNull Run, ?> run, @NonNull TaskListener listener) {
try {
return new ExpandableMessage(context).expandAll(run, listener);
} catch (Exception e) {
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java
index 0a73f04f3..3493321b2 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java
@@ -11,7 +11,7 @@
import org.kohsuke.github.GHRepository;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Collections;
import java.util.List;
@@ -35,11 +35,11 @@ GitHubRepositoryName createName(String url) {
}
@Override
- public List repos(@Nonnull Run, ?> run, @Nonnull final TaskListener listener) {
+ public List repos(@NonNull Run, ?> run, @NonNull final TaskListener listener) {
List urls = Collections.singletonList(url);
return from(urls).transformAndConcat(new NullSafeFunction>() {
@Override
- protected Iterable applyNullSafe(@Nonnull String url) {
+ protected Iterable applyNullSafe(@NonNull String url) {
GitHubRepositoryName name = createName(url);
if (name != null) {
return name.resolve();
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java
index 74b353f45..a6055a863 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java
@@ -8,7 +8,7 @@
import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
/**
@@ -34,7 +34,7 @@ public String getSha() {
* Expands env vars and token macro in entered sha
*/
@Override
- public String get(@Nonnull Run, ?> run, @Nonnull TaskListener listener) throws IOException, InterruptedException {
+ public String get(@NonNull Run, ?> run, @NonNull TaskListener listener) throws IOException, InterruptedException {
return new ExpandableMessage(sha).expandAll(run, listener);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java
index 947db9075..1f1dcb7fc 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java
@@ -6,7 +6,7 @@
import org.kohsuke.github.GHCommitState;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
/**
* Allows to set state in any case
@@ -24,7 +24,7 @@ public AnyBuildResult() {
* @return true in any case
*/
@Override
- public boolean matches(@Nonnull Run, ?> run) {
+ public boolean matches(@NonNull Run, ?> run) {
return true;
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java
index 9600e4b22..8fcd53185 100644
--- a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java
+++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java
@@ -9,7 +9,7 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import static hudson.model.Result.FAILURE;
import static hudson.model.Result.SUCCESS;
@@ -45,7 +45,7 @@ public String getResult() {
* @return matches if run result better than or equal to selected
*/
@Override
- public boolean matches(@Nonnull Run, ?> run) {
+ public boolean matches(@NonNull Run, ?> run) {
return defaultIfNull(run.getResult(), Result.NOT_BUILT).isBetterOrEqualTo(fromString(trimToEmpty(result)));
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java b/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java
index 81c5d6565..b4a8e72bd 100644
--- a/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java
+++ b/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java
@@ -1,24 +1,70 @@
package org.jenkinsci.plugins.github.util;
+import hudson.model.Job;
import hudson.model.Run;
import hudson.plugins.git.Revision;
import hudson.plugins.git.util.Build;
import hudson.plugins.git.util.BuildData;
import org.eclipse.jgit.lib.ObjectId;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
+import java.util.List;
+import java.util.Set;
/**
* Stores common methods for {@link BuildData} handling.
*
- * @author Oleg Nenashev
+ * @author Oleg Nenashev
* @since 1.10
*/
public final class BuildDataHelper {
private BuildDataHelper() {
}
+ /**
+ * Calculate build data from downstream builds, that could be a shared library
+ * which is loaded first in a pipeline. For that reason, this method compares
+ * all remote URLs for each build data, with the real project name, to determine
+ * the proper build data. This way, the SHA returned in the build data will
+ * relate to the project
+ *
+ * @param parentName name of the parent build
+ * @param parentFullName full name of the parent build
+ * @param buildDataList the list of build datas from a build run
+ * @return the build data related to the project, null if not found
+ */
+ public static BuildData calculateBuildData(
+ String parentName, String parentFullName, List buildDataList
+ ) {
+
+ if (buildDataList == null) {
+ return null;
+ }
+
+ if (buildDataList.size() == 1) {
+ return buildDataList.get(0);
+ }
+
+ String projectName = parentFullName.replace(parentName, "");
+
+ if (projectName.endsWith("/")) {
+ projectName = projectName.substring(0, projectName.lastIndexOf('/'));
+ }
+
+ for (BuildData buildData : buildDataList) {
+ Set remoteUrls = buildData.getRemoteUrls();
+
+ for (String remoteUrl : remoteUrls) {
+ if (remoteUrl.contains(projectName)) {
+ return buildData;
+ }
+ }
+ }
+
+ return null;
+ }
+
/**
* Gets SHA1 from the build.
*
@@ -27,9 +73,16 @@ private BuildDataHelper() {
* @return SHA1 of the las
* @throws IOException Cannot get the info about commit ID
*/
- @Nonnull
- public static ObjectId getCommitSHA1(@Nonnull Run, ?> build) throws IOException {
- BuildData buildData = build.getAction(BuildData.class);
+ @NonNull
+ public static ObjectId getCommitSHA1(@NonNull Run, ?> build) throws IOException {
+ List buildDataList = build.getActions(BuildData.class);
+
+ Job, ?> parent = build.getParent();
+
+ BuildData buildData = calculateBuildData(
+ parent.getName(), parent.getFullName(), buildDataList
+ );
+
if (buildData == null) {
throw new IOException(Messages.BuildDataHelper_NoBuildDataError());
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java
index 8a83f00e7..4ccfcde28 100644
--- a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java
+++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java
@@ -26,10 +26,11 @@
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
-import javax.annotation.CheckReturnValue;
import java.util.Iterator;
import java.util.List;
+import edu.umd.cs.findbugs.annotations.CheckReturnValue;
+
import static com.google.common.base.Preconditions.checkNotNull;
/**
@@ -79,6 +80,16 @@ public final FluentIterableWrapper filter(Predicate super E> predicate) {
return from(Iterables.filter(iterable, predicate));
}
+ /**
+ * Returns the elements from this fluent iterable that are instances of the supplied type. The
+ * resulting fluent iterable's iterator does not support {@code remove()}.
+ * @since 1.25.0
+ */
+ @CheckReturnValue
+ public final FluentIterableWrapper filter(Class clazz) {
+ return from(Iterables.filter(iterable, clazz));
+ }
+
/**
* Returns a fluent iterable that applies {@code function} to each element of this
* fluent iterable.
diff --git a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java
index 1ca60cd97..eafbc2c39 100644
--- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java
+++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java
@@ -5,16 +5,19 @@
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import hudson.model.AbstractProject;
+import hudson.model.BuildableItem;
+import hudson.model.Item;
import hudson.model.Job;
import hudson.triggers.Trigger;
+import hudson.triggers.TriggerDescriptor;
import jenkins.model.ParameterizedJobMixIn;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
-import javax.annotation.CheckForNull;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
import java.util.Collection;
+import java.util.Map;
import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor;
-import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
/**
* Utility class which holds converters or predicates (matchers) to filter or convert job lists
@@ -33,51 +36,34 @@ private JobInfoHelpers() {
*
* @return predicate with true on apply if job contains trigger of given class
*/
- public static Predicate withTrigger(final Class extends Trigger> clazz) {
- return new Predicate() {
- public boolean apply(Job job) {
- return triggerFrom(job, clazz) != null;
- }
- };
+ public static Predicate withTrigger(final Class extends Trigger> clazz) {
+ return item -> triggerFrom(item, clazz) != null;
}
/**
* Can be useful to ignore disabled jobs on reregistering hooks
*
- * @return predicate with true on apply if job is buildable
+ * @return predicate with true on apply if item is buildable
*/
- public static Predicate isBuildable() {
- return new Predicate() {
- public boolean apply(Job job) {
- return job != null && job.isBuildable();
- }
- };
+ public static Predicate isBuildable() {
+ return item -> item instanceof Job ? ((Job, ?>) item).isBuildable() : item instanceof BuildableItem;
}
/**
* @return function which helps to convert job to repo names associated with this job
*/
- public static Function> associatedNames() {
- return new Function>() {
- public Collection apply(Job job) {
- return GitHubRepositoryNameContributor.parseAssociatedNames(job);
- }
- };
+ public static Function> associatedNames() {
+ return GitHubRepositoryNameContributor::parseAssociatedNames;
}
/**
- * If any of event subscriber interested in hook for job, then return true
+ * If any of event subscriber interested in hook for item, then return true
* By default, push hook subscriber is interested in job with gh-push-trigger
*
- * @return predicate with true if job alive and should have hook
+ * @return predicate with true if item alive and should have hook
*/
- public static Predicate isAlive() {
- return new Predicate() {
- @Override
- public boolean apply(Job job) {
- return !from(GHEventsSubscriber.all()).filter(isApplicableFor(job)).toList().isEmpty();
- }
- };
+ public static Predicate isAlive() {
+ return item -> GHEventsSubscriber.all().stream().anyMatch(isApplicableFor(item));
}
/**
@@ -87,13 +73,30 @@ public boolean apply(Job job) {
*
* @return Trigger instance with required class or null
* TODO use standard method in 1.621+
+ * @deprecated use {@link #triggerFrom(Item, Class)}
*/
+ @Deprecated
@CheckForNull
public static T triggerFrom(Job, ?> job, Class tClass) {
- if (job instanceof ParameterizedJobMixIn.ParameterizedJob) {
- ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job;
+ return triggerFrom((Item) job, tClass);
+ }
+
+ /**
+ * @param item job to search trigger in
+ * @param tClass trigger with class which we want to receive from job
+ * @param type of trigger
+ *
+ * @return Trigger instance with required class or null
+ * @since 1.25.0
+ * TODO use standard method in 1.621+
+ */
+ @CheckForNull
+ public static T triggerFrom(Item item, Class tClass) {
+ if (item instanceof ParameterizedJobMixIn.ParameterizedJob) {
+ ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) item;
- for (Trigger candidate : pJob.getTriggers().values()) {
+ Map> triggerMap = pJob.getTriggers();
+ for (Trigger candidate : triggerMap.values()) {
if (tClass.isInstance(candidate)) {
return tClass.cast(candidate);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java
index 9250253c0..3a0918247 100644
--- a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java
+++ b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java
@@ -2,7 +2,7 @@
import com.google.common.base.Function;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -15,11 +15,11 @@ public abstract class NullSafeFunction implements Function {
@Override
public T apply(F input) {
- return applyNullSafe(checkNotNull(input, "This function not allows to use null as argument"));
+ return applyNullSafe(checkNotNull(input, "This function does not allow using null as argument"));
}
/**
* This method will be called inside of {@link #apply(Object)}
*/
- protected abstract T applyNullSafe(@Nonnull F input);
+ protected abstract T applyNullSafe(@NonNull F input);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java
index 5e9987d7c..847753d59 100644
--- a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java
+++ b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java
@@ -2,7 +2,7 @@
import com.google.common.base.Predicate;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -22,5 +22,5 @@ public boolean apply(T input) {
/**
* This method will be called inside of {@link #apply(Object)}
*/
- protected abstract boolean applyNullSafe(@Nonnull T input);
+ protected abstract boolean applyNullSafe(@NonNull T input);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java
index b17f82116..71d19fed6 100644
--- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java
@@ -3,10 +3,10 @@
import org.kohsuke.github.GHEvent;
import org.kohsuke.stapler.AnnotationHandler;
import org.kohsuke.stapler.InjectedParameter;
-import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerRequest2;
import org.slf4j.Logger;
-import javax.servlet.ServletException;
+import jakarta.servlet.ServletException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@@ -42,7 +42,7 @@ class PayloadHandler extends AnnotationHandler {
* @return parsed {@link GHEvent} or null on empty header or unknown value
*/
@Override
- public Object parse(StaplerRequest req, GHEventHeader a, Class type, String param) throws ServletException {
+ public Object parse(StaplerRequest2 req, GHEventHeader a, Class type, String param) throws ServletException {
isTrue(GHEvent.class.isAssignableFrom(type),
"Parameter '%s' should has type %s, not %s", param,
GHEvent.class.getName(),
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java
index 58c2e1492..f7f192503 100644
--- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java
@@ -8,11 +8,11 @@
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.kohsuke.stapler.AnnotationHandler;
import org.kohsuke.stapler.InjectedParameter;
-import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerRequest2;
import org.slf4j.Logger;
-import javax.annotation.Nonnull;
-import javax.servlet.ServletException;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import jakarta.servlet.ServletException;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -39,15 +39,17 @@
class PayloadHandler extends AnnotationHandler {
private static final Logger LOGGER = getLogger(PayloadHandler.class);
+ public static final String APPLICATION_JSON = "application/json";
+ public static final String FORM_URLENCODED = "application/x-www-form-urlencoded";
/**
* Registered handlers of specified content-types
*
* @see Developer manual
*/
- private static final Map> PAYLOAD_PROCESS =
- ImmutableMap.>builder()
- .put("application/json", fromApplicationJson())
- .put("application/x-www-form-urlencoded", fromForm())
+ private static final Map> PAYLOAD_PROCESS =
+ ImmutableMap.>builder()
+ .put(APPLICATION_JSON, fromApplicationJson())
+ .put(FORM_URLENCODED, fromForm())
.build();
/**
@@ -56,8 +58,8 @@ class PayloadHandler extends AnnotationHandler {
* @return String payload extracted from request or null on any problem
*/
@Override
- public Object parse(StaplerRequest req, GHEventPayload a, Class type, String param) throws ServletException {
- if (notNull(req, "Why StaplerRequest is null?").getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) {
+ public Object parse(StaplerRequest2 req, GHEventPayload a, Class type, String param) throws ServletException {
+ if (notNull(req, "Why StaplerRequest2 is null?").getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) {
// if self test for custom hook url
return null;
}
@@ -80,10 +82,10 @@ public Object parse(StaplerRequest req, GHEventPayload a, Class type, String par
*
* @return function to extract payload from form request parameters
*/
- protected static Function fromForm() {
- return new NullSafeFunction() {
+ protected static Function fromForm() {
+ return new NullSafeFunction() {
@Override
- protected String applyNullSafe(@Nonnull StaplerRequest request) {
+ protected String applyNullSafe(@NonNull StaplerRequest2 request) {
return request.getParameter("payload");
}
};
@@ -94,10 +96,10 @@ protected String applyNullSafe(@Nonnull StaplerRequest request) {
*
* @return function to extract payload from body
*/
- protected static Function fromApplicationJson() {
- return new NullSafeFunction() {
+ protected static Function fromApplicationJson() {
+ return new NullSafeFunction() {
@Override
- protected String applyNullSafe(@Nonnull StaplerRequest request) {
+ protected String applyNullSafe(@NonNull StaplerRequest2 request) {
try {
return IOUtils.toString(request.getInputStream(), Charsets.UTF_8);
} catch (IOException e) {
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java
new file mode 100644
index 000000000..491223c76
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java
@@ -0,0 +1,136 @@
+package org.jenkinsci.plugins.github.webhook;
+
+import hudson.util.Secret;
+import org.apache.commons.codec.binary.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import java.security.MessageDigest;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Utility class for dealing with signatures of incoming requests.
+ *
+ * @see API documentation
+ * @since 1.21.0
+ */
+public class GHWebhookSignature {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GHWebhookSignature.class);
+ private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
+ private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+ public static final String INVALID_SIGNATURE = "COMPUTED_INVALID_SIGNATURE";
+
+ private final String payload;
+ private final Secret secret;
+
+ private GHWebhookSignature(String payload, Secret secret) {
+ this.payload = payload;
+ this.secret = secret;
+ }
+
+ /**
+ * @param payload Clear-text to create signature of.
+ * @param secret Key to sign with.
+ */
+ public static GHWebhookSignature webhookSignature(String payload, Secret secret) {
+ checkNotNull(payload, "Payload can't be null");
+ checkNotNull(secret, "Secret should be defined to compute sign");
+ return new GHWebhookSignature(payload, secret);
+ }
+
+
+ /**
+ * Computes a RFC 2104-compliant HMAC digest using SHA1 of a payloadFrom with a given key (secret).
+ *
+ * @deprecated Use {@link #sha256()} for enhanced security
+ * @return HMAC digest of payloadFrom using secret as key. Will return COMPUTED_INVALID_SIGNATURE
+ * on any exception during computation.
+ */
+ @Deprecated
+ public String sha1() {
+ return computeSignature(HMAC_SHA1_ALGORITHM);
+ }
+
+ /**
+ * Computes a RFC 2104-compliant HMAC digest using SHA256 of a payload with a given key (secret).
+ * This is the recommended method for webhook signature validation.
+ *
+ * @return HMAC digest of payload using secret as key. Will return COMPUTED_INVALID_SIGNATURE
+ * on any exception during computation.
+ * @since 1.45.0
+ */
+ public String sha256() {
+ return computeSignature(HMAC_SHA256_ALGORITHM);
+ }
+ /**
+ * Computes HMAC signature using the specified algorithm.
+ *
+ * @param algorithm The HMAC algorithm to use (e.g., "HmacSHA1", "HmacSHA256")
+ * @return HMAC digest as hex string, or INVALID_SIGNATURE on error
+ */
+ private String computeSignature(String algorithm) {
+ try {
+ final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), algorithm);
+ final Mac mac = Mac.getInstance(algorithm);
+ mac.init(keySpec);
+ final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(UTF_8));
+
+ return Hex.encodeHexString(rawHMACBytes);
+ } catch (Exception e) {
+ LOGGER.error("Error computing {} signature", algorithm, e);
+ return INVALID_SIGNATURE;
+ }
+ }
+
+ /**
+ * @param digest computed signature from external place (GitHub)
+ *
+ * @return true if computed and provided signatures identical
+ * @deprecated Use {@link #matches(String, SignatureAlgorithm)} for explicit algorithm selection
+ */
+ @Deprecated
+ public boolean matches(String digest) {
+ return matches(digest, SignatureAlgorithm.SHA1);
+ }
+
+ /**
+ * Validates a signature using the specified algorithm.
+ * Uses constant-time comparison to prevent timing attacks.
+ *
+ * @param digest the signature to validate (without algorithm prefix)
+ * @param algorithm the signature algorithm to use
+ * @return true if computed and provided signatures match
+ * @since 1.45.0
+ */
+ public boolean matches(String digest, SignatureAlgorithm algorithm) {
+ String computed;
+ switch (algorithm) {
+ case SHA256:
+ computed = sha256();
+ break;
+ case SHA1:
+ computed = sha1();
+ break;
+ default:
+ LOGGER.warn("Unsupported signature algorithm: {}", algorithm);
+ return false;
+ }
+
+ LOGGER.trace("Signature validation: algorithm={} calculated={} provided={}",
+ algorithm, computed, digest);
+ if (digest == null && computed == null) {
+ return true;
+ } else if (digest == null || computed == null) {
+ return false;
+ } else {
+ // Use constant-time comparison to prevent timing attacks
+ return MessageDigest.isEqual(computed.getBytes(UTF_8), digest.getBytes(UTF_8));
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java
index d2c835ca4..9a36c06f7 100644
--- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java
@@ -1,23 +1,32 @@
package org.jenkinsci.plugins.github.webhook;
import com.cloudbees.jenkins.GitHubWebHook;
+import com.google.common.base.Optional;
+import hudson.util.Secret;
import org.jenkinsci.main.modules.instance_identity.InstanceIdentity;
+import org.jenkinsci.plugins.github.GitHubPlugin;
+import org.jenkinsci.plugins.github.config.HookSecretConfig;
import org.jenkinsci.plugins.github.config.GitHubPluginConfig;
import org.jenkinsci.plugins.github.util.FluentIterableWrapper;
import org.kohsuke.github.GHEvent;
import org.kohsuke.stapler.HttpResponses;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.StaplerRequest2;
+import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.interceptor.Interceptor;
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import org.slf4j.Logger;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
+import java.util.List;
import static com.cloudbees.jenkins.GitHubWebHook.X_INSTANCE_IDENTITY;
import static com.google.common.base.Charsets.UTF_8;
@@ -26,13 +35,15 @@
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
-import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static jakarta.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
import static org.apache.commons.codec.binary.Base64.encodeBase64;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
+import static org.apache.commons.lang3.StringUtils.substringAfter;
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
import static org.kohsuke.stapler.HttpResponses.error;
import static org.kohsuke.stapler.HttpResponses.errorWithoutStack;
+import static org.slf4j.LoggerFactory.getLogger;
/**
* InterceptorAnnotation annotation to use on WebMethod signature.
@@ -46,26 +57,46 @@
@InterceptorAnnotation(RequirePostWithGHHookPayload.Processor.class)
public @interface RequirePostWithGHHookPayload {
class Processor extends Interceptor {
+ private static final Logger LOGGER = getLogger(Processor.class);
+ /**
+ * Header key being used for the legacy SHA-1 payload signatures.
+ *
+ * @see Developer manual
+ * @deprecated Use SHA-256 signatures with X-Hub-Signature-256 header
+ */
+ @Deprecated
+ public static final String SIGNATURE_HEADER = "X-Hub-Signature";
+ /**
+ * Header key being used for the SHA-256 payload signatures (recommended).
+ *
+ * @see
+ * GitHub Documentation
+ * @since 1.45.0
+ */
+ public static final String SIGNATURE_HEADER_SHA256 = "X-Hub-Signature-256";
+ public static final String SHA1_PREFIX = "sha1=";
+ public static final String SHA256_PREFIX = "sha256=";
@Override
- public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, Object[] arguments)
- throws IllegalAccessException, InvocationTargetException {
+ public Object invoke(StaplerRequest2 req, StaplerResponse2 rsp, Object instance, Object[] arguments)
+ throws IllegalAccessException, InvocationTargetException, ServletException {
shouldBePostMethod(req);
returnsInstanceIdentityIfLocalUrlTest(req);
shouldContainParseablePayload(arguments);
+ shouldProvideValidSignature(req, arguments);
return target.invoke(req, rsp, instance, arguments);
}
/**
- * Duplicates {@link @org.kohsuke.stapler.interceptor.RequirePOST} precheck.
+ * Duplicates {@link org.kohsuke.stapler.interceptor.RequirePOST} precheck.
* As of it can't guarantee order of multiply interceptor calls,
* it should implement all features of required interceptors in one class
*
* @throws InvocationTargetException if method os not POST
*/
- protected void shouldBePostMethod(StaplerRequest request) throws InvocationTargetException {
+ protected void shouldBePostMethod(StaplerRequest2 request) throws InvocationTargetException {
if (!request.getMethod().equals("POST")) {
throw new InvocationTargetException(error(SC_METHOD_NOT_ALLOWED, "Method POST required"));
}
@@ -74,12 +105,12 @@ protected void shouldBePostMethod(StaplerRequest request) throws InvocationTarge
/**
* Used for {@link GitHubPluginConfig#doCheckHookUrl(String)}}
*/
- protected void returnsInstanceIdentityIfLocalUrlTest(StaplerRequest req) throws InvocationTargetException {
+ protected void returnsInstanceIdentityIfLocalUrlTest(StaplerRequest2 req) throws InvocationTargetException {
if (req.getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) {
// when the configuration page provides the self-check button, it makes a request with this header.
throw new InvocationTargetException(new HttpResponses.HttpResponseException() {
@Override
- public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node)
+ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node)
throws IOException, ServletException {
RSAPublicKey key = new InstanceIdentity().getPublic();
rsp.setStatus(HttpServletResponse.SC_OK);
@@ -94,7 +125,6 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod
* If any other argument will be added to root action index method, then arg count check should be changed
*
* @param arguments event and payload. Both not null and not blank
- *
* @throws InvocationTargetException if any of preconditions is not satisfied
*/
protected void shouldContainParseablePayload(Object[] arguments) throws InvocationTargetException {
@@ -113,12 +143,106 @@ protected void shouldContainParseablePayload(Object[] arguments) throws Invocati
);
}
+ /**
+ * Checks that an incoming request has a valid signature,
+ * if a hook secret is specified in the GitHub plugin config.
+ * If no hook secret is configured, then the signature is ignored.
+ *
+ * Uses the configured signature algorithm (SHA-256 by default, SHA-1 for legacy support).
+ *
+ * @param req Incoming request.
+ * @throws InvocationTargetException if any of preconditions is not satisfied
+ */
+ protected void shouldProvideValidSignature(StaplerRequest2 req, Object[] args)
+ throws InvocationTargetException {
+ List secretConfigs = GitHubPlugin.configuration().getHookSecretConfigs();
+
+ if (!secretConfigs.isEmpty()) {
+ boolean validSignatureFound = false;
+
+ for (HookSecretConfig config : secretConfigs) {
+ Secret secret = config.getHookSecret();
+ if (secret == null) {
+ continue;
+ }
+
+ SignatureAlgorithm algorithm = config.getSignatureAlgorithm();
+ String headerName = algorithm.getHeaderName();
+ String expectedPrefix = algorithm.getSignaturePrefix();
+
+ Optional signHeader = Optional.fromNullable(req.getHeader(headerName));
+ if (!signHeader.isPresent()) {
+ LOGGER.debug("No signature header {} found for algorithm {}", headerName, algorithm);
+ continue;
+ }
+
+ String fullSignature = signHeader.get();
+ if (!fullSignature.startsWith(expectedPrefix)) {
+ LOGGER.debug("Signature header {} does not start with expected prefix {}",
+ fullSignature, expectedPrefix);
+ continue;
+ }
+
+ String digest = substringAfter(fullSignature, expectedPrefix);
+ LOGGER.trace("Verifying {} signature from header {}", algorithm, fullSignature);
+
+ boolean isValid = GHWebhookSignature.webhookSignature(payloadFrom(req, args), secret)
+ .matches(digest, algorithm);
+
+ if (isValid) {
+ validSignatureFound = true;
+ // Log deprecation warning for SHA-1 usage
+ if (algorithm == SignatureAlgorithm.SHA1) {
+ LOGGER.warn("Using deprecated SHA-1 signature validation. "
+ + "Consider upgrading webhook configuration to use SHA-256 "
+ + "for enhanced security.");
+ } else {
+ LOGGER.debug("Successfully validated {} signature", algorithm);
+ }
+ break;
+ } else {
+ LOGGER.debug("Signature validation failed for algorithm {}", algorithm);
+ }
+ }
+
+ isTrue(validSignatureFound,
+ "No valid signature found. Ensure webhook is configured with a supported signature algorithm "
+ + "(SHA-256 recommended, SHA-1 for legacy compatibility).");
+ }
+ }
+
+ /**
+ * Extracts parsed payload from args and prepare it to calculating hash
+ * (if json - pass as is, if form - url-encode it with prefix)
+ *
+ * @return ready-to-hash payload
+ */
+ protected String payloadFrom(StaplerRequest2 req, Object[] args) {
+ final String parsedPayload = (String) args[1];
+
+ if (req.getContentType().equals(GHEventPayload.PayloadHandler.APPLICATION_JSON)) {
+ return parsedPayload;
+ } else if (req.getContentType().equals(GHEventPayload.PayloadHandler.FORM_URLENCODED)) {
+ try {
+ return String.format("payload=%s", URLEncoder.encode(
+ parsedPayload,
+ StandardCharsets.UTF_8.toString())
+ );
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ } else {
+ LOGGER.error("Unknown content type {}", req.getContentType());
+
+ }
+ return "";
+ }
+
/**
* Utility method to stop preprocessing if condition is false
*
* @param condition on false throws exception
* @param msg to add to exception
- *
* @throws InvocationTargetException BAD REQUEST 400 status code with message
*/
private void isTrue(boolean condition, String msg) throws InvocationTargetException {
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithm.java b/src/main/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithm.java
new file mode 100644
index 000000000..6668f6e81
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithm.java
@@ -0,0 +1,98 @@
+package org.jenkinsci.plugins.github.webhook;
+
+/**
+ * Enumeration of supported webhook signature algorithms.
+ *
+ * @since 1.45.0
+ */
+public enum SignatureAlgorithm {
+ /**
+ * SHA-256 HMAC signature validation (recommended).
+ * Uses X-Hub-Signature-256 header with sha256= prefix.
+ */
+ SHA256("sha256", "X-Hub-Signature-256", "HmacSHA256"),
+
+ /**
+ * SHA-1 HMAC signature validation (legacy).
+ * Uses X-Hub-Signature header with sha1= prefix.
+ *
+ * @deprecated Use SHA256 for enhanced security
+ */
+ @Deprecated
+ SHA1("sha1", "X-Hub-Signature", "HmacSHA1");
+
+ private final String prefix;
+ private final String headerName;
+ private final String javaAlgorithm;
+
+ /**
+ * System property to override default signature algorithm.
+ * Set to "SHA1" to use legacy SHA-1 as default for backwards compatibility.
+ */
+ public static final String DEFAULT_ALGORITHM_PROPERTY = "jenkins.github.webhook.signature.default";
+
+ /**
+ * Gets the default algorithm for new configurations.
+ * Defaults to SHA-256 for security, but can be overridden via system property.
+ * This is evaluated dynamically to respect system property changes.
+ *
+ * @return the default algorithm based on current system property
+ */
+ public static SignatureAlgorithm getDefault() {
+ return getDefaultAlgorithm();
+ }
+
+ SignatureAlgorithm(String prefix, String headerName, String javaAlgorithm) {
+ this.prefix = prefix;
+ this.headerName = headerName;
+ this.javaAlgorithm = javaAlgorithm;
+ }
+
+ /**
+ * @return the prefix used in signature strings (e.g. "sha256", "sha1")
+ */
+ public String getPrefix() {
+ return prefix;
+ }
+
+ /**
+ * @return the HTTP header name for this algorithm
+ */
+ public String getHeaderName() {
+ return headerName;
+ }
+
+ /**
+ * @return the Java algorithm name for HMAC computation
+ */
+ public String getJavaAlgorithm() {
+ return javaAlgorithm;
+ }
+
+ /**
+ * @return the expected signature prefix including equals sign (e.g. "sha256=", "sha1=")
+ */
+ public String getSignaturePrefix() {
+ return prefix + "=";
+ }
+
+ /**
+ * Determines the default signature algorithm based on system property.
+ * Defaults to SHA-256 for security, but allows SHA-1 override for legacy environments.
+ *
+ * @return the default algorithm to use
+ */
+ private static SignatureAlgorithm getDefaultAlgorithm() {
+ String property = System.getProperty(DEFAULT_ALGORITHM_PROPERTY);
+ if (property == null || property.trim().isEmpty()) {
+ // No property set, use secure SHA-256 default
+ return SHA256;
+ }
+ try {
+ return SignatureAlgorithm.valueOf(property.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // Invalid property value, default to secure SHA-256
+ return SHA256;
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java
index 4e5a3cbce..e809c8b05 100644
--- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java
@@ -3,10 +3,15 @@
import com.cloudbees.jenkins.GitHubRepositoryName;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
+import hudson.model.Item;
import hudson.model.Job;
-import org.apache.commons.lang.Validate;
+import hudson.util.Secret;
+import org.apache.commons.lang3.Validate;
+import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor;
+import org.jenkinsci.plugins.github.config.HookSecretConfig;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
+import org.jenkinsci.plugins.github.util.FluentIterableWrapper;
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.jenkinsci.plugins.github.util.misc.NullSafePredicate;
import org.kohsuke.github.GHEvent;
@@ -16,19 +21,20 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nonnull;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import static com.cloudbees.jenkins.GitHubRepositoryNameContributor.parseAssociatedNames;
-import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.base.Predicates.or;
import static java.lang.String.format;
-import static org.apache.commons.collections.CollectionUtils.isEqualCollection;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks;
import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.extractEvents;
import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor;
@@ -51,7 +57,7 @@ public class WebhookManager {
*
* @param endpoint url which will be created as hook on GH
*/
- private WebhookManager(URL endpoint) {
+ protected WebhookManager(URL endpoint) {
this.endpoint = endpoint;
}
@@ -76,24 +82,46 @@ public static WebhookManager forHookUrl(URL endpoint) {
*
* @return runnable to create hooks on run
* @see #createHookSubscribedTo(List)
+ * @deprecated use {@link #registerFor(Item)}
*/
+ @Deprecated
public Runnable registerFor(final Job, ?> project) {
- final Collection names = parseAssociatedNames(project);
+ return registerFor((Item) project);
+ }
+
+ /**
+ * Creates runnable with ability to create hooks for given project
+ * For each GH repo name contributed by {@link com.cloudbees.jenkins.GitHubRepositoryNameContributor},
+ * this runnable creates hook (with clean old one).
+ *
+ * Hook events job interested in, contributes to full set instances of {@link GHEventsSubscriber}.
+ * New events will be merged with old ones from existent hook.
+ *
+ * By default only push event is registered
+ *
+ * @param item to find for which repos we should create hooks
+ *
+ * @return runnable to create hooks on run
+ * @see #createHookSubscribedTo(List)
+ * @since 1.25.0
+ */
+ public Runnable registerFor(final Item item) {
+ final Collection names = parseAssociatedNames(item);
final List events = from(GHEventsSubscriber.all())
- .filter(isApplicableFor(project))
+ .filter(isApplicableFor(item))
.transformAndConcat(extractEvents()).toList();
return new Runnable() {
public void run() {
if (events.isEmpty()) {
LOGGER.debug("No any subscriber interested in {}, but hooks creation launched, skipping...",
- project.getFullName());
+ item.getFullName());
return;
}
LOGGER.info("GitHub webhooks activated for job {} with {} (events: {})",
- project.getFullName(), names, events);
+ item.getFullName(), names, events);
from(names)
.transform(createHookSubscribedTo(events))
@@ -115,10 +143,10 @@ public void run() {
*/
public void unregisterFor(GitHubRepositoryName name, List aliveRepos) {
try {
- GHRepository repo = checkNotNull(
- from(name.resolve(allowedToManageHooks())).firstMatch(withAdminAccess()).orNull(),
- "There is no credentials with admin access to manage hooks on %s", name
- );
+ GHRepository repo = repoWithWebhookAccess(name);
+ if (repo == null) {
+ return;
+ }
LOGGER.debug("Check {} for redundant hooks...", repo);
@@ -137,8 +165,24 @@ public void unregisterFor(GitHubRepositoryName name, List
}
}
+ private GHRepository repoWithWebhookAccess(GitHubRepositoryName name) {
+ FluentIterableWrapper reposAllowedtoManageWebhooks = from(name.resolve(allowedToManageHooks()));
+ if (!reposAllowedtoManageWebhooks.first().isPresent()) {
+ LOGGER.debug("There are no github repos configured to allow webhook management for: {}", name);
+ return null;
+ }
+ com.google.common.base.Optional repoWithAdminAccess = reposAllowedtoManageWebhooks
+ .firstMatch(withAdminAccess());
+ if (!repoWithAdminAccess.isPresent()) {
+ LOGGER.info("None of the github repos configured have admin access for: {}", name);
+ return null;
+ }
+ GHRepository repo = repoWithAdminAccess.get();
+ return repo;
+ }
+
/**
- * Main logic of {@link #registerFor(Job)}.
+ * Main logic of {@link #registerFor(Item)}.
* Updates hooks with replacing old ones with merged new ones
*
* @param events calculated events list to be registered in hook
@@ -148,12 +192,12 @@ public void unregisterFor(GitHubRepositoryName name, List
protected Function createHookSubscribedTo(final List events) {
return new NullSafeFunction() {
@Override
- protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) {
+ protected GHHook applyNullSafe(@NonNull GitHubRepositoryName name) {
try {
- GHRepository repo = checkNotNull(
- from(name.resolve(allowedToManageHooks())).firstMatch(withAdminAccess()).orNull(),
- "There is no credentials with admin access to manage hooks on %s", name
- );
+ GHRepository repo = repoWithWebhookAccess(name);
+ if (repo == null) {
+ return null;
+ }
Validate.notEmpty(events, "Events list for hook can't be empty");
@@ -164,7 +208,7 @@ protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) {
Set alreadyRegistered = from(hooks)
.transformAndConcat(eventsFromHook()).toSet();
- if (hooks.size() == 1 && isEqualCollection(alreadyRegistered, events)) {
+ if (hooks.size() == 1 && alreadyRegistered.containsAll(events)) {
LOGGER.debug("Hook already registered for events {}", events);
return null;
}
@@ -176,9 +220,9 @@ protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) {
.filter(log("Replaced hook")).toList();
return createWebhook(endpoint, merged).apply(repo);
- } catch (Throwable t) {
- LOGGER.warn("Failed to add GitHub webhook for {}", name, t);
- GitHubHookRegisterProblemMonitor.get().registerProblem(name, t);
+ } catch (Exception e) {
+ LOGGER.warn("Failed to add GitHub webhook for {}", name, e);
+ GitHubHookRegisterProblemMonitor.get().registerProblem(name, e);
}
return null;
}
@@ -192,10 +236,10 @@ protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) {
*
* @return always true predicate
*/
- private Predicate log(final String format) {
+ protected Predicate log(final String format) {
return new NullSafePredicate() {
@Override
- protected boolean applyNullSafe(@Nonnull GHHook input) {
+ protected boolean applyNullSafe(@NonNull GHHook input) {
LOGGER.debug(format("%s {} (events: {})", format), input.getUrl(), input.getEvents());
return true;
}
@@ -210,7 +254,7 @@ protected boolean applyNullSafe(@Nonnull GHHook input) {
protected Predicate withAdminAccess() {
return new NullSafePredicate() {
@Override
- protected boolean applyNullSafe(@Nonnull GHRepository repo) {
+ protected boolean applyNullSafe(@NonNull GHRepository repo) {
return repo.hasAdminAccess();
}
};
@@ -225,7 +269,7 @@ protected boolean applyNullSafe(@Nonnull GHRepository repo) {
*/
protected Predicate serviceWebhookFor(final URL url) {
return new NullSafePredicate() {
- protected boolean applyNullSafe(@Nonnull GHHook hook) {
+ protected boolean applyNullSafe(@NonNull GHHook hook) {
return hook.getName().equals("jenkins")
&& hook.getConfig().get("jenkins_hook_url").equals(url.toExternalForm());
}
@@ -241,7 +285,7 @@ protected boolean applyNullSafe(@Nonnull GHHook hook) {
*/
protected Predicate webhookFor(final URL url) {
return new NullSafePredicate() {
- protected boolean applyNullSafe(@Nonnull GHHook hook) {
+ protected boolean applyNullSafe(@NonNull GHHook hook) {
return hook.getName().equals("web")
&& hook.getConfig().get("url").equals(url.toExternalForm());
}
@@ -254,7 +298,7 @@ protected boolean applyNullSafe(@Nonnull GHHook hook) {
protected Function> eventsFromHook() {
return new NullSafeFunction>() {
@Override
- protected Iterable applyNullSafe(@Nonnull GHHook input) {
+ protected Iterable applyNullSafe(@NonNull GHHook input) {
return input.getEvents();
}
};
@@ -270,7 +314,7 @@ protected Iterable applyNullSafe(@Nonnull GHHook input) {
protected Function> fetchHooks() {
return new NullSafeFunction>() {
@Override
- protected List applyNullSafe(@Nonnull GHRepository repo) {
+ protected List applyNullSafe(@NonNull GHRepository repo) {
try {
return repo.getHooks();
} catch (IOException e) {
@@ -288,9 +332,21 @@ protected List applyNullSafe(@Nonnull GHRepository repo) {
*/
protected Function createWebhook(final URL url, final Set events) {
return new NullSafeFunction() {
- protected GHHook applyNullSafe(@Nonnull GHRepository repo) {
+ protected GHHook applyNullSafe(@NonNull GHRepository repo) {
try {
- return repo.createWebHook(url, events);
+ final HashMap config = new HashMap<>();
+ config.put("url", url.toExternalForm());
+ config.put("content_type", "json");
+
+ // We need to pick a secret to use, so use the first one defined.
+ final Optional secret = GitHubPlugin.configuration().getHookSecretConfigs().stream().
+ map(HookSecretConfig::getHookSecret).filter(Objects::nonNull).findFirst();
+
+ if (secret.isPresent()) {
+ config.put("secret", secret.get().getPlainText());
+ }
+
+ return repo.createHook("web", config, events, true);
} catch (IOException e) {
throw new GHException("Failed to create hook", e);
}
@@ -303,7 +359,7 @@ protected GHHook applyNullSafe(@Nonnull GHRepository repo) {
*/
protected Predicate deleteWebhook() {
return new NullSafePredicate() {
- protected boolean applyNullSafe(@Nonnull GHHook hook) {
+ protected boolean applyNullSafe(@NonNull GHHook hook) {
try {
hook.delete();
return true;
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java
index bee94ab34..95180fddb 100644
--- a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java
@@ -3,15 +3,21 @@
import com.cloudbees.jenkins.GitHubPushTrigger;
import com.cloudbees.jenkins.GitHubRepositoryName;
import com.cloudbees.jenkins.GitHubRepositoryNameContributor;
-import com.cloudbees.jenkins.GitHubTrigger;
+import com.cloudbees.jenkins.GitHubTriggerEvent;
import com.cloudbees.jenkins.GitHubWebHook;
import hudson.Extension;
-import hudson.model.Job;
+import hudson.ExtensionList;
+import hudson.model.Item;
import hudson.security.ACL;
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
import jenkins.model.Jenkins;
-import net.sf.json.JSONObject;
+import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.kohsuke.github.GHEvent;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GitHub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,7 +47,7 @@ public class DefaultPushGHEventSubscriber extends GHEventsSubscriber {
* @return true if project has {@link GitHubPushTrigger}
*/
@Override
- protected boolean isApplicable(Job, ?> project) {
+ protected boolean isApplicable(Item project) {
return withTrigger(GitHubPushTrigger.class).apply(project);
}
@@ -57,16 +63,20 @@ protected Set events() {
* Calls {@link GitHubPushTrigger} in all projects to handle this hook
*
* @param event only PUSH event
- * @param payload payload of gh-event. Never blank
*/
@Override
- protected void onEvent(GHEvent event, String payload) {
- JSONObject json = JSONObject.fromObject(payload);
- String repoUrl = json.getJSONObject("repository").getString("url");
- final String pusherName = json.getJSONObject("pusher").getString("name");
-
- LOGGER.info("Received POST for {}", repoUrl);
- final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl);
+ protected void onEvent(final GHSubscriberEvent event) {
+ GHEventPayload.Push push;
+ try {
+ push = GitHub.offline().parseEventPayload(new StringReader(event.getPayload()), GHEventPayload.Push.class);
+ } catch (IOException e) {
+ LOGGER.warn("Received malformed PushEvent: " + event.getPayload(), e);
+ return;
+ }
+ URL htmlUrl = push.getRepository().getHtmlUrl();
+ final String pusherName = push.getPusher().getName();
+ LOGGER.info("Received PushEvent for {} from {}", htmlUrl, event.getOrigin());
+ final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(htmlUrl.toExternalForm());
if (changedRepository != null) {
// run in high privilege to see all the projects anonymous users don't see.
@@ -75,29 +85,35 @@ protected void onEvent(GHEvent event, String payload) {
ACL.impersonate(ACL.SYSTEM, new Runnable() {
@Override
public void run() {
- for (Job, ?> job : Jenkins.getInstance().getAllItems(Job.class)) {
- GitHubTrigger trigger = triggerFrom(job, GitHubPushTrigger.class);
+ for (Item job : Jenkins.getInstance().getAllItems(Item.class)) {
+ GitHubPushTrigger trigger = triggerFrom(job, GitHubPushTrigger.class);
if (trigger != null) {
- LOGGER.debug("Considering to poke {}", job.getFullDisplayName());
- if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) {
- LOGGER.info("Poked {}", job.getFullDisplayName());
- trigger.onPost(pusherName);
+ String fullDisplayName = job.getFullDisplayName();
+ LOGGER.debug("Considering to poke {}", fullDisplayName);
+ if (GitHubRepositoryNameContributor.parseAssociatedNames(job)
+ .contains(changedRepository)) {
+ LOGGER.info("Poked {}", fullDisplayName);
+ trigger.onPost(GitHubTriggerEvent.create()
+ .withTimestamp(event.getTimestamp())
+ .withOrigin(event.getOrigin())
+ .withTriggeredByUser(pusherName)
+ .build()
+ );
} else {
LOGGER.debug("Skipped {} because it doesn't have a matching repository.",
- job.getFullDisplayName());
+ fullDisplayName);
}
}
}
}
});
- for (GitHubWebHook.Listener listener : Jenkins.getInstance()
- .getExtensionList(GitHubWebHook.Listener.class)) {
+ for (GitHubWebHook.Listener listener : ExtensionList.lookup(GitHubWebHook.Listener.class)) {
listener.onPushRepositoryChanged(pusherName, changedRepository);
}
} else {
- LOGGER.warn("Malformed repo url {}", repoUrl);
+ LOGGER.warn("Malformed repo html url {}", htmlUrl);
}
}
}
diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java
index a5a0007bd..bc7141bf0 100644
--- a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java
+++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java
@@ -2,19 +2,22 @@
import com.cloudbees.jenkins.GitHubRepositoryName;
import hudson.Extension;
-import hudson.model.Job;
-import net.sf.json.JSONObject;
+import hudson.model.Item;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Set;
+import jakarta.inject.Inject;
import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.kohsuke.github.GHEvent;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHOrganization;
+import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.GitHub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.inject.Inject;
-import java.util.Set;
-
import static com.google.common.collect.Sets.immutableEnumSet;
-import static net.sf.json.JSONObject.fromObject;
import static org.kohsuke.github.GHEvent.PING;
/**
@@ -32,14 +35,13 @@ public class PingGHEventSubscriber extends GHEventsSubscriber {
private transient GitHubHookRegisterProblemMonitor monitor;
/**
- * This subscriber is not applicable to any job
+ * This subscriber is not applicable to any item
*
* @param project ignored
- *
* @return always false
*/
@Override
- protected boolean isApplicable(Job, ?> project) {
+ protected boolean isApplicable(Item project) {
return false;
}
@@ -59,15 +61,21 @@ protected Set events() {
*/
@Override
protected void onEvent(GHEvent event, String payload) {
- JSONObject parsedPayload = fromObject(payload);
- JSONObject repository = parsedPayload.optJSONObject("repository");
+ GHEventPayload.Ping ping;
+ try {
+ ping = GitHub.offline().parseEventPayload(new StringReader(payload), GHEventPayload.Ping.class);
+ } catch (IOException e) {
+ LOGGER.warn("Received malformed PingEvent: " + payload, e);
+ return;
+ }
+ GHRepository repository = ping.getRepository();
if (repository != null) {
- LOGGER.info("{} webhook received from repo <{}>!", event, repository.getString("html_url"));
- monitor.resolveProblem(GitHubRepositoryName.create(repository.getString("html_url")));
+ LOGGER.info("{} webhook received from repo <{}>!", event, repository.getHtmlUrl());
+ monitor.resolveProblem(GitHubRepositoryName.create(repository.getHtmlUrl().toExternalForm()));
} else {
- JSONObject organization = parsedPayload.optJSONObject("organization");
+ GHOrganization organization = ping.getOrganization();
if (organization != null) {
- LOGGER.info("{} webhook received from org <{}>!", event, organization.getString("url"));
+ LOGGER.info("{} webhook received from org <{}>!", event, organization.getUrl());
} else {
LOGGER.warn("{} webhook received with unexpected payload", event);
}
diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config_zh_CN.properties b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config_zh_CN.properties
new file mode 100644
index 000000000..5ec971fca
--- /dev/null
+++ b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config_zh_CN.properties
@@ -0,0 +1,23 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Build\ status\ message=\u6784\u5EFA\u72B6\u6001\u6D88\u606F
diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy
index c9a140f5c..768800958 100644
--- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy
+++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy
@@ -4,17 +4,14 @@ import com.cloudbees.jenkins.GitHubPushTrigger
tr {
td(colspan: 4) {
- div(id: 'gh-hooks-warn')
+ def url = descriptor.getCheckMethod('hookRegistered').toCheckUrl()
+ def input = "input[name='${GitHubPushTrigger.class.getName().replace('.', '-')}']"
+
+ div(id: 'gh-hooks-warn',
+ 'data-url': url,
+ 'data-input': input
+ )
}
}
script(src:"${rootURL}${h.getResourcePath()}/plugin/github/js/warning.js")
-script {
- text("""
-InlineWarning.setup({
- id: 'gh-hooks-warn',
- url: ${descriptor.getCheckMethod('hookRegistered').toCheckUrl()},
- input: 'input[name="${GitHubPushTrigger.class.getName().replace(".", "-")}"]'
-}).start();
-""")
-}
diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html
index fd7204221..b1d61d307 100644
--- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html
+++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html
@@ -1 +1,6 @@
-This job will be triggered if jenkins will receive PUSH GitHub hook from repo defined in scm section
\ No newline at end of file
+When Jenkins receives a GitHub push hook, GitHub Plugin checks to see
+whether the hook came from a GitHub repository which matches the Git repository defined in SCM/Git section of this job.
+If they match and this option is enabled, GitHub Plugin triggers a one-time polling on GITScm.
+When GITScm polls GitHub, it finds that there is a change and initiates a build.
+The last sentence describes the behavior of Git plugin,
+thus the polling and initiating the build is not a part of GitHub plugin.
diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy b/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy
index 297388577..0e5ff7150 100644
--- a/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy
+++ b/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy
@@ -9,6 +9,8 @@ if (instance == null) {
instance = new GitHubSetCommitStatusBuilder()
}
+f.dropdownDescriptorSelector(title: _('Commit context: '), field: 'contextSource')
+
f.advanced() {
f.entry(title: _('Build status message'), field: 'statusMessage') {
f.property()
diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_zh_CN.properties b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_zh_CN.properties
new file mode 100644
index 000000000..2deaede1b
--- /dev/null
+++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_zh_CN.properties
@@ -0,0 +1,3 @@
+github.project=GitHub \u9879\u76EE
+github.project.url=\u9879\u76EE URL
+github.build.display.name=\u663E\u793A\u540D\u79F0
diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html
index 9b5def6e0..96299f423 100644
--- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html
+++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html
@@ -1,8 +1,8 @@
This value will be used as context name for
- commit status if status builder or
- status publisher is defined for this project. It should be small and clear.
+ commit status if status builder or
+ status publisher is defined for this project. It should be small and clear.
Enter the URL for the GitHub hosted project (without the tree/master or tree/branch part).
-
+
For example:
- http://github.com/rails/rails for the Rails project.
+ https://github.com/rails/rails for the Rails project.
-
\ No newline at end of file
+
diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html
index c1041b6bc..41700ba59 100644
--- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html
+++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html
@@ -4,6 +4,6 @@
- Zum Beispiel http://github.com/rails/rails für das Rails-Projekt.
+ Zum Beispiel https://github.com/rails/rails für das Rails-Projekt.
-
\ No newline at end of file
+
diff --git a/src/main/resources/images/symbols/logo-github.svg b/src/main/resources/images/symbols/logo-github.svg
new file mode 100644
index 000000000..4c15b0297
--- /dev/null
+++ b/src/main/resources/images/symbols/logo-github.svg
@@ -0,0 +1 @@
+
diff --git a/src/main/resources/lib/github/blockWrapper.jelly b/src/main/resources/lib/github/blockWrapper.jelly
new file mode 100644
index 000000000..d43a2fe51
--- /dev/null
+++ b/src/main/resources/lib/github/blockWrapper.jelly
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties
index 9d0342903..509773102 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties
+++ b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties
@@ -1,7 +1,19 @@
-global.config.url.is.empty=Jenkins URL is empty. Set explicitly Jenkins URL in global configuration or in GitHub plugin configuration to manage hooks.
-global.config.hook.url.is.malformed=Malformed GH hook url in global configuration ({0}). Please check Jenkins URL is valid and ends with slash or use overrided hook url
+global.config.url.is.empty=The Jenkins URL is empty. Explicitly set the Jenkins URL in the global configuration \
+ or in the GitHub plugin configuration to manage webhooks.
+global.config.hook.url.is.malformed=There is a malformed GitHub webhook URL in the global configuration ({0}). \
+ Please ensure that the Jenkins URL is valid and ends with a forward slash or use the webhook URL override.
common.expandable.message.title=Expandable message
hooks.problem.administrative.monitor.displayname=GitHub Hooks Problems
-hooks.problem.administrative.monitor.description=Some of the hooks failed to be registered or were removed. You can view detailed list of them at this page. Also you can manage list of ignored repos.
-github.trigger.check.method.warning.details=Hook for repo {0}/{1} on {2} failed to be registered or were removed. More info can be found on global manage page. This message will be dismissed if Jenkins receives a PING event from repo or repo will be ignored in global configuration.
+hooks.problem.administrative.monitor.description=Some of the webhooks failed to be registered or were removed. \
+ You can view a detailed list of them at this page. Also you can manage the list of ignored repos.
+github.trigger.check.method.warning.details=The webhook for repo {0}/{1} on {2} failed to be registered \
+ or was removed. \
+ More info can be found on the global configuration page. This message will be dismissed if Jenkins receives \
+ a PING event from repo webhook or if you add the repo to the ignore list in the global configuration.
unknown.error=Unknown error
+duplicate.events.administrative.monitor.displayname=GitHub Duplicate Events
+duplicate.events.administrative.monitor.description=Warns about duplicate events received from GitHub.
+duplicate.events.administrative.monitor.blurb=Duplicate events were received from GitHub, possibly due to \
+ misconfiguration (e.g., multiple webhooks targeting the same Jenkins controller at the repository or organization \
+ level), potentially causing redundant builds or at least wasted work. \
+ Click here to inspect the last tracked duplicate event payload.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/description.jelly b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/description.jelly
new file mode 100644
index 000000000..11cde3e78
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/description.jelly
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/message.jelly b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/message.jelly
new file mode 100644
index 000000000..d67740516
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/message.jelly
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.groovy b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.groovy
index dd113d103..9c059da5e 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.groovy
@@ -22,7 +22,19 @@ l.layout(title: _('page.title'), permission: app.ADMINISTER) {
div {
p {
- text(_('help.for.page.and.debug.info'))
+ text(_('help.for.page.and.debug.shows'))
+ text(' ')
+
+ text(_('help.for.page.and.debug.system.pre'))
+ text(' ')
+ a(_('help.for.page.and.debug.system.log'), href: "${rootURL}/log/all")
+ text(_('help.for.page.and.debug.system.suffix'))
+
+ text(' ')
+ text(_('help.for.page.and.debug.log.pre'))
+ text(' ')
+ a(_('help.for.page.and.debug.log.enable'), href: "${rootURL}/log/levels")
+ text(_('help.for.page.and.debug.log.suffix'))
}
ul {
diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.properties b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.properties
index ea6ddf26e..2db4bfbaa 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.properties
+++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/index.properties
@@ -1,13 +1,20 @@
page.title=GitHub Hooks Problems
ignore=Ignore
-disignore=Disignore
+disignore=Unignore
ignored.projects=Ignored Projects
project.header=Project
message.header=Message
-help.for.problems=This table shows problems with registering/removing hooks for corresponding repo. \
- Message will be dismissed if Jenkins will receive PING hook for repo, or if you add this repo to ignore list. This messages will not be saved to the disk, \
- so all of them will be cleared after jenkins restart
-help.for.ignored=This table shows list with ignored projects. Any problem with repos in this list will be declined by administrative monitor. \
- You can remove repo from this list. This list will be saved on each change and reloaded after jenkins restart.
-help.for.page.and.debug.info=This page shows hooks problems and ignored projects. You can view detailed stacktrace of any problem in system log. \
- For better debug in jenkins interface, enable this logs:
+help.for.problems=This table shows any problems with registering/removing repo webhooks. \
+ A message will be dismissed if Jenkins receives a PING event from the corresponding repo webhook, \
+ or if you add the repo to the ignore list. These messages will not be saved to disk, \
+ so they will all be cleared when Jenkins restarts.
+help.for.ignored=This table lists any ignored projects. Any problem with the repos in this list will be declined by \
+ administrative monitor. \
+ You can remove a repo from this list. This list will be saved on each change and reloaded when Jenkins restarts.
+help.for.page.and.debug.shows=This page shows problems with webhooks, and ignored projects.
+help.for.page.and.debug.system.pre=A detailed stacktrace for any of the problems can be found in the
+help.for.page.and.debug.system.log=system log
+help.for.page.and.debug.system.suffix=.
+help.for.page.and.debug.log.pre=For improved debugging in the Jenkins interface,
+help.for.page.and.debug.log.enable=enable these logs
+help.for.page.and.debug.log.suffix=:
diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.groovy b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.groovy
index ce7c1f180..1a993d9a2 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.groovy
@@ -2,10 +2,10 @@ package org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor
def f = namespace(lib.FormTagLib)
-div(class: 'warning') {
+div(class: 'alert alert-warning') {
form(method: 'post', action: "${rootURL}/${my?.url}/act", name: my?.id) {
- text(_('hook.registering.problem'))
f.submit(name: 'yes', value: _('view'))
f.submit(name: 'no', value: _('dismiss'))
}
+ text(_('hook.registering.problem'))
}
diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.properties b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.properties
index 6b027ffc9..231009d1d 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.properties
+++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor/message.properties
@@ -1,3 +1,4 @@
view=View
dismiss=Dismiss
-hook.registering.problem=There are some problems while registering/removing hooks for GitHub. You can view the list of failed repos
+hook.registering.problem=There were some problems while registering or removing one or more GitHub webhooks. \
+ Would you like to view the problems?
diff --git a/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/help-content.html b/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/help-content.html
index e90cbd68f..11eaaf9da 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/help-content.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/help-content.html
@@ -1,4 +1,4 @@
- Message content that will be expanded using core variable expansion i.e. ${WORKSPACE}
+ Message content that will be expanded using core variable expansion i.e. ${WORKSPACE}
and Token Macro Plugin tokens.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy
index 25b3c5b34..96077fbb5 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy
@@ -1,8 +1,10 @@
package org.jenkinsci.plugins.github.config.GitHubPluginConfig
import com.cloudbees.jenkins.GitHubPushTrigger
+import lib.FormTagLib
-def f = namespace(lib.FormTagLib);
+def f = namespace(FormTagLib);
+def g = namespace("/lib/github")
f.section(title: descriptor.displayName) {
f.entry(title: _("GitHub Servers"),
@@ -23,19 +25,32 @@ f.section(title: descriptor.displayName) {
if (GitHubPushTrigger.ALLOW_HOOKURL_OVERRIDE) {
f.entry(title: _("Override Hook URL")) {
- table(width: "100%", style: "margin-left: 7px;") {
- f.optionalBlock(title: _("Specify another hook url for GitHub configuration"),
+ g.blockWrapper {
+ f.optionalBlock(title: _("Specify another hook URL for GitHub configuration"),
+ name: "isOverrideHookUrl",
inline: true,
- field: "overrideHookUrl",
- checked: instance.overrideHookURL) {
+ checked: instance.isOverrideHookUrl()) {
f.entry(field: "hookUrl") {
- f.textbox()
+ f.textbox(checkMethod: "post", name: "hookUrl")
}
}
}
}
}
-
+
+ f.entry(title: _("Shared secrets")) {
+ f.repeatableProperty(
+ field: "hookSecretConfigs",
+ add: _("Add shared secret")
+ ) {
+ f.entry(title: "") {
+ div(align: "right") {
+ f.repeatableDeleteButton()
+ }
+ }
+ }
+ }
+
f.entry(title: _("Additional actions"), help: descriptor.getHelpFile('additional')) {
f.hetero_list(items: [],
addCaption: _("Manage additional GitHub actions"),
@@ -44,4 +59,3 @@ f.section(title: descriptor.displayName) {
}
}
}
-
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties
new file mode 100644
index 000000000..6ddcfbde4
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties
@@ -0,0 +1,33 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+GitHub\ Servers=GitHub \u670D\u52A1\u5668
+Add\ GitHub\ Server=\u6DFB\u52A0 GitHub \u670D\u52A1\u5668
+
+Re-register\ hooks\ for\ all\ jobs=\u7ED9\u6240\u6709\u4EFB\u52A1\u91CD\u65B0\u6CE8\u518C hook
+Scanning\ all\ items...=\u626B\u63CF\u6240\u6709\u7684\u9879\u76EE...
+
+Override\ Hook\ URL=\u8986\u76D6 Hook URL
+Specify\ another\ hook\ URL\ for\ GitHub\ configuration=\u4E3A GitHub \u6307\u5B9A\u53E6\u5916\u4E00\u4E2A Hook URL
+
+Additional\ actions=\u9644\u52A0\u52A8\u4F5C
+Manage\ additional\ GitHub\ actions=\u7BA1\u7406 GitHub \u9644\u52A0\u52A8\u4F5C
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-additional.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-additional.html
index 030669671..de6e3a2a6 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-additional.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-additional.html
@@ -1,4 +1,4 @@
- Additional actions can help you with some routine. For example you can convert your existing login + password
- (stored in credentials or directly) to GitHub personal token.
+ Additional actions can help you with some routines. For example, you can convert your existing login + password
+ (stored in credentials or directly) to a GitHub personal token.
- If your Jenkins runs inside the firewall and not directly reachable from the internet,
+ If your Jenkins runs inside a firewall and is not directly reachable from the internet,
set up a reverse proxy, port tunneling, and so on so that GitHub can deliver a POST request
to your Jenkins at ${app.rootUrl}github-webhook/.
Then specify the URL that GitHub should POST to here.
-
\ No newline at end of file
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help.jelly b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help.jelly
index 36cec9f3d..6203eac96 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help.jelly
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help.jelly
@@ -5,38 +5,32 @@
By default
- This plugin don't do anything with GitHub api unless you add config with credentials.
- So if you don't want to add any config, you can setup hooks for this jenkins instance manually.
+ This plugin doesn't do anything with the GitHub API unless you add a configuration with credentials.
+ So if you don't want to add any configuration, you can set up hooks for this Jenkins instance manually.
- In this mode, in addition to configure projects with "Build when a change is pushed to GitHub",
+ In this mode, in addition to configuring projects with "GitHub hook trigger for GITScm polling",
you need to ensure that Jenkins gets a POST to its
-
- ${app.rootUrl}github-webhook/
-
+ ${app.rootUrl}github-webhook/.
-
If you setup credentials
+
If you set up credentials
- In this mode, Jenkins will add/remove hook URLs to GitHub based on the project configuration of
- Jenkins.
+ In this mode, Jenkins will add/remove hook URLs to GitHub based on the project configuration.
Jenkins has a single post-commit hook URL for all the repositories, and this URL will be added
- to
- all the GitHub repositories Jenkins is interested in. You should provide credentials with scope
- admin:repo_hook
- for every repo which should be managed by Jenkins. It needs to read current list of hooks,
- create new hooks and remove old.
+ to all the GitHub repositories Jenkins is interested in. You should provide credentials with scope
+ admin:repo_hook for every repository which should be managed by Jenkins. It needs to read the
+ current list of hooks, create new hooks and remove old hooks.
- Hook URL is
+ The Hook URL is
${app.rootUrl}github-webhook/
,
and it needs to be accessible from the internet. If you have a firewall and such between
- GitHub
- and Jenkins, you can set up a reverse proxy and override the hook URL that Jenkins registers
- to GitHub,
- by checking "override hook URL" in advanced configuration and specify the URL GitHub should POST to.
+ GitHub and Jenkins, you can set up a reverse proxy and override the hook URL that Jenkins registers
+ to GitHub, by checking "override hook URL" in the advanced configuration and specify to which URL
+ GitHub should POST.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config.groovy
index 354ab71b7..ab649ac49 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config.groovy
@@ -5,13 +5,16 @@ import org.jenkinsci.plugins.github.config.GitHubServerConfig
def f = namespace(lib.FormTagLib);
def c = namespace(lib.CredentialsTagLib)
+f.entry(title: _("Name"), field: "name") {
+ f.textbox()
+}
f.entry(title: _("API URL"), field: "apiUrl") {
f.textbox(default: GitHubServerConfig.GITHUB_URL)
}
f.entry(title: _("Credentials"), field: "credentialsId") {
- c.select()
+ c.select(context:app, includeUser:false, expressionAllowed:false)
}
f.block() {
@@ -23,9 +26,8 @@ f.block() {
)
}
-
-f.entry(title: _("Manage hooks"), field: "manageHooks") {
- f.checkbox(default: true)
+f.entry() {
+ f.checkbox(title: _("Manage hooks"), field: "manageHooks")
}
f.advanced() {
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties
new file mode 100644
index 000000000..6bd83598d
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties
@@ -0,0 +1,28 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Name=\u540D\u79F0
+Credentials=\u51ED\u636E
+Test\ connection=\u8FDE\u63A5\u6D4B\u8BD5
+Testing...=\u6D4B\u8BD5\u4E2D...
+Manage\ hooks=\u7BA1\u7406 Hook
+GitHub\ client\ cache\ size\ (MB)=GitHub \u5BA2\u6237\u7AEF\u7F13\u5B58(MB)
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-apiUrl.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-apiUrl.html
index dd0e7cd2d..dc7f026f7 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-apiUrl.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-apiUrl.html
@@ -1,7 +1,7 @@
API endpoint of a GitHub server.
- To use public github.com, leave this field
+ To use public github.com, leave this field
to the default value of https://api.github.com.
Otherwise if you use GitHub Enterprise, specify its API endpoint here
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-clientCacheSize.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-clientCacheSize.html
index d094e8a94..62137c8e1 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-clientCacheSize.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-clientCacheSize.html
@@ -4,9 +4,9 @@
in $JENKINS_HOME to cache data retrieved from GitHub API calls.
A cache will help improve the performance by avoiding unnecessary data transfer, and by doing so it also
makes it less likely to hit API rate limit
- (by the use of conditional GET calls.)
+ (by the use of conditional GET calls).
- In an unlikely event that cache is causing a problem, set this to 0 to disable cache altogether.
+ In the unlikely event that cache is causing a problem, set this to 0 to disable cache altogether.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-credentialsId.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-credentialsId.html
index cf4e8e9bf..e32edce56 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-credentialsId.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-credentialsId.html
@@ -9,15 +9,15 @@
- In Jenkins create credentials as «Secret Text», provided by
- Plain Credentials Plugin
+ In Jenkins, create credentials as «Secret Text», provided by
+ Plain Credentials Plugin.
- WARN! Creds are filtered on changing custom GitHub url
+ WARNING! Credentials are filtered on changing custom GitHub URL.
- If you have an existing GitHub login and password you can convert it to a token automatically with help of «Manage
- additional GitHub actions»
+ If you have an existing GitHub login and password you can convert it to a token automatically with the help of «Manage
+ additional GitHub actions».
- Is this config will be used to manage creds for repos where it has admin rights?
- If unchecked, this credentials still can be used to manipulate commit statuses, but will be ignored to manage hooks
+ Will this configuration be used to manage credentials for repositories where it has admin rights?
+ If unchecked, this credentials still can be used to manipulate commit statuses, but will be ignored to manage hooks.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-name.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-name.html
new file mode 100644
index 000000000..1f9e5fbdc
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-name.html
@@ -0,0 +1,6 @@
+
+ An optional name to help with the disambiguation of API URLs. If you have multiple GitHub Enterprise servers with non-helpful
+ names such as s21356.example.com and s21368.example.com then giving these names can
+ help users when they need to select the correct server from a drop-down list. If you do not provide a name,
+ then a "best guess" will be made from the hostname part of the API URL.
+
- Pair of GitHub token and server url. If no any custom url specified, then default api.github.com will be used.
+ Pair of GitHub token and server URL. If no custom URL is specified, then the default api.github.com will be used.
If your Jenkins uses multiple repositories that are spread across different
- user accounts, you can list them all here as separate configs.
+ user accounts, you can list them all here as separate configurations.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config.groovy
index cf7996ee6..c60b8bbbc 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config.groovy
@@ -12,7 +12,7 @@ f.entry(title: _("GitHub API URL"), field: "apiUrl",
f.radioBlock(checked: true, name: "creds", value: "plugin", title: "From credentials") {
f.entry(title: _("Credentials"), field: "credentialsId") {
- c.select()
+ c.select(context: app, includeUser: true, expressionAllowed: false)
}
f.block() {
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config_zh_CN.properties
new file mode 100644
index 000000000..e8172ff04
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config_zh_CN.properties
@@ -0,0 +1,26 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+From credentials=\u4ECE\u51ED\u636E
+Credentials=\u51ED\u636E
+Create\ token\ credentials=\u521B\u5EFA token \u51ED\u636E
+Creating...=\u521B\u5EFA\u4E2D...
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/help.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/help.html
index 69a3674af..66500d136 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/help.html
@@ -1,8 +1,8 @@
- Helper to convert existing username-password credentials or directly login+password to
+ Helper to convert existing username-password credentials or directly login+password to a
GitHub personal token.
- This helper don't stores any entered data, but only registers token with all scopes needed to plugin.
- After token registration it will be stored as «Secret text» credentials with domain requirements corresponding to
- given api url. It will be available after refreshing the global config page
+ This helper doesn't store any entered data, but only registers a new token with all scopes needed to plugin.
+ After token registration, it will be stored as «Secret text» credentials with domain requirements corresponding to
+ given API URL. It will be available after refreshing the Global Confirmation page.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy
new file mode 100644
index 000000000..2e5cce9ff
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy
@@ -0,0 +1,12 @@
+package org.jenkinsci.plugins.github.config.HookSecretConfig
+
+def f = namespace(lib.FormTagLib);
+def c = namespace(lib.CredentialsTagLib);
+
+f.entry(title: _("Shared secret"), field: "credentialsId", help: descriptor.getHelpFile('sharedSecret')) {
+ c.select(context: app, includeUser: false, expressionAllowed: false)
+}
+
+f.entry(title: _("Signature algorithm"), field: "signatureAlgorithm") {
+ f.select()
+}
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config_zh_CN.properties
new file mode 100644
index 000000000..e9958e627
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config_zh_CN.properties
@@ -0,0 +1,24 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Shared\ secret=\u5171\u4EAB Secret
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-sharedSecret.html b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-sharedSecret.html
new file mode 100644
index 000000000..17cd59cb5
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-sharedSecret.html
@@ -0,0 +1,5 @@
+
+ A shared secret token GitHub will use to sign requests in order for Jenkins to verify that the request came from GitHub.
+ If left blank, this feature will not be used.
+ Please use a different token from the token secret.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-signatureAlgorithm.html b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-signatureAlgorithm.html
new file mode 100644
index 000000000..5092fb6d9
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-signatureAlgorithm.html
@@ -0,0 +1,13 @@
+
+
Choose the signature algorithm for webhook validation:
+
+
SHA-256 (Recommended): Modern, secure HMAC signature validation using the
+ X-Hub-Signature-256 header. This is GitHub's recommended approach for enhanced security.
+
SHA-1 (Legacy): Legacy HMAC signature validation using the
+ X-Hub-Signature header. Only use this for existing webhooks during migration period.
+
+
Note: When changing algorithms, ensure your GitHub webhook configuration uses the corresponding
+ signature header (X-Hub-Signature-256 for SHA-256 or X-Hub-Signature for SHA-1).
+
System Property Override: The default algorithm can be overridden using the system property
+ -Djenkins.github.webhook.signature.default=SHA1 for backwards compatibility with legacy CI environments.
+
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/config/Messages.properties
new file mode 100644
index 000000000..63f7db6ac
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/config/Messages.properties
@@ -0,0 +1 @@
+GitHubServerConfig.displayName={0} ({1})
diff --git a/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config_zh_CN.properties
new file mode 100644
index 000000000..cd38978f6
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config_zh_CN.properties
@@ -0,0 +1,24 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Status=\u72B6\u6001
+Message=\u6D88\u606F
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config.groovy
index 2b807f165..c059c8f05 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config.groovy
@@ -14,6 +14,7 @@ f.section(title: _('Where:')) {
f.section(title: _('What:')) {
f.dropdownDescriptorSelector(title: _('Commit context: '), field: 'contextSource')
f.dropdownDescriptorSelector(title: _('Status result: '), field: 'statusResultSource')
+ f.dropdownDescriptorSelector(title: _('Status backref: '), field: 'statusBackrefSource')
}
f.advanced {
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config_zh_CN.properties
new file mode 100644
index 000000000..72661bac2
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config_zh_CN.properties
@@ -0,0 +1,25 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Advanced:=\u9AD8\u7EA7\uFF1A
+Handle\ errors=\u9519\u8BEF\u5904\u7406
+Add\ error\ handler=\u6DFB\u52A0\u9519\u8BEF\u5904\u7406
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/help.html
index a969a0037..2392a39ce 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/help.html
@@ -1,3 +1,3 @@
- Using GitHub status api sets status of the commit
-
\ No newline at end of file
+ Using GitHub status api sets status of the commit.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config_zh_CN.properties
new file mode 100644
index 000000000..cfeaefd5d
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config_zh_CN.properties
@@ -0,0 +1,23 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Result\ on\ failure=\u5931\u8D25\u7ED3\u679C
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/help.html
index 06ec1a2a4..545795ea5 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/help.html
@@ -1,3 +1,3 @@
- Any repository provided by the programmatic contributors list
-
\ No newline at end of file
+ Any repository provided by the programmatic contributors list.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/help.html
index 3ef306832..52941d500 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/help.html
@@ -1,3 +1,3 @@
- Uses data-action (located at ${build.url}/git/) to determine actual SHA
-
\ No newline at end of file
+ Uses data-action (located at ${build.url}/git/) to determine actual SHA.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource/config.groovy
new file mode 100644
index 000000000..4f8a98388
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource/config.groovy
@@ -0,0 +1,7 @@
+package org.jenkinsci.plugins.github.status.sources.BuildRefBackrefSource
+
+
+def f = namespace(lib.FormTagLib);
+
+f.helpLink(url: descriptor.getHelpFile())
+f.helpArea()
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource/help.html
new file mode 100644
index 000000000..5201f8800
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSource/help.html
@@ -0,0 +1,3 @@
+
+ Points commit status backref back to the producing build page.
+
- You can define in which cases you want to publish exact state and message for the commit. You can define multiply cases.
+ You can define in which cases you want to publish exact state and message for the commit. You can define multiple cases.
First match (starting from top) wins. If no one matches, PENDING status + warn message will be used.
-
\ No newline at end of file
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/help.html
index 41cfb814a..d8c9f3e0d 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/help.html
@@ -1,3 +1,3 @@
- Uses display name property defined in "Github project property" with fallback to job name.
-
\ No newline at end of file
+ Uses display name property defined in "GitHub project property" with fallback to job name.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/help.html
index d9a7ebf49..d2bea2b45 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/help.html
@@ -1,3 +1,3 @@
- Writes simple message about build result and duration
-
\ No newline at end of file
+ Writes simple message about build result and duration.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/config.groovy
new file mode 100644
index 000000000..1340398e3
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/config.groovy
@@ -0,0 +1,8 @@
+package org.jenkinsci.plugins.github.status.sources.ManuallyEnteredBackrefSource
+
+
+def f = namespace(lib.FormTagLib);
+
+f.entry(title: _('Backref URL'), field: 'backref') {
+ f.textbox()
+}
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/help-backref.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/help-backref.html
new file mode 100644
index 000000000..4528d2bcb
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/help-backref.html
@@ -0,0 +1,3 @@
+
+ A backref URL. Allows env vars and token macro.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/help.html
new file mode 100644
index 000000000..9dfe523d5
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredBackrefSource/help.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
+ Allows env vars and token macros.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help.html
index 1b6bd211e..fb102e2be 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help.html
@@ -1,3 +1,3 @@
- You can define context name manually
-
\ No newline at end of file
+ You can define context name manually.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help-sha.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help-sha.html
index da5ec9ebc..215946abf 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help-sha.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help-sha.html
@@ -1,3 +1,3 @@
- Allows env vars and token macro
-
\ No newline at end of file
+ Allows env vars and token macro.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help.html
index 9829ba7da..51e2d457e 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help.html
@@ -1,3 +1,3 @@
- Allows to define commit sha manually
-
\ No newline at end of file
+ Allows to define commit SHA manually.
+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config_zh_CN.properties
new file mode 100644
index 000000000..cd38978f6
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config_zh_CN.properties
@@ -0,0 +1,24 @@
+# The MIT License
+#
+# Copyright (c) 2018, suren
+#
+# 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.
+
+Status=\u72B6\u6001
+Message=\u6D88\u606F
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/help-message.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/help-message.html
index da5ec9ebc..215946abf 100644
--- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/help-message.html
+++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/help-message.html
@@ -1,3 +1,3 @@