Skip to content

Commit

Permalink
[JENKINS-43507] Switch to trait based configuration
Browse files Browse the repository at this point in the history
Also:

- Adds support for building the merge commits of pull requests
- Adds support for trusted revisions
- Adds support for configuring Git extensions and Mercurial additional behaviours
- Adds lots of tests (traits make code that was previously hard to test easier to test)

Should:

- Migrate existing configuration to the same effective configuration
- Maintain backwards binary compatibility
- Not maintain backwards source compatibility (if you wrote a plugin to the old APIs you will need to fix when you upgrade the plugin dependency)
  • Loading branch information
stephenc committed Jun 12, 2017
1 parent 50166af commit a4c6bf3
Show file tree
Hide file tree
Showing 97 changed files with 9,680 additions and 1,077 deletions.
36 changes: 26 additions & 10 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</parent>

<artifactId>cloudbees-bitbucket-branch-source</artifactId>
<version>2.1.3-SNAPSHOT</version>
<version>2.2.0-SNAPSHOT</version>
<packaging>hpi</packaging>

<name>Bitbucket Branch Source Plugin</name>
Expand All @@ -48,7 +48,8 @@

<properties>
<jenkins.version>1.642.3</jenkins.version>
<scm-api.version>2.2.0-20170411.160059-4</scm-api.version>
<scm-api.version>2.2.0-SNAPSHOT</scm-api.version>
<git.version>3.4.0-SNAPSHOT</git.version>
</properties>

<scm>
Expand All @@ -58,16 +59,24 @@
<tag>HEAD</tag>
</scm>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>${scm-api.version}</version>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>${scm-api.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>git</artifactId>
<version>2.6.5</version>
<version>${git.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
Expand All @@ -78,7 +87,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>mercurial</artifactId>
<version>1.58</version>
<version>2.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
Expand All @@ -93,7 +102,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>branch-api</artifactId>
<version>2.0.6</version>
<version>2.0.10-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -118,7 +127,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>git</artifactId>
<version>2.6.5</version>
<version>${git.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
Expand Down Expand Up @@ -156,6 +165,13 @@
<plugin>
<groupId>org.jenkins-ci.tools</groupId>
<artifactId>maven-hpi-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>generate-taglib-interface</goal>
</goals>
</execution>
</executions>
<configuration>
<compatibleSinceVersion>2.0.0</compatibleSinceVersion>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,147 +25,108 @@

import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.FilePath;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.model.listeners.SCMListener;
import hudson.plugins.git.Revision;
import hudson.plugins.git.util.BuildData;
import hudson.plugins.mercurial.MercurialTagAction;
import hudson.plugins.mercurial.MercurialSCMSource;
import hudson.scm.SCM;
import hudson.scm.SCMRevisionState;
import java.io.File;
import java.io.IOException;
import jenkins.scm.api.SCMHead;
import javax.annotation.CheckForNull;
import jenkins.plugins.git.AbstractGitSCMSource;
import jenkins.scm.api.SCMHeadObserver;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.SCMSourceOwner;
import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;

/**
* This class encapsulates all Bitbucket notifications logic.
* {@link JobCompletedListener} sends a notification to Bitbucket after a build finishes.
* Only builds derived from a job that was created as part of a multi branch project will be processed by this listener.
*
* The way the notification is sent is defined by the implementation of {@link BitbucketNotifier} returned by {@link #getNotifier(BitbucketApi)}.
*
*/
public class BitbucketBuildStatusNotifications {

private static void createBuildCommitStatus(@NonNull Run<?,?> build, @NonNull TaskListener listener, @NonNull BitbucketApi bitbucket)
private static void createStatus(@NonNull Run<?, ?> build, @NonNull TaskListener listener,
@NonNull BitbucketApi bitbucket, @NonNull String hash)
throws IOException, InterruptedException {
String revision = extractRevision(build);
if (revision != null) {
Result result = build.getResult();
String url;
try {
url = DisplayURLProvider.get().getRunURL(build);
} catch (IllegalStateException e) {
listener.getLogger().println("Can not determine Jenkins root URL. Commit status notifications are disabled until a root URL is configured in Jenkins global configuration.");
return;
}
BitbucketBuildStatus status = null;
if (Result.SUCCESS.equals(result)) {
status = new BitbucketBuildStatus(revision, "This commit looks good", "SUCCESSFUL", url, build.getParent().getName(), build.getDisplayName());
} else if (Result.UNSTABLE.equals(result)) {
status = new BitbucketBuildStatus(revision, "This commit has test failures", "FAILED", url, build.getParent().getName(), build.getDisplayName());
} else if (Result.FAILURE.equals(result)) {
status = new BitbucketBuildStatus(revision, "There was a failure building this commit", "FAILED", url, build.getParent().getName(), build.getDisplayName());
} else if (result != null) { // ABORTED etc.
status = new BitbucketBuildStatus(revision, "Something is wrong with the build of this commit", "FAILED", url, build.getParent().getName(), build.getDisplayName());
} else {
status = new BitbucketBuildStatus(revision, "The tests have started...", "INPROGRESS", url, build.getParent().getName(), build.getDisplayName());
}
if (status != null) {
getNotifier(bitbucket).buildStatus(status);
}
if (result != null) {
listener.getLogger().println("[Bitbucket] Build result notified");
}
String url;
try {
url = DisplayURLProvider.get().getRunURL(build);
} catch (IllegalStateException e) {
listener.getLogger().println(
"Can not determine Jenkins root URL. Commit status notifications are disabled until a root URL is"
+ " configured in Jenkins global configuration.");
return;
}
}

@CheckForNull
private static String extractRevision(Run<?, ?> build) {
String revision = null;
BuildData gitBuildData = build.getAction(BuildData.class);
if (gitBuildData != null) {
Revision lastBuiltRevision = gitBuildData.getLastBuiltRevision();
if (lastBuiltRevision != null) {
revision = lastBuiltRevision.getSha1String();
}
String key = build.getParent().getFullName(); // use the job full name as the key for the status
String name = build.getDisplayName(); // use the build number as the display name of the status
BitbucketBuildStatus status;
Result result = build.getResult();
if (Result.SUCCESS.equals(result)) {
status = new BitbucketBuildStatus(hash, "This commit looks good", "SUCCESSFUL", url, key, name);
} else if (Result.UNSTABLE.equals(result)) {
status = new BitbucketBuildStatus(hash, "This commit has test failures", "FAILED", url, key, name);
} else if (Result.FAILURE.equals(result)) {
status = new BitbucketBuildStatus(hash, "There was a failure building this commit", "FAILED", url, key,
name);
} else if (result != null) { // ABORTED etc.
status = new BitbucketBuildStatus(hash, "Something is wrong with the build of this commit", "FAILED", url,
key, name);
} else {
MercurialTagAction action = build.getAction(MercurialTagAction.class);
if (action != null) {
revision = action.getId();
}
status = new BitbucketBuildStatus(hash, "The tests have started...", "INPROGRESS", url, key, name);
}
new BitbucketChangesetCommentNotifier(bitbucket).buildStatus(status);
if (result != null) {
listener.getLogger().println("[Bitbucket] Build result notified");
}
return revision;
}

private static void createPullRequestCommitStatus(Run<?,?> build, TaskListener listener, BitbucketApi bitbucket)
private static void sendNotifications(Run<?, ?> build, TaskListener listener)
throws IOException, InterruptedException {
createBuildCommitStatus(build, listener, bitbucket);
}

private static BitbucketNotifier getNotifier(BitbucketApi bitbucket) {
return new BitbucketChangesetCommentNotifier(bitbucket);
}
final SCMSource s = SCMSource.SourceByItem.findSource(build.getParent());
if (!(s instanceof BitbucketSCMSource)) {
return;
}
BitbucketSCMSource source = (BitbucketSCMSource) s;
if (new BitbucketSCMSourceContext(null, SCMHeadObserver.none())
.withTraits(source.getTraits())
.notificationsDisabled()) {
return;
}
SCMRevision r = SCMRevisionAction.getRevision(build); // TODO JENKINS-44648 getRevision(s, build)
String hash = getHash(r);
if (hash == null) {
return;
}
if (r instanceof PullRequestSCMRevision) {
listener.getLogger().println("[Bitbucket] Notifying pull request build result");
createStatus(build, listener, source.buildBitbucketClient((PullRequestSCMHead) r.getHead()), hash);

@CheckForNull
private static BitbucketSCMSource lookUpSCMSource(Run<?, ?> build) {
ItemGroup<?> multiBranchProject = build.getParent().getParent();
if (multiBranchProject instanceof SCMSourceOwner) {
SCMSourceOwner scmSourceOwner = (SCMSourceOwner) multiBranchProject;
BitbucketSCMSource source = lookUpBitbucketSCMSource(scmSourceOwner);
if (source != null) {
return source;
}
} else {
listener.getLogger().println("[Bitbucket] Notifying commit build result");
createStatus(build, listener, source.buildBitbucketClient(), hash);
}
return null;
}



/**
* It is possible having more than one SCMSource in our MultiBranch project.
* TODO: Does it make sense having more than one of the same type?
*
* @param scmSourceOwner An {@link Item} that owns {@link SCMSource} instances.
* @return A source or null
*/
@CheckForNull
private static BitbucketSCMSource lookUpBitbucketSCMSource(final SCMSourceOwner scmSourceOwner) {
for (SCMSource scmSource : scmSourceOwner.getSCMSources()) {
if (scmSource instanceof BitbucketSCMSource) {
return (BitbucketSCMSource) scmSource;
}
private static String getHash(@CheckForNull SCMRevision revision) {
if (revision instanceof PullRequestSCMRevision) {
// unwrap
revision = ((PullRequestSCMRevision) revision).getPull();
}
return null;
}

private static void sendNotifications(Run<?, ?> build, TaskListener listener)
throws IOException, InterruptedException {
BitbucketSCMSource source = lookUpSCMSource(build);
if (source != null && extractRevision(build) != null) {
SCMHead head = SCMHead.HeadByItem.findHead(build.getParent());
if (head instanceof PullRequestSCMHead) {
listener.getLogger().println("[Bitbucket] Notifying pull request build result");
createPullRequestCommitStatus(build, listener, source.buildBitbucketClient((PullRequestSCMHead) head));
} else {
listener.getLogger().println("[Bitbucket] Notifying commit build result");
createBuildCommitStatus(build, listener, source.buildBitbucketClient());
}
if (revision instanceof MercurialSCMSource.MercurialRevision) {
return ((MercurialSCMSource.MercurialRevision) revision).getHash();
} else if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) {
return ((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash();
}
return null;
}

/**
Expand All @@ -175,18 +136,23 @@ private static void sendNotifications(Run<?, ?> build, TaskListener listener)
public static class JobCheckOutListener extends SCMListener {

@Override
public void onCheckout(Run<?, ?> build, SCM scm, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState pollingBaseline) throws Exception {
sendNotifications(build, listener);
public void onCheckout(Run<?, ?> build, SCM scm, FilePath workspace, TaskListener listener, File changelogFile,
SCMRevisionState pollingBaseline) throws Exception {
try {
sendNotifications(build, listener);
} catch (IOException | InterruptedException e) {
e.printStackTrace(listener.error("Could not send notifications"));
}
}
}

/**
* Sends notifications to Bitbucket on Run completed.
*/
@Extension
public static class JobCompletedListener extends RunListener<Run<?,?>> {
public static class JobCompletedListener extends RunListener<Run<?, ?>> {

@Override
@Override
public void onCompleted(Run<?, ?> build, TaskListener listener) {
try {
sendNotifications(build, listener);
Expand Down
Loading

0 comments on commit a4c6bf3

Please sign in to comment.