From 58a61481fac3b9d920e233639a1a605fa65ad38b Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Fri, 20 Feb 2015 18:42:45 +0300 Subject: [PATCH 001/228] [FIXED JENKINS-23995] Set commit status context --- pom.xml | 2 +- src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java | 2 +- .../com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 24c807ac7..f04057c3a 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ org.jenkins-ci.plugins github-api - 1.42 + 1.59 org.jenkins-ci.plugins diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index 31cac448e..6c169142b 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -124,7 +124,7 @@ private void updateCommitStatus(@Nonnull AbstractBuild build, @Nonnull Bui } listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getUrl() + "/commit/" + sha1)); - repository.createCommitStatus(sha1, state, build.getAbsoluteUrl(), msg); + repository.createCommitStatus(sha1, state, build.getAbsoluteUrl(), msg, build.getProject().getFullName()); } } } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 9c7258362..573df855e 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -29,7 +29,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { for (GHRepository repository : name.resolve()) { listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getUrl() + "/commit/" + sha1)); - repository.createCommitStatus(sha1, GHCommitState.PENDING, build.getAbsoluteUrl(), Messages.CommitNotifier_Pending(build.getDisplayName())); + repository.createCommitStatus(sha1, GHCommitState.PENDING, build.getAbsoluteUrl(), Messages.CommitNotifier_Pending(build.getDisplayName()), build.getProject().getFullName()); } } return true; From 00525dd547fc74b896cde41fd7b6050d9d554e25 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Wed, 25 Feb 2015 13:12:21 +0300 Subject: [PATCH 002/228] [maven-release-plugin] prepare release github-1.11 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f04057c3a..f0961b59b 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11-SNAPSHOT + 1.11 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ 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 + github-1.11 From 27a9d6ffd2f58bf5fe6351ae47483fda6f666044 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Wed, 25 Feb 2015 13:12:26 +0300 Subject: [PATCH 003/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f0961b59b..3ee3aff25 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11 + 1.12-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.11 + HEAD From 2a49bac65fb7e05cd083e8b445ad2fcfb689ae0a Mon Sep 17 00:00:00 2001 From: Ryan Gardner Date: Fri, 20 Mar 2015 13:36:31 -0400 Subject: [PATCH 004/228] Address JENKINS-25127 by using getAllIItems instead of getItems --- src/main/java/com/cloudbees/jenkins/Cleaner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index eed651658..74eb6473c 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -52,7 +52,7 @@ protected void doRun() throws Exception { } // subtract all the live repositories - for (AbstractProject job : Hudson.getInstance().getItems(AbstractProject.class)) { + for (AbstractProject job : Hudson.getInstance().getAllItems(AbstractProject.class)) { GitHubPushTrigger trigger = job.getTrigger(GitHubPushTrigger.class); if (trigger!=null) { names.removeAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); From 5fd940ceacc45282ec519321b12ae071b8c3af5f Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Thu, 26 Mar 2015 14:02:05 +0300 Subject: [PATCH 005/228] [maven-release-plugin] prepare release github-1.11.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3ee3aff25..392aa49a3 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.12-SNAPSHOT + 1.11.1 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ 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 + github-1.11.1 From 6fae140d86787a811096cf5e1177d4deb5d28310 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Thu, 26 Mar 2015 14:02:10 +0300 Subject: [PATCH 006/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 392aa49a3..d7ecd0613 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11.1 + 1.11.2-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.11.1 + HEAD From a0145d2f62ae138be429cba6f68fdaf5469bdeba Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Fri, 27 Mar 2015 21:46:47 +0300 Subject: [PATCH 007/228] Simplify for loop readability --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index e6476dae3..2d18644be 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -147,12 +147,12 @@ public void start(AbstractProject project, boolean newInstance) { getDescriptor().queue.execute(new Runnable() { public void run() { LOGGER.log(Level.INFO, "Adding GitHub webhooks for {0}", names); - OUTER: + for (GitHubRepositoryName name : names) { for (GHRepository repo : name.resolve()) { try { if(createJenkinsHook(repo, getDescriptor().getHookUrl())) { - continue OUTER; + break; } } catch (Throwable e) { LOGGER.log(Level.WARNING, "Failed to add GitHub webhook for "+name, e); From c98e36f64c20bad70e4ad6ac7133e2f17f2e4b63 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Fri, 27 Mar 2015 22:30:41 +0300 Subject: [PATCH 008/228] Move to separate method to have ability re-register hooks --- .../cloudbees/jenkins/GitHubPushTrigger.java | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 2d18644be..e0a6c8a0e 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -141,27 +141,31 @@ public Set getGitHubRepositories() { public void start(AbstractProject project, boolean newInstance) { super.start(project, newInstance); if (newInstance && getDescriptor().isManageHook()) { - // make sure we have hooks installed. do this lazily to avoid blocking the UI thread. - final Collection names = GitHubRepositoryNameContributor.parseAssociatedNames(job); - - getDescriptor().queue.execute(new Runnable() { - public void run() { - LOGGER.log(Level.INFO, "Adding GitHub webhooks for {0}", names); - - for (GitHubRepositoryName name : names) { - for (GHRepository repo : name.resolve()) { - try { - if(createJenkinsHook(repo, getDescriptor().getHookUrl())) { - break; - } - } catch (Throwable e) { - LOGGER.log(Level.WARNING, "Failed to add GitHub webhook for "+name, e); + registerHooks(); + } + } + + public void registerHooks() { + // make sure we have hooks installed. do this lazily to avoid blocking the UI thread. + final Collection names = GitHubRepositoryNameContributor.parseAssociatedNames(job); + + getDescriptor().queue.execute(new Runnable() { + public void run() { + LOGGER.log(Level.INFO, "Adding GitHub webhooks for {0}", names); + + for (GitHubRepositoryName name : names) { + for (GHRepository repo : name.resolve()) { + try { + if(createJenkinsHook(repo, getDescriptor().getHookUrl())) { + break; } + } catch (Throwable e) { + LOGGER.log(Level.WARNING, "Failed to add GitHub webhook for "+name, e); } } } - }); - } + } + }); } private boolean createJenkinsHook(GHRepository repo, URL url) { From 6154cedfe18f265aea25756284a3472696eceaff Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Fri, 27 Mar 2015 22:56:28 +0300 Subject: [PATCH 009/228] [JENKINS-25127] Add Button for registering all hooks --- .../cloudbees/jenkins/GitHubPushTrigger.java | 38 +++++++++++++++++++ .../jenkins/GitHubPushTrigger/global.jelly | 1 + 2 files changed, 39 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index e0a6c8a0e..417d65d4d 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -33,6 +33,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.codec.binary.Base64; @@ -145,6 +146,11 @@ public void start(AbstractProject project, boolean newInstance) { } } + /** + * Tries to register hook for current associated job. + * Useful for using from groovy scripts. + * @since 1.11.2 + */ public void registerHooks() { // make sure we have hooks installed. do this lazily to avoid blocking the UI thread. final Collection names = GitHubRepositoryNameContributor.parseAssociatedNames(job); @@ -232,6 +238,7 @@ public void writeLogTo(XMLOutput out) throws IOException { @Extension public static class DescriptorImpl extends TriggerDescriptor { + private static final Logger LOGGER = Logger.getLogger(DescriptorImpl.class.getName()); private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(MasterComputer.threadPoolForRemoting); private boolean manageHook; @@ -324,6 +331,37 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) { } + public FormValidation doReRegister() { + if (!manageHook) { + return FormValidation.error("Works only when Jenkins manages hooks"); + } + + int triggered = 0; + for (AbstractProject job : getJenkinsInstance().getAllItems(AbstractProject.class)) { + if (!job.isBuildable()) { + continue; + } + + GitHubPushTrigger trigger = job.getTrigger(GitHubPushTrigger.class); + if (trigger!=null) { + LOGGER.log(Level.FINE, "Calling registerHooks() for {0}", job.getFullName()); + trigger.registerHooks(); + triggered++; + } + } + + LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", triggered); + return FormValidation.ok("Called re-register hooks for " + triggered + " jobs"); + } + + public static final Jenkins getJenkinsInstance() throws IllegalStateException { + Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + throw new IllegalStateException("Jenkins has not been started, or was already shut down"); + } + return instance; + } + public static DescriptorImpl get() { return Trigger.all().get(DescriptorImpl.class); } diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly index f6494fa98..3f1802c25 100644 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly +++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly @@ -31,5 +31,6 @@ + \ No newline at end of file From 8e053ecd4c239ef4af7082aed23cee0d54c7482f Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Tue, 31 Mar 2015 23:10:06 +0300 Subject: [PATCH 010/228] [maven-release-plugin] prepare release github-1.11.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d7ecd0613..fa9c5d20f 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11.2-SNAPSHOT + 1.11.2 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ 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 + github-1.11.2 From 20fad8e3254b8142ff40f9d5250a867e5b8462e8 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Tue, 31 Mar 2015 23:10:12 +0300 Subject: [PATCH 011/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fa9c5d20f..29f6a0993 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11.2 + 1.11.3-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.11.2 + HEAD From 163aceec56ff5ff6879f1e42d36760949795bb92 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 13 Apr 2015 23:41:04 -0700 Subject: [PATCH 012/228] Bumping up to github-api 1.67 ... to deal with backward incompatible API change in github-api. The getUrl() method has changed in some of those methods to return API URL as opposed to HTML URL. This change fixes that. --- pom.xml | 2 +- .../com/cloudbees/jenkins/GitHubCommitNotifier.java | 2 +- .../com/cloudbees/jenkins/GitHubRepositoryName.java | 12 ++++-------- .../jenkins/GitHubSetCommitStatusBuilder.java | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 29f6a0993..341038429 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ org.jenkins-ci.plugins github-api - 1.59 + 1.67 org.jenkins-ci.plugins diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index 6c169142b..77f2daf40 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -123,7 +123,7 @@ private void updateCommitStatus(@Nonnull AbstractBuild build, @Nonnull Bui msg = Messages.CommitNotifier_Failed(build.getDisplayName(), duration); } - listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getUrl() + "/commit/" + sha1)); + listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1)); repository.createCommitStatus(sha1, state, build.getAbsoluteUrl(), msg, build.getProject().getFullName()); } } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 22f3f83d8..63012c9a5 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -138,13 +138,9 @@ protected boolean filter(V v) { * Does this repository match the repository referenced in the given {@link GHCommitPointer}? */ public boolean matches(GHCommitPointer commit) { - try { - return userName.equals(commit.getUser().getLogin()) - && repositoryName.equals(commit.getRepository().getName()) - && host.equals(new URL(commit.getRepository().getUrl()).getHost()); - } catch (MalformedURLException e) { - return false; - } + return userName.equals(commit.getUser().getLogin()) + && repositoryName.equals(commit.getRepository().getName()) + && host.equals(commit.getRepository().getHtmlUrl().getHost()); } /** @@ -153,7 +149,7 @@ public boolean matches(GHCommitPointer commit) { public boolean matches(GHRepository repo) throws IOException { return userName.equals(repo.getOwner().getLogin()) // TODO: use getOwnerName && repositoryName.equals(repo.getName()) - && host.equals(new URL(repo.getUrl()).getHost()); + && host.equals(repo.getHtmlUrl().getHost()); } @Override diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 573df855e..0023fdbaa 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -28,7 +28,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { for (GHRepository repository : name.resolve()) { - listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getUrl() + "/commit/" + sha1)); + listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1)); repository.createCommitStatus(sha1, GHCommitState.PENDING, build.getAbsoluteUrl(), Messages.CommitNotifier_Pending(build.getDisplayName()), build.getProject().getFullName()); } } From b2e2e6b3469f1bc2dce9423e115fbda193b59646 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 13 Apr 2015 23:44:33 -0700 Subject: [PATCH 013/228] [maven-release-plugin] prepare release github-1.11.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 341038429..979f9d715 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11.3-SNAPSHOT + 1.11.3 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ 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 + github-1.11.3 From e5e5674f03dea7b0d18a7de515c1a15bb2b484ab Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 13 Apr 2015 23:44:36 -0700 Subject: [PATCH 014/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 979f9d715..6cf8f0b69 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11.3 + 1.11.4-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -82,7 +82,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.11.3 + HEAD From 5287c6c8e76d2bc18e86372f5d40179964a80d2f Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Tue, 16 Jun 2015 18:45:39 +0300 Subject: [PATCH 015/228] fix for build and run with jdk8 (JENKINS-18537) --- pom.xml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 6cf8f0b69..a55d659b5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,8 @@ org.jenkins-ci.plugins plugin - 1.532 + + 1.554.1 com.coravy.hudson.plugins.github @@ -105,13 +106,20 @@ + + 3.3 + 2.5.1 + + - - org.apache.maven.plugins - maven-release-plugin - 2.5 - + + maven-compiler-plugin + + 1.6 + 1.6 + + From 1996e97ba3c9470909c8aaddb867e891c97cc557 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Tue, 16 Jun 2015 19:12:59 +0300 Subject: [PATCH 016/228] cosmetic changes - spaces + slf4j in GitHubWebHook class --- .../cloudbees/jenkins/GitHubPushTrigger.java | 33 +++++----- .../GitHubRepositoryNameContributor.java | 9 ++- .../com/cloudbees/jenkins/GitHubWebHook.java | 64 +++++++++---------- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 417d65d4d..32a5c7811 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -52,7 +52,7 @@ * * @author Kohsuke Kawaguchi */ -public class GitHubPushTrigger extends Trigger> implements GitHubTrigger { +public class GitHubPushTrigger extends Trigger> implements GitHubTrigger { @DataBoundConstructor public GitHubPushTrigger() { } @@ -78,13 +78,14 @@ private boolean runPolling() { try { PrintStream logger = listener.getLogger(); long start = System.currentTimeMillis(); - logger.println("Started on "+ DateFormat.getDateTimeInstance().format(new Date())); + logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date())); boolean result = job.poll(listener).hasChanges(); - logger.println("Done. Took "+ Util.getTimeSpanString(System.currentTimeMillis()-start)); - if(result) + logger.println("Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start)); + if (result) { logger.println("Changes found"); - else + } else { logger.println("No changes"); + } return result; } catch (Error e) { e.printStackTrace(listener.error("Failed to record SCM polling")); @@ -105,7 +106,7 @@ private boolean runPolling() { public void run() { if (runPolling()) { - String name = " #"+job.getNextBuildNumber(); + String name = " #" + job.getNextBuildNumber(); GitHubPushCause cause; try { cause = new GitHubPushCause(getLogFile(), pushBy); @@ -127,19 +128,19 @@ public void run() { * Returns the file that records the last/current polling activity. */ public File getLogFile() { - return new File(job.getRootDir(),"github-polling.log"); + return new File(job.getRootDir(), "github-polling.log"); } /** - * @deprecated - * Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)} + * @deprecated Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)} */ + @Deprecated public Set getGitHubRepositories() { return Collections.emptySet(); } @Override - public void start(AbstractProject project, boolean newInstance) { + public void start(AbstractProject project, boolean newInstance) { super.start(project, newInstance); if (newInstance && getDescriptor().isManageHook()) { registerHooks(); @@ -200,14 +201,14 @@ public Collection getProjectActions() { @Override public DescriptorImpl getDescriptor() { - return (DescriptorImpl)super.getDescriptor(); + return (DescriptorImpl) super.getDescriptor(); } /** * Action object for {@link Project}. Used to display the polling log. */ public final class GitHubWebHookPollingAction implements Action { - public AbstractProject getOwner() { + public AbstractProject getOwner() { return job; } @@ -282,7 +283,7 @@ public URL getHookUrl() throws MalformedURLException { } public boolean hasOverrideURL() { - return hookUrl!=null; + return hookUrl != null; } public List getCredentials() { @@ -298,7 +299,7 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti } else { hookUrl = null; } - credentials = req.bindJSONToList(Credential.class,hookMode.get("credentials")); + credentials = req.bindJSONToList(Credential.class, hookMode.get("credentials")); save(); return true; } @@ -313,7 +314,7 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) { return FormValidation.error("Got "+con.getResponseCode()+" from "+value); } String v = con.getHeaderField(GitHubWebHook.X_INSTANCE_IDENTITY); - if (v==null) { + 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 " + value + " is talking to any Jenkins. Are you running your own app?"); } @@ -374,7 +375,7 @@ public static boolean allowsHookUrlOverride() { /** * Set to false to prevent the user from overriding the hook URL. */ - public static boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(GitHubPushTrigger.class.getName()+".disableOverride"); + public static boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(GitHubPushTrigger.class.getName() + ".disableOverride"); private static final Logger LOGGER = Logger.getLogger(GitHubPushTrigger.class.getName()); } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java index 1fa73ac8b..fc4cc2e95 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java @@ -36,10 +36,11 @@ public static ExtensionList all() { return Jenkins.getInstance().getExtensionList(GitHubRepositoryNameContributor.class); } - public static Collection parseAssociatedNames(AbstractProject job) { + public static Collection parseAssociatedNames(AbstractProject job) { Set names = new HashSet(); - for (GitHubRepositoryNameContributor c : all()) - c.parseAssociatedNames(job,names); + for (GitHubRepositoryNameContributor c : all()) { + c.parseAssociatedNames(job, names); + } return names; } @@ -77,6 +78,7 @@ protected static void addRepositories(SCM scm, EnvVars env, Collection job, Collection result) { @@ -88,6 +90,7 @@ public void parseAssociatedNames(AbstractProject job, Collection login(String host, String userName) { // if the username is not an organization, we should have the right user account on file for (Credential c : l) { - if (c.username.equals(userName)) + if (c.username.equals(userName)) { try { return Collections.singleton(c.login()); } catch (IOException e) { - LOGGER.log(WARNING,"Failed to login with username="+c.username,e); + LOGGER.warn("Failed to login with username={}", c.username, e); return Collections.emptyList(); } + } } // otherwise try all the credentials since we don't know which one would work return new Iterable() { public Iterator iterator() { return new FilterIterator( - new AdaptedIterator(l) { - protected GitHub adapt(Credential c) { - try { - return c.login(); - } catch (IOException e) { - LOGGER.log(WARNING,"Failed to login with username="+c.username,e); - return null; + new AdaptedIterator(l) { + protected GitHub adapt(Credential c) { + try { + return c.login(); + } catch (IOException e) { + LOGGER.warn("Failed to login with username={}", c.username, e); + return null; + } } - } - }) { + }) { protected boolean filter(GitHub g) { - return g!=null; + return g != null; } }; } @@ -154,10 +153,10 @@ protected boolean filter(GitHub g) { */ @RequirePOST public void doIndex(StaplerRequest req, StaplerResponse rsp) { - if (req.getHeader(URL_VALIDATION_HEADER)!=null) { + if (req.getHeader(URL_VALIDATION_HEADER) != null) { // when the configuration page provides the self-check button, it makes a request with this header. RSAPublicKey key = identity.getPublic(); - rsp.setHeader(X_INSTANCE_IDENTITY,new String(Base64.encodeBase64(key.getEncoded()))); + rsp.setHeader(X_INSTANCE_IDENTITY, new String(Base64.encodeBase64(key.getEncoded()))); rsp.setStatus(200); return; } @@ -169,7 +168,7 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) { throw new IllegalArgumentException("Not intended to be browsed interactively (must specify payload parameter). " + "Make sure payload version is 'application/vnd.github+form'."); } - processGitHubPayload(payload,GitHubPushTrigger.class); + processGitHubPayload(payload, GitHubPushTrigger.class); } else if (eventType != null && !eventType.isEmpty()) { throw new IllegalArgumentException("Github Webhook event of type " + eventType + " is not supported. " + "Only push events are current supported"); @@ -180,7 +179,7 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) { if (payload == null) { throw new IllegalArgumentException("Not intended to be browsed interactively (must specify payload parameter)"); } - processGitHubPayload(payload,GitHubPushTrigger.class); + processGitHubPayload(payload, GitHubPushTrigger.class); } } @@ -189,13 +188,13 @@ public void processGitHubPayload(String payload, Class> tri String repoUrl = o.getJSONObject("repository").getString("url"); // something like 'https://github.com/kohsuke/foo' String pusherName = o.getJSONObject("pusher").getString("name"); - LOGGER.info("Received POST for "+repoUrl); - LOGGER.fine("Full details of the POST was "+o.toString()); + LOGGER.info("Received POST for {}", repoUrl); + LOGGER.debug("Full details of the POST was {}", o.toString()); Matcher matcher = REPOSITORY_NAME_PATTERN.matcher(repoUrl); if (matcher.matches()) { GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); if (changedRepository == null) { - LOGGER.warning("Malformed repo url "+repoUrl); + LOGGER.warn("Malformed repo url {}", repoUrl); return; } @@ -205,25 +204,25 @@ public void processGitHubPayload(String payload, Class> tri Authentication old = SecurityContextHolder.getContext().getAuthentication(); SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); try { - for (AbstractProject job : Hudson.getInstance().getAllItems(AbstractProject.class)) { + for (AbstractProject job : Jenkins.getInstance().getAllItems(AbstractProject.class)) { GitHubTrigger trigger = (GitHubTrigger) job.getTrigger(triggerClass); - if (trigger!=null) { - LOGGER.fine("Considering to poke "+job.getFullDisplayName()); + if (trigger != null) { + LOGGER.debug("Considering to poke {}", job.getFullDisplayName()); if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) { - LOGGER.info("Poked "+job.getFullDisplayName()); + LOGGER.info("Poked {}", job.getFullDisplayName()); trigger.onPost(pusherName); } else - LOGGER.fine("Skipped "+job.getFullDisplayName()+" because it doesn't have a matching repository."); + LOGGER.debug("Skipped {} because it doesn't have a matching repository.", job.getFullDisplayName()); } } } finally { SecurityContextHolder.getContext().setAuthentication(old); } - for (Listener listener: Jenkins.getInstance().getExtensionList(Listener.class)) { + for (Listener listener : Jenkins.getInstance().getExtensionList(Listener.class)) { listener.onPushRepositoryChanged(pusherName, changedRepository); } } else { - LOGGER.warning("Malformed repo url "+repoUrl); + LOGGER.warn("Malformed repo url {}", repoUrl); } } @@ -234,10 +233,10 @@ public void processGitHubPayload(String payload, Class> tri /*package*/ static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation"; /*package*/ static final String X_INSTANCE_IDENTITY = "X-Instance-Identity"; - private static final Logger LOGGER = Logger.getLogger(GitHubWebHook.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubWebHook.class); public static GitHubWebHook get() { - return Hudson.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class); + return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class); } /** @@ -252,6 +251,7 @@ public static abstract class Listener implements ExtensionPoint { * * @param pusherName the pusher name. * @param changedRepository the changed repository. + * * @since 1.8 */ public abstract void onPushRepositoryChanged(String pusherName, GitHubRepositoryName changedRepository); From 45a2229aa954d8d613e655d88322b9a88a88e996 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 17 Jun 2015 03:28:12 +0300 Subject: [PATCH 017/228] copypaste from guava's fluent iterable class (for not to update full guava) --- .../github/util/FluentIterableWrapper.java | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java diff --git a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java new file mode 100644 index 000000000..133082bb5 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2008 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jenkinsci.plugins.github.util; + + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +import javax.annotation.CheckReturnValue; +import java.util.Iterator; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Mostly copypaste from guava's FluentIterable + */ +public abstract class FluentIterableWrapper implements Iterable { + private final Iterable iterable; + + FluentIterableWrapper(Iterable iterable) { + this.iterable = checkNotNull(iterable); + } + + @Override + public Iterator iterator() { + return iterable.iterator(); + } + + /** + * Returns a fluent iterable that wraps {@code iterable}, or {@code iterable} itself if it + * is already a {@code FluentIterable}. + */ + public static FluentIterableWrapper from(final Iterable iterable) { + return (iterable instanceof FluentIterableWrapper) + ? (FluentIterableWrapper) iterable + : new FluentIterableWrapper(iterable) {}; + } + + /** + * Returns a fluent iterable whose iterators traverse first the elements of this fluent iterable, + * followed by those of {@code other}. The iterators are not polled until necessary. + * + *

The returned iterable's {@code Iterator} supports {@code remove()} when the corresponding + * {@code Iterator} supports it. + */ + @CheckReturnValue + public final FluentIterableWrapper append(Iterable other) { + return from(Iterables.concat(iterable, other)); + } + + /** + * Returns the elements from this fluent iterable that satisfy a predicate. The + * resulting fluent iterable's iterator does not support {@code remove()}. + */ + @CheckReturnValue + public final FluentIterableWrapper filter(Predicate predicate) { + return from(Iterables.filter(iterable, predicate)); + } + + /** + * Returns a fluent iterable that applies {@code function} to each element of this + * fluent iterable. + * + *

The returned fluent iterable's iterator supports {@code remove()} if this iterable's + * iterator does. After a successful {@code remove()} call, this fluent iterable no longer + * contains the corresponding element. + */ + public final FluentIterableWrapper transform(Function function) { + return from(Iterables.transform(iterable, function)); + } + + /** + * Applies {@code function} to each element of this fluent iterable and returns + * a fluent iterable with the concatenated combination of results. {@code function} + * returns an Iterable of results. + * + *

The returned fluent iterable's iterator supports {@code remove()} if this + * function-returned iterables' iterator does. After a successful {@code remove()} call, + * the returned fluent iterable no longer contains the corresponding element. + */ + public FluentIterableWrapper transformAndConcat( + Function> function) { + return from(Iterables.concat(transform(function))); + } + + /** + * Returns an {@link Optional} containing the first element in this fluent iterable that + * satisfies the given predicate, if such an element exists. + * + *

Warning: avoid using a {@code predicate} that matches {@code null}. If {@code null} + * is matched in this fluent iterable, a {@link NullPointerException} will be thrown. + */ + public final Optional firstMatch(Predicate predicate) { + return Iterables.tryFind(iterable, predicate); + } + + /** + * Returns list from wrapped iterable + */ + public List toList() { + return Lists.newArrayList(iterable); + } + + /** + * Returns an {@code ImmutableSet} containing all of the elements from this fluent iterable with + * duplicates removed. + * + */ + public final ImmutableSet toSet() { + return ImmutableSet.copyOf(iterable); + } + + +} From a38c6ef6d97f9fbffd4dda22551747578042b8e5 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 17 Jun 2015 03:30:33 +0300 Subject: [PATCH 018/228] new classes to work with hooks - manager, ext point and job utility --- pom.xml | 7 + .../plugins/github/util/JobInfoHelpers.java | 60 ++++ .../github/webhook/GHEventsListener.java | 77 +++++ .../github/webhook/WebhookManager.java | 293 ++++++++++++++++++ 4 files changed, 437 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java diff --git a/pom.xml b/pom.xml index a55d659b5..4807f0bcc 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,13 @@ + + + org.slf4j + slf4j-jdk14 + 1.7.7 + + org.jenkins-ci.plugins github-api diff --git a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java new file mode 100644 index 000000000..f57786e56 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -0,0 +1,60 @@ +package org.jenkinsci.plugins.github.util; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.cloudbees.jenkins.GitHubRepositoryNameContributor; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import hudson.model.AbstractProject; +import hudson.triggers.Trigger; + +import java.util.Collection; + +/** + * Utility class which holds converters or predicates (matchers) to filter or convert job lists + * + * @author lanwen (Merkushev Kirill) + * Date: 16.06.15 + */ +public final class JobInfoHelpers { + + private JobInfoHelpers() { + throw new IllegalAccessError("Do not instantiate it"); + } + + /** + * @param clazz trigger class to check in job + * + * @return predicate with true on apply if job contains trigger of given class + */ + public static Predicate withTrigger(final Class clazz) { + return new Predicate() { + public boolean apply(AbstractProject job) { + return job.getTrigger(clazz) != null; + } + }; + } + + /** + * Can be useful to ignore disabled jobs on reregistering hooks + * + * @return predicate with true on apply if job is buildable + */ + public static Predicate isBuildable() { + return new Predicate() { + public boolean apply(AbstractProject job) { + return job.isBuildable(); + } + }; + } + + /** + * @return function which helps to convert job to repo names associated with this job + */ + public static Function> associatedNames() { + return new Function>() { + public Collection apply(AbstractProject job) { + return GitHubRepositoryNameContributor.parseAssociatedNames(job); + } + }; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java new file mode 100644 index 000000000..c3cc2832e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java @@ -0,0 +1,77 @@ +package org.jenkinsci.plugins.github.webhook; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import com.google.common.annotations.Beta; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.AbstractProject; +import jenkins.model.Jenkins; +import org.kohsuke.github.GHEvent; + +import java.util.Set; + +import static com.google.common.collect.Sets.immutableEnumSet; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; +import static org.kohsuke.github.GHEvent.PUSH; + +/** + * Extension point to contribute events plugin interested in. + * This point should return true in {@link #isApplicable(AbstractProject)} + * only if it can parse hooks with events contributed in {@link #events()} + * + * Each time this plugin wants to get events list from contributors it asks for applicable status + * + * @author lanwen (Merkushev Kirill) + * Date: 16.06.15 + */ +public abstract class GHEventsListener implements ExtensionPoint { + + public abstract boolean isApplicable(AbstractProject project); + + public abstract Set events(); + + @Beta + public void processEvent(GHEvent event, String payload) { + // TODO can be changed + } + + public static ExtensionList all() { + return Jenkins.getInstance().getExtensionList(GHEventsListener.class); + } + + public static Function> extractEvents() { + return new Function>() { + @Override + public Set apply(GHEventsListener provider) { + return provider.events(); + } + }; + } + + public static Predicate isApplicableFor(final AbstractProject project) { + return new Predicate() { + @Override + public boolean apply(GHEventsListener provider) { + return provider.isApplicable(project); + } + }; + } + + @Extension + @SuppressWarnings("unused") + public static class DefaultPushGHEventListener extends GHEventsListener { + @Override + public boolean isApplicable(AbstractProject project) { + return withTrigger(GitHubPushTrigger.class).apply(project); + } + + @Override + public Set events() { + return immutableEnumSet(PUSH); + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java new file mode 100644 index 000000000..213b1ce41 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -0,0 +1,293 @@ +package org.jenkinsci.plugins.github.webhook; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import hudson.model.AbstractProject; +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHException; +import org.kohsuke.github.GHHook; +import org.kohsuke.github.GHRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.List; +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.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.jenkinsci.plugins.github.webhook.GHEventsListener.extractEvents; +import static org.jenkinsci.plugins.github.webhook.GHEventsListener.isApplicableFor; + +/** + * Class to incapsulate manipulation with webhooks on GH + * Each manager works with only one hook url (created with {@link #forHookUrl(URL)}) + * + * @author lanwen (Merkushev Kirill) + * Date: 16.06.15 + */ +public class WebhookManager { + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPushTrigger.class); + + private final URL endpoint; + + /** + * Use {@link #forHookUrl(URL)} to create new one + * + * @param endpoint url which will be created as hook on GH + */ + private WebhookManager(URL endpoint) { + this.endpoint = endpoint; + } + + /** + * @see #WebhookManager(URL) + */ + public static WebhookManager forHookUrl(URL endpoint) { + return new WebhookManager(endpoint); + } + + /** + * 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 GHEventsListener}. + * New events will be merged with old ones from existent hook. + * + * By default only push event is registered + * + * @param project to find for which repos we should create hooks + * + * @return runnable to create hooks on run + * @see #createHookSubscribedTo(List) + */ + public Runnable registerFor(final AbstractProject project) { + final Collection names = parseAssociatedNames(project); + + final List events = from(GHEventsListener.all()) + .filter(isApplicableFor(project)) + .transformAndConcat(extractEvents()).toList(); + + return new Runnable() { + public void run() { + LOGGER.info("GitHub webhooks activated for job {} with {} (events: {})", + project.getFullName(), names, events); + + from(names) + .transform(createHookSubscribedTo(events)) + .filter(notNull()) + .filter(log("Created hook")).toList(); + } + }; + } + + /** + * Used to cleanup old hooks in case of removed or reconfigured trigger + * since JENKINS-28138 this method permanently removes service hooks + * + * So if the trigger for given name was only reconfigured, this method filters only service hooks + * (with help of alive names list), otherwise this method removes all hooks for managed url + * + * @param name repository to clean hooks + * @param alive repository list which has enabled trigger in jobs + */ + public void unregisterFor(GitHubRepositoryName name, List alive) { + try { + GHRepository repo = checkNotNull( + from(name.resolve()).firstMatch(withAdminAccess()).orNull(), + "There is no admin access to manage hooks on %s", name + ); + + LOGGER.debug("Check {} for redundant hooks...", repo); + + Predicate predicate = alive.contains(name) + ? serviceWebhookFor(endpoint) // permanently clear service hooks (JENKINS-28138) + : or(serviceWebhookFor(endpoint), webhookFor(endpoint)); + + from(fetchHooks().apply(repo)) + .filter(predicate) + .filter(deleteWebhook()) + .filter(log("Deleted hook")).toList(); + + } catch (Throwable t) { + LOGGER.warn("Failed to remove hook from {}", name, t); + } + } + + /** + * Main logic of {@link #registerFor(AbstractProject)}. + * Updates hooks with replacing old ones with merged new ones + * + * @param events calculated events list to be registered in hook + * + * @return function to register hooks for given events + */ + protected Function createHookSubscribedTo(final List events) { + return new Function() { + @Override + public GHHook apply(GitHubRepositoryName name) { + try { + GHRepository repo = checkNotNull( + from(name.resolve()).firstMatch(withAdminAccess()).orNull(), + "There is no admin access to manage hooks on %s", name + ); + + Set hooks = from(fetchHooks().apply(repo)) + .filter(webhookFor(endpoint)) + .toSet(); + + Set merged = from(hooks) + .transformAndConcat(eventsFromHook()) + .append(events).toSet(); + + from(hooks) + .filter(deleteWebhook()) + .filter(log("Replaced hook")).toList(); + + return createWebhook(endpoint, merged).apply(repo); + } catch (Throwable t) { + LOGGER.warn("Failed to add GitHub webhook for {}", name, t); + } + return null; + } + }; + } + + /** + * Mostly debug method. Logs hook manipulation result + * + * @param format prepended comment for log + * + * @return always true predicate + */ + private Predicate log(final String format) { + return new Predicate() { + @Override + public boolean apply(GHHook input) { + LOGGER.debug(format("%s {} (events: {})", format), input.getUrl(), input.getEvents()); + return true; + } + }; + } + + /** + * Filters repos with admin rights (to manage hooks) + * + * @return true if we have admin rights for repo + */ + protected Predicate withAdminAccess() { + return new Predicate() { + @Override + public boolean apply(GHRepository repo) { + return repo.hasAdminAccess(); + } + }; + } + + /** + * Finds "Jenkins (GitHub)" service webhook + * + * @param url jenkins endpoint url + * + * @return true if hook is service hook + */ + protected Predicate serviceWebhookFor(final URL url) { + return new Predicate() { + public boolean apply(GHHook hook) { + return hook.getName().equals("jenkins") + && hook.getConfig().get("jenkins_hook_url").equals(url.toExternalForm()); + } + }; + } + + /** + * Finds hook with endpoint url + * + * @param url jenkins endpoint url + * + * @return true if hook is standard webhook + */ + protected Predicate webhookFor(final URL url) { + return new Predicate() { + public boolean apply(GHHook hook) { + return hook.getName().equals("web") + && hook.getConfig().get("url").equals(url.toExternalForm()); + } + }; + } + + /** + * @return converter to extract events from each hook + */ + protected Function> eventsFromHook() { + return new Function>() { + @Override + public Iterable apply(GHHook input) { + return input.getEvents(); + } + }; + } + + /* + * ACTIONS + */ + + /** + * @return converter to fetch from GH hooks list for each repo + */ + protected Function> fetchHooks() { + return new Function>() { + @Override + public List apply(GHRepository repo) { + try { + return repo.getHooks(); + } catch (IOException e) { + throw new GHException("Failed to fetch post-commit hooks", e); + } + } + }; + } + + /** + * @param url jenkins endpoint url + * @param events list of GH events jenkins interested in + * + * @return converter to create GH hook for given url with given events + */ + protected Function createWebhook(final URL url, final Set events) { + return new Function() { + public GHHook apply(GHRepository repo) { + try { + return repo.createWebHook(url, events); + } catch (IOException e) { + throw new GHException("Failed to create hook", e); + } + } + }; + } + + /** + * @return annihilator for hook, returns true if deletion was successful + */ + protected Predicate deleteWebhook() { + return new Predicate() { + public boolean apply(GHHook hook) { + try { + hook.delete(); + return true; + } catch (IOException e) { + throw new GHException("Failed to delete post-commit hook", e); + } + } + }; + } +} From f02238e4598de767525bbc9de855a6fee007f269 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 17 Jun 2015 03:32:46 +0300 Subject: [PATCH 019/228] refactored cleaner and push trigger to work with hook manager --- .../java/com/cloudbees/jenkins/Cleaner.java | 92 ++++++--------- .../cloudbees/jenkins/GitHubPushTrigger.java | 107 ++++++++---------- 2 files changed, 83 insertions(+), 116 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index 74eb6473c..4ee1bfdd1 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -3,22 +3,20 @@ import com.cloudbees.jenkins.GitHubPushTrigger.DescriptorImpl; import hudson.Extension; import hudson.model.AbstractProject; -import hudson.model.Hudson; import hudson.model.PeriodicWork; import hudson.triggers.Trigger; -import hudson.util.TimeUnit2; -import org.kohsuke.github.GHException; -import org.kohsuke.github.GHHook; -import org.kohsuke.github.GHRepository; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.webhook.WebhookManager; -import java.io.IOException; import java.net.URL; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.associatedNames; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; /** * Removes post-commit hooks from repositories that we no longer care. @@ -29,70 +27,52 @@ */ @Extension public class Cleaner extends PeriodicWork { - private final Set couldHaveBeenRemoved = new HashSet(); + /** + * Queue contains repo names prepared to cleanup. + * After configure method on job, trigger calls {@link #onStop(AbstractProject)} + * 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 namesq = new ConcurrentLinkedQueue(); /** * Called when a {@link GitHubPushTrigger} is about to be removed. */ - synchronized void onStop(AbstractProject job) { - couldHaveBeenRemoved.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); + /* package */ void onStop(AbstractProject job) { + namesq.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); } @Override public long getRecurrencePeriod() { - return TimeUnit2.MINUTES.toMillis(3); + return TimeUnit.MINUTES.toMillis(3); } + /** + * Each run this work fetches alive repo names (which has trigger for it) + * then if names queue is not empty (any job was reconfigured with GH trigger change), + * next name passed to {@link WebhookManager} with list of active names to check and unregister old hooks + * + * @throws Exception + */ @Override protected void doRun() throws Exception { - List names; - synchronized (this) {// atomically obtain what we need to check - names = new ArrayList(couldHaveBeenRemoved); - couldHaveBeenRemoved.clear(); - } + URL url = Trigger.all().get(DescriptorImpl.class).getHookUrl(); - // subtract all the live repositories - for (AbstractProject job : Hudson.getInstance().getAllItems(AbstractProject.class)) { - GitHubPushTrigger trigger = job.getTrigger(GitHubPushTrigger.class); - if (trigger!=null) { - names.removeAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); - } - } + List jobs = Jenkins.getInstance().getAllItems(AbstractProject.class); + List alive = from(jobs) + .filter(withTrigger(GitHubPushTrigger.class)) // live repos + .transformAndConcat(associatedNames()).toList(); - // these are the repos that we are no longer interested. - // erase our hooks - OUTER: - for (GitHubRepositoryName r : names) { - for (GHRepository repo : r.resolve()) { - try { - removeHook(repo, Trigger.all().get(DescriptorImpl.class).getHookUrl()); - LOGGER.fine("Removed a hook from "+r+""); - continue OUTER; - } catch (Throwable e) { - LOGGER.log(Level.WARNING,"Failed to remove hook from "+r,e); - } - } - } - } + while (!namesq.isEmpty()) { + GitHubRepositoryName name = namesq.poll(); - //Maybe we should create a remove hook method in the Github API - //something like public void removeHook(String name, Map config) - private void removeHook(GHRepository repo, URL url) { - try { - String urlExternalForm = url.toExternalForm(); - for (GHHook h : repo.getHooks()) { - if (h.getName().equals("jenkins") && h.getConfig().get("jenkins_hook_url").equals(urlExternalForm)) { - h.delete(); - } - } - } catch (IOException e) { - throw new GHException("Failed to update post-commit hooks", e); + WebhookManager.forHookUrl(url).unregisterFor(name, alive); } } public static Cleaner get() { return PeriodicWork.all().get(Cleaner.class); } - - private static final Logger LOGGER = Logger.getLogger(Cleaner.class.getName()); } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 32a5c7811..56a246e83 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -1,27 +1,37 @@ package com.cloudbees.jenkins; +import com.google.common.base.Charsets; +import com.google.common.base.Function; import hudson.Extension; import hudson.Util; import hudson.console.AnnotatedLargeText; +import hudson.model.AbstractProject; import hudson.model.Action; -import hudson.model.Hudson; -import hudson.model.Hudson.MasterComputer; import hudson.model.Item; -import hudson.model.AbstractProject; import hudson.model.Project; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.FormValidation; import hudson.util.SequentialExecutionQueue; import hudson.util.StreamTaskListener; +import jenkins.model.Jenkins; +import jenkins.model.Jenkins.MasterComputer; +import net.sf.json.JSONObject; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.jelly.XMLOutput; +import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +import org.jenkinsci.plugins.github.webhook.WebhookManager; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.nio.charset.Charset; import java.security.interfaces.RSAPublicKey; import java.text.DateFormat; import java.util.ArrayList; @@ -33,19 +43,10 @@ import java.util.logging.Level; import java.util.logging.Logger; -import jenkins.model.Jenkins; -import net.sf.json.JSONObject; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.jelly.XMLOutput; -import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; -import org.kohsuke.github.GHException; -import org.kohsuke.github.GHRepository; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; - -import javax.inject.Inject; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; +import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; /** * Triggers a build when we receive a GitHub post-commit webhook. @@ -149,40 +150,16 @@ public void start(AbstractProject project, boolean newInstance) { /** * Tries to register hook for current associated job. + * Do this lazily to avoid blocking the UI thread. * Useful for using from groovy scripts. * @since 1.11.2 */ public void registerHooks() { - // make sure we have hooks installed. do this lazily to avoid blocking the UI thread. - final Collection names = GitHubRepositoryNameContributor.parseAssociatedNames(job); - - getDescriptor().queue.execute(new Runnable() { - public void run() { - LOGGER.log(Level.INFO, "Adding GitHub webhooks for {0}", names); - - for (GitHubRepositoryName name : names) { - for (GHRepository repo : name.resolve()) { - try { - if(createJenkinsHook(repo, getDescriptor().getHookUrl())) { - break; - } - } catch (Throwable e) { - LOGGER.log(Level.WARNING, "Failed to add GitHub webhook for "+name, e); - } - } - } - } - }); + URL hookUrl = getDescriptor().getHookUrl(); + Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job); + getDescriptor().queue.execute(hookRegistrator); } - private boolean createJenkinsHook(GHRepository repo, URL url) { - try { - repo.createHook("jenkins", Collections.singletonMap("jenkins_hook_url", url.toExternalForm()), null, true); - return true; - } catch (IOException e) { - throw new GHException("Failed to update jenkins hooks", e); - } - } @Override public void stop() { @@ -233,7 +210,7 @@ public String getLog() throws IOException { * @since 1.350 */ public void writeLogTo(XMLOutput out) throws IOException { - new AnnotatedLargeText(getLogFile(), Charset.defaultCharset(),true,this).writeHtmlTo(0,out.asWriter()); + new AnnotatedLargeText(getLogFile(), Charsets.UTF_8, true, this).writeHtmlTo(0, out.asWriter()); } } @@ -278,8 +255,14 @@ public void setManageHook(boolean v) { /** * Returns the URL that GitHub should post. */ - public URL getHookUrl() throws MalformedURLException { - return hookUrl!=null ? new URL(hookUrl) : new URL(Hudson.getInstance().getRootUrl()+GitHubWebHook.get().getUrlName()+'/'); + public URL getHookUrl() { + try { + return hookUrl != null + ? new URL(hookUrl) + : new URL(Jenkins.getInstance().getRootUrl() + GitHubWebHook.get().getUrlName() + '/'); + } catch (MalformedURLException e) { + throw new RuntimeException("Hook url is malformed", e); + } } public boolean hasOverrideURL() { @@ -337,22 +320,26 @@ public FormValidation doReRegister() { return FormValidation.error("Works only when Jenkins manages hooks"); } - int triggered = 0; - for (AbstractProject job : getJenkinsInstance().getAllItems(AbstractProject.class)) { - if (!job.isBuildable()) { - continue; - } + List registered = from(getJenkinsInstance().getAllItems(AbstractProject.class)) + .filter(isBuildable()) + .filter(withTrigger(GitHubPushTrigger.class)) + .transform(reRegisterHooks()).toList(); - GitHubPushTrigger trigger = job.getTrigger(GitHubPushTrigger.class); - if (trigger!=null) { + + LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", registered.size()); + return FormValidation.ok("Called re-register hooks for %s jobs", registered.size()); + } + + private Function reRegisterHooks() { + return new Function() { + @Override + public GitHubPushTrigger apply(AbstractProject job) { + GitHubPushTrigger trigger = (GitHubPushTrigger) job.getTrigger(GitHubPushTrigger.class); LOGGER.log(Level.FINE, "Calling registerHooks() for {0}", job.getFullName()); trigger.registerHooks(); - triggered++; + return trigger; } - } - - LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", triggered); - return FormValidation.ok("Called re-register hooks for " + triggered + " jobs"); + }; } public static final Jenkins getJenkinsInstance() throws IllegalStateException { From cba53536a77aa0615899725796cd2f8299b48c8f Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 17 Jun 2015 12:05:57 +0300 Subject: [PATCH 020/228] since jdoc + fix logger class --- .../org/jenkinsci/plugins/github/util/JobInfoHelpers.java | 2 +- .../jenkinsci/plugins/github/webhook/GHEventsListener.java | 2 +- .../org/jenkinsci/plugins/github/webhook/WebhookManager.java | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) 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 f57786e56..1f6880770 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -13,7 +13,7 @@ * Utility class which holds converters or predicates (matchers) to filter or convert job lists * * @author lanwen (Merkushev Kirill) - * Date: 16.06.15 + * @since 1.11.4 */ public final class JobInfoHelpers { diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java index c3cc2832e..cf39c0c42 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java @@ -25,7 +25,7 @@ * Each time this plugin wants to get events list from contributors it asks for applicable status * * @author lanwen (Merkushev Kirill) - * Date: 16.06.15 + * @since 1.11.4 */ public abstract class GHEventsListener implements ExtensionPoint { 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 213b1ce41..91a0d9141 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -1,6 +1,5 @@ package org.jenkinsci.plugins.github.webhook; -import com.cloudbees.jenkins.GitHubPushTrigger; import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.base.Function; import com.google.common.base.Predicate; @@ -32,10 +31,10 @@ * Each manager works with only one hook url (created with {@link #forHookUrl(URL)}) * * @author lanwen (Merkushev Kirill) - * Date: 16.06.15 + * @since 1.11.4 */ public class WebhookManager { - private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPushTrigger.class); + private static final Logger LOGGER = LoggerFactory.getLogger(WebhookManager.class); private final URL endpoint; From 03b091774e36739ac7f844ded97bd718bc31ef7b Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 03:07:31 +0300 Subject: [PATCH 021/228] move to separate class default gh-push-event listener add javadocs add simple test for filtering by trigger --- .../com/cloudbees/jenkins/GitHubWebHook.java | 4 +- .../github/webhook/GHEventsListener.java | 43 ++++++++++--------- .../listener/DefaultPushGHEventListener.java | 41 ++++++++++++++++++ .../github/util/JobInfoHelpersTest.java | 36 ++++++++++++++++ .../DefaultPushGHEventListenerTest.java | 33 ++++++++++++++ 5 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index e09c5a992..d0f40b45f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -32,6 +32,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.apache.commons.lang.StringUtils.isNotEmpty; + /** * Receives github hook. @@ -169,7 +171,7 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) { "Make sure payload version is 'application/vnd.github+form'."); } processGitHubPayload(payload, GitHubPushTrigger.class); - } else if (eventType != null && !eventType.isEmpty()) { + } else if (isNotEmpty(eventType)) { throw new IllegalArgumentException("Github Webhook event of type " + eventType + " is not supported. " + "Only push events are current supported"); } else { diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java index cf39c0c42..e91331a5e 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java @@ -1,10 +1,8 @@ package org.jenkinsci.plugins.github.webhook; -import com.cloudbees.jenkins.GitHubPushTrigger; import com.google.common.annotations.Beta; import com.google.common.base.Function; import com.google.common.base.Predicate; -import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.model.AbstractProject; @@ -13,10 +11,6 @@ import java.util.Set; -import static com.google.common.collect.Sets.immutableEnumSet; -import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; -import static org.kohsuke.github.GHEvent.PUSH; - /** * Extension point to contribute events plugin interested in. * This point should return true in {@link #isApplicable(AbstractProject)} @@ -29,8 +23,18 @@ */ public abstract class GHEventsListener implements ExtensionPoint { + /** + * Should return true only if this listener interested in {@link #events()} set for this project + * + * @param project to check + * + * @return true to provide events to register and listen for this project + */ public abstract boolean isApplicable(AbstractProject project); + /** + * @return immutable set of events this listener wants to register and then listen to + */ public abstract Set events(); @Beta @@ -42,6 +46,11 @@ public static ExtensionList all() { return Jenkins.getInstance().getExtensionList(GHEventsListener.class); } + /** + * Converts every provider to set of GHEvents + * + * @return converter to use in iterable manipulations + */ public static Function> extractEvents() { return new Function>() { @Override @@ -51,6 +60,13 @@ public Set apply(GHEventsListener provider) { }; } + /** + * Helps to filter only GHEventsListeners that can return TRUE on given project + * + * @param project to check every GHEventsListener for being applicable + * + * @return predicate to use in iterable filtering + */ public static Predicate isApplicableFor(final AbstractProject project) { return new Predicate() { @Override @@ -59,19 +75,4 @@ public boolean apply(GHEventsListener provider) { } }; } - - @Extension - @SuppressWarnings("unused") - public static class DefaultPushGHEventListener extends GHEventsListener { - @Override - public boolean isApplicable(AbstractProject project) { - return withTrigger(GitHubPushTrigger.class).apply(project); - } - - @Override - public Set events() { - return immutableEnumSet(PUSH); - } - } - } diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java new file mode 100644 index 000000000..affc1dee8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java @@ -0,0 +1,41 @@ +package org.jenkinsci.plugins.github.webhook.listener; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import hudson.Extension; +import hudson.model.AbstractProject; +import org.jenkinsci.plugins.github.webhook.GHEventsListener; +import org.kohsuke.github.GHEvent; + +import java.util.Set; + +import static com.google.common.collect.Sets.immutableEnumSet; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; +import static org.kohsuke.github.GHEvent.PUSH; + +/** + * @author lanwen (Merkushev Kirill) + * @since 1.11.4 + */ +@Extension +@SuppressWarnings("unused") +public class DefaultPushGHEventListener extends GHEventsListener { + /** + * This listener is applicable only for job with GHPush trigger + * + * @param project to check for trigger + * + * @return true if project has {@link GitHubPushTrigger} + */ + @Override + public boolean isApplicable(AbstractProject project) { + return withTrigger(GitHubPushTrigger.class).apply(project); + } + + /** + * @return set with only push event + */ + @Override + public Set events() { + return immutableEnumSet(PUSH); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java new file mode 100644 index 000000000..8d2b74c76 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java @@ -0,0 +1,36 @@ +package org.jenkinsci.plugins.github.util; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import hudson.model.FreeStyleProject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; +import static org.junit.Assert.assertThat; + +/** + * @author lanwen (Merkushev Kirill) + * Date: 03.07.15 + */ +public class JobInfoHelpersTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + public void shouldMatchForProjectWithTrigger() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + prj.addTrigger(new GitHubPushTrigger()); + + assertThat("with trigger", withTrigger(GitHubPushTrigger.class).apply(prj), is(true)); + } + + @Test + public void shouldNotMatchProjectWithoutTrigger() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + + assertThat("without trigger", withTrigger(GitHubPushTrigger.class).apply(prj), is(false)); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java new file mode 100644 index 000000000..43ba999b8 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java @@ -0,0 +1,33 @@ +package org.jenkinsci.plugins.github.webhook.listener; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import hudson.model.FreeStyleProject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * @author lanwen (Merkushev Kirill) + * Date: 03.07.15 + */ +public class DefaultPushGHEventListenerTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + public void shouldBeNotApplicableForProjectWithoutTrigger() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + assertThat(new DefaultPushGHEventListener().isApplicable(prj), is(false)); + } + + @Test + public void shouldBeApplicableForProjectWithTrigger() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + prj.addTrigger(new GitHubPushTrigger()); + assertThat(new DefaultPushGHEventListener().isApplicable(prj), is(true)); + } +} From eedfad73361650dde7f3ec084a7852a91ed681f3 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 03:09:21 +0300 Subject: [PATCH 022/228] add tests for whook manager --- pom.xml | 29 +++- .../github/webhook/WebhookManagerTest.java | 152 ++++++++++++++++++ 2 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java diff --git a/pom.xml b/pom.xml index 4807f0bcc..bbe51f956 100644 --- a/pom.xml +++ b/pom.xml @@ -51,12 +51,6 @@ git 2.0 - - org.jmock - jmock-junit4 - 2.5.1 - test - org.eclipse.jgit org.eclipse.jgit @@ -74,6 +68,29 @@ 1.3 provided + + + + junit + junit + 4.12 + test + + + + org.jmock + jmock-junit4 + 2.5.1 + test + + + + org.mockito + mockito-core + 1.10.19 + test + + diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java new file mode 100644 index 000000000..928cc28cc --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -0,0 +1,152 @@ +package org.jenkinsci.plugins.github.webhook; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableMap; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHHook; +import org.kohsuke.github.GHRepository; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.EnumSet; + +import static com.google.common.collect.ImmutableList.copyOf; +import static com.google.common.collect.Lists.asList; +import static com.google.common.collect.Lists.newArrayList; +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; +import static org.junit.Assert.assertThat; +import static org.kohsuke.github.GHEvent.CREATE; +import static org.kohsuke.github.GHEvent.PULL_REQUEST; +import static org.kohsuke.github.GHEvent.PUSH; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class WebhookManagerTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + public static final String HOOK_ENDPOINT = "http://hook.endpoint/"; + + @Spy + private WebhookManager manager = forHookUrl(endpoint()); + + @Spy + private GitHubRepositoryName nonactive = new GitHubRepositoryName("github.com", "dummy", "dummy"); + + @Spy + private GitHubRepositoryName active = new GitHubRepositoryName("github.com", "dummy", "active"); + + @Mock + private GHRepository repo; + + + @Test + public void shouldDoNothingOnNoAdminRights() throws Exception { + manager.unregisterFor(nonactive, newArrayList(active)); + verify(manager, times(1)).withAdminAccess(); + verify(manager, never()).fetchHooks(); + } + + @Test + public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception { + when(nonactive.resolve()).thenReturn(newArrayList(repo)); + when(repo.hasAdminAccess()).thenReturn(true); + + manager.unregisterFor(nonactive, newArrayList(active)); + + verify(manager, times(1)).serviceWebhookFor(endpoint()); + verify(manager, times(1)).webhookFor(endpoint()); + verify(manager, times(1)).fetchHooks(); + } + + @Test + public void shouldSearchOnlyServiceHookOnActiveName() throws Exception { + when(active.resolve()).thenReturn(newArrayList(repo)); + when(repo.hasAdminAccess()).thenReturn(true); + + manager.unregisterFor(active, newArrayList(active)); + + verify(manager, times(1)).serviceWebhookFor(endpoint()); + verify(manager, never()).webhookFor(endpoint()); + verify(manager, times(1)).fetchHooks(); + } + + @Test + @WithoutJenkins + public void shouldMatchAdminAccessWhenTrue() throws Exception { + when(repo.hasAdminAccess()).thenReturn(true); + + assertThat("has admin access", manager.withAdminAccess().apply(repo), is(true)); + } + + @Test + @WithoutJenkins + public void shouldMatchAdminAccessWhenFalse() throws Exception { + when(repo.hasAdminAccess()).thenReturn(false); + + assertThat("has no admin access", manager.withAdminAccess().apply(repo), is(false)); + } + + @Test + @WithoutJenkins + public void shouldMatchWebHook() { + when(repo.hasAdminAccess()).thenReturn(false); + + GHHook hook = hook(PUSH); + + assertThat("webhook has web name and url prop", manager.webhookFor(endpoint()).apply(hook), is(true)); + } + + @Test + public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException { + when(nonactive.resolve()).thenReturn(newArrayList(repo)); + when(repo.hasAdminAccess()).thenReturn(true); + Predicate del = spy(Predicate.class); + when(manager.deleteWebhook()).thenReturn(del); + + GHHook hook = hook(CREATE); + GHHook prhook = hook(PULL_REQUEST); + when(repo.getHooks()).thenReturn(newArrayList(hook, prhook)); + + manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive); + verify(del, times(2)).apply(any(GHHook.class)); + verify(manager).createWebhook(endpoint(), EnumSet.copyOf(newArrayList(CREATE, PULL_REQUEST, PUSH))); + } + + private URL endpoint() { + try { + return new URL(HOOK_ENDPOINT); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private GHHook hook(GHEvent event, GHEvent... events) { + GHHook hook = mock(GHHook.class); + when(hook.getName()).thenReturn("web"); + when(hook.getConfig()).thenReturn(ImmutableMap.of("url", endpoint().toExternalForm())); + when(hook.getEvents()).thenReturn(EnumSet.copyOf(asList(event, events))); + return hook; + } +} From 6a08dee8086fca6e3b8721917ce85597c20cd606 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 13:42:37 +0300 Subject: [PATCH 023/228] add dependency to hamcrest for test purposes --- pom.xml | 68 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/pom.xml b/pom.xml index bbe51f956..ee609fbc9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ 4.0.0 + org.jenkins-ci.plugins plugin @@ -40,36 +41,47 @@ slf4j-jdk14 1.7.7 - - - org.jenkins-ci.plugins - github-api - 1.67 - - - org.jenkins-ci.plugins - git - 2.0 - - - org.eclipse.jgit - org.eclipse.jgit - 0.12.1 - - - org.jenkins-ci.plugins - multiple-scms - 0.2 - true - - - org.jenkins-ci.modules - instance-identity - 1.3 - provided - + + + org.jenkins-ci.plugins + github-api + 1.67 + + + + org.jenkins-ci.plugins + git + 2.0 + + + + org.eclipse.jgit + org.eclipse.jgit + 0.12.1 + + + + org.jenkins-ci.plugins + multiple-scms + 0.2 + true + + + + org.jenkins-ci.modules + instance-identity + 1.3 + provided + + + org.hamcrest + hamcrest-all + 1.3 + test + + junit junit From c60db37affb5278ac1eb2ad823e8d8aafaddbcbf Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 13:43:48 +0300 Subject: [PATCH 024/228] tests for webhook manager - default push event test - hook match predicate --- .../listener/DefaultPushGHEventListener.java | 2 + .../github/webhook/WebhookManagerTest.java | 79 ++++++++++++++----- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java index affc1dee8..5cfa0aa4e 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java @@ -13,6 +13,8 @@ import static org.kohsuke.github.GHEvent.PUSH; /** + * By default this plugin interested in push events only when job uses {@link GitHubPushTrigger} + * * @author lanwen (Merkushev Kirill) * @since 1.11.4 */ diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index 928cc28cc..3e0df96a3 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -1,8 +1,11 @@ package org.jenkinsci.plugins.github.webhook; +import com.cloudbees.jenkins.GitHubPushTrigger; import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; +import hudson.model.FreeStyleProject; +import hudson.plugins.git.GitSCM; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -18,6 +21,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Collections; import java.util.EnumSet; import static com.google.common.collect.ImmutableList.copyOf; @@ -43,13 +47,15 @@ @RunWith(MockitoJUnitRunner.class) public class WebhookManagerTest { + public static final GitSCM GIT_SCM = new GitSCM("ssh://git@github.com/dummy/dummy.git"); + public static final URL HOOK_ENDPOINT = endpoint("http://hook.endpoint/"); + public static final URL ANOTHER_HOOK_ENDPOINT = endpoint("http://another.url/"); + @Rule public JenkinsRule jenkins = new JenkinsRule(); - - public static final String HOOK_ENDPOINT = "http://hook.endpoint/"; - + @Spy - private WebhookManager manager = forHookUrl(endpoint()); + private WebhookManager manager = forHookUrl(HOOK_ENDPOINT); @Spy private GitHubRepositoryName nonactive = new GitHubRepositoryName("github.com", "dummy", "dummy"); @@ -75,8 +81,8 @@ public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception manager.unregisterFor(nonactive, newArrayList(active)); - verify(manager, times(1)).serviceWebhookFor(endpoint()); - verify(manager, times(1)).webhookFor(endpoint()); + verify(manager, times(1)).serviceWebhookFor(HOOK_ENDPOINT); + verify(manager, times(1)).webhookFor(HOOK_ENDPOINT); verify(manager, times(1)).fetchHooks(); } @@ -87,8 +93,8 @@ public void shouldSearchOnlyServiceHookOnActiveName() throws Exception { manager.unregisterFor(active, newArrayList(active)); - verify(manager, times(1)).serviceWebhookFor(endpoint()); - verify(manager, never()).webhookFor(endpoint()); + verify(manager, times(1)).serviceWebhookFor(HOOK_ENDPOINT); + verify(manager, never()).webhookFor(HOOK_ENDPOINT); verify(manager, times(1)).fetchHooks(); } @@ -113,9 +119,20 @@ public void shouldMatchAdminAccessWhenFalse() throws Exception { public void shouldMatchWebHook() { when(repo.hasAdminAccess()).thenReturn(false); - GHHook hook = hook(PUSH); + GHHook hook = hook(HOOK_ENDPOINT, PUSH); - assertThat("webhook has web name and url prop", manager.webhookFor(endpoint()).apply(hook), is(true)); + assertThat("webhook has web name and url prop", manager.webhookFor(HOOK_ENDPOINT).apply(hook), is(true)); + } + + @Test + @WithoutJenkins + public void shouldNotMatchOtherUrlWebHook() { + when(repo.hasAdminAccess()).thenReturn(false); + + GHHook hook = hook(ANOTHER_HOOK_ENDPOINT, PUSH); + + assertThat("webhook has web name and another url prop", + manager.webhookFor(HOOK_ENDPOINT).apply(hook), is(false)); } @Test @@ -125,28 +142,48 @@ public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException Predicate del = spy(Predicate.class); when(manager.deleteWebhook()).thenReturn(del); - GHHook hook = hook(CREATE); - GHHook prhook = hook(PULL_REQUEST); + GHHook hook = hook(HOOK_ENDPOINT, CREATE); + GHHook prhook = hook(HOOK_ENDPOINT, PULL_REQUEST); when(repo.getHooks()).thenReturn(newArrayList(hook, prhook)); manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive); verify(del, times(2)).apply(any(GHHook.class)); - verify(manager).createWebhook(endpoint(), EnumSet.copyOf(newArrayList(CREATE, PULL_REQUEST, PUSH))); + verify(manager).createWebhook(HOOK_ENDPOINT, EnumSet.copyOf(newArrayList(CREATE, PULL_REQUEST, PUSH))); } - private URL endpoint() { - try { - return new URL(HOOK_ENDPOINT); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } + @Test + public void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOException { + FreeStyleProject project = jenkins.createFreeStyleProject(); + project.setScm(GIT_SCM); + + manager.registerFor(project).run(); + verify(manager).createHookSubscribedTo(Collections.emptyList()); } - private GHHook hook(GHEvent event, GHEvent... events) { + @Test + public void shouldAddPushEventByDefault() throws IOException { + FreeStyleProject project = jenkins.createFreeStyleProject(); + project.addTrigger(new GitHubPushTrigger()); + project.setScm(GIT_SCM); + + manager.registerFor(project).run(); + verify(manager).createHookSubscribedTo(newArrayList(PUSH)); + } + + + private GHHook hook(URL endpoint, GHEvent event, GHEvent... events) { GHHook hook = mock(GHHook.class); when(hook.getName()).thenReturn("web"); - when(hook.getConfig()).thenReturn(ImmutableMap.of("url", endpoint().toExternalForm())); + when(hook.getConfig()).thenReturn(ImmutableMap.of("url", endpoint.toExternalForm())); when(hook.getEvents()).thenReturn(EnumSet.copyOf(asList(event, events))); return hook; } + + private static URL endpoint(String endpoint) { + try { + return new URL(endpoint); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } } From 8fed5023f48d3ef8ac3d2e23e3bf2ded633c9fe2 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 13:47:51 +0300 Subject: [PATCH 025/228] get rid of event parser in listener for now --- .../plugins/github/webhook/GHEventsListener.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java index e91331a5e..cc47bdbf4 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java @@ -37,11 +37,9 @@ public abstract class GHEventsListener implements ExtensionPoint { */ public abstract Set events(); - @Beta - public void processEvent(GHEvent event, String payload) { - // TODO can be changed - } - + /** + * @return All listener extensions + */ public static ExtensionList all() { return Jenkins.getInstance().getExtensionList(GHEventsListener.class); } From 09add52886d70ed682d70f0a833be2013cf4b67f Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 13:55:47 +0300 Subject: [PATCH 026/228] process ping event in simple way --- src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index d0f40b45f..78f7eac5f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -164,6 +164,12 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) { } String eventType = req.getHeader("X-GitHub-Event"); + + if("ping".equals(eventType)) { + LOGGER.info("Got a ping request from GitHub"); + return; + } + if ("push".equals(eventType)) { String payload = req.getParameter("payload"); if (payload == null) { From b92628385dbadc90b77569e5ad4dae2f8697861e Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 15:07:59 +0300 Subject: [PATCH 027/228] throw and catch config exception on mailformed hook url in global configuration --- .../cloudbees/jenkins/GitHubPushTrigger.java | 18 +++++-- .../internal/GHPluginConfigException.java | 10 ++++ .../GitHubPushTriggerConfigSubmitTest.java | 47 +++++++++++++++---- ...om.cloudbees.jenkins.GitHubPushTrigger.xml | 12 +++++ .../config.xml | 35 ++++++++++++++ 5 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/internal/GHPluginConfigException.java create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 56a246e83..24dbc5200 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -20,7 +20,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.jelly.XMLOutput; import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; -import org.jenkinsci.plugins.github.webhook.WebhookManager; +import org.jenkinsci.plugins.github.internal.GHPluginConfigException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -152,10 +152,17 @@ public void start(AbstractProject project, boolean newInstance) { * Tries to register hook for current associated job. * Do this lazily to avoid blocking the UI thread. * Useful for using from groovy scripts. + * * @since 1.11.2 */ public void registerHooks() { - URL hookUrl = getDescriptor().getHookUrl(); + URL hookUrl; + try { + hookUrl = getDescriptor().getHookUrl(); + } catch (GHPluginConfigException e) { + LOGGER.log(Level.SEVERE, "Skip registration of GHHook ({0})", e.getMessage()); + return; + } Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job); getDescriptor().queue.execute(hookRegistrator); } @@ -207,6 +214,7 @@ public String getLog() throws IOException { /** * Writes the annotated log to the given output. + * * @since 1.350 */ public void writeLogTo(XMLOutput out) throws IOException { @@ -255,13 +263,15 @@ public void setManageHook(boolean v) { /** * Returns the URL that GitHub should post. */ - public URL getHookUrl() { + public URL getHookUrl() throws GHPluginConfigException { try { return hookUrl != null ? new URL(hookUrl) : new URL(Jenkins.getInstance().getRootUrl() + GitHubWebHook.get().getUrlName() + '/'); } catch (MalformedURLException e) { - throw new RuntimeException("Hook url is malformed", e); + throw new GHPluginConfigException( + "Mailformed GH hook url in global configuration (%s)", e.getMessage() + ); } } diff --git a/src/main/java/org/jenkinsci/plugins/github/internal/GHPluginConfigException.java b/src/main/java/org/jenkinsci/plugins/github/internal/GHPluginConfigException.java new file mode 100644 index 000000000..e3de1ac22 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GHPluginConfigException.java @@ -0,0 +1,10 @@ +package org.jenkinsci.plugins.github.internal; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GHPluginConfigException extends RuntimeException { + public GHPluginConfigException(String message, Object... args) { + super(String.format(message, args)); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java index d05de98b6..3e5be1104 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java @@ -1,32 +1,47 @@ package com.cloudbees.jenkins; +import com.gargoylesoftware.htmlunit.html.HtmlButton; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import hudson.util.Secret; +import org.jenkinsci.plugins.github.internal.GHPluginConfigException; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; +import org.kohsuke.stapler.Stapler; + import java.net.URL; import java.util.List; -import org.jvnet.hudson.test.HudsonTestCase; -import org.kohsuke.stapler.Stapler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * Test Class for {@link GitHubPushTrigger}. * * @author Seiji Sogabe */ -public class GitHubPushTriggerConfigSubmitTest extends HudsonTestCase { +public class GitHubPushTriggerConfigSubmitTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); private static final String WEBHOOK_URL = "http://jenkinsci.example.com/jenkins/github-webhook/"; + @Test public void testConfigSubmit_AutoManageHook() throws Exception { - WebClient client = configureWebClient(); + JenkinsRule.WebClient client = configureWebClient(); HtmlPage p = client.goTo("configure"); HtmlForm f = p.getFormByName("config"); f.getInputByValue("auto").setChecked(true); f.getInputByName("_.hasHookUrl").setChecked(true); f.getInputByName("_.hookUrl").setValueAttribute(WEBHOOK_URL); f.getInputByName("_.username").setValueAttribute("jenkins"); - submit(f); + jenkins.submit(f); GitHubPushTrigger.DescriptorImpl d = getDescriptor(); assertTrue(d.isManageHook()); @@ -39,24 +54,36 @@ public void testConfigSubmit_AutoManageHook() throws Exception { assertEquals("jenkins", credential.username); } + @Test public void testConfigSubmit_ManuallyManageHook() throws Exception { - - WebClient client = configureWebClient(); + JenkinsRule.WebClient client = configureWebClient(); HtmlPage p = client.goTo("configure"); HtmlForm f = p.getFormByName("config"); f.getInputByValue("none").setChecked(true); - submit(f); + jenkins.submit(f); GitHubPushTrigger.DescriptorImpl d = getDescriptor(); assertFalse(d.isManageHook()); } + @Test + @LocalData + public void shouldDontThrowExcMailformedHookUrl() { + new GitHubPushTrigger().registerHooks(); + } + + @Test(expected = GHPluginConfigException.class) + @LocalData + public void shouldThrowExcMailformedHookUrlGetter() { + new GitHubPushTrigger().getDescriptor().getHookUrl(); + } + private GitHubPushTrigger.DescriptorImpl getDescriptor() { return (GitHubPushTrigger.DescriptorImpl) GitHubPushTrigger.DescriptorImpl.get(); } - private WebClient configureWebClient() { - WebClient client = new WebClient(); + private JenkinsRule.WebClient configureWebClient() { + JenkinsRule.WebClient client = jenkins.createWebClient(); client.setThrowExceptionOnFailingStatusCode(false); client.setCssEnabled(false); client.setJavaScriptEnabled(true); diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml new file mode 100644 index 000000000..6b0594647 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml @@ -0,0 +1,12 @@ + + + true + h + + + user + + some-oauth-token + + + \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml b/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml new file mode 100644 index 000000000..e1bc0dc00 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml @@ -0,0 +1,35 @@ + + + + 1.554.1 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + + 5 + 0 + + + + Все + false + false + + + + Все + 0 + + + + \ No newline at end of file From 0c35c25001ab1c100fe2241df461373af018cc41 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Fri, 3 Jul 2015 15:30:00 +0300 Subject: [PATCH 028/228] cleanup useless jdocs --- src/main/java/com/cloudbees/jenkins/Cleaner.java | 10 ++++------ src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 4 ++-- .../jenkinsci/plugins/github/util/JobInfoHelpers.java | 2 +- .../plugins/github/webhook/GHEventsListener.java | 2 +- .../plugins/github/webhook/WebhookManager.java | 2 +- .../webhook/listener/DefaultPushGHEventListener.java | 2 +- .../plugins/github/util/JobInfoHelpersTest.java | 1 - .../listener/DefaultPushGHEventListenerTest.java | 1 - 8 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index 4ee1bfdd1..0a0eb26d6 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -35,13 +35,13 @@ public class Cleaner extends PeriodicWork { * This queue is thread-safe, so any thread can write or * fetch names to this queue without additional sync */ - private final Queue namesq = new ConcurrentLinkedQueue(); + private final Queue сleanQueue = new ConcurrentLinkedQueue(); /** * Called when a {@link GitHubPushTrigger} is about to be removed. */ /* package */ void onStop(AbstractProject job) { - namesq.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); + сleanQueue.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); } @Override @@ -53,8 +53,6 @@ public long getRecurrencePeriod() { * Each run this work fetches alive repo names (which has trigger for it) * then if names queue is not empty (any job was reconfigured with GH trigger change), * next name passed to {@link WebhookManager} with list of active names to check and unregister old hooks - * - * @throws Exception */ @Override protected void doRun() throws Exception { @@ -65,8 +63,8 @@ protected void doRun() throws Exception { .filter(withTrigger(GitHubPushTrigger.class)) // live repos .transformAndConcat(associatedNames()).toList(); - while (!namesq.isEmpty()) { - GitHubRepositoryName name = namesq.poll(); + while (!сleanQueue.isEmpty()) { + GitHubRepositoryName name = сleanQueue.poll(); WebhookManager.forHookUrl(url).unregisterFor(name, alive); } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 78f7eac5f..3f17a0df2 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -164,8 +164,8 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) { } String eventType = req.getHeader("X-GitHub-Event"); - - if("ping".equals(eventType)) { + + if ("ping".equals(eventType)) { LOGGER.info("Got a ping request from GitHub"); return; } 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 1f6880770..3cb854e99 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -13,7 +13,7 @@ * Utility class which holds converters or predicates (matchers) to filter or convert job lists * * @author lanwen (Merkushev Kirill) - * @since 1.11.4 + * @since TODO */ public final class JobInfoHelpers { diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java index cc47bdbf4..9c4948574 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java @@ -19,7 +19,7 @@ * Each time this plugin wants to get events list from contributors it asks for applicable status * * @author lanwen (Merkushev Kirill) - * @since 1.11.4 + * @since TODO */ public abstract class GHEventsListener implements ExtensionPoint { 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 91a0d9141..38592318a 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -31,7 +31,7 @@ * Each manager works with only one hook url (created with {@link #forHookUrl(URL)}) * * @author lanwen (Merkushev Kirill) - * @since 1.11.4 + * @since TODO */ public class WebhookManager { private static final Logger LOGGER = LoggerFactory.getLogger(WebhookManager.class); diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java index 5cfa0aa4e..c46165ac4 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java @@ -16,7 +16,7 @@ * By default this plugin interested in push events only when job uses {@link GitHubPushTrigger} * * @author lanwen (Merkushev Kirill) - * @since 1.11.4 + * @since TODO */ @Extension @SuppressWarnings("unused") diff --git a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java index 8d2b74c76..27057cf5a 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java @@ -12,7 +12,6 @@ /** * @author lanwen (Merkushev Kirill) - * Date: 03.07.15 */ public class JobInfoHelpersTest { diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java index 43ba999b8..e13349711 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java @@ -11,7 +11,6 @@ /** * @author lanwen (Merkushev Kirill) - * Date: 03.07.15 */ public class DefaultPushGHEventListenerTest { From 0924fae5b158c4f8b6e34eea02f1841cb0c17dd3 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 5 Jul 2015 16:16:07 +0300 Subject: [PATCH 029/228] rename alive - aliveRepos in cleaner and WHManager --- src/main/java/com/cloudbees/jenkins/Cleaner.java | 7 +++---- .../plugins/github/util/FluentIterableWrapper.java | 3 +++ .../jenkinsci/plugins/github/webhook/WebhookManager.java | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index 0a0eb26d6..c01aef3b6 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -1,6 +1,5 @@ package com.cloudbees.jenkins; -import com.cloudbees.jenkins.GitHubPushTrigger.DescriptorImpl; import hudson.Extension; import hudson.model.AbstractProject; import hudson.model.PeriodicWork; @@ -56,17 +55,17 @@ public long getRecurrencePeriod() { */ @Override protected void doRun() throws Exception { - URL url = Trigger.all().get(DescriptorImpl.class).getHookUrl(); + URL url = Trigger.all().get(GitHubPushTrigger.DescriptorImpl.class).getHookUrl(); List jobs = Jenkins.getInstance().getAllItems(AbstractProject.class); - List alive = from(jobs) + List aliveRepos = from(jobs) .filter(withTrigger(GitHubPushTrigger.class)) // live repos .transformAndConcat(associatedNames()).toList(); while (!сleanQueue.isEmpty()) { GitHubRepositoryName name = сleanQueue.poll(); - WebhookManager.forHookUrl(url).unregisterFor(name, alive); + WebhookManager.forHookUrl(url).unregisterFor(name, aliveRepos); } } 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 133082bb5..3c37afeae 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java @@ -23,6 +23,8 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import javax.annotation.CheckReturnValue; import java.util.Iterator; @@ -33,6 +35,7 @@ /** * Mostly copypaste from guava's FluentIterable */ +@Restricted(NoExternalUse.class) public abstract class FluentIterableWrapper implements Iterable { private final Iterable iterable; 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 38592318a..c114de7f2 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -94,12 +94,12 @@ public void run() { * since JENKINS-28138 this method permanently removes service hooks * * So if the trigger for given name was only reconfigured, this method filters only service hooks - * (with help of alive names list), otherwise this method removes all hooks for managed url + * (with help of aliveRepos names list), otherwise this method removes all hooks for managed url * * @param name repository to clean hooks - * @param alive repository list which has enabled trigger in jobs + * @param aliveRepos repository list which has enabled trigger in jobs */ - public void unregisterFor(GitHubRepositoryName name, List alive) { + public void unregisterFor(GitHubRepositoryName name, List aliveRepos) { try { GHRepository repo = checkNotNull( from(name.resolve()).firstMatch(withAdminAccess()).orNull(), @@ -108,7 +108,7 @@ public void unregisterFor(GitHubRepositoryName name, List LOGGER.debug("Check {} for redundant hooks...", repo); - Predicate predicate = alive.contains(name) + Predicate predicate = aliveRepos.contains(name) ? serviceWebhookFor(endpoint) // permanently clear service hooks (JENKINS-28138) : or(serviceWebhookFor(endpoint), webhookFor(endpoint)); From c04604ba53f7ec41994f572cae973cec3406911e Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 5 Jul 2015 17:20:00 +0300 Subject: [PATCH 030/228] make configurable alive state of job for cleaner with help of gh event listeners --- .../java/com/cloudbees/jenkins/Cleaner.java | 4 ++-- .../plugins/github/util/JobInfoHelpers.java | 21 +++++++++++++++++++ .../github/webhook/WebhookManager.java | 3 +++ .../github/util/JobInfoHelpersTest.java | 16 ++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index c01aef3b6..2d2362f0a 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -15,7 +15,7 @@ import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.associatedNames; -import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive; /** * Removes post-commit hooks from repositories that we no longer care. @@ -59,7 +59,7 @@ protected void doRun() throws Exception { List jobs = Jenkins.getInstance().getAllItems(AbstractProject.class); List aliveRepos = from(jobs) - .filter(withTrigger(GitHubPushTrigger.class)) // live repos + .filter(isAlive()) // live repos .transformAndConcat(associatedNames()).toList(); while (!сleanQueue.isEmpty()) { 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 3cb854e99..4a0abe53c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -6,9 +6,13 @@ import com.google.common.base.Predicate; import hudson.model.AbstractProject; import hudson.triggers.Trigger; +import org.jenkinsci.plugins.github.webhook.GHEventsListener; import java.util.Collection; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.jenkinsci.plugins.github.webhook.GHEventsListener.isApplicableFor; + /** * Utility class which holds converters or predicates (matchers) to filter or convert job lists * @@ -57,4 +61,21 @@ public Collection apply(AbstractProject job) { } }; } + + + /** + * If any of event listeners interested in hook for job, then return true + * By default, push hook listener is interested in job with gh-push-trigger + * + * @return predicate with true if job alive and should have hook + */ + public static Predicate isAlive() { + return new Predicate() { + @Override + public boolean apply(AbstractProject job) { + return !from(GHEventsListener.all()).filter(isApplicableFor(job)).toList().isEmpty(); + } + }; + } + } 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 c114de7f2..a380a9b2a 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -4,6 +4,7 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import hudson.model.AbstractProject; +import org.apache.commons.lang.Validate; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHException; import org.kohsuke.github.GHHook; @@ -148,6 +149,8 @@ public GHHook apply(GitHubRepositoryName name) { .transformAndConcat(eventsFromHook()) .append(events).toSet(); + Validate.notEmpty(events, "Events list for hook can't be empty"); + from(hooks) .filter(deleteWebhook()) .filter(log("Replaced hook")).toList(); diff --git a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java index 27057cf5a..5099b9763 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java @@ -7,6 +7,7 @@ import org.jvnet.hudson.test.JenkinsRule; import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; import static org.junit.Assert.assertThat; @@ -26,10 +27,25 @@ public void shouldMatchForProjectWithTrigger() throws Exception { assertThat("with trigger", withTrigger(GitHubPushTrigger.class).apply(prj), is(true)); } + @Test + public void shouldSeeProjectWithTriggerIsAliveForCleaner() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + prj.addTrigger(new GitHubPushTrigger()); + + assertThat("with trigger", isAlive().apply(prj), is(true)); + } + @Test public void shouldNotMatchProjectWithoutTrigger() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); assertThat("without trigger", withTrigger(GitHubPushTrigger.class).apply(prj), is(false)); } + + @Test + public void shouldSeeProjectWithoutTriggerIsNotAliveForCleaner() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + + assertThat("without trigger", isAlive().apply(prj), is(false)); + } } From ba83b1b13943ab0663ba3d5f63c274becd5d7f75 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 5 Jul 2015 17:34:17 +0300 Subject: [PATCH 031/228] log in debug ping event --- src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 2 +- .../jenkinsci/plugins/github/util/FluentIterableWrapper.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 3f17a0df2..4a5c17652 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -166,7 +166,7 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) { String eventType = req.getHeader("X-GitHub-Event"); if ("ping".equals(eventType)) { - LOGGER.info("Got a ping request from GitHub"); + LOGGER.debug("Got a ping request from GitHub"); return; } 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 3c37afeae..5ed5bdfbc 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java @@ -132,5 +132,4 @@ public final ImmutableSet toSet() { return ImmutableSet.copyOf(iterable); } - } From 83abac54e2842b708ebed48b736797ef5d3b40c7 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 5 Jul 2015 18:32:46 +0300 Subject: [PATCH 032/228] rename GHEventsListener to Subscriber and move ext point to separate pkg --- .../github/extension/GHEventsSubscriber.java | 75 ++++++++++++++++++ .../plugins/github/util/JobInfoHelpers.java | 17 +++-- .../github/webhook/GHEventsListener.java | 76 ------------------- .../github/webhook/WebhookManager.java | 9 ++- .../DefaultPushGHEventSubscriber.java} | 8 +- .../DefaultPushGHEventListenerTest.java | 6 +- 6 files changed, 96 insertions(+), 95 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java delete mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java rename src/main/java/org/jenkinsci/plugins/github/webhook/{listener/DefaultPushGHEventListener.java => subscriber/DefaultPushGHEventSubscriber.java} (79%) rename src/test/java/org/jenkinsci/plugins/github/webhook/{listener => subscriber}/DefaultPushGHEventListenerTest.java (77%) diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java new file mode 100644 index 000000000..1d502d207 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -0,0 +1,75 @@ +package org.jenkinsci.plugins.github.extension; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.AbstractProject; +import jenkins.model.Jenkins; +import org.kohsuke.github.GHEvent; + +import java.util.Set; + +/** + * Extension point to contribute events from GH, which plugin interested in. + * This point should return true in {@link #isApplicable(AbstractProject)} + * only if it can parse hooks with events contributed in {@link #events()} + * + * Each time this plugin wants to get events list from contributors it asks for applicable status + * + * @author lanwen (Merkushev Kirill) + * @since TODO + */ +public abstract class GHEventsSubscriber implements ExtensionPoint { + + /** + * Should return true only if this subscriber interested in {@link #events()} set for this project + * + * @param project to check + * + * @return true to provide events to register and subscribe for this project + */ + public abstract boolean isApplicable(AbstractProject project); + + /** + * @return immutable set of events this subscriber wants to register and then subscribe to + */ + public abstract Set events(); + + /** + * @return All subscriber extensions + */ + public static ExtensionList all() { + return Jenkins.getInstance().getExtensionList(GHEventsSubscriber.class); + } + + /** + * Converts each subscriber to set of GHEvents + * + * @return converter to use in iterable manipulations + */ + public static Function> extractEvents() { + return new Function>() { + @Override + public Set apply(GHEventsSubscriber provider) { + return provider.events(); + } + }; + } + + /** + * Helps to filter only GHEventsSubscribers that can return TRUE on given project + * + * @param project to check every GHEventsSubscriber for being applicable + * + * @return predicate to use in iterable filtering + */ + public static Predicate isApplicableFor(final AbstractProject project) { + return new Predicate() { + @Override + public boolean apply(GHEventsSubscriber provider) { + return provider.isApplicable(project); + } + }; + } +} 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 4a0abe53c..87a1ed5d9 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -5,13 +5,14 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import hudson.model.AbstractProject; +import hudson.model.Job; import hudson.triggers.Trigger; -import org.jenkinsci.plugins.github.webhook.GHEventsListener; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import java.util.Collection; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; -import static org.jenkinsci.plugins.github.webhook.GHEventsListener.isApplicableFor; +import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor; /** * Utility class which holds converters or predicates (matchers) to filter or convert job lists @@ -43,9 +44,9 @@ public boolean apply(AbstractProject job) { * * @return predicate with true on apply if job is buildable */ - public static Predicate isBuildable() { - return new Predicate() { - public boolean apply(AbstractProject job) { + public static Predicate isBuildable() { + return new Predicate() { + public boolean apply(Job job) { return job.isBuildable(); } }; @@ -64,8 +65,8 @@ public Collection apply(AbstractProject job) { /** - * If any of event listeners interested in hook for job, then return true - * By default, push hook listener is interested in job with gh-push-trigger + * If any of event subscriber interested in hook for job, 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 */ @@ -73,7 +74,7 @@ public static Predicate isAlive() { return new Predicate() { @Override public boolean apply(AbstractProject job) { - return !from(GHEventsListener.all()).filter(isApplicableFor(job)).toList().isEmpty(); + return !from(GHEventsSubscriber.all()).filter(isApplicableFor(job)).toList().isEmpty(); } }; } diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java deleted file mode 100644 index 9c4948574..000000000 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventsListener.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.jenkinsci.plugins.github.webhook; - -import com.google.common.annotations.Beta; -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import hudson.ExtensionList; -import hudson.ExtensionPoint; -import hudson.model.AbstractProject; -import jenkins.model.Jenkins; -import org.kohsuke.github.GHEvent; - -import java.util.Set; - -/** - * Extension point to contribute events plugin interested in. - * This point should return true in {@link #isApplicable(AbstractProject)} - * only if it can parse hooks with events contributed in {@link #events()} - * - * Each time this plugin wants to get events list from contributors it asks for applicable status - * - * @author lanwen (Merkushev Kirill) - * @since TODO - */ -public abstract class GHEventsListener implements ExtensionPoint { - - /** - * Should return true only if this listener interested in {@link #events()} set for this project - * - * @param project to check - * - * @return true to provide events to register and listen for this project - */ - public abstract boolean isApplicable(AbstractProject project); - - /** - * @return immutable set of events this listener wants to register and then listen to - */ - public abstract Set events(); - - /** - * @return All listener extensions - */ - public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(GHEventsListener.class); - } - - /** - * Converts every provider to set of GHEvents - * - * @return converter to use in iterable manipulations - */ - public static Function> extractEvents() { - return new Function>() { - @Override - public Set apply(GHEventsListener provider) { - return provider.events(); - } - }; - } - - /** - * Helps to filter only GHEventsListeners that can return TRUE on given project - * - * @param project to check every GHEventsListener for being applicable - * - * @return predicate to use in iterable filtering - */ - public static Predicate isApplicableFor(final AbstractProject project) { - return new Predicate() { - @Override - public boolean apply(GHEventsListener provider) { - return provider.isApplicable(project); - } - }; - } -} 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 a380a9b2a..944bb4563 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -5,6 +5,7 @@ import com.google.common.base.Predicate; import hudson.model.AbstractProject; import org.apache.commons.lang.Validate; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHException; import org.kohsuke.github.GHHook; @@ -24,8 +25,8 @@ import static com.google.common.base.Predicates.or; import static java.lang.String.format; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; -import static org.jenkinsci.plugins.github.webhook.GHEventsListener.extractEvents; -import static org.jenkinsci.plugins.github.webhook.GHEventsListener.isApplicableFor; +import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.extractEvents; +import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor; /** * Class to incapsulate manipulation with webhooks on GH @@ -60,7 +61,7 @@ public static WebhookManager forHookUrl(URL endpoint) { * 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 GHEventsListener}. + * 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 @@ -73,7 +74,7 @@ public static WebhookManager forHookUrl(URL endpoint) { public Runnable registerFor(final AbstractProject project) { final Collection names = parseAssociatedNames(project); - final List events = from(GHEventsListener.all()) + final List events = from(GHEventsSubscriber.all()) .filter(isApplicableFor(project)) .transformAndConcat(extractEvents()).toList(); diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java similarity index 79% rename from src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java rename to src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java index c46165ac4..9f6995cf5 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListener.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java @@ -1,9 +1,9 @@ -package org.jenkinsci.plugins.github.webhook.listener; +package org.jenkinsci.plugins.github.webhook.subscriber; import com.cloudbees.jenkins.GitHubPushTrigger; import hudson.Extension; import hudson.model.AbstractProject; -import org.jenkinsci.plugins.github.webhook.GHEventsListener; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; import java.util.Set; @@ -20,9 +20,9 @@ */ @Extension @SuppressWarnings("unused") -public class DefaultPushGHEventListener extends GHEventsListener { +public class DefaultPushGHEventSubscriber extends GHEventsSubscriber { /** - * This listener is applicable only for job with GHPush trigger + * This subscriber is applicable only for job with GHPush trigger * * @param project to check for trigger * diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java similarity index 77% rename from src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java rename to src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java index e13349711..209530f80 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/listener/DefaultPushGHEventListenerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java @@ -1,4 +1,4 @@ -package org.jenkinsci.plugins.github.webhook.listener; +package org.jenkinsci.plugins.github.webhook.subscriber; import com.cloudbees.jenkins.GitHubPushTrigger; import hudson.model.FreeStyleProject; @@ -20,13 +20,13 @@ public class DefaultPushGHEventListenerTest { @Test public void shouldBeNotApplicableForProjectWithoutTrigger() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); - assertThat(new DefaultPushGHEventListener().isApplicable(prj), is(false)); + assertThat(new DefaultPushGHEventSubscriber().isApplicable(prj), is(false)); } @Test public void shouldBeApplicableForProjectWithTrigger() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); prj.addTrigger(new GitHubPushTrigger()); - assertThat(new DefaultPushGHEventListener().isApplicable(prj), is(true)); + assertThat(new DefaultPushGHEventSubscriber().isApplicable(prj), is(true)); } } From af1c50da7bc9a9308cc551082a3dd5c2e1c06dc2 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 03:15:13 +0300 Subject: [PATCH 033/228] [JENKINS-28139] move all preprocess hook logic to separate classes --- .../plugins/github/webhook/GHEventHeader.java | 67 ++++++++ .../github/webhook/GHEventPayload.java | 100 ++++++++++++ .../webhook/RequirePostWithGHHookPayload.java | 147 ++++++++++++++++++ 3 files changed, 314 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java new file mode 100644 index 000000000..8efb0e66c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java @@ -0,0 +1,67 @@ +package org.jenkinsci.plugins.github.webhook; + +import org.kohsuke.github.GHEvent; +import org.kohsuke.stapler.AnnotationHandler; +import org.kohsuke.stapler.InjectedParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.slf4j.Logger; + +import javax.servlet.ServletException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.apache.commons.lang3.StringUtils.upperCase; +import static org.apache.commons.lang3.Validate.isTrue; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * InjectedParameter annotation to use on WebMethod parameters. + * Handles GitHub's X-GitHub-Event header. + * + * @author lanwen (Merkushev Kirill) + * @see Web Method + */ +@Retention(RUNTIME) +@Target(PARAMETER) +@Documented +@InjectedParameter(GHEventHeader.PayloadHandler.class) +public @interface GHEventHeader { + class PayloadHandler extends AnnotationHandler { + /** + * @see Developer manual + */ + public static final String EVENT_HEADER = "X-GitHub-Event"; + private static final Logger LOGGER = getLogger(PayloadHandler.class); + + /** + * @param type should be combined with type of {@link GHEvent} + * + * @return parsed {@link GHEvent} or null on empty header or unknown value + */ + @Override + public Object parse(StaplerRequest request, GHEventHeader a, Class type, String parameterName) throws ServletException { + isTrue(GHEvent.class.isAssignableFrom(type), + "Parameter '%s' should has type %s, not %s", parameterName, + GHEvent.class.getName(), + type.getName() + ); + + String header = request.getHeader(EVENT_HEADER); + LOGGER.debug("Header {} -> {}", EVENT_HEADER, header); + + if (header == null) { + return null; + } + + try { + return GHEvent.valueOf(upperCase(header)); + } catch (IllegalArgumentException e) { + LOGGER.debug("Unknown event - {}", e.getMessage()); + return null; + } + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java new file mode 100644 index 000000000..873c15da6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java @@ -0,0 +1,100 @@ +package org.jenkinsci.plugins.github.webhook; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.io.IOUtils; +import org.kohsuke.stapler.AnnotationHandler; +import org.kohsuke.stapler.InjectedParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.slf4j.Logger; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Map; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * InjectedParameter annotation to use on WebMethod parameters. + * Handles GitHub's payload of webhook + * + * @author lanwen (Merkushev Kirill) + * @see Web Method + */ +@Retention(RUNTIME) +@Target(PARAMETER) +@Documented +@InjectedParameter(GHEventPayload.PayloadHandler.class) +public @interface GHEventPayload { + class PayloadHandler extends AnnotationHandler { + private static final Logger LOGGER = getLogger(PayloadHandler.class); + + /** + * 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()) + .build(); + + /** + * @param type string type expected + * + * @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 { + String contentType = req.getContentType(); + + if (!PAYLOAD_PROCESS.containsKey(contentType)) { + LOGGER.error("Unknown content type {}", contentType); + return null; + } + + String payload = PAYLOAD_PROCESS.get(contentType).apply(req); + + LOGGER.trace("Payload {}", payload); + return payload; + } + + /** + * used for application/x-www-form-urlencoded content-type + * @return function to extract payload from form request parameters + */ + protected static Function fromForm() { + return new Function() { + @Override + public String apply(StaplerRequest request) { + return request.getParameter("payload"); + } + }; + } + + /** + * used for application/json content-type + * @return function to extract payload from body + */ + protected static Function fromApplicationJson() { + return new Function() { + @Override + public String apply(StaplerRequest request) { + try { + return IOUtils.toString(request.getInputStream(), Charsets.UTF_8); + } catch (IOException e) { + LOGGER.error("Can't get payload from request: {}", e.getMessage()); + return null; + } + } + }; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java new file mode 100644 index 000000000..55af91258 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -0,0 +1,147 @@ +package org.jenkinsci.plugins.github.webhook; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubWebHook; +import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +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.interceptor.Interceptor; +import org.kohsuke.stapler.interceptor.InterceptorAnnotation; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.security.interfaces.RSAPublicKey; +import java.util.logging.Logger; + +import static com.google.common.base.Predicates.instanceOf; +import static com.google.common.collect.Lists.newArrayList; +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 javax.servlet.http.HttpServletResponse.SC_OK; +import static org.apache.commons.codec.binary.Base64.encodeBase64; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.kohsuke.stapler.HttpResponses.error; + +/** + * InterceptorAnnotation annotation to use on WebMethod signature. + * Encapsulates preprocess logic of parsing GHHook or test connection request + * + * @author lanwen (Merkushev Kirill) + * @see Web Method + */ +@Retention(RUNTIME) +@Target({METHOD, FIELD}) +@InterceptorAnnotation(RequirePostWithGHHookPayload.Processor.class) +public @interface RequirePostWithGHHookPayload { + class Processor extends Interceptor { + private static final Logger LOGGER = Logger.getLogger(Processor.class.getName()); + + @Override + public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, Object[] arguments) + throws IllegalAccessException, InvocationTargetException { + + shouldBePostMethod(req); + returnsInstanceIdentityIfLocalUrlTest(req); + logPingEvent(req); + shouldContainParseablePayload(arguments); + + return target.invoke(req, rsp, instance, arguments); + } + + /** + * 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 { + if (!request.getMethod().equals("POST")) { + throw new InvocationTargetException(error(SC_METHOD_NOT_ALLOWED, "Method POST required")); + } + } + + /** + * Used for {@link GitHubPushTrigger.DescriptorImpl#doCheckHookUrl(java.lang.String)} + */ + protected void returnsInstanceIdentityIfLocalUrlTest(StaplerRequest 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) + throws IOException, ServletException { + RSAPublicKey key = new InstanceIdentity().getPublic(); + rsp.setStatus(HttpServletResponse.SC_OK); + rsp.setHeader(GitHubWebHook.X_INSTANCE_IDENTITY, new String(encodeBase64(key.getEncoded()))); + } + }); + } + } + + /** + * Additional logic to log ping event. In future can be replaced with separate + * {@link org.jenkinsci.plugins.github.extension.GHEventsSubscriber} with + * filtering of PING event to contribute. + * + * Wait for https://github.com/kohsuke/github-api/pull/204 will be released + * + * @throws InvocationTargetException returns OK 200 to client on ping event + */ + protected void logPingEvent(StaplerRequest req) throws InvocationTargetException { + if ("ping".equals(req.getHeader(GHEventHeader.PayloadHandler.EVENT_HEADER))) { + // until https://github.com/kohsuke/github-api/pull/204 will not be released + // after that use GHEvent.PING event form arguments + + LOGGER.info("Got ping event from GH"); + throw new InvocationTargetException(new HttpResponses.HttpResponseException() { + public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) + throws IOException { + rsp.setStatus(SC_OK); + rsp.getWriter().println("Ping received!"); + } + }); + } + } + + /** + * Precheck arguments contains not null GHEvent and not blank payload. + * 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 { + isTrue(arguments.length == 2, "GHHook root action should take (GHEvent) event and (String) payload"); + + FluentIterableWrapper from = from(newArrayList(arguments)); + isTrue(from.firstMatch(instanceOf(GHEvent.class)).isPresent(), "Hook should contain event type"); + isTrue(isNotBlank((String) from.firstMatch(instanceOf(String.class)).or("")), "Hook should contain payload"); + } + + /** + * 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 { + if (!condition) { + throw new InvocationTargetException(error(SC_BAD_REQUEST, msg)); + } + } + } +} + From 81639d0a674440a63b070221d449f67d95e19e59 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 03:16:44 +0300 Subject: [PATCH 034/228] [JENKINS-28139] add method to extension point for parse payload of any GHEvent --- .../github/extension/GHEventsSubscriber.java | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) 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 1d502d207..dec2c2f5c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -11,11 +11,11 @@ import java.util.Set; /** - * Extension point to contribute events from GH, which plugin interested in. + * Extension point to subscribe events from GH, which plugin interested in. * This point should return true in {@link #isApplicable(AbstractProject)} * only if it can parse hooks with events contributed in {@link #events()} * - * Each time this plugin wants to get events list from contributors it asks for applicable status + * Each time this plugin wants to get events list from subscribers it asks for applicable status * * @author lanwen (Merkushev Kirill) * @since TODO @@ -32,10 +32,24 @@ public abstract class GHEventsSubscriber implements ExtensionPoint { public abstract boolean isApplicable(AbstractProject project); /** - * @return immutable set of events this subscriber wants to register and then subscribe to + * Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)} + * + * @return immutable set of events this subscriber wants to register and then subscribe to. */ public abstract Set events(); + /** + * 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 + * parse logic + * + * @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 + */ + public void onEvent(GHEvent event, String payload) { + // do nothing by default + } + /** * @return All subscriber extensions */ @@ -72,4 +86,38 @@ public boolean apply(GHEventsSubscriber provider) { } }; } + + /** + * Predicate which returns true on apply if current subscriber is interested in event + * + * @param event should be one of {@link #events()} set to return true on apply + * + * @return predicate to match against {@link GHEventsSubscriber} + */ + public static Predicate isInterestedIn(final GHEvent event) { + return new Predicate() { + @Override + public boolean apply(GHEventsSubscriber subscriber) { + return subscriber.events().contains(event); + } + }; + } + + /** + * Function which calls {@link #onEvent(GHEvent, String)} 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. + */ + public static Function processEvent(final GHEvent event, final String payload) { + return new Function() { + @Override + public Void apply(GHEventsSubscriber subscriber) { + subscriber.onEvent(event, payload); + return null; + } + }; + } } From cafc78638bab5b36ef1c44b7fb03f1200c3886d4 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 03:18:45 +0300 Subject: [PATCH 035/228] [JENKINS-28139] move all parsing logic from webhook root action to extension - also add integration tests --- pom.xml | 14 +- .../com/cloudbees/jenkins/GitHubWebHook.java | 178 +++--------------- .../DefaultPushGHEventSubscriber.java | 65 +++++++ .../jenkins/GitHubWebHookFullTest.java | 148 +++++++++++++++ .../GitHubWebHookFullTest/payloads/ping.json | 134 +++++++++++++ .../GitHubWebHookFullTest/payloads/push.json | 153 +++++++++++++++ 6 files changed, 535 insertions(+), 157 deletions(-) create mode 100644 src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping.json create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json diff --git a/pom.xml b/pom.xml index ee609fbc9..749c98ee8 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,12 @@ - + + org.apache.commons + commons-lang3 + 3.4 + + org.slf4j slf4j-jdk14 @@ -103,6 +108,13 @@ test + + com.jayway.restassured + rest-assured + 2.4.0 + test + + diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 4a5c17652..d1de1de4e 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -3,36 +3,28 @@ import com.cloudbees.jenkins.GitHubPushTrigger.DescriptorImpl; import hudson.Extension; import hudson.ExtensionPoint; -import hudson.model.AbstractProject; import hudson.model.RootAction; import hudson.model.UnprotectedRootAction; -import hudson.security.ACL; -import hudson.triggers.Trigger; import hudson.util.AdaptedIterator; import hudson.util.Iterators.FilterIterator; import jenkins.model.Jenkins; -import net.sf.json.JSONObject; -import org.acegisecurity.Authentication; -import org.acegisecurity.context.SecurityContextHolder; -import org.apache.commons.codec.binary.Base64; -import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.jenkinsci.plugins.github.webhook.GHEventHeader; +import org.jenkinsci.plugins.github.webhook.GHEventPayload; +import org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload; +import org.kohsuke.github.GHEvent; import org.kohsuke.github.GitHub; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.interceptor.RequirePOST; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; import java.io.IOException; -import java.security.interfaces.RSAPublicKey; import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import static org.apache.commons.lang.StringUtils.isNotEmpty; +import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isInterestedIn; +import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.processEvent; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** @@ -42,9 +34,8 @@ */ @Extension public class GitHubWebHook implements UnprotectedRootAction { - @Inject - InstanceIdentity identity; - + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubWebHook.class); + public String getIconFileName() { return null; } @@ -97,151 +88,26 @@ protected boolean filter(GitHub g) { }; } - /* - - { - "after":"ea50ac0026d6d9c284e04afba1cc95d86dc3d976", - "before":"501f46e557f8fc5e0fa4c88a7f4597ef597dd1bf", - "commits":[ - { - "added":["b"], - "author":{"email":"kk@kohsuke.org","name":"Kohsuke Kawaguchi","username":"kohsuke"}, - "id":"3c696af1225e63ed531f5656e8f9cc252e4c96a2", - "message":"another commit", - "modified":[], - "removed":[], - "timestamp":"2010-12-08T14:31:24-08:00", - "url":"https://github.com/kohsuke/foo/commit/3c696af1225e63ed531f5656e8f9cc252e4c96a2" - },{ - "added":["d"], - "author":{"email":"kk@kohsuke.org","name":"Kohsuke Kawaguchi","username":"kohsuke"}, - "id":"ea50ac0026d6d9c284e04afba1cc95d86dc3d976", - "message":"new commit", - "modified":[], - "removed":[], - "timestamp":"2010-12-08T14:32:11-08:00", - "url":"https://github.com/kohsuke/foo/commit/ea50ac0026d6d9c284e04afba1cc95d86dc3d976" - } - ], - "compare":"https://github.com/kohsuke/foo/compare/501f46e...ea50ac0", - "forced":false, - "pusher":{"email":"kk@kohsuke.org","name":"kohsuke"}, - "ref":"refs/heads/master", - "repository":{ - "created_at":"2010/12/08 12:44:13 -0800", - "description":"testing", - "fork":false, - "forks":1, - "has_downloads":true, - "has_issues":true, - "has_wiki":true, - "homepage":"testing", - "name":"foo", - "open_issues":0, - "owner":{"email":"kk@kohsuke.org","name":"kohsuke"}, - "private":false, - "pushed_at":"2010/12/08 14:32:23 -0800", - "url":"https://github.com/kohsuke/foo","watchers":1 - } - } - - */ - - /** - * Receives the webhook call. - * - * 1 push to 2 branches will result in 2 push notifications. + * Receives the webhook call + * + * @param event GH event type. Never null + * @param payload Payload from hook. Never blank */ - @RequirePOST - public void doIndex(StaplerRequest req, StaplerResponse rsp) { - if (req.getHeader(URL_VALIDATION_HEADER) != null) { - // when the configuration page provides the self-check button, it makes a request with this header. - RSAPublicKey key = identity.getPublic(); - rsp.setHeader(X_INSTANCE_IDENTITY, new String(Base64.encodeBase64(key.getEncoded()))); - rsp.setStatus(200); - return; - } - - String eventType = req.getHeader("X-GitHub-Event"); - - if ("ping".equals(eventType)) { - LOGGER.debug("Got a ping request from GitHub"); - return; - } - - if ("push".equals(eventType)) { - String payload = req.getParameter("payload"); - if (payload == null) { - throw new IllegalArgumentException("Not intended to be browsed interactively (must specify payload parameter). " + - "Make sure payload version is 'application/vnd.github+form'."); - } - processGitHubPayload(payload, GitHubPushTrigger.class); - } else if (isNotEmpty(eventType)) { - throw new IllegalArgumentException("Github Webhook event of type " + eventType + " is not supported. " + - "Only push events are current supported"); - } else { - //Support github services that don't specify a header. - //Github webhook specifies a "X-Github-Event" header but services do not. - String payload = req.getParameter("payload"); - if (payload == null) { - throw new IllegalArgumentException("Not intended to be browsed interactively (must specify payload parameter)"); - } - processGitHubPayload(payload, GitHubPushTrigger.class); - } - } - - public void processGitHubPayload(String payload, Class> triggerClass) { - JSONObject o = JSONObject.fromObject(payload); - String repoUrl = o.getJSONObject("repository").getString("url"); // something like 'https://github.com/kohsuke/foo' - String pusherName = o.getJSONObject("pusher").getString("name"); - - LOGGER.info("Received POST for {}", repoUrl); - LOGGER.debug("Full details of the POST was {}", o.toString()); - Matcher matcher = REPOSITORY_NAME_PATTERN.matcher(repoUrl); - if (matcher.matches()) { - GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); - if (changedRepository == null) { - LOGGER.warn("Malformed repo url {}", repoUrl); - return; - } - - // run in high privilege to see all the projects anonymous users don't see. - // this is safe because when we actually schedule a build, it's a build that can - // happen at some random time anyway. - Authentication old = SecurityContextHolder.getContext().getAuthentication(); - SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); - try { - for (AbstractProject job : Jenkins.getInstance().getAllItems(AbstractProject.class)) { - GitHubTrigger trigger = (GitHubTrigger) job.getTrigger(triggerClass); - if (trigger != null) { - LOGGER.debug("Considering to poke {}", job.getFullDisplayName()); - if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) { - LOGGER.info("Poked {}", job.getFullDisplayName()); - trigger.onPost(pusherName); - } else - LOGGER.debug("Skipped {} because it doesn't have a matching repository.", job.getFullDisplayName()); - } - } - } finally { - SecurityContextHolder.getContext().setAuthentication(old); - } - for (Listener listener : Jenkins.getInstance().getExtensionList(Listener.class)) { - listener.onPushRepositoryChanged(pusherName, changedRepository); - } - } else { - LOGGER.warn("Malformed repo url {}", repoUrl); - } + @SuppressWarnings("unused") + @RequirePostWithGHHookPayload + public void doIndex(@GHEventHeader GHEvent event, @GHEventPayload String payload) { + from(GHEventsSubscriber.all()) + .filter(isInterestedIn(event)) + .transform(processEvent(event, payload)).toList(); } - private static final Pattern REPOSITORY_NAME_PATTERN = Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)"); public static final String URLNAME = "github-webhook"; // headers used for testing the endpoint configuration - /*package*/ static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation"; - /*package*/ static final String X_INSTANCE_IDENTITY = "X-Instance-Identity"; + public static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation"; + public static final String X_INSTANCE_IDENTITY = "X-Instance-Identity"; - private static final Logger LOGGER = LoggerFactory.getLogger(GitHubWebHook.class); public static GitHubWebHook get() { return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class); 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 9f6995cf5..82f6d4d70 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 @@ -1,12 +1,23 @@ package org.jenkinsci.plugins.github.webhook.subscriber; import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.cloudbees.jenkins.GitHubRepositoryNameContributor; +import com.cloudbees.jenkins.GitHubTrigger; +import com.cloudbees.jenkins.GitHubWebHook; import hudson.Extension; import hudson.model.AbstractProject; +import hudson.security.ACL; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static com.google.common.collect.Sets.immutableEnumSet; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; @@ -21,6 +32,9 @@ @Extension @SuppressWarnings("unused") public class DefaultPushGHEventSubscriber extends GHEventsSubscriber { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPushGHEventSubscriber.class); + private static final Pattern REPOSITORY_NAME_PATTERN = Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)"); + /** * This subscriber is applicable only for job with GHPush trigger * @@ -40,4 +54,55 @@ public boolean isApplicable(AbstractProject project) { public Set events() { return immutableEnumSet(PUSH); } + + /** + * Calls {@link GitHubPushTrigger} in all projects to handle this hook + * + * @param event only PUSH event + * @param payload payload of gh-event. Never blank + */ + @Override + public void onEvent(GHEvent event, String payload) { + JSONObject json = JSONObject.fromObject(payload); + String repoUrl = json.getJSONObject("repository").getString("url"); // something like 'https://github.com/kohsuke/foo' + final String pusherName = json.getJSONObject("pusher").getString("name"); + + LOGGER.info("Received POST for {}", repoUrl); + LOGGER.debug("Full details of the POST was {}", json.toString()); + Matcher matcher = REPOSITORY_NAME_PATTERN.matcher(repoUrl); + if (matcher.matches()) { + final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); + if (changedRepository == null) { + LOGGER.warn("Malformed repo url {}", repoUrl); + return; + } + + // run in high privilege to see all the projects anonymous users don't see. + // this is safe because when we actually schedule a build, it's a build that can + // happen at some random time anyway. + ACL.impersonate(ACL.SYSTEM, new Runnable() { + @Override + public void run() { + for (AbstractProject job : Jenkins.getInstance().getAllItems(AbstractProject.class)) { + GitHubTrigger trigger = job.getTrigger(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); + } else + LOGGER.debug("Skipped {} because it doesn't have a matching repository.", job.getFullDisplayName()); + } + } + } + }); + + for (GitHubWebHook.Listener listener : Jenkins.getInstance().getExtensionList(GitHubWebHook.Listener.class)) { + listener.onPushRepositoryChanged(pusherName, changedRepository); + } + + } else { + LOGGER.warn("Malformed repo url {}", repoUrl); + } + } } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java new file mode 100644 index 000000000..d3d950934 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -0,0 +1,148 @@ +package com.cloudbees.jenkins; + +import com.google.common.base.Charsets; +import com.google.common.net.HttpHeaders; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.response.Header; +import com.jayway.restassured.specification.RequestSpecification; +import org.apache.commons.io.IOUtils; +import org.jenkinsci.plugins.github.webhook.GHEventHeader; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.jvnet.hudson.test.JenkinsRule; +import org.kohsuke.github.GHEvent; + +import java.io.File; +import java.io.IOException; + +import static com.jayway.restassured.RestAssured.given; +import static com.jayway.restassured.config.EncoderConfig.encoderConfig; +import static com.jayway.restassured.config.RestAssuredConfig.newConfig; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.apache.commons.lang3.ClassUtils.PACKAGE_SEPARATOR; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubWebHookFullTest { + + public static final String APPLICATION_JSON = "application/json"; + public static final String FORM = "application/x-www-form-urlencoded"; + + public static final Header JSON_CONTENT_TYPE = new Header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + public static final Header FORM_CONTENT_TYPE = new Header(HttpHeaders.CONTENT_TYPE, FORM); + public static final String NOT_NULL_VALUE = "nonnull"; + + private RequestSpecification spec; + + @ClassRule + public static JenkinsRule jenkins = new JenkinsRule(); + + @Rule + public ExternalResource setup = new ExternalResource() { + @Override + protected void before() throws Throwable { + spec = new RequestSpecBuilder() + .setBaseUri(jenkins.getInstance().getRootUrl()) + .setBasePath(GitHubWebHook.URLNAME.concat("/")) + .setConfig(newConfig() + .encoderConfig(encoderConfig() + .defaultContentCharset(Charsets.UTF_8) + .appendDefaultContentCharsetToContentTypeIfUndefined(false))) + .build(); + } + }; + + @Test + public void shouldParseJsonWebHookFromGH() throws Exception { + given().spec(spec) + .header(eventHeader(GHEvent.PUSH)) + .header(JSON_CONTENT_TYPE) + .content(classpath("payloads/push.json")) + .log().all() + .expect().log().all().statusCode(SC_OK).post(); + } + + @Test + public void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { + given().spec(spec) + .header(eventHeader(GHEvent.PUSH)) + .header(FORM_CONTENT_TYPE) + .formParam("payload", classpath("payloads/push.json")) + .log().all() + .expect().log().all().statusCode(SC_OK).post(); + } + + @Test + public void shouldParsePingFromGH() throws Exception { + given().spec(spec) + .header(eventHeader("ping")) + .header(JSON_CONTENT_TYPE) + .content(classpath("payloads/ping.json")) + .log().all() + .expect().log().all() + .statusCode(SC_OK) + .body(containsString("Ping received!")) + .post(); + } + + @Test + public void shouldReturnErrOnEmptyPayloadAndHeader() throws Exception { + given().spec(spec) + .log().all() + .expect().log().all() + .statusCode(SC_BAD_REQUEST) + .body(containsString("Hook should contain event type")) + .post(); + } + + @Test + public void shouldReturnErrOnEmptyPayload() throws Exception { + given().spec(spec) + .header(eventHeader(GHEvent.PUSH)) + .log().all() + .expect().log().all() + .statusCode(SC_BAD_REQUEST) + .body(containsString("Hook should contain payload")) + .post(); + } + + @Test + public void shouldReturnErrOnGetReq() throws Exception { + given().spec(spec) + .log().all().expect().log().all() + .statusCode(SC_METHOD_NOT_ALLOWED) + .get(); + } + + @Test + public void shouldProcessSelfTest() throws Exception { + given().spec(spec) + .header(new Header(GitHubWebHook.URL_VALIDATION_HEADER, NOT_NULL_VALUE)) + .log().all() + .expect().log().all() + .statusCode(SC_OK) + .header(GitHubWebHook.X_INSTANCE_IDENTITY, notNullValue()) + .post(); + } + + public Header eventHeader(GHEvent event) { + return eventHeader(event.name().toLowerCase()); + } + + public Header eventHeader(String event) { + return new Header(GHEventHeader.PayloadHandler.EVENT_HEADER, event); + } + + public String classpath(String path) throws IOException { + return IOUtils.toString(getClass().getClassLoader().getResourceAsStream( + getClass().getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path + ), Charsets.UTF_8); + } +} diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping.json new file mode 100644 index 000000000..c26ab21e0 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping.json @@ -0,0 +1,134 @@ +{ + "zen": "Half measures are as bad as nothing at all.", + "hook_id": 5275258, + "hook": { + "url": "https://api.github.com/repos/lanwen/test/hooks/5275258", + "test_url": "https://api.github.com/repos/lanwen/test/hooks/5275258/test", + "ping_url": "https://api.github.com/repos/lanwen/test/hooks/5275258/pings", + "id": 5275258, + "name": "web", + "active": true, + "events": [ + "push" + ], + "config": { + "url": "http://requestb.in/115qkgl1", + "content_type": "json", + "insecure_ssl": "0", + "secret": "" + }, + "last_response": { + "code": null, + "status": "unused", + "message": null + }, + "updated_at": "2015-07-10T14:50:17Z", + "created_at": "2015-07-10T14:50:17Z" + }, + "repository": { + "id": 11257160, + "name": "test", + "full_name": "lanwen/test", + "owner": { + "login": "lanwen", + "id": 1964214, + "avatar_url": "https://avatars.githubusercontent.com/u/1964214?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/lanwen", + "html_url": "https://github.com/lanwen", + "followers_url": "https://api.github.com/users/lanwen/followers", + "following_url": "https://api.github.com/users/lanwen/following{/other_user}", + "gists_url": "https://api.github.com/users/lanwen/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lanwen/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lanwen/subscriptions", + "organizations_url": "https://api.github.com/users/lanwen/orgs", + "repos_url": "https://api.github.com/users/lanwen/repos", + "events_url": "https://api.github.com/users/lanwen/events{/privacy}", + "received_events_url": "https://api.github.com/users/lanwen/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/lanwen/test", + "description": "Test repo", + "fork": false, + "url": "https://api.github.com/repos/lanwen/test", + "forks_url": "https://api.github.com/repos/lanwen/test/forks", + "keys_url": "https://api.github.com/repos/lanwen/test/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/lanwen/test/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/lanwen/test/teams", + "hooks_url": "https://api.github.com/repos/lanwen/test/hooks", + "issue_events_url": "https://api.github.com/repos/lanwen/test/issues/events{/number}", + "events_url": "https://api.github.com/repos/lanwen/test/events", + "assignees_url": "https://api.github.com/repos/lanwen/test/assignees{/user}", + "branches_url": "https://api.github.com/repos/lanwen/test/branches{/branch}", + "tags_url": "https://api.github.com/repos/lanwen/test/tags", + "blobs_url": "https://api.github.com/repos/lanwen/test/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/lanwen/test/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/lanwen/test/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/lanwen/test/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/lanwen/test/statuses/{sha}", + "languages_url": "https://api.github.com/repos/lanwen/test/languages", + "stargazers_url": "https://api.github.com/repos/lanwen/test/stargazers", + "contributors_url": "https://api.github.com/repos/lanwen/test/contributors", + "subscribers_url": "https://api.github.com/repos/lanwen/test/subscribers", + "subscription_url": "https://api.github.com/repos/lanwen/test/subscription", + "commits_url": "https://api.github.com/repos/lanwen/test/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/lanwen/test/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/lanwen/test/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/lanwen/test/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/lanwen/test/contents/{+path}", + "compare_url": "https://api.github.com/repos/lanwen/test/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/lanwen/test/merges", + "archive_url": "https://api.github.com/repos/lanwen/test/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/lanwen/test/downloads", + "issues_url": "https://api.github.com/repos/lanwen/test/issues{/number}", + "pulls_url": "https://api.github.com/repos/lanwen/test/pulls{/number}", + "milestones_url": "https://api.github.com/repos/lanwen/test/milestones{/number}", + "notifications_url": "https://api.github.com/repos/lanwen/test/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/lanwen/test/labels{/name}", + "releases_url": "https://api.github.com/repos/lanwen/test/releases{/id}", + "created_at": "2013-07-08T15:04:11Z", + "updated_at": "2014-04-27T10:27:33Z", + "pushed_at": "2014-04-27T10:27:34Z", + "git_url": "git://github.com/lanwen/test.git", + "ssh_url": "git@github.com:lanwen/test.git", + "clone_url": "https://github.com/lanwen/test.git", + "svn_url": "https://github.com/lanwen/test", + "homepage": null, + "size": 148, + "stargazers_count": 0, + "watchers_count": 0, + "language": "CSS", + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + }, + "sender": { + "login": "lanwen", + "id": 1964214, + "avatar_url": "https://avatars.githubusercontent.com/u/1964214?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/lanwen", + "html_url": "https://github.com/lanwen", + "followers_url": "https://api.github.com/users/lanwen/followers", + "following_url": "https://api.github.com/users/lanwen/following{/other_user}", + "gists_url": "https://api.github.com/users/lanwen/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lanwen/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lanwen/subscriptions", + "organizations_url": "https://api.github.com/users/lanwen/orgs", + "repos_url": "https://api.github.com/users/lanwen/repos", + "events_url": "https://api.github.com/users/lanwen/events{/privacy}", + "received_events_url": "https://api.github.com/users/lanwen/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json new file mode 100644 index 000000000..0d006823d --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json @@ -0,0 +1,153 @@ +{ + "ref": "refs/heads/master", + "before": "a5e67044f52db16f5c128bd898083d38871fd9e7", + "after": "1eee2db8927ab3f7ec983b2e6052f351dd61a419", + "created": false, + "deleted": false, + "forced": false, + "base_ref": null, + "compare": "https://github.com/lanwen/test/compare/a5e67044f52d...1eee2db8927a", + "commits": [ + { + "id": "1eee2db8927ab3f7ec983b2e6052f351dd61a419", + "distinct": true, + "message": "Update README.md", + "timestamp": "2015-07-10T18:44:33+03:00", + "url": "https://github.com/lanwen/test/commit/1eee2db8927ab3f7ec983b2e6052f351dd61a419", + "author": { + "name": "Merkushev Kirill", + "email": "lanwen@users.noreply.github.com", + "username": "lanwen" + }, + "committer": { + "name": "Merkushev Kirill", + "email": "lanwen@users.noreply.github.com", + "username": "lanwen" + }, + "added": [], + "removed": [], + "modified": [ + "README.md" + ] + } + ], + "head_commit": { + "id": "1eee2db8927ab3f7ec983b2e6052f351dd61a419", + "distinct": true, + "message": "Update README.md", + "timestamp": "2015-07-10T18:44:33+03:00", + "url": "https://github.com/lanwen/test/commit/1eee2db8927ab3f7ec983b2e6052f351dd61a419", + "author": { + "name": "Merkushev Kirill", + "email": "lanwen@users.noreply.github.com", + "username": "lanwen" + }, + "committer": { + "name": "Merkushev Kirill", + "email": "lanwen@users.noreply.github.com", + "username": "lanwen" + }, + "added": [], + "removed": [], + "modified": [ + "README.md" + ] + }, + "repository": { + "id": 11257160, + "name": "test", + "full_name": "lanwen/test", + "owner": { + "name": "lanwen", + "email": "lanwen@users.noreply.github.com" + }, + "private": false, + "html_url": "https://github.com/lanwen/test", + "description": "Personal blog", + "fork": false, + "url": "https://github.com/lanwen/test", + "forks_url": "https://api.github.com/repos/lanwen/test/forks", + "keys_url": "https://api.github.com/repos/lanwen/test/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/lanwen/test/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/lanwen/test/teams", + "hooks_url": "https://api.github.com/repos/lanwen/test/hooks", + "issue_events_url": "https://api.github.com/repos/lanwen/test/issues/events{/number}", + "events_url": "https://api.github.com/repos/lanwen/test/events", + "assignees_url": "https://api.github.com/repos/lanwen/test/assignees{/user}", + "branches_url": "https://api.github.com/repos/lanwen/test/branches{/branch}", + "tags_url": "https://api.github.com/repos/lanwen/test/tags", + "blobs_url": "https://api.github.com/repos/lanwen/test/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/lanwen/test/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/lanwen/test/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/lanwen/test/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/lanwen/test/statuses/{sha}", + "languages_url": "https://api.github.com/repos/lanwen/test/languages", + "stargazers_url": "https://api.github.com/repos/lanwen/test/stargazers", + "contributors_url": "https://api.github.com/repos/lanwen/test/contributors", + "subscribers_url": "https://api.github.com/repos/lanwen/test/subscribers", + "subscription_url": "https://api.github.com/repos/lanwen/test/subscription", + "commits_url": "https://api.github.com/repos/lanwen/test/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/lanwen/test/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/lanwen/test/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/lanwen/test/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/lanwen/test/contents/{+path}", + "compare_url": "https://api.github.com/repos/lanwen/test/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/lanwen/test/merges", + "archive_url": "https://api.github.com/repos/lanwen/test/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/lanwen/test/downloads", + "issues_url": "https://api.github.com/repos/lanwen/test/issues{/number}", + "pulls_url": "https://api.github.com/repos/lanwen/test/pulls{/number}", + "milestones_url": "https://api.github.com/repos/lanwen/test/milestones{/number}", + "notifications_url": "https://api.github.com/repos/lanwen/test/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/lanwen/test/labels{/name}", + "releases_url": "https://api.github.com/repos/lanwen/test/releases{/id}", + "created_at": 1373295851, + "updated_at": "2015-07-10T15:44:33Z", + "pushed_at": 1436543073, + "git_url": "git://github.com/lanwen/test.git", + "ssh_url": "git@github.com:lanwen/test.git", + "clone_url": "https://github.com/lanwen/test.git", + "svn_url": "https://github.com/lanwen/test", + "homepage": null, + "size": 148, + "stargazers_count": 0, + "watchers_count": 0, + "language": "CSS", + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "stargazers": 0, + "master_branch": "master" + }, + "pusher": { + "name": "lanwen", + "email": "lanwen@users.noreply.github.com" + }, + "sender": { + "login": "lanwen", + "id": 1964214, + "avatar_url": "https://avatars.githubusercontent.com/u/1964214?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/lanwen", + "html_url": "https://github.com/lanwen", + "followers_url": "https://api.github.com/users/lanwen/followers", + "following_url": "https://api.github.com/users/lanwen/following{/other_user}", + "gists_url": "https://api.github.com/users/lanwen/gists{/gist_id}", + "starred_url": "https://api.github.com/users/lanwen/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lanwen/subscriptions", + "organizations_url": "https://api.github.com/users/lanwen/orgs", + "repos_url": "https://api.github.com/users/lanwen/repos", + "events_url": "https://api.github.com/users/lanwen/events{/privacy}", + "received_events_url": "https://api.github.com/users/lanwen/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file From 15b5e8b924498363fc1e8e991202a3a5bdc008ab Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 15:01:42 +0300 Subject: [PATCH 036/228] [JENKINS-28139] add tests for all webhook preprocess classes --- .../webhook/RequirePostWithGHHookPayload.java | 3 +- .../github/webhook/GHEventHeaderTest.java | 60 ++++++++++++++++ .../github/webhook/GHEventPayloadTest.java | 49 +++++++++++++ .../RequirePostWithGHHookPayloadTest.java | 68 +++++++++++++++++++ 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java 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 55af91258..80de06697 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -124,7 +124,8 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod * @throws InvocationTargetException if any of preconditions is not satisfied */ protected void shouldContainParseablePayload(Object[] arguments) throws InvocationTargetException { - isTrue(arguments.length == 2, "GHHook root action should take (GHEvent) event and (String) payload"); + isTrue(arguments.length == 2, + "GHHook root action should take <(GHEvent) event> and <(String) payload> only"); FluentIterableWrapper from = from(newArrayList(arguments)); isTrue(from.firstMatch(instanceOf(GHEvent.class)).isPresent(), "Hook should contain event type"); diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java new file mode 100644 index 000000000..d013196d6 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java @@ -0,0 +1,60 @@ +package org.jenkinsci.plugins.github.webhook; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHEvent; +import org.kohsuke.stapler.StaplerRequest; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class GHEventHeaderTest { + + public static final String STRING_PUSH_HEADER = "push"; + public static final String PARAM_NAME = "event"; + public static final String UNKNOWN_EVENT = "unkn"; + + @Mock + private StaplerRequest req; + + @Mock + private GHEventHeader ann; + + @Test + public void shouldReturnParsedPushHeader() throws Exception { + when(req.getHeader(GHEventHeader.PayloadHandler.EVENT_HEADER)).thenReturn(STRING_PUSH_HEADER); + Object event = new GHEventHeader.PayloadHandler().parse(req, ann, GHEvent.class, PARAM_NAME); + + assertThat("instance of event", event, instanceOf(GHEvent.class)); + assertThat("parsed event", (GHEvent) event, equalTo(GHEvent.PUSH)); + } + + @Test + public void shouldReturnNullOnEmptyHeader() throws Exception { + Object event = new GHEventHeader.PayloadHandler().parse(req, ann, GHEvent.class, PARAM_NAME); + + assertThat("event with empty header", event, nullValue()); + } + + @Test + public void shouldReturnNullOnUnknownEventHeader() throws Exception { + when(req.getHeader(GHEventHeader.PayloadHandler.EVENT_HEADER)).thenReturn(UNKNOWN_EVENT); + Object event = new GHEventHeader.PayloadHandler().parse(req, ann, GHEvent.class, PARAM_NAME); + + assertThat("event with unknown event header", event, nullValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExcOnWrongTypeOfHeader() throws Exception { + new GHEventHeader.PayloadHandler().parse(req, ann, String.class, PARAM_NAME); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java new file mode 100644 index 000000000..f0d0accfb --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java @@ -0,0 +1,49 @@ +package org.jenkinsci.plugins.github.webhook; + +import com.cloudbees.jenkins.GitHubWebHookFullTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.stapler.StaplerRequest; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class GHEventPayloadTest { + + public static final String NOT_EMPTY_PAYLOAD_CONTENT = "{}"; + public static final String PARAM_NAME = "payload"; + public static final String UNKNOWN_CONTENT_TYPE = "text/plain"; + + @Mock + private StaplerRequest req; + + @Mock + private GHEventPayload ann; + + @Test + public void shouldReturnPayloadFromForm() throws Exception { + when(req.getContentType()).thenReturn(GitHubWebHookFullTest.FORM); + when(req.getParameter(PARAM_NAME)).thenReturn(NOT_EMPTY_PAYLOAD_CONTENT); + Object payload = new GHEventPayload.PayloadHandler().parse(req, ann, String.class, PARAM_NAME); + + assertThat("class", payload, instanceOf(String.class)); + assertThat("content", (String) payload, equalTo(NOT_EMPTY_PAYLOAD_CONTENT)); + } + + @Test + public void shouldReturnNullOnUnknownContentType() throws Exception { + when(req.getContentType()).thenReturn(UNKNOWN_CONTENT_TYPE); + Object payload = new GHEventPayload.PayloadHandler().parse(req, ann, String.class, PARAM_NAME); + + assertThat("payload should be null", payload, nullValue()); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java new file mode 100644 index 000000000..d4bf1c03f --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java @@ -0,0 +1,68 @@ +package org.jenkinsci.plugins.github.webhook; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHEvent; +import org.kohsuke.stapler.StaplerRequest; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.lang.reflect.InvocationTargetException; + +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class RequirePostWithGHHookPayloadTest { + + @Mock + private StaplerRequest req; + + @Test + public void shouldPassOnlyPost() throws Exception { + when(req.getMethod()).thenReturn("POST"); + new RequirePostWithGHHookPayload.Processor().shouldBePostMethod(req); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnNotPost() throws Exception { + when(req.getMethod()).thenReturn("GET"); + new RequirePostWithGHHookPayload.Processor().shouldBePostMethod(req); + } + + @Test + public void shouldPassOnGHEventAndNotBlankPayload() throws Exception { + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[]{GHEvent.PUSH, "{}"}); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnNullGHEventAndNotBlankPayload() throws Exception { + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[]{null, "{}"}); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnGHEventAndBlankPayload() throws Exception { + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[] {GHEvent.PUSH, " "}); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnNulls() throws Exception { + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[] {null, null}); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnGreaterCountOfArgs() throws Exception { + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[] {GHEvent.PUSH, "{}", " "} + ); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnLessCountOfArgs() throws Exception { + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[] {GHEvent.PUSH} + ); + } +} From a88fc10ee77e1f166c60c1940fd75656e0939aa4 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 15:10:26 +0300 Subject: [PATCH 037/228] change error to warn on reregister btn --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 3 ++- .../java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 24dbc5200..e4d2cf75f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -325,9 +325,10 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) { } + @SuppressWarnings("unused") public FormValidation doReRegister() { if (!manageHook) { - return FormValidation.error("Works only when Jenkins manages hooks"); + return FormValidation.warning("Works only when Jenkins manages hooks"); } List registered = from(getJenkinsInstance().getAllItems(AbstractProject.class)) 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 87a1ed5d9..88ecf0c63 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -63,7 +63,6 @@ public Collection apply(AbstractProject job) { }; } - /** * If any of event subscriber interested in hook for job, then return true * By default, push hook subscriber is interested in job with gh-push-trigger @@ -78,5 +77,5 @@ public boolean apply(AbstractProject job) { } }; } - } + From 029f05d6812ae6ce0bd0da7856e66756686236b9 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 15:53:03 +0300 Subject: [PATCH 038/228] make all subscriber api protected to use this api via static methods - this helps handle nulls --- .../github/extension/GHEventsSubscriber.java | 25 +++++--- .../DefaultPushGHEventSubscriber.java | 6 +- .../cloudbees/jenkins/GitHubWebHookTest.java | 59 +++++++++++++++++++ .../extension/GHEventsSubscriberTest.java | 41 +++++++++++++ 4 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java 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 dec2c2f5c..af15406c7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -8,8 +8,12 @@ import jenkins.model.Jenkins; import org.kohsuke.github.GHEvent; +import java.util.HashSet; import java.util.Set; +import static java.util.Collections.emptySet; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + /** * Extension point to subscribe events from GH, which plugin interested in. * This point should return true in {@link #isApplicable(AbstractProject)} @@ -24,29 +28,32 @@ public abstract class GHEventsSubscriber implements ExtensionPoint { /** * Should return true only if this subscriber interested in {@link #events()} set for this project + * Don't call it directly, use {@link #isApplicableFor(AbstractProject)} static function * * @param project to check * * @return true to provide events to register and subscribe for this project */ - public abstract boolean isApplicable(AbstractProject project); + protected abstract boolean isApplicable(AbstractProject project); /** * Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)} + * Don't call it directly, use {@link #extractEvents()} or {@link #isInterestedIn(GHEvent)} static functions * * @return immutable set of events this subscriber wants to register and then subscribe to. */ - public abstract Set events(); + protected abstract Set events(); /** * 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 * parse logic + * Don't call it directly, use {@link #processEvent(GHEvent, String)} 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 */ - public void onEvent(GHEvent event, String payload) { + protected void onEvent(GHEvent event, String payload) { // do nothing by default } @@ -65,8 +72,8 @@ public static ExtensionList all() { public static Function> extractEvents() { return new Function>() { @Override - public Set apply(GHEventsSubscriber provider) { - return provider.events(); + public Set apply(GHEventsSubscriber subscriber) { + return defaultIfNull(subscriber.events(), new HashSet()); } }; } @@ -81,8 +88,8 @@ public Set apply(GHEventsSubscriber provider) { public static Predicate isApplicableFor(final AbstractProject project) { return new Predicate() { @Override - public boolean apply(GHEventsSubscriber provider) { - return provider.isApplicable(project); + public boolean apply(GHEventsSubscriber subscriber) { + return subscriber.isApplicable(project); } }; } @@ -98,7 +105,7 @@ public static Predicate isInterestedIn(final GHEvent event) return new Predicate() { @Override public boolean apply(GHEventsSubscriber subscriber) { - return subscriber.events().contains(event); + return defaultIfNull(subscriber.events(), emptySet()).contains(event); } }; } @@ -106,7 +113,7 @@ public boolean apply(GHEventsSubscriber subscriber) { /** * Function which calls {@link #onEvent(GHEvent, String)} for every subscriber on apply * - * @param event from hook. Applied only with event from {@link #events()} set + * @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. 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 82f6d4d70..f7209d965 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 @@ -43,7 +43,7 @@ public class DefaultPushGHEventSubscriber extends GHEventsSubscriber { * @return true if project has {@link GitHubPushTrigger} */ @Override - public boolean isApplicable(AbstractProject project) { + protected boolean isApplicable(AbstractProject project) { return withTrigger(GitHubPushTrigger.class).apply(project); } @@ -51,7 +51,7 @@ public boolean isApplicable(AbstractProject project) { * @return set with only push event */ @Override - public Set events() { + protected Set events() { return immutableEnumSet(PUSH); } @@ -62,7 +62,7 @@ public Set events() { * @param payload payload of gh-event. Never blank */ @Override - public void onEvent(GHEvent event, String payload) { + protected void onEvent(GHEvent event, String payload) { JSONObject json = JSONObject.fromObject(payload); String repoUrl = json.getJSONObject("repository").getString("url"); // something like 'https://github.com/kohsuke/foo' final String pusherName = json.getJSONObject("pusher").getString("name"); diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java new file mode 100644 index 000000000..8c7d0bdc1 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java @@ -0,0 +1,59 @@ +package com.cloudbees.jenkins; + +import hudson.model.AbstractProject; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.github.GHEvent; + +import java.util.Set; + +import static com.google.common.collect.Sets.immutableEnumSet; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubWebHookTest { + + public static final String PAYLOAD = "{}"; + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test(expected = GotEventException.class) + public void shouldCallExtensionInterestedInIssues() throws Exception { + new GitHubWebHook().doIndex(GHEvent.ISSUES, PAYLOAD); + } + + @Test + public void shouldNotCallAnyExtensionsWithPublicEventIfNotRegistered() throws Exception { + new GitHubWebHook().doIndex(GHEvent.PUBLIC, PAYLOAD); + } + + @TestExtension + @SuppressWarnings("unused") + public static class IssueSubscriber extends GHEventsSubscriber { + @Override + protected boolean isApplicable(AbstractProject project) { + return true; + } + + @Override + protected Set events() { + return immutableEnumSet(GHEvent.ISSUES); + } + + @Override + protected void onEvent(GHEvent event, String payload) { + throw new GotEventException(String.format("got event %s", event)); + } + } + + public static class GotEventException extends RuntimeException { + public GotEventException(String message) { + super(message); + } + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java b/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java new file mode 100644 index 000000000..704d41702 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java @@ -0,0 +1,41 @@ +package org.jenkinsci.plugins.github.extension; + +import hudson.model.AbstractProject; +import org.junit.Test; +import org.kohsuke.github.GHEvent; + +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GHEventsSubscriberTest { + + @Test + public void shouldReturnEmptySetInsteadOfNull() throws Exception { + Set set = GHEventsSubscriber.extractEvents().apply(new NullSubscriber()); + assertThat("null should be replaced", set, hasSize(0)); + } + + @Test + public void shouldMatchAgainstEmptySetInsteadOfNull() throws Exception { + boolean result = GHEventsSubscriber.isInterestedIn(GHEvent.PUSH).apply(new NullSubscriber()); + assertThat("null should be replaced", result, is(false)); + } + + public static class NullSubscriber extends GHEventsSubscriber { + @Override + protected boolean isApplicable(AbstractProject project) { + return true; + } + + @Override + protected Set events() { + return null; + } + } +} From 050696097f6ee20c2b8652e010abf812af4e2f77 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sat, 11 Jul 2015 16:06:47 +0300 Subject: [PATCH 039/228] add test to verify onEvent from subscriber calls trigger --- .../jenkins/GitHubWebHookFullTest.java | 6 ++--- .../DefaultPushGHEventListenerTest.java | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index d3d950934..6cc1445cc 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -140,9 +140,9 @@ public Header eventHeader(String event) { return new Header(GHEventHeader.PayloadHandler.EVENT_HEADER, event); } - public String classpath(String path) throws IOException { - return IOUtils.toString(getClass().getClassLoader().getResourceAsStream( - getClass().getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path + public static String classpath(String path) throws IOException { + return IOUtils.toString(GitHubWebHookFullTest.class.getClassLoader().getResourceAsStream( + GitHubWebHookFullTest.class.getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path ), Charsets.UTF_8); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java index 209530f80..87ec8c2e1 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java @@ -1,19 +1,27 @@ package org.jenkinsci.plugins.github.webhook.subscriber; import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubWebHookFullTest; import hudson.model.FreeStyleProject; +import hudson.plugins.git.GitSCM; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.kohsuke.github.GHEvent; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author lanwen (Merkushev Kirill) */ public class DefaultPushGHEventListenerTest { + public static final GitSCM GIT_SCM_FROM_RESOURCE = new GitSCM("ssh://git@github.com/lanwen/test.git"); + public static final String TRIGGERED_BY_USER_FROM_RESOURCE = "lanwen"; + @Rule public JenkinsRule jenkins = new JenkinsRule(); @@ -29,4 +37,18 @@ public void shouldBeApplicableForProjectWithTrigger() throws Exception { prj.addTrigger(new GitHubPushTrigger()); assertThat(new DefaultPushGHEventSubscriber().isApplicable(prj), is(true)); } + + @Test + public void shouldParsePushPayload() throws Exception { + GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); + + FreeStyleProject prj = jenkins.createFreeStyleProject(); + prj.addTrigger(trigger); + prj.setScm(GIT_SCM_FROM_RESOURCE); + + new DefaultPushGHEventSubscriber() + .onEvent(GHEvent.PUSH, GitHubWebHookFullTest.classpath("payloads/push.json")); + + verify(trigger).onPost(TRIGGERED_BY_USER_FROM_RESOURCE); + } } From 0ed510e7298e6a7b4723c864b774afa91133a252 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 12 Jul 2015 01:31:04 +0300 Subject: [PATCH 040/228] don't replace every time same hook on configuration saving --- .../github/webhook/WebhookManager.java | 19 +++++++++++++------ .../github/webhook/WebhookManagerTest.java | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) 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 944bb4563..1eb7b759f 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -24,9 +24,10 @@ 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.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.apache.commons.collections.CollectionUtils.isEqualCollection; import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.extractEvents; import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** * Class to incapsulate manipulation with webhooks on GH @@ -98,7 +99,7 @@ public void run() { * So if the trigger for given name was only reconfigured, this method filters only service hooks * (with help of aliveRepos names list), otherwise this method removes all hooks for managed url * - * @param name repository to clean hooks + * @param name repository to clean hooks * @param aliveRepos repository list which has enabled trigger in jobs */ public void unregisterFor(GitHubRepositoryName name, List aliveRepos) { @@ -142,15 +143,21 @@ public GHHook apply(GitHubRepositoryName name) { "There is no admin access to manage hooks on %s", name ); + Validate.notEmpty(events, "Events list for hook can't be empty"); + Set hooks = from(fetchHooks().apply(repo)) .filter(webhookFor(endpoint)) .toSet(); - Set merged = from(hooks) - .transformAndConcat(eventsFromHook()) - .append(events).toSet(); + Set alreadyRegistered = from(hooks) + .transformAndConcat(eventsFromHook()).toSet(); - Validate.notEmpty(events, "Events list for hook can't be empty"); + if (hooks.size() == 1 && isEqualCollection(alreadyRegistered, events)) { + LOGGER.debug("Hook already registered for events {}", events); + return null; + } + + Set merged = from(alreadyRegistered).append(events).toSet(); from(hooks) .filter(deleteWebhook()) diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index 3e0df96a3..559740bff 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -34,6 +34,7 @@ import static org.kohsuke.github.GHEvent.PULL_REQUEST; import static org.kohsuke.github.GHEvent.PUSH; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anySet; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -151,6 +152,19 @@ public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException verify(manager).createWebhook(HOOK_ENDPOINT, EnumSet.copyOf(newArrayList(CREATE, PULL_REQUEST, PUSH))); } + @Test + public void shouldNotReplaceAlreadyRegisteredHook() throws IOException { + when(nonactive.resolve()).thenReturn(newArrayList(repo)); + when(repo.hasAdminAccess()).thenReturn(true); + + GHHook hook = hook(HOOK_ENDPOINT, PUSH); + when(repo.getHooks()).thenReturn(newArrayList(hook)); + + manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive); + verify(manager, never()).deleteWebhook(); + verify(manager, never()).createWebhook(any(URL.class), anySet()); + } + @Test public void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOException { FreeStyleProject project = jenkins.createFreeStyleProject(); From b2f08d3383d2a7bd4dde57449b7a58d0372b7ba4 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 12 Jul 2015 01:31:56 +0300 Subject: [PATCH 041/228] remove duplicated log event from default subscriber --- .../plugins/github/extension/GHEventsSubscriber.java | 4 ++-- .../webhook/subscriber/DefaultPushGHEventSubscriber.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) 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 af15406c7..dac1b28a1 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -8,7 +8,7 @@ import jenkins.model.Jenkins; import org.kohsuke.github.GHEvent; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; import static java.util.Collections.emptySet; @@ -73,7 +73,7 @@ public static Function> extractEvents() { return new Function>() { @Override public Set apply(GHEventsSubscriber subscriber) { - return defaultIfNull(subscriber.events(), new HashSet()); + return defaultIfNull(subscriber.events(), Collections.emptySet()); } }; } 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 f7209d965..f6ac3b42f 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 @@ -68,7 +68,6 @@ protected void onEvent(GHEvent event, String payload) { final String pusherName = json.getJSONObject("pusher").getString("name"); LOGGER.info("Received POST for {}", repoUrl); - LOGGER.debug("Full details of the POST was {}", json.toString()); Matcher matcher = REPOSITORY_NAME_PATTERN.matcher(repoUrl); if (matcher.matches()) { final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); From af7867d2bdc6f7a79e5d560eefd2c8c5a0c3be92 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 12 Jul 2015 01:48:49 +0300 Subject: [PATCH 042/228] add mention of guava's code usage --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 360bf5550..bd6c91254 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,7 @@ License 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. + + +This plugin uses part of Guava's code in class named +`org.jenkinsci.plugins.github.util.FluentIterableWrapper` licensed under Apache 2.0 license From 948d2df963c4ffdb832445391ddaa41e8714f247 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 12 Jul 2015 02:46:48 +0300 Subject: [PATCH 043/228] move all hook registration code to root action --- .../cloudbees/jenkins/GitHubPushTrigger.java | 41 +--------- .../com/cloudbees/jenkins/GitHubWebHook.java | 79 +++++++++++++++++-- .../GitHubPushTriggerConfigSubmitTest.java | 10 ++- 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index e4d2cf75f..0e58f154d 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -43,11 +43,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; -import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable; -import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; -import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; - /** * Triggers a build when we receive a GitHub post-commit webhook. * @@ -156,15 +151,7 @@ public void start(AbstractProject project, boolean newInstance) { * @since 1.11.2 */ public void registerHooks() { - URL hookUrl; - try { - hookUrl = getDescriptor().getHookUrl(); - } catch (GHPluginConfigException e) { - LOGGER.log(Level.SEVERE, "Skip registration of GHHook ({0})", e.getMessage()); - return; - } - Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job); - getDescriptor().queue.execute(hookRegistrator); + GitHubWebHook.get().registerHookFor(job); } @@ -331,36 +318,12 @@ public FormValidation doReRegister() { return FormValidation.warning("Works only when Jenkins manages hooks"); } - List registered = from(getJenkinsInstance().getAllItems(AbstractProject.class)) - .filter(isBuildable()) - .filter(withTrigger(GitHubPushTrigger.class)) - .transform(reRegisterHooks()).toList(); - + List registered = GitHubWebHook.get().reRegisterAllHooks(); LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", registered.size()); return FormValidation.ok("Called re-register hooks for %s jobs", registered.size()); } - private Function reRegisterHooks() { - return new Function() { - @Override - public GitHubPushTrigger apply(AbstractProject job) { - GitHubPushTrigger trigger = (GitHubPushTrigger) job.getTrigger(GitHubPushTrigger.class); - LOGGER.log(Level.FINE, "Calling registerHooks() for {0}", job.getFullName()); - trigger.registerHooks(); - return trigger; - } - }; - } - - public static final Jenkins getJenkinsInstance() throws IllegalStateException { - Jenkins instance = Jenkins.getInstance(); - if (instance == null) { - throw new IllegalStateException("Jenkins has not been started, or was already shut down"); - } - return instance; - } - public static DescriptorImpl get() { return Trigger.all().get(DescriptorImpl.class); } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index d1de1de4e..462fc5f4c 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -1,14 +1,19 @@ package com.cloudbees.jenkins; import com.cloudbees.jenkins.GitHubPushTrigger.DescriptorImpl; +import com.google.common.base.Function; import hudson.Extension; import hudson.ExtensionPoint; +import hudson.model.AbstractProject; import hudson.model.RootAction; import hudson.model.UnprotectedRootAction; +import hudson.triggers.Trigger; import hudson.util.AdaptedIterator; import hudson.util.Iterators.FilterIterator; +import hudson.util.SequentialExecutionQueue; import jenkins.model.Jenkins; 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; @@ -18,13 +23,19 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.URL; import java.util.Collections; import java.util.Iterator; import java.util.List; +import static hudson.model.Computer.threadPoolForRemoting; +import static org.apache.commons.lang3.Validate.notNull; import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isInterestedIn; import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.processEvent; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable; +import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; /** @@ -35,7 +46,15 @@ @Extension public class GitHubWebHook implements UnprotectedRootAction { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubWebHook.class); - + public static final String URLNAME = "github-webhook"; + + // 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"; + + private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting); + + public String getIconFileName() { return null; } @@ -88,10 +107,33 @@ protected boolean filter(GitHub g) { }; } + /** + * If any wants to auto-register hook, then should call this method + * Example code: + * {@code GitHubWebHook.get().registerHookFor(job);} + * + * @param job not null project to register hook for + */ + public void registerHookFor(AbstractProject job) { + reRegisterHookForJob().apply(job); + } + + /** + * Calls {@link #registerHookFor(AbstractProject)} for every project which have subscriber + * + * @return list of jobs which jenkins tried to register hook + */ + public List reRegisterAllHooks() { + return from(getJenkinsInstance().getAllItems(AbstractProject.class)) + .filter(isBuildable()) + .filter(isAlive()) + .transform(reRegisterHookForJob()).toList(); + } + /** * Receives the webhook call - * - * @param event GH event type. Never null + * + * @param event GH event type. Never null * @param payload Payload from hook. Never blank */ @SuppressWarnings("unused") @@ -102,17 +144,40 @@ public void doIndex(@GHEventHeader GHEvent event, @GHEventPayload String payload .transform(processEvent(event, payload)).toList(); } - public static final String URLNAME = "github-webhook"; + private Function reRegisterHookForJob() { + return new Function() { + @Override + public AbstractProject apply(AbstractProject job) { + LOGGER.debug("Calling registerHooks() for {0}", notNull(job, "Job can't be null").getFullName()); - // 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"; + // We should handle wrong url of self defined hook url here in any case with try-catch :( + URL hookUrl; + try { + hookUrl = Trigger.all().get(GitHubPushTrigger.DescriptorImpl.class).getHookUrl(); + } catch (GHPluginConfigException e) { + LOGGER.error("Skip registration of GHHook ({0})", e.getMessage()); + return job; + } + Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job); + queue.execute(hookRegistrator); + return job; + } + }; + } public static GitHubWebHook get() { return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class); } + public static Jenkins getJenkinsInstance() throws IllegalStateException { + Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + throw new IllegalStateException("Jenkins has not been started, or was already shut down"); + } + return instance; + } + /** * Other plugins may be interested in listening for these updates. * diff --git a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java index 3e5be1104..746f6a9d9 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java @@ -1,8 +1,8 @@ package com.cloudbees.jenkins; -import com.gargoylesoftware.htmlunit.html.HtmlButton; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.model.FreeStyleProject; import hudson.util.Secret; import org.jenkinsci.plugins.github.internal.GHPluginConfigException; import org.junit.Rule; @@ -11,6 +11,7 @@ import org.jvnet.hudson.test.recipes.LocalData; import org.kohsuke.stapler.Stapler; +import java.io.IOException; import java.net.URL; import java.util.List; @@ -68,8 +69,11 @@ public void testConfigSubmit_ManuallyManageHook() throws Exception { @Test @LocalData - public void shouldDontThrowExcMailformedHookUrl() { - new GitHubPushTrigger().registerHooks(); + public void shouldDontThrowExcMailformedHookUrl() throws IOException { + FreeStyleProject job = jenkins.createFreeStyleProject(); + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.start(job, true); + trigger.registerHooks(); } @Test(expected = GHPluginConfigException.class) From a72162eab863af4c8a8e5243807962637fc0707e Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 15 Jul 2015 17:24:18 +0300 Subject: [PATCH 044/228] integration test formatting --- .../cloudbees/jenkins/GitHubWebHookFullTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index 6cc1445cc..65fa22ce4 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -34,7 +34,7 @@ public class GitHubWebHookFullTest { public static final String APPLICATION_JSON = "application/json"; public static final String FORM = "application/x-www-form-urlencoded"; - + public static final Header JSON_CONTENT_TYPE = new Header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); public static final Header FORM_CONTENT_TYPE = new Header(HttpHeaders.CONTENT_TYPE, FORM); public static final String NOT_NULL_VALUE = "nonnull"; @@ -78,7 +78,7 @@ public void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { .log().all() .expect().log().all().statusCode(SC_OK).post(); } - + @Test public void shouldParsePingFromGH() throws Exception { given().spec(spec) @@ -101,8 +101,8 @@ public void shouldReturnErrOnEmptyPayloadAndHeader() throws Exception { .body(containsString("Hook should contain event type")) .post(); } - - @Test + + @Test public void shouldReturnErrOnEmptyPayload() throws Exception { given().spec(spec) .header(eventHeader(GHEvent.PUSH)) @@ -131,15 +131,15 @@ public void shouldProcessSelfTest() throws Exception { .header(GitHubWebHook.X_INSTANCE_IDENTITY, notNullValue()) .post(); } - + public Header eventHeader(GHEvent event) { return eventHeader(event.name().toLowerCase()); } - + public Header eventHeader(String event) { return new Header(GHEventHeader.PayloadHandler.EVENT_HEADER, event); } - + public static String classpath(String path) throws IOException { return IOUtils.toString(GitHubWebHookFullTest.class.getClassLoader().getResourceAsStream( GitHubWebHookFullTest.class.getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path From 3a9e6a0e730d30a15533cadcc69dce0ebbdcd702 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 15 Jul 2015 19:45:43 +0300 Subject: [PATCH 045/228] Nonnull annotations --- src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 462fc5f4c..1fc49328f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.io.IOException; import java.net.URL; import java.util.Collections; @@ -138,7 +139,7 @@ public List reRegisterAllHooks() { */ @SuppressWarnings("unused") @RequirePostWithGHHookPayload - public void doIndex(@GHEventHeader GHEvent event, @GHEventPayload String payload) { + public void doIndex(@Nonnull @GHEventHeader GHEvent event, @Nonnull @GHEventPayload String payload) { from(GHEventsSubscriber.all()) .filter(isInterestedIn(event)) .transform(processEvent(event, payload)).toList(); @@ -170,6 +171,7 @@ public static GitHubWebHook get() { return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class); } + @Nonnull public static Jenkins getJenkinsInstance() throws IllegalStateException { Jenkins instance = Jenkins.getInstance(); if (instance == null) { From 601eb56c22421ee58c34e41ac621533646d1ea71 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 15 Jul 2015 22:44:34 +0300 Subject: [PATCH 046/228] Mention about admin:repo_hook scope for credentials --- .../com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly index 8313adb99..067dac634 100644 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly +++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly @@ -3,7 +3,9 @@
In this mode, Jenkins will add/remove hook URLs to GitHub based on the project configuration of Jenkins. 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. + 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.

This URL is ${app.rootUrl}github-webhook/, From 5d0b38bafa6f125fa306f7db9c0c3626e38a5077 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 15 Jul 2015 22:45:10 +0300 Subject: [PATCH 047/228] use slf4j style template for logging in gh-webhook class --- src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 1fc49328f..be362f26c 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -149,14 +149,14 @@ private Function reRegisterHookForJob() { return new Function() { @Override public AbstractProject apply(AbstractProject job) { - LOGGER.debug("Calling registerHooks() for {0}", notNull(job, "Job can't be null").getFullName()); + LOGGER.debug("Calling registerHooks() for {}", notNull(job, "Job can't be null").getFullName()); // We should handle wrong url of self defined hook url here in any case with try-catch :( URL hookUrl; try { hookUrl = Trigger.all().get(GitHubPushTrigger.DescriptorImpl.class).getHookUrl(); } catch (GHPluginConfigException e) { - LOGGER.error("Skip registration of GHHook ({0})", e.getMessage()); + LOGGER.error("Skip registration of GHHook ({})", e.getMessage()); return job; } Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job); From 373086fa5d4acdab7d8dbe0ad483ef74083f478b Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Sun, 19 Jul 2015 02:24:55 +0300 Subject: [PATCH 048/228] [maven-release-plugin] prepare release github-1.12.0-alpha-1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 749c98ee8..84e9a3e13 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.11.4-SNAPSHOT + 1.12.0-alpha-1 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -131,7 +131,7 @@ 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 + github-1.12.0-alpha-1 From c6a08eac88aa843d7fc7bf76ef93934c14fe2246 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Sun, 19 Jul 2015 02:25:00 +0300 Subject: [PATCH 049/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 84e9a3e13..098806a24 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.12.0-alpha-1 + 1.12.0-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -131,7 +131,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.12.0-alpha-1 + HEAD From b5a89ce727338fcdb47cc2b81a9157f125a692af Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Sun, 19 Jul 2015 16:40:02 +0300 Subject: [PATCH 050/228] handle exceptions of extensions --- .../github/extension/GHEventsSubscriber.java | 10 ++- .../cloudbees/jenkins/GitHubWebHookTest.java | 86 +++++++++++++++++-- 2 files changed, 90 insertions(+), 6 deletions(-) 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 dac1b28a1..5e4336af4 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -7,6 +7,8 @@ import hudson.model.AbstractProject; import jenkins.model.Jenkins; import org.kohsuke.github.GHEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Set; @@ -25,6 +27,7 @@ * @since TODO */ public abstract class GHEventsSubscriber implements ExtensionPoint { + private static final Logger LOGGER = LoggerFactory.getLogger(GHEventsSubscriber.class); /** * Should return true only if this subscriber interested in {@link #events()} set for this project @@ -122,7 +125,12 @@ public static Function processEvent(final GHEvent even return new Function() { @Override public Void apply(GHEventsSubscriber subscriber) { - subscriber.onEvent(event, payload); + try { + subscriber.onEvent(event, payload); + } catch (Throwable t) { + LOGGER.error("Subscriber {} failed to process {} hook, skipping...", + subscriber.getClass().getName(), event, t); + } return null; } }; diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java index 8c7d0bdc1..fb28a64cd 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java @@ -1,7 +1,9 @@ package com.cloudbees.jenkins; +import com.google.inject.Inject; import hudson.model.AbstractProject; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -11,6 +13,11 @@ import java.util.Set; import static com.google.common.collect.Sets.immutableEnumSet; +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; /** * @author lanwen (Merkushev Kirill) @@ -18,23 +25,88 @@ public class GitHubWebHookTest { public static final String PAYLOAD = "{}"; - + @Rule public JenkinsRule jenkins = new JenkinsRule(); - @Test(expected = GotEventException.class) + @Inject + private IssueSubscriber subscriber; + + @Inject + private PullRequestSubscriber pullRequestSubscriber; + + @Inject + private ThrowablePullRequestSubscriber throwablePullRequestSubscriber; + + @Before + public void setUp() throws Exception { + jenkins.getInstance().getInjector().injectMembers(this); + } + + @Test public void shouldCallExtensionInterestedInIssues() throws Exception { new GitHubWebHook().doIndex(GHEvent.ISSUES, PAYLOAD); + assertThat("should get interested event", subscriber.lastEvent(), equalTo(GHEvent.ISSUES)); } @Test public void shouldNotCallAnyExtensionsWithPublicEventIfNotRegistered() throws Exception { new GitHubWebHook().doIndex(GHEvent.PUBLIC, PAYLOAD); + assertThat("should not get not interested event", subscriber.lastEvent(), nullValue()); + } + + @Test + public void shouldCatchThrowableOnFailedSubscriber() throws Exception { + new GitHubWebHook().doIndex(GHEvent.PULL_REQUEST, PAYLOAD); + assertThat("each extension should get event", + asList( + pullRequestSubscriber.lastEvent(), + throwablePullRequestSubscriber.lastEvent() + ), everyItem(equalTo(GHEvent.PULL_REQUEST))); + } + + @TestExtension + @SuppressWarnings("unused") + public static class IssueSubscriber extends TestSubscriber { + + public IssueSubscriber() { + super(GHEvent.ISSUES); + } + } + + @TestExtension + @SuppressWarnings("unused") + public static class PullRequestSubscriber extends TestSubscriber { + + public PullRequestSubscriber() { + super(GHEvent.PULL_REQUEST); + } } @TestExtension @SuppressWarnings("unused") - public static class IssueSubscriber extends GHEventsSubscriber { + public static class ThrowablePullRequestSubscriber extends TestSubscriber { + + public ThrowablePullRequestSubscriber() { + super(GHEvent.PULL_REQUEST); + } + + @Override + protected void onEvent(GHEvent event, String payload) { + super.onEvent(event, payload); + throw new GotEventException("Something went wrong!"); + } + } + + public static class TestSubscriber extends GHEventsSubscriber { + + private GHEvent interested; + private GHEvent event; + + public TestSubscriber(GHEvent interested) { + this.interested = interested; + } + @Override protected boolean isApplicable(AbstractProject project) { return true; @@ -42,12 +114,16 @@ protected boolean isApplicable(AbstractProject project) { @Override protected Set events() { - return immutableEnumSet(GHEvent.ISSUES); + return immutableEnumSet(interested); } @Override protected void onEvent(GHEvent event, String payload) { - throw new GotEventException(String.format("got event %s", event)); + this.event = event; + } + + public GHEvent lastEvent() { + return event; } } From b73f1e8e654d3491bdaac9920f349dcc48032a23 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Wed, 22 Jul 2015 16:39:20 +0300 Subject: [PATCH 051/228] [maven-release-plugin] prepare release github-1.12.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 098806a24..8f4af3497 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.12.0-SNAPSHOT + 1.12.0 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -131,7 +131,7 @@ 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 + github-1.12.0 From 6f1354b86d5a88b1ce955e5663e6626dcc4b4a4e Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Wed, 22 Jul 2015 16:39:25 +0300 Subject: [PATCH 052/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8f4af3497..a40c93524 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.12.0 + 1.12.1-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -131,7 +131,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.12.0 + HEAD From 389d2def83d9a6c2ec1c817f75863ba71a38ebd8 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 29 Jul 2015 16:24:58 +0300 Subject: [PATCH 053/228] change since tag for new api introduced in 1.12.0 --- .../jenkinsci/plugins/github/extension/GHEventsSubscriber.java | 2 +- .../java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java | 2 +- .../org/jenkinsci/plugins/github/webhook/WebhookManager.java | 2 +- .../github/webhook/subscriber/DefaultPushGHEventSubscriber.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 5e4336af4..f29f2ab21 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -24,7 +24,7 @@ * Each time this plugin wants to get events list from subscribers it asks for applicable status * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.12.0 */ public abstract class GHEventsSubscriber implements ExtensionPoint { private static final Logger LOGGER = LoggerFactory.getLogger(GHEventsSubscriber.class); 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 88ecf0c63..3e5c0e47a 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -18,7 +18,7 @@ * Utility class which holds converters or predicates (matchers) to filter or convert job lists * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.12.0 */ public final class JobInfoHelpers { 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 1eb7b759f..35fc05c11 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -34,7 +34,7 @@ * Each manager works with only one hook url (created with {@link #forHookUrl(URL)}) * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.12.0 */ public class WebhookManager { private static final Logger LOGGER = LoggerFactory.getLogger(WebhookManager.class); 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 f6ac3b42f..ed39ba22e 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 @@ -27,7 +27,7 @@ * By default this plugin interested in push events only when job uses {@link GitHubPushTrigger} * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.12.0 */ @Extension @SuppressWarnings("unused") From b2f27265d8c6f947ea70fdc60467a06ead3ef957 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 29 Jul 2015 16:26:40 +0300 Subject: [PATCH 054/228] use Validate state instead of "if" to validate jenkins instance is not null --- src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index be362f26c..e707a5408 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -12,6 +12,7 @@ import hudson.util.Iterators.FilterIterator; import hudson.util.SequentialExecutionQueue; import jenkins.model.Jenkins; +import org.apache.commons.lang3.Validate; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.jenkinsci.plugins.github.internal.GHPluginConfigException; import org.jenkinsci.plugins.github.webhook.GHEventHeader; @@ -174,9 +175,7 @@ public static GitHubWebHook get() { @Nonnull public static Jenkins getJenkinsInstance() throws IllegalStateException { Jenkins instance = Jenkins.getInstance(); - if (instance == null) { - throw new IllegalStateException("Jenkins has not been started, or was already shut down"); - } + Validate.validState(instance != null, "Jenkins has not been started, or was already shut down"); return instance; } From 131a281ca28cfd798dca7578940eacf01ca2ad3b Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 29 Jul 2015 16:29:22 +0300 Subject: [PATCH 055/228] extend FluentIterableWrapper with "first()" method from guava will be useful in repository name --- .../cloudbees/jenkins/GitHubRepositoryName.java | 2 ++ .../plugins/github/util/FluentIterableWrapper.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 63012c9a5..c2203d650 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -118,6 +118,8 @@ protected GHRepository adapt(GitHub item) { * * This is useful if the caller only relies on the read access to the repository and doesn't need to * walk possible candidates. + * + * Result can be null */ public GHRepository resolveOne() { for (GHRepository r : resolve()) 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 5ed5bdfbc..e06d29b33 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java @@ -116,6 +116,20 @@ public final Optional firstMatch(Predicate predicate) { return Iterables.tryFind(iterable, predicate); } + /** + * Returns an {@link Optional} containing the first element in this fluent iterable. + * If the iterable is empty, {@code Optional.absent()} is returned. + * + * @throws NullPointerException if the first element is null; if this is a possibility, use + * {@code iterator().next()} or {@link Iterables#getFirst} instead. + */ + public final Optional first() { + Iterator iterator = iterable.iterator(); + return iterator.hasNext() + ? Optional.of(iterator.next()) + : Optional.absent(); + } + /** * Returns list from wrapped iterable */ From 18e24ade68c16adbd0a290e46e83df508b7f7132 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 29 Jul 2015 16:30:31 +0300 Subject: [PATCH 056/228] add null-safe predicate and function with precheck arg for null --- .../github/util/misc/NullSafeFunction.java | 29 +++++++++++++++++ .../github/util/misc/NullSafePredicate.java | 31 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java 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 new file mode 100644 index 000000000..c318ace42 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java @@ -0,0 +1,29 @@ +package org.jenkinsci.plugins.github.util.misc; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This abstract class calls {@link #applyNullSafe(Object)} only after success validation of inner object for null + * + * {@inheritDoc} + * + * @author lanwen (Merkushev Kirill) + */ +public abstract class NullSafeFunction implements Function { + /** + * {@inheritDoc} + */ + @Override + public T apply(@Nullable F input) { + return applyNullSafe(Preconditions.checkNotNull(input, "This function not allows to use null as argument")); + } + + /** + * This method will be called inside of {@link #apply(Object)} + */ + 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 new file mode 100644 index 000000000..9ba3a4269 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java @@ -0,0 +1,31 @@ +package org.jenkinsci.plugins.github.util.misc; + +import com.google.common.base.Predicate; + +import javax.annotation.Nonnull; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * This abstract class calls {@link #applyNullSafe(Object)} only after success validation of inner object for null + * + * {@inheritDoc} + * + * @author lanwen (Merkushev Kirill) + */ + +public abstract class NullSafePredicate implements Predicate { + + /** + * {@inheritDoc} + */ + @Override + public boolean apply(T input) { + return applyNullSafe(checkNotNull(input, "Argument for this predicate can't be null")); + } + + /** + * This method will be called inside of {@link #apply(Object)} + */ + protected abstract boolean applyNullSafe(@Nonnull T input); +} From 33087ebdaa61576cb5c9489e46cffcce5535f882 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 29 Jul 2015 16:32:34 +0300 Subject: [PATCH 057/228] fix "Unknown content type null" in logs caused by self-test validation for custom hook url --- .../plugins/github/webhook/GHEventPayload.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 873c15da6..fe3543ee6 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java @@ -1,5 +1,6 @@ package org.jenkinsci.plugins.github.webhook; +import com.cloudbees.jenkins.GitHubWebHook; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; @@ -53,6 +54,11 @@ class PayloadHandler extends AnnotationHandler { */ @Override public Object parse(StaplerRequest req, GHEventPayload a, Class type, String param) throws ServletException { + if (req.getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { + // if self test for custom hook url + return null; + } + String contentType = req.getContentType(); if (!PAYLOAD_PROCESS.containsKey(contentType)) { @@ -68,6 +74,7 @@ public Object parse(StaplerRequest req, GHEventPayload a, Class type, String par /** * used for application/x-www-form-urlencoded content-type + * * @return function to extract payload from form request parameters */ protected static Function fromForm() { @@ -80,7 +87,8 @@ public String apply(StaplerRequest request) { } /** - * used for application/json content-type + * used for application/json content-type + * * @return function to extract payload from body */ protected static Function fromApplicationJson() { From 9723aab7fbba7133bc833c8d1c4308f3853689b2 Mon Sep 17 00:00:00 2001 From: MerkushevKirill Date: Wed, 29 Jul 2015 17:13:46 +0300 Subject: [PATCH 058/228] add checkForNull for resolveOne in GHRepoName --- .../java/com/cloudbees/jenkins/GitHubRepositoryName.java | 4 ++-- .../plugins/github/util/misc/NullSafeFunction.java | 6 +----- .../plugins/github/util/misc/NullSafePredicate.java | 5 ----- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index c2203d650..d7732d0f7 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -6,6 +6,7 @@ import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +import javax.annotation.CheckForNull; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -118,9 +119,8 @@ protected GHRepository adapt(GitHub item) { * * This is useful if the caller only relies on the read access to the repository and doesn't need to * walk possible candidates. - * - * Result can be null */ + @CheckForNull public GHRepository resolveOne() { for (GHRepository r : resolve()) return r; 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 c318ace42..4d0e6c02b 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 @@ -8,15 +8,11 @@ /** * This abstract class calls {@link #applyNullSafe(Object)} only after success validation of inner object for null - * - * {@inheritDoc} * * @author lanwen (Merkushev Kirill) */ public abstract class NullSafeFunction implements Function { - /** - * {@inheritDoc} - */ + @Override public T apply(@Nullable F input) { return applyNullSafe(Preconditions.checkNotNull(input, "This function not allows to use null as argument")); 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 9ba3a4269..5e9987d7c 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 @@ -9,16 +9,11 @@ /** * This abstract class calls {@link #applyNullSafe(Object)} only after success validation of inner object for null * - * {@inheritDoc} - * * @author lanwen (Merkushev Kirill) */ public abstract class NullSafePredicate implements Predicate { - /** - * {@inheritDoc} - */ @Override public boolean apply(T input) { return applyNullSafe(checkNotNull(input, "Argument for this predicate can't be null")); From 18aca714ac992d6c08fa4999fd09cd0e396ff50b Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Tue, 4 Aug 2015 20:48:01 +0300 Subject: [PATCH 059/228] [FIXED JENKINS-29787] Fix NPE for race condition. See JENKINS-29794 for details. --- .../java/com/cloudbees/jenkins/GitHubPushTrigger.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 0e58f154d..b924a5de4 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -157,6 +157,10 @@ public void registerHooks() { @Override public void stop() { + if (job == null) { + return; + } + if (getDescriptor().isManageHook()) { Cleaner cleaner = Cleaner.get(); if (cleaner != null) { @@ -167,6 +171,10 @@ public void stop() { @Override public Collection getProjectActions() { + if (job == null) { + return Collections.emptyList(); + } + return Collections.singleton(new GitHubWebHookPollingAction()); } From 038dabad4a85ac93e6acdf6ebc0dfc91d5633d78 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 10 Aug 2015 17:16:52 +0300 Subject: [PATCH 060/228] [maven-release-plugin] prepare release github-1.12.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a40c93524..cdcdc7a32 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.12.1-SNAPSHOT + 1.12.1 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -131,7 +131,7 @@ 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 + github-1.12.1 From a5a2f976b2e602dfbbc4421a6ca98d54e7e7fd14 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 10 Aug 2015 17:16:57 +0300 Subject: [PATCH 061/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index cdcdc7a32..e39d8be4c 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.12.1 + 1.13.0-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -131,7 +131,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.12.1 + HEAD From 5aeec4fea480cbf1f029e0dc226388209392fa86 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 13:09:16 +0300 Subject: [PATCH 062/228] [JENKINS-24702] add deps to migrate to credentials-plugin instead of own plain token usage --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index e39d8be4c..11f214874 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,18 @@ 0.12.1 + + org.jenkins-ci.plugins + credentials + 1.22 + + + + org.jenkins-ci.plugins + plain-credentials + 1.1 + + org.jenkins-ci.plugins multiple-scms From b322d95c0456078be4a7ed9b4152e1991e666f25 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 13:53:14 +0300 Subject: [PATCH 063/228] [JENKINS-24702] new configuration point in global github-plugin - with credentials usage - with migrator from old gh-push-trigger descriptor config options to new gh-plugin - with bean class to store deprecated creds (used only for migration) --- .../plugins/github/GitHubPlugin.java | 89 ++++++ .../github/config/GitHubPluginConfig.java | 170 +++++++++++ .../github/config/GitHubServerConfig.java | 280 ++++++++++++++++++ .../config/GitHubTokenCredentialsCreator.java | 234 +++++++++++++++ .../plugins/github/deprecated/Credential.java | 37 +++ .../plugins/github/migration/Migrator.java | 99 +++++++ .../plugins/github/GitHubPlugin/config.groovy | 7 + .../config/GitHubPluginConfig/config.groovy | 50 ++++ .../config/GitHubServerConfig/config.groovy | 30 ++ .../config.groovy | 47 +++ 10 files changed, 1043 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config.groovy diff --git a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java new file mode 100644 index 000000000..317bd3682 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java @@ -0,0 +1,89 @@ +package org.jenkinsci.plugins.github; + +import hudson.Plugin; +import hudson.model.Descriptor.FormException; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; +import org.jenkinsci.plugins.github.migration.Migrator; +import org.kohsuke.stapler.StaplerRequest; + +import javax.servlet.ServletException; +import java.io.IOException; + +import static java.lang.String.format; +import static org.apache.commons.lang3.Validate.notNull; + +/** + * Main entry point for this plugin + * Stores global configuration + * + * @author lanwen (Merkushev Kirill) + */ +public class GitHubPlugin extends Plugin { + private GitHubPluginConfig configuration = new GitHubPluginConfig(); + + public GitHubPluginConfig getConfiguration() { + return configuration; + } + + /** + * Launched before plugin starts + * Adds alias for {@link GitHubPlugin} to simplify resulting xml + */ + public static void init() { + Jenkins.XSTREAM2.alias("github-plugin", GitHubPlugin.class); + Migrator.enableCompatibilityAliases(); + } + + @Override + public void start() throws Exception { + init(); + load(); + } + + /** + * Launches migration after plugin already initialized + */ + @Override + public void postInitialize() throws Exception { + new Migrator().migrate(); + } + + @Override + public void configure(StaplerRequest req, JSONObject formData) throws IOException, ServletException, FormException { + try { + configuration = req.bindJSON(GitHubPluginConfig.class, formData); + } catch (Exception e) { + throw new FormException( + format("Mailformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); + } + save(); + } + + @Override + protected void load() throws IOException { + super.load(); + if (configuration == null) { + configuration = new GitHubPluginConfig(); + save(); + } + } + + /** + * @return instance of this plugin + */ + public static GitHubPlugin get() { + return notNull(Jenkins.getInstance(), "Jenkins is not ready to return instance") + .getPlugin(GitHubPlugin.class); + } + + /** + * Shortcut method for {@link GitHubPlugin#get()#getConfiguration()}. + * + * @return configuration of plugin + */ + public static GitHubPluginConfig configuration() { + return get().getConfiguration(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java new file mode 100644 index 000000000..7932f9a1c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -0,0 +1,170 @@ +package org.jenkinsci.plugins.github.config; + +import com.cloudbees.jenkins.GitHubWebHook; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.AbstractProject; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import jenkins.model.Jenkins; +import org.apache.commons.codec.binary.Base64; +import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.internal.GHPluginConfigException; +import org.kohsuke.github.GitHub; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; + +/** + * Global configuration to store all GH Plugin settings + * such as hook managing policy, credentials etc. + * + * @author lanwen (Merkushev Kirill) + * @since TODO + */ +public class GitHubPluginConfig extends AbstractDescribableImpl { + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPluginConfig.class); + + private List configs = new ArrayList(); + private URL hookUrl; + private transient boolean overrideHookUrl; + + @DataBoundConstructor + public GitHubPluginConfig() { + } + + public List getConfigs() { + return configs; + } + + @DataBoundSetter + public void setConfigs(List configs) { + this.configs = configs; + } + + public boolean isManageHooks() { + return from(getConfigs()).filter(allowedToManageHooks()).first().isPresent(); + } + + @DataBoundSetter + public void setHookUrl(URL hookUrl) { + if (overrideHookUrl) { + this.hookUrl = hookUrl; + } else { + this.hookUrl = null; + } + } + + @DataBoundSetter + public void setOverrideHookUrl(boolean overrideHookUrl) { + this.overrideHookUrl = overrideHookUrl; + } + + public URL getHookUrl() throws GHPluginConfigException { + try { + return hookUrl != null + ? hookUrl + : new URL(Jenkins.getInstance().getRootUrl() + GitHubWebHook.get().getUrlName() + '/'); + } catch (MalformedURLException e) { + throw new GHPluginConfigException( + "Mailformed GH hook url in global configuration (%s)", e.getMessage() + ); + } + } + + public boolean isOverrideHookURL() { + return hookUrl != null; + } + + /** + * 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) { + // try all the credentials since we don't know which one would work + return from(getConfigs()) + .filter(match) + .transform(loginToGithub()) + .filter(Predicates.notNull()); + } + + public List actions() { + return Collections.singletonList(Jenkins.getInstance().getDescriptor(GitHubTokenCredentialsCreator.class)); + } + + @Extension + public static class GitHubPluginConfigDescriptor extends Descriptor { + + /** + * Used to get current instance identity. It compared with same value when testing hook url availability + */ + @Inject + @SuppressWarnings("unused") + private transient InstanceIdentity identity; + + @Override + public String getDisplayName() { + return "GitHub Plugin Configuration"; + } + + @SuppressWarnings("unused") + public FormValidation doReRegister() { + if (!GitHubPlugin.configuration().isManageHooks()) { + return FormValidation.warning("Works only when Jenkins manages hooks (one ore more creds specified)"); + } + + 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()); + } + + @SuppressWarnings("unused") + public FormValidation doCheckHookUrl(@QueryParameter String value) { + try { + HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty(GitHubWebHook.URL_VALIDATION_HEADER, "true"); + con.connect(); + if (con.getResponseCode() != 200) { + return FormValidation.error("Got %d from %s", con.getResponseCode(), 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. " + + "Are you running your own app?", value); + } + RSAPublicKey key = identity.getPublic(); + String expected = new String(Base64.encodeBase64(key.getEncoded())); + if (!expected.equals(v)) { + // if it responds but with a different ID, that's more likely wrong than correct + return FormValidation.error("%s is connecting to different Jenkins instances", value); + } + + return FormValidation.ok(); + } catch (IOException e) { + return FormValidation.error(e, "Failed to test a connection to %s", value); + } + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java new file mode 100644 index 000000000..02d1eef76 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -0,0 +1,280 @@ +package org.jenkinsci.plugins.github.config; + +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.Predicate; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.security.ACL; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import hudson.util.Secret; +import jenkins.model.Jenkins; +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.github.GitHub; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +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.domains.URIRequirementBuilder.fromUri; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * This object represents configuration of each credentials-github pair. + * If no api url explicitly defined, default url used. + * So one github server can be used with many creds and one token can be used multiply times in lot of gh servers + * + * @author lanwen (Merkushev Kirill) + * @since TODO + */ +@XStreamAlias("github-server-config") +public class GitHubServerConfig extends AbstractDescribableImpl { + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubServerConfig.class); + + /** + * 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"; + + /** + * Used as default token value if no any creds found by given credsId. + */ + private static final String UNKNOWN_TOKEN = "unkn"; + + private String apiUrl; + private boolean dontUseItToMangeHooks; + private final String credentialsId; + + /** + * only to set to default apiUrl when uncheck + */ + private transient boolean custom; + + @DataBoundConstructor + public GitHubServerConfig(String credentialsId) { + this.credentialsId = credentialsId; + } + + /** + * {@link #custom} field should be defined earlier. Because of we get full content of optional block, + * even if it already unchecked. So if we want to return api url to default value - custom value should affect + * + * @param apiUrl custom url if GH. Set api url to default value if custom is unchecked or value is blank + */ + @DataBoundSetter + public void setApiUrl(String apiUrl) { + if (custom) { + this.apiUrl = defaultIfBlank(apiUrl, GITHUB_URL); + } else { + this.apiUrl = GITHUB_URL; + } + } + + /** + * Should be called before {@link #setApiUrl(String)} + * + * @param custom true if optional block "Custom GH Api Url" checked in UI + */ + @DataBoundSetter + public void setCustom(boolean custom) { + this.custom = custom; + } + + /** + * This server config will not be used to manage GH Hooks if true + * + * @param dontUseItToMangeHooks true to ignore this config on hook manipulations + */ + @DataBoundSetter + public void setDontUseItToMangeHooks(boolean dontUseItToMangeHooks) { + this.dontUseItToMangeHooks = dontUseItToMangeHooks; + } + + public String getApiUrl() { + return apiUrl; + } + + /** + * @see #isUrlCustom(String) + */ + public boolean isCustom() { + return isUrlCustom(apiUrl); + } + + public boolean isDontUseItToMangeHooks() { + return dontUseItToMangeHooks; + } + + public String getCredentialsId() { + return credentialsId; + } + + /** + * Checks GH url for equality to default api url + * + * @param apiUrl should be not blank and not equal to default url to return true + * + * @return true if url not blank and not equal to default + */ + public static boolean isUrlCustom(String apiUrl) { + return isNotBlank(apiUrl) && !GITHUB_URL.equals(apiUrl); + } + + /** + * Converts server config to authorized GH instance. If login process is not successful it returns null + * + * @return function to convert config to gh instance + */ + @CheckForNull + public static Function loginToGithub() { + return new NullSafeFunction() { + @Override + public GitHub applyNullSafe(@Nonnull GitHubServerConfig github) { + String accessToken = tokenFor(github.getCredentialsId()); + + try { + if (isNotBlank(github.getApiUrl())) { + return GitHub.connectToEnterprise(github.getApiUrl(), accessToken); + } + + return GitHub.connectUsingOAuth(accessToken); + } catch (IOException e) { + LOGGER.warn("Failed to login with creds {}", github.getCredentialsId(), e); + return null; + } + } + }; + } + + /** + * Tries to find {@link StringCredentials} by id and returns token from it. + * Returns {@link #UNKNOWN_TOKEN} if no any creds found with this id. + * + * @param credentialsId id to find creds + * + * @return token from creds or default non empty string + */ + @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(); + } + + /** + * Returns true if given host is part of stored (or default if blank) api url + * + * @param host host to find in api url + * + * @return predicate to match against {@link GitHubServerConfig} + */ + public static Predicate withHost(final String host) { + return new NullSafePredicate() { + @Override + protected boolean applyNullSafe(@Nonnull GitHubServerConfig github) { + return defaultIfEmpty(github.getApiUrl(), GITHUB_URL).contains(host); + } + }; + } + + /** + * Returns true if config can be used in hooks managing + * + * @return predicate to match against {@link GitHubServerConfig} + */ + public static Predicate allowedToManageHooks() { + return new NullSafePredicate() { + @Override + protected boolean applyNullSafe(@NonNull GitHubServerConfig github) { + return !github.isDontUseItToMangeHooks(); + } + }; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return "GitHub Server Config"; + } + + @SuppressWarnings("unused") + public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl) { + if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + return new ListBoxModel(); + } + return new StandardListBoxModel() + .withEmptySelection() + .withAll(lookupCredentials( + StringCredentials.class, + Jenkins.getInstance(), + ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()) + ); + } + + @SuppressWarnings("unused") + public FormValidation doVerifyCredentials( + @QueryParameter String apiUrl, @QueryParameter String credentialsId) throws IOException { + try { + GitHub gitHub; + if (isNotBlank(apiUrl)) { + gitHub = GitHub.connectToEnterprise(apiUrl, tokenFor(credentialsId)); + } else { + gitHub = GitHub.connectUsingOAuth(tokenFor(credentialsId)); + } + + if (gitHub.isCredentialValid()) { + return FormValidation.ok("Credentials verifyed, rate limit: %s", gitHub.getRateLimit().remaining); + } else { + return FormValidation.error("Failed to validate the account"); + } + } catch (IOException e) { + return FormValidation.error(e, "Failed to validate the account"); + } + } + + @SuppressWarnings("unused") + public FormValidation doCheckApiUrl(@QueryParameter String value) { + try { + new URL(value); + } catch (MalformedURLException e) { + return FormValidation.error("Mailformed GitHub url (%s)", e.getMessage()); + } + + if (GITHUB_URL.equals(value)) { + return FormValidation.ok(); + } + + if (value.endsWith("/api/v3") || value.endsWith("/api/v3/")) { + return FormValidation.ok(); + } + + return FormValidation.warning("GitHub Enterprise API URL ends with \"/api/v3\""); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java new file mode 100644 index 000000000..fa8a42423 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java @@ -0,0 +1,234 @@ +package org.jenkinsci.plugins.github.config; + +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.domains.DomainSpecification; +import com.cloudbees.plugins.credentials.domains.HostnameSpecification; +import com.cloudbees.plugins.credentials.domains.SchemeSpecification; +import hudson.Extension; +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.security.ACL; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import hudson.util.Secret; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; +import org.kohsuke.github.GHAuthorization; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.stapler.QueryParameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.UUID; + +import static com.cloudbees.plugins.credentials.CredentialsMatchers.firstOrNull; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.withId; +import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials; +import static com.cloudbees.plugins.credentials.domains.URIRequirementBuilder.fromUri; +import static java.lang.String.format; +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; +import static org.kohsuke.github.GHAuthorization.REPO_STATUS; + + +/** + * Helper class to convert username+password credentials or directly login+password to GH token + * and save it as token credentials with help of plain-credentials plugin + * + * @author lanwen (Merkushev Kirill) + * @since TODO + */ +@Extension +public class GitHubTokenCredentialsCreator extends Descriptor implements + Describable { + + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubTokenCredentialsCreator.class); + + /** + * Default scope required for this plugin. + * + * - admin:repo_hook - for managing hooks (read, write and delete old ones) + * - repo - to see private repos + * - repo:status - to manipulate commit statuses + */ + public static final List GH_PLUGIN_REQUIRED_SCOPE = asList( + AMIN_HOOK, + REPO, + REPO_STATUS + ); + + public GitHubTokenCredentialsCreator() { + super(GitHubTokenCredentialsCreator.class); + } + + @Override + public GitHubTokenCredentialsCreator getDescriptor() { + return this; + } + + @Override + public String getDisplayName() { + return "Convert login and password to token"; + } + + @SuppressWarnings("unused") + public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl) { + if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + return new ListBoxModel(); + } + return new StandardUsernameListBoxModel() + .withEmptySelection() + .withAll(lookupCredentials( + StandardUsernamePasswordCredentials.class, + Jenkins.getInstance(), + ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()) + ); + } + + @SuppressWarnings("unused") + public FormValidation doCreateTokenByCredentials( + @QueryParameter String apiUrl, + @QueryParameter String credentialsId) { + + if (isEmpty(credentialsId)) { + return FormValidation.error("Please specify credentials to create token"); + } + + StandardUsernamePasswordCredentials creds = firstOrNull(lookupCredentials( + StandardUsernamePasswordCredentials.class, + Jenkins.getInstance(), + ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()), + withId(credentialsId)); + + GHAuthorization token; + + try { + token = createToken( + notNull(creds, "Why selected creds is null?").getUsername(), + creds.getPassword().getPlainText(), + defaultIfBlank(apiUrl, GITHUB_URL) + ); + } catch (IOException e) { + return FormValidation.error(e, "Can't create GH token - %s", e.getMessage()); + } + + StandardCredentials credentials = createCredentials(apiUrl, token.getToken(), creds.getUsername()); + + return FormValidation.ok("Created credentials with id %s (can use it for GitHub Server Config)", + credentials.getId()); + } + + @SuppressWarnings("unused") + public FormValidation doCreateTokenByPassword( + @QueryParameter String apiUrl, + @QueryParameter String login, + @QueryParameter String password) { + + try { + GHAuthorization token = createToken(login, password, defaultIfBlank(apiUrl, GITHUB_URL)); + StandardCredentials credentials = createCredentials(apiUrl, token.getToken(), login); + + return FormValidation.ok( + "Created credentials with id %s (can use it for GitHub Server Config)", + credentials.getId()); + } catch (IOException e) { + return FormValidation.error(e, "Can't create GH token for %s - %s", login, e.getMessage()); + } + } + + /** + * Can be used to convert given login and password to GH personal token as more secured way to interact with api + * + * @param username gh login + * @param password gh password + * @param apiUrl gh api url. Can be null or empty to default + * + * @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, + @Nullable String apiUrl) throws IOException { + GitHub gitHub = new GitHubBuilder() + .withEndpoint(defaultIfBlank(apiUrl, GITHUB_URL)) + .withPassword(username, password) + .build(); + + return gitHub.createToken( + GH_PLUGIN_REQUIRED_SCOPE, + format("Jenkins GitHub Plugin token (%s)", Jenkins.getInstance().getRootUrl()), + Jenkins.getInstance().getRootUrl() + ); + } + + /** + * Creates {@link org.jenkinsci.plugins.plaincredentials.StringCredentials} with previously created GH token. + * Adds them to domain extracted from server url (will be generated if no any exists before). + * Domain will have domain requirements consists of scheme and host from serverAPIUrl arg + * + * @param serverAPIUrl to add to domain with host and scheme requirement from this url + * @param token GH Personal token + * @param username used to add to description of newly created creds + * + * @return credentials object + * @see #createCredentials(String, StandardCredentials) + */ + public StandardCredentials createCredentials(@Nullable String serverAPIUrl, String token, String username) { + String url = defaultIfBlank(serverAPIUrl, GITHUB_URL); + String description = format("GitHub (%s) auto generated token credentials for %s", url, username); + StringCredentialsImpl creds = new StringCredentialsImpl( + CredentialsScope.GLOBAL, + UUID.randomUUID().toString(), + description, + Secret.fromString(token)); + return createCredentials(url, creds); + } + + /** + * Saves given creds in jenkins for domain extracted from server api url + * + * @param serverAPIUrl to extract (and create if no any) domain + * @param credentials creds to save + * + * @return saved creds + */ + private StandardCredentials createCredentials(@Nonnull String serverAPIUrl, + final StandardCredentials credentials) { + URI serverUri = URI.create(defaultIfBlank(serverAPIUrl, GITHUB_URL)); + + List specifications = asList( + new SchemeSpecification(serverUri.getScheme()), + new HostnameSpecification(serverUri.getHost(), null) + ); + + final Domain domain = new Domain(serverUri.getHost(), "GitHub domain (autogenerated)", specifications); + ACL.impersonate(ACL.SYSTEM, new Runnable() { // do it with system rights + @Override + public void run() { + try { + new SystemCredentialsProvider.StoreImpl().addDomain(domain, credentials); + } catch (IOException e) { + LOGGER.error("Can't add creds for domain", e); + } + } + }); + + return credentials; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java b/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java new file mode 100644 index 000000000..ec2d8f69b --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java @@ -0,0 +1,37 @@ +package org.jenkinsci.plugins.github.deprecated; + +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Credential to access GitHub. + * Used only for migration. + * + * @author Kohsuke Kawaguchi + * @deprecated Please use {@link org.jenkinsci.plugins.github.config.GitHubServerConfig} instead + * + */ +@Deprecated +public class Credential { + private final transient String username; + private final transient String apiUrl; + private final transient String oauthAccessToken; + + @DataBoundConstructor + public Credential(String username, String apiUrl, String oauthAccessToken) { + this.username = username; + this.apiUrl = apiUrl; + this.oauthAccessToken = oauthAccessToken; + } + + public String getUsername() { + return username; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getOauthAccessToken() { + return oauthAccessToken; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java new file mode 100644 index 000000000..a2bb91b17 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java @@ -0,0 +1,99 @@ +package org.jenkinsci.plugins.github.migration; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator; +import org.jenkinsci.plugins.github.deprecated.Credential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.isUrlCustom; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; + +/** + * Helper class incapsulates migration process from old configs to new ones + * After 1.12.0 this plugin uses {@link GitHubPlugin} to store all global configuration instead of + * push trigger descriptor + * + * @author lanwen (Merkushev Kirill) + * @since TODO + */ +public class Migrator { + private static final Logger LOGGER = LoggerFactory.getLogger(Migrator.class); + + /** + * Loads {@link GitHubPushTrigger.DescriptorImpl} and migrate all values + * to {@link org.jenkinsci.plugins.github.config.GitHubPluginConfig} + * + * @throws IOException if any read-save problems as it critical to work process of this plugin + */ + public void migrate() throws IOException { + LOGGER.debug("Check if GitHub Plugin needs config migration"); + GitHubPushTrigger.DescriptorImpl descriptor = GitHubPushTrigger.DescriptorImpl.get(); + descriptor.load(); + + if (isNotEmpty(descriptor.getCredentials())) { + LOGGER.warn("Migration for old GitHub Plugin credentials started"); + GitHubPlugin.configuration().getConfigs().addAll( + from(descriptor.getCredentials()).transform(toGHServerConfig()).toList() + ); + + descriptor.clearCredentials(); + descriptor.save(); + GitHubPlugin.get().save(); + } + + if (descriptor.getDeprecatedHookUrl() != null) { + LOGGER.warn("Migration for old GitHub Plugin hook url started"); + GitHubPlugin.configuration().setOverrideHookUrl(true); + GitHubPlugin.configuration().setHookUrl(descriptor.getDeprecatedHookUrl()); + descriptor.clearDeprecatedHookUrl(); + descriptor.save(); + GitHubPlugin.get().save(); + } + } + + /** + * Creates new string credentials from token + * + * @return converter to get all useful info from old plain creds and crete new server config + */ + @VisibleForTesting + protected Function toGHServerConfig() { + return new Function() { + @Override + public GitHubServerConfig apply(Credential input) { + LOGGER.info("Migrate GitHub Plugin creds for {} {}", input.getUsername(), input.getApiUrl()); + GitHubTokenCredentialsCreator creator = + Jenkins.getInstance().getDescriptorByType(GitHubTokenCredentialsCreator.class); + + StandardCredentials credentials = creator.createCredentials( + input.getApiUrl(), + input.getOauthAccessToken(), + input.getUsername() + ); + + GitHubServerConfig gitHubServerConfig = new GitHubServerConfig(credentials.getId()); + gitHubServerConfig.setCustom(isUrlCustom(input.getApiUrl())); + gitHubServerConfig.setApiUrl(input.getApiUrl()); + + return gitHubServerConfig; + } + }; + } + + /** + * - Old plain credentials moved to deprecated package as used only for migration + */ + public static void enableCompatibilityAliases() { + Jenkins.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.Credential", Credential.class); + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy new file mode 100644 index 000000000..cf5317d0f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.GitHubPlugin + +def st = namespace("jelly:stapler"); + +set("instance", my.configuration); +set("descriptor", instance.descriptor); +st.include(from: descriptor, page: descriptor.configPage, optional: false) 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 new file mode 100644 index 000000000..c42a0dad2 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy @@ -0,0 +1,50 @@ +package org.jenkinsci.plugins.github.config.GitHubPluginConfig + +import com.cloudbees.jenkins.GitHubPushTrigger + +def f = namespace(lib.FormTagLib); + +f.section(title: descriptor.displayName) { + + f.entry(title: _("Servers configs with credentials to manage GitHub integrations"), + description: _("List of GitHub Servers to manage hooks, set commit statuses etc.")) { + f.repeatableHeteroProperty( + field: "configs", + hasHeader: "true", + addCaption: _("Add GitHub Server Config"), + deleteCaption: _("Delete config")) + } + + if (instance.manageHooks) { + f.validateButton( + title: _("Re-register hooks for all jobs"), + progress: _("Scanning all items..."), + method: "reRegister" + ) + } + + f.advanced() { + 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"), + inline: true, + field: "overrideHookUrl", + checked: instance.overrideHookURL) { + f.entry(field: "hookUrl") { + f.textbox() + } + } + } + } + } + + f.entry(title: _("Additional actions")) { + f.hetero_list(items: [], + addCaption: _("Manage additional GitHub actions"), + name: "actions", + oneEach: "true", hasHeader: "true", descriptors: instance.actions()) + } + } +} + 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 new file mode 100644 index 000000000..f40b07c21 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config.groovy @@ -0,0 +1,30 @@ +package org.jenkinsci.plugins.github.config.GitHubServerConfig + +import org.jenkinsci.plugins.github.config.GitHubServerConfig + +def f = namespace(lib.FormTagLib); +def c = namespace(lib.CredentialsTagLib) + + +f.entry(title: _("Don't manage hooks with this config")) { + f.checkbox( field: "dontUseItToMangeHooks") +} + +f.entry(title: _("Credentials"), field: "credentialsId") { + c.select() +} + +f.optionalBlock(title: _("Custom GitHub API URL"), inline: true, name: "custom", checked: instance?.custom) { + f.entry(title: _("GitHub API URL"), field: "apiUrl") { + f.textbox(default: GitHubServerConfig.GITHUB_URL) + } +} + +f.block() { + f.validateButton( + title: _("Verify credentials"), + progress: _("Verifying..."), + method: "verifyCredentials", + with: "apiUrl,credentialsId" + ) +} 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 new file mode 100644 index 000000000..98b4f321a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/config.groovy @@ -0,0 +1,47 @@ +package org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator + +import org.jenkinsci.plugins.github.config.GitHubServerConfig + +def f = namespace(lib.FormTagLib); +def c = namespace(lib.CredentialsTagLib) + +f.entry(title: _("GitHub API URL"), field: "apiUrl") { + f.textbox(default: GitHubServerConfig.GITHUB_URL) +} + +f.radioBlock(checked: true, name: "creds", value: "plugin", title: "From credentials") { + f.entry(title: _("Credentials"), field: "credentialsId") { + c.select() + } + + f.block() { + f.validateButton( + title: _("Create token credentials"), + progress: _("Creating..."), + method: "createTokenByCredentials", + with: "apiUrl,credentialsId" + ) + } +} + +f.radioBlock(checked: false, name: "creds", value: "manually", title: "From login and password") { + + f.entry(title: _("Login"), field: "login") { + f.textbox() + } + + f.entry(title: _("Password"), field: "password") { + f.password() + } + + f.block() { + f.validateButton( + title: _("Create token credentials"), + progress: _("Creating..."), + method: "createTokenByPassword", + with: "apiUrl,login,password" + ) + } +} + + From 456cc65b76a8f0fc5bc5fb75f2cb1fbc4bb73a78 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 14:24:53 +0300 Subject: [PATCH 064/228] use slf4j logger in gh-push-trigger class --- .../cloudbees/jenkins/GitHubPushTrigger.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index b924a5de4..7fb7244aa 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -24,6 +24,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.File; @@ -40,8 +42,6 @@ import java.util.Date; import java.util.List; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; /** * Triggers a build when we receive a GitHub post-commit webhook. @@ -85,17 +85,17 @@ private boolean runPolling() { return result; } catch (Error e) { e.printStackTrace(listener.error("Failed to record SCM polling")); - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + LOGGER.error("Failed to record SCM polling", e); throw e; } catch (RuntimeException e) { e.printStackTrace(listener.error("Failed to record SCM polling")); - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + LOGGER.error("Failed to record SCM polling", e); throw e; } finally { listener.close(); } } catch (IOException e) { - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + LOGGER.error("Failed to record SCM polling", e); } return false; } @@ -107,13 +107,13 @@ public void run() { try { cause = new GitHubPushCause(getLogFile(), pushBy); } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to parse the polling log",e); + LOGGER.warn("Failed to parse the polling log", e); cause = new GitHubPushCause(pushBy); } if (job.scheduleBuild(cause)) { - LOGGER.info("SCM changes detected in "+ job.getName()+". Triggering "+name); + LOGGER.info("SCM changes detected in " + job.getName() + ". Triggering " + name); } else { - LOGGER.info("SCM changes detected in "+ job.getName()+". Job is already in the queue"); + LOGGER.info("SCM changes detected in " + job.getName() + ". Job is already in the queue"); } } } @@ -346,5 +346,5 @@ public static boolean allowsHookUrlOverride() { */ public static boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(GitHubPushTrigger.class.getName() + ".disableOverride"); - private static final Logger LOGGER = Logger.getLogger(GitHubPushTrigger.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPushTrigger.class); } From 7b69cf4ac692abebb08977a3b4c776844e0e0749 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 14:29:54 +0300 Subject: [PATCH 065/228] use slf4j logger in gh-repo-name + reformat some code + add getters for fields --- .../jenkins/GitHubRepositoryName.java | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index d7732d0f7..859d9f7d5 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -5,15 +5,16 @@ import org.kohsuke.github.GHCommitPointer; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +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.Arrays; import java.util.Iterator; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,55 +25,59 @@ */ public class GitHubRepositoryName { + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubRepositoryName.class); + private static final Pattern[] URL_PATTERNS = { - /** - * The first set of patterns extract the host, owner and repository names - * 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"), - /** - * 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("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"), - Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"), - Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"), - Pattern.compile("ssh://git@([^/]+)/([^/]+)/([^/]+)/?") + /** + * The first set of patterns extract the host, owner and repository names + * 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"), + /** + * 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("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"), + Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"), + Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"), + Pattern.compile("ssh://git@([^/]+)/([^/]+)/([^/]+)/?") }; /** * Create {@link GitHubRepositoryName} from URL - * - * @param url - * must be non-null + * + * @param url must be non-null + * * @return parsed {@link GitHubRepositoryName} or null if it cannot be - * parsed from the specified URL + * parsed from the specified URL */ - public static GitHubRepositoryName create(final String url) { - LOGGER.log(Level.FINE, "Constructing from URL {0}", url); + @CheckForNull + public static GitHubRepositoryName create(@Nonnull final String url) { + LOGGER.debug("Constructing from URL {}", url); for (Pattern p : URL_PATTERNS) { - Matcher m = p.matcher(url.trim()); + Matcher m = p.matcher(trimToEmpty(url)); if (m.matches()) { - LOGGER.log(Level.FINE, "URL matches {0}", m); - GitHubRepositoryName ret = new GitHubRepositoryName(m.group(1), m.group(2), - m.group(3)); - LOGGER.log(Level.FINE, "Object is {0}", ret); + LOGGER.debug("URL matches {}", m); + GitHubRepositoryName ret = new GitHubRepositoryName(m.group(1), m.group(2), m.group(3)); + LOGGER.debug("Object is {}", ret); return ret; } } - LOGGER.log(Level.WARNING, "Could not match URL {0}", url); + LOGGER.warn("Could not match URL {}", url); return null; } - public final String host, userName, repositoryName; + public final String host; + public final String userName; + public final String repositoryName; public GitHubRepositoryName(String host, String userName, String repositoryName) { this.host = host; @@ -80,6 +85,17 @@ public GitHubRepositoryName(String host, String userName, String repositoryName) this.repositoryName = repositoryName; } + public String getHost() { + return host; + } + + public String getUserName() { + return userName; + } + + public String getRepositoryName() { + return repositoryName; + } /** * Resolves this name to the actual reference by {@link GHRepository}. * @@ -141,8 +157,8 @@ protected boolean filter(V v) { */ public boolean matches(GHCommitPointer commit) { return userName.equals(commit.getUser().getLogin()) - && repositoryName.equals(commit.getRepository().getName()) - && host.equals(commit.getRepository().getHtmlUrl().getHost()); + && repositoryName.equals(commit.getRepository().getName()) + && host.equals(commit.getRepository().getHtmlUrl().getHost()); } /** @@ -150,8 +166,8 @@ public boolean matches(GHCommitPointer commit) { */ public boolean matches(GHRepository repo) throws IOException { return userName.equals(repo.getOwner().getLogin()) // TODO: use getOwnerName - && repositoryName.equals(repo.getName()) - && host.equals(repo.getHtmlUrl().getHost()); + && repositoryName.equals(repo.getName()) + && host.equals(repo.getHtmlUrl().getHost()); } @Override @@ -174,5 +190,4 @@ public String toString() { return "GitHubRepository[host="+host+",username="+userName+",repository="+repositoryName+"]"; } - private static final Logger LOGGER = Logger.getLogger(GitHubRepositoryName.class.getName()); } From fa7e64a7c59cf9b0553dcab33e90aeeca236b459 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 14:34:16 +0300 Subject: [PATCH 066/228] use commons-lang3 to generate hash code, to string and equals for gh-repo-name --- .../jenkins/GitHubRepositoryName.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 859d9f7d5..03d17a2b2 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -2,6 +2,9 @@ import hudson.util.AdaptedIterator; import hudson.util.Iterators.FilterIterator; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.kohsuke.github.GHCommitPointer; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; @@ -18,6 +21,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; +import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE; /** * Uniquely identifies a repository on GitHub. * @@ -171,23 +177,19 @@ public boolean matches(GHRepository repo) throws IOException { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - GitHubRepositoryName that = (GitHubRepositoryName) o; - - return repositoryName.equals(that.repositoryName) && userName.equals(that.userName) && host.equals(that.host); + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); } @Override public int hashCode() { - return Arrays.hashCode(new Object[] {host, userName, repositoryName}); + return new HashCodeBuilder().append(host).append(userName).append(repositoryName).build(); } @Override public String toString() { - return "GitHubRepository[host="+host+",username="+userName+",repository="+repositoryName+"]"; + return new ToStringBuilder(this, SHORT_PREFIX_STYLE) + .append("host", host).append("username", userName).append("repository", repositoryName).build(); } } From 808eeafe02b59d06b5c05c02082ffbac0c9c39ef Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 14:36:38 +0300 Subject: [PATCH 067/228] [JENKINS-24702] remove usage of old configs cleanup all gh-push-trigger descriptor related lines --- .../java/com/cloudbees/jenkins/Cleaner.java | 3 +- .../com/cloudbees/jenkins/Credential.java | 64 -------- .../cloudbees/jenkins/GitHubPushTrigger.java | 150 ++++++++---------- .../jenkins/GitHubRepositoryName.java | 93 ++++++----- .../com/cloudbees/jenkins/GitHubWebHook.java | 50 +----- .../github/webhook/WebhookManager.java | 9 +- .../cloudbees/jenkins/Credential/config.jelly | 12 -- .../jenkins/GitHubPushTrigger/global.jelly | 36 ----- .../GitHubPushTriggerConfigSubmitTest.java | 106 ------------- .../github/webhook/WebhookManagerTest.java | 38 ++++- ...om.cloudbees.jenkins.GitHubPushTrigger.xml | 12 -- .../config.xml | 35 ---- 12 files changed, 156 insertions(+), 452 deletions(-) delete mode 100644 src/main/java/com/cloudbees/jenkins/Credential.java delete mode 100644 src/main/resources/com/cloudbees/jenkins/Credential/config.jelly delete mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly delete mode 100644 src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java delete mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml delete mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index 2d2362f0a..40e44acd7 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -5,6 +5,7 @@ import hudson.model.PeriodicWork; import hudson.triggers.Trigger; import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.webhook.WebhookManager; import java.net.URL; @@ -55,7 +56,7 @@ public long getRecurrencePeriod() { */ @Override protected void doRun() throws Exception { - URL url = Trigger.all().get(GitHubPushTrigger.DescriptorImpl.class).getHookUrl(); + URL url = GitHubPlugin.configuration().getHookUrl(); List jobs = Jenkins.getInstance().getAllItems(AbstractProject.class); List aliveRepos = from(jobs) diff --git a/src/main/java/com/cloudbees/jenkins/Credential.java b/src/main/java/com/cloudbees/jenkins/Credential.java deleted file mode 100644 index 71a689421..000000000 --- a/src/main/java/com/cloudbees/jenkins/Credential.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.cloudbees.jenkins; - -import hudson.Extension; -import hudson.Util; -import hudson.model.AbstractDescribableImpl; -import hudson.model.Descriptor; -import hudson.util.FormValidation; -import hudson.util.Secret; -import org.kohsuke.github.GitHub; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; - -import java.io.IOException; - -/** - * Credential to access GitHub. - * - * @author Kohsuke Kawaguchi - */ -public class Credential extends AbstractDescribableImpl { - public final String username; - public final String apiUrl; - public final String oauthAccessToken; - - @DataBoundConstructor - public Credential(String username, String apiUrl, String oauthAccessToken) { - this.username = username; - this.apiUrl = apiUrl; - this.oauthAccessToken = oauthAccessToken; - } - - public GitHub login() throws IOException { - if (Util.fixEmpty(apiUrl) != null) { - return GitHub.connectToEnterprise(apiUrl,oauthAccessToken); - } - return GitHub.connect(username,oauthAccessToken); - } - - @Extension - public static class DescriptorImpl extends Descriptor { - @Override - public String getDisplayName() { - return ""; // unused - } - - public FormValidation doValidate(@QueryParameter String apiUrl, @QueryParameter String username, @QueryParameter String oauthAccessToken) throws IOException { - try { - GitHub gitHub; - if (Util.fixEmpty(apiUrl) != null) { - gitHub = GitHub.connectToEnterprise(apiUrl,oauthAccessToken); - } else { - gitHub = GitHub.connect(username,oauthAccessToken); - } - - if (gitHub.isCredentialValid()) - return FormValidation.ok("Verified"); - else - return FormValidation.error("Failed to validate the account"); - } catch (IOException e) { - return FormValidation.error(e,"Failed to validate the account"); - } - } - } -} diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 7fb7244aa..1660cbc9f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -1,9 +1,9 @@ package com.cloudbees.jenkins; import com.google.common.base.Charsets; -import com.google.common.base.Function; import hudson.Extension; import hudson.Util; +import hudson.XmlFile; import hudson.console.AnnotatedLargeText; import hudson.model.AbstractProject; import hudson.model.Action; @@ -11,38 +11,34 @@ import hudson.model.Project; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; -import hudson.util.FormValidation; import hudson.util.SequentialExecutionQueue; import hudson.util.StreamTaskListener; import jenkins.model.Jenkins; import jenkins.model.Jenkins.MasterComputer; -import net.sf.json.JSONObject; -import org.apache.commons.codec.binary.Base64; import org.apache.commons.jelly.XMLOutput; -import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; +import org.jenkinsci.plugins.github.deprecated.Credential; import org.jenkinsci.plugins.github.internal.GHPluginConfigException; +import org.jenkinsci.plugins.github.migration.Migrator; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.io.PrintStream; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.security.interfaces.RSAPublicKey; import java.text.DateFormat; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; +import static org.apache.commons.lang3.StringUtils.isEmpty; + /** * Triggers a build when we receive a GitHub post-commit webhook. * @@ -138,7 +134,7 @@ public Set getGitHubRepositories() { @Override public void start(AbstractProject project, boolean newInstance) { super.start(project, newInstance); - if (newInstance && getDescriptor().isManageHook()) { + if (newInstance && GitHubPlugin.configuration().isManageHooks()) { registerHooks(); } } @@ -154,14 +150,13 @@ public void registerHooks() { GitHubWebHook.get().registerHookFor(job); } - @Override public void stop() { if (job == null) { return; } - if (getDescriptor().isManageHook()) { + if (GitHubPlugin.configuration().isManageHooks()) { Cleaner cleaner = Cleaner.get(); if (cleaner != null) { cleaner.onStop(job); @@ -219,19 +214,11 @@ public void writeLogTo(XMLOutput out) throws IOException { @Extension public static class DescriptorImpl extends TriggerDescriptor { - private static final Logger LOGGER = Logger.getLogger(DescriptorImpl.class.getName()); private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(MasterComputer.threadPoolForRemoting); - private boolean manageHook; - private String hookUrl; - private volatile List credentials = new ArrayList(); - - @Inject - private transient InstanceIdentity identity; + private transient String hookUrl; - public DescriptorImpl() { - load(); - } + private transient List credentials; @Override public boolean isApplicable(Item item) { @@ -245,91 +232,80 @@ public String getDisplayName() { /** * True if Jenkins should auto-manage hooks. + * + * @deprecated Use {@link GitHubPluginConfig#isManageHooks()} instead */ + @Deprecated public boolean isManageHook() { - return manageHook; - } - - public void setManageHook(boolean v) { - manageHook = v; - save(); + return GitHubPlugin.configuration().isManageHooks(); } /** * Returns the URL that GitHub should post. + * + * @deprecated use {@link GitHubPluginConfig#getHookUrl()} instead */ + @Deprecated public URL getHookUrl() throws GHPluginConfigException { - try { - return hookUrl != null - ? new URL(hookUrl) - : new URL(Jenkins.getInstance().getRootUrl() + GitHubWebHook.get().getUrlName() + '/'); - } catch (MalformedURLException e) { - throw new GHPluginConfigException( - "Mailformed GH hook url in global configuration (%s)", e.getMessage() - ); - } - } - - public boolean hasOverrideURL() { - return hookUrl != null; + return GitHubPlugin.configuration().getHookUrl(); } + /** + * @return null after migration + * @deprecated use {@link GitHubPluginConfig#getConfigs()} instead. + */ + @Deprecated public List getCredentials() { return credentials; } - @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { - JSONObject hookMode = json.getJSONObject("hookMode"); - manageHook = "auto".equals(hookMode.getString("value")); - if (hookMode.optBoolean("hasHookUrl")) { - hookUrl = hookMode.optString("hookUrl"); - } else { - hookUrl = null; + /** + * Used only for migration + * + * @return null after migration + * @deprecated use {@link GitHubPluginConfig#getHookUrl()} + */ + @Deprecated + public URL getDeprecatedHookUrl() { + if (isEmpty(hookUrl)) { + return null; } - credentials = req.bindJSONToList(Credential.class, hookMode.get("credentials")); - save(); - return true; - } - - public FormValidation doCheckHookUrl(@QueryParameter String value) { try { - HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection(); - con.setRequestMethod("POST"); - con.setRequestProperty(GitHubWebHook.URL_VALIDATION_HEADER, "true"); - con.connect(); - if (con.getResponseCode()!=200) { - return FormValidation.error("Got "+con.getResponseCode()+" from "+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 " + value + " is talking to any Jenkins. Are you running your own app?"); - } - RSAPublicKey key = identity.getPublic(); - String expected = new String(Base64.encodeBase64(key.getEncoded())); - if (!expected.equals(v)) { - // if it responds but with a different ID, that's more likely wrong than correct - return FormValidation.error(value+" is connecting to different Jenkins instances"); - } - - return FormValidation.ok(); - } catch (IOException e) { - return FormValidation.error(e,"Failed to test a connection to "+value); + return new URL(hookUrl); + } catch (MalformedURLException e) { + LOGGER.warn("Mailformed hook url skipped while migration ({})", e.getMessage()); + return null; } + } + /** + * Used to cleanup after migration + */ + public void clearDeprecatedHookUrl() { + this.hookUrl = null; } - @SuppressWarnings("unused") - public FormValidation doReRegister() { - if (!manageHook) { - return FormValidation.warning("Works only when Jenkins manages hooks"); - } + /** + * Used to cleanup after migration + */ + public void clearCredentials() { + this.credentials = null; + } - List registered = GitHubWebHook.get().reRegisterAllHooks(); + /** + * @deprecated use {@link GitHubPluginConfig#isOverrideHookURL()} + */ + @Deprecated + public boolean hasOverrideURL() { + return GitHubPlugin.configuration().isOverrideHookURL(); + } - LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", registered.size()); - return FormValidation.ok("Called re-register hooks for %s jobs", registered.size()); + /** + * Uses global xstream to enable migration alias used in {@link Migrator#enableCompatibilityAliases()} + */ + @Override + protected XmlFile getConfigFile() { + return new XmlFile(Jenkins.XSTREAM2, super.getConfigFile().getFile()); } public static DescriptorImpl get() { diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 03d17a2b2..46de1c258 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -1,10 +1,14 @@ package com.cloudbees.jenkins; -import hudson.util.AdaptedIterator; -import hudson.util.Iterators.FilterIterator; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.kohsuke.github.GHCommitPointer; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; @@ -14,16 +18,17 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Arrays; -import java.util.Iterator; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static com.google.common.base.Predicates.and; +import static com.google.common.base.Predicates.notNull; import static java.lang.String.format; import static org.apache.commons.lang3.StringUtils.trimToEmpty; import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.withHost; +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; + /** * Uniquely identifies a repository on GitHub. * @@ -102,38 +107,42 @@ public String getUserName() { public String getRepositoryName() { return repositoryName; } + + /** + * Resolves this name to the actual reference by {@link GHRepository} + * + * Shortcut for {@link #resolve(Predicate)} with always true predicate + * ({@link Predicates#alwaysTrue()}) as argument + */ + public Iterable resolve() { + return resolve(Predicates.alwaysTrue()); + } + /** * Resolves this name to the actual reference by {@link GHRepository}. * - *

- * Since the system can store multiple credentials, and only some of them might be able to see this name in question, - * this method uses {@link GitHubWebHook#login(String, String)} and attempt to find the right credential that can + * Since the system can store multiple credentials, + * and only some of them might be able to see this name in question, + * this method uses {@link org.jenkinsci.plugins.github.config.GitHubPluginConfig#findGithubConfig(Predicate)} + * and attempt to find the right credential that can * access this repository. * - *

+ * Any predicate as argument will be combined with {@link GitHubServerConfig#withHost(String)} to find only + * corresponding for this repo name authenticated github repository + * * This method walks multiple repositories for each credential that can access the repository. Depending on * what you are trying to do with the repository, you might have to keep trying until a {@link GHRepository} * with suitable permission is returned. + * + * @param predicate helps to filter only useful for resolve {@link GitHubServerConfig}s + * + * @return iterable with lazy login process for getting authenticated repos + * @since TODO */ - public Iterable resolve() { - return new Iterable() { - public Iterator iterator() { - return filterNull(new AdaptedIterator(GitHubWebHook.get().login(host,userName)) { - protected GHRepository adapt(GitHub item) { - try { - GHRepository repo = item.getUser(userName).getRepository(repositoryName); - if (repo == null) { - repo = item.getOrganization(userName).getRepository(repositoryName); - } - return repo; - } catch (IOException e) { - LOGGER.log(Level.WARNING,"Failed to obtain repository "+this,e); - return null; - } - } - }); - } - }; + public Iterable resolve(Predicate predicate) { + return from(GitHubPlugin.configuration().findGithubConfig(and(withHost(host), predicate))) + .transform(toGHRepository(this)) + .filter(notNull()); } /** @@ -144,18 +153,7 @@ protected GHRepository adapt(GitHub item) { */ @CheckForNull public GHRepository resolveOne() { - for (GHRepository r : resolve()) - return r; - return null; - } - - private Iterator filterNull(Iterator itr) { - return new FilterIterator(itr) { - @Override - protected boolean filter(V v) { - return v!=null; - } - }; + return from(resolve()).first().orNull(); } /** @@ -192,4 +190,17 @@ public String toString() { .append("host", host).append("username", userName).append("repository", repositoryName).build(); } + private static Function toGHRepository(final GitHubRepositoryName repoName) { + return new NullSafeFunction() { + @Override + protected GHRepository applyNullSafe(@Nonnull GitHub gitHub) { + try { + return gitHub.getRepository(format("%s/%s", repoName.getUserName(), repoName.getRepositoryName())); + } catch (IOException e) { + LOGGER.warn("Failed to obtain repository {}", this, e); + return null; + } + } + }; + } } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index e707a5408..802c84c54 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -1,6 +1,5 @@ package com.cloudbees.jenkins; -import com.cloudbees.jenkins.GitHubPushTrigger.DescriptorImpl; import com.google.common.base.Function; import hudson.Extension; import hudson.ExtensionPoint; @@ -8,26 +7,21 @@ import hudson.model.RootAction; import hudson.model.UnprotectedRootAction; import hudson.triggers.Trigger; -import hudson.util.AdaptedIterator; -import hudson.util.Iterators.FilterIterator; import hudson.util.SequentialExecutionQueue; import jenkins.model.Jenkins; import org.apache.commons.lang3.Validate; +import org.jenkinsci.plugins.github.GitHubPlugin; 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.github.GHEvent; -import org.kohsuke.github.GitHub; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.io.IOException; import java.net.URL; -import java.util.Collections; -import java.util.Iterator; import java.util.List; import static hudson.model.Computer.threadPoolForRemoting; @@ -69,46 +63,6 @@ public String getUrlName() { return URLNAME; } - /** - * Logs in as the given user and returns the connection object. - */ - public Iterable login(String host, String userName) { - final List l = DescriptorImpl.get().getCredentials(); - - // if the username is not an organization, we should have the right user account on file - for (Credential c : l) { - if (c.username.equals(userName)) { - try { - return Collections.singleton(c.login()); - } catch (IOException e) { - LOGGER.warn("Failed to login with username={}", c.username, e); - return Collections.emptyList(); - } - } - } - - // otherwise try all the credentials since we don't know which one would work - return new Iterable() { - public Iterator iterator() { - return new FilterIterator( - new AdaptedIterator(l) { - protected GitHub adapt(Credential c) { - try { - return c.login(); - } catch (IOException e) { - LOGGER.warn("Failed to login with username={}", c.username, e); - return null; - } - } - }) { - protected boolean filter(GitHub g) { - return g != null; - } - }; - } - }; - } - /** * If any wants to auto-register hook, then should call this method * Example code: @@ -155,7 +109,7 @@ public AbstractProject apply(AbstractProject job) { // We should handle wrong url of self defined hook url here in any case with try-catch :( URL hookUrl; try { - hookUrl = Trigger.all().get(GitHubPushTrigger.DescriptorImpl.class).getHookUrl(); + hookUrl = GitHubPlugin.configuration().getHookUrl(); } catch (GHPluginConfigException e) { LOGGER.error("Skip registration of GHHook ({})", e.getMessage()); return job; 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 35fc05c11..ba575bdb2 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -25,6 +25,7 @@ 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; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; @@ -105,8 +106,8 @@ public void run() { public void unregisterFor(GitHubRepositoryName name, List aliveRepos) { try { GHRepository repo = checkNotNull( - from(name.resolve()).firstMatch(withAdminAccess()).orNull(), - "There is no admin access to manage hooks on %s", name + from(name.resolve(allowedToManageHooks())).firstMatch(withAdminAccess()).orNull(), + "There is no credentials with admin access to manage hooks on %s", name ); LOGGER.debug("Check {} for redundant hooks...", repo); @@ -139,8 +140,8 @@ protected Function createHookSubscribedTo(final Li public GHHook apply(GitHubRepositoryName name) { try { GHRepository repo = checkNotNull( - from(name.resolve()).firstMatch(withAdminAccess()).orNull(), - "There is no admin access to manage hooks on %s", name + from(name.resolve(allowedToManageHooks())).firstMatch(withAdminAccess()).orNull(), + "There is no credentials with admin access to manage hooks on %s", name ); Validate.notEmpty(events, "Events list for hook can't be empty"); diff --git a/src/main/resources/com/cloudbees/jenkins/Credential/config.jelly b/src/main/resources/com/cloudbees/jenkins/Credential/config.jelly deleted file mode 100644 index 111f0206c..000000000 --- a/src/main/resources/com/cloudbees/jenkins/Credential/config.jelly +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly deleted file mode 100644 index 3f1802c25..000000000 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/global.jelly +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - -
- - -
- - -
-
-
- - -
- - - - - - -
\ No newline at end of file diff --git a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java deleted file mode 100644 index 746f6a9d9..000000000 --- a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.cloudbees.jenkins; - -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import hudson.model.FreeStyleProject; -import hudson.util.Secret; -import org.jenkinsci.plugins.github.internal.GHPluginConfigException; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.recipes.LocalData; -import org.kohsuke.stapler.Stapler; - -import java.io.IOException; -import java.net.URL; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Test Class for {@link GitHubPushTrigger}. - * - * @author Seiji Sogabe - */ -public class GitHubPushTriggerConfigSubmitTest { - - @Rule - public JenkinsRule jenkins = new JenkinsRule(); - - private static final String WEBHOOK_URL = "http://jenkinsci.example.com/jenkins/github-webhook/"; - - @Test - public void testConfigSubmit_AutoManageHook() throws Exception { - - JenkinsRule.WebClient client = configureWebClient(); - HtmlPage p = client.goTo("configure"); - HtmlForm f = p.getFormByName("config"); - f.getInputByValue("auto").setChecked(true); - f.getInputByName("_.hasHookUrl").setChecked(true); - f.getInputByName("_.hookUrl").setValueAttribute(WEBHOOK_URL); - f.getInputByName("_.username").setValueAttribute("jenkins"); - jenkins.submit(f); - - GitHubPushTrigger.DescriptorImpl d = getDescriptor(); - assertTrue(d.isManageHook()); - assertEquals(new URL(WEBHOOK_URL), d.getHookUrl()); - - List credentials = d.getCredentials(); - assertNotNull(credentials); - assertEquals(1, credentials.size()); - Credential credential = credentials.get(0); - assertEquals("jenkins", credential.username); - } - - @Test - public void testConfigSubmit_ManuallyManageHook() throws Exception { - JenkinsRule.WebClient client = configureWebClient(); - HtmlPage p = client.goTo("configure"); - HtmlForm f = p.getFormByName("config"); - f.getInputByValue("none").setChecked(true); - jenkins.submit(f); - - GitHubPushTrigger.DescriptorImpl d = getDescriptor(); - assertFalse(d.isManageHook()); - } - - @Test - @LocalData - public void shouldDontThrowExcMailformedHookUrl() throws IOException { - FreeStyleProject job = jenkins.createFreeStyleProject(); - GitHubPushTrigger trigger = new GitHubPushTrigger(); - trigger.start(job, true); - trigger.registerHooks(); - } - - @Test(expected = GHPluginConfigException.class) - @LocalData - public void shouldThrowExcMailformedHookUrlGetter() { - new GitHubPushTrigger().getDescriptor().getHookUrl(); - } - - private GitHubPushTrigger.DescriptorImpl getDescriptor() { - return (GitHubPushTrigger.DescriptorImpl) GitHubPushTrigger.DescriptorImpl.get(); - } - - private JenkinsRule.WebClient configureWebClient() { - JenkinsRule.WebClient client = jenkins.createWebClient(); - client.setThrowExceptionOnFailingStatusCode(false); - client.setCssEnabled(false); - client.setJavaScriptEnabled(true); - return client; - } - - // workaround - static { - Stapler.CONVERT_UTILS.register(new org.apache.commons.beanutils.Converter() { - - public Secret convert(Class type, Object value) { - return Secret.fromString(value.toString()); - } - }, Secret.class); - } -} diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index 559740bff..7d735cda9 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -4,8 +4,12 @@ import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import hudson.model.FreeStyleProject; import hudson.plugins.git.GitSCM; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,6 +32,7 @@ import static com.google.common.collect.Lists.asList; import static com.google.common.collect.Lists.newArrayList; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; import static org.junit.Assert.assertThat; import static org.kohsuke.github.GHEvent.CREATE; @@ -35,6 +40,7 @@ import static org.kohsuke.github.GHEvent.PUSH; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anySet; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -51,10 +57,10 @@ public class WebhookManagerTest { public static final GitSCM GIT_SCM = new GitSCM("ssh://git@github.com/dummy/dummy.git"); public static final URL HOOK_ENDPOINT = endpoint("http://hook.endpoint/"); public static final URL ANOTHER_HOOK_ENDPOINT = endpoint("http://another.url/"); - + @Rule public JenkinsRule jenkins = new JenkinsRule(); - + @Spy private WebhookManager manager = forHookUrl(HOOK_ENDPOINT); @@ -77,7 +83,7 @@ public void shouldDoNothingOnNoAdminRights() throws Exception { @Test public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception { - when(nonactive.resolve()).thenReturn(newArrayList(repo)); + doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); manager.unregisterFor(nonactive, newArrayList(active)); @@ -89,7 +95,7 @@ public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception @Test public void shouldSearchOnlyServiceHookOnActiveName() throws Exception { - when(active.resolve()).thenReturn(newArrayList(repo)); + doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); manager.unregisterFor(active, newArrayList(active)); @@ -138,7 +144,7 @@ public void shouldNotMatchOtherUrlWebHook() { @Test public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException { - when(nonactive.resolve()).thenReturn(newArrayList(repo)); + doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); Predicate del = spy(Predicate.class); when(manager.deleteWebhook()).thenReturn(del); @@ -154,7 +160,7 @@ public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException @Test public void shouldNotReplaceAlreadyRegisteredHook() throws IOException { - when(nonactive.resolve()).thenReturn(newArrayList(repo)); + doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); GHHook hook = hook(HOOK_ENDPOINT, PUSH); @@ -184,6 +190,26 @@ public void shouldAddPushEventByDefault() throws IOException { verify(manager).createHookSubscribedTo(newArrayList(PUSH)); } + @Test + public void shouldSelectOnlyHookManagedCreds() { + GitHubServerConfig conf = new GitHubServerConfig(""); + conf.setDontUseItToMangeHooks(true); + GitHubPlugin.configuration().getConfigs().add(conf); + + assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH)) + .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue()); + } + + @Test + public void shouldNotSelectCredsWithCustomHost() { + GitHubServerConfig conf = new GitHubServerConfig(""); + conf.setApiUrl(ANOTHER_HOOK_ENDPOINT.toString()); + conf.setDontUseItToMangeHooks(true); + GitHubPlugin.configuration().getConfigs().add(conf); + + assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH)) + .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue()); + } private GHHook hook(URL endpoint, GHEvent event, GHEvent... events) { GHHook hook = mock(GHHook.class); diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml deleted file mode 100644 index 6b0594647..000000000 --- a/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/com.cloudbees.jenkins.GitHubPushTrigger.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - true - h - - - user - - some-oauth-token - - - \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml b/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml deleted file mode 100644 index e1bc0dc00..000000000 --- a/src/test/resources/com/cloudbees/jenkins/GitHubPushTriggerConfigSubmitTest/config.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - 1.554.1 - 2 - NORMAL - true - - - false - - ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} - ${ITEM_ROOTDIR}/builds - - - - - - 5 - 0 - - - - Все - false - false - - - - Все - 0 - - - - \ No newline at end of file From c6609b8b04a241e7800f431c48d465e023047626 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 14:37:35 +0300 Subject: [PATCH 068/228] [JENKINS-24702] add tests for new credentials usage code - for migration process - for changes in hooks - for configuration logic --- .../jenkins/GlobalConfigSubmitTest.java | 58 +++++++++++++ .../github/config/GitHubPluginConfigTest.java | 37 ++++++++ .../github/config/GitHubServerConfigTest.java | 71 +++++++++++++++ .../github/migration/MigratorTest.java | 87 +++++++++++++++++++ .../test/GitHubServerConfigMatcher.java | 34 ++++++++ ...om.cloudbees.jenkins.GitHubPushTrigger.xml | 22 +++++ .../shouldMigrateCredentials/config.xml | 35 ++++++++ ...om.cloudbees.jenkins.GitHubPushTrigger.xml | 4 + .../shouldMigrateHookUrl/config.xml | 35 ++++++++ ...om.cloudbees.jenkins.GitHubPushTrigger.xml | 6 ++ .../config.xml | 35 ++++++++ 11 files changed, 424 insertions(+) create mode 100644 src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/com.cloudbees.jenkins.GitHubPushTrigger.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/com.cloudbees.jenkins.GitHubPushTrigger.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml diff --git a/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java b/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java new file mode 100644 index 000000000..8761f9785 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java @@ -0,0 +1,58 @@ +package com.cloudbees.jenkins; + +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.URL; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +/** + * Test Class for {@link GitHubPushTrigger}. + * + * @author Seiji Sogabe + */ +public class GlobalConfigSubmitTest { + + public static final String OVERRIDE_HOOK_URL_CHECKBOX = "_.overrideHookUrl"; + public static final String HOOK_URL_INPUT = "_.hookUrl"; + + private static final String WEBHOOK_URL = "http://jenkinsci.example.com/jenkins/github-webhook/"; + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + public void shouldTurnOnOverridingWhenThereIsCredentials() throws Exception { + HtmlForm form = globalConfig(); + + form.getInputByName(OVERRIDE_HOOK_URL_CHECKBOX).setChecked(true); + form.getInputByName(HOOK_URL_INPUT).setValueAttribute(WEBHOOK_URL); + jenkins.submit(form); + + assertThat(GitHubPlugin.configuration().isOverrideHookURL(), is(true)); + assertThat(GitHubPlugin.configuration().getHookUrl(), equalTo(new URL(WEBHOOK_URL))); + } + + public HtmlForm globalConfig() throws IOException, SAXException { + JenkinsRule.WebClient client = configureWebClient(); + HtmlPage p = client.goTo("configure"); + return p.getFormByName("config"); + } + + private JenkinsRule.WebClient configureWebClient() { + JenkinsRule.WebClient client = jenkins.createWebClient(); + client.setThrowExceptionOnFailingStatusCode(false); + client.setCssEnabled(false); + client.setJavaScriptEnabled(true); + return client; + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java new file mode 100644 index 000000000..129de6778 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java @@ -0,0 +1,37 @@ +package org.jenkinsci.plugins.github.config; + +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubPluginConfigTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + public void shouldNotManageHooksOnEmptyCreds() throws Exception { + assertThat(GitHubPlugin.configuration().isManageHooks(), is(false)); + } + + @Test + public void shouldManageHooksOnMangedConfig() throws Exception { + GitHubPlugin.configuration().getConfigs().add(new GitHubServerConfig("")); + assertThat(GitHubPlugin.configuration().isManageHooks(), is(true)); + } + + @Test + public void shouldNotManageHooksOnNotMangedConfig() throws Exception { + GitHubServerConfig conf = new GitHubServerConfig(""); + conf.setDontUseItToMangeHooks(true); + GitHubPlugin.configuration().getConfigs().add(conf); + assertThat(GitHubPlugin.configuration().isManageHooks(), is(false)); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java new file mode 100644 index 000000000..1105ee610 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java @@ -0,0 +1,71 @@ +package org.jenkinsci.plugins.github.config; + +import org.junit.Test; + +import java.net.URI; + +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.isUrlCustom; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.withHost; +import static org.junit.Assert.assertThat; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubServerConfigTest { + + public static final String CUSTOM_GH_SERVER = "http://some.com"; + public static final String DEFAULT_GH_API_HOST = "api.github.com"; + + @Test + public void shouldMatchAllowedConfig() throws Exception { + assertThat(allowedToManageHooks().apply(new GitHubServerConfig("")), is(true)); + } + + @Test + public void shouldNotMatchNotAllowedConfig() throws Exception { + GitHubServerConfig input = new GitHubServerConfig(""); + input.setDontUseItToMangeHooks(true); + assertThat(allowedToManageHooks().apply(input), is(false)); + } + + @Test + public void shouldMatchNonEqualToGHUrl() throws Exception { + assertThat(isUrlCustom(CUSTOM_GH_SERVER), is(true)); + } + + @Test + public void shouldNotMatchEmptyUrl() throws Exception { + assertThat(isUrlCustom(""), is(false)); + } + + @Test + public void shouldNotMatchNullUrl() throws Exception { + assertThat(isUrlCustom(null), is(false)); + } + + @Test + public void shouldNotMatchDefaultUrl() throws Exception { + assertThat(isUrlCustom(GITHUB_URL), is(false)); + } + + @Test + public void shouldMatchDefaultConfigWithGHDefaultHost() throws Exception { + assertThat(withHost(DEFAULT_GH_API_HOST).apply(new GitHubServerConfig("")), is(true)); + } + + @Test + public void shouldNotMatchNonDefaultConfigWithGHDefaultHost() throws Exception { + GitHubServerConfig input = new GitHubServerConfig(""); + input.setCustom(true); + input.setApiUrl(CUSTOM_GH_SERVER); + assertThat(withHost(DEFAULT_GH_API_HOST).apply(input), is(false)); + } + + @Test + public void shouldNotMatchDefaultConfigWithNonDefaultHost() throws Exception { + assertThat(withHost(URI.create(CUSTOM_GH_SERVER).getHost()).apply(new GitHubServerConfig("")), is(false)); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java new file mode 100644 index 000000000..f7ca3aa2b --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java @@ -0,0 +1,87 @@ +package org.jenkinsci.plugins.github.migration; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubWebHook; +import hudson.model.FreeStyleProject; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.jenkinsci.plugins.github.deprecated.Credential; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; + +import java.io.IOException; + +import static java.lang.String.valueOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withApiUrl; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withCredsWithToken; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class MigratorTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + public static final String HOOK_FROM_LOCAL_DATA = "http://some.proxy.example.com/webhook"; + public static final String CUSTOM_GH_URL = "http://custom.github.example.com/api/v3"; + public static final String TOKEN = "some-oauth-token"; + public static final String TOKEN2 = "some-oauth-token2"; + public static final String TOKEN3 = "some-oauth-token3"; + + /** + * Just ignore mailformed hook in old config + */ + @Test + @LocalData + public void shouldNotThrowExcMailformedHookUrlInOldConfig() throws IOException { + FreeStyleProject job = jenkins.createFreeStyleProject(); + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.start(job, true); + trigger.registerHooks(); + + assertThat("self hook url", trigger.getDescriptor().getDeprecatedHookUrl(), nullValue()); + assertThat("imported hook url", valueOf(trigger.getDescriptor().getHookUrl()), + containsString(Jenkins.getInstance().getRootUrl() + GitHubWebHook.URLNAME)); + assertThat("in plugin - override", GitHubPlugin.configuration().isOverrideHookURL(), is(false)); + } + + @Test + @LocalData + public void shouldMigrateHookUrl() { + assertThat("in plugin - override", GitHubPlugin.configuration().isOverrideHookURL(), is(true)); + assertThat("in plugin", valueOf(GitHubPlugin.configuration().getHookUrl()), is(HOOK_FROM_LOCAL_DATA)); + + assertThat("should nullify hook url after migration", + GitHubPushTrigger.DescriptorImpl.get().getDeprecatedHookUrl(), nullValue()); + } + + @Test + @LocalData + public void shouldMigrateCredentials() throws Exception { + assertThat("should migrate 3 configs", GitHubPlugin.configuration().getConfigs(), hasSize(3)); + assertThat("migrate custom url", GitHubPlugin.configuration().getConfigs(), hasItems( + both(withApiUrl(is(CUSTOM_GH_URL))).and(withCredsWithToken(TOKEN2)), + both(withApiUrl(is(GITHUB_URL))).and(withCredsWithToken(TOKEN)), + both(withApiUrl(is(GITHUB_URL))).and(withCredsWithToken(TOKEN3)) + )); + } + + @Test + public void shouldConvertCredsToServerConfig() throws Exception { + GitHubServerConfig conf = new Migrator().toGHServerConfig() + .apply(new Credential("name", CUSTOM_GH_URL, "token")); + assertThat(conf, both(withCredsWithToken("token")).and(withApiUrl(is(CUSTOM_GH_URL)))); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java b/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java new file mode 100644 index 000000000..5df68b9ca --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java @@ -0,0 +1,34 @@ +package org.jenkinsci.plugins.github.test; + +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; + +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.tokenFor; + +/** + * @author lanwen (Merkushev Kirill) + */ +public final class GitHubServerConfigMatcher { + private GitHubServerConfigMatcher() { + } + + public static Matcher withApiUrl(Matcher matcher) { + return new FeatureMatcher(matcher, "api url", "") { + @Override + protected String featureValueOf(GitHubServerConfig actual) { + return actual.getApiUrl(); + } + }; + } + + public static Matcher withCredsWithToken(String token) { + return new FeatureMatcher(is(token), "token in creds", "") { + @Override + protected String featureValueOf(GitHubServerConfig actual) { + return tokenFor(actual.getCredentialsId()); + } + }; + } +} diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/com.cloudbees.jenkins.GitHubPushTrigger.xml new file mode 100644 index 000000000..e36febeb9 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/com.cloudbees.jenkins.GitHubPushTrigger.xml @@ -0,0 +1,22 @@ + + + true + http://some.proxy.example.com/webhook + + + user + + some-oauth-token + + + user2 + http://custom.github.example.com/api/v3 + some-oauth-token2 + + + user3 + https://api.github.com + some-oauth-token3 + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml new file mode 100644 index 000000000..62bdf2390 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml @@ -0,0 +1,35 @@ + + + + 1.554.1 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + + 5 + 0 + + + + Все + false + false + + + + Все + 0 + + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/com.cloudbees.jenkins.GitHubPushTrigger.xml new file mode 100644 index 000000000..6610d3c3f --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/com.cloudbees.jenkins.GitHubPushTrigger.xml @@ -0,0 +1,4 @@ + + + http://some.proxy.example.com/webhook + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml new file mode 100644 index 000000000..62bdf2390 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml @@ -0,0 +1,35 @@ + + + + 1.554.1 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + + 5 + 0 + + + + Все + false + false + + + + Все + 0 + + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml new file mode 100644 index 000000000..a89e727e4 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml @@ -0,0 +1,6 @@ + + + true + h + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml new file mode 100644 index 000000000..62bdf2390 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml @@ -0,0 +1,35 @@ + + + + 1.554.1 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + + 5 + 0 + + + + Все + false + false + + + + Все + 0 + + + + \ No newline at end of file From cec7468a97bcca2abf5aca3b45fb433a712e37a3 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 31 Jul 2015 22:50:16 +0300 Subject: [PATCH 069/228] add help files to new credentials UI - change dontManageHooks=false to manageHooks=true --- .../github/config/GitHubServerConfig.java | 16 +++---- .../jenkins/Credential/help-apiUrl.html | 5 --- .../jenkins/Credential/help-password.html | 3 -- .../jenkins/Credential/help-username.html | 4 -- .../jenkins/GitHubPushTrigger/help-auto.jelly | 18 -------- .../GitHubPushTrigger/help-manual.jelly | 9 ---- .../config/GitHubPluginConfig/config.groovy | 7 +-- .../GitHubPluginConfig/help-additional.html | 4 ++ .../help-overrideHookUrl.jelly} | 2 +- .../config/GitHubPluginConfig/help.jelly | 43 +++++++++++++++++++ .../config/GitHubServerConfig/config.groovy | 9 ++-- .../help-credentialsId.html | 23 ++++++++++ .../GitHubServerConfig/help-custom.html | 5 +++ .../GitHubServerConfig/help-manageHooks.html | 4 ++ .../config/GitHubServerConfig/help.html | 5 +++ .../config.groovy | 3 +- .../GitHubTokenCredentialsCreator/help.html | 8 ++++ .../github/config/GitHubPluginConfigTest.java | 2 +- .../github/config/GitHubServerConfigTest.java | 2 +- .../github/webhook/WebhookManagerTest.java | 4 +- 20 files changed, 117 insertions(+), 59 deletions(-) delete mode 100644 src/main/resources/com/cloudbees/jenkins/Credential/help-apiUrl.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/Credential/help-password.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/Credential/help-username.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly delete mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-manual.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-additional.html rename src/main/resources/{com/cloudbees/jenkins/GitHubPushTrigger/help-hookUrl.jelly => org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly} (74%) create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-credentialsId.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-custom.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/help.html 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 02d1eef76..8dea83798 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -63,7 +63,7 @@ public class GitHubServerConfig extends AbstractDescribableImpl allowedToManageHooks() { return new NullSafePredicate() { @Override protected boolean applyNullSafe(@NonNull GitHubServerConfig github) { - return !github.isDontUseItToMangeHooks(); + return github.isManageHooks(); } }; } diff --git a/src/main/resources/com/cloudbees/jenkins/Credential/help-apiUrl.html b/src/main/resources/com/cloudbees/jenkins/Credential/help-apiUrl.html deleted file mode 100644 index 298aa93b6..000000000 --- a/src/main/resources/com/cloudbees/jenkins/Credential/help-apiUrl.html +++ /dev/null @@ -1,5 +0,0 @@ -

- If you use GitHub Enterprise you may specify the API end point here - (e.g., "https://ghe.acme.com/api/v3/"). If left empty, the public - https://api.github.com/ endpoint will be assumed. -
diff --git a/src/main/resources/com/cloudbees/jenkins/Credential/help-password.html b/src/main/resources/com/cloudbees/jenkins/Credential/help-password.html deleted file mode 100644 index a274f65d3..000000000 --- a/src/main/resources/com/cloudbees/jenkins/Credential/help-password.html +++ /dev/null @@ -1,3 +0,0 @@ -
- Password is no longer required if you specify an OAuth token. -
diff --git a/src/main/resources/com/cloudbees/jenkins/Credential/help-username.html b/src/main/resources/com/cloudbees/jenkins/Credential/help-username.html deleted file mode 100644 index 01d975178..000000000 --- a/src/main/resources/com/cloudbees/jenkins/Credential/help-username.html +++ /dev/null @@ -1,4 +0,0 @@ -
- If your Jenkins uses multiple repositories that are spread across different - user accounts, you can list them all here. -
diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly deleted file mode 100644 index 067dac634..000000000 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-auto.jelly +++ /dev/null @@ -1,18 +0,0 @@ - - -
- In this mode, Jenkins will add/remove hook URLs to GitHub based on the project configuration of Jenkins. - 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. - -

- This 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" and specify the URL GitHub should POST to. -

-
-
-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-manual.jelly b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-manual.jelly deleted file mode 100644 index 8641301b9..000000000 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-manual.jelly +++ /dev/null @@ -1,9 +0,0 @@ - - -
- Don't let Jenkins talk to GitHub and manage post commit hook URLs, and opt to do it manually. - In this mode, in addition to configure projects with "Build when a change is pushed to GitHub", - you need to ensure that Jenkins gets a POST to its ${app.rootUrl}github-webhook/ -
-
-
\ No newline at end of file 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 c42a0dad2..4a038b551 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 @@ -5,9 +5,10 @@ import com.cloudbees.jenkins.GitHubPushTrigger def f = namespace(lib.FormTagLib); f.section(title: descriptor.displayName) { - f.entry(title: _("Servers configs with credentials to manage GitHub integrations"), - description: _("List of GitHub Servers to manage hooks, set commit statuses etc.")) { + description: _("List of GitHub Servers to manage hooks, set commit statuses etc."), + help: descriptor.getHelpFile()) { + f.repeatableHeteroProperty( field: "configs", hasHeader: "true", @@ -39,7 +40,7 @@ f.section(title: descriptor.displayName) { } } - f.entry(title: _("Additional actions")) { + f.entry(title: _("Additional actions"), help: descriptor.getHelpFile('additional')) { f.hetero_list(items: [], addCaption: _("Manage additional GitHub actions"), name: "actions", 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 new file mode 100644 index 000000000..291d5e1ba --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-additional.html @@ -0,0 +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. +
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-hookUrl.jelly b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly similarity index 74% rename from src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-hookUrl.jelly rename to src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly index d84ce10ab..cfaeb0feb 100644 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help-hookUrl.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly @@ -1,4 +1,4 @@ - +
If your Jenkins runs inside the firewall and not directly reachable from the internet, 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 new file mode 100644 index 000000000..f4ec7df78 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help.jelly @@ -0,0 +1,43 @@ + + +
+

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. +
+ In this mode, in addition to configure projects with "Build when a change is pushed to GitHub", + you need to ensure that Jenkins gets a POST to its + + ${app.rootUrl}github-webhook/ + +

+ +

If you setup credentials

+

+ In this mode, Jenkins will add/remove hook URLs to GitHub based on the project configuration of + Jenkins. + 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. + +

+ This 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. +

+

+
+
+
\ No newline at end of file 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 f40b07c21..a2aec8b0d 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 @@ -6,15 +6,18 @@ def f = namespace(lib.FormTagLib); def c = namespace(lib.CredentialsTagLib) -f.entry(title: _("Don't manage hooks with this config")) { - f.checkbox( field: "dontUseItToMangeHooks") +f.entry(title: _("Manage hooks"), field: "manageHooks") { + f.checkbox(default: true) } f.entry(title: _("Credentials"), field: "credentialsId") { c.select() } -f.optionalBlock(title: _("Custom GitHub API URL"), inline: true, name: "custom", checked: instance?.custom) { +f.optionalBlock(title: _("Custom GitHub API URL"), + inline: true, + field: "custom", + checked: instance?.custom) { f.entry(title: _("GitHub API URL"), field: "apiUrl") { f.textbox(default: GitHubServerConfig.GITHUB_URL) } 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 new file mode 100644 index 000000000..c5289aa14 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-credentialsId.html @@ -0,0 +1,23 @@ +
+ You can create own personal access token at GitHub settings. +
+ Token should be registered with scopes: +
    +
  • admin:repo_hook - for managing hooks (read, write and delete old ones)
  • +
  • repo - to see private repos
  • +
  • repo:status - to manipulate commit statuses
  • +
+ +
+ In Jenkins create credentials as «Secret Text», provided by + Plain Credentials Plugin
+ +

+ WARN! Creds are filtered on changing custom GitHub url
+

+ +

+ If you have existed GitHub login and password you can convert it to token automatically with help of «Manage + additional GitHub actions» +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-custom.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-custom.html new file mode 100644 index 000000000..65eb7ca89 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-custom.html @@ -0,0 +1,5 @@ +
+ If you use GitHub Enterprise you may specify the API end point here + (e.g., https://ghe.acme.com/api/v3/). Otherwise, the public + https://api.github.com/ endpoint will be assumed. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html new file mode 100644 index 000000000..13a50fbf3 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html @@ -0,0 +1,4 @@ +
+ 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 +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html new file mode 100644 index 000000000..86053496a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html @@ -0,0 +1,5 @@ +
+ Pair of GitHub token and server url. If no any custom url specified, then 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. +
\ No newline at end of file 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 98b4f321a..ad044ff02 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 @@ -5,7 +5,8 @@ import org.jenkinsci.plugins.github.config.GitHubServerConfig def f = namespace(lib.FormTagLib); def c = namespace(lib.CredentialsTagLib) -f.entry(title: _("GitHub API URL"), field: "apiUrl") { +f.entry(title: _("GitHub API URL"), field: "apiUrl", + help: '/descriptor/org.jenkinsci.plugins.github.config.GitHubServerConfig/help/custom') { f.textbox(default: GitHubServerConfig.GITHUB_URL) } 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 new file mode 100644 index 000000000..8a8297bc7 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator/help.html @@ -0,0 +1,8 @@ +
+ Helper to convert existing username-password credentials or directly login+password to + 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 +
\ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java index 129de6778..c69c95f47 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java @@ -30,7 +30,7 @@ public void shouldManageHooksOnMangedConfig() throws Exception { @Test public void shouldNotManageHooksOnNotMangedConfig() throws Exception { GitHubServerConfig conf = new GitHubServerConfig(""); - conf.setDontUseItToMangeHooks(true); + conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); assertThat(GitHubPlugin.configuration().isManageHooks(), is(false)); } diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java index 1105ee610..4463c1c9d 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java @@ -27,7 +27,7 @@ public void shouldMatchAllowedConfig() throws Exception { @Test public void shouldNotMatchNotAllowedConfig() throws Exception { GitHubServerConfig input = new GitHubServerConfig(""); - input.setDontUseItToMangeHooks(true); + input.setManageHooks(false); assertThat(allowedToManageHooks().apply(input), is(false)); } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index 7d735cda9..5423cffd2 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -193,7 +193,7 @@ public void shouldAddPushEventByDefault() throws IOException { @Test public void shouldSelectOnlyHookManagedCreds() { GitHubServerConfig conf = new GitHubServerConfig(""); - conf.setDontUseItToMangeHooks(true); + conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH)) @@ -204,7 +204,7 @@ public void shouldSelectOnlyHookManagedCreds() { public void shouldNotSelectCredsWithCustomHost() { GitHubServerConfig conf = new GitHubServerConfig(""); conf.setApiUrl(ANOTHER_HOOK_ENDPOINT.toString()); - conf.setDontUseItToMangeHooks(true); + conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH)) From 2851a1d84e01f17b58dc267ada0a1c7777dd6a33 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 2 Aug 2015 18:58:04 +0300 Subject: [PATCH 070/228] newline at the end of help files --- .../jenkinsci/plugins/github/config/GitHubServerConfig.java | 2 +- .../org/jenkinsci/plugins/github/GitHubPlugin/config.groovy | 4 ++-- .../github/config/GitHubPluginConfig/help-additional.html | 2 +- .../plugins/github/config/GitHubPluginConfig/help.jelly | 2 +- .../github/config/GitHubServerConfig/help-manageHooks.html | 2 +- .../plugins/github/config/GitHubServerConfig/help.html | 2 +- .../github/config/GitHubTokenCredentialsCreator/config.groovy | 2 -- .../github/config/GitHubTokenCredentialsCreator/help.html | 2 +- .../MigratorTest/shouldMigrateCredentials/config.xml | 4 ++-- .../migration/MigratorTest/shouldMigrateHookUrl/config.xml | 4 ++-- .../shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml | 4 ++-- 11 files changed, 14 insertions(+), 16 deletions(-) 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 8dea83798..2f28e9b89 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -60,7 +60,7 @@ public class GitHubServerConfig extends AbstractDescribableImpl 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. -
\ 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 f4ec7df78..76481a22d 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 @@ -40,4 +40,4 @@

- \ No newline at end of file + diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html index 13a50fbf3..eef82f875 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-manageHooks.html @@ -1,4 +1,4 @@
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 -
\ No newline at end of file + diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html index 86053496a..8781a2872 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help.html @@ -2,4 +2,4 @@ Pair of GitHub token and server url. If no any custom url specified, then 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. - \ No newline at end of file + 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 ad044ff02..8ac932db0 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 @@ -44,5 +44,3 @@ f.radioBlock(checked: false, name: "creds", value: "manually", title: "From logi ) } } - - 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 8a8297bc7..69a3674af 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 @@ -5,4 +5,4 @@ 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 - \ No newline at end of file + diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml index 62bdf2390..b11975415 100644 --- a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateCredentials/config.xml @@ -21,13 +21,13 @@ - Все + All false false - Все + All 0 diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml index 62bdf2390..b11975415 100644 --- a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldMigrateHookUrl/config.xml @@ -21,13 +21,13 @@ - Все + All false false - Все + All 0 diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml index 62bdf2390..b11975415 100644 --- a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml @@ -21,13 +21,13 @@ - Все + All false false - Все + All 0 From 71cdab4318bd0b82f9ac31051507d7062e4924bf Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 3 Aug 2015 21:08:18 +0300 Subject: [PATCH 071/228] get help file by descriptor method call instead of string --- .../github/config/GitHubTokenCredentialsCreator/config.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ac932db0..1f579c485 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 @@ -6,7 +6,7 @@ def f = namespace(lib.FormTagLib); def c = namespace(lib.CredentialsTagLib) f.entry(title: _("GitHub API URL"), field: "apiUrl", - help: '/descriptor/org.jenkinsci.plugins.github.config.GitHubServerConfig/help/custom') { + help: app.getDescriptor(GitHubServerConfig.class)?.getHelpFile("custom")) { f.textbox(default: GitHubServerConfig.GITHUB_URL) } From 758676cdc172c7c7ba70a1f2429341c9811df3fc Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 10 Aug 2015 00:37:47 +0300 Subject: [PATCH 072/228] make visible btn "reregister hooks" all the time, but move it to advanced also fix some phrases in javadocs and help --- .../plugins/github/config/GitHubServerConfig.java | 6 +++--- .../github/config/GitHubPluginConfig/config.groovy | 8 +++----- .../plugins/github/config/GitHubPluginConfig/help.jelly | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) 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 2f28e9b89..b8b02795f 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -62,7 +62,7 @@ public class GitHubServerConfig extends AbstractDescribableImpl - In this mode, in addition to configure projects with "Build when a change is pushed to GitHub", + In this mode, in addition to configure projects with "Build when a change is pushed to GitHub", you need to ensure that Jenkins gets a POST to its ${app.rootUrl}github-webhook/ @@ -26,7 +26,7 @@ create new hooks and remove old.

- This URL is + Hook URL is ${app.rootUrl}github-webhook/ From 2176fd6b3fae5272c47856fed6ecf7fb6a32afc6 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 10 Aug 2015 19:51:45 +0300 Subject: [PATCH 073/228] rename field "custom" to "customApiUrl" --- .../github/config/GitHubServerConfig.java | 19 +++++++++++-------- .../plugins/github/migration/Migrator.java | 2 +- .../config/GitHubServerConfig/config.groovy | 4 ++-- ...elp-custom.html => help-customApiUrl.html} | 0 .../config.groovy | 2 +- .../github/config/GitHubServerConfigTest.java | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) rename src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/{help-custom.html => help-customApiUrl.html} (100%) 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 b8b02795f..4bf58ced7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -18,6 +18,8 @@ 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.NoExternalUse; import org.kohsuke.github.GitHub; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -67,9 +69,9 @@ public class GitHubServerConfig extends AbstractDescribableImpl Date: Tue, 11 Aug 2015 16:31:46 +0300 Subject: [PATCH 074/228] [maven-release-plugin] prepare release github-1.13.0-alpha-1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 11f214874..54b09f724 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.13.0-SNAPSHOT + 1.13.0-alpha-1 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -143,7 +143,7 @@ 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 + github-1.13.0-alpha-1 From 81fbf3fe420cbe4eeceb1dd3f2291a0f60d9eb66 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Tue, 11 Aug 2015 16:31:51 +0300 Subject: [PATCH 075/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 54b09f724..11f214874 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.13.0-alpha-1 + 1.13.0-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -143,7 +143,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.13.0-alpha-1 + HEAD From 210856047fedc79e479d642f740083a4739414f9 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 12 Aug 2015 22:32:25 +0300 Subject: [PATCH 076/228] use GlobalConfiguration as extension point to store all global config this simplifies resulting config classes and xml-files --- .../plugins/github/GitHubPlugin.java | 59 ++----- .../github/config/GitHubPluginConfig.java | 151 +++++++++++------- .../plugins/github/migration/Migrator.java | 12 +- .../plugins/github/GitHubPlugin/config.groovy | 7 - .../github/migration/MigratorTest.java | 13 ++ ...om.cloudbees.jenkins.GitHubPushTrigger.xml | 2 + .../shouldLoadDataAfterStart/config.xml | 35 ++++ .../github-plugin-configuration.xml | 16 ++ 8 files changed, 183 insertions(+), 112 deletions(-) delete mode 100644 src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/com.cloudbees.jenkins.GitHubPushTrigger.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml create mode 100644 src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/github-plugin-configuration.xml diff --git a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java index 317bd3682..f5d3553e8 100644 --- a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java +++ b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java @@ -1,45 +1,34 @@ package org.jenkinsci.plugins.github; import hudson.Plugin; -import hudson.model.Descriptor.FormException; -import jenkins.model.Jenkins; -import net.sf.json.JSONObject; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.migration.Migrator; -import org.kohsuke.stapler.StaplerRequest; -import javax.servlet.ServletException; -import java.io.IOException; +import javax.annotation.Nonnull; -import static java.lang.String.format; -import static org.apache.commons.lang3.Validate.notNull; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; /** * Main entry point for this plugin - * Stores global configuration + * + * Launches migration from old config versions + * Contains helper method to get global plugin configuration - {@link #configuration()} * * @author lanwen (Merkushev Kirill) */ public class GitHubPlugin extends Plugin { - private GitHubPluginConfig configuration = new GitHubPluginConfig(); - - public GitHubPluginConfig getConfiguration() { - return configuration; - } - /** * Launched before plugin starts * Adds alias for {@link GitHubPlugin} to simplify resulting xml */ public static void init() { - Jenkins.XSTREAM2.alias("github-plugin", GitHubPlugin.class); Migrator.enableCompatibilityAliases(); + Migrator.enableAliases(); } @Override public void start() throws Exception { init(); - load(); } /** @@ -50,40 +39,16 @@ public void postInitialize() throws Exception { new Migrator().migrate(); } - @Override - public void configure(StaplerRequest req, JSONObject formData) throws IOException, ServletException, FormException { - try { - configuration = req.bindJSON(GitHubPluginConfig.class, formData); - } catch (Exception e) { - throw new FormException( - format("Mailformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); - } - save(); - } - - @Override - protected void load() throws IOException { - super.load(); - if (configuration == null) { - configuration = new GitHubPluginConfig(); - save(); - } - } - - /** - * @return instance of this plugin - */ - public static GitHubPlugin get() { - return notNull(Jenkins.getInstance(), "Jenkins is not ready to return instance") - .getPlugin(GitHubPlugin.class); - } - /** - * Shortcut method for {@link GitHubPlugin#get()#getConfiguration()}. + * Shortcut method for getting instance of {@link GitHubPluginConfig}. * * @return configuration of plugin */ + @Nonnull public static GitHubPluginConfig configuration() { - return get().getConfiguration(); + return defaultIfNull( + GitHubPluginConfig.all().get(GitHubPluginConfig.class), + GitHubPluginConfig.EMPTY_CONFIG + ); } } 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 7932f9a1c..c4c68a7c3 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -4,19 +4,21 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import hudson.Extension; -import hudson.model.AbstractDescribableImpl; +import hudson.XmlFile; import hudson.model.AbstractProject; import hudson.model.Descriptor; import hudson.util.FormValidation; +import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; +import net.sf.json.JSONObject; import org.apache.commons.codec.binary.Base64; import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.internal.GHPluginConfigException; +import org.jenkinsci.plugins.github.migration.Migrator; import org.kohsuke.github.GitHub; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +32,7 @@ import java.util.Collections; import java.util.List; +import static java.lang.String.format; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; @@ -41,31 +44,50 @@ * @author lanwen (Merkushev Kirill) * @since TODO */ -public class GitHubPluginConfig extends AbstractDescribableImpl { +@Extension +public class GitHubPluginConfig extends GlobalConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPluginConfig.class); + public static final String GITHUB_PLUGIN_CONFIGURATION_ID = "github-plugin-configuration"; + + /** + * Helps to avoid null in {@link GitHubPlugin#configuration()} + */ + public static final GitHubPluginConfig EMPTY_CONFIG = + new GitHubPluginConfig(Collections.emptyList()); private List configs = new ArrayList(); private URL hookUrl; private transient boolean overrideHookUrl; - @DataBoundConstructor + /** + * Used to get current instance identity. + * It compared with same value when testing hook url availability in {@link #doCheckHookUrl(String)} + */ + @Inject + @SuppressWarnings("unused") + private transient InstanceIdentity identity; + public GitHubPluginConfig() { + load(); } - public List getConfigs() { - return configs; + public GitHubPluginConfig(List configs) { + this.configs = configs; } - @DataBoundSetter + @SuppressWarnings("unused") public void setConfigs(List configs) { this.configs = configs; } + public List getConfigs() { + return configs; + } + public boolean isManageHooks() { return from(getConfigs()).filter(allowedToManageHooks()).first().isPresent(); } - @DataBoundSetter public void setHookUrl(URL hookUrl) { if (overrideHookUrl) { this.hookUrl = hookUrl; @@ -74,7 +96,6 @@ public void setHookUrl(URL hookUrl) { } } - @DataBoundSetter public void setOverrideHookUrl(boolean overrideHookUrl) { this.overrideHookUrl = overrideHookUrl; } @@ -111,60 +132,78 @@ public List actions() { return Collections.singletonList(Jenkins.getInstance().getDescriptor(GitHubTokenCredentialsCreator.class)); } - @Extension - public static class GitHubPluginConfigDescriptor extends Descriptor { + /** + * To avoid long class name as id in xml tag name and config file + */ + @Override + public String getId() { + return GITHUB_PLUGIN_CONFIGURATION_ID; + } - /** - * Used to get current instance identity. It compared with same value when testing hook url availability - */ - @Inject - @SuppressWarnings("unused") - private transient InstanceIdentity identity; + /** + * @return config file with global {@link com.thoughtworks.xstream.XStream} instance + * with enabled aliases in {@link Migrator#enableAliases()} + */ + @Override + protected XmlFile getConfigFile() { + return new XmlFile(Jenkins.XSTREAM2, super.getConfigFile().getFile()); + } - @Override - public String getDisplayName() { - return "GitHub Plugin Configuration"; + @Override + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + try { + req.bindJSON(this, json); + } catch (Exception e) { + throw new FormException( + format("Mailformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); } + save(); + return true; + } - @SuppressWarnings("unused") - public FormValidation doReRegister() { - if (!GitHubPlugin.configuration().isManageHooks()) { - return FormValidation.warning("Works only when Jenkins manages hooks (one ore more creds specified)"); - } - - List registered = GitHubWebHook.get().reRegisterAllHooks(); + @Override + public String getDisplayName() { + return "GitHub Plugin Configuration"; + } - LOGGER.info("Called registerHooks() for {} jobs", registered.size()); - return FormValidation.ok("Called re-register hooks for %s jobs", registered.size()); + @SuppressWarnings("unused") + public FormValidation doReRegister() { + if (!GitHubPlugin.configuration().isManageHooks()) { + return FormValidation.warning("Works only when Jenkins manages hooks (one ore more creds specified)"); } - @SuppressWarnings("unused") - public FormValidation doCheckHookUrl(@QueryParameter String value) { - try { - HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection(); - con.setRequestMethod("POST"); - con.setRequestProperty(GitHubWebHook.URL_VALIDATION_HEADER, "true"); - con.connect(); - if (con.getResponseCode() != 200) { - return FormValidation.error("Got %d from %s", con.getResponseCode(), 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. " + - "Are you running your own app?", value); - } - RSAPublicKey key = identity.getPublic(); - String expected = new String(Base64.encodeBase64(key.getEncoded())); - if (!expected.equals(v)) { - // if it responds but with a different ID, that's more likely wrong than correct - return FormValidation.error("%s is connecting to different Jenkins instances", value); - } - - return FormValidation.ok(); - } catch (IOException e) { - return FormValidation.error(e, "Failed to test a connection to %s", value); + 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()); + } + + @SuppressWarnings("unused") + public FormValidation doCheckHookUrl(@QueryParameter String value) { + try { + HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty(GitHubWebHook.URL_VALIDATION_HEADER, "true"); + con.connect(); + if (con.getResponseCode() != 200) { + return FormValidation.error("Got %d from %s", con.getResponseCode(), 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. " + + "Are you running your own app?", value); + } + RSAPublicKey key = identity.getPublic(); + String expected = new String(Base64.encodeBase64(key.getEncoded())); + if (!expected.equals(v)) { + // if it responds but with a different ID, that's more likely wrong than correct + return FormValidation.error("%s is connecting to different Jenkins instances", value); + } + + return FormValidation.ok(); + } catch (IOException e) { + return FormValidation.error(e, "Failed to test a connection to %s", value); } } } 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 c1585c527..b6544569c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java +++ b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java @@ -6,6 +6,7 @@ import com.google.common.base.Function; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator; import org.jenkinsci.plugins.github.deprecated.Credential; @@ -48,7 +49,7 @@ public void migrate() throws IOException { descriptor.clearCredentials(); descriptor.save(); - GitHubPlugin.get().save(); + GitHubPlugin.configuration().save(); } if (descriptor.getDeprecatedHookUrl() != null) { @@ -57,7 +58,7 @@ public void migrate() throws IOException { GitHubPlugin.configuration().setHookUrl(descriptor.getDeprecatedHookUrl()); descriptor.clearDeprecatedHookUrl(); descriptor.save(); - GitHubPlugin.get().save(); + GitHubPlugin.configuration().save(); } } @@ -96,4 +97,11 @@ public GitHubServerConfig apply(Credential input) { public static void enableCompatibilityAliases() { Jenkins.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.Credential", Credential.class); } + + /** + * Simplifies long node names in config files + */ + public static void enableAliases() { + Jenkins.XSTREAM2.alias(GitHubPluginConfig.GITHUB_PLUGIN_CONFIGURATION_ID, GitHubPluginConfig.class); + } } diff --git a/src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy deleted file mode 100644 index 5fdf665d6..000000000 --- a/src/main/resources/org/jenkinsci/plugins/github/GitHubPlugin/config.groovy +++ /dev/null @@ -1,7 +0,0 @@ -package org.jenkinsci.plugins.github.GitHubPlugin - -def st = namespace("jelly:stapler"); - -instance = my.configuration -descriptor = instance.descriptor -st.include(from: descriptor, page: descriptor.configPage, optional: false) diff --git a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java index f7ca3aa2b..e2524a8f5 100644 --- a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java @@ -18,6 +18,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; @@ -78,6 +79,18 @@ public void shouldMigrateCredentials() throws Exception { )); } + @Test + @LocalData + public void shouldLoadDataAfterStart() throws Exception { + assertThat("should load 3 configs", GitHubPlugin.configuration().getConfigs(), hasSize(2)); + assertThat("migrate custom url", GitHubPlugin.configuration().getConfigs(), hasItems( + withApiUrl(is(CUSTOM_GH_URL)), + withApiUrl(is(GITHUB_URL)) + )); + assertThat("should load hook url", + GitHubPlugin.configuration().getHookUrl().toString(), equalTo(HOOK_FROM_LOCAL_DATA)); + } + @Test public void shouldConvertCredsToServerConfig() throws Exception { GitHubServerConfig conf = new Migrator().toGHServerConfig() diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/com.cloudbees.jenkins.GitHubPushTrigger.xml new file mode 100644 index 000000000..53adc8e31 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/com.cloudbees.jenkins.GitHubPushTrigger.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml new file mode 100644 index 000000000..b11975415 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml @@ -0,0 +1,35 @@ + + + + 1.554.1 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + + 5 + 0 + + + + All + false + false + + + + All + 0 + + + + \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/github-plugin-configuration.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/github-plugin-configuration.xml new file mode 100644 index 000000000..1ce659d15 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/github-plugin-configuration.xml @@ -0,0 +1,16 @@ + + + + + https://api.github.com + true + a06436b7-7862-41fd-b7dc-3fec57c81f14 + + + http://custom.github.example.com/api/v3 + true + aae86cb0-e6d2-4520-80a9-89ab80129a4f + + + http://some.proxy.example.com/webhook + \ No newline at end of file From fbca8979bb477b79956733d0945178b5b17b09c6 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 17 Aug 2015 14:15:26 +0300 Subject: [PATCH 077/228] [maven-release-plugin] prepare release github-1.13.0-alpha-2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 11f214874..51b8b3473 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.13.0-SNAPSHOT + 1.13.0-alpha-2 GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -143,7 +143,7 @@ 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 + github-1.13.0-alpha-2 From 909f546ef7198137738cc8c650d5cdb7aa4b1f0e Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 17 Aug 2015 14:15:32 +0300 Subject: [PATCH 078/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 51b8b3473..11f214874 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github hpi - 1.13.0-alpha-2 + 1.13.0-SNAPSHOT GitHub plugin http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin @@ -143,7 +143,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.13.0-alpha-2 + HEAD From 9b5fba601717c07d13686a61845200e74c3e4884 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 17 Aug 2015 17:54:53 +0300 Subject: [PATCH 079/228] checkstyle with contributing guides no any functional change, only code style and maven-checkstyle-plugin in pom --- CONTRIBUTING.md | 122 ++++++ pom.xml | 386 ++++++++++-------- .../java/com/cloudbees/jenkins/Cleaner.java | 9 +- .../jenkins/GitHubCommitNotifier.java | 75 ++-- .../cloudbees/jenkins/GitHubPushTrigger.java | 13 +- .../jenkins/GitHubRepositoryName.java | 3 + .../GitHubRepositoryNameContributor.java | 16 +- .../jenkins/GitHubSetCommitStatusBuilder.java | 30 +- .../com/cloudbees/jenkins/GitHubTrigger.java | 14 +- .../com/cloudbees/jenkins/GitHubWebHook.java | 6 +- .../jenkins/GitHubWebHookCrumbExclusion.java | 29 +- .../plugins/github/GithubLinkAction.java | 17 +- .../plugins/github/GithubLinkAnnotator.java | 25 +- .../plugins/github/GithubProjectProperty.java | 26 +- .../hudson/plugins/github/GithubUrl.java | 16 +- .../plugins/github/GitHubPlugin.java | 2 +- .../plugins/github/deprecated/Credential.java | 1 - .../github/extension/GHEventsSubscriber.java | 2 +- .../plugins/github/util/BuildDataHelper.java | 17 +- .../github/util/FluentIterableWrapper.java | 7 +- .../plugins/github/util/JobInfoHelpers.java | 6 +- .../github/util/misc/NullSafeFunction.java | 2 +- .../plugins/github/webhook/GHEventHeader.java | 6 +- .../webhook/RequirePostWithGHHookPayload.java | 21 +- .../github/webhook/WebhookManager.java | 2 +- .../DefaultPushGHEventSubscriber.java | 12 +- .../checkstyle/checkstyle-config.xml | 203 +++++++++ 27 files changed, 716 insertions(+), 352 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 src/test/resources/checkstyle/checkstyle-config.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..d73654ed7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,122 @@ +# Code Style Guidelines + +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`. + +## Indentation + +1. **Use spaces.** Tabs are banned. +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`. +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. +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. + +## Line Length + +To the greatest extent possible, please wrap lines to ensure that they do not exceed 120 characters. + +## 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. + +## 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. + +### 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) + +### 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 + ``` + /** + * The count of widgets + */ + private int widgetCount; + + /** + * Returns the count of widgets. + * + * @return the count of widgets. + */ + public int getWidgetCount() { + return widgetCount; + } + + /** + * Sets the count of widgets. + * + * @param widgetCount the count of widgets. + */ + public void setWidgetCount(int widgetCount) { + 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. + +### 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 + +## 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 + +- https://wiki.jenkins-ci.org/display/JENKINS/contributing +- https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins +- https://wiki.jenkins-ci.org/display/JENKINS/GitHub+commit+messages diff --git a/pom.xml b/pom.xml index 11f214874..e370f3fee 100644 --- a/pom.xml +++ b/pom.xml @@ -1,187 +1,213 @@ - - 4.0.0 + + + 4.0.0 + + + org.jenkins-ci.plugins + plugin + + 1.554.1 + + + com.coravy.hudson.plugins.github + github + 1.13.0-SNAPSHOT + hpi + + GitHub plugin + http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + kohsuke + Kohsuke Kawaguchi + + + juretta + Stefan Saasen + + + + + 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 + - - org.jenkins-ci.plugins - plugin - - 1.554.1 - - - com.coravy.hudson.plugins.github - github - hpi - 1.13.0-SNAPSHOT - GitHub plugin - http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin - - - - Apache 2 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - - kohsuke - Kohsuke Kawaguchi - - - juretta - Stefan Saasen - - - - - - org.apache.commons - commons-lang3 - 3.4 - - - - org.slf4j - slf4j-jdk14 - 1.7.7 - - - - org.jenkins-ci.plugins - github-api - 1.67 - - - - org.jenkins-ci.plugins - git - 2.0 - - - - org.eclipse.jgit - org.eclipse.jgit - 0.12.1 - - - - org.jenkins-ci.plugins - credentials - 1.22 - - - - org.jenkins-ci.plugins - plain-credentials - 1.1 - - - - org.jenkins-ci.plugins - multiple-scms - 0.2 - true - - - - org.jenkins-ci.modules - instance-identity - 1.3 - provided - - - - - org.hamcrest - hamcrest-all - 1.3 - test - - - - junit - junit - 4.12 - test - - - - org.jmock - jmock-junit4 - 2.5.1 - test - - - - org.mockito - mockito-core - 1.10.19 - test - - - - com.jayway.restassured - rest-assured - 2.4.0 - test - - - - - - - - com.google.guava - guava - 11.0.1 - - - - - - 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 - - - - - repo.jenkins-ci.org - Jenkins Repository - http://repo.jenkins-ci.org/public/ - - - jgit-repository - Eclipse JGit Repository - http://download.eclipse.org/jgit/maven - - - - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - + + JIRA + https://issues.jenkins-ci.org/browse/JENKINS/component/15896 + 3.3 2.5.1 - - - - - maven-compiler-plugin - - 1.6 - 1.6 - - - - - - + + + repo.jenkins-ci.org + Jenkins Repository + http://repo.jenkins-ci.org/public/ + + + jgit-repository + Eclipse JGit Repository + http://download.eclipse.org/jgit/maven + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + + + com.google.guava + guava + 11.0.1 + + + + + + org.apache.commons + commons-lang3 + 3.4 + + + + org.slf4j + slf4j-jdk14 + 1.7.7 + + + + org.jenkins-ci.plugins + github-api + 1.67 + + + + org.jenkins-ci.plugins + git + 2.0 + + + + org.eclipse.jgit + org.eclipse.jgit + 0.12.1 + + + + org.jenkins-ci.plugins + credentials + 1.22 + + + + org.jenkins-ci.plugins + plain-credentials + 1.1 + + + + org.jenkins-ci.plugins + multiple-scms + 0.2 + true + + + + org.jenkins-ci.modules + instance-identity + 1.3 + provided + + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + + junit + junit + 4.12 + test + + + + org.jmock + jmock-junit4 + 2.5.1 + test + + + + org.mockito + mockito-core + 1.10.19 + test + + + + com.jayway.restassured + rest-assured + 2.4.0 + test + + + + + + + + maven-compiler-plugin + + 1.6 + 1.6 + + + + + maven-checkstyle-plugin + 2.16 + + + checkstyle + validate + + check + + + + + UTF-8 + true + true + false + + src/test/resources/checkstyle/checkstyle-config.xml + + + + + + diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index 40e44acd7..ad6cf2d61 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -3,7 +3,6 @@ import hudson.Extension; import hudson.model.AbstractProject; import hudson.model.PeriodicWork; -import hudson.triggers.Trigger; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.webhook.WebhookManager; @@ -35,13 +34,13 @@ public class Cleaner extends PeriodicWork { * This queue is thread-safe, so any thread can write or * fetch names to this queue without additional sync */ - private final Queue сleanQueue = new ConcurrentLinkedQueue(); + private final Queue cleanQueue = new ConcurrentLinkedQueue(); /** * Called when a {@link GitHubPushTrigger} is about to be removed. */ /* package */ void onStop(AbstractProject job) { - сleanQueue.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); + cleanQueue.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); } @Override @@ -63,8 +62,8 @@ protected void doRun() throws Exception { .filter(isAlive()) // live repos .transformAndConcat(associatedNames()).toList(); - while (!сleanQueue.isEmpty()) { - GitHubRepositoryName name = сleanQueue.poll(); + while (!cleanQueue.isEmpty()) { + GitHubRepositoryName name = cleanQueue.poll(); WebhookManager.forHookUrl(url).unregisterFor(name, aliveRepos); } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index 77f2daf40..e054e2c85 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -6,33 +6,26 @@ import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; -import hudson.model.Describable; -import hudson.model.Descriptor; import hudson.model.Result; -import hudson.plugins.git.GitSCM; -import hudson.plugins.git.util.BuildData; -import hudson.scm.SCM; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; +import hudson.util.ListBoxModel; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.transport.RemoteConfig; -import org.eclipse.jgit.transport.URIish; -import org.jvnet.localizer.Localizable; +import org.jenkinsci.plugins.github.util.BuildDataHelper; import org.kohsuke.github.GHCommitState; -import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; import org.kohsuke.stapler.DataBoundConstructor; +import javax.annotation.Nonnull; import java.io.IOException; -import static hudson.model.Result.*; -import hudson.plugins.git.Revision; -import hudson.util.ListBoxModel; -import javax.annotation.Nonnull; -import org.jenkinsci.plugins.github.util.BuildDataHelper; +import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; +import static hudson.model.Result.FAILURE; +import static hudson.model.Result.SUCCESS; +import static hudson.model.Result.UNSTABLE; +import static java.lang.String.format; /** * Create commit status notifications on the commits based on the outcome of the build. @@ -44,42 +37,50 @@ public class GitHubCommitNotifier extends Notifier { private final String resultOnFailure; private static final Result[] SUPPORTED_RESULTS = {FAILURE, UNSTABLE, SUCCESS}; - + @DataBoundConstructor public GitHubCommitNotifier(String resultOnFailure) { this.resultOnFailure = resultOnFailure; } - + @Deprecated public GitHubCommitNotifier() { this(getDefaultResultOnFailure().toString()); } - public @Nonnull String getResultOnFailure() { + @Nonnull + public String getResultOnFailure() { return resultOnFailure != null ? resultOnFailure : getDefaultResultOnFailure().toString(); } - - public static @Nonnull Result getDefaultResultOnFailure() { + + @Nonnull + public static Result getDefaultResultOnFailure() { return SUPPORTED_RESULTS[0]; } - - /*package*/ @Nonnull Result getEffectiveResultOnFailure() { + + + @Nonnull + /*package*/ Result getEffectiveResultOnFailure() { if (resultOnFailure == null) { return getDefaultResultOnFailure(); } - + for (Result result : SUPPORTED_RESULTS) { - if (result.toString().equals(resultOnFailure)) return result; + if (result.toString().equals(resultOnFailure)) { + return result; + } } return getDefaultResultOnFailure(); } - + public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + public boolean perform(AbstractBuild build, + Launcher launcher, + BuildListener listener) throws InterruptedException, IOException { try { updateCommitStatus(build, listener); return true; @@ -88,18 +89,21 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen if (buildResult.equals(Result.FAILURE)) { throw error; } else { - listener.error("[GitHub Commit Notifier] - " + error.getMessage()); + listener.error(format("[GitHub Commit Notifier] - %s", error.getMessage())); if (buildResult.isWorseThan(build.getResult())) { - listener.getLogger().println("[GitHub Commit Notifier] - Build result will be set to " + buildResult); + listener.getLogger().println( + format("[GitHub Commit Notifier] - Build result will be set to %s", buildResult) + ); build.setResult(buildResult); } } } return true; } - - private void updateCommitStatus(@Nonnull AbstractBuild build, @Nonnull BuildListener listener) throws InterruptedException, IOException { - final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); + + private void updateCommitStatus(@Nonnull AbstractBuild build, + @Nonnull BuildListener listener) throws InterruptedException, IOException { + final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { for (GHRepository repository : name.resolve()) { GHCommitState state; @@ -123,8 +127,11 @@ private void updateCommitStatus(@Nonnull AbstractBuild build, @Nonnull Bui msg = Messages.CommitNotifier_Failed(build.getDisplayName(), duration); } - listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1)); - repository.createCommitStatus(sha1, state, build.getAbsoluteUrl(), msg, build.getProject().getFullName()); + listener.getLogger().println( + GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1) + ); + repository.createCommitStatus( + sha1, state, build.getAbsoluteUrl(), msg, build.getProject().getFullName()); } } } @@ -139,7 +146,7 @@ public boolean isApplicable(Class aClass) { public String getDisplayName() { return "Set build status on GitHub commit"; } - + public ListBoxModel doFillResultOnFailureItems() { ListBoxModel items = new ListBoxModel(); for (Result result : SUPPORTED_RESULTS) { diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 1660cbc9f..b98239cb3 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -208,13 +208,15 @@ public String getLog() throws IOException { * @since 1.350 */ public void writeLogTo(XMLOutput out) throws IOException { - new AnnotatedLargeText(getLogFile(), Charsets.UTF_8, true, this).writeHtmlTo(0, out.asWriter()); + new AnnotatedLargeText(getLogFile(), Charsets.UTF_8, true, this) + .writeHtmlTo(0, out.asWriter()); } } @Extension public static class DescriptorImpl extends TriggerDescriptor { - private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(MasterComputer.threadPoolForRemoting); + private final transient SequentialExecutionQueue queue = + new SequentialExecutionQueue(MasterComputer.threadPoolForRemoting); private transient String hookUrl; @@ -301,7 +303,8 @@ public boolean hasOverrideURL() { } /** - * Uses global xstream to enable migration alias used in {@link Migrator#enableCompatibilityAliases()} + * Uses global xstream to enable migration alias used in + * {@link Migrator#enableCompatibilityAliases()} */ @Override protected XmlFile getConfigFile() { @@ -320,7 +323,9 @@ public static boolean allowsHookUrlOverride() { /** * Set to false to prevent the user from overriding the hook URL. */ - public static boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(GitHubPushTrigger.class.getName() + ".disableOverride"); + public static final boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean( + GitHubPushTrigger.class.getName() + ".disableOverride" + ); private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPushTrigger.class); } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 46de1c258..b95aa1668 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -86,8 +86,11 @@ public static GitHubRepositoryName create(@Nonnull final String url) { return null; } + @SuppressWarnings("visibilitymodifier") public final String host; + @SuppressWarnings("visibilitymodifier") public final String userName; + @SuppressWarnings("visibilitymodifier") public final String repositoryName; public GitHubRepositoryName(String host, String userName, String repositoryName) { diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java index fc4cc2e95..9485cb2da 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java @@ -13,6 +13,8 @@ import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; import org.jenkinsci.plugins.multiplescms.MultiSCM; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.HashSet; @@ -26,11 +28,13 @@ * @since 1.7 */ public abstract class GitHubRepositoryNameContributor implements ExtensionPoint { + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubRepositoryNameContributor.class); + /** * Looks at the definition of {@link AbstractProject} and list up the related github repositories, * then puts them into the collection. */ - public abstract void parseAssociatedNames(AbstractProject job, Collection result); + public abstract void parseAssociatedNames(AbstractProject job, Collection result); public static ExtensionList all() { return Jenkins.getInstance().getExtensionList(GitHubRepositoryNameContributor.class); @@ -45,14 +49,14 @@ public static Collection parseAssociatedNames(AbstractProj } - static abstract class AbstractFromSCMImpl extends GitHubRepositoryNameContributor { + abstract static class AbstractFromSCMImpl extends GitHubRepositoryNameContributor { protected EnvVars buildEnv(AbstractProject job) { EnvVars env = new EnvVars(); for (EnvironmentContributor contributor : EnvironmentContributor.all()) { try { contributor.buildEnvironmentFor(job, env, TaskListener.NULL); } catch (Exception e) { - // ignore + LOGGER.debug("{} failed to build environment ({})", contributor.getClass(), e.getMessage()); } } return env; @@ -89,11 +93,13 @@ public void parseAssociatedNames(AbstractProject job, Collection job, Collection result) { diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 0023fdbaa..927a40885 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -3,19 +3,19 @@ import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; -import hudson.tasks.Builder; +import hudson.model.AbstractProject; import hudson.model.BuildListener; -import hudson.model.Descriptor; import hudson.tasks.BuildStepDescriptor; -import hudson.model.AbstractProject; -import org.kohsuke.stapler.DataBoundConstructor; -import hudson.plugins.git.util.BuildData; +import hudson.tasks.Builder; import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.util.BuildDataHelper; import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHRepository; +import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; -import org.jenkinsci.plugins.github.util.BuildDataHelper; + +import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; @Extension public class GitHubSetCommitStatusBuilder extends Builder { @@ -24,12 +24,20 @@ public GitHubSetCommitStatusBuilder() { } @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); + public boolean perform(AbstractBuild build, + Launcher launcher, + BuildListener listener) throws InterruptedException, IOException { + final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { for (GHRepository repository : name.resolve()) { - listener.getLogger().println(Messages.GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1)); - repository.createCommitStatus(sha1, GHCommitState.PENDING, build.getAbsoluteUrl(), Messages.CommitNotifier_Pending(build.getDisplayName()), build.getProject().getFullName()); + listener.getLogger().println( + GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1) + ); + repository.createCommitStatus(sha1, + GHCommitState.PENDING, + build.getAbsoluteUrl(), + Messages.CommitNotifier_Pending(build.getDisplayName()), + build.getProject().getFullName()); } } return true; @@ -47,4 +55,4 @@ public String getDisplayName() { return "Set build status to \"pending\" on GitHub commit"; } } -} \ No newline at end of file +} diff --git a/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java index cb225313a..c63c6a710 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java @@ -17,10 +17,11 @@ public interface GitHubTrigger { @Deprecated - public void onPost(); + void onPost(); // TODO: document me - public void onPost(String triggeredByUser); + void onPost(String triggeredByUser); + /** * Obtains the list of the repositories that this trigger is looking at. * @@ -32,20 +33,19 @@ public interface GitHubTrigger { * Alternatively, if the implementation doesn't worry about the backward compatibility, it can * implement this method to return an empty collection, then just implement {@link GitHubRepositoryNameContributor}. * - * @deprecated - * Call {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)} instead. + * @deprecated Call {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)} instead. */ - public Set getGitHubRepositories(); + Set getGitHubRepositories(); /** * Contributes {@link GitHubRepositoryName} from {@link GitHubTrigger#getGitHubRepositories()} * for backward compatibility */ @Extension - public static class GitHubRepositoryNameContributorImpl extends GitHubRepositoryNameContributor { + class GitHubRepositoryNameContributorImpl extends GitHubRepositoryNameContributor { @Override public void parseAssociatedNames(AbstractProject job, Collection result) { - for (GitHubTrigger ght : Util.filter(job.getTriggers().values(),GitHubTrigger.class)) { + for (GitHubTrigger ght : Util.filter(job.getTriggers().values(), GitHubTrigger.class)) { result.addAll(ght.getGitHubRepositories()); } } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 802c84c54..80db4f056 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -6,7 +6,6 @@ import hudson.model.AbstractProject; import hudson.model.RootAction; import hudson.model.UnprotectedRootAction; -import hudson.triggers.Trigger; import hudson.util.SequentialExecutionQueue; import jenkins.model.Jenkins; import org.apache.commons.lang3.Validate; @@ -48,8 +47,7 @@ public class GitHubWebHook implements UnprotectedRootAction { public static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation"; public static final String X_INSTANCE_IDENTITY = "X-Instance-Identity"; - private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting); - + private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting); public String getIconFileName() { return null; @@ -138,7 +136,7 @@ public static Jenkins getJenkinsInstance() throws IllegalStateException { * * @since 1.8 */ - public static abstract class Listener implements ExtensionPoint { + public abstract static class Listener implements ExtensionPoint { /** * Called when there is a change notification on a specific repository. diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java index 3de70a85a..b102a5ed4 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java @@ -7,26 +7,23 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - import java.io.IOException; -import java.util.logging.Logger; @Extension public class GitHubWebHookCrumbExclusion extends CrumbExclusion { - private static final Logger LOGGER = Logger.getLogger("com.cloudbees.jenkins.GitHubWebHookCrumbExclusion"); - - @Override - 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; - } - return false; - } + @Override + 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; + } + return false; + } - public String getExclusionPath() { - return "/" + GitHubWebHook.URLNAME + "/"; - } + public String getExclusionPath() { + return "/" + GitHubWebHook.URLNAME + "/"; + } } 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 9b75a0c5f..1db8758e2 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java @@ -4,7 +4,7 @@ /** * Add the Github Logo/Icon to the sidebar. - * + * * @author Stefan Saasen */ public final class GithubLinkAction implements Action { @@ -15,26 +15,17 @@ public GithubLinkAction(GithubProjectProperty githubProjectProperty) { this.projectProperty = githubProjectProperty; } - /* - * (non-Javadoc) - * @see hudson.model.Action#getDisplayName() - */ + @Override public String getDisplayName() { return "GitHub"; } - /* - * (non-Javadoc) - * @see hudson.model.Action#getIconFileName() - */ + @Override public String getIconFileName() { return "/plugin/github/logov3.png"; } - /* - * (non-Javadoc) - * @see hudson.model.Action#getUrlName() - */ + @Override public String getUrlName() { return projectProperty.getProjectUrl().baseUrl(); } 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 65aafdf3a..ed46c8dd2 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java @@ -15,21 +15,20 @@ *

* It's based on the TracLinkAnnotator. *

- * - * @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 + * @todo Change the annotator to use GithubUrl instead of the String url. + * Knowledge about the github url structure should be encapsulated in + * GithubUrl. */ @Extension public class GithubLinkAnnotator extends ChangeLogAnnotator { @Override - public void annotate(AbstractBuild build, Entry change, - MarkupText text) { + public void annotate(AbstractBuild build, Entry change, MarkupText text) { final GithubProjectProperty p = build.getProject().getProperty( GithubProjectProperty.class); - if (null == p || null == p.getProjectUrl()) { + if (null == p) { return; } annotate(p.getProjectUrl(), text, change); @@ -40,10 +39,10 @@ void annotate(final GithubUrl url, final MarkupText text, final Entry change) { for (LinkMarkup markup : MARKUPS) { markup.process(text, base); } - - if(change instanceof GitChangeSet) { - GitChangeSet cs = (GitChangeSet)change; - text.wrapBy("", " (commit: "+cs.getId()+")"); + + if (change instanceof GitChangeSet) { + GitChangeSet cs = (GitChangeSet) change; + text.wrapBy("", " (commit: " + cs.getId() + ")"); } } @@ -71,7 +70,7 @@ void process(MarkupText text, String url) { .compile("ANYWORD"); } - private static final LinkMarkup[] MARKUPS = new LinkMarkup[] { new LinkMarkup( + private static final LinkMarkup[] MARKUPS = new LinkMarkup[]{new LinkMarkup( "(?:C|c)lose(?:s?)\\s(? * As of now this is only the URL to the github project. - * - * @todo Should we store the GithubUrl instead of the String? + * * @author Stefan Saasen + * @todo Should we store the GithubUrl instead of the String? */ -public final class GithubProjectProperty extends - JobProperty> { +public final class GithubProjectProperty extends JobProperty> { /** * This will the URL to the project main branch. @@ -52,14 +49,7 @@ public Collection getJobActions(AbstractProject job) { } return Collections.emptyList(); } - /* - @Override - public JobPropertyDescriptor getDescriptor() { - return DESCRIPTOR; - } - public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); - */ @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { @@ -92,6 +82,6 @@ public JobProperty newInstance(StaplerRequest req, JSONObject formData) throw } } - + private static final Logger LOGGER = Logger.getLogger(GitHubPushTrigger.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 d6ace0f02..b331adcb3 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java @@ -3,7 +3,6 @@ import org.apache.commons.lang.StringUtils; /** - * * @author Stefan Saasen */ public final class GithubUrl { @@ -12,7 +11,7 @@ public final class GithubUrl { * Normalizes the github URL. *

* Removes unwanted path elements (e.g. tree/master). - * + * * @return URL to the project or null if input is invalid. */ private static String normalize(String url) { @@ -35,28 +34,21 @@ private static String normalize(String url) { this.baseUrl = normalize(input); } - /* - * (non-Javadoc) - * @see java.lang.Object#toString() - */ @Override public String toString() { return this.baseUrl; } - /** - * - * @return - */ public String baseUrl() { return this.baseUrl; } /** * Returns the URL to a particular commit. - * + * * @param id - the git SHA1 hash - * @return URL String (e.g. http://github.com/juretta/hudson-github-plugin/commit/5e31203faea681c41577b685818a361089fac1fc) + * + * @return URL String (e.g. http://github.com/juretta/github-plugin/commit/5e31203faea681c41577b685818a361089fac1fc) */ public String commitId(final String id) { return new StringBuilder().append(baseUrl).append("commit/").append(id).toString(); diff --git a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java index f5d3553e8..2ab3aea20 100644 --- a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java +++ b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java @@ -47,7 +47,7 @@ public void postInitialize() throws Exception { @Nonnull public static GitHubPluginConfig configuration() { return defaultIfNull( - GitHubPluginConfig.all().get(GitHubPluginConfig.class), + GitHubPluginConfig.all().get(GitHubPluginConfig.class), GitHubPluginConfig.EMPTY_CONFIG ); } diff --git a/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java b/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java index ec2d8f69b..01443a8e1 100644 --- a/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java +++ b/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java @@ -8,7 +8,6 @@ * * @author Kohsuke Kawaguchi * @deprecated Please use {@link org.jenkinsci.plugins.github.config.GitHubServerConfig} instead - * */ @Deprecated public class Credential { 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 f29f2ab21..79f798ef8 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -128,7 +128,7 @@ public Void apply(GHEventsSubscriber subscriber) { try { subscriber.onEvent(event, payload); } catch (Throwable t) { - LOGGER.error("Subscriber {} failed to process {} hook, skipping...", + LOGGER.error("Subscriber {} failed to process {} hook, skipping...", subscriber.getClass().getName(), event, t); } return null; 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 5a526a758..46f7f5dbe 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java @@ -3,24 +3,31 @@ import hudson.model.AbstractBuild; import hudson.plugins.git.Revision; import hudson.plugins.git.util.BuildData; -import java.io.IOException; -import javax.annotation.Nonnull; import org.eclipse.jgit.lib.ObjectId; +import javax.annotation.Nonnull; +import java.io.IOException; + /** * Stores common methods for {@link BuildData} handling. + * * @author Oleg Nenashev * @since 1.10 */ -public class BuildDataHelper { - +public final class BuildDataHelper { + private BuildDataHelper() { + } + /** * Gets SHA1 from the build. + * * @param build + * * @return SHA1 of the las * @throws IOException Cannot get the info about commit ID */ - public static @Nonnull ObjectId getCommitSHA1(@Nonnull AbstractBuild build) throws IOException { + @Nonnull + public static ObjectId getCommitSHA1(@Nonnull AbstractBuild build) throws IOException { BuildData buildData = build.getAction(BuildData.class); 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 e06d29b33..8a83f00e7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java @@ -55,7 +55,7 @@ public Iterator iterator() { public static FluentIterableWrapper from(final Iterable iterable) { return (iterable instanceof FluentIterableWrapper) ? (FluentIterableWrapper) iterable - : new FluentIterableWrapper(iterable) {}; + : new FluentIterableWrapper(iterable) { }; } /** @@ -121,7 +121,7 @@ public final Optional firstMatch(Predicate predicate) { * If the iterable is empty, {@code Optional.absent()} is returned. * * @throws NullPointerException if the first element is null; if this is a possibility, use - * {@code iterator().next()} or {@link Iterables#getFirst} instead. + * {@code iterator().next()} or {@link Iterables#getFirst} instead. */ public final Optional first() { Iterator iterator = iterable.iterator(); @@ -129,7 +129,7 @@ public final Optional first() { ? Optional.of(iterator.next()) : Optional.absent(); } - + /** * Returns list from wrapped iterable */ @@ -140,7 +140,6 @@ public List toList() { /** * Returns an {@code ImmutableSet} containing all of the elements from this fluent iterable with * duplicates removed. - * */ public final ImmutableSet toSet() { return ImmutableSet.copyOf(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 3e5c0e47a..aa0f3bb33 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -11,8 +11,8 @@ import java.util.Collection; -import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; 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 @@ -66,8 +66,8 @@ public Collection apply(AbstractProject job) { /** * If any of event subscriber interested in hook for job, 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 job alive and should have hook */ public static Predicate isAlive() { return new Predicate() { 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 4d0e6c02b..e54632748 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 @@ -12,7 +12,7 @@ * @author lanwen (Merkushev Kirill) */ public abstract class NullSafeFunction implements Function { - + @Override public T apply(@Nullable F input) { return applyNullSafe(Preconditions.checkNotNull(input, "This function not allows to use null as argument")); 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 8efb0e66c..b17f82116 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java @@ -42,14 +42,14 @@ class PayloadHandler extends AnnotationHandler { * @return parsed {@link GHEvent} or null on empty header or unknown value */ @Override - public Object parse(StaplerRequest request, GHEventHeader a, Class type, String parameterName) throws ServletException { + public Object parse(StaplerRequest req, GHEventHeader a, Class type, String param) throws ServletException { isTrue(GHEvent.class.isAssignableFrom(type), - "Parameter '%s' should has type %s, not %s", parameterName, + "Parameter '%s' should has type %s, not %s", param, GHEvent.class.getName(), type.getName() ); - String header = request.getHeader(EVENT_HEADER); + String header = req.getHeader(EVENT_HEADER); LOGGER.debug("Header {} -> {}", EVENT_HEADER, header); if (header == null) { 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 80de06697..e35378506 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -1,8 +1,8 @@ package org.jenkinsci.plugins.github.webhook; -import com.cloudbees.jenkins.GitHubPushTrigger; import com.cloudbees.jenkins.GitHubWebHook; import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.util.FluentIterableWrapper; import org.kohsuke.github.GHEvent; import org.kohsuke.stapler.HttpResponses; @@ -73,7 +73,7 @@ protected void shouldBePostMethod(StaplerRequest request) throws InvocationTarge } /** - * Used for {@link GitHubPushTrigger.DescriptorImpl#doCheckHookUrl(java.lang.String)} + * Used for {@link GitHubPluginConfig#doCheckHookUrl(String)}} */ protected void returnsInstanceIdentityIfLocalUrlTest(StaplerRequest req) throws InvocationTargetException { if (req.getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { @@ -124,18 +124,27 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod * @throws InvocationTargetException if any of preconditions is not satisfied */ protected void shouldContainParseablePayload(Object[] arguments) throws InvocationTargetException { - isTrue(arguments.length == 2, + isTrue(arguments.length == 2, "GHHook root action should take <(GHEvent) event> and <(String) payload> only"); FluentIterableWrapper from = from(newArrayList(arguments)); - isTrue(from.firstMatch(instanceOf(GHEvent.class)).isPresent(), "Hook should contain event type"); - isTrue(isNotBlank((String) from.firstMatch(instanceOf(String.class)).or("")), "Hook should contain payload"); + + isTrue( + from.firstMatch(instanceOf(GHEvent.class)).isPresent(), + "Hook should contain event type" + ); + isTrue( + isNotBlank((String) from.firstMatch(instanceOf(String.class)).or("")), + "Hook should contain payload" + ); } /** * Utility method to stop preprocessing if condition is false + * * @param condition on false throws exception - * @param msg to add to 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/WebhookManager.java b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java index ba575bdb2..b30150887 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -247,7 +247,7 @@ public Iterable apply(GHHook input) { } }; } - + /* * ACTIONS */ 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 ed39ba22e..2d41c5657 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 @@ -64,7 +64,8 @@ protected Set events() { @Override protected void onEvent(GHEvent event, String payload) { JSONObject json = JSONObject.fromObject(payload); - String repoUrl = json.getJSONObject("repository").getString("url"); // something like 'https://github.com/kohsuke/foo' + // something like 'https://github.com/bar/foo' + String repoUrl = json.getJSONObject("repository").getString("url"); final String pusherName = json.getJSONObject("pusher").getString("name"); LOGGER.info("Received POST for {}", repoUrl); @@ -89,14 +90,17 @@ public void run() { if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) { LOGGER.info("Poked {}", job.getFullDisplayName()); trigger.onPost(pusherName); - } else - LOGGER.debug("Skipped {} because it doesn't have a matching repository.", job.getFullDisplayName()); + } else { + LOGGER.debug("Skipped {} because it doesn't have a matching repository.", + job.getFullDisplayName()); + } } } } }); - for (GitHubWebHook.Listener listener : Jenkins.getInstance().getExtensionList(GitHubWebHook.Listener.class)) { + for (GitHubWebHook.Listener listener : Jenkins.getInstance() + .getExtensionList(GitHubWebHook.Listener.class)) { listener.onPushRepositoryChanged(pusherName, changedRepository); } diff --git a/src/test/resources/checkstyle/checkstyle-config.xml b/src/test/resources/checkstyle/checkstyle-config.xml new file mode 100644 index 000000000..e56f0d1d3 --- /dev/null +++ b/src/test/resources/checkstyle/checkstyle-config.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 73fccaedaff75ff3e441a8c766cdff27ec6ce851 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 17 Aug 2015 23:42:26 +0300 Subject: [PATCH 080/228] Remove included page --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d73654ed7..168895366 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,4 +119,3 @@ with [github-plugin](https://issues.jenkins-ci.org/browse/JENKINS/component/1589 - https://wiki.jenkins-ci.org/display/JENKINS/contributing - https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins -- https://wiki.jenkins-ci.org/display/JENKINS/GitHub+commit+messages From 007e9efeb075ee50ca2a0b07b6a3df968d4f1826 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Tue, 18 Aug 2015 01:11:45 +0300 Subject: [PATCH 081/228] unexpected npe fixes for findbugs --- .../cloudbees/jenkins/GitHubPushCause.java | 8 +++- .../GitHubRepositoryNameContributor.java | 2 +- .../github/config/GitHubPluginConfig.java | 3 +- .../config/GitHubTokenCredentialsCreator.java | 3 +- .../github/extension/GHEventsSubscriber.java | 22 ++++++----- .../plugins/github/util/JobInfoHelpers.java | 4 +- .../github/util/misc/NullSafeFunction.java | 8 ++-- .../github/webhook/GHEventPayload.java | 13 ++++--- .../webhook/RequirePostWithGHHookPayload.java | 4 +- .../github/webhook/WebhookManager.java | 39 ++++++++++--------- .../github/util/JobInfoHelpersTest.java | 17 ++++++-- 11 files changed, 76 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java b/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java index 8282d685e..3fe337618 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushCause.java @@ -1,9 +1,13 @@ package com.cloudbees.jenkins; import hudson.triggers.SCMTrigger.SCMTriggerCause; + import java.io.File; import java.io.IOException; +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; + /** * UI object that says a build is started by GitHub post-commit hook. * @@ -31,7 +35,7 @@ public GitHubPushCause(File pollingLog, String pusher) throws IOException { @Override public String getShortDescription() { - String pusher = pushedBy != null ? pushedBy : ""; - return "Started by GitHub push by " + pusher; + return format("Started by GitHub push by %s", trimToEmpty(pushedBy)); } } + diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java index 9485cb2da..34a8c61fd 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java @@ -56,7 +56,7 @@ protected EnvVars buildEnv(AbstractProject job) { try { contributor.buildEnvironmentFor(job, env, TaskListener.NULL); } catch (Exception e) { - LOGGER.debug("{} failed to build environment ({})", contributor.getClass(), e.getMessage()); + LOGGER.debug("{} failed to build env ({}), skipping", contributor.getClass(), e.getMessage(), e); } } return env; 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 c4c68a7c3..c719dbdfd 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.List; +import static com.google.common.base.Charsets.UTF_8; import static java.lang.String.format; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub; @@ -195,7 +196,7 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) { + "Are you running your own app?", value); } RSAPublicKey key = identity.getPublic(); - String expected = new String(Base64.encodeBase64(key.getEncoded())); + String expected = new String(Base64.encodeBase64(key.getEncoded()), UTF_8); if (!expected.equals(v)) { // if it responds but with a different ID, that's more likely wrong than correct return FormValidation.error("%s is connecting to different Jenkins instances", value); 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 fa8a42423..fe7c2e571 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java @@ -9,6 +9,7 @@ import com.cloudbees.plugins.credentials.domains.DomainSpecification; import com.cloudbees.plugins.credentials.domains.HostnameSpecification; import com.cloudbees.plugins.credentials.domains.SchemeSpecification; +import com.google.common.collect.ImmutableList; import hudson.Extension; import hudson.model.Describable; import hudson.model.Descriptor; @@ -67,7 +68,7 @@ public class GitHubTokenCredentialsCreator extends Descriptor GH_PLUGIN_REQUIRED_SCOPE = asList( + public static final List GH_PLUGIN_REQUIRED_SCOPE = ImmutableList.of( AMIN_HOOK, REPO, REPO_STATUS 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 79f798ef8..38794432c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -6,10 +6,14 @@ import hudson.ExtensionPoint; import hudson.model.AbstractProject; import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; +import org.jenkinsci.plugins.github.util.misc.NullSafePredicate; import org.kohsuke.github.GHEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collections; import java.util.Set; @@ -37,7 +41,7 @@ public abstract class GHEventsSubscriber implements ExtensionPoint { * * @return true to provide events to register and subscribe for this project */ - protected abstract boolean isApplicable(AbstractProject project); + protected abstract boolean isApplicable(@Nullable AbstractProject project); /** * Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)} @@ -73,9 +77,9 @@ public static ExtensionList all() { * @return converter to use in iterable manipulations */ public static Function> extractEvents() { - return new Function>() { + return new NullSafeFunction>() { @Override - public Set apply(GHEventsSubscriber subscriber) { + protected Set applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { return defaultIfNull(subscriber.events(), Collections.emptySet()); } }; @@ -89,9 +93,9 @@ public Set apply(GHEventsSubscriber subscriber) { * @return predicate to use in iterable filtering */ public static Predicate isApplicableFor(final AbstractProject project) { - return new Predicate() { + return new NullSafePredicate() { @Override - public boolean apply(GHEventsSubscriber subscriber) { + protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { return subscriber.isApplicable(project); } }; @@ -105,9 +109,9 @@ public boolean apply(GHEventsSubscriber subscriber) { * @return predicate to match against {@link GHEventsSubscriber} */ public static Predicate isInterestedIn(final GHEvent event) { - return new Predicate() { + return new NullSafePredicate() { @Override - public boolean apply(GHEventsSubscriber subscriber) { + protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { return defaultIfNull(subscriber.events(), emptySet()).contains(event); } }; @@ -122,9 +126,9 @@ public boolean apply(GHEventsSubscriber subscriber) { * @return function to process {@link GHEventsSubscriber} list. Returns null on apply. */ public static Function processEvent(final GHEvent event, final String payload) { - return new Function() { + return new NullSafeFunction() { @Override - public Void apply(GHEventsSubscriber subscriber) { + protected Void applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { try { subscriber.onEvent(event, payload); } catch (Throwable t) { 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 aa0f3bb33..b2dedd09c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -34,7 +34,7 @@ private JobInfoHelpers() { public static Predicate withTrigger(final Class clazz) { return new Predicate() { public boolean apply(AbstractProject job) { - return job.getTrigger(clazz) != null; + return job != null && job.getTrigger(clazz) != null; } }; } @@ -47,7 +47,7 @@ public boolean apply(AbstractProject job) { public static Predicate isBuildable() { return new Predicate() { public boolean apply(Job job) { - return job.isBuildable(); + return job != null && job.isBuildable(); } }; } 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 e54632748..9250253c0 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 @@ -1,10 +1,10 @@ package org.jenkinsci.plugins.github.util.misc; import com.google.common.base.Function; -import com.google.common.base.Preconditions; import javax.annotation.Nonnull; -import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; /** * This abstract class calls {@link #applyNullSafe(Object)} only after success validation of inner object for null @@ -14,8 +14,8 @@ public abstract class NullSafeFunction implements Function { @Override - public T apply(@Nullable F input) { - return applyNullSafe(Preconditions.checkNotNull(input, "This function not allows to use null as argument")); + public T apply(F input) { + return applyNullSafe(checkNotNull(input, "This function not allows to use null as argument")); } /** 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 fe3543ee6..58c2e1492 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java @@ -5,11 +5,13 @@ import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import org.apache.commons.io.IOUtils; +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.slf4j.Logger; +import javax.annotation.Nonnull; import javax.servlet.ServletException; import java.io.IOException; import java.lang.annotation.Documented; @@ -19,6 +21,7 @@ import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.apache.commons.lang3.Validate.notNull; import static org.slf4j.LoggerFactory.getLogger; /** @@ -54,7 +57,7 @@ class PayloadHandler extends AnnotationHandler { */ @Override public Object parse(StaplerRequest req, GHEventPayload a, Class type, String param) throws ServletException { - if (req.getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { + if (notNull(req, "Why StaplerRequest is null?").getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { // if self test for custom hook url return null; } @@ -78,9 +81,9 @@ 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 Function() { + return new NullSafeFunction() { @Override - public String apply(StaplerRequest request) { + protected String applyNullSafe(@Nonnull StaplerRequest request) { return request.getParameter("payload"); } }; @@ -92,9 +95,9 @@ public String apply(StaplerRequest request) { * @return function to extract payload from body */ protected static Function fromApplicationJson() { - return new Function() { + return new NullSafeFunction() { @Override - public String apply(StaplerRequest request) { + protected String applyNullSafe(@Nonnull StaplerRequest request) { try { return IOUtils.toString(request.getInputStream(), Charsets.UTF_8); } catch (IOException e) { 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 e35378506..b499e3ffc 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -20,6 +20,8 @@ import java.security.interfaces.RSAPublicKey; import java.util.logging.Logger; +import static com.cloudbees.jenkins.GitHubWebHook.X_INSTANCE_IDENTITY; +import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Predicates.instanceOf; import static com.google.common.collect.Lists.newArrayList; import static java.lang.annotation.ElementType.FIELD; @@ -84,7 +86,7 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod throws IOException, ServletException { RSAPublicKey key = new InstanceIdentity().getPublic(); rsp.setStatus(HttpServletResponse.SC_OK); - rsp.setHeader(GitHubWebHook.X_INSTANCE_IDENTITY, new String(encodeBase64(key.getEncoded()))); + rsp.setHeader(X_INSTANCE_IDENTITY, new String(encodeBase64(key.getEncoded()), UTF_8)); } }); } 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 b30150887..1a06ceba5 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -6,6 +6,8 @@ import hudson.model.AbstractProject; import org.apache.commons.lang.Validate; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; +import org.jenkinsci.plugins.github.util.misc.NullSafePredicate; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHException; import org.kohsuke.github.GHHook; @@ -13,6 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.io.IOException; import java.net.URL; import java.util.Collection; @@ -135,9 +138,9 @@ public void unregisterFor(GitHubRepositoryName name, List * @return function to register hooks for given events */ protected Function createHookSubscribedTo(final List events) { - return new Function() { + return new NullSafeFunction() { @Override - public GHHook apply(GitHubRepositoryName name) { + protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) { try { GHRepository repo = checkNotNull( from(name.resolve(allowedToManageHooks())).firstMatch(withAdminAccess()).orNull(), @@ -181,9 +184,9 @@ public GHHook apply(GitHubRepositoryName name) { * @return always true predicate */ private Predicate log(final String format) { - return new Predicate() { + return new NullSafePredicate() { @Override - public boolean apply(GHHook input) { + protected boolean applyNullSafe(@Nonnull GHHook input) { LOGGER.debug(format("%s {} (events: {})", format), input.getUrl(), input.getEvents()); return true; } @@ -196,9 +199,9 @@ public boolean apply(GHHook input) { * @return true if we have admin rights for repo */ protected Predicate withAdminAccess() { - return new Predicate() { + return new NullSafePredicate() { @Override - public boolean apply(GHRepository repo) { + protected boolean applyNullSafe(@Nonnull GHRepository repo) { return repo.hasAdminAccess(); } }; @@ -212,8 +215,8 @@ public boolean apply(GHRepository repo) { * @return true if hook is service hook */ protected Predicate serviceWebhookFor(final URL url) { - return new Predicate() { - public boolean apply(GHHook hook) { + return new NullSafePredicate() { + protected boolean applyNullSafe(@Nonnull GHHook hook) { return hook.getName().equals("jenkins") && hook.getConfig().get("jenkins_hook_url").equals(url.toExternalForm()); } @@ -228,8 +231,8 @@ public boolean apply(GHHook hook) { * @return true if hook is standard webhook */ protected Predicate webhookFor(final URL url) { - return new Predicate() { - public boolean apply(GHHook hook) { + return new NullSafePredicate() { + protected boolean applyNullSafe(@Nonnull GHHook hook) { return hook.getName().equals("web") && hook.getConfig().get("url").equals(url.toExternalForm()); } @@ -240,9 +243,9 @@ public boolean apply(GHHook hook) { * @return converter to extract events from each hook */ protected Function> eventsFromHook() { - return new Function>() { + return new NullSafeFunction>() { @Override - public Iterable apply(GHHook input) { + protected Iterable applyNullSafe(@Nonnull GHHook input) { return input.getEvents(); } }; @@ -256,9 +259,9 @@ public Iterable apply(GHHook input) { * @return converter to fetch from GH hooks list for each repo */ protected Function> fetchHooks() { - return new Function>() { + return new NullSafeFunction>() { @Override - public List apply(GHRepository repo) { + protected List applyNullSafe(@Nonnull GHRepository repo) { try { return repo.getHooks(); } catch (IOException e) { @@ -275,8 +278,8 @@ public List apply(GHRepository repo) { * @return converter to create GH hook for given url with given events */ protected Function createWebhook(final URL url, final Set events) { - return new Function() { - public GHHook apply(GHRepository repo) { + return new NullSafeFunction() { + protected GHHook applyNullSafe(@Nonnull GHRepository repo) { try { return repo.createWebHook(url, events); } catch (IOException e) { @@ -290,8 +293,8 @@ public GHHook apply(GHRepository repo) { * @return annihilator for hook, returns true if deletion was successful */ protected Predicate deleteWebhook() { - return new Predicate() { - public boolean apply(GHHook hook) { + return new NullSafePredicate() { + protected boolean applyNullSafe(@Nonnull GHHook hook) { try { hook.delete(); return true; diff --git a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java index 5099b9763..0be499962 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java @@ -2,12 +2,13 @@ import com.cloudbees.jenkins.GitHubPushTrigger; import hudson.model.FreeStyleProject; -import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import static org.hamcrest.Matchers.is; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; import static org.junit.Assert.assertThat; @@ -16,8 +17,8 @@ */ public class JobInfoHelpersTest { - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + @ClassRule + public static JenkinsRule jenkins = new JenkinsRule(); @Test public void shouldMatchForProjectWithTrigger() throws Exception { @@ -42,6 +43,16 @@ public void shouldNotMatchProjectWithoutTrigger() throws Exception { assertThat("without trigger", withTrigger(GitHubPushTrigger.class).apply(prj), is(false)); } + @Test + public void shouldNotMatchNullProject() throws Exception { + assertThat("null project", withTrigger(GitHubPushTrigger.class).apply(null), is(false)); + } + + @Test + public void shouldReturnNotBuildableOnNullProject() throws Exception { + assertThat("null project", isBuildable().apply(null), is(false)); + } + @Test public void shouldSeeProjectWithoutTriggerIsNotAliveForCleaner() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); From 6c9748911d4461555a6556ce41477b795c1c8cbe Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Tue, 18 Aug 2015 18:35:53 +0300 Subject: [PATCH 082/228] add separate PING event subscriber + bump gh-api to 1.69 --- pom.xml | 2 +- .../webhook/RequirePostWithGHHookPayload.java | 29 --------- .../subscriber/PingGHEventSubscriber.java | 59 +++++++++++++++++++ .../jenkins/GitHubWebHookFullTest.java | 3 +- .../subscriber/PingGHEventSubscriberTest.java | 33 +++++++++++ 5 files changed, 94 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java diff --git a/pom.xml b/pom.xml index e370f3fee..eaa4f8500 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ org.jenkins-ci.plugins github-api - 1.67 + 1.69 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 b499e3ffc..97aa20429 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -18,7 +18,6 @@ import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; import java.security.interfaces.RSAPublicKey; -import java.util.logging.Logger; import static com.cloudbees.jenkins.GitHubWebHook.X_INSTANCE_IDENTITY; import static com.google.common.base.Charsets.UTF_8; @@ -29,7 +28,6 @@ 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 javax.servlet.http.HttpServletResponse.SC_OK; import static org.apache.commons.codec.binary.Base64.encodeBase64; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; @@ -47,7 +45,6 @@ @InterceptorAnnotation(RequirePostWithGHHookPayload.Processor.class) public @interface RequirePostWithGHHookPayload { class Processor extends Interceptor { - private static final Logger LOGGER = Logger.getLogger(Processor.class.getName()); @Override public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, Object[] arguments) @@ -55,7 +52,6 @@ public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, O shouldBePostMethod(req); returnsInstanceIdentityIfLocalUrlTest(req); - logPingEvent(req); shouldContainParseablePayload(arguments); return target.invoke(req, rsp, instance, arguments); @@ -92,31 +88,6 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod } } - /** - * Additional logic to log ping event. In future can be replaced with separate - * {@link org.jenkinsci.plugins.github.extension.GHEventsSubscriber} with - * filtering of PING event to contribute. - * - * Wait for https://github.com/kohsuke/github-api/pull/204 will be released - * - * @throws InvocationTargetException returns OK 200 to client on ping event - */ - protected void logPingEvent(StaplerRequest req) throws InvocationTargetException { - if ("ping".equals(req.getHeader(GHEventHeader.PayloadHandler.EVENT_HEADER))) { - // until https://github.com/kohsuke/github-api/pull/204 will not be released - // after that use GHEvent.PING event form arguments - - LOGGER.info("Got ping event from GH"); - throw new InvocationTargetException(new HttpResponses.HttpResponseException() { - public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) - throws IOException { - rsp.setStatus(SC_OK); - rsp.getWriter().println("Ping received!"); - } - }); - } - } - /** * Precheck arguments contains not null GHEvent and not blank payload. * If any other argument will be added to root action index method, then arg count check should be changed 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 new file mode 100644 index 000000000..dca33be0b --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java @@ -0,0 +1,59 @@ +package org.jenkinsci.plugins.github.webhook.subscriber; + +import hudson.Extension; +import hudson.model.AbstractProject; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.kohsuke.github.GHEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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; + +/** + * Get ping events to log them + * + * @author lanwen (Merkushev Kirill) + * @since TODO + */ +@Extension +@SuppressWarnings("unused") +public class PingGHEventSubscriber extends GHEventsSubscriber { + private static final Logger LOGGER = LoggerFactory.getLogger(PingGHEventSubscriber.class); + + /** + * This subscriber is not applicable to any job + * + * @param project ignored + * + * @return always false + */ + @Override + protected boolean isApplicable(AbstractProject project) { + return false; + } + + /** + * @return set with only ping event + */ + @Override + protected Set events() { + return immutableEnumSet(PING); + } + + /** + * Logs repo on ping event + * + * @param event only PING event + * @param payload payload of gh-event. Never blank + */ + @Override + protected void onEvent(GHEvent event, String payload) { + // something like + String repo = fromObject(payload).getJSONObject("repository").getString("url"); + LOGGER.info("{} webhook received from repo <{}>!", event, repo); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index 65fa22ce4..149768766 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -82,13 +82,12 @@ public void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { @Test public void shouldParsePingFromGH() throws Exception { given().spec(spec) - .header(eventHeader("ping")) + .header(eventHeader(GHEvent.PING)) .header(JSON_CONTENT_TYPE) .content(classpath("payloads/ping.json")) .log().all() .expect().log().all() .statusCode(SC_OK) - .body(containsString("Ping received!")) .post(); } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java new file mode 100644 index 000000000..dc11769aa --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java @@ -0,0 +1,33 @@ +package org.jenkinsci.plugins.github.webhook.subscriber; + +import hudson.model.FreeStyleProject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; +import org.kohsuke.github.GHEvent; + +import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class PingGHEventSubscriberTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + public void shouldBeNotApplicableForProjects() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + assertThat(new PingGHEventSubscriber().isApplicable(prj), is(false)); + } + + @Test + @WithoutJenkins + public void shouldParsePingPayload() throws Exception { + new PingGHEventSubscriber().onEvent(GHEvent.PING, classpath("payloads/ping.json")); + } +} From 134cda2e341fd5f97cd3f4a2f672cca378ec64c5 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 24 Aug 2015 01:10:38 +0300 Subject: [PATCH 083/228] change since annotations to new release version (1.13.0) --- .../java/com/cloudbees/jenkins/GitHubCommitNotifier.java | 7 ++++++- .../java/com/cloudbees/jenkins/GitHubRepositoryName.java | 2 +- .../plugins/github/config/GitHubPluginConfig.java | 2 +- .../plugins/github/config/GitHubServerConfig.java | 2 +- .../github/config/GitHubTokenCredentialsCreator.java | 2 +- .../org/jenkinsci/plugins/github/migration/Migrator.java | 2 +- .../github/webhook/subscriber/PingGHEventSubscriber.java | 2 +- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index e054e2c85..eab6878bd 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -31,13 +31,15 @@ * Create commit status notifications on the commits based on the outcome of the build. * * @author Nicolas De Loof - * @since TODO: define a version Result on failure is configurable. */ public class GitHubCommitNotifier extends Notifier { private final String resultOnFailure; private static final Result[] SUPPORTED_RESULTS = {FAILURE, UNSTABLE, SUCCESS}; + /** + * @since 1.10 + */ @DataBoundConstructor public GitHubCommitNotifier(String resultOnFailure) { this.resultOnFailure = resultOnFailure; @@ -48,6 +50,9 @@ public GitHubCommitNotifier() { this(getDefaultResultOnFailure().toString()); } + /** + * @since 1.10 + */ @Nonnull public String getResultOnFailure() { return resultOnFailure != null ? resultOnFailure : getDefaultResultOnFailure().toString(); diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index b95aa1668..560f84757 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -140,7 +140,7 @@ public Iterable resolve() { * @param predicate helps to filter only useful for resolve {@link GitHubServerConfig}s * * @return iterable with lazy login process for getting authenticated repos - * @since TODO + * @since 1.13.0 */ public Iterable resolve(Predicate predicate) { return from(GitHubPlugin.configuration().findGithubConfig(and(withHost(host), predicate))) 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 c719dbdfd..461494e84 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -43,7 +43,7 @@ * such as hook managing policy, credentials etc. * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.13.0 */ @Extension public class GitHubPluginConfig extends GlobalConfiguration { 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 4bf58ced7..03eacd24f 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -48,7 +48,7 @@ * So one github server can be used with many creds and one token can be used multiply times in lot of gh servers * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.13.0 */ @XStreamAlias("github-server-config") public class GitHubServerConfig extends AbstractDescribableImpl { 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 fe7c2e571..a3e957475 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java @@ -53,7 +53,7 @@ * and save it as token credentials with help of plain-credentials plugin * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.13.0 */ @Extension public class GitHubTokenCredentialsCreator extends Descriptor implements 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 b6544569c..20d4aa60f 100644 --- a/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java +++ b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java @@ -25,7 +25,7 @@ * push trigger descriptor * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.13.0 */ public class Migrator { private static final Logger LOGGER = LoggerFactory.getLogger(Migrator.class); 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 dca33be0b..f8d3b27d2 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 @@ -17,7 +17,7 @@ * Get ping events to log them * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.13.0 */ @Extension @SuppressWarnings("unused") From ef8496bfa4ebce65fa9c02ab6c50a70c643fc195 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 24 Aug 2015 01:14:57 +0300 Subject: [PATCH 084/228] [maven-release-plugin] prepare release github-1.13.0 --- pom.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index eaa4f8500..e07e010a3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -12,7 +11,7 @@ com.coravy.hudson.plugins.github github - 1.13.0-SNAPSHOT + 1.13.0 hpi GitHub plugin @@ -40,7 +39,7 @@ 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 + github-1.13.0 From 2f7f4bfcfcb3dae8408dbb9674dbedd36b22f291 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 24 Aug 2015 01:15:05 +0300 Subject: [PATCH 085/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e07e010a3..c606bf77b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github - 1.13.0 + 1.14.0-SNAPSHOT hpi GitHub plugin @@ -39,7 +39,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.13.0 + HEAD From 7bcf65ae781ae2e798d3d7f8d78946a65185cd43 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 24 Aug 2015 01:23:17 +0300 Subject: [PATCH 086/228] [maven-release-plugin] rollback the release of github-1.13.0 --- pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c606bf77b..eaa4f8500 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -11,7 +12,7 @@ com.coravy.hudson.plugins.github github - 1.14.0-SNAPSHOT + 1.13.0-SNAPSHOT hpi GitHub plugin From ec401070f05ab66075b7f5034e17861c72c96c34 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 24 Aug 2015 01:34:19 +0300 Subject: [PATCH 087/228] suppress checkstyle on Messages.java --- pom.xml | 3 +++ .../resources/checkstyle/checkstyle-suppressions.xml | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/test/resources/checkstyle/checkstyle-suppressions.xml diff --git a/pom.xml b/pom.xml index eaa4f8500..de5fc096a 100644 --- a/pom.xml +++ b/pom.xml @@ -206,6 +206,9 @@ src/test/resources/checkstyle/checkstyle-config.xml + + src/test/resources/checkstyle/checkstyle-suppressions.xml + diff --git a/src/test/resources/checkstyle/checkstyle-suppressions.xml b/src/test/resources/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 000000000..4770865fb --- /dev/null +++ b/src/test/resources/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + From 88c6afc96b9557ddd19a3ae6ed1ea1089d103448 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 24 Aug 2015 02:01:37 +0300 Subject: [PATCH 088/228] [maven-release-plugin] prepare release github-1.13.0 --- pom.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index de5fc096a..6e63de569 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -12,7 +11,7 @@ com.coravy.hudson.plugins.github github - 1.13.0-SNAPSHOT + 1.13.0 hpi GitHub plugin @@ -40,7 +39,7 @@ 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 + github-1.13.0 From 8791c2a668a954c7394ab8911af3c6ffe7c854f4 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Mon, 24 Aug 2015 02:01:43 +0300 Subject: [PATCH 089/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6e63de569..9723d6c7f 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.coravy.hudson.plugins.github github - 1.13.0 + 1.14.0-SNAPSHOT hpi GitHub plugin @@ -39,7 +39,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.13.0 + HEAD From e4f7c8e8e152fbec8d705815885aaf1046394329 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 30 Aug 2015 19:04:52 +0300 Subject: [PATCH 090/228] don't run cleaner if we don't manage hooks --- src/main/java/com/cloudbees/jenkins/Cleaner.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index ad6cf2d61..ea89635d9 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -55,6 +55,10 @@ public long getRecurrencePeriod() { */ @Override protected void doRun() throws Exception { + if (!GitHubPlugin.configuration().isManageHooks()) { + return; + } + URL url = GitHubPlugin.configuration().getHookUrl(); List jobs = Jenkins.getInstance().getAllItems(AbstractProject.class); From fe0c542855f49932f93d26f00f784dfcff86d310 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 30 Aug 2015 19:06:15 +0300 Subject: [PATCH 091/228] throw config exception on empty jenkins root url --- .../plugins/github/config/GitHubPluginConfig.java | 14 ++++++++++---- .../jenkinsci/plugins/github/Messages.properties | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/github/Messages.properties 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 461494e84..bf8ae38a2 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -14,6 +14,7 @@ import org.apache.commons.codec.binary.Base64; import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.Messages; import org.jenkinsci.plugins.github.internal.GHPluginConfigException; import org.jenkinsci.plugins.github.migration.Migrator; import org.kohsuke.github.GitHub; @@ -34,6 +35,7 @@ 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.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; @@ -103,13 +105,17 @@ public void setOverrideHookUrl(boolean overrideHookUrl) { public URL getHookUrl() throws GHPluginConfigException { try { + String jenkinsUrl = Jenkins.getInstance().getRootUrl(); + + if (isEmpty(jenkinsUrl)) { + throw new GHPluginConfigException(Messages.global_config_url_is_empty()); + } + return hookUrl != null ? hookUrl - : new URL(Jenkins.getInstance().getRootUrl() + GitHubWebHook.get().getUrlName() + '/'); + : new URL(jenkinsUrl + GitHubWebHook.get().getUrlName() + '/'); } catch (MalformedURLException e) { - throw new GHPluginConfigException( - "Mailformed GH hook url in global configuration (%s)", e.getMessage() - ); + throw new GHPluginConfigException(Messages.global_config_hook_url_is_mailformed(e.getMessage())); } } diff --git a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties new file mode 100644 index 000000000..eb041faad --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties @@ -0,0 +1,2 @@ +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.mailformed=Mailformed GH hook url in global configuration ({0}) From b5db64c7b11605c4de959605878c89364ac6a96e Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 31 Aug 2015 14:17:31 +0300 Subject: [PATCH 092/228] check hook url for override before checking jenkins default url + typo fix in word malformed --- .../github/config/GitHubPluginConfig.java | 52 ++++++++++++++----- .../plugins/github/Messages.properties | 2 +- 2 files changed, 40 insertions(+), 14 deletions(-) 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 bf8ae38a2..b60deb3d6 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -35,7 +35,7 @@ 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; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; @@ -103,19 +103,15 @@ public void setOverrideHookUrl(boolean overrideHookUrl) { this.overrideHookUrl = overrideHookUrl; } + /** + * @return hook url used as endpoint to search and write auto-managed hooks in GH + * @throws GHPluginConfigException if default jenkins url is malformed + */ public URL getHookUrl() throws GHPluginConfigException { - try { - String jenkinsUrl = Jenkins.getInstance().getRootUrl(); - - if (isEmpty(jenkinsUrl)) { - throw new GHPluginConfigException(Messages.global_config_url_is_empty()); - } - - return hookUrl != null - ? hookUrl - : new URL(jenkinsUrl + GitHubWebHook.get().getUrlName() + '/'); - } catch (MalformedURLException e) { - throw new GHPluginConfigException(Messages.global_config_hook_url_is_mailformed(e.getMessage())); + if (hookUrl != null) { + return hookUrl; + } else { + return constructDefaultUrl(); } } @@ -213,4 +209,34 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) { return FormValidation.error(e, "Failed to test a connection to %s", value); } } + + /** + * Used by default in {@link #getHookUrl()} + * + * @return url to be used in GH hooks configuration as main endpoint + * @throws GHPluginConfigException if jenkins root url empty of malformed + */ + private URL constructDefaultUrl() { + String jenkinsUrl = Jenkins.getInstance().getRootUrl(); + validateConfig(isNotEmpty(jenkinsUrl), Messages.global_config_url_is_empty()); + try { + return new URL(jenkinsUrl + GitHubWebHook.get().getUrlName() + '/'); + } catch (MalformedURLException e) { + throw new GHPluginConfigException(Messages.global_config_hook_url_is_malformed(e.getMessage())); + } + } + + /** + * Util method just to hide one more if for better readability + * + * @param state to check. If false, then exception will be thrown + * @param message message to describe exception in case of false state + * + * @throws GHPluginConfigException if state is false + */ + private void validateConfig(boolean state, String message) { + if (!state) { + throw new GHPluginConfigException(message); + } + } } diff --git a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties index eb041faad..464b5c807 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties @@ -1,2 +1,2 @@ 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.mailformed=Mailformed GH hook url in global configuration ({0}) +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 From 061d849cd41f079a205656afa4d3583fce821351 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 31 Aug 2015 23:38:44 +0300 Subject: [PATCH 093/228] [FIXES JENKINS-30223] return back com.cloudbees.jenkins.Credential as of it makes fail to boot jenkins after installation of plugin which depends on this class. Also remove migration to another package for this class and use it directly --- .../com/cloudbees/jenkins/Credential.java | 67 +++++++++++++++++++ .../cloudbees/jenkins/GitHubPushTrigger.java | 1 - .../github/config/GitHubServerConfig.java | 5 ++ .../plugins/github/deprecated/Credential.java | 36 ---------- .../plugins/github/migration/Migrator.java | 9 ++- .../github/migration/MigratorTest.java | 2 +- 6 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/cloudbees/jenkins/Credential.java delete mode 100644 src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java diff --git a/src/main/java/com/cloudbees/jenkins/Credential.java b/src/main/java/com/cloudbees/jenkins/Credential.java new file mode 100644 index 000000000..d5b801a7b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/Credential.java @@ -0,0 +1,67 @@ +package com.cloudbees.jenkins; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.kohsuke.github.GitHub; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.CheckForNull; +import java.io.IOException; + +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; + +/** + * Credential to access GitHub. + * Used only for migration. + * + * @author Kohsuke Kawaguchi + * @see org.jenkinsci.plugins.github.config.GitHubPluginConfig + * @see GitHubServerConfig + * @deprecated since 1.13.0 plugin uses credentials-plugin to manage tokens. All configuration moved to + * {@link org.jenkinsci.plugins.github.config.GitHubPluginConfig} which can be fetched via + * {@link GitHubPlugin#configuration()}. You can fetch corresponding config with creds by + * {@link org.jenkinsci.plugins.github.config.GitHubPluginConfig#findGithubConfig(Predicate)} which returns + * iterable over authorized nonnull {@link GitHub}s matched your predicate + */ +@Deprecated +public class Credential { + @SuppressWarnings("visibilitymodifier") + public final transient String username; + @SuppressWarnings("visibilitymodifier") + public final transient String apiUrl; + @SuppressWarnings("visibilitymodifier") + public final transient String oauthAccessToken; + + @DataBoundConstructor + public Credential(String username, String apiUrl, String oauthAccessToken) { + this.username = username; + this.apiUrl = apiUrl; + this.oauthAccessToken = oauthAccessToken; + } + + public String getUsername() { + return username; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getOauthAccessToken() { + return oauthAccessToken; + } + + /** + * @return authorized first {@link GitHub} from global config or null if no any + * @throws IOException never thrown, but in signature for backward compatibility + * @deprecated see class javadoc. Now any instance return same GH. Please use new api to fetch another + */ + @CheckForNull + @Deprecated + public GitHub login() throws IOException { + return from(GitHubPlugin.configuration().findGithubConfig(Predicates.alwaysTrue())) + .first().orNull(); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index b98239cb3..5d0427d04 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -18,7 +18,6 @@ import org.apache.commons.jelly.XMLOutput; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; -import org.jenkinsci.plugins.github.deprecated.Credential; import org.jenkinsci.plugins.github.internal.GHPluginConfigException; import org.jenkinsci.plugins.github.migration.Migrator; import org.kohsuke.stapler.DataBoundConstructor; 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 03eacd24f..1fa853020 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -191,6 +191,11 @@ public static String tokenFor(String credentialsId) { /** * Returns true if given host is part of stored (or default if blank) api url * + * For example: + * withHost(api.github.com).apply(config for ~empty~) = true + * withHost(api.github.com).apply(config for api.github.com) = true + * withHost(api.github.com).apply(config for github.company.com) = false + * * @param host host to find in api url * * @return predicate to match against {@link GitHubServerConfig} diff --git a/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java b/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java deleted file mode 100644 index 01443a8e1..000000000 --- a/src/main/java/org/jenkinsci/plugins/github/deprecated/Credential.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jenkinsci.plugins.github.deprecated; - -import org.kohsuke.stapler.DataBoundConstructor; - -/** - * Credential to access GitHub. - * Used only for migration. - * - * @author Kohsuke Kawaguchi - * @deprecated Please use {@link org.jenkinsci.plugins.github.config.GitHubServerConfig} instead - */ -@Deprecated -public class Credential { - private final transient String username; - private final transient String apiUrl; - private final transient String oauthAccessToken; - - @DataBoundConstructor - public Credential(String username, String apiUrl, String oauthAccessToken) { - this.username = username; - this.apiUrl = apiUrl; - this.oauthAccessToken = oauthAccessToken; - } - - public String getUsername() { - return username; - } - - public String getApiUrl() { - return apiUrl; - } - - public String getOauthAccessToken() { - return oauthAccessToken; - } -} 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 20d4aa60f..c34849042 100644 --- a/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java +++ b/src/main/java/org/jenkinsci/plugins/github/migration/Migrator.java @@ -1,5 +1,6 @@ package org.jenkinsci.plugins.github.migration; +import com.cloudbees.jenkins.Credential; import com.cloudbees.jenkins.GitHubPushTrigger; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.google.common.annotations.VisibleForTesting; @@ -9,7 +10,6 @@ import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator; -import org.jenkinsci.plugins.github.deprecated.Credential; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,10 +92,13 @@ public GitHubServerConfig apply(Credential input) { } /** - * - Old plain credentials moved to deprecated package as used only for migration + * Enable xml migration from deprecated nodes to new + * + * Can be used for example as + * Jenkins.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.Credential", Credential.class); */ public static void enableCompatibilityAliases() { - Jenkins.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.Credential", Credential.class); + // not used at this moment } /** diff --git a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java index e2524a8f5..6fd01cb02 100644 --- a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java @@ -1,12 +1,12 @@ package org.jenkinsci.plugins.github.migration; +import com.cloudbees.jenkins.Credential; import com.cloudbees.jenkins.GitHubPushTrigger; import com.cloudbees.jenkins.GitHubWebHook; import hudson.model.FreeStyleProject; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.jenkinsci.plugins.github.deprecated.Credential; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; From 756172217d4065835d854c2293b96ce1005fa09f Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Tue, 1 Sep 2015 15:31:56 +0300 Subject: [PATCH 094/228] additional request logging for form submitting --- .../org/jenkinsci/plugins/github/config/GitHubPluginConfig.java | 2 ++ 1 file changed, 2 insertions(+) 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 b60deb3d6..83b1524f3 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -157,6 +157,8 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti try { req.bindJSON(this, json); } 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("Mailformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); } From 27f9802d1d9d0fab313850f801d53fa0a4b7bd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mun=CC=83iz?= Date: Wed, 1 Jul 2015 22:46:35 +0200 Subject: [PATCH 095/228] [JENKINS-27136] Workflow plugin support --- pom.xml | 44 +++-- .../java/com/cloudbees/jenkins/Cleaner.java | 8 +- .../cloudbees/jenkins/GitHubPushTrigger.java | 25 ++- .../GitHubRepositoryNameContributor.java | 92 +++++----- .../com/cloudbees/jenkins/GitHubTrigger.java | 12 +- .../com/cloudbees/jenkins/GitHubWebHook.java | 15 +- .../github/config/GitHubPluginConfig.java | 6 +- .../github/extension/GHEventsSubscriber.java | 5 +- .../plugins/github/util/JobInfoHelpers.java | 33 ++-- .../github/webhook/WebhookManager.java | 8 +- .../DefaultPushGHEventSubscriber.java | 20 ++- .../subscriber/PingGHEventSubscriber.java | 4 +- .../cloudbees/jenkins/GitHubWebHookTest.java | 5 +- .../extension/GHEventsSubscriberTest.java | 5 +- .../webhook/subscriber/WebhookWorkflow.java | 46 +++++ .../payloads/push-wf.json | 160 ++++++++++++++++++ 16 files changed, 372 insertions(+), 116 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json diff --git a/pom.xml b/pom.xml index 9723d6c7f..b0707a383 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,12 @@ - + 4.0.0 org.jenkins-ci.plugins plugin - - 1.554.1 + 1.580 com.coravy.hudson.plugins.github @@ -41,7 +41,7 @@ https://github.com/jenkinsci/github-plugin HEAD - + JIRA https://issues.jenkins-ci.org/browse/JENKINS/component/15896 @@ -102,13 +102,7 @@ org.jenkins-ci.plugins git - 2.0 - - - - org.eclipse.jgit - org.eclipse.jgit - 0.12.1 + 2.4.0 @@ -123,13 +117,6 @@ 1.1 - - org.jenkins-ci.plugins - multiple-scms - 0.2 - true - - org.jenkins-ci.modules instance-identity @@ -152,13 +139,6 @@ test - - org.jmock - jmock-junit4 - 2.5.1 - test - - org.mockito mockito-core @@ -173,6 +153,20 @@ test + + org.jenkins-ci.plugins.workflow + workflow-job + 1.4 + test + + + + org.jenkins-ci.plugins.workflow + workflow-cps + 1.4 + test + + diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index ea89635d9..7544ca6e2 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.AbstractProject; +import hudson.model.Job; import hudson.model.PeriodicWork; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.GitHubPlugin; @@ -28,7 +28,7 @@ public class Cleaner extends PeriodicWork { /** * Queue contains repo names prepared to cleanup. - * After configure method on job, trigger calls {@link #onStop(AbstractProject)} + * After configure method on job, trigger calls {@link #onStop(Job)} * which converts to repo names with help of contributors. * * This queue is thread-safe, so any thread can write or @@ -39,7 +39,7 @@ public class Cleaner extends PeriodicWork { /** * Called when a {@link GitHubPushTrigger} is about to be removed. */ - /* package */ void onStop(AbstractProject job) { + /* package */ void onStop(Job job) { cleanQueue.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job)); } @@ -61,7 +61,7 @@ protected void doRun() throws Exception { URL url = GitHubPlugin.configuration().getHookUrl(); - List jobs = Jenkins.getInstance().getAllItems(AbstractProject.class); + List jobs = Jenkins.getInstance().getAllItems(Job.class); List aliveRepos = from(jobs) .filter(isAlive()) // live repos .transformAndConcat(associatedNames()).toList(); diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 5d0427d04..04a8befee 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -8,6 +8,7 @@ import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Item; +import hudson.model.Job; import hudson.model.Project; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; @@ -15,6 +16,8 @@ import hudson.util.StreamTaskListener; import jenkins.model.Jenkins; import jenkins.model.Jenkins.MasterComputer; +import jenkins.model.ParameterizedJobMixIn; +import jenkins.triggers.SCMTriggerItem.SCMTriggerItems; import org.apache.commons.jelly.XMLOutput; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; @@ -24,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import java.io.File; import java.io.IOException; import java.io.PrintStream; @@ -43,7 +47,8 @@ * * @author Kohsuke Kawaguchi */ -public class GitHubPushTrigger extends Trigger> implements GitHubTrigger { +public class GitHubPushTrigger extends Trigger> implements GitHubTrigger { + @DataBoundConstructor public GitHubPushTrigger() { } @@ -70,7 +75,7 @@ private boolean runPolling() { PrintStream logger = listener.getLogger(); long start = System.currentTimeMillis(); logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date())); - boolean result = job.poll(listener).hasChanges(); + boolean result = SCMTriggerItems.asSCMTriggerItem(job).poll(listener).hasChanges(); logger.println("Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start)); if (result) { logger.println("Changes found"); @@ -105,7 +110,14 @@ public void run() { LOGGER.warn("Failed to parse the polling log", e); cause = new GitHubPushCause(pushBy); } - if (job.scheduleBuild(cause)) { + // TODO use standard method in 1.621+ + ParameterizedJobMixIn scheduledJob = new ParameterizedJobMixIn() { + @Override + protected Job asJob() { + return job; + } + }; + if (scheduledJob.scheduleBuild(cause)) { LOGGER.info("SCM changes detected in " + job.getName() + ". Triggering " + name); } else { LOGGER.info("SCM changes detected in " + job.getName() + ". Job is already in the queue"); @@ -131,7 +143,7 @@ public Set getGitHubRepositories() { } @Override - public void start(AbstractProject project, boolean newInstance) { + public void start(Job project, boolean newInstance) { super.start(project, newInstance); if (newInstance && GitHubPlugin.configuration().isManageHooks()) { registerHooks(); @@ -181,7 +193,7 @@ public DescriptorImpl getDescriptor() { * Action object for {@link Project}. Used to display the polling log. */ public final class GitHubWebHookPollingAction implements Action { - public AbstractProject getOwner() { + public Job getOwner() { return job; } @@ -223,7 +235,8 @@ public static class DescriptorImpl extends TriggerDescriptor { @Override public boolean isApplicable(Item item) { - return item instanceof AbstractProject; + return item instanceof Job && SCMTriggerItems.asSCMTriggerItem(item) != null + && item instanceof ParameterizedJobMixIn.ParameterizedJob; } @Override diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java index 34a8c61fd..b36101fe9 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java @@ -4,21 +4,23 @@ import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; +import hudson.Util; import hudson.model.AbstractProject; import hudson.model.EnvironmentContributor; +import hudson.model.Job; import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; import hudson.scm.SCM; import jenkins.model.Jenkins; +import jenkins.triggers.SCMTriggerItem; +import jenkins.triggers.SCMTriggerItem.SCMTriggerItems; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; -import org.jenkinsci.plugins.multiplescms.MultiSCM; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Set; /** @@ -33,14 +35,40 @@ 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)} */ - public abstract void parseAssociatedNames(AbstractProject job, Collection result); + @Deprecated + public void parseAssociatedNames(AbstractProject job, Collection result) { + parseAssociatedNames((Job) job, result); + } + + /** + * Looks at the definition of {@link Job} and list up the related github repositories, + * then puts them into the collection. + */ + public /*abstract*/ void parseAssociatedNames(Job job, Collection result) { + if (Util.isOverridden(GitHubRepositoryNameContributor.class, getClass(), + "parseAssociatedNames", AbstractProject.class, Collection.class) && job instanceof AbstractProject) { + parseAssociatedNames((AbstractProject) job, result); + } else { + throw new AbstractMethodError("you must override the new overload of parseAssociatedNames"); + } + } public static ExtensionList all() { return Jenkins.getInstance().getExtensionList(GitHubRepositoryNameContributor.class); } + /** + * @deprecated Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(Job)} + */ + @Deprecated public static Collection parseAssociatedNames(AbstractProject job) { + return parseAssociatedNames((Job) job); + } + + public static Collection parseAssociatedNames(Job job) { Set names = new HashSet(); for (GitHubRepositoryNameContributor c : all()) { c.parseAssociatedNames(job, names); @@ -48,15 +76,30 @@ public static Collection parseAssociatedNames(AbstractProj return names; } + /** + * Default implementation that looks at SCMs + */ + @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()) { + addRepositories(scm, envVars, result); + } + } + } - abstract static class AbstractFromSCMImpl extends GitHubRepositoryNameContributor { - protected EnvVars buildEnv(AbstractProject job) { + protected EnvVars buildEnv(Job job) { EnvVars env = new EnvVars(); for (EnvironmentContributor contributor : EnvironmentContributor.all()) { try { contributor.buildEnvironmentFor(job, env, TaskListener.NULL); } catch (Exception e) { - LOGGER.debug("{} failed to build env ({}), skipping", contributor.getClass(), e.getMessage(), e); + LOGGER.debug(e.getMessage(), e); + // ignore } } return env; @@ -77,41 +120,4 @@ protected static void addRepositories(SCM scm, EnvVars env, Collection job, Collection result) { - addRepositories(job.getScm(), buildEnv(job), result); - } - } - - /** - * MultiSCM support separated into a different extension point since this is an optional dependency - */ - @Extension(optional = true) - @SuppressWarnings("unused") - public static class FromMultiSCM extends AbstractFromSCMImpl { - // make this class fail to load if MultiSCM is not present - public FromMultiSCM() { - MultiSCM.class.toString(); - } - - @Override - public void parseAssociatedNames(AbstractProject job, Collection result) { - if (job.getScm() instanceof MultiSCM) { - EnvVars env = buildEnv(job); - - MultiSCM multiSCM = (MultiSCM) job.getScm(); - List scmList = multiSCM.getConfiguredSCMs(); - for (SCM scm : scmList) { - addRepositories(scm, env, result); - } - } - } - } } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java index c63c6a710..1908b934d 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubTrigger.java @@ -3,7 +3,9 @@ import hudson.Extension; import hudson.Util; import hudson.model.AbstractProject; +import hudson.model.Job; import hudson.triggers.Trigger; +import jenkins.model.ParameterizedJobMixIn; import java.util.Collection; import java.util.Set; @@ -44,9 +46,13 @@ public interface GitHubTrigger { @Extension class GitHubRepositoryNameContributorImpl extends GitHubRepositoryNameContributor { @Override - public void parseAssociatedNames(AbstractProject job, Collection result) { - for (GitHubTrigger ght : Util.filter(job.getTriggers().values(), GitHubTrigger.class)) { - result.addAll(ght.getGitHubRepositories()); + public void parseAssociatedNames(Job job, Collection result) { + if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { + ParameterizedJobMixIn.ParameterizedJob p = (ParameterizedJobMixIn.ParameterizedJob) job; + // 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/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 80db4f056..7c66ff144 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -3,7 +3,7 @@ import com.google.common.base.Function; import hudson.Extension; import hudson.ExtensionPoint; -import hudson.model.AbstractProject; +import hudson.model.Job; import hudson.model.RootAction; import hudson.model.UnprotectedRootAction; import hudson.util.SequentialExecutionQueue; @@ -68,7 +68,7 @@ public String getUrlName() { * * @param job not null project to register hook for */ - public void registerHookFor(AbstractProject job) { + public void registerHookFor(Job job) { reRegisterHookForJob().apply(job); } @@ -77,8 +77,8 @@ public void registerHookFor(AbstractProject job) { * * @return list of jobs which jenkins tried to register hook */ - public List reRegisterAllHooks() { - return from(getJenkinsInstance().getAllItems(AbstractProject.class)) + public List reRegisterAllHooks() { + return from(getJenkinsInstance().getAllItems(Job.class)) .filter(isBuildable()) .filter(isAlive()) .transform(reRegisterHookForJob()).toList(); @@ -98,10 +98,10 @@ public void doIndex(@Nonnull @GHEventHeader GHEvent event, @Nonnull @GHEventPayl .transform(processEvent(event, payload)).toList(); } - private Function reRegisterHookForJob() { - return new Function() { + private Function reRegisterHookForJob() { + return new Function() { @Override - public AbstractProject apply(AbstractProject job) { + public Job apply(Job job) { LOGGER.debug("Calling registerHooks() for {}", notNull(job, "Job can't be null").getFullName()); // We should handle wrong url of self defined hook url here in any case with try-catch :( @@ -114,7 +114,6 @@ public AbstractProject apply(AbstractProject job) { } Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job); queue.execute(hookRegistrator); - return job; } }; 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 83b1524f3..745356a8d 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -3,10 +3,11 @@ import com.cloudbees.jenkins.GitHubWebHook; import com.google.common.base.Predicate; import com.google.common.base.Predicates; + import hudson.Extension; import hudson.XmlFile; -import hudson.model.AbstractProject; import hudson.model.Descriptor; +import hudson.model.Job; import hudson.util.FormValidation; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; @@ -24,6 +25,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -177,7 +179,7 @@ public FormValidation doReRegister() { return FormValidation.warning("Works only when Jenkins manages hooks (one ore 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()); 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 38794432c..00126537b 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -5,6 +5,7 @@ import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.model.AbstractProject; +import hudson.model.Job; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.jenkinsci.plugins.github.util.misc.NullSafePredicate; @@ -41,7 +42,7 @@ public abstract class GHEventsSubscriber implements ExtensionPoint { * * @return true to provide events to register and subscribe for this project */ - protected abstract boolean isApplicable(@Nullable AbstractProject project); + protected abstract boolean isApplicable(@Nullable Job project); /** * Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)} @@ -92,7 +93,7 @@ protected Set applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { * * @return predicate to use in iterable filtering */ - public static Predicate isApplicableFor(final AbstractProject project) { + public static Predicate isApplicableFor(final Job project) { return new NullSafePredicate() { @Override protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { 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 b2dedd09c..5439485c0 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -4,9 +4,9 @@ import com.cloudbees.jenkins.GitHubRepositoryNameContributor; import com.google.common.base.Function; import com.google.common.base.Predicate; -import hudson.model.AbstractProject; import hudson.model.Job; import hudson.triggers.Trigger; +import jenkins.model.ParameterizedJobMixIn; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import java.util.Collection; @@ -31,10 +31,21 @@ private JobInfoHelpers() { * * @return predicate with true on apply if job contains trigger of given class */ - public static Predicate withTrigger(final Class clazz) { - return new Predicate() { - public boolean apply(AbstractProject job) { - return job != null && job.getTrigger(clazz) != null; + public static Predicate withTrigger(final Class clazz) { + return new Predicate() { + public boolean apply(Job job) { + if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { + ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job; + // TODO use standard method in 1.621+ + for (Trigger trigger : pJob.getTriggers().values()) { + if (clazz.isInstance(trigger)) { + return true; + } + } + return false; + } else { + return false; + } } }; } @@ -55,9 +66,9 @@ public boolean apply(Job job) { /** * @return function which helps to convert job to repo names associated with this job */ - public static Function> associatedNames() { - return new Function>() { - public Collection apply(AbstractProject job) { + public static Function> associatedNames() { + return new Function>() { + public Collection apply(Job job) { return GitHubRepositoryNameContributor.parseAssociatedNames(job); } }; @@ -69,10 +80,10 @@ public Collection apply(AbstractProject job) { * * @return predicate with true if job alive and should have hook */ - public static Predicate isAlive() { - return new Predicate() { + public static Predicate isAlive() { + return new Predicate() { @Override - public boolean apply(AbstractProject job) { + public boolean apply(Job job) { return !from(GHEventsSubscriber.all()).filter(isApplicableFor(job)).toList().isEmpty(); } }; 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 1a06ceba5..2c52a215f 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -3,7 +3,9 @@ import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.base.Function; import com.google.common.base.Predicate; -import hudson.model.AbstractProject; + +import hudson.model.Job; + import org.apache.commons.lang.Validate; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; @@ -76,7 +78,7 @@ public static WebhookManager forHookUrl(URL endpoint) { * @return runnable to create hooks on run * @see #createHookSubscribedTo(List) */ - public Runnable registerFor(final AbstractProject project) { + public Runnable registerFor(final Job project) { final Collection names = parseAssociatedNames(project); final List events = from(GHEventsSubscriber.all()) @@ -130,7 +132,7 @@ public void unregisterFor(GitHubRepositoryName name, List } /** - * Main logic of {@link #registerFor(AbstractProject)}. + * Main logic of {@link #registerFor(Job)}. * Updates hooks with replacing old ones with merged new ones * * @param events calculated events list to be registered in hook 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 2d41c5657..f2bea92d1 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 @@ -6,9 +6,11 @@ import com.cloudbees.jenkins.GitHubTrigger; import com.cloudbees.jenkins.GitHubWebHook; import hudson.Extension; -import hudson.model.AbstractProject; +import hudson.model.Job; import hudson.security.ACL; +import hudson.triggers.Trigger; import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn; import net.sf.json.JSONObject; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; @@ -43,7 +45,7 @@ public class DefaultPushGHEventSubscriber extends GHEventsSubscriber { * @return true if project has {@link GitHubPushTrigger} */ @Override - protected boolean isApplicable(AbstractProject project) { + protected boolean isApplicable(Job project) { return withTrigger(GitHubPushTrigger.class).apply(project); } @@ -83,8 +85,18 @@ protected void onEvent(GHEvent event, String payload) { ACL.impersonate(ACL.SYSTEM, new Runnable() { @Override public void run() { - for (AbstractProject job : Jenkins.getInstance().getAllItems(AbstractProject.class)) { - GitHubTrigger trigger = job.getTrigger(GitHubPushTrigger.class); + for (Job job : Jenkins.getInstance().getAllItems(Job.class)) { + GitHubTrigger trigger = null; + if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { + ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job; + // TODO use standard method in 1.621+ + for (Trigger candidate : pJob.getTriggers().values()) { + if (candidate instanceof GitHubTrigger) { + trigger = (GitHubTrigger) candidate; + break; + } + } + } if (trigger != null) { LOGGER.debug("Considering to poke {}", job.getFullDisplayName()); if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) { 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 f8d3b27d2..18de50e3c 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 @@ -1,7 +1,7 @@ package org.jenkinsci.plugins.github.webhook.subscriber; import hudson.Extension; -import hudson.model.AbstractProject; +import hudson.model.Job; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; import org.slf4j.Logger; @@ -32,7 +32,7 @@ public class PingGHEventSubscriber extends GHEventsSubscriber { * @return always false */ @Override - protected boolean isApplicable(AbstractProject project) { + protected boolean isApplicable(Job project) { return false; } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java index fb28a64cd..2f88604e3 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java @@ -1,7 +1,10 @@ package com.cloudbees.jenkins; import com.google.inject.Inject; + import hudson.model.AbstractProject; +import hudson.model.Job; + import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.junit.Before; import org.junit.Rule; @@ -108,7 +111,7 @@ public TestSubscriber(GHEvent interested) { } @Override - protected boolean isApplicable(AbstractProject project) { + protected boolean isApplicable(Job project) { return true; } diff --git a/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java b/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java index 704d41702..2ab02c55f 100644 --- a/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.github.extension; -import hudson.model.AbstractProject; +import hudson.model.Job; + import org.junit.Test; import org.kohsuke.github.GHEvent; @@ -29,7 +30,7 @@ public void shouldMatchAgainstEmptySetInsteadOfNull() throws Exception { public static class NullSubscriber extends GHEventsSubscriber { @Override - protected boolean isApplicable(AbstractProject project) { + protected boolean isApplicable(Job project) { return true; } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java new file mode 100644 index 000000000..0fc29c828 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java @@ -0,0 +1,46 @@ +package org.jenkinsci.plugins.github.webhook.subscriber; + +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; + +import java.util.HashMap; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.kohsuke.github.GHEvent; + +import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubWebHookFullTest; + +public class WebhookWorkflow { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void receivePushHookOnWorkflow() throws Exception { + WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "Test Workflow"); + + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.start(job, false); + job.addTrigger(trigger); + job.setDefinition(new CpsFlowDefinition("node {" + + "git 'https://github.com/amuniz/github-plugin.git'" + + "}")); + + // Trigger the build once to register SCMs + WorkflowRun lastRun = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); + // Testing hack! This will make the polling believe that there was remote changes to build + lastRun.getActions(BuildData.class).get(0).buildsByBranchName = new HashMap(); + + // Then simulate a GitHub push + new DefaultPushGHEventSubscriber() + .onEvent(GHEvent.PUSH, GitHubWebHookFullTest.classpath("payloads/push-wf.json")); + j.waitUntilNoActivity(); + j.assertBuildStatusSuccess(job.getBuildByNumber(2)); + } +} diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json new file mode 100644 index 000000000..a3918e37d --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json @@ -0,0 +1,160 @@ + +{ + "ref": "refs/heads/gh-trigger-JENKINS-27136", + "before": "9897530d541d838c19fa479a09e2549679a7577e", + "after": "a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", + "created": false, + "deleted": false, + "forced": false, + "base_ref": null, + "compare": "https://github.com/amuniz/github-plugin/compare/9897530d541d...a9fe2e7d69ac", + "commits": [ + { + "id": "a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", + "distinct": true, + "message": "Fixing formatting issues and findbugs", + "timestamp": "2015-07-06T12:19:25+02:00", + "url": "https://github.com/amuniz/github-plugin/commit/a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", + "author": { + "name": "Antonio Muñiz", + "email": "amuniz@cloudbees.com", + "username": "amuniz" + }, + "committer": { + "name": "Antonio Muñiz", + "email": "amuniz@cloudbees.com", + "username": "amuniz" + }, + "added": [], + "removed": [], + "modified": [ + "src/main/java/com/cloudbees/jenkins/Cleaner.java", + "src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java", + "src/main/java/com/cloudbees/jenkins/GitHubTrigger.java", + "src/main/java/com/cloudbees/jenkins/GitHubWebHook.java" + ] + } + ], + "head_commit": { + "id": "a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", + "distinct": true, + "message": "Fixing formatting issues and findbugs", + "timestamp": "2015-07-06T12:19:25+02:00", + "url": "https://github.com/amuniz/github-plugin/commit/a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", + "author": { + "name": "Antonio Muñiz", + "email": "amuniz@cloudbees.com", + "username": "amuniz" + }, + "committer": { + "name": "Antonio Muñiz", + "email": "amuniz@cloudbees.com", + "username": "amuniz" + }, + "added": [], + "removed": [], + "modified": [ + "src/main/java/com/cloudbees/jenkins/Cleaner.java", + "src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java", + "src/main/java/com/cloudbees/jenkins/GitHubTrigger.java", + "src/main/java/com/cloudbees/jenkins/GitHubWebHook.java" + ] + }, + "repository": { + "id": 38361321, + "name": "github-plugin", + "full_name": "amuniz/github-plugin", + "owner": { + "name": "amuniz", + "email": "amuniz@users.noreply.github.com" + }, + "private": false, + "html_url": "https://github.com/amuniz/github-plugin", + "description": "Jenkins github plugin", + "fork": true, + "url": "https://github.com/amuniz/github-plugin", + "forks_url": "https://api.github.com/repos/amuniz/github-plugin/forks", + "keys_url": "https://api.github.com/repos/amuniz/github-plugin/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/amuniz/github-plugin/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/amuniz/github-plugin/teams", + "hooks_url": "https://api.github.com/repos/amuniz/github-plugin/hooks", + "issue_events_url": "https://api.github.com/repos/amuniz/github-plugin/issues/events{/number}", + "events_url": "https://api.github.com/repos/amuniz/github-plugin/events", + "assignees_url": "https://api.github.com/repos/amuniz/github-plugin/assignees{/user}", + "branches_url": "https://api.github.com/repos/amuniz/github-plugin/branches{/branch}", + "tags_url": "https://api.github.com/repos/amuniz/github-plugin/tags", + "blobs_url": "https://api.github.com/repos/amuniz/github-plugin/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/amuniz/github-plugin/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/amuniz/github-plugin/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/amuniz/github-plugin/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/amuniz/github-plugin/statuses/{sha}", + "languages_url": "https://api.github.com/repos/amuniz/github-plugin/languages", + "stargazers_url": "https://api.github.com/repos/amuniz/github-plugin/stargazers", + "contributors_url": "https://api.github.com/repos/amuniz/github-plugin/contributors", + "subscribers_url": "https://api.github.com/repos/amuniz/github-plugin/subscribers", + "subscription_url": "https://api.github.com/repos/amuniz/github-plugin/subscription", + "commits_url": "https://api.github.com/repos/amuniz/github-plugin/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/amuniz/github-plugin/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/amuniz/github-plugin/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/amuniz/github-plugin/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/amuniz/github-plugin/contents/{+path}", + "compare_url": "https://api.github.com/repos/amuniz/github-plugin/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/amuniz/github-plugin/merges", + "archive_url": "https://api.github.com/repos/amuniz/github-plugin/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/amuniz/github-plugin/downloads", + "issues_url": "https://api.github.com/repos/amuniz/github-plugin/issues{/number}", + "pulls_url": "https://api.github.com/repos/amuniz/github-plugin/pulls{/number}", + "milestones_url": "https://api.github.com/repos/amuniz/github-plugin/milestones{/number}", + "notifications_url": "https://api.github.com/repos/amuniz/github-plugin/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/amuniz/github-plugin/labels{/name}", + "releases_url": "https://api.github.com/repos/amuniz/github-plugin/releases{/id}", + "created_at": 1435741847, + "updated_at": "2015-07-01T09:10:47Z", + "pushed_at": 1436177972, + "git_url": "git://github.com/amuniz/github-plugin.git", + "ssh_url": "git@github.com:amuniz/github-plugin.git", + "clone_url": "https://github.com/amuniz/github-plugin.git", + "svn_url": "https://github.com/amuniz/github-plugin", + "homepage": "http://jenkins-ci.org/", + "size": 981, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Java", + "has_issues": false, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "stargazers": 0, + "master_branch": "master" + }, + "pusher": { + "name": "amuniz", + "email": "amuniz@users.noreply.github.com" + }, + "sender": { + "login": "amuniz", + "id": 1017585, + "avatar_url": "https://avatars.githubusercontent.com/u/1017585?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/amuniz", + "html_url": "https://github.com/amuniz", + "followers_url": "https://api.github.com/users/amuniz/followers", + "following_url": "https://api.github.com/users/amuniz/following{/other_user}", + "gists_url": "https://api.github.com/users/amuniz/gists{/gist_id}", + "starred_url": "https://api.github.com/users/amuniz/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/amuniz/subscriptions", + "organizations_url": "https://api.github.com/users/amuniz/orgs", + "repos_url": "https://api.github.com/users/amuniz/repos", + "events_url": "https://api.github.com/users/amuniz/events{/privacy}", + "received_events_url": "https://api.github.com/users/amuniz/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file From 446925c6215e1df81ef78cfaefba4dd25562ed71 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Thu, 3 Sep 2015 18:33:08 +0300 Subject: [PATCH 096/228] [JENKINS-27136] Add more tests for wf-stuff --- .../cloudbees/jenkins/GitHubPushTrigger.java | 11 +- .../GitHubRepositoryNameContributor.java | 26 ++- .../plugins/github/util/JobInfoHelpers.java | 56 ++++-- .../DefaultPushGHEventSubscriber.java | 15 +- .../jenkins/GitHubPushTriggerTest.java | 59 +++++++ .../jenkins/GitHubWebHookFullTest.java | 8 +- .../github/util/JobInfoHelpersTest.java | 29 ++++ .../DefaultPushGHEventListenerTest.java | 46 ++++- .../webhook/subscriber/WebhookWorkflow.java | 46 ----- .../payloads/push-wf.json | 160 ------------------ .../workflow-definition.groovy | 3 + 11 files changed, 207 insertions(+), 252 deletions(-) create mode 100644 src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java delete mode 100644 src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java delete mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json create mode 100644 src/test/resources/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest/workflow-definition.groovy diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 04a8befee..f5f55caaf 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -27,7 +27,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.io.File; import java.io.IOException; import java.io.PrintStream; @@ -41,6 +40,7 @@ import java.util.Set; import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.asParameterizedJobMixIn; /** * Triggers a build when we receive a GitHub post-commit webhook. @@ -110,14 +110,7 @@ public void run() { LOGGER.warn("Failed to parse the polling log", e); cause = new GitHubPushCause(pushBy); } - // TODO use standard method in 1.621+ - ParameterizedJobMixIn scheduledJob = new ParameterizedJobMixIn() { - @Override - protected Job asJob() { - return job; - } - }; - if (scheduledJob.scheduleBuild(cause)) { + if (asParameterizedJobMixIn(job).scheduleBuild(cause)) { LOGGER.info("SCM changes detected in " + job.getName() + ". Triggering " + name); } else { LOGGER.info("SCM changes detected in " + job.getName() + ". Job is already in the queue"); diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java index b36101fe9..948072527 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryNameContributor.java @@ -48,20 +48,37 @@ public void parseAssociatedNames(AbstractProject job, Collection job, Collection result) { - if (Util.isOverridden(GitHubRepositoryNameContributor.class, getClass(), - "parseAssociatedNames", AbstractProject.class, Collection.class) && job instanceof AbstractProject) { + if (overriddenMethodHasDeprecatedSignature(job)) { parseAssociatedNames((AbstractProject) job, result); } else { throw new AbstractMethodError("you must override the new overload of parseAssociatedNames"); } } + /** + * 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 + */ + private boolean overriddenMethodHasDeprecatedSignature(Job job) { + return Util.isOverridden( + GitHubRepositoryNameContributor.class, + getClass(), + "parseAssociatedNames", + AbstractProject.class, + Collection.class + ) && job instanceof AbstractProject; + } + public static ExtensionList all() { return Jenkins.getInstance().getExtensionList(GitHubRepositoryNameContributor.class); } /** - * @deprecated Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(Job)} + * @deprecated Use {@link #parseAssociatedNames(Job)} */ @Deprecated public static Collection parseAssociatedNames(AbstractProject job) { @@ -98,8 +115,7 @@ protected EnvVars buildEnv(Job job) { try { contributor.buildEnvironmentFor(job, env, TaskListener.NULL); } catch (Exception e) { - LOGGER.debug(e.getMessage(), e); - // ignore + LOGGER.debug("{} failed to build env ({}), skipping", contributor.getClass(), e.getMessage(), e); } } return env; 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 5439485c0..1ca60cd97 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -4,11 +4,13 @@ import com.cloudbees.jenkins.GitHubRepositoryNameContributor; import com.google.common.base.Function; import com.google.common.base.Predicate; +import hudson.model.AbstractProject; import hudson.model.Job; import hudson.triggers.Trigger; import jenkins.model.ParameterizedJobMixIn; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import javax.annotation.CheckForNull; import java.util.Collection; import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor; @@ -34,18 +36,7 @@ private JobInfoHelpers() { public static Predicate withTrigger(final Class clazz) { return new Predicate() { public boolean apply(Job job) { - if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { - ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job; - // TODO use standard method in 1.621+ - for (Trigger trigger : pJob.getTriggers().values()) { - if (clazz.isInstance(trigger)) { - return true; - } - } - return false; - } else { - return false; - } + return triggerFrom(job, clazz) != null; } }; } @@ -88,5 +79,46 @@ public boolean apply(Job job) { } }; } + + /** + * @param job 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 + * TODO use standard method in 1.621+ + */ + @CheckForNull + public static T triggerFrom(Job job, Class tClass) { + if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { + ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job; + + for (Trigger candidate : pJob.getTriggers().values()) { + if (tClass.isInstance(candidate)) { + return tClass.cast(candidate); + } + } + } + return null; + } + + /** + * Converts any child class of {@link Job} (such as {@link AbstractProject} + * to {@link ParameterizedJobMixIn} to use it for workflow + * + * @param job to wrap + * @param any child type of Job + * + * @return ParameterizedJobMixIn + * TODO use standard method in 1.621+ + */ + public static ParameterizedJobMixIn asParameterizedJobMixIn(final T job) { + return new ParameterizedJobMixIn() { + @Override + protected Job asJob() { + return job; + } + }; + } } 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 f2bea92d1..f52a5017d 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 @@ -8,9 +8,7 @@ import hudson.Extension; import hudson.model.Job; import hudson.security.ACL; -import hudson.triggers.Trigger; import jenkins.model.Jenkins; -import jenkins.model.ParameterizedJobMixIn; import net.sf.json.JSONObject; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; @@ -22,6 +20,7 @@ import java.util.regex.Pattern; import static com.google.common.collect.Sets.immutableEnumSet; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.triggerFrom; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; import static org.kohsuke.github.GHEvent.PUSH; @@ -86,17 +85,7 @@ protected void onEvent(GHEvent event, String payload) { @Override public void run() { for (Job job : Jenkins.getInstance().getAllItems(Job.class)) { - GitHubTrigger trigger = null; - if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { - ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job; - // TODO use standard method in 1.621+ - for (Trigger candidate : pJob.getTriggers().values()) { - if (candidate instanceof GitHubTrigger) { - trigger = (GitHubTrigger) candidate; - break; - } - } - } + GitHubTrigger trigger = triggerFrom(job, GitHubPushTrigger.class); if (trigger != null) { LOGGER.debug("Considering to poke {}", job.getFullDisplayName()); if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) { diff --git a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java new file mode 100644 index 000000000..8fba96f00 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java @@ -0,0 +1,59 @@ +package com.cloudbees.jenkins; + +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildData; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.webhook.subscriber.DefaultPushGHEventListenerTest; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.webhook.subscriber.DefaultPushGHEventListenerTest.TRIGGERED_BY_USER_FROM_RESOURCE; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubPushTriggerTest { + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + /** + * This test requires internet access to get real git revision + */ + @Test + @Issue("JENKINS-27136") + public void shouldStartWorkflowByTrigger() throws Exception { + WorkflowJob job = jRule.getInstance().createProject(WorkflowJob.class, "test-workflow-job"); + GitHubPushTrigger trigger = new GitHubPushTrigger(); + trigger.start(job, false); + job.addTrigger(trigger); + job.setDefinition( + new CpsFlowDefinition(classpath(DefaultPushGHEventListenerTest.class, "workflow-definition.groovy")) + ); + + // Trigger the build once to register SCMs + WorkflowRun lastRun = jRule.assertBuildStatusSuccess(job.scheduleBuild2(0)); + // Testing hack! This will make the polling believe that there was remote changes to build + BuildData buildData = lastRun.getActions(BuildData.class).get(0); + buildData.buildsByBranchName = new HashMap(); + buildData.getLastBuiltRevision().setSha1(ObjectId.zeroId()); + + trigger.onPost(TRIGGERED_BY_USER_FROM_RESOURCE); + + TimeUnit.SECONDS.sleep(job.getQuietPeriod()); + jRule.waitUntilNoActivity(); + + assertThat("should be 2 build after hook", job.getLastBuild().getNumber(), is(2)); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index 149768766..1302e8f53 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -140,8 +140,12 @@ public Header eventHeader(String event) { } public static String classpath(String path) throws IOException { - return IOUtils.toString(GitHubWebHookFullTest.class.getClassLoader().getResourceAsStream( - GitHubWebHookFullTest.class.getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path + return classpath(GitHubWebHookFullTest.class, path); + } + + public static String classpath(Class clazz, String path) throws IOException { + return IOUtils.toString(clazz.getClassLoader().getResourceAsStream( + clazz.getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path ), Charsets.UTF_8); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java index 0be499962..6571a5911 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java @@ -2,13 +2,16 @@ import com.cloudbees.jenkins.GitHubPushTrigger; import hudson.model.FreeStyleProject; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable; +import static org.jenkinsci.plugins.github.util.JobInfoHelpers.triggerFrom; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; import static org.junit.Assert.assertThat; @@ -59,4 +62,30 @@ public void shouldSeeProjectWithoutTriggerIsNotAliveForCleaner() throws Exceptio assertThat("without trigger", isAlive().apply(prj), is(false)); } + + @Test + public void shouldGetTriggerFromAbstractProject() throws Exception { + GitHubPushTrigger trigger = new GitHubPushTrigger(); + + FreeStyleProject prj = jenkins.createFreeStyleProject(); + prj.addTrigger(trigger); + + assertThat("with trigger in free style job", triggerFrom(prj, GitHubPushTrigger.class), is(trigger)); + } + + @Test + public void shouldGetTriggerFromWorkflow() throws Exception { + GitHubPushTrigger trigger = new GitHubPushTrigger(); + WorkflowJob job = jenkins.getInstance().createProject(WorkflowJob.class, "Test Workflow"); + job.addTrigger(trigger); + + assertThat("with trigger in workflow", triggerFrom(job, GitHubPushTrigger.class), is(trigger)); + } + + @Test + public void shouldNotGetTriggerWhenNoOne() throws Exception { + FreeStyleProject prj = jenkins.createFreeStyleProject(); + + assertThat("without trigger in project", triggerFrom(prj, GitHubPushTrigger.class), nullValue()); + } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java index 87ec8c2e1..9826d8c47 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java @@ -1,17 +1,21 @@ package org.jenkinsci.plugins.github.webhook.subscriber; import com.cloudbees.jenkins.GitHubPushTrigger; -import com.cloudbees.jenkins.GitHubWebHookFullTest; import hudson.model.FreeStyleProject; import hudson.plugins.git.GitSCM; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.kohsuke.github.GHEvent; +import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; /** @@ -41,14 +45,46 @@ public void shouldBeApplicableForProjectWithTrigger() throws Exception { @Test public void shouldParsePushPayload() throws Exception { GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); - + FreeStyleProject prj = jenkins.createFreeStyleProject(); prj.addTrigger(trigger); prj.setScm(GIT_SCM_FROM_RESOURCE); - + + new DefaultPushGHEventSubscriber() + .onEvent(GHEvent.PUSH, classpath("payloads/push.json")); + + verify(trigger).onPost(TRIGGERED_BY_USER_FROM_RESOURCE); + } + + @Test + @Issue("JENKINS-27136") + public void shouldReceivePushHookOnWorkflow() throws Exception { + WorkflowJob job = jenkins.getInstance().createProject(WorkflowJob.class, "test-workflow-job"); + + GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); + job.addTrigger(trigger); + job.setDefinition(new CpsFlowDefinition(classpath(getClass(), "workflow-definition.groovy"))); + // Trigger the build once to register SCMs + jenkins.assertBuildStatusSuccess(job.scheduleBuild2(0)); + new DefaultPushGHEventSubscriber() - .onEvent(GHEvent.PUSH, GitHubWebHookFullTest.classpath("payloads/push.json")); - + .onEvent(GHEvent.PUSH, classpath("payloads/push.json")); + verify(trigger).onPost(TRIGGERED_BY_USER_FROM_RESOURCE); } + + @Test + @Issue("JENKINS-27136") + public void shouldNotReceivePushHookOnWorkflowWithNoBuilds() throws Exception { + WorkflowJob job = jenkins.getInstance().createProject(WorkflowJob.class, "test-workflow-job"); + + GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); + job.addTrigger(trigger); + job.setDefinition(new CpsFlowDefinition(classpath(getClass(), "workflow-definition.groovy"))); + + new DefaultPushGHEventSubscriber() + .onEvent(GHEvent.PUSH, classpath("payloads/push.json")); + + verify(trigger, never()).onPost(TRIGGERED_BY_USER_FROM_RESOURCE); + } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java deleted file mode 100644 index 0fc29c828..000000000 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/WebhookWorkflow.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.jenkinsci.plugins.github.webhook.subscriber; - -import hudson.plugins.git.util.Build; -import hudson.plugins.git.util.BuildData; - -import java.util.HashMap; - -import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; -import org.kohsuke.github.GHEvent; - -import com.cloudbees.jenkins.GitHubPushTrigger; -import com.cloudbees.jenkins.GitHubWebHookFullTest; - -public class WebhookWorkflow { - - @Rule - public JenkinsRule j = new JenkinsRule(); - - @Test - public void receivePushHookOnWorkflow() throws Exception { - WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "Test Workflow"); - - GitHubPushTrigger trigger = new GitHubPushTrigger(); - trigger.start(job, false); - job.addTrigger(trigger); - job.setDefinition(new CpsFlowDefinition("node {" + - "git 'https://github.com/amuniz/github-plugin.git'" + - "}")); - - // Trigger the build once to register SCMs - WorkflowRun lastRun = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); - // Testing hack! This will make the polling believe that there was remote changes to build - lastRun.getActions(BuildData.class).get(0).buildsByBranchName = new HashMap(); - - // Then simulate a GitHub push - new DefaultPushGHEventSubscriber() - .onEvent(GHEvent.PUSH, GitHubWebHookFullTest.classpath("payloads/push-wf.json")); - j.waitUntilNoActivity(); - j.assertBuildStatusSuccess(job.getBuildByNumber(2)); - } -} diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json deleted file mode 100644 index a3918e37d..000000000 --- a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push-wf.json +++ /dev/null @@ -1,160 +0,0 @@ - -{ - "ref": "refs/heads/gh-trigger-JENKINS-27136", - "before": "9897530d541d838c19fa479a09e2549679a7577e", - "after": "a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", - "created": false, - "deleted": false, - "forced": false, - "base_ref": null, - "compare": "https://github.com/amuniz/github-plugin/compare/9897530d541d...a9fe2e7d69ac", - "commits": [ - { - "id": "a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", - "distinct": true, - "message": "Fixing formatting issues and findbugs", - "timestamp": "2015-07-06T12:19:25+02:00", - "url": "https://github.com/amuniz/github-plugin/commit/a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", - "author": { - "name": "Antonio Muñiz", - "email": "amuniz@cloudbees.com", - "username": "amuniz" - }, - "committer": { - "name": "Antonio Muñiz", - "email": "amuniz@cloudbees.com", - "username": "amuniz" - }, - "added": [], - "removed": [], - "modified": [ - "src/main/java/com/cloudbees/jenkins/Cleaner.java", - "src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java", - "src/main/java/com/cloudbees/jenkins/GitHubTrigger.java", - "src/main/java/com/cloudbees/jenkins/GitHubWebHook.java" - ] - } - ], - "head_commit": { - "id": "a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", - "distinct": true, - "message": "Fixing formatting issues and findbugs", - "timestamp": "2015-07-06T12:19:25+02:00", - "url": "https://github.com/amuniz/github-plugin/commit/a9fe2e7d69ace1450fef68fed26e7e0182cd42d9", - "author": { - "name": "Antonio Muñiz", - "email": "amuniz@cloudbees.com", - "username": "amuniz" - }, - "committer": { - "name": "Antonio Muñiz", - "email": "amuniz@cloudbees.com", - "username": "amuniz" - }, - "added": [], - "removed": [], - "modified": [ - "src/main/java/com/cloudbees/jenkins/Cleaner.java", - "src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java", - "src/main/java/com/cloudbees/jenkins/GitHubTrigger.java", - "src/main/java/com/cloudbees/jenkins/GitHubWebHook.java" - ] - }, - "repository": { - "id": 38361321, - "name": "github-plugin", - "full_name": "amuniz/github-plugin", - "owner": { - "name": "amuniz", - "email": "amuniz@users.noreply.github.com" - }, - "private": false, - "html_url": "https://github.com/amuniz/github-plugin", - "description": "Jenkins github plugin", - "fork": true, - "url": "https://github.com/amuniz/github-plugin", - "forks_url": "https://api.github.com/repos/amuniz/github-plugin/forks", - "keys_url": "https://api.github.com/repos/amuniz/github-plugin/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/amuniz/github-plugin/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/amuniz/github-plugin/teams", - "hooks_url": "https://api.github.com/repos/amuniz/github-plugin/hooks", - "issue_events_url": "https://api.github.com/repos/amuniz/github-plugin/issues/events{/number}", - "events_url": "https://api.github.com/repos/amuniz/github-plugin/events", - "assignees_url": "https://api.github.com/repos/amuniz/github-plugin/assignees{/user}", - "branches_url": "https://api.github.com/repos/amuniz/github-plugin/branches{/branch}", - "tags_url": "https://api.github.com/repos/amuniz/github-plugin/tags", - "blobs_url": "https://api.github.com/repos/amuniz/github-plugin/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/amuniz/github-plugin/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/amuniz/github-plugin/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/amuniz/github-plugin/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/amuniz/github-plugin/statuses/{sha}", - "languages_url": "https://api.github.com/repos/amuniz/github-plugin/languages", - "stargazers_url": "https://api.github.com/repos/amuniz/github-plugin/stargazers", - "contributors_url": "https://api.github.com/repos/amuniz/github-plugin/contributors", - "subscribers_url": "https://api.github.com/repos/amuniz/github-plugin/subscribers", - "subscription_url": "https://api.github.com/repos/amuniz/github-plugin/subscription", - "commits_url": "https://api.github.com/repos/amuniz/github-plugin/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/amuniz/github-plugin/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/amuniz/github-plugin/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/amuniz/github-plugin/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/amuniz/github-plugin/contents/{+path}", - "compare_url": "https://api.github.com/repos/amuniz/github-plugin/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/amuniz/github-plugin/merges", - "archive_url": "https://api.github.com/repos/amuniz/github-plugin/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/amuniz/github-plugin/downloads", - "issues_url": "https://api.github.com/repos/amuniz/github-plugin/issues{/number}", - "pulls_url": "https://api.github.com/repos/amuniz/github-plugin/pulls{/number}", - "milestones_url": "https://api.github.com/repos/amuniz/github-plugin/milestones{/number}", - "notifications_url": "https://api.github.com/repos/amuniz/github-plugin/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/amuniz/github-plugin/labels{/name}", - "releases_url": "https://api.github.com/repos/amuniz/github-plugin/releases{/id}", - "created_at": 1435741847, - "updated_at": "2015-07-01T09:10:47Z", - "pushed_at": 1436177972, - "git_url": "git://github.com/amuniz/github-plugin.git", - "ssh_url": "git@github.com:amuniz/github-plugin.git", - "clone_url": "https://github.com/amuniz/github-plugin.git", - "svn_url": "https://github.com/amuniz/github-plugin", - "homepage": "http://jenkins-ci.org/", - "size": 981, - "stargazers_count": 0, - "watchers_count": 0, - "language": "Java", - "has_issues": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "forks_count": 0, - "mirror_url": null, - "open_issues_count": 0, - "forks": 0, - "open_issues": 0, - "watchers": 0, - "default_branch": "master", - "stargazers": 0, - "master_branch": "master" - }, - "pusher": { - "name": "amuniz", - "email": "amuniz@users.noreply.github.com" - }, - "sender": { - "login": "amuniz", - "id": 1017585, - "avatar_url": "https://avatars.githubusercontent.com/u/1017585?v=3", - "gravatar_id": "", - "url": "https://api.github.com/users/amuniz", - "html_url": "https://github.com/amuniz", - "followers_url": "https://api.github.com/users/amuniz/followers", - "following_url": "https://api.github.com/users/amuniz/following{/other_user}", - "gists_url": "https://api.github.com/users/amuniz/gists{/gist_id}", - "starred_url": "https://api.github.com/users/amuniz/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/amuniz/subscriptions", - "organizations_url": "https://api.github.com/users/amuniz/orgs", - "repos_url": "https://api.github.com/users/amuniz/repos", - "events_url": "https://api.github.com/users/amuniz/events{/privacy}", - "received_events_url": "https://api.github.com/users/amuniz/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file diff --git a/src/test/resources/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest/workflow-definition.groovy b/src/test/resources/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest/workflow-definition.groovy new file mode 100644 index 000000000..15818d401 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest/workflow-definition.groovy @@ -0,0 +1,3 @@ +node { + git 'https://github.com/lanwen/test.git' +} \ No newline at end of file From 8e58ca6c87d03060931a2005dbb2220af5183cd5 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 21 Sep 2015 22:00:32 +0300 Subject: [PATCH 097/228] [maven-release-plugin] prepare release github-1.14.0-alpha-1 --- pom.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index b0707a383..b2d34baf6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -11,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.0-SNAPSHOT + 1.14.0-alpha-1 hpi GitHub plugin @@ -39,7 +38,7 @@ 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 + github-1.14.0-alpha-1 From ab695241b57435bca98f4470ebf12aefb0730fdf Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 21 Sep 2015 22:00:37 +0300 Subject: [PATCH 098/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b2d34baf6..4746066ff 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.0-alpha-1 + 1.14.0-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.14.0-alpha-1 + HEAD From 5de4c7bf84d3a1e3755dafcf835a286d55be6480 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 22 Sep 2015 15:50:37 -0400 Subject: [PATCH 099/228] Fixed Javadoc link. --- .../plugins/github/extension/GHEventsSubscriber.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 00126537b..bdef0e98c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -4,7 +4,6 @@ import com.google.common.base.Predicate; import hudson.ExtensionList; import hudson.ExtensionPoint; -import hudson.model.AbstractProject; import hudson.model.Job; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; @@ -23,7 +22,7 @@ /** * Extension point to subscribe events from GH, which plugin interested in. - * This point should return true in {@link #isApplicable(AbstractProject)} + * This point should return true in {@link #isApplicable} * only if it can parse hooks with events contributed in {@link #events()} * * Each time this plugin wants to get events list from subscribers it asks for applicable status @@ -36,7 +35,7 @@ public abstract class GHEventsSubscriber implements ExtensionPoint { /** * Should return true only if this subscriber interested in {@link #events()} set for this project - * Don't call it directly, use {@link #isApplicableFor(AbstractProject)} static function + * Don't call it directly, use {@link #isApplicableFor} static function * * @param project to check * @@ -92,6 +91,7 @@ protected Set applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { * @param project to check every GHEventsSubscriber for being applicable * * @return predicate to use in iterable filtering + * @see #isApplicable */ public static Predicate isApplicableFor(final Job project) { return new NullSafePredicate() { From e573a28c3a3a75890c19512dc899c8da511d2c80 Mon Sep 17 00:00:00 2001 From: James Nord Date: Tue, 22 Sep 2015 21:31:39 +0100 Subject: [PATCH 100/228] don't be platform specific in checkstyle. While notable the existing checkstyle means the build is DOA on windows that uses CRLF as a system linefeed. Whilst this change enforces unix termination, it will break windows clients configured with `core.autocrlf=true" but changing files can lead to platform specific test failures (especially with resource files under test) so EOL conversion is in this persons eyes evil and to be avoided at all costs. It would probably be worthwile to setup .gitattributes in the root of the repository to stop any eol conversion - but I shall leave that up to the owners for an excersize at their leisure. --- src/test/resources/checkstyle/checkstyle-config.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/resources/checkstyle/checkstyle-config.xml b/src/test/resources/checkstyle/checkstyle-config.xml index e56f0d1d3..ba6926328 100644 --- a/src/test/resources/checkstyle/checkstyle-config.xml +++ b/src/test/resources/checkstyle/checkstyle-config.xml @@ -42,7 +42,9 @@ - + + + From 942f63a49ae4bb0561dcf1c8f6c033c011fde9a8 Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Wed, 23 Sep 2015 11:50:06 +0300 Subject: [PATCH 101/228] Create .gitattributes file to handle line endings --- .gitattributes | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d8700498f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,38 @@ +# From https://github.com/Danimoth/gitattributes/blob/master/Java.gitattributes +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# +# The above will handle all files NOT found below +# +# These files are text and should be normalized (Convert crlf => lf) +*.css text +*.df text +*.htm text +*.html text +*.java text +*.js text +*.json text +*.jsp text +*.jspf text +*.properties text +*.sh text +*.svg text +*.tld text +*.txt text +*.xml text + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.class binary +*.dll binary +*.ear binary +*.gif binary +*.ico binary +*.jar binary +*.jpg binary +*.jpeg binary +*.png binary +*.so binary +*.war binary From 03b1c879698eb76c8348b7e8b3514f1c9fb7ad41 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 23 Sep 2015 18:33:52 -0400 Subject: [PATCH 102/228] [FIXED JENKINS-30626] Handle pings from organization webhooks Closes #89 --- .../subscriber/PingGHEventSubscriber.java | 17 ++++-- .../subscriber/PingGHEventSubscriberTest.java | 9 ++++ .../payloads/orgping.json | 52 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/orgping.json 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 18de50e3c..160d7fe44 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,6 +2,7 @@ import hudson.Extension; import hudson.model.Job; +import net.sf.json.JSONObject; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; import org.slf4j.Logger; @@ -52,8 +53,18 @@ protected Set events() { */ @Override protected void onEvent(GHEvent event, String payload) { - // something like - String repo = fromObject(payload).getJSONObject("repository").getString("url"); - LOGGER.info("{} webhook received from repo <{}>!", event, repo); + JSONObject parsedPayload = fromObject(payload); + JSONObject repository = parsedPayload.optJSONObject("repository"); + if (repository != null) { + // something like + LOGGER.info("{} webhook received from repo <{}>!", event, repository.getString("url")); + } else { + JSONObject organization = parsedPayload.optJSONObject("organization"); + if (organization != null) { + LOGGER.info("{} webhook received from org <{}>!", event, organization.getString("url")); + } else { + LOGGER.warn("{} webhook received with unexpected payload", event); + } + } } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java index dc11769aa..347ce6198 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java @@ -10,6 +10,7 @@ import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import org.jvnet.hudson.test.Issue; /** * @author lanwen (Merkushev Kirill) @@ -30,4 +31,12 @@ public void shouldBeNotApplicableForProjects() throws Exception { public void shouldParsePingPayload() throws Exception { new PingGHEventSubscriber().onEvent(GHEvent.PING, classpath("payloads/ping.json")); } + + @Issue("JENKINS-30626") + @Test + @WithoutJenkins + public void shouldParseOrgPingPayload() throws Exception { + new PingGHEventSubscriber().onEvent(GHEvent.PING, classpath("payloads/orgping.json")); + } + } diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/orgping.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/orgping.json new file mode 100644 index 000000000..289507785 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/orgping.json @@ -0,0 +1,52 @@ +{ + "zen": "Mind your words, they are important.", + "hook_id": 5926787, + "hook": { + "url": "https://api.github.com/orgs/cloudbeers/hooks/5926787", + "ping_url": "https://api.github.com/orgs/cloudbeers/hooks/5926787/pings", + "id": 5926787, + "name": "web", + "active": true, + "events": [ + "*" + ], + "config": { + "url": "https://jenkins.ci.cloudbees.com/github-webhook/", + "content_type": "json", + "insecure_ssl": "0", + "secret": "" + }, + "updated_at": "2015-09-24T10:13:54Z", + "created_at": "2015-09-24T10:13:54Z" + }, + "organization": { + "login": "cloudbeers", + "id": 4181899, + "url": "https://api.github.com/orgs/cloudbeers", + "repos_url": "https://api.github.com/orgs/cloudbeers/repos", + "events_url": "https://api.github.com/orgs/cloudbeers/events", + "members_url": "https://api.github.com/orgs/cloudbeers/members{/member}", + "public_members_url": "https://api.github.com/orgs/cloudbeers/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/4181899?v=3", + "description": null + }, + "sender": { + "login": "jglick", + "id": 154109, + "avatar_url": "https://avatars.githubusercontent.com/u/154109?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/jglick", + "html_url": "https://github.com/jglick", + "followers_url": "https://api.github.com/users/jglick/followers", + "following_url": "https://api.github.com/users/jglick/following{/other_user}", + "gists_url": "https://api.github.com/users/jglick/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jglick/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jglick/subscriptions", + "organizations_url": "https://api.github.com/users/jglick/orgs", + "repos_url": "https://api.github.com/users/jglick/repos", + "events_url": "https://api.github.com/users/jglick/events{/privacy}", + "received_events_url": "https://api.github.com/users/jglick/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file From d88889a03521815cf7327eae0faa5e1d38b44cb9 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Thu, 24 Sep 2015 00:37:07 +0300 Subject: [PATCH 103/228] using okHttp connector with cache and proxy for github this can speed up fetching data from GH and reduce rate limits consuming --- pom.xml | 7 + .../github/config/GitHubPluginConfig.java | 34 ++++- .../github/config/GitHubServerConfig.java | 73 ++++++---- .../github/internal/GitHubLoginFunction.java | 137 ++++++++++++++++++ .../config/GitHubPluginConfig/config.groovy | 4 + .../help-clientCacheSize.html | 7 + 6 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html diff --git a/pom.xml b/pom.xml index 4746066ff..18657df78 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,13 @@ 1.7.7 + + com.squareup.okhttp + okhttp-urlconnection + 2.5.0 + false + + org.jenkins-ci.plugins github-api 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 745356a8d..32be95ec3 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -3,7 +3,6 @@ import com.cloudbees.jenkins.GitHubWebHook; import com.google.common.base.Predicate; import com.google.common.base.Predicates; - import hudson.Extension; import hudson.XmlFile; import hudson.model.Descriptor; @@ -25,7 +24,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; - import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -54,6 +52,13 @@ public class GitHubPluginConfig extends GlobalConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPluginConfig.class); public static final String GITHUB_PLUGIN_CONFIGURATION_ID = "github-plugin-configuration"; + /** + * Default value in MB for client cache size + * + * @see #getClientCacheSize() + */ + private static final int DEFAULT_CLIENT_CACHE_SIZE_MB = 20; + /** * Helps to avoid null in {@link GitHubPlugin#configuration()} */ @@ -62,6 +67,13 @@ public class GitHubPluginConfig extends GlobalConfiguration { private List configs = new ArrayList(); private URL hookUrl; + + /** + * @see #getClientCacheSize() + * @see #setClientCacheSize(int) + */ + private int clientCacheSize = DEFAULT_CLIENT_CACHE_SIZE_MB; + private transient boolean overrideHookUrl; /** @@ -121,6 +133,24 @@ public boolean isOverrideHookURL() { return hookUrl != null; } + /** + * Capacity of cache for GitHub client in MB. + * + * Defaults to 20 MB + * + * @since TODO + */ + public int getClientCacheSize() { + return clientCacheSize; + } + + /** + * @param clientCacheSize capacity of cache for GitHub client in MB, set to <= 0 to turn off this feature + */ + public void setClientCacheSize(int clientCacheSize) { + this.clientCacheSize = clientCacheSize; + } + /** * Filters all stored configs against given predicate then * logs in as the given user and returns the non null connection objects 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 1fa853020..163ae7d3b 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -14,6 +14,7 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import jenkins.model.Jenkins; +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; @@ -73,6 +74,11 @@ public class GitHubServerConfig extends AbstractDescribableImpl loginToGithub() { - return new NullSafeFunction() { - @Override - public GitHub applyNullSafe(@Nonnull GitHubServerConfig github) { - String accessToken = tokenFor(github.getCredentialsId()); - - try { - if (isNotBlank(github.getApiUrl())) { - return GitHub.connectToEnterprise(github.getApiUrl(), accessToken); - } - - return GitHub.connectUsingOAuth(accessToken); - } catch (IOException e) { - LOGGER.warn("Failed to login with creds {}", github.getCredentialsId(), e); - return null; - } - } - }; + return new ClientCacheFunction(); } /** @@ -248,16 +255,14 @@ ACL.SYSTEM, fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build()) @SuppressWarnings("unused") public FormValidation doVerifyCredentials( @QueryParameter String apiUrl, @QueryParameter String credentialsId) throws IOException { - try { - GitHub gitHub; - if (isNotBlank(apiUrl)) { - gitHub = GitHub.connectToEnterprise(apiUrl, tokenFor(credentialsId)); - } else { - gitHub = GitHub.connectUsingOAuth(tokenFor(credentialsId)); - } - if (gitHub.isCredentialValid()) { - return FormValidation.ok("Credentials verifyed, rate limit: %s", gitHub.getRateLimit().remaining); + GitHubServerConfig config = new GitHubServerConfig(credentialsId); + config.setApiUrl(apiUrl); + GitHub gitHub = new GitHubLoginFunction().apply(config); + + try { + if (gitHub != null && gitHub.isCredentialValid()) { + return FormValidation.ok("Credentials verified, rate limit: %s", gitHub.getRateLimit().remaining); } else { return FormValidation.error("Failed to validate the account"); } @@ -271,7 +276,7 @@ public FormValidation doCheckApiUrl(@QueryParameter String value) { try { new URL(value); } catch (MalformedURLException e) { - return FormValidation.error("Mailformed GitHub url (%s)", e.getMessage()); + return FormValidation.error("Malformed GitHub url (%s)", e.getMessage()); } if (GITHUB_URL.equals(value)) { @@ -285,4 +290,18 @@ public FormValidation doCheckApiUrl(@QueryParameter String value) { return FormValidation.warning("GitHub Enterprise API URL ends with \"/api/v3\""); } } + + /** + * Function to get authorized GH client and cache it in config + * has {@link #loginToGithub()} static factory + */ + private static class ClientCacheFunction extends NullSafeFunction { + @Override + 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/internal/GitHubLoginFunction.java b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java new file mode 100644 index 000000000..6941a8c83 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java @@ -0,0 +1,137 @@ +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 jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; +import org.kohsuke.accmod.Restricted; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; + +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.jenkinsci.plugins.github.GitHubPlugin.configuration; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; +import static org.jenkinsci.plugins.github.config.GitHubServerConfig.tokenFor; + +/** + * Converts server config to authorized GH instance on {@link #applyNullSafe(GitHubServerConfig)}. + * If login process is not successful it returns null + * + * Uses okHttp (https://github.com/square/okhttp) as connector to have ability to use cache and proxy + * The capacity of cache can be changed in advanced section of global configuration for plugin + * + * Don't use this class in any place directly + * as of it have public static factory {@link GitHubServerConfig#loginToGithub()} + * + * @author lanwen (Merkushev Kirill) + * @see GitHubServerConfig#loginToGithub() + */ +@Restricted(NoExternalUse.class) +public class GitHubLoginFunction extends NullSafeFunction { + + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubLoginFunction.class); + + /** + * Called by {@link #apply(Object)} + * Logins to GH and returns client instance + * + * @param github config where token saved + * + * @return authorized client or null on login error + */ + @Override + @CheckForNull + protected GitHub applyNullSafe(@Nonnull GitHubServerConfig github) { + String accessToken = tokenFor(github.getCredentialsId()); + + GitHubBuilder builder = new GitHubBuilder() + .withOAuthToken(accessToken) + .withConnector(connector(defaultIfBlank(github.getApiUrl(), GITHUB_URL))) + .withRateLimitHandler(RateLimitHandler.FAIL); + try { + if (isNotBlank(github.getApiUrl())) { + builder.withEndpoint(github.getApiUrl()); + } + LOGGER.debug("Create new GH client with creds id {}", github.getCredentialsId()); + return builder.build(); + } catch (IOException e) { + LOGGER.warn("Failed to login with creds {}", github.getCredentialsId(), e); + return null; + } + } + + /** + * Uses proxy if configured on pluginManager/advanced page + * + * @param apiUrl GitHub's url to build proxy to + * + * @return proxy to use it in connector + */ + private Proxy getProxy(String apiUrl) { + Jenkins jenkins = GitHubWebHook.getJenkinsInstance(); + + if (jenkins.proxy == null) { + return Proxy.NO_PROXY; + } else { + return jenkins.proxy.createProxy(apiUrl); + } + } + + /** + * okHttp connector to be used as backend for GitHub client. + * Uses proxy of jenkins + * If cache size > 0, uses cache + * + * @param apiUrl to build proxy + * + * @return connector to be used as backend for client + */ + private OkHttpConnector connector(String apiUrl) { + Jenkins jenkins = GitHubWebHook.getJenkinsInstance(); + OkHttpClient client = new OkHttpClient().setProxy(getProxy(apiUrl)); + + if (configuration().getClientCacheSize() > 0) { + File cacheDir = new File(jenkins.getRootDir(), GitHubPlugin.class.getName() + ".cache"); + Cache cache = new Cache(cacheDir, configuration().getClientCacheSize() * 1024 * 1024); + 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; + } + + @Override + public HttpURLConnection connect(URL url) throws IOException { + return urlFactory.open(url); + } + } +} 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 4e49e528a..09d69d2a7 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 @@ -38,6 +38,10 @@ f.section(title: descriptor.displayName) { } } + f.entry(title: _("GitHub client cache size (MB)"), field: "clientCacheSize") { + f.textbox() + } + f.entry(title: _("Additional actions"), help: descriptor.getHelpFile('additional')) { f.hetero_list(items: [], addCaption: _("Manage additional GitHub actions"), diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html new file mode 100644 index 000000000..b15f4ee37 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html @@ -0,0 +1,7 @@ +
+ Cache size in MB used by GitHub client. This can speed up fetching data form GH and reduce rate limits consuming.
+ GH + okHttp do all work for results reliability + (Conditional-requests in GitHub documentation)
+ + Set to 0 to disable this feature +
From 77bcc99c1cf29c90362067f82d27f64c8755bac7 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sat, 26 Sep 2015 15:15:16 +0300 Subject: [PATCH 104/228] [maven-release-plugin] prepare release github-1.14.0-alpha-2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 18657df78..d63251284 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.0-SNAPSHOT + 1.14.0-alpha-2 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + github-1.14.0-alpha-2 From ce32bc5fa5b71d3cba513bdda6f34007ae8032b8 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sat, 26 Sep 2015 15:15:22 +0300 Subject: [PATCH 105/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d63251284..18657df78 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.0-alpha-2 + 1.14.0-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.14.0-alpha-2 + HEAD From d4853d954c41056773e5fddbc75a0ea5ef042d1f Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 27 Sep 2015 22:42:43 +0300 Subject: [PATCH 106/228] fix checking creds on github enterprise also additional fixes mentioned in #88 --- .../plugins/github/config/GitHubServerConfig.java | 1 + .../github/internal/GitHubLoginFunction.java | 13 ++++++++++--- .../GitHubPluginConfig/help-clientCacheSize.html | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) 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 163ae7d3b..61fda510b 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -257,6 +257,7 @@ public FormValidation doVerifyCredentials( @QueryParameter String apiUrl, @QueryParameter String credentialsId) throws IOException { GitHubServerConfig config = new GitHubServerConfig(credentialsId); + config.setCustomApiUrl(isUrlCustom(apiUrl)); config.setApiUrl(apiUrl); GitHub gitHub = new GitHubLoginFunction().apply(config); 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 6941a8c83..724331fda 100644 --- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java @@ -83,8 +83,9 @@ protected GitHub applyNullSafe(@Nonnull GitHubServerConfig github) { * * @param apiUrl GitHub's url to build proxy to * - * @return proxy to use it in connector + * @return proxy to use it in connector. Should not be null as it can lead to unexpected behaviour */ + @Nonnull private Proxy getProxy(String apiUrl) { Jenkins jenkins = GitHubWebHook.getJenkinsInstance(); @@ -105,11 +106,10 @@ private Proxy getProxy(String apiUrl) { * @return connector to be used as backend for client */ private OkHttpConnector connector(String apiUrl) { - Jenkins jenkins = GitHubWebHook.getJenkinsInstance(); OkHttpClient client = new OkHttpClient().setProxy(getProxy(apiUrl)); if (configuration().getClientCacheSize() > 0) { - File cacheDir = new File(jenkins.getRootDir(), GitHubPlugin.class.getName() + ".cache"); + File cacheDir = getCacheBaseDirFor(GitHubWebHook.getJenkinsInstance()); Cache cache = new Cache(cacheDir, configuration().getClientCacheSize() * 1024 * 1024); client.setCache(cache); } @@ -117,6 +117,13 @@ private OkHttpConnector connector(String apiUrl) { return new OkHttpConnector(new OkUrlFactory(client)); } + /** + * @return directory with cache for GitHub client + */ + public static File getCacheBaseDirFor(Jenkins jenkins) { + return new File(jenkins.getRootDir(), GitHubPlugin.class.getName() + ".cache"); + } + /** * Copy-paste due to class loading issues * diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html index b15f4ee37..d29cc2b16 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html @@ -3,5 +3,5 @@ GH + okHttp do all work for results reliability (Conditional-requests in GitHub documentation)
- Set to 0 to disable this feature + Set 0 to disable this feature From 0037970689fd4d201eb8e9847dfd892627d95a74 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 28 Sep 2015 01:06:01 +0300 Subject: [PATCH 107/228] simply return on empty list of events to subscribe, without error --- .../github/webhook/WebhookManager.java | 8 +++++-- .../github/webhook/WebhookManagerTest.java | 22 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) 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 2c52a215f..fb7384316 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -3,9 +3,7 @@ import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.base.Function; import com.google.common.base.Predicate; - import hudson.model.Job; - import org.apache.commons.lang.Validate; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; @@ -87,6 +85,12 @@ public Runnable registerFor(final Job project) { return new Runnable() { public void run() { + if (events.isEmpty()) { + LOGGER.debug("No any subscriber interested in {}, but hooks creation launched, skipping...", + project.getFullName()); + return; + } + LOGGER.info("GitHub webhooks activated for job {} with {} (events: {})", project.getFullName(), names, events); diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index 5423cffd2..27d9ecbce 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -5,7 +5,6 @@ import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import hudson.model.FreeStyleProject; import hudson.plugins.git.GitSCM; import org.jenkinsci.plugins.github.GitHubPlugin; @@ -39,7 +38,8 @@ import static org.kohsuke.github.GHEvent.PULL_REQUEST; import static org.kohsuke.github.GHEvent.PUSH; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anySet; +import static org.mockito.Matchers.anyListOf; +import static org.mockito.Matchers.anySetOf; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -168,7 +168,7 @@ public void shouldNotReplaceAlreadyRegisteredHook() throws IOException { manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive); verify(manager, never()).deleteWebhook(); - verify(manager, never()).createWebhook(any(URL.class), anySet()); + verify(manager, never()).createWebhook(any(URL.class), anySetOf(GHEvent.class)); } @Test @@ -177,7 +177,7 @@ public void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOEx project.setScm(GIT_SCM); manager.registerFor(project).run(); - verify(manager).createHookSubscribedTo(Collections.emptyList()); + verify(manager, never()).createHookSubscribedTo(anyListOf(GHEvent.class)); } @Test @@ -190,12 +190,22 @@ public void shouldAddPushEventByDefault() throws IOException { verify(manager).createHookSubscribedTo(newArrayList(PUSH)); } + @Test + public void shouldReturnNullOnGettingEmptyEventsListToSubscribe() throws IOException { + doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class)); + when(repo.hasAdminAccess()).thenReturn(true); + + assertThat("empty events list not allowed to be registered", + forHookUrl(HOOK_ENDPOINT) + .createHookSubscribedTo(Collections.emptyList()).apply(active), nullValue()); + } + @Test public void shouldSelectOnlyHookManagedCreds() { GitHubServerConfig conf = new GitHubServerConfig(""); conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); - + assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH)) .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue()); } @@ -206,7 +216,7 @@ public void shouldNotSelectCredsWithCustomHost() { conf.setApiUrl(ANOTHER_HOOK_ENDPOINT.toString()); conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); - + assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH)) .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue()); } From de96c1ab90c0704846c0cd75ef94f1f6088b163a Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Tue, 29 Sep 2015 00:27:04 +0300 Subject: [PATCH 108/228] add coverage badge closes #92 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd6c91254..8271a69dc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ Jenkins Github Plugin -===================== +===================== + +[![Coverage](https://img.shields.io/sonar/http/sonar.lanwen.ru/com.coravy.hudson.plugins.github:github/coverage.svg?style=flat)](http://sonar.lanwen.ru/dashboard/index?id=com.coravy.hudson.plugins.github:github) Read more: [http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin) From ef8d88e4ee7573cd2796c8ff46f62324b19f0024 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 5 Oct 2015 01:04:20 +0300 Subject: [PATCH 109/228] separate cache for each server config This change requires java 1.7 to use java.nio as more effective way to operate files. This implementation creates directory with name as hash of each pair . Redundant dirs removed on each save of global config. --- pom.xml | 42 +++- .../github/config/GitHubPluginConfig.java | 37 +--- .../github/config/GitHubServerConfig.java | 40 +++- .../github/internal/GitHubClientCacheOps.java | 193 ++++++++++++++++++ .../github/internal/GitHubLoginFunction.java | 24 +-- .../config/GitHubPluginConfig/config.groovy | 6 +- .../config/GitHubServerConfig/config.groovy | 15 +- .../help-clientCacheSize.html | 0 .../GitHubClientCacheCleanupTest.java | 128 ++++++++++++ .../internal/GitHubClientCacheOpsTest.java | 99 +++++++++ .../GitHubClientCacheCleanupTest/user.json | 43 ++++ 11 files changed, 562 insertions(+), 65 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java rename src/main/resources/org/jenkinsci/plugins/github/config/{GitHubPluginConfig => GitHubServerConfig}/help-clientCacheSize.html (100%) create mode 100644 src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java create mode 100644 src/test/resources/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest/user.json diff --git a/pom.xml b/pom.xml index 18657df78..7311984b1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -173,6 +174,41 @@ test
+ + + com.github.tomakehurst + wiremock + 1.57 + test + 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 + + + + @@ -180,8 +216,8 @@ maven-compiler-plugin - 1.6 - 1.6 + 1.7 + 1.7 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 32be95ec3..c48f7086e 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -38,6 +38,7 @@ 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; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.clearRedundantCaches; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** @@ -52,13 +53,6 @@ public class GitHubPluginConfig extends GlobalConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPluginConfig.class); public static final String GITHUB_PLUGIN_CONFIGURATION_ID = "github-plugin-configuration"; - /** - * Default value in MB for client cache size - * - * @see #getClientCacheSize() - */ - private static final int DEFAULT_CLIENT_CACHE_SIZE_MB = 20; - /** * Helps to avoid null in {@link GitHubPlugin#configuration()} */ @@ -68,12 +62,6 @@ public class GitHubPluginConfig extends GlobalConfiguration { private List configs = new ArrayList(); private URL hookUrl; - /** - * @see #getClientCacheSize() - * @see #setClientCacheSize(int) - */ - private int clientCacheSize = DEFAULT_CLIENT_CACHE_SIZE_MB; - private transient boolean overrideHookUrl; /** @@ -133,24 +121,6 @@ public boolean isOverrideHookURL() { return hookUrl != null; } - /** - * Capacity of cache for GitHub client in MB. - * - * Defaults to 20 MB - * - * @since TODO - */ - public int getClientCacheSize() { - return clientCacheSize; - } - - /** - * @param clientCacheSize capacity of cache for GitHub client in MB, set to <= 0 to turn off this feature - */ - public void setClientCacheSize(int clientCacheSize) { - this.clientCacheSize = clientCacheSize; - } - /** * Filters all stored configs against given predicate then * logs in as the given user and returns the non null connection objects @@ -195,6 +165,7 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti format("Mailformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); } save(); + clearRedundantCaches(configs); return true; } @@ -250,7 +221,7 @@ public FormValidation doCheckHookUrl(@QueryParameter String value) { * @return url to be used in GH hooks configuration as main endpoint * @throws GHPluginConfigException if jenkins root url empty of malformed */ - private URL constructDefaultUrl() { + private static URL constructDefaultUrl() { String jenkinsUrl = Jenkins.getInstance().getRootUrl(); validateConfig(isNotEmpty(jenkinsUrl), Messages.global_config_url_is_empty()); try { @@ -268,7 +239,7 @@ private URL constructDefaultUrl() { * * @throws GHPluginConfigException if state is false */ - private void validateConfig(boolean state, String message) { + private static void validateConfig(boolean state, String message) { if (!state) { throw new GHPluginConfigException(message); } 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 61fda510b..5b9187435 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -65,10 +65,23 @@ public class GitHubServerConfig extends AbstractDescribableImpl withEnabledCache() { + return new WithEnabledCache(); + } + + /** + * @return function to convert {@link GitHubServerConfig} to {@link Cache} + */ + public static Function toCacheDir() { + return new ToCacheDir(); + } + + /** + * Extracts relative to base cache dir name of cache folder for each config + * For example if the full path to cache folder is + * "$JENKINS_HOME/org.jenkinsci.plugins.github.GitHubPlugin.cache/keirurna", this function returns "keirurna" + * + * @return function to extract folder name from cache object + */ + public static Function cacheToName() { + return new CacheToName(); + } + + /** + * To accept for cleaning only not active cache dirs + * + * @param caches set of active cache names, extracted with help of {@link #cacheToName()} + * + * @return filter to accept only names not in set + */ + public static DirectoryStream.Filter notInCaches(Set caches) { + checkNotNull(caches, "set of active caches can't be null"); + return new NotInCachesFilter(caches); + } + + /** + * This directory contains all other cache dirs for each client config + * + * @return path to base cache directory. + */ + public static Path getBaseCacheDir() { + return new File(GitHubWebHook.getJenkinsInstance().getRootDir(), + GitHubPlugin.class.getName() + ".cache").toPath(); + } + + /** + * Removes all not active dirs with old caches. + * This method is invoked after each save of global plugin config + * + * @param configs active server configs to exclude caches from cleanup + */ + public static void clearRedundantCaches(List configs) { + Path baseCacheDir = getBaseCacheDir(); + + if (notExists(baseCacheDir)) { + return; + } + + final Set actualNames = from(configs).filter(withEnabledCache()).transform(toCacheDir()) + .transform(cacheToName()).toSet(); + + try (DirectoryStream caches = newDirectoryStream(baseCacheDir, notInCaches(actualNames))) { + deleteEveryIn(caches); + } catch (IOException e) { + LOGGER.warn("Can't list cache dirs in {}", baseCacheDir, e); + } + } + + /** + * Removes directories with caches + * + * @param caches paths to directories to be removed + */ + private static void deleteEveryIn(DirectoryStream caches) { + for (Path notActualCache : caches) { + LOGGER.debug("Deleting redundant cache dir {}", notActualCache); + try { + FileUtils.deleteDirectory(notActualCache.toFile()); + } catch (IOException e) { + LOGGER.error("Can't delete cache dir <{}>", notActualCache, e); + } + } + } + + /** + * @see #withEnabledCache() + */ + private static class WithEnabledCache extends NullSafePredicate { + @Override + protected boolean applyNullSafe(@Nonnull GitHubServerConfig config) { + return config.getClientCacheSize() > 0; + } + } + + /** + * @see #toCacheDir() + */ + private static class ToCacheDir extends NullSafeFunction { + + public static final int MB = 1024 * 1024; + + @Override + protected Cache applyNullSafe(@Nonnull GitHubServerConfig config) { + checkArgument(config.getClientCacheSize() > 0, "Cache can't be with size <= 0"); + + Path cacheDir = getBaseCacheDir().resolve(hashed(config)); + + return new Cache(cacheDir.toFile(), config.getClientCacheSize() * MB); + } + + /** + * @param config url and creds id to be hashed + * + * @return unique id for folder name to create cache inside of base cache dir + */ + private static String hashed(GitHubServerConfig config) { + return Hashing.murmur3_32().newHasher() + .putString(trimToEmpty(config.getApiUrl())) + .putString(trimToEmpty(config.getCredentialsId())).hash().toString(); + } + } + + /** + * @see #cacheToName() + */ + private static class CacheToName extends NullSafeFunction { + @Override + protected String applyNullSafe(@Nonnull Cache cache) { + return cache.getDirectory().getName(); + } + } + + /** + * @see #notInCaches(Set) + */ + private static class NotInCachesFilter implements DirectoryStream.Filter { + private final Set activeCacheNames; + + public NotInCachesFilter(Set activeCacheNames) { + this.activeCacheNames = activeCacheNames; + } + + @Override + public boolean accept(Path entry) { + LOGGER.trace("Trying to find <{}> in active caches list...", entry); + return !activeCacheNames.contains(String.valueOf(entry.getFileName())); + } + } +} 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 724331fda..884a887d6 100644 --- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java @@ -5,7 +5,6 @@ import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkUrlFactory; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.kohsuke.accmod.Restricted; @@ -19,7 +18,6 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.Proxy; @@ -27,9 +25,9 @@ import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.jenkinsci.plugins.github.GitHubPlugin.configuration; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.tokenFor; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.toCacheDir; /** * Converts server config to authorized GH instance on {@link #applyNullSafe(GitHubServerConfig)}. @@ -64,7 +62,7 @@ protected GitHub applyNullSafe(@Nonnull GitHubServerConfig github) { GitHubBuilder builder = new GitHubBuilder() .withOAuthToken(accessToken) - .withConnector(connector(defaultIfBlank(github.getApiUrl(), GITHUB_URL))) + .withConnector(connector(github)) .withRateLimitHandler(RateLimitHandler.FAIL); try { if (isNotBlank(github.getApiUrl())) { @@ -101,29 +99,19 @@ private Proxy getProxy(String apiUrl) { * Uses proxy of jenkins * If cache size > 0, uses cache * - * @param apiUrl to build proxy - * * @return connector to be used as backend for client */ - private OkHttpConnector connector(String apiUrl) { - OkHttpClient client = new OkHttpClient().setProxy(getProxy(apiUrl)); + private OkHttpConnector connector(GitHubServerConfig config) { + OkHttpClient client = new OkHttpClient().setProxy(getProxy(defaultIfBlank(config.getApiUrl(), GITHUB_URL))); - if (configuration().getClientCacheSize() > 0) { - File cacheDir = getCacheBaseDirFor(GitHubWebHook.getJenkinsInstance()); - Cache cache = new Cache(cacheDir, configuration().getClientCacheSize() * 1024 * 1024); + if (config.getClientCacheSize() > 0) { + Cache cache = toCacheDir().apply(config); client.setCache(cache); } return new OkHttpConnector(new OkUrlFactory(client)); } - /** - * @return directory with cache for GitHub client - */ - public static File getCacheBaseDirFor(Jenkins jenkins) { - return new File(jenkins.getRootDir(), GitHubPlugin.class.getName() + ".cache"); - } - /** * Copy-paste due to class loading issues * 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 09d69d2a7..0bbf30400 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 @@ -37,11 +37,7 @@ f.section(title: descriptor.displayName) { } } } - - f.entry(title: _("GitHub client cache size (MB)"), field: "clientCacheSize") { - f.textbox() - } - + f.entry(title: _("Additional actions"), help: descriptor.getHelpFile('additional')) { f.hetero_list(items: [], addCaption: _("Manage additional GitHub actions"), 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 0b3242978..aca7f22b3 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 @@ -14,20 +14,27 @@ f.entry(title: _("Credentials"), field: "credentialsId") { c.select() } -f.optionalBlock(title: _("Custom GitHub API URL"), - inline: true, - field: "customApiUrl", +f.optionalBlock(title: _("Custom GitHub API URL"), + inline: true, + field: "customApiUrl", checked: instance?.customApiUrl) { f.entry(title: _("GitHub API URL"), field: "apiUrl") { f.textbox(default: GitHubServerConfig.GITHUB_URL) } } +f.advanced() { + f.entry(title: _("GitHub client cache size (MB)"), field: "clientCacheSize") { + f.textbox(default: GitHubServerConfig.DEFAULT_CLIENT_CACHE_SIZE_MB) + } +} + f.block() { f.validateButton( title: _("Verify credentials"), progress: _("Verifying..."), method: "verifyCredentials", - with: "apiUrl,credentialsId" + with: "apiUrl,credentialsId,clientCacheSize" ) } + diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-clientCacheSize.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-clientCacheSize.html rename to src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/help-clientCacheSize.html diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java new file mode 100644 index 000000000..81194229c --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java @@ -0,0 +1,128 @@ +package org.jenkinsci.plugins.github.internal; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.util.Collections; + +import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.google.common.collect.Lists.newArrayList; +import static java.nio.file.Files.newDirectoryStream; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.clearRedundantCaches; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.getBaseCacheDir; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubClientCacheCleanupTest { + + public static final String DEFAULT_CREDS_ID = ""; + public static final String CHANGED_CREDS_ID = "id"; + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + @Rule + public WireMockRule github = new WireMockRule(); + + @Before + public void setUp() throws Exception { + stubUserResponse(); + } + + @Test + public void shouldCreateCachedFolder() throws Exception { + makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); + + it("should create cached dir", 1); + } + + @Test + public void shouldCreateOnlyOneCachedFolderForSameCredsAndApi() throws Exception { + makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); + makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); + + it("should create and use same cached dir", 1); + } + + @Test + public void shouldCreateCachedFolderForEachCreds() throws Exception { + makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); + makeCachedRequestWithCredsId(CHANGED_CREDS_ID); + + it("should create cached dirs for each config", 2); + } + + @Test + public void shouldRemoveCachedDirAfterClean() throws Exception { + makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); + + clearRedundantCaches(Collections.emptyList()); + + it("should remove cached dir", 0); + } + + @Test + public void shouldRemoveOnlyNotActiveCachedDirAfterClean() throws Exception { + makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); + makeCachedRequestWithCredsId(CHANGED_CREDS_ID); + + GitHubServerConfig config = new GitHubServerConfig(CHANGED_CREDS_ID); + config.setCustomApiUrl(true); + config.setApiUrl(constructApiUrl()); + config.setClientCacheSize(1); + + clearRedundantCaches(newArrayList(config)); + + it("should remove only not active cache dir", 1); + } + + @Test + public void shouldRemoveCacheWhichNotEnabled() throws Exception { + makeCachedRequestWithCredsId(CHANGED_CREDS_ID); + + GitHubServerConfig config = new GitHubServerConfig(CHANGED_CREDS_ID); + config.setCustomApiUrl(true); + config.setApiUrl(constructApiUrl()); + config.setClientCacheSize(0); + + clearRedundantCaches(newArrayList(config)); + + it("should remove not active cache dir", 0); + } + + private void it(String comment, int count) throws IOException { + try (DirectoryStream paths = newDirectoryStream(getBaseCacheDir())) { + assertThat(comment, newArrayList(paths), hasSize(count)); + } + } + + private String constructApiUrl() { + return "http://localhost:" + github.port(); + } + + private void makeCachedRequestWithCredsId(String credsId) throws IOException { + jRule.getInstance().getDescriptorByType(GitHubServerConfig.DescriptorImpl.class) + .doVerifyCredentials(constructApiUrl(), credsId, 1); + } + + private void stubUserResponse() throws IOException { + github.stubFor(get(urlPathEqualTo("/user")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody(classpath(getClass(), "user.json")))); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java new file mode 100644 index 000000000..44d85df13 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java @@ -0,0 +1,99 @@ +package org.jenkinsci.plugins.github.internal; + +import com.squareup.okhttp.Cache; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.toCacheDir; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.withEnabledCache; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubClientCacheOpsTest { + + public static final String CREDENTIALS_ID = "credsid"; + public static final String CREDENTIALS_ID_2 = "credsid2"; + public static final String CUSTOM_API_URL = "http://api.some.unk/"; + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + @Test + public void shouldPointToSameCacheForOneConfig() throws Exception { + GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); + + Cache cache1 = toCacheDir().apply(config); + Cache cache2 = toCacheDir().apply(config); + + assertThat("same config should get same cache", + cache1.getDirectory().getAbsolutePath(), equalTo(cache2.getDirectory().getAbsolutePath())); + } + + @Test + public void shouldPointToDifferentCachesOnChangedApiPath() throws Exception { + GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); + config.setCustomApiUrl(true); + config.setApiUrl(CUSTOM_API_URL); + + GitHubServerConfig config2 = new GitHubServerConfig(CREDENTIALS_ID); + + Cache cache1 = toCacheDir().apply(config); + Cache cache2 = toCacheDir().apply(config2); + + assertThat("with changed url", + cache1.getDirectory().getAbsolutePath(), not(cache2.getDirectory().getAbsolutePath())); + } + + @Test + public void shouldPointToDifferentCachesOnChangedCreds() throws Exception { + GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); + GitHubServerConfig config2 = new GitHubServerConfig(CREDENTIALS_ID_2); + + Cache cache1 = toCacheDir().apply(config); + Cache cache2 = toCacheDir().apply(config2); + + assertThat("with changed creds", + cache1.getDirectory().getAbsolutePath(), not(cache2.getDirectory().getAbsolutePath())); + } + + @Test + @WithoutJenkins + public void shouldReturnEnabledOnCacheGreaterThan0() throws Exception { + GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); + config.setClientCacheSize(1); + + assertThat("1MB", withEnabledCache().apply(config), is(true)); + } + + @Test + @WithoutJenkins + public void shouldReturnNotEnabledOnCacheEq0() throws Exception { + GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); + config.setClientCacheSize(0); + + assertThat("zero cache", withEnabledCache().apply(config), is(false)); + } + + @Test + @WithoutJenkins + public void shouldReturnNotEnabledOnCacheLessThan0() throws Exception { + GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); + config.setClientCacheSize(-1); + + assertThat("-1 value", withEnabledCache().apply(config), is(false)); + } + + @Test + @WithoutJenkins + public void shouldHaveEnabledCacheByDefault() throws Exception { + assertThat("default cache", withEnabledCache().apply(new GitHubServerConfig(CREDENTIALS_ID)), is(true)); + } +} diff --git a/src/test/resources/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest/user.json b/src/test/resources/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest/user.json new file mode 100644 index 000000000..586343543 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest/user.json @@ -0,0 +1,43 @@ +{ + "login": "login", + "id": 2341, + "avatar_url": "", + "gravatar_id": "", + "url": "https://api.github.com/users/login", + "html_url": "https://github.com/login", + "followers_url": "https://api.github.com/users/login/followers", + "following_url": "https://api.github.com/users/login/following{/other_user}", + "gists_url": "https://api.github.com/users/login/gists{/gist_id}", + "starred_url": "https://api.github.com/users/login/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/login/subscriptions", + "organizations_url": "https://api.github.com/users/login/orgs", + "repos_url": "https://api.github.com/users/login/repos", + "events_url": "https://api.github.com/users/login/events{/privacy}", + "received_events_url": "https://api.github.com/users/login/received_events", + "type": "User", + "site_admin": false, + "name": "User", + "company": "Company", + "blog": "http://blog.blog", + "location": "Location", + "email": null, + "hireable": null, + "bio": null, + "public_repos": 1, + "public_gists": 1, + "followers": 1, + "following": 1, + "created_at": "2012-07-12T16:12:59Z", + "updated_at": "2015-10-05T08:55:34Z", + "private_gists": 1, + "total_private_repos": 0, + "owned_private_repos": 0, + "disk_usage": 10, + "collaborators": 0, + "plan": { + "name": "free", + "space": 976562499, + "collaborators": 0, + "private_repos": 0 + } +} \ No newline at end of file From 8eafea825e7737f4384bfbbee98336a5ee70f103 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 12:49:33 +0300 Subject: [PATCH 110/228] fix dynamic port allocation when using wiremock to mock GH --- .../plugins/github/internal/GitHubClientCacheCleanupTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java index 81194229c..4cecd512e 100644 --- a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java @@ -16,6 +16,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.google.common.collect.Lists.newArrayList; import static java.nio.file.Files.newDirectoryStream; import static org.hamcrest.MatcherAssert.assertThat; @@ -35,7 +36,7 @@ public class GitHubClientCacheCleanupTest { public JenkinsRule jRule = new JenkinsRule(); @Rule - public WireMockRule github = new WireMockRule(); + public WireMockRule github = new WireMockRule(wireMockConfig().dynamicPort()); @Before public void setUp() throws Exception { From a9e8f8998724748a0512fabf7501b68e25d9e416 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 20:26:07 +0300 Subject: [PATCH 111/228] add overrides on methods in GitHubWebHook class --- src/main/java/com/cloudbees/jenkins/GitHubWebHook.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 7c66ff144..dd494795c 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -49,14 +49,17 @@ public class GitHubWebHook implements UnprotectedRootAction { private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting); + @Override public String getIconFileName() { return null; } + @Override public String getDisplayName() { return null; } + @Override public String getUrlName() { return URLNAME; } @@ -73,7 +76,7 @@ public void registerHookFor(Job job) { } /** - * Calls {@link #registerHookFor(AbstractProject)} for every project which have subscriber + * Calls {@link #registerHookFor(Job)} for every project which have subscriber * * @return list of jobs which jenkins tried to register hook */ From 5fc99442166e425b35f7346594b1d21041adef3e Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 20:26:49 +0300 Subject: [PATCH 112/228] change since to new 1.14.0 ver --- .../org/jenkinsci/plugins/github/config/GitHubServerConfig.java | 2 +- .../jenkinsci/plugins/github/internal/GitHubClientCacheOps.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 5b9187435..bd8594731 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -157,7 +157,7 @@ public String getCredentialsId() { * * Defaults to 20 MB * - * @since TODO + * @since 1.14.0 */ public int getClientCacheSize() { return clientCacheSize; 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 3bad4f13a..2666ba630 100644 --- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java @@ -32,7 +32,7 @@ * Class with util functions to operate GitHub client cache * * @author lanwen (Merkushev Kirill) - * @since TODO + * @since 1.14.0 */ public final class GitHubClientCacheOps { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubClientCacheOps.class); From 5c55f50eb7ae21f75f73827f622ac7a9e4692883 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 21:27:19 +0300 Subject: [PATCH 113/228] change developers pom section to reflect current state --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 7311984b1..e38925dfc 100644 --- a/pom.xml +++ b/pom.xml @@ -26,12 +26,12 @@ - kohsuke - Kohsuke Kawaguchi + lanwen + Merkushev Kirill - juretta - Stefan Saasen + KostyaSha + Kanstantsin Shautsou From 8bf6253e3f2bb8b9c57b456b1343467e3ade82a5 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 21:35:44 +0300 Subject: [PATCH 114/228] [maven-release-plugin] prepare release github-1.14.0 --- pom.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index e38925dfc..c32f2af9a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -11,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.0-SNAPSHOT + 1.14.0 hpi GitHub plugin @@ -39,7 +38,7 @@ 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 + github-1.14.0 From 2f99129a5aafa3e82a3476a8be76d4404aef4f35 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 21:35:49 +0300 Subject: [PATCH 115/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c32f2af9a..7fbbaba59 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.0 + 1.14.1-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.14.0 + HEAD From a6eb704e0987ffd7ae34534f05ed2552c6de35d7 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Wed, 7 Oct 2015 23:03:29 +0300 Subject: [PATCH 116/228] ignore files when clean root cache dir add tests for notInCache filter --- .../github/internal/GitHubClientCacheOps.java | 5 +++ .../internal/GitHubClientCacheOpsTest.java | 38 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) 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 2666ba630..1610fe48c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java @@ -23,6 +23,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.file.Files.isDirectory; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.notExists; import static org.apache.commons.lang3.StringUtils.trimToEmpty; @@ -186,6 +187,10 @@ public NotInCachesFilter(Set activeCacheNames) { @Override public boolean accept(Path entry) { + if (!isDirectory(entry)) { + LOGGER.debug("{} is not a directory", entry); + return false; + } LOGGER.trace("Trying to find <{}> in active caches list...", entry); return !activeCacheNames.contains(String.valueOf(entry.getFileName())); } diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java index 44d85df13..3a082ac27 100644 --- a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java @@ -2,15 +2,21 @@ import com.squareup.okhttp.Cache; import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; +import java.io.File; + +import static com.google.common.collect.Sets.newHashSet; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.notInCaches; import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.toCacheDir; import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.withEnabledCache; @@ -23,6 +29,9 @@ public class GitHubClientCacheOpsTest { public static final String CREDENTIALS_ID_2 = "credsid2"; public static final String CUSTOM_API_URL = "http://api.some.unk/"; + @ClassRule + public static TemporaryFolder tmp = new TemporaryFolder(); + @Rule public JenkinsRule jRule = new JenkinsRule(); @@ -64,12 +73,35 @@ public void shouldPointToDifferentCachesOnChangedCreds() throws Exception { cache1.getDirectory().getAbsolutePath(), not(cache2.getDirectory().getAbsolutePath())); } + @Test + @WithoutJenkins + public void shouldNotAcceptFilesInFilter() throws Exception { + assertThat("file should not be accepted", + notInCaches(newHashSet("file")).accept(tmp.newFile().toPath()), is(false)); + } + + @Test + @WithoutJenkins + public void shouldNotAcceptDirsInFilterWithNameFromSet() throws Exception { + File dir = tmp.newFolder(); + assertThat("should not accept folders from set", + notInCaches(newHashSet(dir.getName())).accept(dir.toPath()), is(false)); + } + + @Test + @WithoutJenkins + public void shouldAcceptDirsInFilterWithNameNotInSet() throws Exception { + File dir = tmp.newFolder(); + assertThat("should accept folders not in set", + notInCaches(newHashSet(dir.getName() + "abc")).accept(dir.toPath()), is(true)); + } + @Test @WithoutJenkins public void shouldReturnEnabledOnCacheGreaterThan0() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setClientCacheSize(1); - + assertThat("1MB", withEnabledCache().apply(config), is(true)); } @@ -78,7 +110,7 @@ public void shouldReturnEnabledOnCacheGreaterThan0() throws Exception { public void shouldReturnNotEnabledOnCacheEq0() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setClientCacheSize(0); - + assertThat("zero cache", withEnabledCache().apply(config), is(false)); } @@ -87,7 +119,7 @@ public void shouldReturnNotEnabledOnCacheEq0() throws Exception { public void shouldReturnNotEnabledOnCacheLessThan0() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setClientCacheSize(-1); - + assertThat("-1 value", withEnabledCache().apply(config), is(false)); } From 3990a367f84791f37dc978cbc5cc92e3f7b3a339 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 17 Sep 2015 16:45:55 -0400 Subject: [PATCH 117/228] Let GithubProjectProperty be applied to any Job. Normalizing databinding. The projectUrl property was defined in the constructor to be a String, but in the getter to be a GithubUrl, which made no sense and breaks tools looking to work with describables without GUI forms. Changing the getter type would be the simplest fix, but potentially incompatible. Instead changing the property name so that the existing getter can be left alone. --- .../plugins/github/GithubLinkAnnotator.java | 6 ++-- .../plugins/github/GithubProjectProperty.java | 23 +++++++++----- .../github/GithubProjectProperty/config.jelly | 5 ++-- .../help-projectUrlStr.html} | 0 .../help-projectUrlStr_de.html} | 0 .../github/GithubProjectPropertyTest.java | 30 +++++++++++++++++++ 6 files changed, 51 insertions(+), 13 deletions(-) rename src/main/{webapp/help-global.html => resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr.html} (100%) rename src/main/{webapp/help-global_de.html => resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html} (100%) create mode 100644 src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java 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 ed46c8dd2..591c1521e 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java @@ -3,7 +3,7 @@ import hudson.Extension; import hudson.MarkupText; import hudson.MarkupText.SubText; -import hudson.model.AbstractBuild; +import hudson.model.Run; import hudson.plugins.git.GitChangeSet; import hudson.scm.ChangeLogAnnotator; import hudson.scm.ChangeLogSet.Entry; @@ -25,8 +25,8 @@ public class GithubLinkAnnotator extends ChangeLogAnnotator { @Override - public void annotate(AbstractBuild build, Entry change, MarkupText text) { - final GithubProjectProperty p = build.getProject().getProperty( + public void annotate(Run build, Entry change, MarkupText text) { + final GithubProjectProperty p = build.getParent().getProperty( GithubProjectProperty.class); if (null == p) { return; diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java index 8a16f3677..4a4a12faf 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java @@ -2,11 +2,11 @@ import com.cloudbees.jenkins.GitHubPushTrigger; import hudson.Extension; -import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Job; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; +import jenkins.model.ParameterizedJobMixIn; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; @@ -21,9 +21,8 @@ * As of now this is only the URL to the github project. * * @author Stefan Saasen - * @todo Should we store the GithubUrl instead of the String? */ -public final class GithubProjectProperty extends JobProperty> { +public final class GithubProjectProperty extends JobProperty> { /** * This will the URL to the project main branch. @@ -31,8 +30,18 @@ public final class GithubProjectProperty extends JobProperty getJobActions(AbstractProject job) { + public Collection getJobActions(Job job) { if (null != projectUrl) { return Collections.singleton(new GithubLinkAction(this)); } @@ -59,7 +68,7 @@ public DescriptorImpl() { } public boolean isApplicable(Class jobType) { - return AbstractProject.class.isAssignableFrom(jobType); + return ParameterizedJobMixIn.ParameterizedJob.class.isAssignableFrom(jobType); } public String getDisplayName() { diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly index 93bd91a7d..c7eb0111d 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly @@ -1,6 +1,5 @@ - - + + \ No newline at end of file diff --git a/src/main/webapp/help-global.html b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr.html similarity index 100% rename from src/main/webapp/help-global.html rename to src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr.html diff --git a/src/main/webapp/help-global_de.html b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html similarity index 100% rename from src/main/webapp/help-global_de.html rename to src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-projectUrlStr_de.html diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java new file mode 100644 index 000000000..848a5d902 --- /dev/null +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java @@ -0,0 +1,30 @@ +package com.coravy.hudson.plugins.github; + +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.structs.DescribableHelper; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; + +public class GithubProjectPropertyTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void configRoundTrip() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + j.configRoundtrip(p); + assertNull(p.getProperty(GithubProjectProperty.class)); + String url = "https://github.com/a/b/"; + p.addProperty(new GithubProjectProperty(url)); + j.configRoundtrip(p); + GithubProjectProperty prop = p.getProperty(GithubProjectProperty.class); + assertNotNull(prop); + assertEquals(url, prop.getProjectUrl().baseUrl()); + prop = DescribableHelper.instantiate(GithubProjectProperty.class, DescribableHelper.uninstantiate(prop)); + assertEquals(url, prop.getProjectUrl().baseUrl()); + } + +} From e3ba8070652f3ab983162a4deccf785246024be4 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sat, 17 Oct 2015 00:03:36 +0300 Subject: [PATCH 118/228] simplify testing of GHRepoName --- pom.xml | 7 + .../github/GitHubRepositoryNameTest.java | 224 +++++------------- .../github/test/GitHubRepoNameMatchers.java | 66 ++++++ 3 files changed, 127 insertions(+), 170 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/test/GitHubRepoNameMatchers.java diff --git a/pom.xml b/pom.xml index 7fbbaba59..ec92f9b2c 100644 --- a/pom.xml +++ b/pom.xml @@ -173,6 +173,13 @@ test + + com.tngtech.java + junit-dataprovider + 1.10.0 + test + + com.github.tomakehurst diff --git a/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java b/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java index 7afe34ae3..f94f58f04 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java @@ -1,190 +1,74 @@ package com.coravy.hudson.plugins.github; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - import com.cloudbees.jenkins.GitHubRepositoryName; - +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.nullValue; +import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.repo; +import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withHost; +import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withRepoName; +import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withUserName; +import static org.junit.Assert.assertThat; /** * Unit tests of {@link GitHubRepositoryName} */ +@RunWith(DataProviderRunner.class) public class GitHubRepositoryNameTest { - private void testURL(String URL, String host, String owner, String repository) - { - GitHubRepositoryName repo = GitHubRepositoryName.create(URL); - assertNotNull(repo); - assertEquals(host, repo.host); - assertEquals(owner, repo.userName); - assertEquals(repository, repo.repositoryName); - } - - @Test - public void gitAtUrlGitHub() { - testURL("git@github.com:jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitAtUrlOtherHost() { - testURL("git@gh.company.com:jenkinsci/jenkins.git", "gh.company.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitColonUrlGitHub() { - testURL("git://github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitColonUrlOtherHost() { - testURL("git://company.net/jenkinsci/jenkins.git", "company.net", "jenkinsci", "jenkins"); - } - - @Test - public void httpsUrlGitHub() { - testURL("https://user@github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void httpsUrlGitHubWithoutUser() { - testURL("https://github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void httpsUrlOtherHost() { - testURL("https://employee@gh.company.com/jenkinsci/jenkins.git", "gh.company.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitAtUrlGitHubNoSuffix() { - testURL("git@github.com:jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitAtUrlOtherHostNoSuffix() { - testURL("git@gh.company.com:jenkinsci/jenkins", "gh.company.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitColonUrlGitHubNoSuffix() { - testURL("git://github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitColonUrlOtherHostNoSuffix() { - testURL("git://company.net/jenkinsci/jenkins", "company.net", "jenkinsci", "jenkins"); - } - - @Test - public void httpsUrlGitHubNoSuffix() { - testURL("https://user@github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void httpsUrlGitHubWithoutUserNoSuffix() { - testURL("https://github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"); - } - - @Test - public void httpsUrlOtherHostNoSuffix() { - testURL("https://employee@gh.company.com/jenkinsci/jenkins", "gh.company.com", "jenkinsci", "jenkins"); - } - - @Test - public void gitAtUrlGitHubTrailingSlash() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("git@github.com:jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("github.com", repo.host); - } - - @Test - public void gitAtUrlOtherHostTrailingSlash() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("git@gh.company.com:jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("gh.company.com", repo.host); - } - - @Test - public void gitColonUrlGitHubTrailingSlash() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("git://github.com/jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("github.com", repo.host); - } - - @Test - public void gitColonUrlOtherHostTrailingSlash() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("git://company.net/jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("company.net", repo.host); - } - @Test - public void httpsUrlGitHubTrailingSlash() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("https://user@github.com/jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("github.com", repo.host); - } - - @Test - public void httpsUrlGitHubWithoutUserTrailingSlash() { - //this is valid for anonymous usage - GitHubRepositoryName repo = GitHubRepositoryName - .create("https://github.com/jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("github.com", repo.host); + @DataProvider({ + "git@github.com:jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", + "git@github.com:jenkinsci/jenkins/, github.com, jenkinsci, jenkins", + "git@github.com:jenkinsci/jenkins, github.com, jenkinsci, jenkins", + "git@gh.company.com:jenkinsci/jenkins.git, gh.company.com, jenkinsci, jenkins", + "git@gh.company.com:jenkinsci/jenkins, gh.company.com, jenkinsci, jenkins", + "git@gh.company.com:jenkinsci/jenkins/, gh.company.com, jenkinsci, jenkins", + "git://github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", + "git://github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", + "git://github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", + "https://user@github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", + "https://user@github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", + "https://user@github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", + "https://employee@gh.company.com/jenkinsci/jenkins.git, gh.company.com, jenkinsci, jenkins", + "https://employee@gh.company.com/jenkinsci/jenkins, gh.company.com, jenkinsci, jenkins", + "https://employee@gh.company.com/jenkinsci/jenkins/, gh.company.com, jenkinsci, jenkins", + "git://company.net/jenkinsci/jenkins.git, company.net, jenkinsci, jenkins", + "git://company.net/jenkinsci/jenkins, company.net, jenkinsci, jenkins", + "git://company.net/jenkinsci/jenkins/, company.net, jenkinsci, jenkins", + "https://github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", + "https://github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", + "https://github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", + }) + public void githubFullRepo(String url, String host, String user, String repo) { + assertThat(url, repo(allOf( + withHost(host), + withUserName(user), + withRepoName(repo) + ))); } - @Test - public void httpsUrlOtherHostTrailingSlash() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("https://employee@gh.company.com/jenkinsci/jenkins/"); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("gh.company.com", repo.host); - } - @Test public void trimWhitespace() { - GitHubRepositoryName repo = GitHubRepositoryName - .create(" https://user@github.com/jenkinsci/jenkins/ "); - assertNotNull(repo); - assertEquals("jenkinsci", repo.userName); - assertEquals("jenkins", repo.repositoryName); - assertEquals("github.com", repo.host); + assertThat(" https://user@github.com/jenkinsci/jenkins/ ", repo(allOf( + withHost("github.com"), + withUserName("jenkinsci"), + withRepoName("jenkins") + ))); } @Test - public void badProtocol() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("gopher://gopher.floodgap.com"); - assertNull(repo); + @DataProvider(value = { + "gopher://gopher.floodgap.com", + "https//github.com/jenkinsci/jenkins", + "", + "null" + }, trimValues = false) + public void badUrl(String url) { + assertThat(url, repo(nullValue(GitHubRepositoryName.class))); } - - @Test - public void missingColon() { - GitHubRepositoryName repo = GitHubRepositoryName - .create("https//github.com/jenkinsci/jenkins"); - assertNull(repo); - } - } diff --git a/src/test/java/org/jenkinsci/plugins/github/test/GitHubRepoNameMatchers.java b/src/test/java/org/jenkinsci/plugins/github/test/GitHubRepoNameMatchers.java new file mode 100644 index 000000000..fb1d5a5ba --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/GitHubRepoNameMatchers.java @@ -0,0 +1,66 @@ +package org.jenkinsci.plugins.github.test; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import org.hamcrest.Description; +import org.hamcrest.DiagnosingMatcher; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; + +import static org.hamcrest.Matchers.is; + +/** + * @author lanwen (Merkushev Kirill) + */ +public final class GitHubRepoNameMatchers { + private GitHubRepoNameMatchers() { + } + + public static Matcher repo(final Matcher matcher) { + return new DiagnosingMatcher() { + @Override + protected boolean matches(Object url, Description mismatchDescription) { + mismatchDescription.appendText("for url ").appendValue(url).appendText(" instead of expected repo "); + + if (url != null && !(url instanceof String)) { + return false; + } + + GitHubRepositoryName repo = GitHubRepositoryName.create((String) url); + matcher.describeMismatch(repo, mismatchDescription); + return matcher.matches(repo); + } + + @Override + public void describeTo(Description description) { + description.appendText("GitHub full repo ").appendDescriptionOf(matcher); + } + }; + } + + public static Matcher withHost(String host) { + return new FeatureMatcher(is(host), "with host", "host") { + @Override + protected String featureValueOf(GitHubRepositoryName repo) { + return repo.getHost(); + } + }; + } + + public static Matcher withUserName(String username) { + return new FeatureMatcher(is(username), "with username", "username") { + @Override + protected String featureValueOf(GitHubRepositoryName repo) { + return repo.getUserName(); + } + }; + } + + public static Matcher withRepoName(String reponame) { + return new FeatureMatcher(is(reponame), "with reponame", "reponame") { + @Override + protected String featureValueOf(GitHubRepositoryName repo) { + return repo.getRepositoryName(); + } + }; + } +} From ce831f4a0133fa0f597eedf1a1ba625240063b7c Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sat, 17 Oct 2015 00:55:21 +0300 Subject: [PATCH 119/228] add new static factory for GHRepoName to create it from GHProjectProperty --- .../jenkins/GitHubRepositoryName.java | 25 ++++++++++++++--- .../github/GitHubRepositoryNameTest.java | 27 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 560f84757..db1290352 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -1,5 +1,6 @@ package com.cloudbees.jenkins; +import com.coravy.hudson.plugins.github.GithubProjectProperty; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -65,13 +66,12 @@ public class GitHubRepositoryName { /** * Create {@link GitHubRepositoryName} from URL * - * @param url must be non-null + * @param url repo url. Can be null * - * @return parsed {@link GitHubRepositoryName} or null if it cannot be - * parsed from the specified URL + * @return parsed {@link GitHubRepositoryName} or null if it cannot be parsed from the specified URL */ @CheckForNull - public static GitHubRepositoryName create(@Nonnull final String url) { + public static GitHubRepositoryName create(String url) { LOGGER.debug("Constructing from URL {}", url); for (Pattern p : URL_PATTERNS) { Matcher m = p.matcher(trimToEmpty(url)); @@ -86,6 +86,23 @@ public static GitHubRepositoryName create(@Nonnull final String url) { return null; } + /** + * @param projectProperty project property to extract url. Can be null + * + * @return parsed as {@link GitHubRepositoryName} object url to GitHub project + * @see #create(String) + * @since 1.14.1 + */ + @CheckForNull + public static GitHubRepositoryName create(GithubProjectProperty projectProperty) { + if (projectProperty == null) { + return null; + } + + return GitHubRepositoryName.create(projectProperty.getProjectUrlStr()); + } + + @SuppressWarnings("visibilitymodifier") public final String host; @SuppressWarnings("visibilitymodifier") diff --git a/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java b/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java index f94f58f04..0a70c850b 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java @@ -3,10 +3,13 @@ import com.cloudbees.jenkins.GitHubRepositoryName; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import org.apache.commons.lang3.StringUtils; import org.junit.Test; import org.junit.runner.RunWith; +import static com.cloudbees.jenkins.GitHubRepositoryName.create; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.repo; import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withHost; @@ -20,6 +23,9 @@ @RunWith(DataProviderRunner.class) public class GitHubRepositoryNameTest { + public static final String FULL_REPO_NAME = "jenkinsci/jenkins"; + public static final String VALID_HTTPS_GH_PROJECT = "https://github.com/" + FULL_REPO_NAME; + @Test @DataProvider({ "git@github.com:jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", @@ -71,4 +77,25 @@ public void trimWhitespace() { public void badUrl(String url) { assertThat(url, repo(nullValue(GitHubRepositoryName.class))); } + + @Test + public void shouldCreateFromProjectProp() { + assertThat("project prop vs direct", create(new GithubProjectProperty(VALID_HTTPS_GH_PROJECT)), + equalTo(create(VALID_HTTPS_GH_PROJECT))); + } + + @Test + public void shouldIgnoreNull() { + assertThat("null project prop", create((GithubProjectProperty) null), nullValue()); + } + + @Test + public void shouldIgnoreNullValueOfPP() { + assertThat("null project prop", create(new GithubProjectProperty(null)), nullValue()); + } + + @Test + public void shouldIgnoreBadValueOfPP() { + assertThat("null project prop", create(new GithubProjectProperty(StringUtils.EMPTY)), nullValue()); + } } From c2d0af666a50bc6ccd3eb81fd4644aa087741ab7 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sat, 17 Oct 2015 13:43:45 +0300 Subject: [PATCH 120/228] reuse validated logic of GHRepoName in default push subscriber --- .../subscriber/DefaultPushGHEventSubscriber.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) 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 f52a5017d..bee94ab34 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 @@ -16,8 +16,6 @@ import org.slf4j.LoggerFactory; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import static com.google.common.collect.Sets.immutableEnumSet; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.triggerFrom; @@ -34,7 +32,6 @@ @SuppressWarnings("unused") public class DefaultPushGHEventSubscriber extends GHEventsSubscriber { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPushGHEventSubscriber.class); - private static final Pattern REPOSITORY_NAME_PATTERN = Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)"); /** * This subscriber is applicable only for job with GHPush trigger @@ -65,19 +62,13 @@ protected Set events() { @Override protected void onEvent(GHEvent event, String payload) { JSONObject json = JSONObject.fromObject(payload); - // something like 'https://github.com/bar/foo' String repoUrl = json.getJSONObject("repository").getString("url"); final String pusherName = json.getJSONObject("pusher").getString("name"); LOGGER.info("Received POST for {}", repoUrl); - Matcher matcher = REPOSITORY_NAME_PATTERN.matcher(repoUrl); - if (matcher.matches()) { - final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); - if (changedRepository == null) { - LOGGER.warn("Malformed repo url {}", repoUrl); - return; - } + final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); + if (changedRepository != null) { // run in high privilege to see all the projects anonymous users don't see. // this is safe because when we actually schedule a build, it's a build that can // happen at some random time anyway. From a07e375522ba99ba4d12d27be8cf4694cd41295b Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 18 Oct 2015 14:01:08 +0300 Subject: [PATCH 121/228] add functional contribution section to CONTRIB.md --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 168895366..5a161fff9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,18 @@ +# 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 + # Code Style Guidelines Most of rules is checked with help of the *maven-checkstyle-plugin* during the `validate` phase. From 798377f2f165be51f6263cf9d01f8b6c6ca1013e Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Wed, 19 Aug 2015 10:23:02 +0300 Subject: [PATCH 122/228] Enable FindBugs in the local build flows. --- pom.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pom.xml b/pom.xml index eaa4f8500..0d364a46e 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ 3.3 2.5.1 + 3.0.2 @@ -208,6 +209,25 @@ + + + org.codehaus.mojo + findbugs-maven-plugin + ${findbugs-maven-plugin.version} + + Max + Low + true + false + + + + + check + + + + From e1383d6258d5a8ae9c9fa8131f96d17985a83165 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 19 Oct 2015 00:48:36 +0300 Subject: [PATCH 123/228] expandable message to use in builders and publishers --- pom.xml | 29 +++- .../github/common/ExpandableMessage.java | 126 ++++++++++++++++++ .../plugins/github/Messages.properties | 1 + .../common/ExpandableMessage/config.groovy | 7 + .../ExpandableMessage/help-content.html | 6 + .../github/common/ExpandableMessageTest.java | 100 ++++++++++++++ .../plugins/github/test/WithoutPlugins.java | 29 ++++ 7 files changed, 291 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/help-content.html create mode 100644 src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java diff --git a/pom.xml b/pom.xml index ec92f9b2c..623231063 100644 --- a/pom.xml +++ b/pom.xml @@ -123,6 +123,28 @@ 1.1 + + org.jenkins-ci.plugins + token-macro + 1.11 + + + + + com.jayway.restassured + rest-assured + 2.4.0 + test + + + + + org.jenkins-ci.plugins + email-ext + 2.38.2 + true + + org.jenkins-ci.modules instance-identity @@ -152,13 +174,6 @@ test - - com.jayway.restassured - rest-assured - 2.4.0 - test - - org.jenkins-ci.plugins.workflow workflow-job diff --git a/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java b/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java new file mode 100644 index 000000000..3397d37b1 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java @@ -0,0 +1,126 @@ +package org.jenkinsci.plugins.github.common; + +import com.cloudbees.jenkins.GitHubWebHook; +import hudson.Extension; +import hudson.Plugin; +import hudson.model.AbstractBuild; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.Messages; +import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; +import org.jenkinsci.plugins.tokenmacro.TokenMacro; +import org.kohsuke.stapler.DataBoundConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.trimToEmpty; + +/** + * Represents a message that can contain token macros. + * + * uses https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin to expand vars + * + * @author Kanstantsin Shautsou + * @author Alina Karpovich + * @since 1.14.1 + */ +public class ExpandableMessage extends AbstractDescribableImpl { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExpandableMessage.class); + + /** + * https://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin + */ + public static final String EMAIL_EXT_PLUGIN_ID = "email-ext"; + + private final String content; + + @DataBoundConstructor + public ExpandableMessage(String content) { + this.content = content; + } + + /** + * Expands all env vars. In case of AbstractBuild also expands token macro and build vars + * + * @param run build context + * @param listener usually used to log something to console while building env vars + * + * @return string with expanded vars and tokens + */ + public String expandAll(Run run, TaskListener listener) throws IOException, InterruptedException { + if (run instanceof AbstractBuild) { + try { + return TokenMacro.expandAll((AbstractBuild) run, listener, content, false, loadPrivateTokens()); + } catch (MacroEvaluationException e) { + LOGGER.error("Can't process token content {} in {} ({})", + content, run.getParent().getFullName(), e.getMessage()); + LOGGER.trace(e.getMessage(), e); + return content; + } + } else { + // fallback to env vars only because of token-macro allow only AbstractBuild in 1.11 + return run.getEnvironment(listener).expand(trimToEmpty(content)); + } + } + + public String getContent() { + return content; + } + + /** + * Macro list like groovy template (${SCRIPT, template=''}) + * More info about code + * https://wiki.jenkins-ci.org/display/JENKINS/Tips+for+optional+dependencies + * + * @return private macro list from email-ext or empty list if no such plugin installed + */ + @SuppressWarnings("unchecked") + private static List loadPrivateTokens() { + Plugin emailExt = plugin(EMAIL_EXT_PLUGIN_ID); + + if (emailExt != null) { + try { + return new ArrayList((Collection) find(emailExt, "hudson.plugins.emailext.plugins.ContentBuilder") + .getDeclaredMethod("getPrivateMacros") + .invoke(null)); + } catch (NoSuchMethodException | ClassNotFoundException e) { + LOGGER.error("Can't load class", e); + } catch (InvocationTargetException | IllegalAccessException e) { + LOGGER.error("Can't get private macro list from {}", EMAIL_EXT_PLUGIN_ID, e); + } + } + + return Collections.emptyList(); + } + + private static Plugin plugin(String id) { + return GitHubWebHook.getJenkinsInstance().getPlugin(id); + } + + private static Class find(Plugin plugin, String className) throws ClassNotFoundException { + return plugin.getWrapper().classLoader.loadClass(className); + } + + @Override + public DescriptorImpl getDescriptor() { + return (DescriptorImpl) super.getDescriptor(); + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return Messages.common_expandable_message_title(); + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties index 464b5c807..70644fe44 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties @@ -1,2 +1,3 @@ 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 +common.expandable.message.title=Expandable message diff --git a/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/config.groovy new file mode 100644 index 000000000..a135456d0 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.common.ExpandableMessage + +def f = namespace(lib.FormTagLib); + +f.entry(title: _('Content'), field: 'content') { + f.expandableTextbox() +} 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 new file mode 100644 index 000000000..9b690a5e6 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/common/ExpandableMessage/help-content.html @@ -0,0 +1,6 @@ +
+ Message content that will be expanded using core variable expansion i.e. ${WORKSPACE}
+ and Token Macro Plugin tokens.
+ If Email-ext plugin enabled, + then support expanding templates with ${SCRIPT, path="template.groovy"}. +
diff --git a/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java b/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java new file mode 100644 index 000000000..863489a0d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java @@ -0,0 +1,100 @@ +package org.jenkinsci.plugins.github.common; + +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.FreeStyleProject; +import hudson.model.ParametersAction; +import hudson.model.StringParameterValue; +import hudson.plugins.emailext.plugins.ContentBuilder; +import org.jenkinsci.plugins.github.test.WithoutPlugins; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static java.lang.String.format; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class ExpandableMessageTest { + + public static final String ENV_VAR_JOB_NAME = "JOB_NAME"; + public static final String CUSTOM_BUILD_PARAM = "FOO"; + public static final String CUSTOM_PARAM_VAL = "BAR"; + public static final String MSG_FORMAT = "%s - %s - %s"; + public static final String DEFAULT_EMAIL_EXT_TEMPLATE = "${SCRIPT, template=\"groovy-text.template\"}"; + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + @Test + public void shouldNotChangeSignatureOfGettingPrivateMacro() throws Exception { + assertThat("should be static method of email-ext plugin", ContentBuilder.getPrivateMacros(), notNullValue()); + } + + @Test + public void shouldExpandEnvAndBuildVars() throws Exception { + MessageExpander expander = new MessageExpander(new ExpandableMessage( + format(MSG_FORMAT, + asVar(ENV_VAR_JOB_NAME), + asVar(CUSTOM_BUILD_PARAM), + DEFAULT_EMAIL_EXT_TEMPLATE + ) + )); + + FreeStyleProject job = jRule.createFreeStyleProject(); + job.getBuildersList().add(expander); + + job.scheduleBuild2(0, new ParametersAction(new StringParameterValue(CUSTOM_BUILD_PARAM, CUSTOM_PARAM_VAL))) + .get(5, TimeUnit.SECONDS); + + assertThat("job name - var param - template", expander.getResult(), + startsWith(format(MSG_FORMAT, job.getFullName(), CUSTOM_PARAM_VAL, "GENERAL INFO\n\nBUILD"))); + } + + @Test + @WithoutPlugins + public void shouldNotFailWithDisabledEmailExt() throws Exception { + MessageExpander expander = new MessageExpander(new ExpandableMessage(DEFAULT_EMAIL_EXT_TEMPLATE)); + + FreeStyleProject job = jRule.createFreeStyleProject(); + job.getBuildersList().add(expander); + jRule.buildAndAssertSuccess(job); + + assertThat("should not change", expander.getResult(), is(DEFAULT_EMAIL_EXT_TEMPLATE)); + } + + + public static String asVar(String name) { + return format("${%s}", name); + } + + private static class MessageExpander extends TestBuilder { + private ExpandableMessage message; + private String result; + + public MessageExpander(ExpandableMessage message) { + this.message = message; + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) + throws InterruptedException, IOException { + result = message.expandAll(build, listener); + return true; + } + + public String getResult() { + return result; + } + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java b/src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java new file mode 100644 index 000000000..ace237c69 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java @@ -0,0 +1,29 @@ +package org.jenkinsci.plugins.github.test; + +import hudson.LocalPluginManager; +import org.jvnet.hudson.test.JenkinsRecipe; +import org.jvnet.hudson.test.JenkinsRule; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author lanwen (Merkushev Kirill) + */ +@Documented +@JenkinsRecipe(WithoutPlugins.RuleRunnerImpl.class) +@Target(METHOD) +@Retention(RUNTIME) +public @interface WithoutPlugins { + class RuleRunnerImpl extends JenkinsRecipe.Runner { + + @Override + public void setup(JenkinsRule jenkinsRule, WithoutPlugins recipe) throws Exception { + jenkinsRule.setPluginManager(new LocalPluginManager(jenkinsRule.getWebAppRoot())); + } + } +} From b6fb807f85dea65953a974b4131ac627d653348a Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Thu, 19 Nov 2015 00:25:53 +0300 Subject: [PATCH 124/228] context name field in GitHubProjectProperty to use in status builder and publisher --- .../plugins/github/GithubProjectProperty.java | 43 +++++++++++++++---- .../GithubProjectProperty/config.groovy | 17 ++++++++ .../github/GithubProjectProperty/config.jelly | 5 --- .../GithubProjectProperty/config.properties | 2 + .../config_de.properties | 2 + .../help-statusContext.html | 11 +++++ 6 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy delete mode 100644 src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly create mode 100644 src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java index 4a4a12faf..2ba347b06 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java @@ -9,8 +9,10 @@ import jenkins.model.ParameterizedJobMixIn; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.CheckForNull; import java.util.Collection; import java.util.Collections; import java.util.logging.Logger; @@ -18,7 +20,8 @@ /** * Stores the github related project properties. *

- * As of now this is only the URL to the github project. + * - URL to the GitHub project + * - Build status context name * * @author Stefan Saasen */ @@ -29,6 +32,15 @@ public final class GithubProjectProperty extends JobProperty> { */ private String projectUrl; + /** + * GitHub build status context name to use in commit status api + * {@linkplain "https://developer.github.com/v3/repos/statuses/"} + * + * @see com.cloudbees.jenkins.GitHubCommitNotifier + * @see com.cloudbees.jenkins.GitHubSetCommitStatusBuilder + */ + private String statusContext; + @DataBoundConstructor public GithubProjectProperty(String projectUrlStr) { this.projectUrl = new GithubUrl(projectUrlStr).baseUrl(); @@ -51,6 +63,16 @@ public GithubUrl getProjectUrl() { return new GithubUrl(projectUrl); } + @CheckForNull + public String getStatusContext() { + return statusContext; + } + + @DataBoundSetter + public void setStatusContext(String statusContext) { + this.statusContext = statusContext; + } + @Override public Collection getJobActions(Job job) { if (null != projectUrl) { @@ -61,11 +83,11 @@ public Collection getJobActions(Job job) { @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { - - public DescriptorImpl() { - super(GithubProjectProperty.class); - load(); - } + /** + * Used to hide property configuration under checkbox, + * as of not each job is GitHub project + */ + public static final String GITHUB_PROJECT_BLOCK_NAME = "githubProject"; public boolean isApplicable(Class jobType) { return ParameterizedJobMixIn.ParameterizedJob.class.isAssignableFrom(jobType); @@ -77,16 +99,21 @@ public String getDisplayName() { @Override public JobProperty newInstance(StaplerRequest req, JSONObject formData) throws FormException { - GithubProjectProperty tpp = req.bindJSON(GithubProjectProperty.class, formData); + GithubProjectProperty tpp = req.bindJSON( + GithubProjectProperty.class, + formData.getJSONObject(GITHUB_PROJECT_BLOCK_NAME) + ); if (tpp == null) { LOGGER.fine("Couldn't bind JSON"); return null; } + if (tpp.projectUrl == null) { - tpp = null; // not configured LOGGER.fine("projectUrl not found, nullifying GithubProjectProperty"); + return null; } + return tpp; } diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy new file mode 100644 index 000000000..488c6be24 --- /dev/null +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy @@ -0,0 +1,17 @@ +package com.coravy.hudson.plugins.github.GithubProjectProperty + +import static com.coravy.hudson.plugins.github.GithubProjectProperty.DescriptorImpl.GITHUB_PROJECT_BLOCK_NAME + +def f = namespace(lib.FormTagLib); + +f.optionalBlock(name: GITHUB_PROJECT_BLOCK_NAME, title: _('github.project'), checked: instance != null) { + f.entry(field: 'projectUrlStr', title: _('github.project.url')) { + f.textbox() + } + + f.advanced() { + f.entry(title: _('github.build.status.context.for.commits'), field: 'statusContext') { + f.textbox() + } + } +} diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly deleted file mode 100644 index c7eb0111d..000000000 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.jelly +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties index 0802fd58e..176f32934 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties @@ -1 +1,3 @@ github.project=GitHub project +github.project.url=Project url +github.build.status.context.for.commits=Commits status context diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties index dc68c8312..8de2c02f1 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties @@ -1 +1,3 @@ github.project=GitHub-Projekt +github.project.url=Project url +github.build.status.context.for.commits=Commits status context diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html new file mode 100644 index 000000000..9d22766f1 --- /dev/null +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html @@ -0,0 +1,11 @@ +

+

+ This value will be used as context name for + commit status if status builder or + status publisher is defined for this project. +

+ +

+ If you leave it empty, job name will be used for builder and publisher. +

+
\ No newline at end of file From 5dfb7804f9b10b9cf23d036cf3b61c5aeb8f4aa2 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Tue, 24 Nov 2015 00:43:30 +0300 Subject: [PATCH 125/228] remove email-ext private token support as of it internal api and may change --- pom.xml | 26 ++++----- .../github/common/ExpandableMessage.java | 53 +++---------------- .../ExpandableMessage/help-content.html | 2 - .../github/common/ExpandableMessageTest.java | 32 +++-------- 4 files changed, 23 insertions(+), 90 deletions(-) diff --git a/pom.xml b/pom.xml index 623231063..eebaa6185 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -129,22 +130,6 @@ 1.11
- - - com.jayway.restassured - rest-assured - 2.4.0 - test - - - - - org.jenkins-ci.plugins - email-ext - 2.38.2 - true - - org.jenkins-ci.modules instance-identity @@ -153,6 +138,13 @@ + + com.jayway.restassured + rest-assured + 2.4.0 + test + + org.hamcrest hamcrest-all diff --git a/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java b/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java index 3397d37b1..99de936c8 100644 --- a/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java +++ b/src/main/java/org/jenkinsci/plugins/github/common/ExpandableMessage.java @@ -1,8 +1,6 @@ package org.jenkinsci.plugins.github.common; -import com.cloudbees.jenkins.GitHubWebHook; import hudson.Extension; -import hudson.Plugin; import hudson.model.AbstractBuild; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; @@ -16,11 +14,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.List; import static org.apache.commons.lang3.StringUtils.trimToEmpty; @@ -37,11 +31,6 @@ public class ExpandableMessage extends AbstractDescribableImpl run, TaskListener listener) throws IOException, InterruptedException { if (run instanceof AbstractBuild) { try { - return TokenMacro.expandAll((AbstractBuild) run, listener, content, false, loadPrivateTokens()); + return TokenMacro.expandAll( + (AbstractBuild) run, + listener, + content, + false, + Collections.emptyList() + ); } catch (MacroEvaluationException e) { LOGGER.error("Can't process token content {} in {} ({})", content, run.getParent().getFullName(), e.getMessage()); @@ -77,40 +72,6 @@ public String getContent() { return content; } - /** - * Macro list like groovy template (${SCRIPT, template=''}) - * More info about code - * https://wiki.jenkins-ci.org/display/JENKINS/Tips+for+optional+dependencies - * - * @return private macro list from email-ext or empty list if no such plugin installed - */ - @SuppressWarnings("unchecked") - private static List loadPrivateTokens() { - Plugin emailExt = plugin(EMAIL_EXT_PLUGIN_ID); - - if (emailExt != null) { - try { - return new ArrayList((Collection) find(emailExt, "hudson.plugins.emailext.plugins.ContentBuilder") - .getDeclaredMethod("getPrivateMacros") - .invoke(null)); - } catch (NoSuchMethodException | ClassNotFoundException e) { - LOGGER.error("Can't load class", e); - } catch (InvocationTargetException | IllegalAccessException e) { - LOGGER.error("Can't get private macro list from {}", EMAIL_EXT_PLUGIN_ID, e); - } - } - - return Collections.emptyList(); - } - - private static Plugin plugin(String id) { - return GitHubWebHook.getJenkinsInstance().getPlugin(id); - } - - private static Class find(Plugin plugin, String className) throws ClassNotFoundException { - return plugin.getWrapper().classLoader.loadClass(className); - } - @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); 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 9b690a5e6..e90cbd68f 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,6 +1,4 @@
Message content that will be expanded using core variable expansion i.e. ${WORKSPACE}
and Token Macro Plugin tokens.
- If Email-ext plugin enabled, - then support expanding templates with ${SCRIPT, path="template.groovy"}.
diff --git a/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java b/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java index 863489a0d..b99f7b2dd 100644 --- a/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java @@ -6,8 +6,6 @@ import hudson.model.FreeStyleProject; import hudson.model.ParametersAction; import hudson.model.StringParameterValue; -import hudson.plugins.emailext.plugins.ContentBuilder; -import org.jenkinsci.plugins.github.test.WithoutPlugins; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -18,8 +16,6 @@ import static java.lang.String.format; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; /** @@ -31,23 +27,18 @@ public class ExpandableMessageTest { public static final String CUSTOM_BUILD_PARAM = "FOO"; public static final String CUSTOM_PARAM_VAL = "BAR"; public static final String MSG_FORMAT = "%s - %s - %s"; - public static final String DEFAULT_EMAIL_EXT_TEMPLATE = "${SCRIPT, template=\"groovy-text.template\"}"; + public static final String DEFAULT_TOKEN_TEMPLATE = "${ENV, var=\"%s\"}"; @Rule public JenkinsRule jRule = new JenkinsRule(); - @Test - public void shouldNotChangeSignatureOfGettingPrivateMacro() throws Exception { - assertThat("should be static method of email-ext plugin", ContentBuilder.getPrivateMacros(), notNullValue()); - } - @Test public void shouldExpandEnvAndBuildVars() throws Exception { MessageExpander expander = new MessageExpander(new ExpandableMessage( format(MSG_FORMAT, asVar(ENV_VAR_JOB_NAME), asVar(CUSTOM_BUILD_PARAM), - DEFAULT_EMAIL_EXT_TEMPLATE + asTokenVar(ENV_VAR_JOB_NAME) ) )); @@ -58,26 +49,17 @@ public void shouldExpandEnvAndBuildVars() throws Exception { .get(5, TimeUnit.SECONDS); assertThat("job name - var param - template", expander.getResult(), - startsWith(format(MSG_FORMAT, job.getFullName(), CUSTOM_PARAM_VAL, "GENERAL INFO\n\nBUILD"))); + startsWith(format(MSG_FORMAT, job.getFullName(), CUSTOM_PARAM_VAL, job.getFullName()))); } - @Test - @WithoutPlugins - public void shouldNotFailWithDisabledEmailExt() throws Exception { - MessageExpander expander = new MessageExpander(new ExpandableMessage(DEFAULT_EMAIL_EXT_TEMPLATE)); - - FreeStyleProject job = jRule.createFreeStyleProject(); - job.getBuildersList().add(expander); - jRule.buildAndAssertSuccess(job); - - assertThat("should not change", expander.getResult(), is(DEFAULT_EMAIL_EXT_TEMPLATE)); - } - - public static String asVar(String name) { return format("${%s}", name); } + public static String asTokenVar(String name) { + return format(DEFAULT_TOKEN_TEMPLATE, name); + } + private static class MessageExpander extends TestBuilder { private ExpandableMessage message; private String result; From 9f01fd048b01f39c9c0ea8c71817d8fc9b22a08c Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 4 Dec 2015 01:14:40 +0300 Subject: [PATCH 126/228] add custom status message and predefined context to GHCommitNotifier --- .../jenkins/GitHubCommitNotifier.java | 132 ++++++++++++------ .../plugins/github/GithubProjectProperty.java | 29 +++- .../GitHubCommitNotifier/config.groovy | 20 +++ .../jenkins/GitHubCommitNotifier/config.jelly | 7 - .../com/cloudbees/jenkins/Messages.properties | 1 + .../GithubProjectProperty/config.groovy | 2 +- .../GithubProjectProperty/config.properties | 2 +- .../config_de.properties | 2 +- ...atusContext.html => help-displayName.html} | 2 +- 9 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.groovy delete mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.jelly rename src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/{help-statusContext.html => help-displayName.html} (97%) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index eab6878bd..337439216 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -13,19 +13,27 @@ import hudson.tasks.Publisher; import hudson.util.ListBoxModel; import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.util.BuildDataHelper; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHRepository; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import javax.annotation.Nonnull; import java.io.IOException; +import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_DisplayName; import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; +import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; import static hudson.model.Result.FAILURE; import static hudson.model.Result.SUCCESS; import static hudson.model.Result.UNSTABLE; import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; /** * Create commit status notifications on the commits based on the outcome of the build. @@ -34,9 +42,16 @@ */ public class GitHubCommitNotifier extends Notifier { + private ExpandableMessage statusMessage = new ExpandableMessage(""); + private final String resultOnFailure; private static final Result[] SUPPORTED_RESULTS = {FAILURE, UNSTABLE, SUCCESS}; + @Restricted(NoExternalUse.class) + public GitHubCommitNotifier() { + this(getDefaultResultOnFailure().toString()); + } + /** * @since 1.10 */ @@ -45,9 +60,13 @@ public GitHubCommitNotifier(String resultOnFailure) { this.resultOnFailure = resultOnFailure; } - @Deprecated - public GitHubCommitNotifier() { - this(getDefaultResultOnFailure().toString()); + public ExpandableMessage getStatusMessage() { + return statusMessage; + } + + @DataBoundSetter + public void setStatusMessage(ExpandableMessage statusMessage) { + this.statusMessage = statusMessage; } /** @@ -60,22 +79,12 @@ public String getResultOnFailure() { @Nonnull public static Result getDefaultResultOnFailure() { - return SUPPORTED_RESULTS[0]; + return FAILURE; } - @Nonnull /*package*/ Result getEffectiveResultOnFailure() { - if (resultOnFailure == null) { - return getDefaultResultOnFailure(); - } - - for (Result result : SUPPORTED_RESULTS) { - if (result.toString().equals(resultOnFailure)) { - return result; - } - } - return getDefaultResultOnFailure(); + return Result.fromString(trimToEmpty(resultOnFailure)); } public BuildStepMonitor getRequiredMonitorService() { @@ -91,16 +100,14 @@ public boolean perform(AbstractBuild build, return true; } catch (IOException error) { final Result buildResult = getEffectiveResultOnFailure(); - if (buildResult.equals(Result.FAILURE)) { + if (buildResult.equals(FAILURE)) { throw error; } else { listener.error(format("[GitHub Commit Notifier] - %s", error.getMessage())); - if (buildResult.isWorseThan(build.getResult())) { - listener.getLogger().println( - format("[GitHub Commit Notifier] - Build result will be set to %s", buildResult) - ); - build.setResult(buildResult); - } + listener.getLogger().println( + format("[GitHub Commit Notifier] - Build result will be set to %s", buildResult) + ); + build.setResult(buildResult); } } return true; @@ -109,38 +116,74 @@ public boolean perform(AbstractBuild build, private void updateCommitStatus(@Nonnull AbstractBuild build, @Nonnull BuildListener listener) throws InterruptedException, IOException { final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); + + StatusResult status = statusFrom(build); + String message = defaultIfEmpty(statusMessage.expandAll(build, listener), status.getMsg()); + String contextName = displayNameFor(build.getProject()); + for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { for (GHRepository repository : name.resolve()) { - GHCommitState state; - String msg; - - // We do not use `build.getDurationString()` because it appends 'and counting' (build is still running) - final String duration = Util.getTimeSpanString(System.currentTimeMillis() - build.getTimeInMillis()); - - Result result = build.getResult(); - if (result == null) { // Build is ongoing - state = GHCommitState.PENDING; - msg = Messages.CommitNotifier_Pending(build.getDisplayName()); - } else if (result.isBetterOrEqualTo(SUCCESS)) { - state = GHCommitState.SUCCESS; - msg = Messages.CommitNotifier_Success(build.getDisplayName(), duration); - } else if (result.isBetterOrEqualTo(UNSTABLE)) { - state = GHCommitState.FAILURE; - msg = Messages.CommitNotifier_Unstable(build.getDisplayName(), duration); - } else { - state = GHCommitState.ERROR; - msg = Messages.CommitNotifier_Failed(build.getDisplayName(), duration); - } listener.getLogger().println( GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1) ); + repository.createCommitStatus( - sha1, state, build.getAbsoluteUrl(), msg, build.getProject().getFullName()); + sha1, status.getState(), build.getAbsoluteUrl(), + message, + contextName + ); } } } + private static StatusResult statusFrom(@Nonnull AbstractBuild build) { + Result result = build.getResult(); + + // We do not use `build.getDurationString()` because it appends 'and counting' (build is still running) + String duration = Util.getTimeSpanString(System.currentTimeMillis() - build.getTimeInMillis()); + + if (result == null) { // Build is ongoing + return new StatusResult( + GHCommitState.PENDING, + Messages.CommitNotifier_Pending(build.getDisplayName()) + ); + } else if (result.isBetterOrEqualTo(SUCCESS)) { + return new StatusResult( + GHCommitState.SUCCESS, + Messages.CommitNotifier_Success(build.getDisplayName(), duration) + ); + } else if (result.isBetterOrEqualTo(UNSTABLE)) { + return new StatusResult( + GHCommitState.FAILURE, + Messages.CommitNotifier_Unstable(build.getDisplayName(), duration) + ); + } else { + return new StatusResult( + GHCommitState.ERROR, + Messages.CommitNotifier_Failed(build.getDisplayName(), duration) + ); + } + } + + private static class StatusResult { + private GHCommitState state; + private String msg; + + public StatusResult(GHCommitState state, String msg) { + this.state = state; + this.msg = msg; + } + + public GHCommitState getState() { + return state; + } + + public String getMsg() { + return msg; + } + } + @Extension public static class DescriptorImpl extends BuildStepDescriptor { @@ -149,7 +192,7 @@ public boolean isApplicable(Class aClass) { } public String getDisplayName() { - return "Set build status on GitHub commit"; + return GitHubCommitNotifier_DisplayName(); } public ListBoxModel doFillResultOnFailureItems() { @@ -160,5 +203,4 @@ public ListBoxModel doFillResultOnFailureItems() { return items; } } - } diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java index 2ba347b06..9e9808ee4 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java @@ -13,10 +13,13 @@ import org.kohsuke.stapler.StaplerRequest; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import java.util.Collection; import java.util.Collections; import java.util.logging.Logger; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /** * Stores the github related project properties. *

@@ -39,7 +42,7 @@ public final class GithubProjectProperty extends JobProperty> { * @see com.cloudbees.jenkins.GitHubCommitNotifier * @see com.cloudbees.jenkins.GitHubSetCommitStatusBuilder */ - private String statusContext; + private String displayName; @DataBoundConstructor public GithubProjectProperty(String projectUrlStr) { @@ -64,13 +67,13 @@ public GithubUrl getProjectUrl() { } @CheckForNull - public String getStatusContext() { - return statusContext; + public String getDisplayName() { + return displayName; } @DataBoundSetter - public void setStatusContext(String statusContext) { - this.statusContext = statusContext; + public void setDisplayName(String displayName) { + this.displayName = displayName; } @Override @@ -81,6 +84,22 @@ public Collection getJobActions(Job job) { return Collections.emptyList(); } + /** + * Extracts value of display name from given job, or just returns full name if field or prop is not defined + * + * @param job project which wants to get current context name to use in GH status API + * + * @return display name or full job name if field is not defined + */ + public static String displayNameFor(@Nonnull Job job) { + GithubProjectProperty ghProp = job.getProperty(GithubProjectProperty.class); + if (ghProp != null && isNotBlank(ghProp.getDisplayName())) { + return ghProp.getDisplayName(); + } + + return job.getFullName(); + } + @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { /** diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.groovy b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.groovy new file mode 100644 index 000000000..531b3e5e9 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.groovy @@ -0,0 +1,20 @@ +package com.cloudbees.jenkins.GitHubCommitNotifier + +import com.cloudbees.jenkins.GitHubCommitNotifier + +def f = namespace(lib.FormTagLib); + +// prepare default instance +if (instance == null) { + instance = new GitHubCommitNotifier() +} + +f.advanced() { + f.entry(title: _('Build status message'), field: 'statusMessage') { + f.property() + } + + f.entry(title: _('Result on failure'), field: 'resultOnFailure') { + f.select() + } +} diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.jelly b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.jelly deleted file mode 100644 index 8f94eb80a..000000000 --- a/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/config.jelly +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/Messages.properties b/src/main/resources/com/cloudbees/jenkins/Messages.properties index 2721d35d3..48fa52b9f 100644 --- a/src/main/resources/com/cloudbees/jenkins/Messages.properties +++ b/src/main/resources/com/cloudbees/jenkins/Messages.properties @@ -3,3 +3,4 @@ CommitNotifier.Unstable=Build {0} found unstable in {1} CommitNotifier.Failed=Build {0} failed in {1} CommitNotifier.Pending=Build {0} in progress... GitHubCommitNotifier.SettingCommitStatus=Setting commit status on GitHub for {0} +GitHubCommitNotifier.DisplayName=Set build status on GitHub commit diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy index 488c6be24..93944fbc4 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.groovy @@ -10,7 +10,7 @@ f.optionalBlock(name: GITHUB_PROJECT_BLOCK_NAME, title: _('github.project'), che } f.advanced() { - f.entry(title: _('github.build.status.context.for.commits'), field: 'statusContext') { + f.entry(title: _('github.build.display.name'), field: 'displayName') { f.textbox() } } diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties index 176f32934..48720853b 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config.properties @@ -1,3 +1,3 @@ github.project=GitHub project github.project.url=Project url -github.build.status.context.for.commits=Commits status context +github.build.display.name=Display name diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties index 8de2c02f1..09ac5164b 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/config_de.properties @@ -1,3 +1,3 @@ github.project=GitHub-Projekt github.project.url=Project url -github.build.status.context.for.commits=Commits status context +github.build.display.name=Display name diff --git a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html similarity index 97% rename from src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html rename to src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html index 9d22766f1..1446b1ded 100644 --- a/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-statusContext.html +++ b/src/main/resources/com/coravy/hudson/plugins/github/GithubProjectProperty/help-displayName.html @@ -8,4 +8,4 @@

If you leave it empty, job name will be used for builder and publisher.

- \ No newline at end of file + From 0d4c02b8128eb6a043e4f393f33abea818d81a63 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 13 Dec 2015 23:26:52 +0300 Subject: [PATCH 127/228] add custom status message and predefined context to GHCommit Status Builder --- .../jenkins/GitHubSetCommitStatusBuilder.java | 27 ++++++++- .../config.groovy | 16 ++++++ .../com/cloudbees/jenkins/Messages.properties | 1 + .../help-displayName.html | 2 +- .../jenkins/GitHubCommitNotifierTest.java | 55 ++++++++++--------- .../GitHubSetCommitStatusBuilderTest.java | 35 +++++------- 6 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 927a40885..0f4e87d66 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -8,26 +8,47 @@ import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.util.BuildDataHelper; import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHRepository; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import java.io.IOException; import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; +import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; +import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; @Extension public class GitHubSetCommitStatusBuilder extends Builder { + private ExpandableMessage statusMessage = new ExpandableMessage(""); + @DataBoundConstructor public GitHubSetCommitStatusBuilder() { } + public ExpandableMessage getStatusMessage() { + return statusMessage; + } + + @DataBoundSetter + public void setStatusMessage(ExpandableMessage statusMessage) { + this.statusMessage = statusMessage; + } + @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); + String message = defaultIfEmpty( + statusMessage.expandAll(build, listener), + Messages.CommitNotifier_Pending(build.getDisplayName()) + ); + String contextName = displayNameFor(build.getProject()); + for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { for (GHRepository repository : name.resolve()) { listener.getLogger().println( @@ -36,8 +57,8 @@ public boolean perform(AbstractBuild build, repository.createCommitStatus(sha1, GHCommitState.PENDING, build.getAbsoluteUrl(), - Messages.CommitNotifier_Pending(build.getDisplayName()), - build.getProject().getFullName()); + message, + contextName); } } return true; @@ -52,7 +73,7 @@ public boolean isApplicable(Class jobType) { @Override public String getDisplayName() { - return "Set build status to \"pending\" on GitHub commit"; + return Messages.GitHubSetCommitStatusBuilder_DisplayName(); } } } diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy b/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy new file mode 100644 index 000000000..297388577 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder/config.groovy @@ -0,0 +1,16 @@ +package com.cloudbees.jenkins.GitHubSetCommitStatusBuilder + +import com.cloudbees.jenkins.GitHubSetCommitStatusBuilder + +def f = namespace(lib.FormTagLib); + +// prepare default instance +if (instance == null) { + instance = new GitHubSetCommitStatusBuilder() +} + +f.advanced() { + f.entry(title: _('Build status message'), field: 'statusMessage') { + f.property() + } +} diff --git a/src/main/resources/com/cloudbees/jenkins/Messages.properties b/src/main/resources/com/cloudbees/jenkins/Messages.properties index 48fa52b9f..da9c395e8 100644 --- a/src/main/resources/com/cloudbees/jenkins/Messages.properties +++ b/src/main/resources/com/cloudbees/jenkins/Messages.properties @@ -4,3 +4,4 @@ CommitNotifier.Failed=Build {0} failed in {1} CommitNotifier.Pending=Build {0} in progress... GitHubCommitNotifier.SettingCommitStatus=Setting commit status on GitHub for {0} GitHubCommitNotifier.DisplayName=Set build status on GitHub commit +GitHubSetCommitStatusBuilder.DisplayName=Set build status to "pending" on GitHub commit 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 1446b1ded..9b5def6e0 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 @@ -2,7 +2,7 @@

This value will be used as context name for commit status if status builder or - status publisher is defined for this project. + status publisher is defined for this project. It should be small and clear.

diff --git a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java index da0892369..0c0aafa11 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java @@ -12,50 +12,55 @@ import hudson.plugins.git.GitSCM; import org.junit.Rule; import org.junit.Test; -import org.jvnet.hudson.test.Bug; -import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; /** * Tests for {@link GitHubCommitNotifier}. + * * @author Oleg Nenashev */ -public class GitHubCommitNotifierTest extends HudsonTestCase { - +public class GitHubCommitNotifierTest { + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + @Test - @Bug(23641) - public void testNoBuildData() throws Exception, InterruptedException { - FreeStyleProject prj = createFreeStyleProject("23641_noBuildData"); + @Issue("JENKINS-23641") + public void testNoBuildData() throws Exception { + FreeStyleProject prj = jRule.createFreeStyleProject("23641_noBuildData"); prj.getPublishersList().add(new GitHubCommitNotifier()); Build b = prj.scheduleBuild2(0).get(); - assertBuildStatus(Result.FAILURE, b); - assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); + jRule.assertBuildStatus(Result.FAILURE, b); + jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); } - + @Test - @Bug(23641) - public void testNoBuildRevision() throws Exception, InterruptedException { - FreeStyleProject prj = createFreeStyleProject(); + @Issue("JENKINS-23641") + public void testNoBuildRevision() throws Exception { + FreeStyleProject prj = jRule.createFreeStyleProject(); prj.setScm(new GitSCM("http://non.existent.git.repo.nowhere/repo.git")); prj.getPublishersList().add(new GitHubCommitNotifier()); Build b = prj.scheduleBuild2(0).get(); - assertBuildStatus(Result.FAILURE, b); - assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoLastRevisionError(), b); + jRule.assertBuildStatus(Result.FAILURE, b); + jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoLastRevisionError(), b); } - - @Bug(25312) - public @Test void testMarkUnstableOnCommitNotifierFailure() throws Exception, InterruptedException { - FreeStyleProject prj = createFreeStyleProject(); + + @Test + @Issue("JENKINS-25312") + public void testMarkUnstableOnCommitNotifierFailure() throws Exception { + FreeStyleProject prj = jRule.createFreeStyleProject(); prj.getPublishersList().add(new GitHubCommitNotifier(Result.UNSTABLE.toString())); Build b = prj.scheduleBuild2(0).get(); - assertBuildStatus(Result.UNSTABLE, b); + jRule.assertBuildStatus(Result.UNSTABLE, b); } - - @Bug(25312) - public @Test void testMarkSuccessOnCommitNotifierFailure() throws Exception, InterruptedException { - FreeStyleProject prj = createFreeStyleProject(); + + @Test + @Issue("JENKINS-25312") + public void testMarkSuccessOnCommitNotifierFailure() throws Exception { + FreeStyleProject prj = jRule.createFreeStyleProject(); prj.getPublishersList().add(new GitHubCommitNotifier(Result.SUCCESS.toString())); Build b = prj.scheduleBuild2(0).get(); - assertBuildStatus(Result.SUCCESS, b); + jRule.assertBuildStatus(Result.SUCCESS, b); } } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java index dae5eb1df..028e09112 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java @@ -3,35 +3,28 @@ import hudson.model.Build; import hudson.model.FreeStyleProject; import hudson.model.Result; -import hudson.plugins.git.GitSCM; -import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; -import org.jvnet.hudson.test.Bug; -import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; /** * Tests for {@link GitHubSetCommitStatusBuilder}. + * * @author Oleg Nenashev */ -public class GitHubSetCommitStatusBuilderTest extends HudsonTestCase { - +public class GitHubSetCommitStatusBuilderTest { + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + @Test - public void testNoBuildData() throws Exception, InterruptedException { - FreeStyleProject prj = createFreeStyleProject("23641_noBuildData"); + @Issue("JENKINS-23641") + public void testNoBuildData() throws Exception { + FreeStyleProject prj = jRule.createFreeStyleProject("23641_noBuildData"); prj.getBuildersList().add(new GitHubSetCommitStatusBuilder()); Build b = prj.scheduleBuild2(0).get(); - assertBuildStatus(Result.FAILURE, b); - assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); + jRule.assertBuildStatus(Result.FAILURE, b); + jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); } - - // TODO: test fails due to the fatal server communication attempt - /* @Test - public void testNoBuildRevision() throws Exception, InterruptedException { - FreeStyleProject prj = createFreeStyleProject(); - prj.setScm(new GitSCM("http://non.existent.git.repo.nowhere/repo.git")); - prj.getBuildersList().add(new GitHubSetCommitStatusBuilder()); - Build b = prj.scheduleBuild2(0).get(); - assertBuildStatus(Result.FAILURE, b); - assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoLastRevisionError(), b); - } */ } From a8c28c42936ef98ca5d2b88151e420f6d1530e21 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 14 Dec 2015 14:10:38 +0300 Subject: [PATCH 128/228] add since javadoc for new methods --- .../jenkins/GitHubCommitNotifier.java | 6 ++++ .../jenkins/GitHubSetCommitStatusBuilder.java | 6 ++++ .../plugins/github/GithubProjectProperty.java | 8 +++++ .../plugins/github/test/WithoutPlugins.java | 29 ------------------- 4 files changed, 20 insertions(+), 29 deletions(-) delete mode 100644 src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index 337439216..4900bc637 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -60,10 +60,16 @@ public GitHubCommitNotifier(String resultOnFailure) { this.resultOnFailure = resultOnFailure; } + /** + * @since 1.14.1 + */ public ExpandableMessage getStatusMessage() { return statusMessage; } + /** + * @since 1.14.1 + */ @DataBoundSetter public void setStatusMessage(ExpandableMessage statusMessage) { this.statusMessage = statusMessage; diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 0f4e87d66..54245da62 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -29,10 +29,16 @@ public class GitHubSetCommitStatusBuilder extends Builder { public GitHubSetCommitStatusBuilder() { } + /** + * @since 1.14.1 + */ public ExpandableMessage getStatusMessage() { return statusMessage; } + /** + * @since 1.14.1 + */ @DataBoundSetter public void setStatusMessage(ExpandableMessage statusMessage) { this.statusMessage = statusMessage; diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java index 9e9808ee4..f551fb5b9 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java @@ -66,11 +66,18 @@ public GithubUrl getProjectUrl() { return new GithubUrl(projectUrl); } + /** + * @see #displayName + * @since 1.14.1 + */ @CheckForNull public String getDisplayName() { return displayName; } + /** + * @since 1.14.1 + */ @DataBoundSetter public void setDisplayName(String displayName) { this.displayName = displayName; @@ -90,6 +97,7 @@ public Collection getJobActions(Job job) { * @param job project which wants to get current context name to use in GH status API * * @return display name or full job name if field is not defined + * @since 1.14.1 */ public static String displayNameFor(@Nonnull Job job) { GithubProjectProperty ghProp = job.getProperty(GithubProjectProperty.class); diff --git a/src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java b/src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java deleted file mode 100644 index ace237c69..000000000 --- a/src/test/java/org/jenkinsci/plugins/github/test/WithoutPlugins.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.jenkinsci.plugins.github.test; - -import hudson.LocalPluginManager; -import org.jvnet.hudson.test.JenkinsRecipe; -import org.jvnet.hudson.test.JenkinsRule; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * @author lanwen (Merkushev Kirill) - */ -@Documented -@JenkinsRecipe(WithoutPlugins.RuleRunnerImpl.class) -@Target(METHOD) -@Retention(RUNTIME) -public @interface WithoutPlugins { - class RuleRunnerImpl extends JenkinsRecipe.Runner { - - @Override - public void setup(JenkinsRule jenkinsRule, WithoutPlugins recipe) throws Exception { - jenkinsRule.setPluginManager(new LocalPluginManager(jenkinsRule.getWebAppRoot())); - } - } -} From 1ad08375d82f34fc4e794cf8ce8c7cce12570289 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Tue, 15 Dec 2015 22:33:38 +0300 Subject: [PATCH 129/228] [maven-release-plugin] prepare release github-1.14.1 --- pom.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 3ea0ac1d1..e87c158fd 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -11,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.1-SNAPSHOT + 1.14.1 hpi GitHub plugin @@ -39,7 +38,7 @@ 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 + github-1.14.1 From c8af6ec5af5fdebdd36ec6cb4d13875cbdf87437 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Tue, 15 Dec 2015 22:33:44 +0300 Subject: [PATCH 130/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e87c158fd..86981c94d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.1 + 1.14.2-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.14.1 + HEAD From 57d7870f7a170f9b7ae772b3ccc107554cbf6269 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 21 Dec 2015 00:58:57 +0300 Subject: [PATCH 131/228] [FIXES JENKINS-32132] Check status message is null (in case of wrong deserialization) --- .../java/com/cloudbees/jenkins/GitHubCommitNotifier.java | 7 +++++-- .../cloudbees/jenkins/GitHubSetCommitStatusBuilder.java | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index 4900bc637..bb8cb7cf2 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -28,6 +28,7 @@ import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_DisplayName; import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; +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; @@ -41,8 +42,9 @@ * @author Nicolas De Loof */ public class GitHubCommitNotifier extends Notifier { + private static final ExpandableMessage DEFAULT_MESSAGE = new ExpandableMessage(""); - private ExpandableMessage statusMessage = new ExpandableMessage(""); + private ExpandableMessage statusMessage = DEFAULT_MESSAGE; private final String resultOnFailure; private static final Result[] SUPPORTED_RESULTS = {FAILURE, UNSTABLE, SUCCESS}; @@ -124,7 +126,8 @@ private void updateCommitStatus(@Nonnull AbstractBuild build, final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); StatusResult status = statusFrom(build); - String message = defaultIfEmpty(statusMessage.expandAll(build, listener), status.getMsg()); + String message = defaultIfEmpty(firstNonNull(statusMessage, DEFAULT_MESSAGE) + .expandAll(build, listener), status.getMsg()); String contextName = displayNameFor(build.getProject()); for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 54245da62..0c151d05a 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -19,11 +19,14 @@ import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; +import static com.google.common.base.Objects.firstNonNull; import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; @Extension public class GitHubSetCommitStatusBuilder extends Builder { - private ExpandableMessage statusMessage = new ExpandableMessage(""); + private static final ExpandableMessage DEFAULT_MESSAGE = new ExpandableMessage(""); + + private ExpandableMessage statusMessage = DEFAULT_MESSAGE; @DataBoundConstructor public GitHubSetCommitStatusBuilder() { @@ -50,7 +53,7 @@ public boolean perform(AbstractBuild build, BuildListener listener) throws InterruptedException, IOException { final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); String message = defaultIfEmpty( - statusMessage.expandAll(build, listener), + firstNonNull(statusMessage, DEFAULT_MESSAGE).expandAll(build, listener), Messages.CommitNotifier_Pending(build.getDisplayName()) ); String contextName = displayNameFor(build.getProject()); From 6b783dfa0b332e8edb31f73a44c406be08c0867c Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 21 Dec 2015 01:14:05 +0300 Subject: [PATCH 132/228] [maven-release-plugin] prepare release github-1.14.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 86981c94d..19638f3be 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.2-SNAPSHOT + 1.14.2 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + github-1.14.2 From 4197fdf148077fa6448a88cb0dcc20f126a9a578 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 21 Dec 2015 01:14:10 +0300 Subject: [PATCH 133/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 19638f3be..ed65eac39 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.2 + 1.14.3-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.14.2 + HEAD From 4024d6249812fb76cf98fca4bb04098ff47e834e Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 21 Dec 2015 20:43:42 +0300 Subject: [PATCH 134/228] Test for JENKINS-32132 with rule to mock GH --- .../jenkins/GitHubCommitNotifierTest.java | 90 +++++++++- .../GitHubSetCommitStatusBuilderTest.java | 90 +++++++++- .../jenkins/GitHubWebHookFullTest.java | 15 +- .../GitHubClientCacheCleanupTest.java | 30 +--- .../plugins/github/test/GHMockRule.java | 165 ++++++++++++++++++ .../github/test/InjectJenkinsMembersRule.java | 39 +++++ .../shouldLoadNullStatusMessage/config.xml | 35 ++++ .../jobs/step/config.xml | 20 +++ src/test/resources/log4j.properties | 7 + .../github/test/GHMockRule/repos-repo.json | 89 ++++++++++ .../GHMockRule}/user.json | 0 11 files changed, 542 insertions(+), 38 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/config.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/jobs/step/config.xml create mode 100644 src/test/resources/log4j.properties create mode 100644 src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/repos-repo.json rename src/test/resources/org/jenkinsci/plugins/github/{internal/GitHubClientCacheCleanupTest => test/GHMockRule}/user.json (100%) diff --git a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java index 0c0aafa11..bcac993be 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java @@ -1,30 +1,82 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ - package com.cloudbees.jenkins; +import com.github.tomakehurst.wiremock.common.Slf4jNotifier; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import hudson.Launcher; +import hudson.model.AbstractBuild; import hudson.model.Build; +import hudson.model.BuildListener; import hudson.model.FreeStyleProject; import hudson.model.Result; import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.util.BuildData; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; +import org.jenkinsci.plugins.github.test.GHMockRule; +import org.jenkinsci.plugins.github.test.GHMockRule.FixedGHRepoNameTestContributor; +import org.jenkinsci.plugins.github.test.InjectJenkinsMembersRule; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.TestExtension; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.inject.Inject; + +import static com.cloudbees.jenkins.GitHubSetCommitStatusBuilderTest.SOME_SHA; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.mockito.Mockito.when; /** * Tests for {@link GitHubCommitNotifier}. * * @author Oleg Nenashev */ +@RunWith(MockitoJUnitRunner.class) public class GitHubCommitNotifierTest { - @Rule + @Mock + public BuildData data; + + @Mock + public Revision rev; + + @Inject + public GitHubPluginConfig config; + public JenkinsRule jRule = new JenkinsRule(); + @Rule + public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); + + @Rule + public GHMockRule github = new GHMockRule( + new WireMockRule( + wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)) + )) + .stubUser() + .stubRepo() + .stubStatuses(); + + + @Rule + public ExternalResource prep = new ExternalResource() { + @Override + protected void before() throws Throwable { + when(data.getLastBuiltRevision()).thenReturn(rev); + when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); + } + }; + @Test @Issue("JENKINS-23641") public void testNoBuildData() throws Exception { @@ -63,4 +115,28 @@ public void testMarkSuccessOnCommitNotifierFailure() throws Exception { Build b = prj.scheduleBuild2(0).get(); jRule.assertBuildStatus(Result.SUCCESS, b); } + + @Test + public void shouldWriteStatusOnGH() throws Exception { + config.getConfigs().add(github.serverConfig()); + FreeStyleProject prj = jRule.createFreeStyleProject(); + + prj.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { + build.addAction(data); + return true; + } + }); + + prj.getPublishersList().add(new GitHubCommitNotifier(Result.SUCCESS.toString())); + + prj.scheduleBuild2(0).get(); + + github.service().verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); + } + + @TestExtension + public static final FixedGHRepoNameTestContributor CONTRIBUTOR = new FixedGHRepoNameTestContributor(); + } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java index 028e09112..5b9efffe2 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java @@ -1,23 +1,86 @@ package com.cloudbees.jenkins; +import com.github.tomakehurst.wiremock.common.Slf4jNotifier; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import hudson.Launcher; +import hudson.model.AbstractBuild; import hudson.model.Build; +import hudson.model.BuildListener; import hudson.model.FreeStyleProject; import hudson.model.Result; +import hudson.plugins.git.Revision; +import hudson.plugins.git.util.BuildData; +import hudson.tasks.Builder; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; +import org.jenkinsci.plugins.github.test.GHMockRule; +import org.jenkinsci.plugins.github.test.GHMockRule.FixedGHRepoNameTestContributor; +import org.jenkinsci.plugins.github.test.InjectJenkinsMembersRule; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.recipes.LocalData; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.inject.Inject; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static com.google.common.collect.Lists.newArrayList; +import static org.mockito.Mockito.when; /** * Tests for {@link GitHubSetCommitStatusBuilder}. * * @author Oleg Nenashev */ +@RunWith(MockitoJUnitRunner.class) public class GitHubSetCommitStatusBuilderTest { - @Rule + public static final String SOME_SHA = StringUtils.repeat("f", 40); + + @Mock + public BuildData data; + + @Mock + public Revision rev; + + @Inject + public GitHubPluginConfig config; + public JenkinsRule jRule = new JenkinsRule(); + @Rule + public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); + + @Rule + public GHMockRule github = new GHMockRule( + new WireMockRule( + wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)) + )) + .stubUser() + .stubRepo() + .stubStatuses(); + + @Rule + public ExternalResource prep = new ExternalResource() { + @Override + protected void before() throws Throwable { + when(data.getLastBuiltRevision()).thenReturn(rev); + when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); + } + }; + @Test @Issue("JENKINS-23641") public void testNoBuildData() throws Exception { @@ -27,4 +90,29 @@ public void testNoBuildData() throws Exception { jRule.assertBuildStatus(Result.FAILURE, b); jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); } + + @Test + @LocalData + @Issue("JENKINS-32132") + public void shouldLoadNullStatusMessage() throws Exception { + config.getConfigs().add(github.serverConfig()); + FreeStyleProject prj = jRule.getInstance().getItemByFullName("step", FreeStyleProject.class); + + List builders = newArrayList(prj.getBuildersList().toList()); + builders.add(0, new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { + build.addAction(data); + return true; + } + }); + + prj.getBuildersList().replaceBy(builders); + prj.scheduleBuild2(0).get(); + + github.service().verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); + } + + @TestExtension + public static final FixedGHRepoNameTestContributor CONTRIBUTOR = new FixedGHRepoNameTestContributor(); } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index 1302e8f53..1dc60583e 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -20,6 +20,7 @@ import static com.jayway.restassured.RestAssured.given; import static com.jayway.restassured.config.EncoderConfig.encoderConfig; import static com.jayway.restassured.config.RestAssuredConfig.newConfig; +import static java.lang.String.format; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; import static javax.servlet.http.HttpServletResponse.SC_OK; @@ -139,13 +140,17 @@ public Header eventHeader(String event) { return new Header(GHEventHeader.PayloadHandler.EVENT_HEADER, event); } - public static String classpath(String path) throws IOException { + public static String classpath(String path) { return classpath(GitHubWebHookFullTest.class, path); } - public static String classpath(Class clazz, String path) throws IOException { - return IOUtils.toString(clazz.getClassLoader().getResourceAsStream( - clazz.getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path - ), Charsets.UTF_8); + public static String classpath(Class clazz, String path) { + try { + return IOUtils.toString(clazz.getClassLoader().getResourceAsStream( + clazz.getName().replace(PACKAGE_SEPARATOR, File.separator) + File.separator + path + ), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(format("Can't load %s for class %s", path, clazz), e); + } } } diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java index 4cecd512e..e4d71b5d1 100644 --- a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java @@ -2,7 +2,7 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.junit.Before; +import org.jenkinsci.plugins.github.test.GHMockRule; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -12,10 +12,6 @@ import java.nio.file.Path; import java.util.Collections; -import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.google.common.collect.Lists.newArrayList; import static java.nio.file.Files.newDirectoryStream; @@ -36,12 +32,8 @@ public class GitHubClientCacheCleanupTest { public JenkinsRule jRule = new JenkinsRule(); @Rule - public WireMockRule github = new WireMockRule(wireMockConfig().dynamicPort()); + public GHMockRule github = new GHMockRule(new WireMockRule(wireMockConfig().dynamicPort())).stubUser(); - @Before - public void setUp() throws Exception { - stubUserResponse(); - } @Test public void shouldCreateCachedFolder() throws Exception { @@ -82,7 +74,7 @@ public void shouldRemoveOnlyNotActiveCachedDirAfterClean() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CHANGED_CREDS_ID); config.setCustomApiUrl(true); - config.setApiUrl(constructApiUrl()); + config.setApiUrl(github.serverConfig().getApiUrl()); config.setClientCacheSize(1); clearRedundantCaches(newArrayList(config)); @@ -96,7 +88,7 @@ public void shouldRemoveCacheWhichNotEnabled() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CHANGED_CREDS_ID); config.setCustomApiUrl(true); - config.setApiUrl(constructApiUrl()); + config.setApiUrl(github.serverConfig().getApiUrl()); config.setClientCacheSize(0); clearRedundantCaches(newArrayList(config)); @@ -110,20 +102,8 @@ private void it(String comment, int count) throws IOException { } } - private String constructApiUrl() { - return "http://localhost:" + github.port(); - } - private void makeCachedRequestWithCredsId(String credsId) throws IOException { jRule.getInstance().getDescriptorByType(GitHubServerConfig.DescriptorImpl.class) - .doVerifyCredentials(constructApiUrl(), credsId, 1); - } - - private void stubUserResponse() throws IOException { - github.stubFor(get(urlPathEqualTo("/user")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json; charset=utf-8") - .withBody(classpath(getClass(), "user.json")))); + .doVerifyCredentials(github.serverConfig().getApiUrl(), credsId, 1); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java b/src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java new file mode 100644 index 000000000..32575002f --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java @@ -0,0 +1,165 @@ +package org.jenkinsci.plugins.github.test; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.cloudbees.jenkins.GitHubRepositoryNameContributor; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import hudson.model.Job; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static java.lang.String.format; +import static wiremock.org.mortbay.jetty.HttpStatus.ORDINAL_201_Created; + +/** + * Mocks GitHub on localhost with some predefined methods + * + * @author lanwen (Merkushev Kirill) + */ +public class GHMockRule implements TestRule { + + /** + * This repo is used in resource files + */ + public static final GitHubRepositoryName REPO = new GitHubRepositoryName("localhost", "org", "repo"); + + /** + * Wiremock service itself. You can interact with it directly by {@link #service()} method + */ + private WireMockRule service; + + /** + * List of additional stubs. Launched after wiremock has been started + */ + private List setups = new ArrayList<>(); + + public GHMockRule(WireMockRule mocked) { + this.service = mocked; + } + + /** + * @return wiremock rule + */ + public WireMockRule service() { + return service; + } + + /** + * Ready-to-use global config with wiremock service. Just add it to plugin config + * {@code GitHubPlugin.configuration().getConfigs().add(github.serverConfig());} + * + * @return part of global plugin config + */ + public GitHubServerConfig serverConfig() { + GitHubServerConfig conf = new GitHubServerConfig("creds"); + conf.setCustomApiUrl(true); + conf.setApiUrl("http://localhost:" + service().port()); + return conf; + } + + /** + * Main method of rule. Firstly starts wiremock, then run predefined setups + */ + @Override + public Statement apply(final Statement base, Description description) { + return service.apply(new Statement() { + @Override + public void evaluate() throws Throwable { + for (Runnable callable : setups) { + callable.run(); + } + base.evaluate(); + } + }, description); + } + + /** + * Stubs /user response with predefined content + * + * More info: https://developer.github.com/v3/users/#get-the-authenticated-user + */ + public GHMockRule stubUser() { + return addSetup(new Runnable() { + @Override + public void run() { + service().stubFor(get(urlPathEqualTo("/user")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody(classpath(GHMockRule.class, "user.json")))); + } + }); + } + + /** + * Stubs /repos/org/repo response with predefined content + * + * More info: https://developer.github.com/v3/repos/#get + */ + public GHMockRule stubRepo() { + return addSetup(new Runnable() { + @Override + public void run() { + String repo = format("/repos/%s/%s", REPO.getUserName(), REPO.getRepositoryName()); + service().stubFor( + get(urlPathMatching(repo)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody(classpath(GHMockRule.class, "repos-repo.json")))); + } + }); + } + + /** + * Returns 201 CREATED on POST to statuses endpoint (but without content) + * + * More info: https://developer.github.com/v3/repos/statuses/ + */ + public GHMockRule stubStatuses() { + return addSetup(new Runnable() { + @Override + public void run() { + service().stubFor( + post(urlPathMatching( + format("/repos/%s/%s/statuses/.*", REPO.getUserName(), REPO.getRepositoryName())) + ).willReturn(aResponse().withStatus(ORDINAL_201_Created))); + } + }); + } + + /** + * When we call one of predefined stub* methods, wiremock is not not started yet, so we need to create a closure + * + * @param setup closure to setup wiremock + */ + private GHMockRule addSetup(Runnable setup) { + setups.add(setup); + return this; + } + + /** + * Adds predefined repo to list which job can return. This is useful to avoid SCM usage. + * + * {@code @TestExtension + * public static final FixedGHRepoNameTestContributor CONTRIBUTOR = new FixedGHRepoNameTestContributor(); + * } + */ + public static class FixedGHRepoNameTestContributor extends GitHubRepositoryNameContributor { + @Override + public void parseAssociatedNames(Job job, Collection result) { + result.add(GHMockRule.REPO); + } + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java b/src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java new file mode 100644 index 000000000..ae0127783 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java @@ -0,0 +1,39 @@ +package org.jenkinsci.plugins.github.test; + +import org.junit.rules.ExternalResource; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * Helpful class to make possible usage of + * {@code @Inject + * public GitHubPluginConfig config; + * } + * + * in test fields instead of static calls {@link org.jenkinsci.plugins.github.GitHubPlugin#configuration()} + * + * See {@link com.cloudbees.jenkins.GitHubSetCommitStatusBuilderTest} for example + * Should be used after JenkinsRule initialized + * + * {@code public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); } + * + * @author lanwen (Merkushev Kirill) + */ +public class InjectJenkinsMembersRule extends ExternalResource { + + private JenkinsRule jRule; + private Object instance; + + /** + * @param jRule Jenkins rule + * @param instance test class instance + */ + public InjectJenkinsMembersRule(JenkinsRule jRule, Object instance) { + this.jRule = jRule; + this.instance = instance; + } + + @Override + protected void before() throws Throwable { + jRule.getInstance().getInjector().injectMembers(instance); + } +} diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/config.xml b/src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/config.xml new file mode 100644 index 000000000..b11975415 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/config.xml @@ -0,0 +1,35 @@ + + + + 1.554.1 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + + 5 + 0 + + + + All + false + false + + + + All + 0 + + + + \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/jobs/step/config.xml b/src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/jobs/step/config.xml new file mode 100644 index 000000000..273cb31e6 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest/shouldLoadNullStatusMessage/jobs/step/config.xml @@ -0,0 +1,20 @@ + + + + + false + + + + true + false + false + false + + false + + + + + + \ No newline at end of file diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 000000000..cad4b6602 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,7 @@ +# Root logger option +log4j.rootLogger=INFO, stdout + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n diff --git a/src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/repos-repo.json b/src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/repos-repo.json new file mode 100644 index 000000000..2ae371c8b --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/repos-repo.json @@ -0,0 +1,89 @@ +{ + "id": 38941520, + "name": "repo", + "full_name": "org/repo", + "owner": { + "login": "org", + "id": 1964214, + "avatar_url": "http://avatars.githubusercontent.com/u/1964214?v=3", + "gravatar_id": "", + "url": "http://localhost/users/org", + "html_url": "http://github.com/org", + "followers_url": "http://localhost/users/org/followers", + "following_url": "http://localhost/users/org/following{/other_user}", + "gists_url": "http://localhost/users/org/gists{/gist_id}", + "starred_url": "http://localhost/users/org/starred{/owner}{/repo}", + "subscriptions_url": "http://localhost/users/org/subscriptions", + "organizations_url": "http://localhost/users/org/orgs", + "repos_url": "http://localhost/users/org/repos", + "events_url": "http://localhost/users/org/events{/privacy}", + "received_events_url": "http://localhost/users/org/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "http://github.com/org/repo", + "description": "for repo purposes", + "fork": false, + "url": "http://localhost/repos/org/repo", + "forks_url": "http://localhost/repos/org/repo/forks", + "keys_url": "http://localhost/repos/org/repo/keys{/key_id}", + "collaborators_url": "http://localhost/repos/org/repo/collaborators{/collaborator}", + "teams_url": "http://localhost/repos/org/repo/teams", + "hooks_url": "http://localhost/repos/org/repo/hooks", + "issue_events_url": "http://localhost/repos/org/repo/issues/events{/number}", + "events_url": "http://localhost/repos/org/repo/events", + "assignees_url": "http://localhost/repos/org/repo/assignees{/user}", + "branches_url": "http://localhost/repos/org/repo/branches{/branch}", + "tags_url": "http://localhost/repos/org/repo/tags", + "blobs_url": "http://localhost/repos/org/repo/git/blobs{/sha}", + "git_tags_url": "http://localhost/repos/org/repo/git/tags{/sha}", + "git_refs_url": "http://localhost/repos/org/repo/git/refs{/sha}", + "trees_url": "http://localhost/repos/org/repo/git/trees{/sha}", + "statuses_url": "http://localhost/repos/org/repo/statuses/{sha}", + "languages_url": "http://localhost/repos/org/repo/languages", + "stargazers_url": "http://localhost/repos/org/repo/stargazers", + "contributors_url": "http://localhost/repos/org/repo/contributors", + "subscribers_url": "http://localhost/repos/org/repo/subscribers", + "subscription_url": "http://localhost/repos/org/repo/subscription", + "commits_url": "http://localhost/repos/org/repo/commits{/sha}", + "git_commits_url": "http://localhost/repos/org/repo/git/commits{/sha}", + "comments_url": "http://localhost/repos/org/repo/comments{/number}", + "issue_comment_url": "http://localhost/repos/org/repo/issues/comments{/number}", + "contents_url": "http://localhost/repos/org/repo/contents/{+path}", + "compare_url": "http://localhost/repos/org/repo/compare/{base}...{head}", + "merges_url": "http://localhost/repos/org/repo/merges", + "archive_url": "http://localhost/repos/org/repo/{archive_format}{/ref}", + "downloads_url": "http://localhost/repos/org/repo/downloads", + "issues_url": "http://localhost/repos/org/repo/issues{/number}", + "pulls_url": "http://localhost/repos/org/repo/pulls{/number}", + "milestones_url": "http://localhost/repos/org/repo/milestones{/number}", + "notifications_url": "http://localhost/repos/org/repo/notifications{?since,all,participating}", + "labels_url": "http://localhost/repos/org/repo/labels{/name}", + "releases_url": "http://localhost/repos/org/repo/releases{/id}", + "created_at": "2015-07-11T21:47:22Z", + "updated_at": "2015-07-11T21:47:22Z", + "pushed_at": "2015-07-19T22:22:08Z", + "git_url": "git://localhost/org/repo.git", + "ssh_url": "git@localhost:org/repo.git", + "clone_url": "http://localhost/org/repo.git", + "svn_url": "http://localhost/org/repo", + "homepage": null, + "size": 160, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "network_count": 0, + "subscribers_count": 1 +} diff --git a/src/test/resources/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest/user.json b/src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/user.json similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest/user.json rename to src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/user.json From 731ffae997a5f5bc0d4c8df2b3e933d0caef4d7a Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Mon, 28 Dec 2015 20:47:21 +0100 Subject: [PATCH 135/228] Allow Commit Notifications to be used as Workflow Build Steps. --- .../jenkins/GitHubCommitNotifier.java | 26 ++++++++++--------- .../jenkins/GitHubSetCommitStatusBuilder.java | 21 ++++++++------- .../plugins/github/util/BuildDataHelper.java | 4 +-- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index bb8cb7cf2..25196f2e5 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -1,17 +1,20 @@ package com.cloudbees.jenkins; import hudson.Extension; +import hudson.FilePath; import hudson.Launcher; import hudson.Util; -import hudson.model.AbstractBuild; import hudson.model.AbstractProject; -import hudson.model.BuildListener; import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import hudson.util.ListBoxModel; +import jenkins.tasks.SimpleBuildStep; + import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.util.BuildDataHelper; @@ -41,7 +44,7 @@ * * @author Nicolas De Loof */ -public class GitHubCommitNotifier extends Notifier { +public class GitHubCommitNotifier extends Notifier implements SimpleBuildStep { private static final ExpandableMessage DEFAULT_MESSAGE = new ExpandableMessage(""); private ExpandableMessage statusMessage = DEFAULT_MESSAGE; @@ -100,12 +103,12 @@ public BuildStepMonitor getRequiredMonitorService() { } @Override - public boolean perform(AbstractBuild build, + public void perform(Run build, + FilePath ws, Launcher launcher, - BuildListener listener) throws InterruptedException, IOException { + TaskListener listener) throws InterruptedException, IOException { try { updateCommitStatus(build, listener); - return true; } catch (IOException error) { final Result buildResult = getEffectiveResultOnFailure(); if (buildResult.equals(FAILURE)) { @@ -118,19 +121,18 @@ public boolean perform(AbstractBuild build, build.setResult(buildResult); } } - return true; } - private void updateCommitStatus(@Nonnull AbstractBuild build, - @Nonnull BuildListener listener) throws InterruptedException, IOException { + private void updateCommitStatus(@Nonnull Run build, + @Nonnull TaskListener listener) throws InterruptedException, IOException { final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); StatusResult status = statusFrom(build); String message = defaultIfEmpty(firstNonNull(statusMessage, DEFAULT_MESSAGE) .expandAll(build, listener), status.getMsg()); - String contextName = displayNameFor(build.getProject()); + String contextName = displayNameFor(build.getParent()); - for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { + for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getParent())) { for (GHRepository repository : name.resolve()) { listener.getLogger().println( @@ -146,7 +148,7 @@ private void updateCommitStatus(@Nonnull AbstractBuild build, } } - private static StatusResult statusFrom(@Nonnull AbstractBuild build) { + private static StatusResult statusFrom(@Nonnull Run build) { Result result = build.getResult(); // We do not use `build.getDurationString()` because it appends 'and counting' (build is still running) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 0c151d05a..3aa15df14 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -1,12 +1,15 @@ package com.cloudbees.jenkins; import hudson.Extension; +import hudson.FilePath; import hudson.Launcher; -import hudson.model.AbstractBuild; import hudson.model.AbstractProject; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; +import jenkins.tasks.SimpleBuildStep; + import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.util.BuildDataHelper; @@ -23,7 +26,7 @@ import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; @Extension -public class GitHubSetCommitStatusBuilder extends Builder { +public class GitHubSetCommitStatusBuilder extends Builder implements SimpleBuildStep { private static final ExpandableMessage DEFAULT_MESSAGE = new ExpandableMessage(""); private ExpandableMessage statusMessage = DEFAULT_MESSAGE; @@ -48,17 +51,18 @@ public void setStatusMessage(ExpandableMessage statusMessage) { } @Override - public boolean perform(AbstractBuild build, - Launcher launcher, - BuildListener listener) throws InterruptedException, IOException { + public void perform(Run build, + FilePath workspace, + Launcher launcher, + TaskListener listener) throws InterruptedException, IOException { final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); String message = defaultIfEmpty( firstNonNull(statusMessage, DEFAULT_MESSAGE).expandAll(build, listener), Messages.CommitNotifier_Pending(build.getDisplayName()) ); - String contextName = displayNameFor(build.getProject()); + String contextName = displayNameFor(build.getParent()); - for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getProject())) { + for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getParent())) { for (GHRepository repository : name.resolve()) { listener.getLogger().println( GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1) @@ -70,7 +74,6 @@ public boolean perform(AbstractBuild build, contextName); } } - return true; } @Extension 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 46f7f5dbe..6284d1bae 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java @@ -1,6 +1,6 @@ package org.jenkinsci.plugins.github.util; -import hudson.model.AbstractBuild; +import hudson.model.Run; import hudson.plugins.git.Revision; import hudson.plugins.git.util.BuildData; import org.eclipse.jgit.lib.ObjectId; @@ -27,7 +27,7 @@ private BuildDataHelper() { * @throws IOException Cannot get the info about commit ID */ @Nonnull - public static ObjectId getCommitSHA1(@Nonnull AbstractBuild build) throws IOException { + public static ObjectId getCommitSHA1(@Nonnull Run build) throws IOException { BuildData buildData = build.getAction(BuildData.class); if (buildData == null) { throw new IOException(Messages.BuildDataHelper_NoBuildDataError()); From b477fb3d853c43035c5314f6ac26446df75a0503 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Fri, 8 Jan 2016 00:25:56 +0400 Subject: [PATCH 136/228] [maven-release-plugin] prepare release github-1.15.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ed65eac39..0d86cdc89 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.14.3-SNAPSHOT + 1.15.0 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + github-1.15.0 From 2b94ad78c93e20fdd0e15bd0e0e5d6083243b8a5 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Fri, 8 Jan 2016 00:26:01 +0400 Subject: [PATCH 137/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d86cdc89..a9290662e 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.15.0 + 1.15.1-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.15.0 + HEAD From 054719a90eda6fce1653ba12de29ab976da0701c Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Thu, 14 Jan 2016 13:41:06 +0100 Subject: [PATCH 138/228] Use `TransientActionFactory` instead of `GithubProjectProperty.getJobActions`. This makes the "GitHub" link also show up for Workflow projects. --- .../plugins/github/GithubLinkAction.java | 25 ++++++++ .../plugins/github/GithubProjectProperty.java | 11 ---- .../github/GithubLinkActionFactoryTest.java | 57 +++++++++++++++++++ 3 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java 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 1db8758e2..4f9c63901 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java @@ -1,6 +1,12 @@ package com.coravy.hudson.plugins.github; +import java.util.Collection; +import java.util.Collections; + +import hudson.Extension; import hudson.model.Action; +import hudson.model.Job; +import jenkins.model.TransientActionFactory; /** * Add the Github Logo/Icon to the sidebar. @@ -30,4 +36,23 @@ public String getUrlName() { return projectProperty.getProjectUrl().baseUrl(); } + @SuppressWarnings("rawtypes") + @Extension + public static class GithubLinkActionFactory extends TransientActionFactory { + @Override + public Class type() { + return Job.class; + } + + @Override + public Collection createFor(Job j) { + GithubProjectProperty prop = ((Job) j).getProperty(GithubProjectProperty.class); + + if (prop == null) { + return Collections.emptySet(); + } else { + return Collections.singleton(new GithubLinkAction(prop)); + } + } + } } diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java index f551fb5b9..e7a84b21b 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubProjectProperty.java @@ -2,7 +2,6 @@ import com.cloudbees.jenkins.GitHubPushTrigger; import hudson.Extension; -import hudson.model.Action; import hudson.model.Job; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; @@ -14,8 +13,6 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.Collections; import java.util.logging.Logger; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -83,14 +80,6 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } - @Override - public Collection getJobActions(Job job) { - if (null != projectUrl) { - return Collections.singleton(new GithubLinkAction(this)); - } - return Collections.emptyList(); - } - /** * Extracts value of display name from given job, or just returns full name if field or prop is not defined * diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java new file mode 100644 index 000000000..cef4e8bfa --- /dev/null +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java @@ -0,0 +1,57 @@ +package com.coravy.hudson.plugins.github; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.util.Collection; + +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import com.coravy.hudson.plugins.github.GithubLinkAction.GithubLinkActionFactory; + +import hudson.model.Action; + +public class GithubLinkActionFactoryTest { + @Rule + public final JenkinsRule rule = new JenkinsRule(); + + private final GithubLinkActionFactory factory = new GithubLinkActionFactory(); + + private static final String PROJECT_URL = "https://github.com/jenkinsci/github-plugin/"; + + private WorkflowJob createExampleJob() throws IOException { + return rule.getInstance().createProject(WorkflowJob.class, "example"); + } + + private GithubProjectProperty createExampleProperty() { + return new GithubProjectProperty(PROJECT_URL); + } + + @Test + public void shouldCreateGithubLinkActionForJobWithGithubProjectProperty() throws IOException { + final WorkflowJob job = createExampleJob(); + final GithubProjectProperty property = createExampleProperty(); + job.addProperty(property); + + final Collection actions = factory.createFor(job); + assertThat("factored actions list", actions.size(), is(1)); + + final Action action = actions.iterator().next(); + assertThat("instance check", action, is(instanceOf(GithubLinkAction.class))); + assertThat("url of action", action.getUrlName(), is(property.getProjectUrlStr())); + } + + @Test + public void shouldNotCreateGithubLinkActionForJobWithoutGithubProjectProperty() throws IOException { + final WorkflowJob job = createExampleJob(); + + final Collection actions = factory.createFor(job); + assertThat("factored actions list", actions, is(empty())); + } +} From 1adb56c9cc0ecf70389aaf36351b691fecd68901 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Thu, 14 Jan 2016 17:14:46 +0400 Subject: [PATCH 139/228] [maven-release-plugin] prepare release github-1.16.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a9290662e..b5a82135c 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.15.1-SNAPSHOT + 1.16.0 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + github-1.16.0 From a244a4a687493234cb46ed768b01691a8c5f4610 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Thu, 14 Jan 2016 17:14:51 +0400 Subject: [PATCH 140/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b5a82135c..9d603cafa 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.16.0 + 1.16.1-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.16.0 + HEAD From 57aa4802525fe0c60888d74d05b38aa1e7315450 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Wed, 20 Jan 2016 00:34:02 +0300 Subject: [PATCH 141/228] Update checkstyle-config.xml --- src/test/resources/checkstyle/checkstyle-config.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/resources/checkstyle/checkstyle-config.xml b/src/test/resources/checkstyle/checkstyle-config.xml index ba6926328..963d82aab 100644 --- a/src/test/resources/checkstyle/checkstyle-config.xml +++ b/src/test/resources/checkstyle/checkstyle-config.xml @@ -1,7 +1,7 @@ + "-//Puppy Crawl//DTD Check Configuration 1.3//EN" + "http://www.puppycrawl.com/dtds/configuration_1_3.dtd"> - + diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index 7aca233ed..49708f76b 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -1,3 +1,4 @@ +

This plugin integrates GitHub to Jenkins.
diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly index cfaeb0feb..a3d95a60b 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/help-overrideHookUrl.jelly @@ -1,3 +1,4 @@ +
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 5cad14c19..36cec9f3d 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 @@ -1,3 +1,4 @@ +
diff --git a/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java b/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java index 8761f9785..ae3da6ba8 100644 --- a/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java +++ b/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java @@ -50,8 +50,6 @@ public HtmlForm globalConfig() throws IOException, SAXException { private JenkinsRule.WebClient configureWebClient() { JenkinsRule.WebClient client = jenkins.createWebClient(); - client.setThrowExceptionOnFailingStatusCode(false); - client.setCssEnabled(false); client.setJavaScriptEnabled(true); return client; } From c60f5583d19fc995c576dd2464d3ec092684afea Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 3 Apr 2016 00:05:42 +0300 Subject: [PATCH 176/228] turn off concurrency for tests --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8db3ca9c4..6d9573003 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.plugins plugin - 2.3 + 2.6 com.coravy.hudson.plugins.github @@ -52,6 +52,7 @@ false true 3.0.2 + 1 From a33285a46a4ef3033844c36e359b8113689eaf50 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 3 Apr 2016 00:47:34 +0300 Subject: [PATCH 177/228] clean some sections in pom already listed in parent --- pom.xml | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 6d9573003..777924d78 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,6 @@ https://github.com/jenkinsci/github-plugin HEAD - JIRA https://issues.jenkins-ci.org/browse/JENKINS/component/15896 @@ -53,24 +52,20 @@ true 3.0.2 1 + 7 repo.jenkins-ci.org - Jenkins Repository - http://repo.jenkins-ci.org/public/ - - - jgit-repository - Eclipse JGit Repository - http://download.eclipse.org/jgit/maven + https://repo.jenkins-ci.org/public/ + repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ + https://repo.jenkins-ci.org/public/ @@ -227,14 +222,6 @@ - - maven-compiler-plugin - - 1.7 - 1.7 - - - nl.geodienstencentrum.maven sass-maven-plugin From e1728e4f7852b10c891b1fdfa7839057b1ee15f9 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 10 Apr 2016 01:11:09 +0300 Subject: [PATCH 178/228] change logger in commit notifier, reformat code in tests for notifiers --- .../com/cloudbees/jenkins/GitHubCommitNotifier.java | 13 ++++++------- .../cloudbees/jenkins/GitHubCommitNotifierTest.java | 8 +++++--- .../jenkins/GitHubSetCommitStatusBuilderTest.java | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index f4e38a4b7..3d58d667d 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -14,7 +14,6 @@ import hudson.tasks.Publisher; import hudson.util.ListBoxModel; import jenkins.tasks.SimpleBuildStep; - import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.util.BuildDataHelper; @@ -55,7 +54,7 @@ public class GitHubCommitNotifier extends Notifier implements SimpleBuildStep { private final String resultOnFailure; private static final Result[] SUPPORTED_RESULTS = {FAILURE, UNSTABLE, SUCCESS}; - private static final Logger LOGGER = LoggerFactory.getLogger(GitHubWebHook.class); + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubCommitNotifier.class); @Restricted(NoExternalUse.class) public GitHubCommitNotifier() { @@ -109,9 +108,9 @@ public BuildStepMonitor getRequiredMonitorService() { @Override public void perform(Run build, - FilePath ws, - Launcher launcher, - TaskListener listener) throws InterruptedException, IOException { + FilePath ws, + Launcher launcher, + TaskListener listener) throws InterruptedException, IOException { try { updateCommitStatus(build, listener); } catch (IOException error) { @@ -155,8 +154,8 @@ private void updateCommitStatus(@Nonnull Run build, // doesn't exist in the upstream. Don't let the build fail // TODO: ideally we'd like other plugins to designate a commit to put the status update to LOGGER.debug("Failed to update commit status", e); - listener.getLogger().println("Commit doesn't exist in " - + repository.getFullName() + ". Status is not set"); + listener.getLogger() + .format("Commit doesn't exist in %s. Status is not set%n", repository.getFullName()); } } } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java index d5bc13ba6..e3b8756d0 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java @@ -34,6 +34,8 @@ import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError; +import static org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoLastRevisionError; import static org.mockito.Mockito.when; /** @@ -73,7 +75,7 @@ public class GitHubCommitNotifierTest { @Override protected void before() throws Throwable { when(data.getLastBuiltRevision()).thenReturn(rev); - data.lastBuild = new hudson.plugins.git.util.Build(rev,rev,0,Result.SUCCESS); + data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); } }; @@ -85,7 +87,7 @@ public void testNoBuildData() throws Exception { prj.getPublishersList().add(new GitHubCommitNotifier()); Build b = prj.scheduleBuild2(0).get(); jRule.assertBuildStatus(Result.FAILURE, b); - jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); + jRule.assertLogContains(BuildDataHelper_NoBuildDataError(), b); } @Test @@ -96,7 +98,7 @@ public void testNoBuildRevision() throws Exception { prj.getPublishersList().add(new GitHubCommitNotifier()); Build b = prj.scheduleBuild2(0).get(); jRule.assertBuildStatus(Result.FAILURE, b); - jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoLastRevisionError(), b); + jRule.assertLogContains(BuildDataHelper_NoLastRevisionError(), b); } @Test diff --git a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java index 6ae6bdc6c..f879b292a 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java @@ -57,11 +57,11 @@ public class GitHubSetCommitStatusBuilderTest { @Inject public GitHubPluginConfig config; - + public JenkinsRule jRule = new JenkinsRule(); @Rule - public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); + public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); @Rule public GHMockRule github = new GHMockRule( @@ -77,7 +77,7 @@ public class GitHubSetCommitStatusBuilderTest { @Override protected void before() throws Throwable { when(data.getLastBuiltRevision()).thenReturn(rev); - data.lastBuild = new hudson.plugins.git.util.Build(rev,rev,0,Result.SUCCESS); + data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); } }; From 7ab3480641be948ceea8a3591833ec97a4602872 Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Sun, 10 Apr 2016 01:42:22 +0300 Subject: [PATCH 179/228] replace license and wiki with badges in readme --- README.md | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 8271a69dc..175df3d4f 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Jenkins Github Plugin ===================== [![Coverage](https://img.shields.io/sonar/http/sonar.lanwen.ru/com.coravy.hudson.plugins.github:github/coverage.svg?style=flat)](http://sonar.lanwen.ru/dashboard/index?id=com.coravy.hudson.plugins.github:github) +[![License](https://img.shields.io/github/license/jenkinsci/github-plugin.svg)](LICENSE) +[![wiki](https://img.shields.io/badge/GitHub%20Plugin-WIKI-blue.svg?style=flat)](http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin) -Read more: [http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin) Development =========== @@ -50,32 +51,7 @@ Plugin releases mvn release:prepare release:perform -Dusername=juretta -Dpassword=****** -License -------- - - (The MIT License) - - Copyright (c) 2009 Stefan Saasen - - 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. - +## License notes This plugin uses part of Guava's code in class named `org.jenkinsci.plugins.github.util.FluentIterableWrapper` licensed under Apache 2.0 license From 5c15cb01ff8948d06a213ef8216fc45fe47c04ea Mon Sep 17 00:00:00 2001 From: logikal Date: Fri, 18 Mar 2016 12:12:37 -0700 Subject: [PATCH 180/228] Fix typos - s/mailformed/malformed --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 2 +- .../jenkinsci/plugins/github/config/GitHubPluginConfig.java | 2 +- .../jenkinsci/plugins/github/migration/MigratorTest.java | 6 +++--- .../com.cloudbees.jenkins.GitHubPushTrigger.xml | 0 .../config.xml | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/{shouldNotThrowExcMailformedHookUrlInOldConfig => shouldNotThrowExcMalformedHookUrlInOldConfig}/com.cloudbees.jenkins.GitHubPushTrigger.xml (100%) rename src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/{shouldNotThrowExcMailformedHookUrlInOldConfig => shouldNotThrowExcMalformedHookUrlInOldConfig}/config.xml (100%) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 718dbe2f2..a956588df 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -289,7 +289,7 @@ public URL getDeprecatedHookUrl() { try { return new URL(hookUrl); } catch (MalformedURLException e) { - LOGGER.warn("Mailformed hook url skipped while migration ({})", e.getMessage()); + LOGGER.warn("Malformed hook url skipped while migration ({})", e.getMessage()); return null; } } 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 7dc9479bf..90ccae4ba 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -162,7 +162,7 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti LOGGER.debug("Problem while submitting form for GitHub Plugin ({})", e.getMessage(), e); LOGGER.trace("GH form data: {}", json.toString()); throw new FormException( - format("Mailformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); + format("Malformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); } save(); clearRedundantCaches(configs); diff --git a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java index 6fd01cb02..7c901937f 100644 --- a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java @@ -42,11 +42,11 @@ public class MigratorTest { public static final String TOKEN3 = "some-oauth-token3"; /** - * Just ignore mailformed hook in old config + * Just ignore malformed hook in old config */ @Test @LocalData - public void shouldNotThrowExcMailformedHookUrlInOldConfig() throws IOException { + public void shouldNotThrowExcMalformedHookUrlInOldConfig() throws IOException { FreeStyleProject job = jenkins.createFreeStyleProject(); GitHubPushTrigger trigger = new GitHubPushTrigger(); trigger.start(job, true); @@ -87,7 +87,7 @@ public void shouldLoadDataAfterStart() throws Exception { withApiUrl(is(CUSTOM_GH_URL)), withApiUrl(is(GITHUB_URL)) )); - assertThat("should load hook url", + assertThat("should load hook url", GitHubPlugin.configuration().getHookUrl().toString(), equalTo(HOOK_FROM_LOCAL_DATA)); } diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMalformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml rename to src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMalformedHookUrlInOldConfig/com.cloudbees.jenkins.GitHubPushTrigger.xml diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMalformedHookUrlInOldConfig/config.xml similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMailformedHookUrlInOldConfig/config.xml rename to src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldNotThrowExcMalformedHookUrlInOldConfig/config.xml From 25372c78e5a82e8f5add0b4fc1c44dba972dbe10 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Sun, 10 Apr 2016 03:09:50 +0400 Subject: [PATCH 181/228] [maven-release-plugin] prepare release github-1.18.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 777924d78..ad8775390 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.18.2-SNAPSHOT + 1.18.2 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + github-1.18.2 JIRA From 28d8decb822ccee9932394ed271b60f5f23297df Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Sun, 10 Apr 2016 03:09:55 +0400 Subject: [PATCH 182/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ad8775390..fe252dcda 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.18.2 + 1.18.3-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.18.2 + HEAD JIRA From 4a08364cc0d0cd99ca1197f9b0bcfa9453001603 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Mon, 18 Apr 2016 00:51:54 +0300 Subject: [PATCH 183/228] extended setter for status --- .../github/common/CombineErrorHandler.java | 61 ++++++++ .../plugins/github/common/ErrorHandler.java | 13 ++ .../status/GitHubCommitShaSource.java | 18 +++ .../extension/status/GitHubReposSource.java | 18 +++ .../status/GitHubStatusContextSource.java | 17 +++ .../status/GitHubStatusResultSource.java | 36 +++++ .../extension/status/StatusErrorHandler.java | 19 +++ .../status/GitHubCommitStatusSetter.java | 136 ++++++++++++++++++ .../status/err/ShallowAnyErrorHandler.java | 34 +++++ .../sources/AnyDefinedRepositorySource.java | 48 +++++++ .../sources/BuildDataRevisionShaSource.java | 36 +++++ .../sources/DefaultCommitContextSource.java | 35 +++++ .../sources/DefaultStatusResultSource.java | 65 +++++++++ .../GitHubCommitStatusSetter/config.groovy | 36 +++++ .../err/ShallowAnyErrorHandler/config.groovy | 7 + .../AnyDefinedRepositorySource/config.groovy | 7 + .../BuildDataRevisionShaSource/config.groovy | 7 + .../DefaultCommitContextSource/config.groovy | 7 + .../DefaultStatusResultSource/config.groovy | 7 + 19 files changed, 607 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/config.groovy diff --git a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java new file mode 100644 index 000000000..8961c5b9c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java @@ -0,0 +1,61 @@ +package org.jenkinsci.plugins.github.common; + +import hudson.model.Run; +import hudson.model.TaskListener; +import org.apache.commons.collections.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class CombineErrorHandler implements ErrorHandler { + private static final Logger LOG = LoggerFactory.getLogger(CombineErrorHandler.class); + + private List handlers = new ArrayList<>(); + + private CombineErrorHandler() { + } + + public static CombineErrorHandler errorHandling() { + return new CombineErrorHandler(); + } + + public CombineErrorHandler withHandlers(List handlers) { + if (CollectionUtils.isEmpty(handlers)) { + this.handlers.addAll(handlers); + } + return this; + } + + @Override + public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + LOG.debug("Exception in {} ({})", run.getParent().getName(), e.getMessage(), e); + try { + for (ErrorHandler next : handlers) { + if (next.handle(e, run, listener)) { + LOG.debug("Exception in {} ({}) handled by {}", + run.getParent().getName(), + e.getMessage(), + next.getClass()); + return true; + } + } + } catch (Exception unhandled) { + LOG.error("Exception in {} ({}) unhandled", run.getParent().getName(), unhandled.getMessage(), unhandled); + throw new ErrorHandlingException(unhandled); + } + + throw new ErrorHandlingException(e); + } + + public static class ErrorHandlingException extends RuntimeException { + public ErrorHandlingException(Throwable cause) { + super(cause); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java new file mode 100644 index 000000000..e14f88d9b --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java @@ -0,0 +1,13 @@ +package org.jenkinsci.plugins.github.common; + +import hudson.model.Run; +import hudson.model.TaskListener; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public interface ErrorHandler { + boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) throws Exception; +} 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 new file mode 100644 index 000000000..ab82ffda0 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java @@ -0,0 +1,18 @@ +package org.jenkinsci.plugins.github.extension.status; + +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Run; +import hudson.model.TaskListener; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * @author lanwen (Merkushev Kirill) + */ +public abstract class GitHubCommitShaSource extends AbstractDescribableImpl + implements ExtensionPoint { + + public abstract String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException; +} 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 new file mode 100644 index 000000000..b7840cc0c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java @@ -0,0 +1,18 @@ +package org.jenkinsci.plugins.github.extension.status; + +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.kohsuke.github.GHRepository; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * @author lanwen (Merkushev Kirill) + */ +public abstract class GitHubReposSource extends AbstractDescribableImpl implements ExtensionPoint { + + public abstract List repos(@Nonnull Run run, @Nonnull 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 new file mode 100644 index 000000000..99183b24e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java @@ -0,0 +1,17 @@ +package org.jenkinsci.plugins.github.extension.status; + +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Run; +import hudson.model.TaskListener; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public abstract class GitHubStatusContextSource extends AbstractDescribableImpl + implements ExtensionPoint { + + 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 new file mode 100644 index 000000000..d48c80d02 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java @@ -0,0 +1,36 @@ +package org.jenkinsci.plugins.github.extension.status; + +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.kohsuke.github.GHCommitState; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public abstract class GitHubStatusResultSource extends AbstractDescribableImpl + implements ExtensionPoint { + + public abstract StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener); + + public static class StatusResult { + private GHCommitState state; + private String msg; + + public StatusResult(GHCommitState state, String msg) { + this.state = state; + this.msg = msg; + } + + public GHCommitState getState() { + return state; + } + + public String getMsg() { + return msg; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java new file mode 100644 index 000000000..e88def4a6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java @@ -0,0 +1,19 @@ +package org.jenkinsci.plugins.github.extension.status; + +import hudson.DescriptorExtensionList; +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.common.ErrorHandler; + +/** + * @author lanwen (Merkushev Kirill) + */ +public abstract class StatusErrorHandler extends AbstractDescribableImpl + implements ErrorHandler, ExtensionPoint { + + public static DescriptorExtensionList> all() { + return Jenkins.getInstance().getDescriptorList(StatusErrorHandler.class); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java new file mode 100644 index 000000000..2a44ceabe --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java @@ -0,0 +1,136 @@ +package org.jenkinsci.plugins.github.status; + +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractProject; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.BuildStepMonitor; +import hudson.tasks.Notifier; +import hudson.tasks.Publisher; +import jenkins.tasks.SimpleBuildStep; +import org.jenkinsci.plugins.github.common.CombineErrorHandler; +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; +import org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource; +import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource; +import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource; +import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource; +import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource; +import org.jenkinsci.plugins.github.extension.status.GitHubReposSource; +import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource; +import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; +import org.kohsuke.github.GHCommitState; +import org.kohsuke.github.GHRepository; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class GitHubCommitStatusSetter extends Notifier implements SimpleBuildStep { + + private GitHubCommitShaSource commitShaSource = new BuildDataRevisionShaSource(); + private GitHubReposSource reposSource = new AnyDefinedRepositorySource(); + private GitHubStatusContextSource contextSource = new DefaultCommitContextSource(); + private GitHubStatusResultSource statusResultSource = new DefaultStatusResultSource(); + private List errorHandlers = new ArrayList<>(); + + @DataBoundConstructor + public GitHubCommitStatusSetter() { + } + + @DataBoundSetter + public void setCommitShaSource(GitHubCommitShaSource commitShaSource) { + this.commitShaSource = commitShaSource; + } + + @DataBoundSetter + public void setReposSource(GitHubReposSource reposSource) { + this.reposSource = reposSource; + } + + @DataBoundSetter + public void setContextSource(GitHubStatusContextSource contextSource) { + this.contextSource = contextSource; + } + + @DataBoundSetter + public void setStatusResultSource(GitHubStatusResultSource statusResultSource) { + this.statusResultSource = statusResultSource; + } + + @DataBoundSetter + public void setErrorHandlers(List errorHandlers) { + this.errorHandlers = errorHandlers; + } + + public GitHubCommitShaSource getCommitShaSource() { + return commitShaSource; + } + + public GitHubReposSource getReposSource() { + return reposSource; + } + + public GitHubStatusContextSource getContextSource() { + return contextSource; + } + + public GitHubStatusResultSource getStatusResultSource() { + return statusResultSource; + } + + public List getErrorHandlers() { + return errorHandlers; + } + + @Override + 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(); + + GitHubStatusResultSource.StatusResult result = getStatusResultSource().get(run, listener); + + String message = result.getMsg(); + GHCommitState state = result.getState(); + + for (GHRepository repo : repos) { + repo.createCommitStatus(sha, state, backref, message, contextName); + } + + } catch (Exception e) { + CombineErrorHandler.errorHandling().withHandlers(getErrorHandlers()).handle(e, run, listener); + } + } + + @Override + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.NONE; + } + + + @Extension + public static class GitHubCommitStatusSetterDescr extends BuildStepDescriptor { + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + @Override + public String getDisplayName() { + return "[NEW] Set status for GitHub"; + } + + } +} 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 new file mode 100644 index 000000000..23314b258 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java @@ -0,0 +1,34 @@ +package org.jenkinsci.plugins.github.status.err; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class ShallowAnyErrorHandler extends StatusErrorHandler { + + @DataBoundConstructor + public ShallowAnyErrorHandler() { + } + + @Override + public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + listener.error("Exception with status setter (%s) ignored", e.getMessage()); + return true; + } + + @Extension + public static class ShallowAnyErrorHandlerDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Just ignore any errors"; + } + } +} 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 new file mode 100644 index 000000000..1391edcb4 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java @@ -0,0 +1,48 @@ +package org.jenkinsci.plugins.github.status.sources; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.cloudbees.jenkins.GitHubRepositoryNameContributor; +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.extension.status.GitHubReposSource; +import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; +import org.kohsuke.github.GHRepository; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.List; + +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class AnyDefinedRepositorySource extends GitHubReposSource { + + @DataBoundConstructor + public AnyDefinedRepositorySource() { + } + + @Override + public List repos(@Nonnull Run run, @Nonnull TaskListener listener) { + final Collection names = GitHubRepositoryNameContributor + .parseAssociatedNames(run.getParent()); + return from(names).transformAndConcat(new NullSafeFunction>() { + @Override + protected Iterable applyNullSafe(@Nonnull GitHubRepositoryName name) { + return name.resolve(); + } + }).toList(); + } + + @Extension + public static class AnyDefinedRepoSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Any defined in job repository"; + } + } +} 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 new file mode 100644 index 000000000..084712388 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java @@ -0,0 +1,36 @@ +package org.jenkinsci.plugins.github.status.sources; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource; +import org.jenkinsci.plugins.github.util.BuildDataHelper; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class BuildDataRevisionShaSource extends GitHubCommitShaSource { + + @DataBoundConstructor + public BuildDataRevisionShaSource() { + } + + @Override + public String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException { + return ObjectId.toString(BuildDataHelper.getCommitSHA1(run)); + } + + @Extension + public static class BuildDataRevisionShaSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Latest build revision"; + } + } +} 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 new file mode 100644 index 000000000..c1e43a716 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java @@ -0,0 +1,35 @@ +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.github.extension.status.GitHubStatusContextSource; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class DefaultCommitContextSource extends GitHubStatusContextSource { + + @DataBoundConstructor + public DefaultCommitContextSource() { + } + + @Override + public String context(@Nonnull Run run, @Nonnull TaskListener listener) { + return displayNameFor(run.getParent()); + } + + @Extension + public static class DefaultContextSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "From GitHub property with fallback to job name"; + } + } +} 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 new file mode 100644 index 000000000..8eef29c0d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java @@ -0,0 +1,65 @@ +package org.jenkinsci.plugins.github.status.sources; + +import com.cloudbees.jenkins.Messages; +import hudson.Extension; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; +import org.kohsuke.github.GHCommitState; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +import static hudson.model.Result.SUCCESS; +import static hudson.model.Result.UNSTABLE; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class DefaultStatusResultSource extends GitHubStatusResultSource { + + @DataBoundConstructor + public DefaultStatusResultSource() { + } + + @Override + public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) { + Result result = run.getResult(); + + // We do not use `build.getDurationString()` because it appends 'and counting' (build is still running) + String duration = Util.getTimeSpanString(System.currentTimeMillis() - run.getTimeInMillis()); + + if (result == null) { // Build is ongoing + return new GitHubStatusResultSource.StatusResult( + GHCommitState.PENDING, + Messages.CommitNotifier_Pending(run.getDisplayName()) + ); + } else if (result.isBetterOrEqualTo(SUCCESS)) { + return new GitHubStatusResultSource.StatusResult( + GHCommitState.SUCCESS, + Messages.CommitNotifier_Success(run.getDisplayName(), duration) + ); + } else if (result.isBetterOrEqualTo(UNSTABLE)) { + return new GitHubStatusResultSource.StatusResult( + GHCommitState.FAILURE, + Messages.CommitNotifier_Unstable(run.getDisplayName(), duration) + ); + } else { + return new GitHubStatusResultSource.StatusResult( + GHCommitState.ERROR, + Messages.CommitNotifier_Failed(run.getDisplayName(), duration) + ); + } + } + + @Extension + public static class DefaultResultSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "One of default messages and statuses"; + } + } +} 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 new file mode 100644 index 000000000..a60ab2281 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/config.groovy @@ -0,0 +1,36 @@ +package org.jenkinsci.plugins.github.status.GitHubCommitStatusSetter + +import org.apache.commons.collections.CollectionUtils +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler + + +def f = namespace(lib.FormTagLib); + +f.section(title: _('Where:')) { + f.dropdownDescriptorSelector(title: _('Commit SHA: '), field: 'commitShaSource') + f.dropdownDescriptorSelector(title: _('Repositories: '), field: 'reposSource') +} + +f.section(title: _('What:')) { + f.dropdownDescriptorSelector(title: _('Commit context: '), field: 'contextSource') + f.dropdownDescriptorSelector(title: _('Status result: '), field: 'statusResultSource') +} + +f.advanced { + f.section(title: _('Advanced:')) { + f.optionalBlock( + checked: CollectionUtils.isNotEmpty(instance?.errorHandlers), + inline: true, + name: 'errorHandling', + title: 'Handle errors') { + f.block { + f.hetero_list(items: CollectionUtils.isEmpty(instance?.errorHandlers) + ? [] + : instance.errorHandlers, + addCaption: 'Add handler', + name: 'errorHandlers', + oneEach: true, hasHeader: true, descriptors: StatusErrorHandler.all()) + } + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler/config.groovy new file mode 100644 index 000000000..10d115fd4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.status.err.ShallowAnyErrorHandler + + +def f = namespace(lib.FormTagLib); + +f.helpLink(url: descriptor.getHelpFile()) +f.helpArea() diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/config.groovy new file mode 100644 index 000000000..89e55b346 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource + + +def f = namespace(lib.FormTagLib); + +f.helpLink(url: descriptor.getHelpFile()) +f.helpArea() diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/config.groovy new file mode 100644 index 000000000..f1b3a09b4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource + + +def f = namespace(lib.FormTagLib); + +f.helpLink(url: descriptor.getHelpFile()) +f.helpArea() diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/config.groovy new file mode 100644 index 000000000..2ad8060e1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource + + +def f = namespace(lib.FormTagLib); + +f.helpLink(url: descriptor.getHelpFile()) +f.helpArea() diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/config.groovy new file mode 100644 index 000000000..185b6b354 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource + + +def f = namespace(lib.FormTagLib); + +f.helpLink(url: descriptor.getHelpFile()) +f.helpArea() From 11c3ba087f45e401cac593f0b7c9f60840694432 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 22 Apr 2016 02:19:19 +0300 Subject: [PATCH 184/228] changing status error handler --- .../github/common/CombineErrorHandler.java | 12 ++-- .../status/GitHubStatusResultSource.java | 4 +- .../status/GitHubCommitStatusSetter.java | 6 ++ .../err/ChangingBuildStatusErrorHandler.java | 65 +++++++++++++++++++ .../status/err/ShallowAnyErrorHandler.java | 3 +- .../GitHubCommitStatusSetter/config.groovy | 2 +- .../config.groovy | 7 ++ 7 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config.groovy 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 8961c5b9c..820af2f46 100644 --- a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java @@ -2,7 +2,6 @@ import hudson.model.Run; import hudson.model.TaskListener; -import org.apache.commons.collections.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,6 +9,8 @@ import java.util.ArrayList; import java.util.List; +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; + /** * @author lanwen (Merkushev Kirill) */ @@ -26,7 +27,7 @@ public static CombineErrorHandler errorHandling() { } public CombineErrorHandler withHandlers(List handlers) { - if (CollectionUtils.isEmpty(handlers)) { + if (isNotEmpty(handlers)) { this.handlers.addAll(handlers); } return this; @@ -34,11 +35,12 @@ public CombineErrorHandler withHandlers(List handlers) { @Override public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { - LOG.debug("Exception in {} ({})", run.getParent().getName(), e.getMessage(), e); + LOG.debug("Exception in {} will be processed with {} handlers", + run.getParent().getName(), handlers.size(), e); try { for (ErrorHandler next : handlers) { if (next.handle(e, run, listener)) { - LOG.debug("Exception in {} ({}) handled by {}", + LOG.debug("Exception in {} [{}] handled by [{}]", run.getParent().getName(), e.getMessage(), next.getClass()); @@ -46,7 +48,7 @@ public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener } } } catch (Exception unhandled) { - LOG.error("Exception in {} ({}) unhandled", run.getParent().getName(), unhandled.getMessage(), unhandled); + LOG.error("Exception in {} unhandled", run.getParent().getName(), unhandled); throw new ErrorHandlingException(unhandled); } 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 d48c80d02..0c325e97c 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 @@ -7,6 +7,7 @@ import org.kohsuke.github.GHCommitState; import javax.annotation.Nonnull; +import java.io.IOException; /** * @author lanwen (Merkushev Kirill) @@ -14,7 +15,8 @@ public abstract class GitHubStatusResultSource extends AbstractDescribableImpl implements ExtensionPoint { - public abstract StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener); + public abstract StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) + throws IOException, InterruptedException; public static class StatusResult { private GHCommitState state; 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 2a44ceabe..015acf2ca 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java @@ -30,6 +30,8 @@ import java.util.ArrayList; import java.util.List; +import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; + /** * @author lanwen (Merkushev Kirill) */ @@ -106,6 +108,10 @@ public void perform(@Nonnull Run run, @Nonnull FilePath workspace, @Nonnul GHCommitState state = result.getState(); for (GHRepository repo : repos) { + listener.getLogger().println( + GitHubCommitNotifier_SettingCommitStatus(repo.getHtmlUrl() + "/commit/" + sha) + ); + repo.createCommitStatus(sha, state, backref, message, contextName); } 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 new file mode 100644 index 000000000..3041d4b71 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java @@ -0,0 +1,65 @@ +package org.jenkinsci.plugins.github.status.err; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.ListBoxModel; +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +import static hudson.model.Result.FAILURE; +import static hudson.model.Result.UNSTABLE; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class ChangingBuildStatusErrorHandler extends StatusErrorHandler { + + private String result; + + @DataBoundConstructor + public ChangingBuildStatusErrorHandler(String result) { + this.result = result; + } + + public String getResult() { + return result; + } + + @Override + 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); + + run.setResult(toSet); + return true; + } + + @Extension + public static class ChangingBuildStatusErrorHandlerDescriptor extends Descriptor { + private static final Result[] SUPPORTED_RESULTS = { + FAILURE, + UNSTABLE + }; + + @Override + public String getDisplayName() { + return "Change build status"; + } + + @SuppressWarnings("unused") + public ListBoxModel doFillResultItems() { + ListBoxModel items = new ListBoxModel(); + for (Result result : SUPPORTED_RESULTS) { + items.add(result.toString()); + } + return items; + } + } +} 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 23314b258..522f92ed9 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 @@ -20,7 +20,8 @@ public ShallowAnyErrorHandler() { @Override public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { - listener.error("Exception with status setter (%s) ignored", e.getMessage()); + listener.error("[GitHub Commit Status Setter] Failed to update commit status on GitHub. " + + "Ignoring exception [%s]", e.getMessage()); return true; } 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 a60ab2281..2b807f165 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 @@ -27,7 +27,7 @@ f.advanced { f.hetero_list(items: CollectionUtils.isEmpty(instance?.errorHandlers) ? [] : instance.errorHandlers, - addCaption: 'Add handler', + addCaption: 'Add error handler', name: 'errorHandlers', oneEach: true, hasHeader: true, descriptors: StatusErrorHandler.all()) } diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config.groovy new file mode 100644 index 000000000..a4d45f0d9 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler/config.groovy @@ -0,0 +1,7 @@ +package org.jenkinsci.plugins.github.status.err.ChangingBuildStatusErrorHandler + +def f = namespace(lib.FormTagLib); + +f.entry(title: _('Result on failure'), field: 'result') { + f.select() +} From fc067bab6dfdae5b028a0d26e20e3bc91fc7c8fc Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 22 Apr 2016 15:59:18 +0300 Subject: [PATCH 185/228] conditional result setter --- .../status/misc/ConditionalResult.java | 59 ++++++++++++++ .../err/ChangingBuildStatusErrorHandler.java | 6 +- .../ConditionalStatusResultSource.java | 77 +++++++++++++++++++ .../status/sources/misc/AnyBuildResult.java | 31 ++++++++ .../misc/BetterThanOrEqualBuildResult.java | 67 ++++++++++++++++ .../misc/ConditionalResult/config.groovy | 12 +++ .../config.groovy | 15 ++++ .../config.groovy | 17 ++++ 8 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy 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 new file mode 100644 index 000000000..0550b620c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java @@ -0,0 +1,59 @@ +package org.jenkinsci.plugins.github.extension.status.misc; + +import hudson.DescriptorExtensionList; +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.kohsuke.github.GHCommitState; +import org.kohsuke.stapler.DataBoundSetter; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public abstract class ConditionalResult extends AbstractDescribableImpl implements ExtensionPoint { + + protected String status; + protected String message; + + @DataBoundSetter + public void setStatus(String status) { + this.status = status; + } + + @DataBoundSetter + public void setMessage(String message) { + this.message = message; + } + + public String getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public abstract boolean matches(@Nonnull Run run); + + public static abstract class ConditionalResultDescriptor extends Descriptor { + + public static DescriptorExtensionList> all() { + return Jenkins.getInstance().getDescriptorList(ConditionalResult.class); + } + + public ListBoxModel doFillStatusItems() { + ListBoxModel items = new ListBoxModel(); + for (GHCommitState status1 : GHCommitState.values()) { + items.add(status1.name()); + } + return items; + } + } + + +} 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 3041d4b71..8fee1a3ed 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 @@ -45,7 +45,7 @@ public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener public static class ChangingBuildStatusErrorHandlerDescriptor extends Descriptor { private static final Result[] SUPPORTED_RESULTS = { FAILURE, - UNSTABLE + UNSTABLE, }; @Override @@ -56,8 +56,8 @@ public String getDisplayName() { @SuppressWarnings("unused") public ListBoxModel doFillResultItems() { ListBoxModel items = new ListBoxModel(); - for (Result result : SUPPORTED_RESULTS) { - items.add(result.toString()); + for (Result supported : SUPPORTED_RESULTS) { + items.add(supported.toString()); } return items; } 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 new file mode 100644 index 000000000..ceaddb296 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java @@ -0,0 +1,77 @@ +package org.jenkinsci.plugins.github.status.sources; + +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.ListBoxModel; +import org.apache.commons.lang3.EnumUtils; +import org.jenkinsci.plugins.github.common.ExpandableMessage; +import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; +import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.kohsuke.github.GHCommitState; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.kohsuke.github.GHCommitState.ERROR; +import static org.kohsuke.github.GHCommitState.PENDING; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class ConditionalStatusResultSource extends GitHubStatusResultSource { + + private List results; + + @DataBoundConstructor + public ConditionalStatusResultSource(List results) { + this.results = results; + } + + @Nonnull + public List getResults() { + return defaultIfNull(results, Collections.emptyList()); + } + + @Override + public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) + throws IOException, InterruptedException { + + for (ConditionalResult conditionalResult : getResults()) { + if (conditionalResult.matches(run)) { + return new StatusResult( + defaultIfNull(EnumUtils.getEnum(GHCommitState.class, conditionalResult.getStatus()), ERROR), + new ExpandableMessage(conditionalResult.getMessage()).expandAll(run, listener) + ); + } + } + + return new StatusResult( + PENDING, + new ExpandableMessage("Can't define which status to set").expandAll(run, listener) + ); + } + + @Extension + public static class ConditionalStatusResultSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Based on build result manually defined"; + } + + @SuppressWarnings("unused") + public ListBoxModel doFillStatusItems() { + ListBoxModel items = new ListBoxModel(); + for (GHCommitState status : GHCommitState.values()) { + items.add(status.name()); + } + return items; + } + } + +} 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 new file mode 100644 index 000000000..2a9d499d7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java @@ -0,0 +1,31 @@ +package org.jenkinsci.plugins.github.status.sources.misc; + +import hudson.Extension; +import hudson.model.Run; +import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class AnyBuildResult extends ConditionalResult { + + @DataBoundConstructor + public AnyBuildResult() { + } + + @Override + public boolean matches(@Nonnull Run run) { + return true; + } + + @Extension + public static class AnyBuildResultDescriptor extends ConditionalResultDescriptor { + @Override + public String getDisplayName() { + return "Result ANY"; + } + } +} 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 new file mode 100644 index 000000000..f861dd0aa --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java @@ -0,0 +1,67 @@ +package org.jenkinsci.plugins.github.status.sources.misc; + +import hudson.Extension; +import hudson.model.Result; +import hudson.model.Run; +import hudson.util.ListBoxModel; +import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import javax.annotation.Nonnull; + +import static hudson.model.Result.FAILURE; +import static hudson.model.Result.SUCCESS; +import static hudson.model.Result.UNSTABLE; +import static hudson.model.Result.fromString; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class BetterThanOrEqualBuildResult extends ConditionalResult { + + private String result; + + @DataBoundConstructor + public BetterThanOrEqualBuildResult() { + } + + @DataBoundSetter + public void setResult(String result) { + this.result = result; + } + + public String getResult() { + return result; + } + + @Override + public boolean matches(@Nonnull Run run) { + return defaultIfNull(run.getResult(), Result.NOT_BUILT).isBetterOrEqualTo(fromString(trimToEmpty(result))); + } + + @Extension + public static class BetterThanOrEqualBuildResultDescriptor extends ConditionalResultDescriptor { + private static final Result[] SUPPORTED_RESULTS = { + SUCCESS, + UNSTABLE, + FAILURE, + }; + + @Override + public String getDisplayName() { + return "Result better than or equal to"; + } + + @SuppressWarnings("unused") + public ListBoxModel doFillResultItems() { + ListBoxModel items = new ListBoxModel(); + for (Result supported : SUPPORTED_RESULTS) { + items.add(supported.toString()); + } + return items; + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy new file mode 100644 index 000000000..74566a837 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy @@ -0,0 +1,12 @@ +package org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult + + +def f = namespace(lib.FormTagLib); + +f.entry(title: _('Status'), field: 'status') { + f.select() +} + +f.entry(title: _('Message'), field: 'message') { + f.textbox() +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy new file mode 100644 index 000000000..2bf0c52b8 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy @@ -0,0 +1,15 @@ +package org.jenkinsci.plugins.github.status.sources.ConditionalStatusResultSource + +import org.apache.commons.collections.CollectionUtils +import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult.ConditionalResultDescriptor; + +def f = namespace(lib.FormTagLib); + +f.block { + f.hetero_list(items: CollectionUtils.isEmpty(instance?.results) + ? [] + : instance.results, + addCaption: 'If build', + name: 'results', + oneEach: false, hasHeader: true, descriptors: ConditionalResultDescriptor.all()) +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy new file mode 100644 index 000000000..9ec0b0204 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy @@ -0,0 +1,17 @@ +package org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult + + +def f = namespace(lib.FormTagLib); + + +f.entry(title: _('Build result better than or equal to'), field: 'result') { + f.select() +} + +f.entry(title: _('Status'), field: 'status') { + f.select() +} + +f.entry(title: _('Message'), field: 'message') { + f.textbox() +} From 7ac6bc3be81fc2ca3f16509e15a3f540ddecd9dd Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 22 Apr 2016 16:18:38 +0300 Subject: [PATCH 186/228] manually entered context and sha sources --- .../status/GitHubCommitShaSource.java | 2 +- .../status/misc/ConditionalResult.java | 2 +- .../ManuallyEnteredCommitContextSource.java | 35 ++++++++++++++++ .../sources/ManuallyEnteredShaSource.java | 42 +++++++++++++++++++ .../config.groovy | 8 ++++ .../ManuallyEnteredShaSource/config.groovy | 8 ++++ 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/config.groovy 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 ab82ffda0..4912127c9 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 @@ -14,5 +14,5 @@ public abstract class GitHubCommitShaSource extends AbstractDescribableImpl implements ExtensionPoint { - public abstract String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException; + 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/misc/ConditionalResult.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java index 0550b620c..321e79d7b 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 @@ -40,7 +40,7 @@ public String getMessage() { public abstract boolean matches(@Nonnull Run run); - public static abstract class ConditionalResultDescriptor extends Descriptor { + public abstract static class ConditionalResultDescriptor extends Descriptor { public static DescriptorExtensionList> all() { return Jenkins.getInstance().getDescriptorList(ConditionalResult.class); 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 new file mode 100644 index 000000000..1a1179727 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java @@ -0,0 +1,35 @@ +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.github.extension.status.GitHubStatusContextSource; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class ManuallyEnteredCommitContextSource extends GitHubStatusContextSource { + private String context; + + @DataBoundConstructor + public ManuallyEnteredCommitContextSource(String context) { + this.context = context; + } + + @Override + public String context(@Nonnull Run run, @Nonnull TaskListener listener) { + return context; + } + + @Extension + public static class ManuallyEnteredCommitContextSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Manually entered context name"; + } + } +} 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 new file mode 100644 index 000000000..426d11f39 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java @@ -0,0 +1,42 @@ +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.github.common.ExpandableMessage; +import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * @author lanwen (Merkushev Kirill) + */ +public class ManuallyEnteredShaSource extends GitHubCommitShaSource { + + private String sha; + + @DataBoundConstructor + public ManuallyEnteredShaSource(String sha) { + this.sha = sha; + } + + public String getSha() { + return sha; + } + + @Override + public String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException, InterruptedException { + return new ExpandableMessage(sha).expandAll(run, listener); + } + + @Extension + public static class ManuallyEnteredShaSourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Manually entered SHA"; + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/config.groovy new file mode 100644 index 000000000..4990bf142 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/config.groovy @@ -0,0 +1,8 @@ +package org.jenkinsci.plugins.github.status.sources.ManuallyEnteredCommitContextSource + + +def f = namespace(lib.FormTagLib); + +f.entry(title: _('Context name'), field: 'context') { + f.textbox() +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/config.groovy new file mode 100644 index 000000000..ab901a35e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/config.groovy @@ -0,0 +1,8 @@ +package org.jenkinsci.plugins.github.status.sources.ManuallyEnteredShaSource + + +def f = namespace(lib.FormTagLib); + +f.entry(title: _('SHA'), field: 'sha') { + f.textbox() +} From a991b410cd3101914f9aa6009216284c200611c2 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 24 Apr 2016 03:24:22 +0300 Subject: [PATCH 187/228] javadocs and helps for new status setter --- .../github/common/CombineErrorHandler.java | 22 +++++++++ .../plugins/github/common/ErrorHandler.java | 17 +++++++ .../status/GitHubCommitShaSource.java | 12 ++++- .../extension/status/GitHubReposSource.java | 9 ++++ .../status/GitHubStatusContextSource.java | 9 ++++ .../status/GitHubStatusResultSource.java | 14 +++++- .../extension/status/StatusErrorHandler.java | 8 +++ .../status/misc/ConditionalResult.java | 49 ++++++++++++++----- .../status/GitHubCommitStatusSetter.java | 33 ++++++++++--- .../err/ChangingBuildStatusErrorHandler.java | 13 ++++- .../status/err/ShallowAnyErrorHandler.java | 8 ++- .../sources/AnyDefinedRepositorySource.java | 6 +++ .../sources/BuildDataRevisionShaSource.java | 6 +++ .../ConditionalStatusResultSource.java | 18 +++---- .../sources/DefaultCommitContextSource.java | 10 +++- .../sources/DefaultStatusResultSource.java | 47 +++++++++--------- .../ManuallyEnteredCommitContextSource.java | 24 ++++++++- .../sources/ManuallyEnteredShaSource.java | 6 +++ .../status/sources/misc/AnyBuildResult.java | 22 ++++++++- .../misc/BetterThanOrEqualBuildResult.java | 27 +++++++++- .../misc/ConditionalResult/config.groovy | 2 +- .../status/GitHubCommitStatusSetter/help.html | 3 ++ .../AnyDefinedRepositorySource/help.html | 3 ++ .../BuildDataRevisionShaSource/help.html | 3 ++ .../config.groovy | 5 +- .../ConditionalStatusResultSource/help.html | 4 ++ .../DefaultCommitContextSource/help.html | 3 ++ .../DefaultStatusResultSource/help.html | 3 ++ .../help-context.html | 3 ++ .../help.html | 3 ++ .../ManuallyEnteredShaSource/help-sha.html | 3 ++ .../ManuallyEnteredShaSource/help.html | 3 ++ .../config.groovy | 2 +- .../help-message.html | 3 ++ 34 files changed, 338 insertions(+), 65 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help-context.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help-sha.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/help-message.html 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 820af2f46..71fec736e 100644 --- a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java @@ -12,7 +12,12 @@ import static org.apache.commons.collections.CollectionUtils.isNotEmpty; /** + * With help of list of other error handlers handles exception. + * If no one will handle it, exception will be wrapped to {@link ErrorHandlingException} + * and thrown by the handle method + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class CombineErrorHandler implements ErrorHandler { private static final Logger LOG = LoggerFactory.getLogger(CombineErrorHandler.class); @@ -22,6 +27,11 @@ public class CombineErrorHandler implements ErrorHandler { private CombineErrorHandler() { } + /** + * Static factory to produce new instance of this handler + * + * @return new instance + */ public static CombineErrorHandler errorHandling() { return new CombineErrorHandler(); } @@ -33,6 +43,15 @@ public CombineErrorHandler withHandlers(List handlers) { return this; } + /** + * Handles exception with help of other handlers. If no one will handle it, it will be thrown to the top level + * + * @param e exception to handle (log, ignore, process, rethrow) + * @param run run object from the step + * @param listener listener object from the step + * + * @return true if exception handled or rethrows it + */ @Override public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { LOG.debug("Exception in {} will be processed with {} handlers", @@ -55,6 +74,9 @@ public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener throw new ErrorHandlingException(e); } + /** + * Wrapper for the not handled by this handler exceptions + */ public static class ErrorHandlingException extends RuntimeException { public ErrorHandlingException(Throwable cause) { super(cause); 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 e14f88d9b..65c4104f1 100644 --- a/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java @@ -6,8 +6,25 @@ import javax.annotation.Nonnull; /** + * So you can implement bunch of {@link ErrorHandler}s and log, rethrow, ignore exception. + * Useful to control own step exceptions + * (for example {@link org.jenkinsci.plugins.github.status.GitHubCommitStatusSetter}) + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public interface ErrorHandler { + + /** + * Normally should return true if exception is handled and no other handler should do anything. + * If you will return false, the next error handler should try to handle this exception + * + * @param e exception to handle (log, ignore, process, rethrow) + * @param run run object from the step + * @param listener listener object from the step + * + * @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; } 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 4912127c9..325261387 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 @@ -9,10 +9,20 @@ import java.io.IOException; /** + * Extension point to provide commit sha which will be used to set state + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public abstract class GitHubCommitShaSource extends AbstractDescribableImpl implements ExtensionPoint { - public abstract String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException, InterruptedException; + /** + * @param run enclosing run + * @param listener listener of the run. Can be used to fetch env vars + * + * @return plain sha to set state + */ + 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 b7840cc0c..fa21c9bd9 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 @@ -10,9 +10,18 @@ import java.util.List; /** + * Extension point to provide list of resolved repositories where commit is located + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public abstract class GitHubReposSource extends AbstractDescribableImpl implements ExtensionPoint { + /** + * @param run actual run + * @param listener build listener + * + * @return resolved list of GitHub repositories + */ public abstract List repos(@Nonnull Run run, @Nonnull 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 99183b24e..f359f1810 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 @@ -8,10 +8,19 @@ import javax.annotation.Nonnull; /** + * Extension point to provide context of the state. For example `integration-tests` or `build` + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public abstract class GitHubStatusContextSource extends AbstractDescribableImpl implements ExtensionPoint { + /** + * @param run actual run + * @param listener build listener + * + * @return simple short string to represent context of this state + */ 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 0c325e97c..81a14b811 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 @@ -10,14 +10,26 @@ import java.io.IOException; /** + * Extension point to provide exact state and message for the commit + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public abstract class GitHubStatusResultSource extends AbstractDescribableImpl implements ExtensionPoint { - public abstract StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) + /** + * @param run actual run + * @param listener run listener + * + * @return bean with state and already expanded message + */ + public abstract StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException, InterruptedException; + /** + * Bean with state and msg info + */ public static class StatusResult { private GHCommitState state; private String msg; diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java index e88def4a6..c73aa31e7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/StatusErrorHandler.java @@ -8,11 +8,19 @@ import org.jenkinsci.plugins.github.common.ErrorHandler; /** + * Extension point to provide way of how to react on errors in status setter step + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public abstract class StatusErrorHandler extends AbstractDescribableImpl implements ErrorHandler, ExtensionPoint { + /** + * Used in view + * + * @return all of the available error handlers. + */ public static DescriptorExtensionList> all() { return Jenkins.getInstance().getDescriptorList(StatusErrorHandler.class); } 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 321e79d7b..56a7f6805 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 @@ -13,16 +13,21 @@ import javax.annotation.Nonnull; /** + * This extension point allows to define when and what to send as state and message. + * It will be used as part of {@link org.jenkinsci.plugins.github.status.sources.ConditionalStatusResultSource}. + * * @author lanwen (Merkushev Kirill) + * @see org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult + * @since 1.19.0 */ public abstract class ConditionalResult extends AbstractDescribableImpl implements ExtensionPoint { - protected String status; + protected String state; protected String message; @DataBoundSetter - public void setStatus(String status) { - this.status = status; + public void setState(String state) { + this.state = state; } @DataBoundSetter @@ -30,30 +35,52 @@ public void setMessage(String message) { this.message = message; } - public String getStatus() { - return status; + /** + * @return State to set. Will be converted to {@link GHCommitState} + */ + public String getState() { + return state; } + /** + * @return Message to write. Can contain env vars, will be expanded. + */ public String getMessage() { return message; } + /** + * If matches, will be used to set state + * + * @param run to check against + * + * @return true if matches + */ public abstract boolean matches(@Nonnull Run run); + /** + * Should be extended to and marked as {@link hudson.Extension} to be in list + */ public abstract static class ConditionalResultDescriptor extends Descriptor { + /** + * Gets all available extensions. Used in view + * + * @return all descriptors of conditional results + */ public static DescriptorExtensionList> all() { return Jenkins.getInstance().getDescriptorList(ConditionalResult.class); } - public ListBoxModel doFillStatusItems() { + /** + * @return options to fill state items in view + */ + public ListBoxModel doFillStateItems() { ListBoxModel items = new ListBoxModel(); - for (GHCommitState status1 : GHCommitState.values()) { - items.add(status1.name()); + for (GHCommitState commitState : GHCommitState.values()) { + items.add(commitState.name()); } return items; } - } - - + } } 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 015acf2ca..170fa0c62 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java @@ -12,15 +12,15 @@ import hudson.tasks.Publisher; import jenkins.tasks.SimpleBuildStep; import org.jenkinsci.plugins.github.common.CombineErrorHandler; -import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; -import org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource; -import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource; -import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource; -import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource; import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource; import org.jenkinsci.plugins.github.extension.status.GitHubReposSource; 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.DefaultCommitContextSource; +import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource; import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHRepository; import org.kohsuke.stapler.DataBoundConstructor; @@ -33,7 +33,10 @@ import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; /** + * Create commit state notifications on the commits based on the outcome of the build. + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class GitHubCommitStatusSetter extends Notifier implements SimpleBuildStep { @@ -72,26 +75,44 @@ public void setErrorHandlers(List errorHandlers) { this.errorHandlers = errorHandlers; } + /** + * @return SHA provider + */ public GitHubCommitShaSource getCommitShaSource() { return commitShaSource; } + /** + * @return Repository list provider + */ public GitHubReposSource getReposSource() { return reposSource; } + /** + * @return Context provider + */ public GitHubStatusContextSource getContextSource() { return contextSource; } + /** + * @return state + msg provider + */ public GitHubStatusResultSource getStatusResultSource() { return statusResultSource; } + /** + * @return error handlers + */ public List getErrorHandlers() { return errorHandlers; } + /** + * 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) { @@ -135,7 +156,7 @@ public boolean isApplicable(Class jobType) { @Override public String getDisplayName() { - return "[NEW] Set status for GitHub"; + return "[NEW] Set status for GitHub commit"; } } 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 8fee1a3ed..b52b6346d 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 @@ -16,7 +16,10 @@ import static org.apache.commons.lang3.StringUtils.trimToEmpty; /** + * Can change build status in case of errors + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class ChangingBuildStatusErrorHandler extends StatusErrorHandler { @@ -31,6 +34,11 @@ public String getResult() { return result; } + /** + * Logs error to build console and changes build result + * + * @return true as of it terminating handler + */ @Override public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { Result toSet = Result.fromString(trimToEmpty(result)); @@ -43,11 +51,12 @@ public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener @Extension public static class ChangingBuildStatusErrorHandlerDescriptor extends Descriptor { + private static final Result[] SUPPORTED_RESULTS = { - FAILURE, + FAILURE, UNSTABLE, }; - + @Override public String getDisplayName() { return "Change build status"; 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 522f92ed9..fce8dc9ea 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 @@ -10,7 +10,10 @@ import javax.annotation.Nonnull; /** + * Just logs message to the build console and do nothing after it + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class ShallowAnyErrorHandler extends StatusErrorHandler { @@ -18,9 +21,12 @@ public class ShallowAnyErrorHandler extends StatusErrorHandler { public ShallowAnyErrorHandler() { } + /** + * @return true as of its terminating handler + */ @Override public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { - listener.error("[GitHub Commit Status Setter] Failed to update commit status on GitHub. " + + 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 1391edcb4..d6e1d1029 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 @@ -18,7 +18,10 @@ import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** + * Just uses contributors to get list of resolved repositories + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class AnyDefinedRepositorySource extends GitHubReposSource { @@ -26,6 +29,9 @@ public class AnyDefinedRepositorySource extends GitHubReposSource { public AnyDefinedRepositorySource() { } + /** + * @return all repositories which can be found by repo-contributors + */ @Override public List repos(@Nonnull Run run, @Nonnull TaskListener listener) { final Collection names = GitHubRepositoryNameContributor 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 084712388..126122b67 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 @@ -13,7 +13,10 @@ import java.io.IOException; /** + * Gets sha from build data + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class BuildDataRevisionShaSource extends GitHubCommitShaSource { @@ -21,6 +24,9 @@ public class BuildDataRevisionShaSource extends GitHubCommitShaSource { public BuildDataRevisionShaSource() { } + /** + * @return sha from git's scm build data action + */ @Override 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/ConditionalStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java index ceaddb296..6e3034055 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 @@ -4,7 +4,6 @@ import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; -import hudson.util.ListBoxModel; import org.apache.commons.lang3.EnumUtils; import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; @@ -38,6 +37,12 @@ public List getResults() { return defaultIfNull(results, Collections.emptyList()); } + /** + * First matching result win. Or will be used pending state. + * Messages are expanded with token macro and env variables + * + * @return first matched result or pending state with warn msg + */ @Override public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException, InterruptedException { @@ -45,7 +50,7 @@ public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) for (ConditionalResult conditionalResult : getResults()) { if (conditionalResult.matches(run)) { return new StatusResult( - defaultIfNull(EnumUtils.getEnum(GHCommitState.class, conditionalResult.getStatus()), ERROR), + defaultIfNull(EnumUtils.getEnum(GHCommitState.class, conditionalResult.getState()), ERROR), new ExpandableMessage(conditionalResult.getMessage()).expandAll(run, listener) ); } @@ -63,15 +68,6 @@ public static class ConditionalStatusResultSourceDescriptor extends Descriptor 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 8eef29c0d..c33971aff 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 @@ -4,7 +4,6 @@ import hudson.Extension; import hudson.Util; import hudson.model.Descriptor; -import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; @@ -12,12 +11,21 @@ import org.kohsuke.stapler.DataBoundConstructor; import javax.annotation.Nonnull; +import java.io.IOException; +import static hudson.model.Result.FAILURE; import static hudson.model.Result.SUCCESS; import static hudson.model.Result.UNSTABLE; +import static java.util.Arrays.asList; +import static org.jenkinsci.plugins.github.status.sources.misc.AnyBuildResult.onAnyResult; +import static org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult.betterThanOrEqualTo; /** + * Default way to report about build results. + * Reports about time and build status + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class DefaultStatusResultSource extends GitHubStatusResultSource { @@ -26,33 +34,24 @@ public DefaultStatusResultSource() { } @Override - public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) { - Result result = run.getResult(); + 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) String duration = Util.getTimeSpanString(System.currentTimeMillis() - run.getTimeInMillis()); - if (result == null) { // Build is ongoing - return new GitHubStatusResultSource.StatusResult( - GHCommitState.PENDING, - Messages.CommitNotifier_Pending(run.getDisplayName()) - ); - } else if (result.isBetterOrEqualTo(SUCCESS)) { - return new GitHubStatusResultSource.StatusResult( - GHCommitState.SUCCESS, - Messages.CommitNotifier_Success(run.getDisplayName(), duration) - ); - } else if (result.isBetterOrEqualTo(UNSTABLE)) { - return new GitHubStatusResultSource.StatusResult( - GHCommitState.FAILURE, - Messages.CommitNotifier_Unstable(run.getDisplayName(), duration) - ); - } else { - return new GitHubStatusResultSource.StatusResult( - GHCommitState.ERROR, - Messages.CommitNotifier_Failed(run.getDisplayName(), duration) - ); - } + return new ConditionalStatusResultSource(asList( + betterThanOrEqualTo(SUCCESS, + GHCommitState.SUCCESS, Messages.CommitNotifier_Success(run.getDisplayName(), duration)), + + betterThanOrEqualTo(UNSTABLE, + GHCommitState.FAILURE, Messages.CommitNotifier_Unstable(run.getDisplayName(), duration)), + + betterThanOrEqualTo(FAILURE, + GHCommitState.ERROR, Messages.CommitNotifier_Failed(run.getDisplayName(), duration)), + + onAnyResult(GHCommitState.PENDING, Messages.CommitNotifier_Pending(run.getDisplayName())) + )).get(run, listener); } @Extension 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 1a1179727..ee28e2dd7 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 @@ -4,25 +4,45 @@ import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.common.ExpandableMessage; import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource; import org.kohsuke.stapler.DataBoundConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; /** + * Allows to manually enter context + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class ManuallyEnteredCommitContextSource extends GitHubStatusContextSource { + private static final Logger LOG = LoggerFactory.getLogger(ManuallyEnteredCommitContextSource.class); + private String context; - + @DataBoundConstructor public ManuallyEnteredCommitContextSource(String context) { this.context = context; } + public String getContext() { + return context; + } + + /** + * Just returns what user entered. Expands env vars and token macro + */ @Override public String context(@Nonnull Run run, @Nonnull TaskListener listener) { - return context; + try { + return new ExpandableMessage(context).expandAll(run, listener); + } catch (Exception e) { + LOG.debug("Can't expand context, returning as is", e); + return context; + } } @Extension 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 426d11f39..74b353f45 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 @@ -12,7 +12,10 @@ import java.io.IOException; /** + * Allows to enter sha manually + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class ManuallyEnteredShaSource extends GitHubCommitShaSource { @@ -27,6 +30,9 @@ public String getSha() { return sha; } + /** + * Expands env vars and token macro in entered sha + */ @Override 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 2a9d499d7..947db9075 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 @@ -3,12 +3,16 @@ import hudson.Extension; import hudson.model.Run; import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.kohsuke.github.GHCommitState; import org.kohsuke.stapler.DataBoundConstructor; import javax.annotation.Nonnull; /** + * Allows to set state in any case + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class AnyBuildResult extends ConditionalResult { @@ -16,16 +20,32 @@ public class AnyBuildResult extends ConditionalResult { public AnyBuildResult() { } + /** + * @return true in any case + */ @Override public boolean matches(@Nonnull Run run) { return true; } + /** + * @param state state to set + * @param msg message to set. Can contain env vars + * + * @return new instance of this conditional result + */ + public static AnyBuildResult onAnyResult(GHCommitState state, String msg) { + AnyBuildResult cond = new AnyBuildResult(); + cond.setState(state.name()); + cond.setMessage(msg); + return cond; + } + @Extension public static class AnyBuildResultDescriptor extends ConditionalResultDescriptor { @Override public String getDisplayName() { - return "Result ANY"; + return "result ANY"; } } } 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 f861dd0aa..0242d7030 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 @@ -5,6 +5,7 @@ import hudson.model.Run; import hudson.util.ListBoxModel; import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.kohsuke.github.GHCommitState; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -18,7 +19,10 @@ import static org.apache.commons.lang3.StringUtils.trimToEmpty; /** + * if run result better than or equal to selected + * * @author lanwen (Merkushev Kirill) + * @since 1.19.0 */ public class BetterThanOrEqualBuildResult extends ConditionalResult { @@ -37,13 +41,34 @@ public String getResult() { return result; } + /** + * @return matches if run result better than or equal to selected + */ @Override public boolean matches(@Nonnull Run run) { return defaultIfNull(run.getResult(), Result.NOT_BUILT).isBetterOrEqualTo(fromString(trimToEmpty(result))); } + /** + * Convenient way to reuse logic of checking for the build status + * + * @param result to check against + * @param state state to set + * @param msg message to set. Can contain env vars + * + * @return new instance of this conditional result + */ + public static BetterThanOrEqualBuildResult betterThanOrEqualTo(Result result, GHCommitState state, String msg) { + BetterThanOrEqualBuildResult conditional = new BetterThanOrEqualBuildResult(); + conditional.setResult(result.toString()); + conditional.setState(state.name()); + conditional.setMessage(msg); + return conditional; + } + @Extension public static class BetterThanOrEqualBuildResultDescriptor extends ConditionalResultDescriptor { + private static final Result[] SUPPORTED_RESULTS = { SUCCESS, UNSTABLE, @@ -52,7 +77,7 @@ public static class BetterThanOrEqualBuildResultDescriptor extends ConditionalRe @Override public String getDisplayName() { - return "Result better than or equal to"; + return "result better than or equal to"; } @SuppressWarnings("unused") diff --git a/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy index 74566a837..73a57e2a6 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy +++ b/src/main/resources/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult/config.groovy @@ -3,7 +3,7 @@ package org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult def f = namespace(lib.FormTagLib); -f.entry(title: _('Status'), field: 'status') { +f.entry(title: _('Status'), field: 'state') { f.select() } 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 new file mode 100644 index 000000000..a969a0037 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter/help.html @@ -0,0 +1,3 @@ +
+ Using GitHub status api sets status of the commit +
\ No newline at end of file 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 new file mode 100644 index 000000000..06ec1a2a4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource/help.html @@ -0,0 +1,3 @@ +
+ Any repository provided by the programmatic contributors list +
\ No newline at end of file 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 new file mode 100644 index 000000000..3ef306832 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource/help.html @@ -0,0 +1,3 @@ +
+ Uses data-action (located at ${build.url}/git/) to determine actual SHA +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy index 2bf0c52b8..9e16174a4 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/config.groovy @@ -5,11 +5,14 @@ import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult.Cond def f = namespace(lib.FormTagLib); +f.helpLink(url: descriptor.getHelpFile()) +f.helpArea() + f.block { f.hetero_list(items: CollectionUtils.isEmpty(instance?.results) ? [] : instance.results, - addCaption: 'If build', + addCaption: 'If Run', name: 'results', oneEach: false, hasHeader: true, descriptors: ConditionalResultDescriptor.all()) } diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/help.html new file mode 100644 index 000000000..7c6ac5e12 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource/help.html @@ -0,0 +1,4 @@ +
+ You can define in which cases you want to publish exact state and message for the commit. You can define multiply 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 new file mode 100644 index 000000000..41cfb814a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource/help.html @@ -0,0 +1,3 @@ +
+ Uses display name property defined in "Github project property" with fallback to job name. +
\ No newline at end of file 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 new file mode 100644 index 000000000..d9a7ebf49 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource/help.html @@ -0,0 +1,3 @@ +
+ Writes simple message about build result and duration +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help-context.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help-context.html new file mode 100644 index 000000000..e64c8ab5a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help-context.html @@ -0,0 +1,3 @@ +
+ Allows env vars and token macro +
\ No newline at end of file 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 new file mode 100644 index 000000000..1b6bd211e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource/help.html @@ -0,0 +1,3 @@ +
+ You can define context name manually +
\ No newline at end of file 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 new file mode 100644 index 000000000..da5ec9ebc --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help-sha.html @@ -0,0 +1,3 @@ +
+ Allows env vars and token macro +
\ No newline at end of file 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 new file mode 100644 index 000000000..9829ba7da --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource/help.html @@ -0,0 +1,3 @@ +
+ Allows to define commit sha manually +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy index 9ec0b0204..de36d678d 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/config.groovy @@ -8,7 +8,7 @@ f.entry(title: _('Build result better than or equal to'), field: 'result') { f.select() } -f.entry(title: _('Status'), field: 'status') { +f.entry(title: _('Status'), field: 'state') { f.select() } 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 new file mode 100644 index 000000000..da5ec9ebc --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult/help-message.html @@ -0,0 +1,3 @@ +
+ Allows env vars and token macro +
\ No newline at end of file From 040d31c984ff70d046211c2c083f08a27695ec0a Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 24 Apr 2016 22:37:50 +0300 Subject: [PATCH 188/228] tests for new status setter --- .../status/misc/ConditionalResult.java | 4 +- .../err/ChangingBuildStatusErrorHandler.java | 7 +- .../status/err/ShallowAnyErrorHandler.java | 4 +- .../ConditionalStatusResultSource.java | 2 + .../sources/DefaultCommitContextSource.java | 3 +- .../misc/BetterThanOrEqualBuildResult.java | 6 +- .../common/CombineErrorHandlerTest.java | 96 +++++++++++++ .../status/GitHubCommitStatusSetterTest.java | 134 ++++++++++++++++++ .../github/status/err/ErrorHandlersTest.java | 53 +++++++ .../ConditionalStatusResultSourceTest.java | 82 +++++++++++ .../DefaultStatusResultSourceTest.java | 57 ++++++++ .../sources/ManuallyEnteredSourcesTest.java | 51 +++++++ .../sources/misc/AnyBuildResultTest.java | 30 ++++ .../BetterThanOrEqualBuildResultTest.java | 55 +++++++ 14 files changed, 568 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java 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 56a7f6805..c1486b331 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 @@ -22,8 +22,8 @@ */ public abstract class ConditionalResult extends AbstractDescribableImpl implements ExtensionPoint { - protected String state; - protected String message; + private String state; + private String message; @DataBoundSetter public void setState(String state) { 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 b52b6346d..1400f9822 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 @@ -51,11 +51,8 @@ public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener @Extension public static class ChangingBuildStatusErrorHandlerDescriptor extends Descriptor { - - private static final Result[] SUPPORTED_RESULTS = { - FAILURE, - UNSTABLE, - }; + + private static final Result[] SUPPORTED_RESULTS = {FAILURE, UNSTABLE}; @Override public String getDisplayName() { 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 fce8dc9ea..ed389b7dc 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 @@ -26,8 +26,8 @@ public ShallowAnyErrorHandler() { */ @Override 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()); + 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/ConditionalStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java index 6e3034055..268ee604b 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 @@ -21,6 +21,8 @@ import static org.kohsuke.github.GHCommitState.PENDING; /** + * Allows to define message and state for commit for different run results + * * @author lanwen (Merkushev Kirill) */ public class ConditionalStatusResultSource extends GitHubStatusResultSource { 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 7c68bf1df..fbd1d3ccb 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 @@ -2,7 +2,6 @@ import hudson.Extension; import hudson.model.Descriptor; -import hudson.model.Job; import hudson.model.Run; import hudson.model.TaskListener; import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource; @@ -26,7 +25,7 @@ public DefaultCommitContextSource() { /** * @return context name - * @see com.coravy.hudson.plugins.github.GithubProjectProperty#displayNameFor(Job) + * @see com.coravy.hudson.plugins.github.GithubProjectProperty#displayNameFor(hudson.model.Job) */ @Override public String context(@Nonnull Run run, @Nonnull TaskListener listener) { 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 0242d7030..9600e4b22 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 @@ -69,11 +69,7 @@ public static BetterThanOrEqualBuildResult betterThanOrEqualTo(Result result, GH @Extension public static class BetterThanOrEqualBuildResultDescriptor extends ConditionalResultDescriptor { - private static final Result[] SUPPORTED_RESULTS = { - SUCCESS, - UNSTABLE, - FAILURE, - }; + private static final Result[] SUPPORTED_RESULTS = {SUCCESS, UNSTABLE, FAILURE}; @Override public String getDisplayName() { diff --git a/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java b/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java new file mode 100644 index 000000000..1fc88683d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java @@ -0,0 +1,96 @@ +package org.jenkinsci.plugins.github.common; + +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.status.err.ChangingBuildStatusErrorHandler; +import org.jenkinsci.plugins.github.status.err.ShallowAnyErrorHandler; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.annotation.Nonnull; +import java.util.Collections; + +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.common.CombineErrorHandler.errorHandling; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class CombineErrorHandlerTest { + + @Mock(answer = Answers.RETURNS_MOCKS) + private Run run; + + @Mock + private TaskListener listener; + + @Rule + public ExpectedException exc = ExpectedException.none(); + + @Test + public void shouldRethrowExceptionIfNoMatch() throws Exception { + exc.expect(CombineErrorHandler.ErrorHandlingException.class); + + errorHandling().handle(new RuntimeException(), run, listener); + } + + @Test + public void shouldRethrowExceptionIfNullHandlersList() throws Exception { + exc.expect(CombineErrorHandler.ErrorHandlingException.class); + + errorHandling().withHandlers(null).handle(new RuntimeException(), run, listener); + } + + @Test + public void shouldHandleExceptionsWithHandler() throws Exception { + boolean handled = errorHandling() + .withHandlers(Collections.singletonList(new ShallowAnyErrorHandler())) + .handle(new RuntimeException(), run, listener); + + assertThat("handling", handled, is(true)); + } + + @Test + public void shouldRethrowExceptionIfExceptionInside() throws Exception { + exc.expect(CombineErrorHandler.ErrorHandlingException.class); + + errorHandling() + .withHandlers(Collections.singletonList( + new ErrorHandler() { + @Override + public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + throw new RuntimeException("wow"); + } + } + )) + .handle(new RuntimeException(), run, listener); + } + + @Test + public void shouldHandleExceptionWithFirstMatchAndSetStatus() throws Exception { + boolean handled = errorHandling() + .withHandlers(asList( + new ChangingBuildStatusErrorHandler(Result.FAILURE.toString()), + new ShallowAnyErrorHandler() + )) + .handle(new RuntimeException(), run, listener); + + assertThat("handling", handled, is(true)); + + verify(run).setResult(Result.FAILURE); + verify(run, times(2)).getParent(); + verifyNoMoreInteractions(run); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java b/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java new file mode 100644 index 000000000..1b13af21a --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java @@ -0,0 +1,134 @@ +package org.jenkinsci.plugins.github.status; + +import com.cloudbees.jenkins.GitHubSetCommitStatusBuilder; +import com.github.tomakehurst.wiremock.common.Slf4jNotifier; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.plugins.git.Revision; +import hudson.plugins.git.util.BuildData; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; +import org.jenkinsci.plugins.github.status.err.ChangingBuildStatusErrorHandler; +import org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource; +import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource; +import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource; +import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource; +import org.jenkinsci.plugins.github.test.GHMockRule; +import org.jenkinsci.plugins.github.test.GHMockRule.FixedGHRepoNameTestContributor; +import org.jenkinsci.plugins.github.test.InjectJenkinsMembersRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.TestExtension; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.inject.Inject; +import java.util.Collections; + +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link GitHubSetCommitStatusBuilder}. + * + * @author Oleg Nenashev + */ +@RunWith(MockitoJUnitRunner.class) +public class GitHubCommitStatusSetterTest { + + public static final String SOME_SHA = StringUtils.repeat("f", 40); + + @Mock + public BuildData data; + + @Mock + public Revision rev; + + @Inject + public GitHubPluginConfig config; + + public JenkinsRule jRule = new JenkinsRule(); + + @Rule + public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); + + @Rule + public GHMockRule github = new GHMockRule( + new WireMockRule( + wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)) + )) + .stubUser() + .stubRepo() + .stubStatuses(); + + @Rule + public ExternalResource prep = new ExternalResource() { + @Override + protected void before() throws Throwable { + when(data.getLastBuiltRevision()).thenReturn(rev); + data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); + when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); + } + }; + + + @Test + public void shouldSetGHCommitStatus() throws Exception { + config.getConfigs().add(github.serverConfig()); + FreeStyleProject prj = jRule.createFreeStyleProject(); + + GitHubCommitStatusSetter statusSetter = new GitHubCommitStatusSetter(); + statusSetter.setCommitShaSource(new BuildDataRevisionShaSource()); + statusSetter.setContextSource(new DefaultCommitContextSource()); + statusSetter.setReposSource(new AnyDefinedRepositorySource()); + statusSetter.setStatusResultSource(new DefaultStatusResultSource()); + + + prj.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { + build.addAction(data); + return true; + } + }); + + prj.getPublishersList().add(statusSetter); + prj.scheduleBuild2(0).get(); + + github.service().verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); + } + + @Test + public void shouldHandleError() throws Exception { + FreeStyleProject prj = jRule.createFreeStyleProject(); + + GitHubCommitStatusSetter statusSetter = new GitHubCommitStatusSetter(); + statusSetter.setCommitShaSource(new BuildDataRevisionShaSource()); + statusSetter.setErrorHandlers(Collections.singletonList( + new ChangingBuildStatusErrorHandler(Result.UNSTABLE.toString()) + )); + statusSetter.setReposSource(new AnyDefinedRepositorySource()); + statusSetter.setStatusResultSource(new DefaultStatusResultSource()); + + prj.getPublishersList().add(statusSetter); + FreeStyleBuild build = prj.scheduleBuild2(0).get(); + jRule.assertBuildStatus(Result.UNSTABLE, build); + } + + @TestExtension + public static final FixedGHRepoNameTestContributor CONTRIBUTOR = new FixedGHRepoNameTestContributor(); +} diff --git a/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java b/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java new file mode 100644 index 000000000..d225e9660 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java @@ -0,0 +1,53 @@ +package org.jenkinsci.plugins.github.status.err; + +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.verify; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class ErrorHandlersTest { + + @Mock + private Run run; + + @Mock + private TaskListener listener; + + @Test + public void shouldSetFailureResultStatus() throws Exception { + boolean handled = new ChangingBuildStatusErrorHandler(Result.FAILURE.toString()) + .handle(new RuntimeException(), run, listener); + + verify(run).setResult(Result.FAILURE); + assertThat("handling", handled, is(true)); + } + + @Test + public void shouldSetFailureResultStatusOnUnknownSetup() throws Exception { + boolean handled = new ChangingBuildStatusErrorHandler("") + .handle(new RuntimeException(), run, listener); + + verify(run).setResult(Result.FAILURE); + assertThat("handling", handled, is(true)); + } + + @Test + public void shouldHandleAndDoNothing() throws Exception { + boolean handled = new ShallowAnyErrorHandler().handle(new RuntimeException(), run, listener); + assertThat("handling", handled, is(true)); + + Mockito.verifyNoMoreInteractions(run); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java new file mode 100644 index 000000000..683d7a037 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java @@ -0,0 +1,82 @@ +package org.jenkinsci.plugins.github.status.sources; + +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; +import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.jenkinsci.plugins.github.status.sources.misc.AnyBuildResult; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHCommitState; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; + +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult.betterThanOrEqualTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class ConditionalStatusResultSourceTest { + + @Mock(answer = Answers.RETURNS_MOCKS) + private Run run; + + @Mock(answer = Answers.RETURNS_MOCKS) + private TaskListener listener; + + @Test + public void shouldReturnPendingByDefault() throws Exception { + GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource(null).get(run, listener); + + assertThat("state", res.getState(), is(GHCommitState.PENDING)); + assertThat("msg", res.getMsg(), notNullValue()); + } + + @Test + public void shouldReturnPendingIfNoMatch() throws Exception { + when(run.getResult()).thenReturn(Result.FAILURE); + + GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource( + Collections.singletonList( + betterThanOrEqualTo(Result.SUCCESS, GHCommitState.SUCCESS, "2") + )) + .get(run, listener); + + assertThat("state", res.getState(), is(GHCommitState.PENDING)); + assertThat("msg", res.getMsg(), notNullValue()); + } + + @Test + public void shouldReturnFirstMatch() throws Exception { + GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource(asList( + AnyBuildResult.onAnyResult(GHCommitState.FAILURE, "1"), + betterThanOrEqualTo(Result.SUCCESS, GHCommitState.SUCCESS, "2") + )).get(run, listener); + + assertThat("state", res.getState(), is(GHCommitState.FAILURE)); + assertThat("msg", res.getMsg(), notNullValue()); + } + + @Test + public void shouldReturnFirstMatch2() throws Exception { + when(run.getResult()).thenReturn(Result.SUCCESS); + + GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource(asList( + betterThanOrEqualTo(Result.SUCCESS, GHCommitState.SUCCESS, "2"), + AnyBuildResult.onAnyResult(GHCommitState.FAILURE, "1") + )).get(run, listener); + + assertThat("state", res.getState(), is(GHCommitState.SUCCESS)); + assertThat("msg", res.getMsg(), notNullValue()); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java new file mode 100644 index 000000000..d4a93e6c3 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java @@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.github.status.sources; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHCommitState; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(DataProviderRunner.class) +public class DefaultStatusResultSourceTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock(answer = Answers.RETURNS_MOCKS) + private Run run; + + @Mock(answer = Answers.RETURNS_MOCKS) + private TaskListener listener; + + @DataProvider + public static Object[][] results() { + return new Object[][]{ + {Result.SUCCESS, GHCommitState.SUCCESS}, + {Result.UNSTABLE, GHCommitState.FAILURE}, + {Result.FAILURE, GHCommitState.ERROR}, + {null, GHCommitState.PENDING}, + }; + } + + @Test + @UseDataProvider("results") + public void shouldReturnConditionalResult(Result actual, GHCommitState expected) throws Exception { + when(run.getResult()).thenReturn(actual); + + GitHubStatusResultSource.StatusResult result = new DefaultStatusResultSource().get(run, listener); + assertThat("state", result.getState(), is(expected)); + } + +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java new file mode 100644 index 000000000..2aea545ba --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java @@ -0,0 +1,51 @@ +package org.jenkinsci.plugins.github.status.sources; + +import hudson.EnvVars; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class ManuallyEnteredSourcesTest { + + public static final String EXPANDED = "expanded"; + @Mock(answer = Answers.RETURNS_MOCKS) + private Run run; + + @Mock(answer = Answers.RETURNS_MOCKS) + private TaskListener listener; + + @Mock(answer = Answers.RETURNS_MOCKS) + private EnvVars env; + + + @Test + public void shouldExpandContext() throws Exception { + when(run.getEnvironment(listener)).thenReturn(env); + when(env.expand(Matchers.anyString())).thenReturn(EXPANDED); + + String context = new ManuallyEnteredCommitContextSource("").context(run, listener); + assertThat(context, equalTo(EXPANDED)); + } + + @Test + public void shouldExpandSha() throws Exception { + when(run.getEnvironment(listener)).thenReturn(env); + when(env.expand(Matchers.anyString())).thenReturn(EXPANDED); + + String context = new ManuallyEnteredShaSource("").get(run, listener); + assertThat(context, equalTo(EXPANDED)); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java new file mode 100644 index 000000000..8b904b06a --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java @@ -0,0 +1,30 @@ +package org.jenkinsci.plugins.github.status.sources.misc; + +import hudson.model.Run; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHCommitState; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(MockitoJUnitRunner.class) +public class AnyBuildResultTest { + + @Mock + private Run run; + + @Test + public void shouldMatchEveryTime() throws Exception { + boolean matches = AnyBuildResult.onAnyResult(GHCommitState.ERROR, "").matches(run); + + assertTrue("matching", matches); + verifyNoMoreInteractions(run); + } + +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java new file mode 100644 index 000000000..ff5c13f5d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java @@ -0,0 +1,55 @@ +package org.jenkinsci.plugins.github.status.sources.misc; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import hudson.model.Result; +import hudson.model.Run; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHCommitState; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.hamcrest.Matchers.is; +import static org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult.betterThanOrEqualTo; +import static org.junit.Assert.assertThat; + +/** + * @author lanwen (Merkushev Kirill) + */ +@RunWith(DataProviderRunner.class) +public class BetterThanOrEqualBuildResultTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private Run run; + + @DataProvider + public static Object[][] results() { + return new Object[][]{ + {Result.SUCCESS, Result.SUCCESS, true}, + {Result.UNSTABLE, Result.UNSTABLE, true}, + {Result.FAILURE, Result.FAILURE, true}, + {Result.FAILURE, Result.UNSTABLE, true}, + {Result.FAILURE, Result.SUCCESS, true}, + {Result.SUCCESS, Result.FAILURE, false}, + {Result.SUCCESS, Result.UNSTABLE, false}, + {Result.UNSTABLE, Result.FAILURE, false}, + }; + } + + @Test + @UseDataProvider("results") + public void shouldMatch(Result defined, Result real, boolean expect) throws Exception { + Mockito.when(run.getResult()).thenReturn(real); + + boolean matched = betterThanOrEqualTo(defined, GHCommitState.FAILURE, "").matches(run); + assertThat("matching", matched, is(expect)); + } +} \ No newline at end of file From 66917efed3155852da79539552f2a921c2e61d9b Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Sun, 24 Apr 2016 23:38:29 +0300 Subject: [PATCH 189/228] reuse new status step code in old classes Deprecate them. Make PENDING setter not to fail build on any errors, as of most of the people don't want to fail the entire build because of we can't set status --- .../jenkins/GitHubCommitNotifier.java | 148 ++++++------------ .../jenkins/GitHubSetCommitStatusBuilder.java | 59 +++---- .../status/GitHubCommitStatusSetter.java | 2 +- .../jenkins/GitHubCommitNotifier/help.html | 5 + .../com/cloudbees/jenkins/Messages.properties | 2 +- .../GitHubSetCommitStatusBuilderTest.java | 5 +- 6 files changed, 85 insertions(+), 136 deletions(-) create mode 100644 src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/help.html diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index 3d58d667d..aea073e81 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -1,9 +1,9 @@ package com.cloudbees.jenkins; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; -import hudson.Util; import hudson.model.AbstractProject; import hudson.model.Result; import hudson.model.Run; @@ -14,32 +14,38 @@ import hudson.tasks.Publisher; import hudson.util.ListBoxModel; import jenkins.tasks.SimpleBuildStep; -import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.common.ExpandableMessage; -import org.jenkinsci.plugins.github.util.BuildDataHelper; +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; +import org.jenkinsci.plugins.github.status.GitHubCommitStatusSetter; +import org.jenkinsci.plugins.github.status.err.ChangingBuildStatusErrorHandler; +import org.jenkinsci.plugins.github.status.err.ShallowAnyErrorHandler; +import org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource; +import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource; +import org.jenkinsci.plugins.github.status.sources.ConditionalStatusResultSource; +import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource; +import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.github.GHCommitState; -import org.kohsuke.github.GHRepository; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Collections; import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_DisplayName; -import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; -import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; 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; -import static java.lang.String.format; -import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; +import static java.util.Arrays.asList; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.trimToEmpty; +import static org.jenkinsci.plugins.github.status.sources.misc.AnyBuildResult.onAnyResult; +import static org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult.betterThanOrEqualTo; /** * Create commit status notifications on the commits based on the outcome of the build. @@ -107,105 +113,41 @@ public BuildStepMonitor getRequiredMonitorService() { } @Override - public void perform(Run build, - FilePath ws, - Launcher launcher, - TaskListener listener) throws InterruptedException, IOException { - try { - updateCommitStatus(build, listener); - } catch (IOException error) { - final Result buildResult = getEffectiveResultOnFailure(); - if (buildResult.equals(FAILURE)) { - throw error; - } else { - listener.error(format("[GitHub Commit Notifier] - %s", error.getMessage())); - listener.getLogger().println( - format("[GitHub Commit Notifier] - Build result will be set to %s", buildResult) - ); - build.setResult(buildResult); - } - } - } - - private void updateCommitStatus(@Nonnull Run build, - @Nonnull TaskListener listener) throws InterruptedException, IOException { - final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); - - StatusResult status = statusFrom(build); - String message = defaultIfEmpty(firstNonNull(statusMessage, DEFAULT_MESSAGE) - .expandAll(build, listener), status.getMsg()); - String contextName = displayNameFor(build.getParent()); - - for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getParent())) { - for (GHRepository repository : name.resolve()) { - - listener.getLogger().println( - GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1) - ); - - try { - repository.createCommitStatus( - sha1, status.getState(), build.getAbsoluteUrl(), - message, - contextName - ); - } catch (FileNotFoundException e) { - // PR builds and other merge activities can create a merge commit that - // doesn't exist in the upstream. Don't let the build fail - // TODO: ideally we'd like other plugins to designate a commit to put the status update to - LOGGER.debug("Failed to update commit status", e); - listener.getLogger() - .format("Commit doesn't exist in %s. Status is not set%n", repository.getFullName()); - } - } - } - } - - private static StatusResult statusFrom(@Nonnull Run build) { - Result result = build.getResult(); - - // We do not use `build.getDurationString()` because it appends 'and counting' (build is still running) - String duration = Util.getTimeSpanString(System.currentTimeMillis() - build.getTimeInMillis()); - - if (result == null) { // Build is ongoing - return new StatusResult( - GHCommitState.PENDING, - Messages.CommitNotifier_Pending(build.getDisplayName()) - ); - } else if (result.isBetterOrEqualTo(SUCCESS)) { - return new StatusResult( - GHCommitState.SUCCESS, - Messages.CommitNotifier_Success(build.getDisplayName(), duration) - ); - } else if (result.isBetterOrEqualTo(UNSTABLE)) { - return new StatusResult( - GHCommitState.FAILURE, - Messages.CommitNotifier_Unstable(build.getDisplayName(), duration) - ); + public void perform(@NonNull Run build, + @NonNull FilePath ws, + @NonNull Launcher launcher, + @NonNull TaskListener listener) throws InterruptedException, IOException { + + GitHubCommitStatusSetter setter = new GitHubCommitStatusSetter(); + setter.setReposSource(new AnyDefinedRepositorySource()); + setter.setCommitShaSource(new BuildDataRevisionShaSource()); + setter.setContextSource(new DefaultCommitContextSource()); + + + String content = firstNonNull(statusMessage, DEFAULT_MESSAGE).getContent(); + + if (isNotBlank(content)) { + setter.setStatusResultSource(new ConditionalStatusResultSource( + asList( + betterThanOrEqualTo(SUCCESS, GHCommitState.SUCCESS, content), + betterThanOrEqualTo(UNSTABLE, GHCommitState.FAILURE, content), + betterThanOrEqualTo(FAILURE, GHCommitState.ERROR, content), + onAnyResult(GHCommitState.PENDING, content) + ))); } else { - return new StatusResult( - GHCommitState.ERROR, - Messages.CommitNotifier_Failed(build.getDisplayName(), duration) - ); + setter.setStatusResultSource(new DefaultStatusResultSource()); } - } - private static class StatusResult { - private GHCommitState state; - private String msg; - - public StatusResult(GHCommitState state, String msg) { - this.state = state; - this.msg = msg; - } - - public GHCommitState getState() { - return state; + if (getEffectiveResultOnFailure().equals(SUCCESS)) { + setter.setErrorHandlers(Collections.singletonList(new ShallowAnyErrorHandler())); + } else if (resultOnFailure == null) { + setter.setErrorHandlers(null); + } else { + setter.setErrorHandlers(Collections.singletonList( + new ChangingBuildStatusErrorHandler(getEffectiveResultOnFailure().toString()))); } - public String getMsg() { - return msg; - } + setter.perform(build, ws, launcher, listener); } @Extension diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 3aa15df14..c5a746ee7 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -1,5 +1,6 @@ package com.cloudbees.jenkins; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; @@ -9,21 +10,25 @@ import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import jenkins.tasks.SimpleBuildStep; - -import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.common.ExpandableMessage; -import org.jenkinsci.plugins.github.util.BuildDataHelper; +import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; +import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; +import org.jenkinsci.plugins.github.status.GitHubCommitStatusSetter; +import org.jenkinsci.plugins.github.status.err.ShallowAnyErrorHandler; +import org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource; +import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource; +import org.jenkinsci.plugins.github.status.sources.ConditionalStatusResultSource; +import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource; import org.kohsuke.github.GHCommitState; -import org.kohsuke.github.GHRepository; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import java.io.IOException; +import java.util.Collections; -import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_SettingCommitStatus; -import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; 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; @Extension public class GitHubSetCommitStatusBuilder extends Builder implements SimpleBuildStep { @@ -51,29 +56,27 @@ public void setStatusMessage(ExpandableMessage statusMessage) { } @Override - public void perform(Run build, - FilePath workspace, - Launcher launcher, - TaskListener listener) throws InterruptedException, IOException { - final String sha1 = ObjectId.toString(BuildDataHelper.getCommitSHA1(build)); - String message = defaultIfEmpty( - firstNonNull(statusMessage, DEFAULT_MESSAGE).expandAll(build, listener), - Messages.CommitNotifier_Pending(build.getDisplayName()) - ); - String contextName = displayNameFor(build.getParent()); + public void perform(@NonNull Run build, + @NonNull FilePath workspace, + @NonNull Launcher launcher, + @NonNull TaskListener listener) throws InterruptedException, IOException { - for (GitHubRepositoryName name : GitHubRepositoryNameContributor.parseAssociatedNames(build.getParent())) { - for (GHRepository repository : name.resolve()) { - listener.getLogger().println( - GitHubCommitNotifier_SettingCommitStatus(repository.getHtmlUrl() + "/commit/" + sha1) - ); - repository.createCommitStatus(sha1, - GHCommitState.PENDING, - build.getAbsoluteUrl(), - message, - contextName); - } - } + GitHubCommitStatusSetter setter = new GitHubCommitStatusSetter(); + setter.setReposSource(new AnyDefinedRepositorySource()); + setter.setCommitShaSource(new BuildDataRevisionShaSource()); + setter.setContextSource(new DefaultCommitContextSource()); + setter.setErrorHandlers(Collections.singletonList(new ShallowAnyErrorHandler())); + + setter.setStatusResultSource(new ConditionalStatusResultSource( + Collections.singletonList( + onAnyResult( + GHCommitState.PENDING, + defaultIfEmpty(firstNonNull(statusMessage, DEFAULT_MESSAGE).getContent(), + Messages.CommitNotifier_Pending(build.getDisplayName())) + ) + ))); + + setter.perform(build, workspace, launcher, listener); } @Extension 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 170fa0c62..d479933cb 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetter.java @@ -156,7 +156,7 @@ public boolean isApplicable(Class jobType) { @Override public String getDisplayName() { - return "[NEW] Set status for GitHub commit"; + return "Set status for GitHub commit [universal]"; } } diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/help.html b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/help.html new file mode 100644 index 000000000..191dc30b3 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/GitHubCommitNotifier/help.html @@ -0,0 +1,5 @@ +
+ This notifier will set GH commit status. + This step is DEPRECATED and will be migrated to new step in one of the next major plugin releases.
+ Please refer to new universal step. +
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/Messages.properties b/src/main/resources/com/cloudbees/jenkins/Messages.properties index da9c395e8..7e7b4f134 100644 --- a/src/main/resources/com/cloudbees/jenkins/Messages.properties +++ b/src/main/resources/com/cloudbees/jenkins/Messages.properties @@ -3,5 +3,5 @@ CommitNotifier.Unstable=Build {0} found unstable in {1} CommitNotifier.Failed=Build {0} failed in {1} CommitNotifier.Pending=Build {0} in progress... GitHubCommitNotifier.SettingCommitStatus=Setting commit status on GitHub for {0} -GitHubCommitNotifier.DisplayName=Set build status on GitHub commit +GitHubCommitNotifier.DisplayName=Set build status on GitHub commit [deprecated] GitHubSetCommitStatusBuilder.DisplayName=Set build status to "pending" on GitHub commit diff --git a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java index f879b292a..7e03528b7 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java @@ -84,12 +84,11 @@ protected void before() throws Throwable { @Test @Issue("JENKINS-23641") - public void testNoBuildData() throws Exception { + public void shouldIgnoreIfNoBuildData() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject("23641_noBuildData"); prj.getBuildersList().add(new GitHubSetCommitStatusBuilder()); Build b = prj.scheduleBuild2(0).get(); - jRule.assertBuildStatus(Result.FAILURE, b); - jRule.assertLogContains(org.jenkinsci.plugins.github.util.Messages.BuildDataHelper_NoBuildDataError(), b); + jRule.assertBuildStatus(Result.SUCCESS, b); } @Test From 2b2b1b988e2ecdebb3824d0647d0ec1b02022ec0 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 28 Apr 2016 06:29:02 -0400 Subject: [PATCH 190/228] Improved logging format. --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index a956588df..c2d343fbb 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -108,7 +108,6 @@ private boolean runPolling() { public void run() { if (runPolling()) { - String name = " #" + job.getNextBuildNumber(); GitHubPushCause cause; try { cause = new GitHubPushCause(getLogFile(), pushBy); @@ -117,9 +116,9 @@ public void run() { cause = new GitHubPushCause(pushBy); } if (asParameterizedJobMixIn(job).scheduleBuild(cause)) { - LOGGER.info("SCM changes detected in " + job.getName() + ". Triggering " + name); + LOGGER.info("SCM changes detected in " + job.getFullName() + ". Triggering #" + job.getNextBuildNumber()); } else { - LOGGER.info("SCM changes detected in " + job.getName() + ". Job is already in the queue"); + LOGGER.info("SCM changes detected in " + job.getFullName() + ". Job is already in the queue"); } } } From 7a164607712fd2e118b3e2d76aa5705b7c0435de Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 28 Apr 2016 06:38:03 -0400 Subject: [PATCH 191/228] I buy a 3k screen so that Checkstyle can complain I am using more than the leftmost third of it? --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index c2d343fbb..0274fb9df 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -116,7 +116,8 @@ public void run() { cause = new GitHubPushCause(pushBy); } if (asParameterizedJobMixIn(job).scheduleBuild(cause)) { - LOGGER.info("SCM changes detected in " + job.getFullName() + ". Triggering #" + job.getNextBuildNumber()); + LOGGER.info("SCM changes detected in " + job.getFullName() + + ". Triggering #" + job.getNextBuildNumber()); } else { LOGGER.info("SCM changes detected in " + job.getFullName() + ". Job is already in the queue"); } From 3f9b6ef06bf80485b933ff7fa26465519eb75ed8 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Mon, 2 May 2016 02:08:39 +0400 Subject: [PATCH 192/228] [maven-release-plugin] prepare release github-1.19.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fe252dcda..4c49d09b6 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.18.3-SNAPSHOT + 1.19.0 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + github-1.19.0 JIRA From e713bfbad436817d02c376d5685e60db4c0b6583 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Mon, 2 May 2016 02:08:45 +0400 Subject: [PATCH 193/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4c49d09b6..94d642672 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.0 + 1.19.1-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - github-1.19.0 + HEAD JIRA From e595e9a4b94ffb433d2338646a50c7b53f25da92 Mon Sep 17 00:00:00 2001 From: Jeremiah Njoroge Date: Mon, 2 May 2016 22:30:24 -0700 Subject: [PATCH 194/228] Minor grammatical fix --- .../github/config/GitHubServerConfig/help-credentialsId.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 c5289aa14..cf4e8e9bf 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 @@ -1,5 +1,5 @@
- You can create own personal access token at GitHub settings. + You can create your own personal access token in your account GitHub settings.
Token should be registered with scopes:
    @@ -17,7 +17,7 @@

    - If you have existed GitHub login and password you can convert it to token automatically with help of «Manage + If you have an existing GitHub login and password you can convert it to a token automatically with help of «Manage additional GitHub actions»

From 7682bf3d863e68a78adacce1baa6dbf435002b57 Mon Sep 17 00:00:00 2001 From: Jeremiah Njoroge Date: Tue, 3 May 2016 02:41:23 -0700 Subject: [PATCH 195/228] Minor grammatical fix to error messages (#122) --- .../org/jenkinsci/plugins/github/Messages.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties index ea70a50d8..9d0342903 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties @@ -2,6 +2,6 @@ global.config.url.is.empty=Jenkins URL is empty. Set explicitly Jenkins URL in g 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 common.expandable.message.title=Expandable message hooks.problem.administrative.monitor.displayname=GitHub Hooks Problems -hooks.problem.administrative.monitor.description=Some of the hooks fails to be registered or 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} fails to be registered or removed. More info can be found on global manage page. This message will be dismissed if Jenkins will receive PING event from repo or repo will be ignored in global configuration. +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. unknown.error=Unknown error From d4af6bedc40ac466ac265c9a8cfa626f72e20ed1 Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Wed, 4 May 2016 23:27:34 +0300 Subject: [PATCH 196/228] Updated okhttp. (#123) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 94d642672..544a78be3 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ com.squareup.okhttp okhttp-urlconnection - 2.5.0 + 2.7.5 false From d9af40249c7daee2c4ad0251eb624d05c410b801 Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Sun, 8 May 2016 20:04:08 +0200 Subject: [PATCH 197/228] use tagname without artifact prefix for releases (#124) --- pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pom.xml b/pom.xml index 544a78be3..0d2666ce4 100644 --- a/pom.xml +++ b/pom.xml @@ -282,6 +282,19 @@
+ + + maven-release-plugin + + v@{project.version} + forked-path + false + clean install + deploy + ${arguments} + jenkins-release,${releaseProfiles} + +
From bf050c1c538293bf4705cfbefdc4e5a7a9e7c3aa Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Thu, 12 May 2016 19:43:17 +0400 Subject: [PATCH 198/228] [maven-release-plugin] prepare release v1.19.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d2666ce4..43df40e59 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.1-SNAPSHOT + 1.19.1 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + v1.19.1 JIRA From 19420c0b9e2cbac3cdb85f115d3028853af2804b Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Thu, 12 May 2016 19:43:22 +0400 Subject: [PATCH 199/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 43df40e59..6c43ad909 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.1 + 1.19.2-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - v1.19.1 + HEAD JIRA From 3b8aaee438bfce3cff40e94e440b21d969ba6d12 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 22 Jun 2016 12:19:45 +0100 Subject: [PATCH 200/228] [FIXED JENKINS-36144] Borrow the SCMTrigger's queue (or fall back to its queue size) --- .../cloudbees/jenkins/GitHubPushTrigger.java | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 0274fb9df..52dbe38c7 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -11,13 +11,17 @@ import hudson.model.Item; import hudson.model.Job; import hudson.model.Project; +import hudson.triggers.SCMTrigger; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.FormValidation; +import hudson.util.NamingThreadFactory; import hudson.util.SequentialExecutionQueue; import hudson.util.StreamTaskListener; +import java.lang.reflect.Field; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import jenkins.model.Jenkins; -import jenkins.model.Jenkins.MasterComputer; import jenkins.model.ParameterizedJobMixIn; import jenkins.triggers.SCMTriggerItem.SCMTriggerItems; import org.apache.commons.jelly.XMLOutput; @@ -45,7 +49,6 @@ import java.util.Set; import static org.apache.commons.lang3.StringUtils.isEmpty; -import static org.jenkinsci.plugins.github.Messages.github_trigger_check_method_warning_details; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.asParameterizedJobMixIn; /** @@ -72,7 +75,9 @@ public void onPost() { */ public void onPost(String triggeredByUser) { final String pushBy = triggeredByUser; - getDescriptor().queue.execute(new Runnable() { + DescriptorImpl d = getDescriptor(); + d.checkThreadPoolSize(); + d.queue.execute(new Runnable() { private boolean runPolling() { try { StreamTaskListener listener = new StreamTaskListener(getLogFile()); @@ -226,7 +231,7 @@ public void writeLogTo(XMLOutput out) throws IOException { @Extension public static class DescriptorImpl extends TriggerDescriptor { private final transient SequentialExecutionQueue queue = - new SequentialExecutionQueue(MasterComputer.threadPoolForRemoting); + new SequentialExecutionQueue(Executors.newSingleThreadExecutor(threadFactory())); private transient String hookUrl; @@ -235,6 +240,43 @@ public static class DescriptorImpl extends TriggerDescriptor { @Inject private transient GitHubHookRegisterProblemMonitor monitor; + @Inject + private transient SCMTrigger.DescriptorImpl scmTrigger; + + private transient int maximumThreads = Integer.MIN_VALUE; + + private static ThreadFactory threadFactory() { + return new NamingThreadFactory(Executors.defaultThreadFactory(), "GitHubPushTrigger"); + } + + public DescriptorImpl() { + checkThreadPoolSize(); + } + + /** + * Update the {@link java.util.concurrent.ExecutorService} instance. + */ + /*package*/ + synchronized void checkThreadPoolSize() { + if (scmTrigger != null) { + int count = scmTrigger.getPollingThreadCount(); + if (maximumThreads != count) { + maximumThreads = count; + try { + Field getQueue = SCMTrigger.DescriptorImpl.class.getDeclaredField("queue"); + getQueue.setAccessible(true); + SequentialExecutionQueue q = (SequentialExecutionQueue) getQueue.get(scmTrigger); + this.queue.setExecutors(q.getExecutors()); + } catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) { + queue.setExecutors( + (count == 0 + ? Executors.newCachedThreadPool(threadFactory()) + : Executors.newFixedThreadPool(maximumThreads, threadFactory()))); + } + } + } + } + @Override public boolean isApplicable(Item item) { return item instanceof Job && SCMTriggerItems.asSCMTriggerItem(item) != null @@ -351,7 +393,7 @@ public FormValidation doCheckHookRegistered(@AncestorInPath Job job) { for (GitHubRepositoryName repo : repos) { if (monitor.isProblemWith(repo)) { return FormValidation.warning( - github_trigger_check_method_warning_details( + org.jenkinsci.plugins.github.Messages.github_trigger_check_method_warning_details( repo.getUserName(), repo.getRepositoryName(), repo.getHost() )); } From 105ee8fe07c05eb17295017c6c9e9ebfb9c6a77b Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 22 Jun 2016 12:23:27 +0100 Subject: [PATCH 201/228] [JENKINS-36144] Ooops need a guard when we can borrow --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 52dbe38c7..a0b521cb2 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -19,6 +19,7 @@ import hudson.util.SequentialExecutionQueue; import hudson.util.StreamTaskListener; import java.lang.reflect.Field; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import jenkins.model.Jenkins; @@ -266,7 +267,11 @@ synchronized void checkThreadPoolSize() { Field getQueue = SCMTrigger.DescriptorImpl.class.getDeclaredField("queue"); getQueue.setAccessible(true); SequentialExecutionQueue q = (SequentialExecutionQueue) getQueue.get(scmTrigger); - this.queue.setExecutors(q.getExecutors()); + ExecutorService executors = q.getExecutors(); + if (this.queue.getExecutors() != executors) { + // guard or otherwise we will shut it down :-( + this.queue.setExecutors(executors); + } } catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) { queue.setExecutors( (count == 0 From 05b895f64d314d0f523c21876d9ca9b0cefb8402 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 22 Jun 2016 12:31:57 +0100 Subject: [PATCH 202/228] [JENKINS-36144] After careful analysis of some of the code paths, safer not to borrow - we will mirror the count though --- .../cloudbees/jenkins/GitHubPushTrigger.java | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index a0b521cb2..76c938646 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -18,8 +18,6 @@ import hudson.util.NamingThreadFactory; import hudson.util.SequentialExecutionQueue; import hudson.util.StreamTaskListener; -import java.lang.reflect.Field; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import jenkins.model.Jenkins; @@ -262,22 +260,10 @@ synchronized void checkThreadPoolSize() { if (scmTrigger != null) { int count = scmTrigger.getPollingThreadCount(); if (maximumThreads != count) { - maximumThreads = count; - try { - Field getQueue = SCMTrigger.DescriptorImpl.class.getDeclaredField("queue"); - getQueue.setAccessible(true); - SequentialExecutionQueue q = (SequentialExecutionQueue) getQueue.get(scmTrigger); - ExecutorService executors = q.getExecutors(); - if (this.queue.getExecutors() != executors) { - // guard or otherwise we will shut it down :-( - this.queue.setExecutors(executors); - } - } catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) { - queue.setExecutors( - (count == 0 - ? Executors.newCachedThreadPool(threadFactory()) - : Executors.newFixedThreadPool(maximumThreads, threadFactory()))); - } + queue.setExecutors( + (count == 0 + ? Executors.newCachedThreadPool(threadFactory()) + : Executors.newFixedThreadPool(maximumThreads, threadFactory()))); } } } From 3c81c4dccc6b731e39363fe43d68136bd8473b64 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 22 Jun 2016 12:37:11 +0100 Subject: [PATCH 203/228] [JENKINS-36144] Align with code conventions --- .../java/com/cloudbees/jenkins/GitHubPushTrigger.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 76c938646..d43e3cb39 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -244,10 +244,6 @@ public static class DescriptorImpl extends TriggerDescriptor { private transient int maximumThreads = Integer.MIN_VALUE; - private static ThreadFactory threadFactory() { - return new NamingThreadFactory(Executors.defaultThreadFactory(), "GitHubPushTrigger"); - } - public DescriptorImpl() { checkThreadPoolSize(); } @@ -366,6 +362,10 @@ public static boolean allowsHookUrlOverride() { return ALLOW_HOOKURL_OVERRIDE; } + private static ThreadFactory threadFactory() { + return new NamingThreadFactory(Executors.defaultThreadFactory(), "GitHubPushTrigger"); + } + /** * Checks that repo defined in this job is not in administrative monitor as failed to be registered. * If that so, shows warning with some instructions From 3fd5cfe155ea8ee6e45cb450ad85c94be2f9edda Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 23 Jun 2016 00:03:23 +0100 Subject: [PATCH 204/228] [JENKINS-36144] More verbose function name --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index d43e3cb39..ca5d1dbfd 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -75,7 +75,7 @@ public void onPost() { public void onPost(String triggeredByUser) { final String pushBy = triggeredByUser; DescriptorImpl d = getDescriptor(); - d.checkThreadPoolSize(); + d.checkThreadPoolSizeAndUpdateIfNecessary(); d.queue.execute(new Runnable() { private boolean runPolling() { try { @@ -245,14 +245,14 @@ public static class DescriptorImpl extends TriggerDescriptor { private transient int maximumThreads = Integer.MIN_VALUE; public DescriptorImpl() { - checkThreadPoolSize(); + checkThreadPoolSizeAndUpdateIfNecessary(); } /** * Update the {@link java.util.concurrent.ExecutorService} instance. */ /*package*/ - synchronized void checkThreadPoolSize() { + synchronized void checkThreadPoolSizeAndUpdateIfNecessary() { if (scmTrigger != null) { int count = scmTrigger.getPollingThreadCount(); if (maximumThreads != count) { From dddd6bcf48a138afea5a0409db0917565f6b1856 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 23 Jun 2016 00:04:16 +0100 Subject: [PATCH 205/228] [JENKINS-36144] Code review catches bugs... who'd have thunk it --- src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index ca5d1dbfd..15d2b421f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -256,6 +256,7 @@ synchronized void checkThreadPoolSizeAndUpdateIfNecessary() { if (scmTrigger != null) { int count = scmTrigger.getPollingThreadCount(); if (maximumThreads != count) { + maximumThreads = count; queue.setExecutors( (count == 0 ? Executors.newCachedThreadPool(threadFactory()) From 4007938bf4b37d8ca314f9bb40b7d2560feb46c6 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Fri, 24 Jun 2016 19:20:43 +0400 Subject: [PATCH 206/228] [maven-release-plugin] prepare release v1.19.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6c43ad909..d6b806247 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.2-SNAPSHOT + 1.19.2 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + v1.19.2 JIRA From e9caf00f01508e35b744ebe0dd9985c84b0d9469 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Fri, 24 Jun 2016 19:20:49 +0400 Subject: [PATCH 207/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d6b806247..b4a31b58b 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.2 + 1.19.3-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - v1.19.2 + HEAD JIRA From ca75bde125b2b8a9bd6f9ce15e54c68de30a5cac Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Mon, 11 Jul 2016 07:48:51 -0600 Subject: [PATCH 208/228] [JENKINS-36445] Show first 7 characters of SHA1 in change log hyperlink (#128) Most git repository browsing systems (bitbucket, cgit, github, gitweb) display a short form of the SHA1 (commonly the first 7 characters) with a clickable hyperlink that will take them to a page that includes the full SHA1. This change reduces the visible SHA1 text in the list of changes to the first 7 characters of the SHA1, consistent with those other repository browsers. It also adds assertions that the expected format is used and simplifies the existing annotator tests. --- .../plugins/github/GithubLinkAnnotator.java | 7 +- .../github/GithubLinkAnnotatorTest.java | 94 +++++++++++++++---- 2 files changed, 80 insertions(+), 21 deletions(-) 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 591c1521e..388901f02 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java @@ -8,6 +8,8 @@ import hudson.scm.ChangeLogAnnotator; import hudson.scm.ChangeLogSet.Entry; +import static java.lang.String.format; + import java.util.regex.Pattern; /** @@ -42,7 +44,10 @@ void annotate(final GithubUrl url, final MarkupText text, final Entry change) { if (change instanceof GitChangeSet) { GitChangeSet cs = (GitChangeSet) change; - text.wrapBy("", " (commit: " + cs.getId() + ")"); + final String id = cs.getId(); + text.wrapBy("", format(" (commit: %s)", + url.commitId(id), + id.substring(0, Math.min(id.length(), 7)))); } } diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java index 65b55e6d3..aba3bb86e 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java @@ -1,37 +1,91 @@ package com.coravy.hudson.plugins.github; -import static org.junit.Assert.assertEquals; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; import hudson.MarkupText; - +import hudson.plugins.git.GitChangeSet; +import java.util.ArrayList; +import java.util.Random; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import static java.lang.String.format; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +@RunWith(DataProviderRunner.class) public class GithubLinkAnnotatorTest { - private final static String GITHUB_URL = "http://github.com/juretta/iphone-project-tools/"; + private final static String GITHUB_URL = "http://github.com/juretta/iphone-project-tools"; + private final static String SHA1 = "badbeef136cd854f4dd6fa40bf94c0c657681dd5"; + private final static Random RANDOM = new Random(); + private final String expectedChangeSetAnnotation = " (" + + "" + + "commit: " + SHA1.substring(0, 7) + + ")"; + private static GitChangeSet changeSet; + + @Before + public void createChangeSet() throws Exception { + ArrayList lines = new ArrayList(); + lines.add("commit " + SHA1); + lines.add("tree 66236cf9a1ac0c589172b450ed01f019a5697c49"); + lines.add("parent e74a24e995305bd67a180f0ebc57927e2b8783ce"); + lines.add("author Author Name 1363879004 +0100"); + lines.add("committer Committer Name 1364199539 -0400"); + lines.add(""); + lines.add(" Committer and author are different in this commit."); + lines.add(""); + changeSet = new GitChangeSet(lines, true); + } + + private static Object[] genActualAndExpected(String keyword) { + int issueNumber = RANDOM.nextInt(1000000); + final String innerText = keyword + " #" + issueNumber; + final String startHREF = ""; + final String endHREF = ""; + final String annotatedText = startHREF + innerText + endHREF; + return new Object[]{ + // Input text to the annotate method + format("An issue %s link", innerText), + // Expected result from the annotate method + format("An issue %s link", annotatedText) + }; + } + + @DataProvider + public static Object[][] annotations() { + return new Object[][]{ + genActualAndExpected("Closes"), + genActualAndExpected("Close"), + genActualAndExpected("closes"), + genActualAndExpected("close") + }; + } + + @Test + @UseDataProvider("annotations") + public void inputIsExpected(String input, String expected) throws Exception { + assertThat(format("For input '%s'", input), + annotate(input, null), + is(expected)); + } @Test - public final void testAnnotateStringMarkupText() { - assertAnnotatedTextEquals("An issue Closes #1 link", - "An issue Closes #1 link"); - assertAnnotatedTextEquals("An issue Close #1 link", - "An issue Close #1 link"); - assertAnnotatedTextEquals("An issue closes #123 link", - "An issue closes #123 link"); - assertAnnotatedTextEquals("An issue close #9876 link", - "An issue close #9876 link"); + @UseDataProvider("annotations") + public void inputIsExpectedWithChangeSet(String input, String expected) throws Exception { + assertThat(format("For changeset input '%s'", input), + annotate(input, changeSet), + is(expected + expectedChangeSetAnnotation)); } - private void assertAnnotatedTextEquals(final String originalText, - final String expectedAnnotatedText) { + private String annotate(final String originalText, GitChangeSet changeSet) { MarkupText markupText = new MarkupText(originalText); GithubLinkAnnotator annotator = new GithubLinkAnnotator(); - annotator.annotate(new GithubUrl(GITHUB_URL), markupText, null); + annotator.annotate(new GithubUrl(GITHUB_URL), markupText, changeSet); - assertEquals(expectedAnnotatedText, markupText.toString()); + return markupText.toString(true); } } From dfd8f97a742b4382dd70dc4f9fcde27af8e52731 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Wed, 20 Jul 2016 18:58:07 +0400 Subject: [PATCH 209/228] [maven-release-plugin] prepare release v1.19.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b4a31b58b..b9db59ec2 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.3-SNAPSHOT + 1.19.3 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + v1.19.3 JIRA From 300ee683598e0008c26e28758cb6cb1a91ecad2f Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Wed, 20 Jul 2016 18:58:13 +0400 Subject: [PATCH 210/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b9db59ec2..e942366ec 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.3 + 1.19.4-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - v1.19.3 + HEAD JIRA From f6c4832964b25854a1af42f121a8aea852a72c71 Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Wed, 20 Jul 2016 18:17:50 +0300 Subject: [PATCH 211/228] Create NOTICE.md (#130) * Create NOTICE.md * rm notice information from readme --- NOTICE.md | 4 ++++ README.md | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 NOTICE.md diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 000000000..9482c4ac9 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,4 @@ +## License notes + +This plugin uses part of Guava's code in class named `org.jenkinsci.plugins.github.util.FluentIterableWrapper` +licensed under **Apache 2.0** license diff --git a/README.md b/README.md index 175df3d4f..e17706856 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,3 @@ Plugin releases --------------- mvn release:prepare release:perform -Dusername=juretta -Dpassword=****** - - -## License notes - -This plugin uses part of Guava's code in class named -`org.jenkinsci.plugins.github.util.FluentIterableWrapper` licensed under Apache 2.0 license From 4ae71ca221f742e229b4eaf3226c4a851de87ee2 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 21 Jul 2016 10:18:32 +0200 Subject: [PATCH 212/228] Manually entered repository source --- .../ManuallyEnteredRepositorySource.java | 55 +++++++++++++++++++ .../config.groovy | 8 +++ .../help-url.html | 3 + .../ManuallyEnteredRepositorySource/help.html | 3 + 4 files changed, 69 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html 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 new file mode 100644 index 000000000..ba32f64f2 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java @@ -0,0 +1,55 @@ +package org.jenkinsci.plugins.github.status.sources; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.github.extension.status.GitHubReposSource; +import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; +import org.kohsuke.github.GHRepository; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.List; + +import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; + +public class ManuallyEnteredRepositorySource extends GitHubReposSource { + private String url; + + @DataBoundConstructor + public ManuallyEnteredRepositorySource(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + @Override + 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) { + GitHubRepositoryName name = GitHubRepositoryName.create(url); + if (name != null) { + return name.resolve(); + } else { + listener.getLogger().println("Unable to match " + url + " with a GitHub repository."); + return Collections.emptyList(); + } + } + }).toList(); + } + + @Extension + public static class ManuallyEnteredRepositorySourceDescriptor extends Descriptor { + @Override + public String getDisplayName() { + return "Manually entered repository"; + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy new file mode 100644 index 000000000..c58133e23 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy @@ -0,0 +1,8 @@ +package org.jenkinsci.plugins.github.status.sources.AnyDefinedRepositorySource + + +def f = namespace(lib.FormTagLib); + +f.entry(title: _('Repository URL'), field: 'url') { + f.textbox() +} \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html new file mode 100644 index 000000000..69b886b42 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html @@ -0,0 +1,3 @@ +
+ A GitHub repository URL. +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html new file mode 100644 index 000000000..47d95998c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html @@ -0,0 +1,3 @@ +
+ A manually entered repository URL. +
\ No newline at end of file From 0e620aa9afd6499d88ecc20d32b36d5dd9938e62 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 21 Jul 2016 12:16:22 +0200 Subject: [PATCH 213/228] Add missing newlines --- .../sources/ManuallyEnteredRepositorySource/config.groovy | 2 +- .../sources/ManuallyEnteredRepositorySource/help-url.html | 2 +- .../status/sources/ManuallyEnteredRepositorySource/help.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy index c58133e23..747c6a155 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/config.groovy @@ -5,4 +5,4 @@ def f = namespace(lib.FormTagLib); f.entry(title: _('Repository URL'), field: 'url') { f.textbox() -} \ No newline at end of file +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html index 69b886b42..c3057c8dd 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help-url.html @@ -1,3 +1,3 @@
A GitHub repository URL. -
\ No newline at end of file +
diff --git a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html index 47d95998c..6d44c6b3f 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html +++ b/src/main/resources/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource/help.html @@ -1,3 +1,3 @@
A manually entered repository URL. -
\ No newline at end of file +
From 1adbffccbbf0810fc3a706cdbfe1cd7755cc8733 Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Thu, 21 Jul 2016 14:57:11 +0300 Subject: [PATCH 214/228] Create codecov.yml --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..e67465776 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +codecov: + token: 9f11e1c0-2bd1-48d1-910e-24f8cf20cc4f From 1dfca4d127c4c53b6321c8dc4e248739c10120f5 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 21 Jul 2016 17:28:24 +0200 Subject: [PATCH 215/228] Use printf for logging --- .../github/status/sources/ManuallyEnteredRepositorySource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ba32f64f2..d63862753 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 @@ -38,7 +38,7 @@ protected Iterable applyNullSafe(@Nonnull String url) { if (name != null) { return name.resolve(); } else { - listener.getLogger().println("Unable to match " + url + " with a GitHub repository."); + listener.getLogger().printf("Unable to match %s with a GitHub repository.%n", url); return Collections.emptyList(); } } From 330cdbe3499ea5813eeb5ea1e7fbb9750f7192dc Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 21 Jul 2016 18:25:50 +0200 Subject: [PATCH 216/228] Add a test when url isn't valid (repository name is null) --- .../ManuallyEnteredRepositorySource.java | 8 +++- .../ManuallyEnteredRepositorySourceTest.java | 45 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java 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 d63862753..0a73f04f3 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 @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.github.status.sources; import com.cloudbees.jenkins.GitHubRepositoryName; +import com.google.common.annotations.VisibleForTesting; import hudson.Extension; import hudson.model.Descriptor; import hudson.model.Run; @@ -28,13 +29,18 @@ public String getUrl() { return url; } + @VisibleForTesting + GitHubRepositoryName createName(String url) { + return GitHubRepositoryName.create(url); + } + @Override 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) { - GitHubRepositoryName name = GitHubRepositoryName.create(url); + GitHubRepositoryName name = createName(url); if (name != null) { return name.resolve(); } else { diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java new file mode 100644 index 000000000..6ab397e80 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java @@ -0,0 +1,45 @@ +package org.jenkinsci.plugins.github.status.sources; + +import hudson.model.Run; +import hudson.model.TaskListener; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.kohsuke.github.GHRepository; +import org.mockito.Answers; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.PrintStream; +import java.util.List; + +import static com.jayway.restassured.RestAssured.when; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class ManuallyEnteredRepositorySourceTest { + @Mock(answer = Answers.RETURNS_MOCKS) + private Run run; + + @Mock(answer = Answers.RETURNS_MOCKS) + private TaskListener listener; + + @Mock(answer = Answers.RETURNS_MOCKS) + private PrintStream logger; + + @Test + public void nullName() { + ManuallyEnteredRepositorySource instance = Mockito.spy(new ManuallyEnteredRepositorySource("https://github.com/jenkinsci/jenkins")); + doReturn(null).when(instance).createName(Matchers.anyString()); + doReturn(logger).when(listener).getLogger(); + List repos = instance.repos(run, listener); + assertThat("size", repos, hasSize(0)); + verify(listener).getLogger(); + verify(logger).printf(eq("Unable to match %s with a GitHub repository.%n"), eq("https://github.com/jenkinsci/jenkins")); + } +} From 9649beb073fa367c67491510732a3735439c661a Mon Sep 17 00:00:00 2001 From: Merkushev Kirill Date: Thu, 21 Jul 2016 20:31:26 +0300 Subject: [PATCH 217/228] add some override annotations in ghcommitnotifier --- src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index aea073e81..a0e662024 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -108,6 +108,7 @@ public static Result getDefaultResultOnFailure() { return Result.fromString(trimToEmpty(resultOnFailure)); } + @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } @@ -153,10 +154,12 @@ public void perform(@NonNull Run build, @Extension public static class DescriptorImpl extends BuildStepDescriptor { + @Override public boolean isApplicable(Class aClass) { return true; } + @Override public String getDisplayName() { return GitHubCommitNotifier_DisplayName(); } From c01ce0be5a87d86378674ae911fa30d1316167ac Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Sun, 24 Jul 2016 14:41:57 +0400 Subject: [PATCH 218/228] [maven-release-plugin] prepare release v1.20.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e942366ec..ccc3a3610 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.19.4-SNAPSHOT + 1.20.0 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + v1.20.0 JIRA From 5c8af54fa097b97a73da1b8162a4a78f0d67d71b Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Sun, 24 Jul 2016 14:42:08 +0400 Subject: [PATCH 219/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ccc3a3610..c59097de6 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.20.0 + 1.20.1-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - v1.20.0 + HEAD JIRA From b6683ee0d2f365851dde5d09c079104d32c29697 Mon Sep 17 00:00:00 2001 From: martinmine Date: Fri, 1 Jul 2016 16:48:18 +0200 Subject: [PATCH 220/228] [JENKINS-33974] Payload verification using shared secrets --- .../cloudbees/jenkins/GitHubPushTrigger.java | 6 +- .../github/config/GitHubPluginConfig.java | 9 ++ .../github/config/HookSecretConfig.java | 95 +++++++++++++++++++ .../plugins/github/extension/CryptoUtil.java | 72 ++++++++++++++ .../github/webhook/GHEventPayload.java | 6 +- .../webhook/RequirePostWithGHHookPayload.java | 66 +++++++++++++ .../github/webhook/WebhookManager.java | 17 +++- .../config/GitHubPluginConfig/config.groovy | 6 +- .../config/HookSecretConfig/config.groovy | 8 ++ .../HookSecretConfig/help-sharedSecret.html | 4 + .../cloudbees/jenkins/GitHubWebHookTest.java | 1 - .../github/config/HookSecretConfigTest.java | 47 +++++++++ .../github/extension/CryptoUtilTest.java | 56 +++++++++++ .../plugins/github/test/HookSecretHelper.java | 56 +++++++++++ .../RequirePostWithGHHookPayloadTest.java | 94 +++++++++++++++++- 15 files changed, 530 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-sharedSecret.html create mode 100644 src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 15d2b421f..c386afe0f 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -18,8 +18,6 @@ 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.triggers.SCMTriggerItem.SCMTriggerItems; @@ -46,6 +44,8 @@ import java.util.Date; import java.util.List; 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.jenkinsci.plugins.github.util.JobInfoHelpers.asParameterizedJobMixIn; @@ -121,7 +121,7 @@ public void run() { } if (asParameterizedJobMixIn(job).scheduleBuild(cause)) { LOGGER.info("SCM changes detected in " + job.getFullName() - + ". Triggering #" + job.getNextBuildNumber()); + + ". Triggering #" + job.getNextBuildNumber()); } else { LOGGER.info("SCM changes detected in " + job.getFullName() + ". Job is already in the queue"); } 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..5f2392679 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -61,6 +61,7 @@ public class GitHubPluginConfig extends GlobalConfiguration { private List configs = new ArrayList(); private URL hookUrl; + private HookSecretConfig hookSecretConfig = new HookSecretConfig(null); private transient boolean overrideHookUrl; @@ -244,4 +245,12 @@ private static void validateConfig(boolean state, String message) { throw new GHPluginConfigException(message); } } + + public HookSecretConfig getHookSecretConfig() { + return hookSecretConfig; + } + + public void setHookSecretConfig(HookSecretConfig hookSecretConfig) { + this.hookSecretConfig = hookSecretConfig; + } } 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..0285ea763 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java @@ -0,0 +1,95 @@ +package org.jenkinsci.plugins.github.config; + +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import hudson.util.Secret; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nullable; +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 org.apache.commons.lang.StringUtils.isEmpty; + +/** + * Manages storing/retrieval of the shared secret for the hook. + */ +@XStreamAlias("hook-config") +public class HookSecretConfig extends AbstractDescribableImpl { + + private String credentialsId; + + @DataBoundConstructor + public HookSecretConfig(String credentialsId) { + this.credentialsId = credentialsId; + } + + private StringCredentials getHookSecretCredentials() { + if (isEmpty(credentialsId)) { + return null; + } + + return firstOrDefault( + lookupCredentials(StringCredentials.class, + Jenkins.getInstance(), ACL.SYSTEM, + Collections.emptyList()), + withId(credentialsId), null); + } + + /** + * Gets the currently used secret being used for payload verification. + * @return Current secret, null if not set. + */ + @Nullable + public Secret getHookSecret() { + StringCredentials credentials = getHookSecretCredentials(); + if (credentials != null) { + return credentials.getSecret(); + } else { + return null; + } + } + + public String getCredentialsId() { + return credentialsId; + } + + public void setCredentialsId(String credentialsId) { + this.credentialsId = credentialsId; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return "Hook secret configuration"; + } + + @SuppressWarnings("unused") + public ListBoxModel doFillCredentialsIdItems() { + if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + return new ListBoxModel(); + } + + return new StandardListBoxModel() + .withEmptySelection() + .withAll(lookupCredentials( + StringCredentials.class, + Jenkins.getInstance(), + ACL.SYSTEM, + Collections.emptyList()) + ); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java b/src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java new file mode 100644 index 000000000..0e26d15b1 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java @@ -0,0 +1,72 @@ +package org.jenkinsci.plugins.github.extension; + +import hudson.util.Secret; +import org.apache.commons.codec.binary.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/** + * Utility class for dealing with signatures of incoming requests. + * + * @see API documentation + */ +public class CryptoUtil { + private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; + private static final Logger LOGGER = LoggerFactory.getLogger(CryptoUtil.class); + private static final String SHA1_PREFIX = "sha1="; + private static final String DEFAULT_CHARSET = "utf-8"; + public static final String INVALID_SIGNATURE = "INVALID_SIGNATURE"; + + private CryptoUtil() { + } + + /** + * Computes a RFC 2104-compliant HMAC digest using SHA1 of a payload with a given key (secret). + * + * @param payload Clear-text to create signature of. + * @param secret Key to sign with. + * + * @return HMAC digest of payload using secret as key. Will return INVALID_SIGNATURE if any args is null. + */ + @Nullable + public static String computeSHA1Signature(@Nullable final String payload, @Nullable final Secret secret) { + if (payload == null || secret == null) { + return INVALID_SIGNATURE; + } + + try { + final SecretKeySpec keySpec = new SecretKeySpec( + secret.getPlainText().getBytes(DEFAULT_CHARSET), + HMAC_SHA1_ALGORITHM + ); + final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); + mac.init(keySpec); + final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(DEFAULT_CHARSET)); + + return Hex.encodeHexString(rawHMACBytes); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + return null; + } + } + + /** + * Grabs the value after "sha1=" in a string. + * + * @param digest The string to get the sha1 value from. + * + * @return Value after "sha1" present in the digest value. Null if not present. + */ + @Nullable + public static String parseSHA1Value(@Nullable final String digest) { + if (digest != null && digest.startsWith(SHA1_PREFIX)) { + return digest.substring(SHA1_PREFIX.length()); + } else { + return null; + } + } +} 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..51e5ecb62 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java @@ -39,6 +39,8 @@ 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 * @@ -46,8 +48,8 @@ class PayloadHandler extends AnnotationHandler { */ private static final Map> PAYLOAD_PROCESS = ImmutableMap.>builder() - .put("application/json", fromApplicationJson()) - .put("application/x-www-form-urlencoded", fromForm()) + .put(APPLICATION_JSON, fromApplicationJson()) + .put(FORM_URLENCODED, fromForm()) .build(); /** 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..8108dae90 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -1,7 +1,9 @@ package org.jenkinsci.plugins.github.webhook; import com.cloudbees.jenkins.GitHubWebHook; +import hudson.util.Secret; import org.jenkinsci.main.modules.instance_identity.InstanceIdentity; +import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.util.FluentIterableWrapper; import org.kohsuke.github.GHEvent; @@ -10,13 +12,16 @@ import org.kohsuke.stapler.StaplerResponse; 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 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.security.interfaces.RSAPublicKey; import static com.cloudbees.jenkins.GitHubWebHook.X_INSTANCE_IDENTITY; @@ -30,9 +35,13 @@ import static javax.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.jenkinsci.plugins.github.extension.CryptoUtil.INVALID_SIGNATURE; +import static org.jenkinsci.plugins.github.extension.CryptoUtil.computeSHA1Signature; +import static org.jenkinsci.plugins.github.extension.CryptoUtil.parseSHA1Value; 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,6 +55,13 @@ @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 payload signatures. + * + * @see Developer manual + */ + public static final String SIGNATURE_HEADER = "X-Hub-Signature"; @Override public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, Object[] arguments) @@ -54,6 +70,7 @@ public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, O shouldBePostMethod(req); returnsInstanceIdentityIfLocalUrlTest(req); shouldContainParseablePayload(arguments); + shouldProvideValidSignature(req, arguments); return target.invoke(req, rsp, instance, arguments); } @@ -113,6 +130,55 @@ protected void shouldContainParseablePayload(Object[] arguments) throws Invocati ); } + /** + * Checks that an incoming request has a valid signature, if there is specified a signature in the config. + * + * @param req Incoming request. + * + * @throws InvocationTargetException if any of preconditions is not satisfied + */ + protected void shouldProvideValidSignature(StaplerRequest req, Object[] args) throws InvocationTargetException { + final String signature = parseSHA1Value(req.getHeader(SIGNATURE_HEADER)); + final Secret secret = GitHubPlugin.configuration().getHookSecretConfig().getHookSecret(); + final String payload = obtainRequestBody(req, args); + final String computedSignature = computeSHA1Signature(payload, secret); + + if (secret != null) { + isTrue( + signature != null && !INVALID_SIGNATURE.equals(signature), + "Signature must be specified in the header " + SIGNATURE_HEADER + ); + + isTrue( + computedSignature != null, + "Missing payload" + ); + + isTrue( + computedSignature.equals(signature), + String.format("Signatures did not match, computed signature was: %s", computedSignature) + ); + } + } + + protected String obtainRequestBody(StaplerRequest 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, "UTF-8")); + } catch (UnsupportedEncodingException e) { + LOGGER.error(e.getMessage(), e); + } + } else { + LOGGER.error("Unknown content type {}", req.getContentType()); + } + + return null; + } + /** * Utility method to stop preprocessing if condition is false * 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..8e1079ea5 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -4,8 +4,11 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import hudson.model.Job; +import hudson.util.Secret; +import jenkins.model.Jenkins; import org.apache.commons.lang.Validate; import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.jenkinsci.plugins.github.util.misc.NullSafePredicate; @@ -20,6 +23,7 @@ import java.io.IOException; import java.net.URL; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Set; @@ -290,7 +294,18 @@ protected Function createWebhook(final URL url, final Set< return new NullSafeFunction() { 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"); + + final Secret secret = Jenkins.getInstance() + .getDescriptorByType(GitHubPluginConfig.class).getHookSecretConfig().getHookSecret(); + + if (secret != null) { + config.put("secret", secret.getPlainText()); + } + + return repo.createHook("web", config, events, true); } catch (IOException e) { throw new GHException("Failed to create hook", e); } 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..d74c04bea 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 @@ -35,7 +35,11 @@ f.section(title: descriptor.displayName) { } } } - + + f.property( + field: "hookSecretConfig" + ) + f.entry(title: _("Additional actions"), help: descriptor.getHelpFile('additional')) { f.hetero_list(items: [], addCaption: _("Manage additional GitHub actions"), 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..f20e9b409 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy @@ -0,0 +1,8 @@ +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() +} 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..7ea5e3d07 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-sharedSecret.html @@ -0,0 +1,4 @@ +
+ 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. +
\ No newline at end of file diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java index 2f88604e3..bba4ff7b4 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java @@ -2,7 +2,6 @@ import com.google.inject.Inject; -import hudson.model.AbstractProject; import hudson.model.Job; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; diff --git a/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java new file mode 100644 index 000000000..4cfa37dda --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java @@ -0,0 +1,47 @@ +package org.jenkinsci.plugins.github.config; + +import jenkins.model.Jenkins; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecret; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + + +/** + * Test for storing hook secrets. + */ +public class HookSecretConfigTest { + + private static final String SECRET_INIT = "test"; + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + private HookSecretConfig hookSecretConfig; + + @Before + public void setup() { + storeSecret(SECRET_INIT); + hookSecretConfig = Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class).getHookSecretConfig(); + } + + @Test + public void shouldStoreNewSecrets() { + storeSecret(SECRET_INIT); + + assertNotNull("Secret is persistent", hookSecretConfig.getHookSecret()); + assertTrue("Secret correctly stored", SECRET_INIT.equals(hookSecretConfig.getHookSecret().getPlainText())); + } + + @Test + public void shouldOverwriteExistingSecrets() { + final String newSecret = "test2"; + storeSecret(newSecret); + + assertNotNull("Secret is persistent", hookSecretConfig.getHookSecret()); + assertTrue("Secret correctly stored", newSecret.equals(hookSecretConfig.getHookSecret().getPlainText())); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java b/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java new file mode 100644 index 000000000..5074b8652 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java @@ -0,0 +1,56 @@ +package org.jenkinsci.plugins.github.extension; + +import hudson.util.Secret; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.*; + +/** + * Tests for utility class that deals with crypto/hashing of data. + * @author martinmine + */ +@RunWith(MockitoJUnitRunner.class) +public class CryptoUtilTest { + + private static final String SIGNATURE = "85d155c55ed286a300bd1cf124de08d87e914f3a"; + private static final String PAYLOAD = "foo"; + private Secret globalSecret; + private Secret projectSecret; + private Secret secret; + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Before + public void setupSecrets() { + globalSecret = Secret.fromString("global secret"); + projectSecret = Secret.fromString("project secret"); + secret = Secret.fromString("bar"); + } + + @Test + public void shouldComputeSHA1Signature() throws Exception { + final String signature = CryptoUtil.computeSHA1Signature(PAYLOAD, secret); + + assertThat("signature is valid", signature, equalTo(SIGNATURE)); + } + + @Test + public void shouldParseCorrectSHA1Signature() throws Exception { + final String parsedSignature = CryptoUtil.parseSHA1Value("sha1=" + SIGNATURE); + assertThat("parsed signature is valid", parsedSignature, equalTo(SIGNATURE)); + } + + @Test + public void shouldReturnNullWithNoSignature() throws Exception { + final String parsedSignature = CryptoUtil.parseSHA1Value(null); + assertThat("signature is null", parsedSignature, nullValue()); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java b/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java new file mode 100644 index 000000000..33a4137ec --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java @@ -0,0 +1,56 @@ +package org.jenkinsci.plugins.github.test; + +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.domains.Domain; +import hudson.security.ACL; +import hudson.util.Secret; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; +import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +/** + * Helper class for setting the secret text for hooks while testing. + */ +public class HookSecretHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(HookSecretHelper.class); + + private HookSecretHelper() { + } + + /** + * Stores the secret and sets it as the current hook secret. + * @param secretText The secret/key. + */ + public static void storeSecret(final String secretText) { + final StringCredentialsImpl credentials = new StringCredentialsImpl( + CredentialsScope.GLOBAL, + UUID.randomUUID().toString(), + null, + Secret.fromString(secretText) + ); + + ACL.impersonate(ACL.SYSTEM, new Runnable() { + @Override + public void run() { + try { + new SystemCredentialsProvider.StoreImpl().addCredentials( + Domain.global(), + credentials + ); + + } catch (IOException e) { + LOGGER.error("Unable to set hook secret", e); + } + } + }); + + Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class) + .getHookSecretConfig().setCredentialsId(credentials.getId()); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java index d4bf1c03f..3c65c30ad 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java @@ -1,15 +1,22 @@ package org.jenkinsci.plugins.github.webhook; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.jvnet.hudson.test.JenkinsRule; import org.kohsuke.github.GHEvent; import org.kohsuke.stapler.StaplerRequest; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.runners.MockitoJUnitRunner; import java.lang.reflect.InvocationTargetException; -import static org.mockito.Mockito.when; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecret; +import static org.mockito.Mockito.*; /** * @author lanwen (Merkushev Kirill) @@ -17,9 +24,23 @@ @RunWith(MockitoJUnitRunner.class) public class RequirePostWithGHHookPayloadTest { + private static final String SECRET_CONTENT = "secret"; + private static final String PAYLOAD = "sample payload"; + @Mock private StaplerRequest req; + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Spy + private RequirePostWithGHHookPayload.Processor processor; + + @Before + public void setSecret() { + storeSecret(SECRET_CONTENT); + } + @Test public void shouldPassOnlyPost() throws Exception { when(req.getMethod()).thenReturn("POST"); @@ -34,22 +55,26 @@ public void shouldNotPassOnNotPost() throws Exception { @Test public void shouldPassOnGHEventAndNotBlankPayload() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[]{GHEvent.PUSH, "{}"}); + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{GHEvent.PUSH, "{}"}); } @Test(expected = InvocationTargetException.class) public void shouldNotPassOnNullGHEventAndNotBlankPayload() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[]{null, "{}"}); + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{null, "{}"}); } @Test(expected = InvocationTargetException.class) public void shouldNotPassOnGHEventAndBlankPayload() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[] {GHEvent.PUSH, " "}); + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[] {GHEvent.PUSH, " "}); } @Test(expected = InvocationTargetException.class) public void shouldNotPassOnNulls() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload(new Object[] {null, null}); + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[] {null, null}); } @Test(expected = InvocationTargetException.class) @@ -65,4 +90,63 @@ public void shouldNotPassOnLessCountOfArgs() throws Exception { new Object[] {GHEvent.PUSH} ); } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnAbsentSignature() throws Exception { + doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + + processor.shouldProvideValidSignature(req, null); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnInvalidSignature() throws Exception { + final String signature = "sha1=a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"; + + when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); + doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + + processor.shouldProvideValidSignature(req, null); + } + + @Test(expected = InvocationTargetException.class) + public void shouldNotPassOnMalformedSignature() throws Exception { + final String signature = "49d5f5cf800a81f257324912969a2d325d13d3fc"; + + when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); + doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + + processor.shouldProvideValidSignature(req, null); + } + + @Test + public void shouldPassWithValidSignature() throws Exception { + final String signature = "sha1=49d5f5cf800a81f257324912969a2d325d13d3fc"; + + when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); + doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + + processor.shouldProvideValidSignature(req, null); + } + + @Test + public void shouldReturnValidPayloadOnApplicationJson() { + final String payload = "test"; + + doReturn(GHEventPayload.PayloadHandler.APPLICATION_JSON).when(req).getContentType(); + + final String body = processor.obtainRequestBody(req, new Object[]{null, payload}); + + assertThat("valid returned body", body, equalTo(payload)); + } + + @Test + public void shouldReturnValidPayloadOnFormUrlEncoded() { + final String payload = "test"; + + doReturn(GHEventPayload.PayloadHandler.FORM_URLENCODED).when(req).getContentType(); + + final String body = processor.obtainRequestBody(req, new Object[]{null, payload}); + + assertThat("valid returned body", body, equalTo("payload=" + payload)); + } } From 70117b976dc98360543e328a96d28458a3852e90 Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Fri, 12 Aug 2016 00:59:07 +0300 Subject: [PATCH 221/228] Use OOP power to calculate sign, add integration test also don't bother signature validation if no header from github with signature --- .../github/config/GitHubServerConfig.java | 43 +++++++++-- .../github/config/HookSecretConfig.java | 25 +----- .../plugins/github/extension/CryptoUtil.java | 72 ------------------ .../github/webhook/GHWebhookSignature.java | 76 +++++++++++++++++++ .../webhook/RequirePostWithGHHookPayload.java | 48 ++++++------ .../github/webhook/WebhookManager.java | 12 ++- .../HookSecretConfig/help-sharedSecret.html | 1 + .../jenkins/GitHubWebHookFullTest.java | 31 ++++++++ .../github/config/HookSecretConfigTest.java | 5 +- .../github/extension/CryptoUtilTest.java | 47 ++++-------- .../plugins/github/test/HookSecretHelper.java | 17 ++++- .../RequirePostWithGHHookPayloadTest.java | 27 +++---- .../github/webhook/WebhookManagerTest.java | 24 ++++++ ...4e5f2c6086a01281d2e947d932_secret_123.json | 1 + 14 files changed, 246 insertions(+), 183 deletions(-) delete mode 100644 src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java create mode 100644 src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java create mode 100644 src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping_hash_355e155fc3d10c4e5f2c6086a01281d2e947d932_secret_123.json 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..1ff9708ec 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -3,7 +3,9 @@ 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.google.common.base.Supplier; import com.thoughtworks.xstream.annotations.XStreamAlias; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; @@ -15,10 +17,10 @@ import hudson.util.Secret; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.internal.GitHubLoginFunction; +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.jenkinsci.plugins.plaincredentials.StringCredentials; -import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; import org.kohsuke.github.GitHub; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -32,14 +34,16 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; +import java.util.List; -import static com.cloudbees.plugins.credentials.CredentialsMatchers.firstOrDefault; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.filter; import static com.cloudbees.plugins.credentials.CredentialsMatchers.withId; import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials; 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; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; /** * This object represents configuration of each credentials-github pair. @@ -192,8 +196,8 @@ public static Function 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 * @@ -201,12 +205,37 @@ public static Function loginToGithub() { */ @Nonnull public static String tokenFor(String credentialsId) { - StringCredentialsImpl unkn = new StringCredentialsImpl(null, null, null, Secret.fromString(UNKNOWN_TOKEN)); - return firstOrDefault( + 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) { + List creds = filter( lookupCredentials(StringCredentials.class, Jenkins.getInstance(), ACL.SYSTEM, Collections.emptyList()), - withId(credentialsId), unkn).getSecret().getPlainText(); + withId(trimToEmpty(credentialsId)) + ); + + return FluentIterableWrapper.from(creds) + .transform(new NullSafeFunction() { + @Override + protected Secret applyNullSafe(@Nonnull StringCredentials input) { + return input.getSecret(); + } + }).first(); } /** diff --git a/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java index 0285ea763..39e5a8d25 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java @@ -2,7 +2,6 @@ import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.DomainRequirement; -import com.thoughtworks.xstream.annotations.XStreamAlias; import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; @@ -16,15 +15,11 @@ import javax.annotation.Nullable; 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 org.apache.commons.lang.StringUtils.isEmpty; /** * Manages storing/retrieval of the shared secret for the hook. */ -@XStreamAlias("hook-config") public class HookSecretConfig extends AbstractDescribableImpl { private String credentialsId; @@ -34,30 +29,14 @@ public HookSecretConfig(String credentialsId) { this.credentialsId = credentialsId; } - private StringCredentials getHookSecretCredentials() { - if (isEmpty(credentialsId)) { - return null; - } - - return firstOrDefault( - lookupCredentials(StringCredentials.class, - Jenkins.getInstance(), ACL.SYSTEM, - Collections.emptyList()), - withId(credentialsId), null); - } - /** * Gets the currently used secret being used for payload verification. + * * @return Current secret, null if not set. */ @Nullable public Secret getHookSecret() { - StringCredentials credentials = getHookSecretCredentials(); - if (credentials != null) { - return credentials.getSecret(); - } else { - return null; - } + return GitHubServerConfig.secretFor(credentialsId).orNull(); } public String getCredentialsId() { diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java b/src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java deleted file mode 100644 index 0e26d15b1..000000000 --- a/src/main/java/org/jenkinsci/plugins/github/extension/CryptoUtil.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.jenkinsci.plugins.github.extension; - -import hudson.util.Secret; -import org.apache.commons.codec.binary.Hex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -/** - * Utility class for dealing with signatures of incoming requests. - * - * @see API documentation - */ -public class CryptoUtil { - private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; - private static final Logger LOGGER = LoggerFactory.getLogger(CryptoUtil.class); - private static final String SHA1_PREFIX = "sha1="; - private static final String DEFAULT_CHARSET = "utf-8"; - public static final String INVALID_SIGNATURE = "INVALID_SIGNATURE"; - - private CryptoUtil() { - } - - /** - * Computes a RFC 2104-compliant HMAC digest using SHA1 of a payload with a given key (secret). - * - * @param payload Clear-text to create signature of. - * @param secret Key to sign with. - * - * @return HMAC digest of payload using secret as key. Will return INVALID_SIGNATURE if any args is null. - */ - @Nullable - public static String computeSHA1Signature(@Nullable final String payload, @Nullable final Secret secret) { - if (payload == null || secret == null) { - return INVALID_SIGNATURE; - } - - try { - final SecretKeySpec keySpec = new SecretKeySpec( - secret.getPlainText().getBytes(DEFAULT_CHARSET), - HMAC_SHA1_ALGORITHM - ); - final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); - mac.init(keySpec); - final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(DEFAULT_CHARSET)); - - return Hex.encodeHexString(rawHMACBytes); - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - return null; - } - } - - /** - * Grabs the value after "sha1=" in a string. - * - * @param digest The string to get the sha1 value from. - * - * @return Value after "sha1" present in the digest value. Null if not present. - */ - @Nullable - public static String parseSHA1Value(@Nullable final String digest) { - if (digest != null && digest.startsWith(SHA1_PREFIX)) { - return digest.substring(SHA1_PREFIX.length()); - } else { - return null; - } - } -} 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..c1eb060d2 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java @@ -0,0 +1,76 @@ +package org.jenkinsci.plugins.github.webhook; + +import hudson.util.Secret; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +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 TODO + */ +public class GHWebhookSignature { + + private static final Logger LOGGER = LoggerFactory.getLogger(GHWebhookSignature.class); + private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; + 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). + * + * @return HMAC digest of payloadFrom using secret as key. Will return COMPUTED_INVALID_SIGNATURE + * on any exception during computation. + */ + public String sha1() { + try { + final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), HMAC_SHA1_ALGORITHM); + final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); + mac.init(keySpec); + final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(UTF_8)); + + return Hex.encodeHexString(rawHMACBytes); + } catch (Exception e) { + LOGGER.error("", e); + return INVALID_SIGNATURE; + } + } + + /** + * @param digest computed signature from external place (GitHub) + * + * @return true if computed and provided signatures identical + */ + public boolean matches(String digest) { + String computed = sha1(); + LOGGER.trace("Signature: calculated={} provided={}", computed, digest); + return StringUtils.equals(computed, digest); + } +} 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 8108dae90..29a49a9cb 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -1,6 +1,7 @@ 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; @@ -22,6 +23,7 @@ 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 static com.cloudbees.jenkins.GitHubWebHook.X_INSTANCE_IDENTITY; @@ -35,9 +37,7 @@ import static javax.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.jenkinsci.plugins.github.extension.CryptoUtil.INVALID_SIGNATURE; -import static org.jenkinsci.plugins.github.extension.CryptoUtil.computeSHA1Signature; -import static org.jenkinsci.plugins.github.extension.CryptoUtil.parseSHA1Value; +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; @@ -62,6 +62,7 @@ class Processor extends Interceptor { * @see Developer manual */ public static final String SIGNATURE_HEADER = "X-Hub-Signature"; + private static final String SHA1_PREFIX = "sha1="; @Override public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, Object[] arguments) @@ -138,45 +139,44 @@ protected void shouldContainParseablePayload(Object[] arguments) throws Invocati * @throws InvocationTargetException if any of preconditions is not satisfied */ protected void shouldProvideValidSignature(StaplerRequest req, Object[] args) throws InvocationTargetException { - final String signature = parseSHA1Value(req.getHeader(SIGNATURE_HEADER)); - final Secret secret = GitHubPlugin.configuration().getHookSecretConfig().getHookSecret(); - final String payload = obtainRequestBody(req, args); - final String computedSignature = computeSHA1Signature(payload, secret); + Optional signHeader = Optional.fromNullable(req.getHeader(SIGNATURE_HEADER)); + Secret secret = GitHubPlugin.configuration().getHookSecretConfig().getHookSecret(); - if (secret != null) { + if (signHeader.isPresent()) { + String digest = substringAfter(signHeader.get(), SHA1_PREFIX); + LOGGER.trace("Trying to verify sign from header {}", signHeader.get()); isTrue( - signature != null && !INVALID_SIGNATURE.equals(signature), - "Signature must be specified in the header " + SIGNATURE_HEADER - ); - - isTrue( - computedSignature != null, - "Missing payload" - ); - - isTrue( - computedSignature.equals(signature), - String.format("Signatures did not match, computed signature was: %s", computedSignature) + GHWebhookSignature.webhookSignature(payloadFrom(req, args), secret).matches(digest), + String.format("Provided signature [%s] did not match to calculated", digest) ); } } - protected String obtainRequestBody(StaplerRequest req, Object[] args) { + /** + * 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(StaplerRequest 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, "UTF-8")); + 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 null; + } + return ""; } /** 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 8e1079ea5..d3d475ad8 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -5,10 +5,9 @@ import com.google.common.base.Predicate; import hudson.model.Job; import hudson.util.Secret; -import jenkins.model.Jenkins; import org.apache.commons.lang.Validate; +import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor; -import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.jenkinsci.plugins.github.util.misc.NullSafePredicate; @@ -180,9 +179,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; } @@ -298,8 +297,7 @@ protected GHHook applyNullSafe(@Nonnull GHRepository repo) { config.put("url", url.toExternalForm()); config.put("content_type", "json"); - final Secret secret = Jenkins.getInstance() - .getDescriptorByType(GitHubPluginConfig.class).getHookSecretConfig().getHookSecret(); + final Secret secret = GitHubPlugin.configuration().getHookSecretConfig().getHookSecret(); if (secret != null) { config.put("secret", secret.getPlainText()); 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 index 7ea5e3d07..627e3acad 100644 --- 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 @@ -1,4 +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 different from token secret.
\ No newline at end of file diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index 1dc60583e..72e4b3f45 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -6,6 +6,7 @@ import com.jayway.restassured.response.Header; import com.jayway.restassured.specification.RequestSpecification; import org.apache.commons.io.IOUtils; +import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.webhook.GHEventHeader; import org.junit.ClassRule; import org.junit.Rule; @@ -14,6 +15,7 @@ import org.jvnet.hudson.test.JenkinsRule; import org.kohsuke.github.GHEvent; +import javax.inject.Inject; import java.io.File; import java.io.IOException; @@ -27,6 +29,8 @@ import static org.apache.commons.lang3.ClassUtils.PACKAGE_SEPARATOR; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.notNullValue; +import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecretIn; +import static org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER; /** * @author lanwen (Merkushev Kirill) @@ -41,10 +45,21 @@ public class GitHubWebHookFullTest { public static final String NOT_NULL_VALUE = "nonnull"; private RequestSpecification spec; + + @Inject + private GitHubPluginConfig config; @ClassRule public static JenkinsRule jenkins = new JenkinsRule(); + @Rule + public ExternalResource inject = new ExternalResource() { + @Override + protected void before() throws Throwable { + jenkins.getInstance().getInjector().injectMembers(GitHubWebHookFullTest.this); + } + }; + @Rule public ExternalResource setup = new ExternalResource() { @Override @@ -70,6 +85,22 @@ public void shouldParseJsonWebHookFromGH() throws Exception { .expect().log().all().statusCode(SC_OK).post(); } + + @Test + public void shouldParseJsonWebHookFromGHWithSignHeader() throws Exception { + String hash = "355e155fc3d10c4e5f2c6086a01281d2e947d932"; + String secret = "123"; + + storeSecretIn(config, secret); + given().spec(spec) + .header(eventHeader(GHEvent.PUSH)) + .header(JSON_CONTENT_TYPE) + .header(SIGNATURE_HEADER, format("sha1=%s", hash)) + .content(classpath(String.format("payloads/ping_hash_%s_secret_%s.json", hash, secret))) + .log().all() + .expect().log().all().statusCode(SC_OK).post(); + } + @Test public void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { given().spec(spec) diff --git a/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java index 4cfa37dda..0f0cb150c 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java @@ -1,6 +1,6 @@ package org.jenkinsci.plugins.github.config; -import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.GitHubPlugin; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -17,6 +17,7 @@ public class HookSecretConfigTest { private static final String SECRET_INIT = "test"; + @Rule public JenkinsRule jenkinsRule = new JenkinsRule(); @@ -25,7 +26,7 @@ public class HookSecretConfigTest { @Before public void setup() { storeSecret(SECRET_INIT); - hookSecretConfig = Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class).getHookSecretConfig(); + hookSecretConfig = GitHubPlugin.configuration().getHookSecretConfig(); } @Test diff --git a/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java b/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java index 5074b8652..c65877a15 100644 --- a/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java @@ -1,56 +1,41 @@ package org.jenkinsci.plugins.github.extension; import hudson.util.Secret; -import org.junit.Before; -import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; -import org.junit.runner.RunWith; import org.jvnet.hudson.test.JenkinsRule; -import org.mockito.runners.MockitoJUnitRunner; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.*; +import static org.jenkinsci.plugins.github.webhook.GHWebhookSignature.webhookSignature; +import static org.junit.Assert.assertThat; /** * Tests for utility class that deals with crypto/hashing of data. + * * @author martinmine */ -@RunWith(MockitoJUnitRunner.class) public class CryptoUtilTest { private static final String SIGNATURE = "85d155c55ed286a300bd1cf124de08d87e914f3a"; private static final String PAYLOAD = "foo"; - private Secret globalSecret; - private Secret projectSecret; - private Secret secret; + private static final String SECRET = "bar"; - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); - - @Before - public void setupSecrets() { - globalSecret = Secret.fromString("global secret"); - projectSecret = Secret.fromString("project secret"); - secret = Secret.fromString("bar"); - } + @ClassRule + public static JenkinsRule jRule = new JenkinsRule(); @Test public void shouldComputeSHA1Signature() throws Exception { - final String signature = CryptoUtil.computeSHA1Signature(PAYLOAD, secret); - - assertThat("signature is valid", signature, equalTo(SIGNATURE)); - } - - @Test - public void shouldParseCorrectSHA1Signature() throws Exception { - final String parsedSignature = CryptoUtil.parseSHA1Value("sha1=" + SIGNATURE); - assertThat("parsed signature is valid", parsedSignature, equalTo(SIGNATURE)); + assertThat("signature is valid", webhookSignature( + PAYLOAD, + Secret.fromString(SECRET) + ).sha1(), equalTo(SIGNATURE)); } @Test - public void shouldReturnNullWithNoSignature() throws Exception { - final String parsedSignature = CryptoUtil.parseSHA1Value(null); - assertThat("signature is null", parsedSignature, nullValue()); + public void shouldMatchSignature() throws Exception { + assertThat("signature should match", webhookSignature( + PAYLOAD, + Secret.fromString(SECRET) + ).matches(SIGNATURE), equalTo(true)); } } \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java b/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java index 33a4137ec..d9965f440 100644 --- a/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java +++ b/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java @@ -25,9 +25,11 @@ private HookSecretHelper() { /** * Stores the secret and sets it as the current hook secret. + * + * @param config where to save * @param secretText The secret/key. */ - public static void storeSecret(final String secretText) { + public static void storeSecretIn(GitHubPluginConfig config, final String secretText) { final StringCredentialsImpl credentials = new StringCredentialsImpl( CredentialsScope.GLOBAL, UUID.randomUUID().toString(), @@ -49,8 +51,15 @@ public void run() { } } }); - - Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class) - .getHookSecretConfig().setCredentialsId(credentials.getId()); + + config.getHookSecretConfig().setCredentialsId(credentials.getId()); + } + + /** + * Stores the secret and sets it as the current hook secret. + * @param secretText The secret/key. + */ + public static void storeSecret(final String secretText) { + storeSecretIn(Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class), secretText); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java index 3c65c30ad..9e69cc870 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java @@ -16,7 +16,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecret; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) @@ -68,32 +69,32 @@ public void shouldNotPassOnNullGHEventAndNotBlankPayload() throws Exception { @Test(expected = InvocationTargetException.class) public void shouldNotPassOnGHEventAndBlankPayload() throws Exception { new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[] {GHEvent.PUSH, " "}); + new Object[]{GHEvent.PUSH, " "}); } @Test(expected = InvocationTargetException.class) public void shouldNotPassOnNulls() throws Exception { new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[] {null, null}); + new Object[]{null, null}); } @Test(expected = InvocationTargetException.class) public void shouldNotPassOnGreaterCountOfArgs() throws Exception { new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[] {GHEvent.PUSH, "{}", " "} + new Object[]{GHEvent.PUSH, "{}", " "} ); } @Test(expected = InvocationTargetException.class) public void shouldNotPassOnLessCountOfArgs() throws Exception { new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[] {GHEvent.PUSH} + new Object[]{GHEvent.PUSH} ); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnAbsentSignature() throws Exception { - doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + @Test + public void shouldPassOnAbsentSignatureInRequest() throws Exception { + doReturn(PAYLOAD).when(processor).payloadFrom(req, null); processor.shouldProvideValidSignature(req, null); } @@ -103,7 +104,7 @@ public void shouldNotPassOnInvalidSignature() throws Exception { final String signature = "sha1=a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"; when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); - doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + doReturn(PAYLOAD).when(processor).payloadFrom(req, null); processor.shouldProvideValidSignature(req, null); } @@ -113,7 +114,7 @@ public void shouldNotPassOnMalformedSignature() throws Exception { final String signature = "49d5f5cf800a81f257324912969a2d325d13d3fc"; when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); - doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + doReturn(PAYLOAD).when(processor).payloadFrom(req, null); processor.shouldProvideValidSignature(req, null); } @@ -123,7 +124,7 @@ public void shouldPassWithValidSignature() throws Exception { final String signature = "sha1=49d5f5cf800a81f257324912969a2d325d13d3fc"; when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); - doReturn(PAYLOAD).when(processor).obtainRequestBody(req, null); + doReturn(PAYLOAD).when(processor).payloadFrom(req, null); processor.shouldProvideValidSignature(req, null); } @@ -134,7 +135,7 @@ public void shouldReturnValidPayloadOnApplicationJson() { doReturn(GHEventPayload.PayloadHandler.APPLICATION_JSON).when(req).getContentType(); - final String body = processor.obtainRequestBody(req, new Object[]{null, payload}); + final String body = processor.payloadFrom(req, new Object[]{null, payload}); assertThat("valid returned body", body, equalTo(payload)); } @@ -145,7 +146,7 @@ public void shouldReturnValidPayloadOnFormUrlEncoded() { doReturn(GHEventPayload.PayloadHandler.FORM_URLENCODED).when(req).getContentType(); - final String body = processor.obtainRequestBody(req, new Object[]{null, payload}); + final String body = processor.payloadFrom(req, new Object[]{null, payload}); assertThat("valid returned body", body, equalTo("payload=" + payload)); } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index 27d9ecbce..e6952fc12 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -4,6 +4,7 @@ import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import hudson.model.FreeStyleProject; import hudson.plugins.git.GitSCM; @@ -26,20 +27,26 @@ import java.net.URL; import java.util.Collections; import java.util.EnumSet; +import java.util.Map; import static com.google.common.collect.ImmutableList.copyOf; import static com.google.common.collect.Lists.asList; import static com.google.common.collect.Lists.newArrayList; +import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecretIn; import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; import static org.junit.Assert.assertThat; import static org.kohsuke.github.GHEvent.CREATE; import static org.kohsuke.github.GHEvent.PULL_REQUEST; import static org.kohsuke.github.GHEvent.PUSH; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anySetOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -221,6 +228,23 @@ public void shouldNotSelectCredsWithCustomHost() { .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue()); } + @Test + public void shouldSendSecretIfDefined() throws Exception { + String secretText = "secret_text"; + + storeSecretIn(GitHubPlugin.configuration(), secretText); + + manager.createWebhook(HOOK_ENDPOINT, ImmutableSet.of(PUSH)).apply(repo); + + verify(repo).createHook( + anyString(), + (Map) argThat(hasEntry("secret", secretText)), + anySetOf(GHEvent.class), + anyBoolean() + ); + + } + private GHHook hook(URL endpoint, GHEvent event, GHEvent... events) { GHHook hook = mock(GHHook.class); when(hook.getName()).thenReturn("web"); diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping_hash_355e155fc3d10c4e5f2c6086a01281d2e947d932_secret_123.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping_hash_355e155fc3d10c4e5f2c6086a01281d2e947d932_secret_123.json new file mode 100644 index 000000000..e16e775b5 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/ping_hash_355e155fc3d10c4e5f2c6086a01281d2e947d932_secret_123.json @@ -0,0 +1 @@ +{"zen":"It's not fully shipped until it's fast.","hook_id":9480855,"hook":{"type":"Repository","id":9480855,"name":"web","active":true,"events":["push"],"config":{"content_type":"json","insecure_ssl":"0","secret":"********","url":"http://requestb.in/pwz161pw"},"updated_at":"2016-08-11T21:40:12Z","created_at":"2016-08-11T21:40:12Z","url":"https://api.github.com/repos/lanwen/test/hooks/9480855","test_url":"https://api.github.com/repos/lanwen/test/hooks/9480855/test","ping_url":"https://api.github.com/repos/lanwen/test/hooks/9480855/pings","last_response":{"code":null,"status":"unused","message":null}},"repository":{"id":38941520,"name":"test","full_name":"lanwen/test","owner":{"login":"lanwen","id":1964214,"avatar_url":"https://avatars.githubusercontent.com/u/1964214?v=3","gravatar_id":"","url":"https://api.github.com/users/lanwen","html_url":"https://github.com/lanwen","followers_url":"https://api.github.com/users/lanwen/followers","following_url":"https://api.github.com/users/lanwen/following{/other_user}","gists_url":"https://api.github.com/users/lanwen/gists{/gist_id}","starred_url":"https://api.github.com/users/lanwen/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lanwen/subscriptions","organizations_url":"https://api.github.com/users/lanwen/orgs","repos_url":"https://api.github.com/users/lanwen/repos","events_url":"https://api.github.com/users/lanwen/events{/privacy}","received_events_url":"https://api.github.com/users/lanwen/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/lanwen/test","description":"for test purposes","fork":false,"url":"https://api.github.com/repos/lanwen/test","forks_url":"https://api.github.com/repos/lanwen/test/forks","keys_url":"https://api.github.com/repos/lanwen/test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lanwen/test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lanwen/test/teams","hooks_url":"https://api.github.com/repos/lanwen/test/hooks","issue_events_url":"https://api.github.com/repos/lanwen/test/issues/events{/number}","events_url":"https://api.github.com/repos/lanwen/test/events","assignees_url":"https://api.github.com/repos/lanwen/test/assignees{/user}","branches_url":"https://api.github.com/repos/lanwen/test/branches{/branch}","tags_url":"https://api.github.com/repos/lanwen/test/tags","blobs_url":"https://api.github.com/repos/lanwen/test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lanwen/test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lanwen/test/git/refs{/sha}","trees_url":"https://api.github.com/repos/lanwen/test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lanwen/test/statuses/{sha}","languages_url":"https://api.github.com/repos/lanwen/test/languages","stargazers_url":"https://api.github.com/repos/lanwen/test/stargazers","contributors_url":"https://api.github.com/repos/lanwen/test/contributors","subscribers_url":"https://api.github.com/repos/lanwen/test/subscribers","subscription_url":"https://api.github.com/repos/lanwen/test/subscription","commits_url":"https://api.github.com/repos/lanwen/test/commits{/sha}","git_commits_url":"https://api.github.com/repos/lanwen/test/git/commits{/sha}","comments_url":"https://api.github.com/repos/lanwen/test/comments{/number}","issue_comment_url":"https://api.github.com/repos/lanwen/test/issues/comments{/number}","contents_url":"https://api.github.com/repos/lanwen/test/contents/{+path}","compare_url":"https://api.github.com/repos/lanwen/test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lanwen/test/merges","archive_url":"https://api.github.com/repos/lanwen/test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lanwen/test/downloads","issues_url":"https://api.github.com/repos/lanwen/test/issues{/number}","pulls_url":"https://api.github.com/repos/lanwen/test/pulls{/number}","milestones_url":"https://api.github.com/repos/lanwen/test/milestones{/number}","notifications_url":"https://api.github.com/repos/lanwen/test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lanwen/test/labels{/name}","releases_url":"https://api.github.com/repos/lanwen/test/releases{/id}","deployments_url":"https://api.github.com/repos/lanwen/test/deployments","created_at":"2015-07-11T21:47:22Z","updated_at":"2016-08-11T20:06:19Z","pushed_at":"2016-08-11T20:06:17Z","git_url":"git://github.com/lanwen/test.git","ssh_url":"git@github.com:lanwen/test.git","clone_url":"https://github.com/lanwen/test.git","svn_url":"https://github.com/lanwen/test","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"},"sender":{"login":"lanwen","id":1964214,"avatar_url":"https://avatars.githubusercontent.com/u/1964214?v=3","gravatar_id":"","url":"https://api.github.com/users/lanwen","html_url":"https://github.com/lanwen","followers_url":"https://api.github.com/users/lanwen/followers","following_url":"https://api.github.com/users/lanwen/following{/other_user}","gists_url":"https://api.github.com/users/lanwen/gists{/gist_id}","starred_url":"https://api.github.com/users/lanwen/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lanwen/subscriptions","organizations_url":"https://api.github.com/users/lanwen/orgs","repos_url":"https://api.github.com/users/lanwen/repos","events_url":"https://api.github.com/users/lanwen/events{/privacy}","received_events_url":"https://api.github.com/users/lanwen/received_events","type":"User","site_admin":false}} \ No newline at end of file From 01ba06c990dbf236f40ae3175a66304feaae0016 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Mon, 15 Aug 2016 13:05:57 +0400 Subject: [PATCH 222/228] [maven-release-plugin] prepare release v1.21.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c59097de6..bef0c37a2 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.20.1-SNAPSHOT + 1.21.0 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + v1.21.0 JIRA From 8262d62e985ffb5d10c938eb14377cb20cfe1ed9 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Mon, 15 Aug 2016 13:06:05 +0400 Subject: [PATCH 223/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bef0c37a2..1346c1fc6 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.21.0 + 1.21.1-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - v1.21.0 + HEAD JIRA From 05ad7b42033c549e06b71884ed7ec761cfb832fb Mon Sep 17 00:00:00 2001 From: Kirill Merkushev Date: Thu, 18 Aug 2016 23:12:30 +0300 Subject: [PATCH 224/228] [JENKINS-37481] Ignore sign header if sign not defined in Jenkins --- .../webhook/RequirePostWithGHHookPayload.java | 2 +- .../webhook/RequirePostWithGHHookPayloadTest.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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 29a49a9cb..fa479c3de 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -142,7 +142,7 @@ protected void shouldProvideValidSignature(StaplerRequest req, Object[] args) th Optional signHeader = Optional.fromNullable(req.getHeader(SIGNATURE_HEADER)); Secret secret = GitHubPlugin.configuration().getHookSecretConfig().getHookSecret(); - if (signHeader.isPresent()) { + if (signHeader.isPresent() && Optional.fromNullable(secret).isPresent()) { String digest = substringAfter(signHeader.get(), SHA1_PREFIX); LOGGER.trace("Trying to verify sign from header {}", signHeader.get()); isTrue( diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java index 9e69cc870..e13d4e0e1 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java @@ -1,9 +1,12 @@ package org.jenkinsci.plugins.github.webhook; +import org.jenkinsci.plugins.github.GitHubPlugin; +import org.jenkinsci.plugins.github.config.HookSecretConfig; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.kohsuke.github.GHEvent; import org.kohsuke.stapler.StaplerRequest; @@ -129,6 +132,17 @@ public void shouldPassWithValidSignature() throws Exception { processor.shouldProvideValidSignature(req, null); } + @Test + @Issue("JENKINS-37481") + public void shouldIgnoreSignHeaderOnNotDefinedSignInConfig() throws Exception { + GitHubPlugin.configuration().setHookSecretConfig(new HookSecretConfig(null)); + final String signature = "sha1=49d5f5cf800a81f257324912969a2d325d13d3fc"; + + when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); + + processor.shouldProvideValidSignature(req, null); + } + @Test public void shouldReturnValidPayloadOnApplicationJson() { final String payload = "test"; From ef4f66dc87fcb9193526c47a4662b68b9d81cfb2 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Fri, 19 Aug 2016 14:20:54 +0400 Subject: [PATCH 225/228] [maven-release-plugin] prepare release v1.21.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1346c1fc6..97c571793 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.21.1-SNAPSHOT + 1.21.1 hpi GitHub plugin @@ -38,7 +38,7 @@ 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 + v1.21.1 JIRA From c7927b4975a55103d71a7c627c543db39d8529a7 Mon Sep 17 00:00:00 2001 From: lanwen-ci Date: Fri, 19 Aug 2016 14:21:00 +0400 Subject: [PATCH 226/228] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 97c571793..b7afe773e 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.coravy.hudson.plugins.github github - 1.21.1 + 1.21.2-SNAPSHOT hpi GitHub plugin @@ -38,7 +38,7 @@ scm:git:git://github.com/jenkinsci/github-plugin.git scm:git:git@github.com:jenkinsci/github-plugin.git https://github.com/jenkinsci/github-plugin - v1.21.1 + HEAD JIRA From c0fbee2375e7b092a5d4f0d24312b05a42d23fba Mon Sep 17 00:00:00 2001 From: clalleme Date: Mon, 7 Sep 2015 19:04:45 +0200 Subject: [PATCH 227/228] Allow to bann a pusher to avoid triggering loop when your job is pushing to github --- .../plugins/github/config/GitHubPluginConfig.java | 9 +++++++++ .../subscriber/DefaultPushGHEventSubscriber.java | 10 ++++++++-- .../github/config/GitHubPluginConfig/config.groovy | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) 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 5f2392679..0bfdd5be3 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -64,6 +64,7 @@ public class GitHubPluginConfig extends GlobalConfiguration { private HookSecretConfig hookSecretConfig = new HookSecretConfig(null); private transient boolean overrideHookUrl; + private String bannedCommitter = ""; /** * Used to get current instance identity. @@ -102,6 +103,14 @@ public void setHookUrl(URL hookUrl) { } } + public String getBannedCommitter() { + return bannedCommitter; + } + + public void setBannedCommitter(final String aBannedCommitter) { + bannedCommitter = aBannedCommitter; + } + public void setOverrideHookUrl(boolean overrideHookUrl) { this.overrideHookUrl = overrideHookUrl; } 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..d9c9da778 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 @@ -10,6 +10,7 @@ import hudson.security.ACL; import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; import org.slf4j.Logger; @@ -65,7 +66,7 @@ protected void onEvent(GHEvent event, String payload) { String repoUrl = json.getJSONObject("repository").getString("url"); final String pusherName = json.getJSONObject("pusher").getString("name"); - LOGGER.info("Received POST for {}", repoUrl); + LOGGER.info("Received POST for: {}, pusher: {}", repoUrl, pusherName); final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); if (changedRepository != null) { @@ -81,7 +82,12 @@ public void run() { LOGGER.debug("Considering to poke {}", job.getFullDisplayName()); if (GitHubRepositoryNameContributor.parseAssociatedNames(job).contains(changedRepository)) { LOGGER.info("Poked {}", job.getFullDisplayName()); - trigger.onPost(pusherName); + if (pusherName.equalsIgnoreCase(GitHubPlugin.configuration().getBannedCommitter())) { + LOGGER.info("Skipped {} because user is banned: {}.", job.getFullDisplayName() + , pusherName); + } else { + trigger.onPost(pusherName); + } } else { LOGGER.debug("Skipped {} because it doesn't have a matching repository.", job.getFullDisplayName()); 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 d74c04bea..53bbb3bac 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,6 +1,7 @@ package org.jenkinsci.plugins.github.config.GitHubPluginConfig import com.cloudbees.jenkins.GitHubPushTrigger +import org.jenkinsci.plugins.github.config.GitHubServerConfig def f = namespace(lib.FormTagLib); @@ -36,6 +37,10 @@ f.section(title: descriptor.displayName) { } } + f.entry(title: _("Banned committer"), field: "bannedCommitter") { + f.textbox() + } + f.property( field: "hookSecretConfig" ) From 83cb0cb52c3459583bc04bee58ff63cefc732c64 Mon Sep 17 00:00:00 2001 From: Christophe Date: Sat, 20 Aug 2016 14:59:43 +0200 Subject: [PATCH 228/228] rebase and fix CR --- .../DefaultPushGHEventSubscriber.java | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) 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 d9c9da778..f6dd93229 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 @@ -24,7 +24,8 @@ import static org.kohsuke.github.GHEvent.PUSH; /** - * By default this plugin interested in push events only when job uses {@link GitHubPushTrigger} + * By default this plugin interested in push events only when job uses + * {@link GitHubPushTrigger} * * @author lanwen (Merkushev Kirill) * @since 1.12.0 @@ -57,7 +58,7 @@ protected Set events() { /** * Calls {@link GitHubPushTrigger} in all projects to handle this hook * - * @param event only PUSH event + * @param event only PUSH event * @param payload payload of gh-event. Never blank */ @Override @@ -67,32 +68,21 @@ protected void onEvent(GHEvent event, String payload) { final String pusherName = json.getJSONObject("pusher").getString("name"); LOGGER.info("Received POST for: {}, pusher: {}", repoUrl, pusherName); - final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl); + final GitHubRepositoryName changedRepository = GitHubRepositoryName + .create(repoUrl); if (changedRepository != null) { // run in high privilege to see all the projects anonymous users don't see. - // this is safe because when we actually schedule a build, it's a build that can + // this is safe because when we actually schedule a build, it's a build that + // can // happen at some random time anyway. ACL.impersonate(ACL.SYSTEM, new Runnable() { @Override public void run() { for (Job job : Jenkins.getInstance().getAllItems(Job.class)) { - GitHubTrigger 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()); - if (pusherName.equalsIgnoreCase(GitHubPlugin.configuration().getBannedCommitter())) { - LOGGER.info("Skipped {} because user is banned: {}.", job.getFullDisplayName() - , pusherName); - } else { - trigger.onPost(pusherName); - } - } else { - LOGGER.debug("Skipped {} because it doesn't have a matching repository.", - job.getFullDisplayName()); - } - } + final GitHubTrigger trigger = triggerFrom(job, + GitHubPushTrigger.class); + triggerJob(job, trigger, pusherName, changedRepository); } } }); @@ -101,9 +91,30 @@ public void run() { .getExtensionList(GitHubWebHook.Listener.class)) { listener.onPushRepositoryChanged(pusherName, changedRepository); } - } else { - LOGGER.warn("Malformed repo url {}", repoUrl); + LOGGER.warn("Malformed repo url: {}", repoUrl); + } + } + + static void triggerJob(Job job, GitHubTrigger trigger, String pusherName, + GitHubRepositoryName changedRepository) { + if (trigger != null) { + LOGGER.debug("Considering to poke: {}", job.getFullDisplayName()); + if (GitHubRepositoryNameContributor.parseAssociatedNames(job) + .contains(changedRepository)) { + LOGGER.info("Poked: {}", job.getFullDisplayName()); + if (pusherName.equalsIgnoreCase( + GitHubPlugin.configuration().getBannedCommitter())) { + LOGGER.info("Job: {} skipped because user: {} is banned.", + job.getFullDisplayName(), pusherName); + } else { + trigger.onPost(pusherName); + } + } else { + LOGGER.debug( + "Job: {} skipped because it doesn't have a matching repository.", + job.getFullDisplayName()); + } } } }