From 2aaf0beb4480b9394ef0312a4ae3c85eee82db7b Mon Sep 17 00:00:00 2001 From: Ilmari Kontulainen Date: Sun, 17 Apr 2016 19:23:13 +0300 Subject: [PATCH 1/2] Add support for Deveo. --- .../commitPublisher/Constants.java | 26 ++ .../deveo/DeveoBuildStatus.java | 5 + .../commitPublisher/deveo/DeveoPublisher.java | 280 ++++++++++++++++++ .../deveo/DeveoRepositoryParser.java | 44 +++ .../commitPublisher/deveo/DeveoSettings.java | 83 ++++++ .../META-INF/build-server-plugin-voter.xml | 1 + .../deveo/deveoSettings.jsp | 43 +++ 7 files changed, 482 insertions(+) create mode 100644 commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoBuildStatus.java create mode 100644 commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java create mode 100644 commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoRepositoryParser.java create mode 100644 commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoSettings.java create mode 100644 commit-status-publisher-server/src/main/resources/buildServerResources/deveo/deveoSettings.jsp diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java index 30d19b8a..49c7ba25 100644 --- a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/Constants.java @@ -43,6 +43,12 @@ public class Constants { public static final String BITBUCKET_CLOUD_USERNAME = "bitbucketUsername"; public static final String BITBUCKET_CLOUD_PASSWORD = "secure:bitbucketPassword"; + public static final String DEVEO_PUBLISHER_ID = "deveoPublisher"; + public static final String DEVEO_PLUGIN_KEY = "deveoPluginKey"; + public static final String DEVEO_COMPANY_KEY = "deveoCompanyKey"; + public static final String DEVEO_API_HOSTNAME = "deveoApiHostname"; + public static final String DEVEO_ACCOUNT_KEY = "deveoAccountKey"; + @NotNull public String getVcsRootIdParam() { return VCS_ROOT_ID_PARAM; @@ -127,4 +133,24 @@ public String getBitbucketCloudUsername() { public String getBitbucketCloudPassword() { return BITBUCKET_CLOUD_PASSWORD; } + + @NotNull + public String getDeveoPluginKey() { + return DEVEO_PLUGIN_KEY; + } + + @NotNull + public String getDeveoCompanyKey() { + return DEVEO_COMPANY_KEY; + } + + @NotNull + public String getDeveoApiHostname() { + return DEVEO_API_HOSTNAME; + } + + @NotNull + public String getDeveoAccountKey() { + return DEVEO_ACCOUNT_KEY; + } } diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoBuildStatus.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoBuildStatus.java new file mode 100644 index 00000000..77f45b37 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoBuildStatus.java @@ -0,0 +1,5 @@ +package jetbrains.buildServer.commitPublisher.deveo; + +public enum DeveoBuildStatus { + SUCCESSFUL, FAILED +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java new file mode 100644 index 00000000..113458e1 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java @@ -0,0 +1,280 @@ +package jetbrains.buildServer.commitPublisher.deveo; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.intellij.openapi.diagnostic.Logger; +import jetbrains.buildServer.commitPublisher.BaseCommitStatusPublisher; +import jetbrains.buildServer.commitPublisher.Constants; +import jetbrains.buildServer.commitPublisher.PublishError; +import jetbrains.buildServer.commitPublisher.Repository; +import jetbrains.buildServer.serverSide.*; +import jetbrains.buildServer.users.User; +import jetbrains.buildServer.vcs.VcsRootInstance; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.Map; + +public class DeveoPublisher extends BaseCommitStatusPublisher { + private static final Logger LOG = Logger.getInstance(DeveoPublisher.class.getName()); + + private final WebLinks myLinks; + + public DeveoPublisher(@NotNull WebLinks links, + @NotNull Map params) { + super(params); + myLinks = links; + } + + @NotNull + public String toString() { + return "deveo"; + } + + @Override + public String getId() { + return Constants.DEVEO_PUBLISHER_ID; + } + + public boolean buildStarted(@NotNull SRunningBuild build, @NotNull BuildRevision revision) { + return true; + } + + @Override + public boolean buildFinished(@NotNull SFinishedBuild build, @NotNull BuildRevision revision) { + DeveoBuildStatus status = build.getBuildStatus().isSuccessful() ? DeveoBuildStatus.SUCCESSFUL : DeveoBuildStatus.FAILED; + vote(build, revision, status); + return true; + } + + @Override + public boolean buildCommented(@NotNull SBuild build, @NotNull BuildRevision revision, @Nullable User user, @Nullable String comment, boolean buildInProgress) { + return true; + } + + @Override + public boolean buildMarkedAsSuccessful(@NotNull SBuild build, @NotNull BuildRevision revision) { + vote(build, revision, DeveoBuildStatus.SUCCESSFUL); + return true; + } + + @Override + public boolean buildInterrupted(@NotNull SFinishedBuild build, @NotNull BuildRevision revision) { + vote(build, revision, DeveoBuildStatus.FAILED); + return true; + } + + @Override + public boolean buildFailureDetected(@NotNull SRunningBuild build, @NotNull BuildRevision revision) { + vote(build, revision, DeveoBuildStatus.FAILED); + return true; + } + + private void vote(@NotNull SBuild build, + @NotNull BuildRevision revision, + @NotNull DeveoBuildStatus status) { + try { + final VcsRootInstance root = revision.getRoot(); + Repository repository = DeveoRepositoryParser.parseRepository(root); + if (repository == null) { + throw new PublishError("Cannot parse repository from VCS root url " + root.getName()); + } + String msg = createMessage(status, repository.owner(), repository.repositoryName(), build.getFullName(), revision.getRevision(), myLinks.getViewResultsUrl(build)); + vote(msg); + } catch (Exception e) { + throw new PublishError("Cannot publish status to Deveo for VCS root " + + revision.getRoot().getName() + ": " + getMessage(e), e); + } + } + + @NotNull + private String createMessage(@NotNull DeveoBuildStatus status, + @NotNull String project, + @NotNull String repository, + @NotNull String build, + @NotNull String commit, + @NotNull String url) { + StringBuilder data = new StringBuilder(); + data.append("{ ") + .append("\"target\": ").append("\"build").append("\", ") + .append("\"operation\": ").append("\"").append(getDeveoStatusText(status)).append("\", ") + .append("\"project\": ").append("\"").append(project).append("\", ") + .append("\"repository\": ").append("\"").append(repository).append("\", ") + .append("\"name\": ").append("\"").append(build).append("\", ") + .append("\"commits\": ").append("[\"" + commit + "\"]").append(", ") + .append("\"resources\": ").append("[\"").append(url).append("\"] ") + .append("}"); + LOG.debug("DATA: " + data.toString()); + return data.toString(); + } + + private String getDeveoStatusText(DeveoBuildStatus status) { + if (status == DeveoBuildStatus.SUCCESSFUL) { + return "completed"; + } + return "failed"; + } + + private void vote(@NotNull String data) throws URISyntaxException, IOException, + UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, DeveoException { + + URI deveoURI = new URI(getDeveoEventsApiURL()); + + LOG.debug(getApiHostname() + " :: " + data + " :: " + String.valueOf(deveoURI)); + + HttpClient client = HttpClientBuilder.create().build(); + HttpResponse response = null; + + HttpPost request = getDeveoHttpPostRequest(data, deveoURI); + try { + response = client.execute(request); + StatusLine statusLine = response.getStatusLine(); + LOG.debug("Response Status Code was " + statusLine.getStatusCode()); + if (statusLine.getStatusCode() >= 400) + throw new DeveoException(statusLine.getStatusCode(), statusLine.getReasonPhrase(), parseErrorMessage(response)); + } finally { + HttpClientUtils.closeQuietly(response); + releaseConnection(request); + HttpClientUtils.closeQuietly(client); + } + } + + @NotNull + private HttpPost getDeveoHttpPostRequest(@NotNull String data, URI deveoURI) { + HttpPost request = new HttpPost(deveoURI); + request.setHeader(HttpHeaders.ACCEPT, "application/vnd.deveo.v1"); + request.setHeader(HttpHeaders.AUTHORIZATION, "deveo plugin_key='" + getPluginKey() + "',company_key='" + + getCompanyKey() + "',account_key='" + getAccountKey() + "'"); + request.setHeader(HttpHeaders.CONTENT_TYPE, String.valueOf(ContentType.APPLICATION_JSON)); + request.setEntity(new StringEntity(data, ContentType.APPLICATION_JSON)); + return request; + } + + @Nullable + private String parseErrorMessage(@NotNull HttpResponse response) { + HttpEntity entity = response.getEntity(); + if (entity == null) + return null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + entity.writeTo(out); + String str = out.toString("UTF-8"); + LOG.debug("Deveo response: " + str); + JsonElement json = new JsonParser().parse(str); + if (!json.isJsonObject()) + return null; + JsonObject jsonObj = json.getAsJsonObject(); + JsonElement error = jsonObj.get("error"); + if (error == null || !error.isJsonObject()) + return null; + + final JsonObject errorObj = error.getAsJsonObject(); + JsonElement msg = errorObj.get("message"); + if (msg == null) + return null; + StringBuilder result = new StringBuilder(msg.getAsString()); + JsonElement fields = errorObj.get("fields"); + if (fields != null && fields.isJsonObject()) { + result.append(". "); + JsonObject fieldsObj = fields.getAsJsonObject(); + for (Map.Entry e : fieldsObj.entrySet()) { + result.append("Field '").append(e.getKey()).append("': ").append(e.getValue().getAsString()); + } + } + return result.toString(); + } catch (IOException e) { + return null; + } catch (JsonSyntaxException e) { + return null; + } + } + + private void releaseConnection(@Nullable HttpPost post) { + if (post != null) { + try { + post.releaseConnection(); + } catch (Exception e) { + LOG.warn("Error releasing connection", e); + } + } + } + + private String getCompanyKey() { + return myParams.get(Constants.DEVEO_COMPANY_KEY); + } + + private String getApiHostname() { return myParams.get(Constants.DEVEO_API_HOSTNAME); } + + private String getPluginKey() { return myParams.get(Constants.DEVEO_PLUGIN_KEY); } + + private String getAccountKey() { + return myParams.get(Constants.DEVEO_ACCOUNT_KEY); + } + + private String getDeveoEventsApiURL() { + StringBuilder sb = new StringBuilder(getApiHostname()); + if (!sb.toString().endsWith("/")) { + sb.append("/"); + } + return sb.toString() + "api/events"; + } + + private static class DeveoException extends Exception { + private final int myStatusCode; + private final String myReason; + private final String myMessage; + public DeveoException(int statusCode, String reason, @Nullable String message) { + myStatusCode = statusCode; + myReason = reason; + myMessage = message; + } + + public int getStatusCode() { + return myStatusCode; + } + + public String getReason() { + return myReason; + } + + @Nullable + public String getDeveoMessage() { + return myMessage; + } + } + + @NotNull + private String getMessage(@NotNull Exception e) { + if (e instanceof DeveoException) { + DeveoException se = (DeveoException) e; + String result = "response code: " + se.getStatusCode() + ", reason: " + se.getReason(); + String msg = se.getDeveoMessage(); + if (msg != null) { + result += ", message: '" + msg + "'"; + } + return result; + } else { + return e.toString(); + } + } +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoRepositoryParser.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoRepositoryParser.java new file mode 100644 index 00000000..0dbad92a --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoRepositoryParser.java @@ -0,0 +1,44 @@ +package jetbrains.buildServer.commitPublisher.deveo; + +import com.intellij.openapi.diagnostic.Logger; +import jetbrains.buildServer.commitPublisher.GitRepositoryParser; +import jetbrains.buildServer.commitPublisher.Repository; +import jetbrains.buildServer.vcs.VcsRootInstance; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class DeveoRepositoryParser { + private static final Logger LOG = Logger.getInstance(DeveoRepositoryParser.class.getName()); + + public static final Pattern URL_PATTERN = Pattern.compile(".+/projects/([^/]+)/repositories/(?:mercurial|git|subversion)/(.+?)/?$"); + + @Nullable + public static Repository parseRepository(@NotNull VcsRootInstance root) { + String url = null; + if ("jetbrains.git".equals(root.getVcsName()) || "svn".equals(root.getVcsName())) { + url = root.getProperty("url"); + } + if ("mercurial".equals(root.getVcsName())) { + url = root.getProperty("repositoryPath"); + } + return url == null ? null : parseRepository(url, root.getVcsName()); + } + + @Nullable + private static Repository parseRepository(@NotNull String uri, @NotNull String repositoryType) { + Matcher m = URL_PATTERN.matcher(uri); + + if (!m.matches()) { + LOG.warn("Cannot parse " + repositoryType + " repository url " + uri); + return null; + } + String owner = m.group(1); + String repo = m.group(2); + return new Repository(owner, repo); + } +} diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoSettings.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoSettings.java new file mode 100644 index 00000000..a82b9808 --- /dev/null +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoSettings.java @@ -0,0 +1,83 @@ +package jetbrains.buildServer.commitPublisher.deveo; + +import jetbrains.buildServer.commitPublisher.CommitStatusPublisher; +import jetbrains.buildServer.commitPublisher.CommitStatusPublisherSettings; +import jetbrains.buildServer.commitPublisher.Constants; +import jetbrains.buildServer.serverSide.InvalidProperty; +import jetbrains.buildServer.serverSide.PropertiesProcessor; +import jetbrains.buildServer.serverSide.WebLinks; +import jetbrains.buildServer.util.StringUtil; +import jetbrains.buildServer.web.openapi.PluginDescriptor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class DeveoSettings implements CommitStatusPublisherSettings { + + private final PluginDescriptor myDescriptor; + private final WebLinks myLinks; + + public DeveoSettings(@NotNull PluginDescriptor descriptor, + @NotNull WebLinks links) { + myDescriptor = descriptor; + myLinks= links; + } + + @NotNull + public String getId() { + return Constants.DEVEO_PUBLISHER_ID; + } + + @NotNull + public String getName() { + return "Deveo"; + } + + @Nullable + public String getEditSettingsUrl() { + return myDescriptor.getPluginResourcesPath("deveo/deveoSettings.jsp"); + } + + @Nullable + public Map getDefaultParameters() { + return null; + } + + @Nullable + public CommitStatusPublisher createPublisher(@NotNull Map params) { + return new DeveoPublisher(myLinks, params); + } + + @NotNull + public String describeParameters(@NotNull Map params) { + return "Deveo"; + } + + @Nullable + public PropertiesProcessor getParametersProcessor() { + return new PropertiesProcessor() { + public Collection process(Map params) { + List errors = new ArrayList(); + + if (StringUtil.isEmptyOrSpaces(params.get(Constants.DEVEO_PLUGIN_KEY))) + errors.add(new InvalidProperty(Constants.DEVEO_PLUGIN_KEY, "must be specified")); + + if (StringUtil.isEmptyOrSpaces(params.get(Constants.DEVEO_COMPANY_KEY))) + errors.add(new InvalidProperty(Constants.DEVEO_COMPANY_KEY, "must be specified")); + + if (StringUtil.isEmptyOrSpaces(params.get(Constants.DEVEO_API_HOSTNAME))) + errors.add(new InvalidProperty(Constants.DEVEO_API_HOSTNAME, "must be specified")); + + return errors; + } + }; + } + + public boolean isEnabled() { + return true; + } +} diff --git a/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml b/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml index aa0055f4..c71e85cc 100644 --- a/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml +++ b/commit-status-publisher-server/src/main/resources/META-INF/build-server-plugin-voter.xml @@ -10,6 +10,7 @@ + diff --git a/commit-status-publisher-server/src/main/resources/buildServerResources/deveo/deveoSettings.jsp b/commit-status-publisher-server/src/main/resources/buildServerResources/deveo/deveoSettings.jsp new file mode 100644 index 00000000..dcaaf4ba --- /dev/null +++ b/commit-status-publisher-server/src/main/resources/buildServerResources/deveo/deveoSettings.jsp @@ -0,0 +1,43 @@ +<%@ taglib prefix="props" tagdir="/WEB-INF/tags/props" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="l" tagdir="/WEB-INF/tags/layout" %> +<%@ taglib prefix="forms" tagdir="/WEB-INF/tags/forms" %> +<%@ taglib prefix="bs" tagdir="/WEB-INF/tags" %> +<%@ taglib prefix="util" uri="/WEB-INF/functions/util" %> + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ From 83630507b0983921a483f42be4bc6bbb0640c92b Mon Sep 17 00:00:00 2001 From: Ilmari Kontulainen Date: Thu, 17 Nov 2016 09:59:11 +0200 Subject: [PATCH 2/2] Notify about build status only after build has finished or interrupted. --- .../buildServer/commitPublisher/deveo/DeveoPublisher.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java index 113458e1..8fbca807 100644 --- a/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java +++ b/commit-status-publisher-server/src/main/java/jetbrains/buildServer/commitPublisher/deveo/DeveoPublisher.java @@ -84,12 +84,6 @@ public boolean buildInterrupted(@NotNull SFinishedBuild build, @NotNull BuildRev return true; } - @Override - public boolean buildFailureDetected(@NotNull SRunningBuild build, @NotNull BuildRevision revision) { - vote(build, revision, DeveoBuildStatus.FAILED); - return true; - } - private void vote(@NotNull SBuild build, @NotNull BuildRevision revision, @NotNull DeveoBuildStatus status) {