Skip to content

Add support for Deveo. #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 9.1.x
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package jetbrains.buildServer.commitPublisher.deveo;

public enum DeveoBuildStatus {
SUCCESSFUL, FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
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<String, String> 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;
}

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<String, JsonElement> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> getDefaultParameters() {
return null;
}

@Nullable
public CommitStatusPublisher createPublisher(@NotNull Map<String, String> params) {
return new DeveoPublisher(myLinks, params);
}

@NotNull
public String describeParameters(@NotNull Map<String, String> params) {
return "Deveo";
}

@Nullable
public PropertiesProcessor getParametersProcessor() {
return new PropertiesProcessor() {
public Collection<InvalidProperty> process(Map<String, String> params) {
List<InvalidProperty> errors = new ArrayList<InvalidProperty>();

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;
}
}
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

<bean class="jetbrains.buildServer.commitPublisher.stash.StashSettings"/>
<bean class="jetbrains.buildServer.commitPublisher.bitbucketCloud.BitbucketCloudSettings"/>
<bean class="jetbrains.buildServer.commitPublisher.deveo.DeveoSettings"/>
<bean class="jetbrains.buildServer.commitPublisher.gerrit.GerritSettings"/>

<!-- github -->
Original file line number Diff line number Diff line change
@@ -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" %>
<jsp:useBean id="keys" class="jetbrains.buildServer.commitPublisher.Constants"/>
<table style="width: 100%">

<tr>
<th><label for="${keys.deveoApiHostname}">Deveo API hostname: <l:star/></label></th>
<td>
<props:textProperty name="${keys.deveoApiHostname}" style="width:18em;"/>
<span class="error" id="error_${keys.deveoApiHostname}"></span>
</td>
</tr>

<tr>
<th><label for="${keys.deveoCompanyKey}">Deveo Company Key: <l:star/></label></th>
<td>
<props:textProperty name="${keys.deveoCompanyKey}" style="width:18em;"/>
<span class="error" id="error_${keys.deveoCompanyKey}"></span>
</td>
</tr>

<tr>
<th><label for="${keys.deveoAccountKey}">Deveo Account Key: <l:star/></label></th>
<td>
<props:textProperty name="${keys.deveoAccountKey}" style="width:18em;"/>
<span class="error" id="error_${keys.deveoAccountKey}"></span>
</td>
</tr>

<tr>
<th><label for="${keys.deveoPluginKey}">Deveo Plugin Key: <l:star/></label></th>
<td>
<props:textProperty name="${keys.deveoPluginKey}" style="width:18em;"/>
<span class="error" id="error_${keys.deveoPluginKey}"></span>
</td>
</tr>

</table>