From 0cc595f638ac6a0a19bab55bda750384416ff608 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Tue, 19 Jul 2016 15:52:17 -0400 Subject: [PATCH 01/11] Add VstsCompletedStatusPostBuildAction skeleton --- .../VstsCompletedStatusPostBuildAction.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java diff --git a/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java b/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java new file mode 100644 index 000000000..092010233 --- /dev/null +++ b/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java @@ -0,0 +1,58 @@ +package hudson.plugins.tfs; + +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.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * A _Post-Build Action_ that reports the completion status of an associated build to VSTS. + */ +public class VstsCompletedStatusPostBuildAction extends Notifier implements SimpleBuildStep { + + @DataBoundConstructor + public VstsCompletedStatusPostBuildAction() { + + } + + @Override + public void perform( + @Nonnull final Run run, + @Nonnull final FilePath workspace, + @Nonnull final Launcher launcher, + @Nonnull final TaskListener listener + ) throws InterruptedException, IOException { + // TODO: implement + } + + @Override + public BuildStepMonitor getRequiredMonitorService() { + // TODO: implement + return null; + } + + @Extension + public static class DescriptorImpl extends BuildStepDescriptor { + + @Override + public boolean isApplicable(final Class jobType) { + return true; + } + + @Override + public String getDisplayName() { + return "Set completion status for VSTS commit or pull request"; + } + } +} From 18304794fb1cf7397a6f969c7196605abc6b7b3a Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Wed, 20 Jul 2016 13:23:51 -0400 Subject: [PATCH 02/11] Add StringHelper.equal[IgnoringCase]() methods --- .../hudson/plugins/tfs/util/StringHelper.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/hudson/plugins/tfs/util/StringHelper.java b/src/main/java/hudson/plugins/tfs/util/StringHelper.java index 7af015edf..0da4c1d3e 100644 --- a/src/main/java/hudson/plugins/tfs/util/StringHelper.java +++ b/src/main/java/hudson/plugins/tfs/util/StringHelper.java @@ -26,6 +26,28 @@ public static boolean endsWithIgnoreCase(final String haystack, final String nee return haystack.regionMatches(true, toffset, needle, 0, nl); } + public static boolean equal(final String a, final String b) { + return innerEqual(a, b, false); + } + + public static boolean equalIgnoringCase(final String a, final String b) { + return innerEqual(a, b, true); + } + + static boolean innerEqual(final String a, final String b, final boolean ignoreCase) { + if (a == null) { + return b == null; + } + if (b == null) { + return false; + } + final int length = a.length(); + if (length != b.length()) { + return false; + } + return a.regionMatches(ignoreCase, 0, b, 0, length); + } + public static String determineContentTypeWithoutCharset(final String contentType) { if (contentType == null) { return null; From 2d963124c8c9e2ecee12ffc30508d94c3bd4d896 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Wed, 20 Jul 2016 13:25:35 -0400 Subject: [PATCH 03/11] Add UriHelper#areSame() with tests --- .../hudson/plugins/tfs/util/UriHelper.java | 93 ++++++++++++++++ .../plugins/tfs/util/UriHelperTest.java | 103 ++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/src/main/java/hudson/plugins/tfs/util/UriHelper.java b/src/main/java/hudson/plugins/tfs/util/UriHelper.java index 394348077..0944d5060 100644 --- a/src/main/java/hudson/plugins/tfs/util/UriHelper.java +++ b/src/main/java/hudson/plugins/tfs/util/UriHelper.java @@ -7,12 +7,105 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; +import java.util.Collections; import java.util.Iterator; import java.util.Map; +import java.util.TreeMap; public class UriHelper { + + private static final Map SCHEMES_TO_DEFAULT_PORTS; public static final String UTF_8 = "UTF-8"; + static { + final Map defaultPorts = + new TreeMap(String.CASE_INSENSITIVE_ORDER); + defaultPorts.put("ftp", 21); + defaultPorts.put("ssh", 22); + defaultPorts.put("http", 80); + defaultPorts.put("https", 443); + SCHEMES_TO_DEFAULT_PORTS = Collections.unmodifiableMap(defaultPorts); + } + + /** + * Compares two {@link URI} instances to determine if they are equivalent. + * For example, + * {@code HTTP://WWW.EXAMPLE.COM:80/} + * and + * {@code http://www.example.com} + * are considered equivalent. + * This method handles a few more cases than {@link URI#equals(Object)}, such that the scheme's + * default port number will be considered, as will the default path for hosts. + * + * @param a the first URI + * @param b the second URI + * @return {@code true} if a and b represent the same resource; {@code false} otherwise. + */ + public static boolean areSame(final URI a, final URI b) { + if (a == null) { + return b == null; + } + if (b == null) { + return false; + } + + if (!StringHelper.equalIgnoringCase(a.getScheme(), b.getScheme())) { + return false; + } + + if (!StringHelper.equalIgnoringCase(a.getHost(), b.getHost())) { + return false; + } + + final int aPort = normalizePort(a); + final int bPort = normalizePort(b); + if (aPort != bPort) { + return false; + } + + final String aPath = normalizePath(a); + final String bPath = normalizePath(b); + if (!StringHelper.equal(aPath, bPath)) { + return false; + } + + if (!StringHelper.equal(a.getQuery(), b.getQuery())) { + return false; + } + + if (!StringHelper.equal(a.getFragment(), b.getFragment())) { + return false; + } + + return true; + } + + static int normalizePort(final URI uri) { + int port = uri.getPort(); + if (port == -1) { + final String scheme = uri.getScheme(); + if (scheme != null) { + if (SCHEMES_TO_DEFAULT_PORTS.containsKey(scheme)) { + port = SCHEMES_TO_DEFAULT_PORTS.get(scheme); + } + } + } + return port; + } + + static String normalizePath(final URI uri) { + String path = uri.getPath(); + if (path == null) { + path = "/"; + } + else { + if (!path.endsWith("/")) { + path = path + "/"; + } + } + return path; + } + public static boolean isWellFormedUriString(final String uriString) { try { new URI(uriString); diff --git a/src/test/java/hudson/plugins/tfs/util/UriHelperTest.java b/src/test/java/hudson/plugins/tfs/util/UriHelperTest.java index 7399b6d30..fa7a93afa 100644 --- a/src/test/java/hudson/plugins/tfs/util/UriHelperTest.java +++ b/src/test/java/hudson/plugins/tfs/util/UriHelperTest.java @@ -18,6 +18,109 @@ public class UriHelperTest { lifeUniverseEverything.put("answer", "42"); } + private static void assertSame(final String a, final String b) { + areSame(a, b, true); + } + + private static void assertNotSame(final String a, final String b) { + areSame(a, b, false); + } + + private static void areSame(final String a, final String b, final boolean expected) { + final URI uriA = a == null ? null : URI.create(a); + final URI uriB = b == null ? null : URI.create(b); + final String template = "Expected '%s' and '%s' to be considered%s the same."; + final String message = String.format(template, a, b, expected ? "" : " NOT"); + Assert.assertEquals(message, expected, UriHelper.areSame(uriA, uriB)); + Assert.assertEquals(message, expected, UriHelper.areSame(uriB, uriA)); + } + + + @Test public void areSame_bothNull() throws Exception { + assertSame(null, null); + } + + @Test public void areSame_sameInstance() throws Exception { + final URI uri = URI.create("http://one.example.com"); + Assert.assertTrue(UriHelper.areSame(uri, uri)); + } + + @Test public void areSame_identity() throws Exception { + assertSame("http://one.example.com", "http://one.example.com"); + } + + @Test public void areSame_endsWithSlash() throws Exception { + assertSame("http://one.example.com/", "http://one.example.com"); + } + + @Test public void areSame_schemeCase() throws Exception { + assertSame("http://one.example.com", "HTTP://one.example.com"); + } + + @Test public void areSame_hostCase() throws Exception { + assertSame("http://ONE.example.com", "http://one.example.com"); + } + + @Test public void areSame_implicitPort() throws Exception { + assertSame("http://one.example.com", "http://one.example.com:80"); + } + + @Test public void areSame_withPathSlash() throws Exception { + assertSame("http://one.example.com/path/", "http://one.example.com/path/"); + } + + @Test public void areSame_withPathWithoutSlash() throws Exception { + assertSame("http://one.example.com/path/", "http://one.example.com/path"); + } + + @Test public void areSame_withPathQuery() throws Exception { + assertSame("http://one.example.com/search?q=example", "http://one.example.com/search?q=example"); + } + + @Test public void areSame_withPathQueryFragment() throws Exception { + assertSame("http://one.example.com/search?q=example#top", "http://one.example.com/search?q=example#top"); + } + + + @Test public void areSame_oneNull() throws Exception { + + assertNotSame("http://one.example.com/path/", null); + } + + @Test public void areSame_differentScheme() throws Exception { + + assertNotSame("http://one.example.com/path/", "https://one.example.com/path/"); + } + + @Test public void areSame_differentHost() throws Exception { + + assertNotSame("http://one.example.com/path/", "http://two.example.com/path/"); + } + + @Test public void areSame_differentPort() throws Exception { + + assertNotSame("http://one.example.com/path/", "http://one.example.com:8080/path/"); + } + + @Test public void areSame_differentPath() throws Exception { + + assertNotSame("http://one.example.com/path/", "http://one.example.com/"); + } + + @Test public void areSame_differentQuery() throws Exception { + + assertNotSame("http://one.example.com/path?q=example", "http://one.example.com/path?q=different"); + } + + @Test public void areSame_differentFragment() throws Exception { + + assertNotSame("http://one.example.com/path#top", "http://one.example.com/path#bottom"); + } + + @Test public void areSame_differentFragmentAfterQuery() throws Exception { + + assertNotSame("http://one.example.com/path?q=example#top", "http://one.example.com/path?q=example#bottom"); + } @Test public void join_uriNoSlash_pathComponents() throws Exception { final URI collectionUri = URI.create("https://fabrikam-fiber-inc.visualstudio.com"); From 2cb5bd01a10692049c91adf0bad9cd7515dabeaa Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Wed, 20 Jul 2016 15:45:58 -0400 Subject: [PATCH 04/11] Find configured credentials for a collection URI --- .../tfs/VstsCollectionConfiguration.java | 35 +++++++++++++++++++ .../plugins/tfs/VstsPluginGlobalConfig.java | 9 +++++ 2 files changed, 44 insertions(+) diff --git a/src/main/java/hudson/plugins/tfs/VstsCollectionConfiguration.java b/src/main/java/hudson/plugins/tfs/VstsCollectionConfiguration.java index 3e6cc8af1..3cb5e700d 100644 --- a/src/main/java/hudson/plugins/tfs/VstsCollectionConfiguration.java +++ b/src/main/java/hudson/plugins/tfs/VstsCollectionConfiguration.java @@ -5,6 +5,7 @@ import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.cloudbees.plugins.credentials.domains.HostnameRequirement; import hudson.Extension; import hudson.model.AbstractDescribableImpl; @@ -27,6 +28,7 @@ import java.net.URI; import java.net.URL; import java.nio.charset.Charset; +import java.util.Collections; import java.util.List; public class VstsCollectionConfiguration extends AbstractDescribableImpl { @@ -160,4 +162,37 @@ static List findCredentials(final String ho ); return matches; } + + static StandardUsernamePasswordCredentials findCredentialsById(final String credentialsId) { + final Jenkins jenkins = Jenkins.getInstance(); + final List matches = + CredentialsProvider.lookupCredentials( + StandardUsernamePasswordCredentials.class, + jenkins, + ACL.SYSTEM, + Collections.emptyList() + ); + final CredentialsMatcher matcher = CredentialsMatchers.withId(credentialsId); + final StandardUsernamePasswordCredentials result = CredentialsMatchers.firstOrNull(matches, matcher); + return result; + } + + // TODO: we'll probably also want findCredentialsForGitRepo, where we match part of the URL path + public static StandardUsernamePasswordCredentials findCredentialsForCollection(final URI collectionUri) { + final VstsPluginGlobalConfig config = VstsPluginGlobalConfig.get(); + // TODO: consider using a different data structure to speed up this look-up + final List pairs = config.getCollectionConfigurations(); + for (final VstsCollectionConfiguration pair : pairs) { + final String candidateCollectionUrlString = pair.getCollectionUrl(); + final URI candidateCollectionUri = URI.create(candidateCollectionUrlString); + if (UriHelper.areSame(candidateCollectionUri, collectionUri)) { + final String credentialsId = pair.credentialsId; + if (credentialsId != null) { + return findCredentialsById(credentialsId); + } + return null; + } + } + return null; + } } diff --git a/src/main/java/hudson/plugins/tfs/VstsPluginGlobalConfig.java b/src/main/java/hudson/plugins/tfs/VstsPluginGlobalConfig.java index 3ac144899..ed85855b1 100644 --- a/src/main/java/hudson/plugins/tfs/VstsPluginGlobalConfig.java +++ b/src/main/java/hudson/plugins/tfs/VstsPluginGlobalConfig.java @@ -1,8 +1,10 @@ package hudson.plugins.tfs; import hudson.Extension; +import hudson.ExtensionList; import jenkins.model.GlobalConfiguration; import net.sf.json.JSONObject; +import org.apache.commons.lang3.ObjectUtils; import org.kohsuke.stapler.StaplerRequest; import java.util.ArrayList; @@ -17,6 +19,7 @@ public class VstsPluginGlobalConfig extends GlobalConfiguration { private static final Logger LOGGER = Logger.getLogger(VstsPluginGlobalConfig.class.getName()); + private static final VstsPluginGlobalConfig DEFAULT_CONFIG = new VstsPluginGlobalConfig(); private List collectionConfigurations = new ArrayList(); @@ -29,6 +32,12 @@ public VstsPluginGlobalConfig(final List collection this.collectionConfigurations = collectionConfigurations; } + public static VstsPluginGlobalConfig get() { + final ExtensionList configurationExtensions = all(); + final VstsPluginGlobalConfig config = configurationExtensions.get(VstsPluginGlobalConfig.class); + final VstsPluginGlobalConfig result = ObjectUtils.defaultIfNull(config, DEFAULT_CONFIG); + return result; + } public List getCollectionConfigurations() { return collectionConfigurations; From 54169e05ffdfbc958b14d3ab768516da1a151f5c Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 17:28:08 -0400 Subject: [PATCH 05/11] Add GitStatusStateMorpher for case-insensitivity It seems the value of "state" can vary in case. --- .../plugins/tfs/model/GitStatusState.java | 24 ++++++++++++ .../tfs/model/GitStatusStateMorpher.java | 37 +++++++++++++++++++ .../plugins/tfs/model/VstsGitStatusTest.java | 31 ++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 src/main/java/hudson/plugins/tfs/model/GitStatusStateMorpher.java diff --git a/src/main/java/hudson/plugins/tfs/model/GitStatusState.java b/src/main/java/hudson/plugins/tfs/model/GitStatusState.java index 3ae523bf9..96cf8cf98 100644 --- a/src/main/java/hudson/plugins/tfs/model/GitStatusState.java +++ b/src/main/java/hudson/plugins/tfs/model/GitStatusState.java @@ -1,5 +1,9 @@ package hudson.plugins.tfs.model; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + public enum GitStatusState { NotSet(0), @@ -9,6 +13,26 @@ public enum GitStatusState { Error(4), ; + public static final Map CASE_INSENSITIVE_LOOKUP; + + static { + final Map map = new TreeMap(String.CASE_INSENSITIVE_ORDER); + for (final GitStatusState value : GitStatusState.values()) { + map.put(value.name(), value); + } + CASE_INSENSITIVE_LOOKUP = Collections.unmodifiableMap(map); + } + + public static GitStatusState caseInsensitiveValueOf(final String name) { + if (name == null) { + throw new NullPointerException("Name is null"); + } + if (!CASE_INSENSITIVE_LOOKUP.containsKey(name)) { + throw new IllegalArgumentException("No enum constant " + name); + } + return CASE_INSENSITIVE_LOOKUP.get(name); + } + private final int value; GitStatusState(final int value) { diff --git a/src/main/java/hudson/plugins/tfs/model/GitStatusStateMorpher.java b/src/main/java/hudson/plugins/tfs/model/GitStatusStateMorpher.java new file mode 100644 index 000000000..d754f216c --- /dev/null +++ b/src/main/java/hudson/plugins/tfs/model/GitStatusStateMorpher.java @@ -0,0 +1,37 @@ +package hudson.plugins.tfs.model; + +import net.sf.ezmorph.MorphException; +import net.sf.ezmorph.ObjectMorpher; + +public class GitStatusStateMorpher implements ObjectMorpher { + + public static final GitStatusStateMorpher INSTANCE = new GitStatusStateMorpher(); + + private GitStatusStateMorpher() { + + } + + @Override + public Object morph(final Object value) { + if (value == null) { + return null; + } + + if (!supports(value.getClass())) { + throw new MorphException(value.getClass() + " is not supported"); + } + + final String s = value.toString(); + return GitStatusState.caseInsensitiveValueOf(s); + } + + @Override + public Class morphsTo() { + return GitStatusState.class; + } + + @Override + public boolean supports(Class clazz) { + return String.class.isAssignableFrom(clazz); + } +} diff --git a/src/test/java/hudson/plugins/tfs/model/VstsGitStatusTest.java b/src/test/java/hudson/plugins/tfs/model/VstsGitStatusTest.java index 72f697938..b03605ac0 100644 --- a/src/test/java/hudson/plugins/tfs/model/VstsGitStatusTest.java +++ b/src/test/java/hudson/plugins/tfs/model/VstsGitStatusTest.java @@ -1,5 +1,9 @@ package hudson.plugins.tfs.model; +import net.sf.ezmorph.MorpherRegistry; +import net.sf.json.JSONObject; +import net.sf.json.util.JSONTokener; +import net.sf.json.util.JSONUtils; import org.junit.Assert; import org.junit.Test; @@ -30,4 +34,31 @@ public class VstsGitStatusTest { "}"; Assert.assertEquals(expected, actual); } + + @Test public void fromJsonString_vsts() throws Exception { + final String input = + "{" + + "\"state\":\"succeeded\"," + + "\"description\":\"SUCCESS\"," + + "\"targetUrl\":\"https://ci.fabrikam.com/my-project/build/124\"," + + "\"context\":" + + "{" + + "\"name\":\"Build124\"," + + "\"genre\":\"continuous-integration\"" + + "}" + + "}"; + + final JSONTokener tokener = new JSONTokener(input); + final JSONObject jsonObject = JSONObject.fromObject(tokener); + final MorpherRegistry registry = JSONUtils.getMorpherRegistry(); + registry.registerMorpher(GitStatusStateMorpher.INSTANCE); + final VstsGitStatus actual; + try { + actual = (VstsGitStatus) jsonObject.toBean(VstsGitStatus.class); + } + finally { + registry.deregisterMorpher(GitStatusStateMorpher.INSTANCE); + } + Assert.assertEquals(GitStatusState.Succeeded, actual.state); + } } From 338b1391257a427a3096d9fa82c6331407dd7c66 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 17:30:37 -0400 Subject: [PATCH 06/11] Build a VstsGitStatus from a Run --- .../plugins/tfs/model/VstsGitStatus.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java b/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java index ab844c45e..ea023937d 100644 --- a/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java +++ b/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java @@ -1,14 +1,47 @@ package hudson.plugins.tfs.model; +import hudson.model.Job; +import hudson.model.Result; +import hudson.model.Run; import net.sf.json.JSONObject; +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + public class VstsGitStatus { + private static final Map RESULT_TO_STATE; + + static { + final Map resultToStatus = new HashMap(); + resultToStatus.put(Result.SUCCESS, GitStatusState.Succeeded); + resultToStatus.put(Result.UNSTABLE, GitStatusState.Failed); + resultToStatus.put(Result.FAILURE, GitStatusState.Failed); + resultToStatus.put(Result.NOT_BUILT, GitStatusState.Error); + resultToStatus.put(Result.ABORTED, GitStatusState.Error); + RESULT_TO_STATE = Collections.unmodifiableMap(resultToStatus); + } + public GitStatusState state; public String description; public String targetUrl; public GitStatusContext context; + public static VstsGitStatus fromRun(@Nonnull final Run run) { + final VstsGitStatus status = new VstsGitStatus(); + final Result result = run.getResult(); + status.state = RESULT_TO_STATE.get(result); + status.description = result.toString(); + status.targetUrl = run.getAbsoluteUrl(); + final Job project = run.getParent(); + final String runDisplayName = run.getDisplayName(); + final String projectDisplayName = project.getDisplayName(); + status.context = new GitStatusContext(runDisplayName, projectDisplayName); + return status; + } + public String toJson() { final JSONObject jsonObject = JSONObject.fromObject(this); final String result = jsonObject.toString(); From 11ccbf12bbdd8933d82b7ccefb6e373551e24d0f Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 17:31:52 -0400 Subject: [PATCH 07/11] Add VstsRestClient#addCommitStatus() --- .../plugins/tfs/util/VstsRestClient.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/tfs/util/VstsRestClient.java b/src/main/java/hudson/plugins/tfs/util/VstsRestClient.java index 19729ffa2..4e1cdcd76 100644 --- a/src/main/java/hudson/plugins/tfs/util/VstsRestClient.java +++ b/src/main/java/hudson/plugins/tfs/util/VstsRestClient.java @@ -1,10 +1,15 @@ package hudson.plugins.tfs.util; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.plugins.tfs.model.GitCodePushedEventArgs; +import hudson.plugins.tfs.model.GitStatusStateMorpher; import hudson.plugins.tfs.model.HttpMethod; +import hudson.plugins.tfs.model.VstsGitStatus; import hudson.util.Secret; +import net.sf.ezmorph.MorpherRegistry; import net.sf.json.JSONObject; import net.sf.json.util.JSONTokener; +import net.sf.json.util.JSONUtils; import org.apache.commons.io.IOUtils; import javax.xml.bind.DatatypeConverter; @@ -16,11 +21,11 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.nio.charset.Charset; public class VstsRestClient { private static final String AUTHORIZATION = "Authorization"; + private static final String API_VERSION = "api-version"; private static final String NEW_LINE = System.getProperty("line.separator"); private final URI collectionUri; @@ -156,4 +161,24 @@ public String ping() throws IOException { return request(String.class, HttpMethod.GET, requestUri, null); } + public VstsGitStatus addCommitStatus(final GitCodePushedEventArgs args, final VstsGitStatus status) throws IOException { + + final QueryString qs = new QueryString(API_VERSION, "2.1"); + final URI requestUri = UriHelper.join( + collectionUri, args.projectId, + "_apis", "git", + "repositories", args.repoId, + "commits", args.commit, + "statuses", + qs); + + final MorpherRegistry registry = JSONUtils.getMorpherRegistry(); + registry.registerMorpher(GitStatusStateMorpher.INSTANCE); + try { + return request(VstsGitStatus.class, HttpMethod.POST, requestUri, status); + } + finally { + registry.deregisterMorpher(GitStatusStateMorpher.INSTANCE); + } + } } From 6452fa876bae1ec7193cdf5dad610ecff5535653 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 17:32:47 -0400 Subject: [PATCH 08/11] Add rest of Completed Status post-build action --- .../VstsCompletedStatusPostBuildAction.java | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java b/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java index 092010233..0303bf36a 100644 --- a/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java +++ b/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java @@ -1,11 +1,17 @@ package hudson.plugins.tfs; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractProject; +import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; +import hudson.plugins.tfs.model.GitCodePushedEventArgs; +import hudson.plugins.tfs.model.GitStatusState; +import hudson.plugins.tfs.model.VstsGitStatus; +import hudson.plugins.tfs.util.VstsRestClient; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; @@ -15,6 +21,10 @@ import javax.annotation.Nonnull; import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * A _Post-Build Action_ that reports the completion status of an associated build to VSTS. @@ -33,13 +43,38 @@ public void perform( @Nonnull final Launcher launcher, @Nonnull final TaskListener listener ) throws InterruptedException, IOException { - // TODO: implement + try { + // TODO: also add support for a build triggered from a pull request + final CommitParameterAction commitParameter = run.getAction(CommitParameterAction.class); + final GitCodePushedEventArgs args; + if (commitParameter != null) { + args = commitParameter.getGitCodePushedEventArgs(); + } + else { + // TODO: try to guess based on what we _do_ have (i.e. RevisionParameterAction) + return; + } + + final URI collectionUri = args.collectionUri; + final StandardUsernamePasswordCredentials credentials = + VstsCollectionConfiguration.findCredentialsForCollection(collectionUri); + final VstsRestClient client = new VstsRestClient(collectionUri, credentials); + + final VstsGitStatus status = VstsGitStatus.fromRun(run); + // TODO: when code is pushed and polling happens, are we sure we built against the requested commit? + client.addCommitStatus(args, status); + + // TODO: we could contribute an Action to the run, recording the ID of the status we created + } + catch (final Exception e) { + e.printStackTrace(listener.error("Error while trying to update completion status in VSTS")); + } } @Override public BuildStepMonitor getRequiredMonitorService() { - // TODO: implement - return null; + // we don't need the outcome of any previous builds for this step + return BuildStepMonitor.NONE; } @Extension From 87e4e343e35ac055ec0e71b89d864e832b503e83 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 21:24:53 -0400 Subject: [PATCH 09/11] Add VstsPendingStatusBuildStep skeleton --- .../tfs/VstsPendingStatusBuildStep.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java diff --git a/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java b/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java new file mode 100644 index 000000000..f2eb870d7 --- /dev/null +++ b/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java @@ -0,0 +1,50 @@ +package hudson.plugins.tfs; + +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.Builder; +import jenkins.tasks.SimpleBuildStep; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * A _Build Step_ that reports the status of an associated build as "Pending" to VSTS. + */ +public class VstsPendingStatusBuildStep extends Builder implements SimpleBuildStep { + + @DataBoundConstructor + public VstsPendingStatusBuildStep() { + + } + + @Override + public void perform( + @Nonnull final Run run, + @Nonnull final FilePath workspace, + @Nonnull final Launcher launcher, + @Nonnull final TaskListener listener + ) throws InterruptedException, IOException { + // TODO: implement + } + + @Extension + public static class DescriptorImpl extends BuildStepDescriptor { + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + @Override + public String getDisplayName() { + return "Set pending status for VSTS commit or pull request"; + } + } +} From 2c8c1253f6454add4470512eecde5146708d3e87 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 21:36:20 -0400 Subject: [PATCH 10/11] Extract VstsStatus#createFromRun() for re-use --- .../VstsCompletedStatusPostBuildAction.java | 33 +--------------- .../hudson/plugins/tfs/util/VstsStatus.java | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 src/main/java/hudson/plugins/tfs/util/VstsStatus.java diff --git a/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java b/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java index 0303bf36a..49f662c0d 100644 --- a/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java +++ b/src/main/java/hudson/plugins/tfs/VstsCompletedStatusPostBuildAction.java @@ -1,17 +1,12 @@ package hudson.plugins.tfs; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractProject; -import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; -import hudson.plugins.tfs.model.GitCodePushedEventArgs; -import hudson.plugins.tfs.model.GitStatusState; -import hudson.plugins.tfs.model.VstsGitStatus; -import hudson.plugins.tfs.util.VstsRestClient; +import hudson.plugins.tfs.util.VstsStatus; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; @@ -21,10 +16,6 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; /** * A _Post-Build Action_ that reports the completion status of an associated build to VSTS. @@ -44,27 +35,7 @@ public void perform( @Nonnull final TaskListener listener ) throws InterruptedException, IOException { try { - // TODO: also add support for a build triggered from a pull request - final CommitParameterAction commitParameter = run.getAction(CommitParameterAction.class); - final GitCodePushedEventArgs args; - if (commitParameter != null) { - args = commitParameter.getGitCodePushedEventArgs(); - } - else { - // TODO: try to guess based on what we _do_ have (i.e. RevisionParameterAction) - return; - } - - final URI collectionUri = args.collectionUri; - final StandardUsernamePasswordCredentials credentials = - VstsCollectionConfiguration.findCredentialsForCollection(collectionUri); - final VstsRestClient client = new VstsRestClient(collectionUri, credentials); - - final VstsGitStatus status = VstsGitStatus.fromRun(run); - // TODO: when code is pushed and polling happens, are we sure we built against the requested commit? - client.addCommitStatus(args, status); - - // TODO: we could contribute an Action to the run, recording the ID of the status we created + VstsStatus.createFromRun(run); } catch (final Exception e) { e.printStackTrace(listener.error("Error while trying to update completion status in VSTS")); diff --git a/src/main/java/hudson/plugins/tfs/util/VstsStatus.java b/src/main/java/hudson/plugins/tfs/util/VstsStatus.java new file mode 100644 index 000000000..d05fe6871 --- /dev/null +++ b/src/main/java/hudson/plugins/tfs/util/VstsStatus.java @@ -0,0 +1,38 @@ +package hudson.plugins.tfs.util; + +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.model.Run; +import hudson.plugins.tfs.CommitParameterAction; +import hudson.plugins.tfs.VstsCollectionConfiguration; +import hudson.plugins.tfs.model.GitCodePushedEventArgs; +import hudson.plugins.tfs.model.VstsGitStatus; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.net.URI; + +public class VstsStatus { + public static void createFromRun(@Nonnull Run run) throws IOException { + // TODO: also add support for a build triggered from a pull request + final CommitParameterAction commitParameter = run.getAction(CommitParameterAction.class); + final GitCodePushedEventArgs args; + if (commitParameter != null) { + args = commitParameter.getGitCodePushedEventArgs(); + } + else { + // TODO: try to guess based on what we _do_ have (i.e. RevisionParameterAction) + return; + } + + final URI collectionUri = args.collectionUri; + final StandardUsernamePasswordCredentials credentials = + VstsCollectionConfiguration.findCredentialsForCollection(collectionUri); + final VstsRestClient client = new VstsRestClient(collectionUri, credentials); + + final VstsGitStatus status = VstsGitStatus.fromRun(run); + // TODO: when code is pushed and polling happens, are we sure we built against the requested commit? + client.addCommitStatus(args, status); + + // TODO: we could contribute an Action to the run, recording the ID of the status we created + } +} From b3c7fdfec28163d82fe986a9ed94d5361e3554b7 Mon Sep 17 00:00:00 2001 From: Oli Dagenais Date: Fri, 22 Jul 2016 22:03:14 -0400 Subject: [PATCH 11/11] Implement rest of pending status build step --- .../hudson/plugins/tfs/VstsPendingStatusBuildStep.java | 8 +++++++- .../java/hudson/plugins/tfs/model/VstsGitStatus.java | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java b/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java index f2eb870d7..612fc8532 100644 --- a/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java +++ b/src/main/java/hudson/plugins/tfs/VstsPendingStatusBuildStep.java @@ -6,6 +6,7 @@ import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.TaskListener; +import hudson.plugins.tfs.util.VstsStatus; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import jenkins.tasks.SimpleBuildStep; @@ -31,7 +32,12 @@ public void perform( @Nonnull final Launcher launcher, @Nonnull final TaskListener listener ) throws InterruptedException, IOException { - // TODO: implement + try { + VstsStatus.createFromRun(run); + } + catch (final Exception e) { + e.printStackTrace(listener.error("Error while trying to update pending status in VSTS")); + } } @Extension diff --git a/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java b/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java index ea023937d..0ae726d94 100644 --- a/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java +++ b/src/main/java/hudson/plugins/tfs/model/VstsGitStatus.java @@ -32,8 +32,14 @@ public class VstsGitStatus { public static VstsGitStatus fromRun(@Nonnull final Run run) { final VstsGitStatus status = new VstsGitStatus(); final Result result = run.getResult(); - status.state = RESULT_TO_STATE.get(result); - status.description = result.toString(); + if (result == null) { + status.state = GitStatusState.Pending; + status.description = status.state.toString(); + } + else { + status.state = RESULT_TO_STATE.get(result); + status.description = result.toString(); + } status.targetUrl = run.getAbsoluteUrl(); final Job project = run.getParent(); final String runDisplayName = run.getDisplayName();