Merge "log the configured storyboard url on failure"
diff --git a/BUCK b/BUCK
index d9f1b97..d3cde83 100644
--- a/BUCK
+++ b/BUCK
@@ -1,3 +1,5 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
gerrit_plugin(
name = 'its-storyboard',
srcs = glob(['src/main/java/**/*.java']),
@@ -55,16 +57,13 @@
name = 'its-storyboard_tests',
srcs = glob(['src/test/java/**/*.java']),
labels = ['its-storyboard'],
- source_under_test = [':its-storyboard__plugin'],
- deps = [
+ deps = GERRIT_PLUGIN_API + GERRIT_TESTS + [
':its-storyboard__plugin',
'//plugins/its-base:its-base_tests-utils',
':its-base_stripped',
- '//gerrit-plugin-api:lib',
'//lib/easymock:easymock',
'//lib:guava',
'//lib/guice:guice',
- '//lib/jgit:jgit',
'//lib:junit',
'//lib/log:api',
'//lib/log:impl_log4j',
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
index b117517..d28013d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardClient.java
@@ -14,101 +14,108 @@
package com.googlesource.gerrit.plugins.its.storyboard;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.net.HttpURLConnection;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+
public class StoryboardClient {
- private static final Logger log = LoggerFactory.getLogger(
- StoryboardClient.class);
+ private static final Logger log =
+ LoggerFactory.getLogger(StoryboardClient.class);
public static final String STORIES_ENDPOINT = "/api/v1/stories";
public static final String SYS_INFO_ENDPOINT = "/api/v1/systeminfo";
+ public static final String TASKS_ENDPOINT = "/api/v1/tasks";
private final String baseUrl;
- private final String username;
private final String password;
- public StoryboardClient(final String baseUrl, final String username,
- String password) {
+ public StoryboardClient(final String baseUrl, String password) {
this.baseUrl = baseUrl;
- this.username = username;
this.password = password;
}
// generic method to get data from a REST endpoint
public String getData(final String url) throws IOException {
- CloseableHttpClient client = HttpClients.createDefault();
- String responseJson = null;
- try {
- HttpGet httpget = new HttpGet(url);
+ HttpGet httpget = new HttpGet(url);
+ try (CloseableHttpClient client = HttpClients.createDefault();
+ CloseableHttpResponse response = client.execute(httpget)) {
log.debug("Making request for " + httpget.getRequestLine());
- CloseableHttpResponse response = client.execute(httpget);
- try {
- StatusLine sl = response.getStatusLine();
- int responseCode = sl.getStatusCode();
- if (responseCode == HttpURLConnection.HTTP_OK) {
- log.debug("Retreiving data from response " + httpget.getRequestLine());
- InputStream inputStream = response.getEntity().getContent();
- Reader reader = new InputStreamReader(inputStream);
- int contentLength = (int) response.getEntity().getContentLength();
- char[] charArray = new char[contentLength];
- reader.read(charArray);
- responseJson = new String(charArray);
- log.debug("Data retreived: " + responseJson);
- } else {
- log.error("Failed request: " + httpget.getRequestLine() +
- " with response: " + responseCode);
- }
- } finally {
- response.close();
+ StatusLine sl = response.getStatusLine();
+ int responseCode = sl.getStatusCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ log.debug("Retreiving data from response " + httpget.getRequestLine());
+ InputStream inputStream = response.getEntity().getContent();
+ Reader reader = new InputStreamReader(inputStream);
+ int contentLength = (int) response.getEntity().getContentLength();
+ char[] charArray = new char[contentLength];
+ reader.read(charArray);
+ String responseJson = new String(charArray);
+ log.debug("Data retreived: " + responseJson);
+ return responseJson;
+ } else {
+ log.error("Failed request: " + httpget.getRequestLine()
+ + " with response: " + responseCode);
}
- } finally {
- client.close();
}
- return responseJson;
+ return null;
}
// generic method to POST data with a REST endpoint
- public void postData(final String url, final String data)
- throws IOException {
- CloseableHttpClient httpclient = HttpClients.createDefault();
+ public void postData(final String url, final String data) throws IOException {
- try {
- HttpPost httpPost = new HttpPost(url);
- httpPost.addHeader("Authorization", "Bearer " + password);
- httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
- httpPost.setEntity(new StringEntity(data, "utf-8"));
-
+ HttpPost httpPost = new HttpPost(url);
+ httpPost.addHeader("Authorization", "Bearer " + password);
+ httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
+ httpPost.setEntity(new StringEntity(data, "utf-8"));
+ try (CloseableHttpClient httpclient = HttpClients.createDefault();
+ CloseableHttpResponse response = httpclient.execute(httpPost)) {
log.debug("Executing request " + httpPost.getRequestLine());
- CloseableHttpResponse response = httpclient.execute(httpPost);
- try {
- int responseCode = response.getStatusLine().getStatusCode();
- if (responseCode == HttpURLConnection.HTTP_OK) {
- log.info("Updated " + url + " with " + data);
- } else {
- log.error("Failed to add comment, response: " + responseCode +
- " (" + response.getStatusLine().getReasonPhrase() + ")");
- }
- } finally {
- response.close();
+ int responseCode = response.getStatusLine().getStatusCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ log.info("Updated " + url + " with " + data);
+ } else {
+ log.error("Failed to post, response: " + responseCode + " ("
+ + response.getStatusLine().getReasonPhrase() + ")");
}
- } finally {
- httpclient.close();
+ }
+ }
+
+ // generic method to PUT data with a REST endpoint
+ public void putData(final String url, final String data) throws IOException {
+
+ HttpPut HttpPut = new HttpPut(url);
+ HttpPut.addHeader("Authorization", "Bearer " + password);
+ HttpPut.addHeader("Content-Type", "application/json; charset=utf-8");
+ HttpPut.setEntity(new StringEntity(data, "utf-8"));
+ try (CloseableHttpClient httpclient = HttpClients.createDefault();
+ CloseableHttpResponse response = httpclient.execute(HttpPut)) {
+ log.debug("Executing request " + HttpPut.getRequestLine());
+ int responseCode = response.getStatusLine().getStatusCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ log.info("Updated " + url + " with " + data);
+ } else {
+ log.error("Failed to post, response: " + responseCode + " ("
+ + response.getStatusLine().getReasonPhrase() + ")");
+ }
}
}
@@ -116,18 +123,51 @@
return getData(this.baseUrl + SYS_INFO_ENDPOINT);
}
- public String getStory(final String issueId) throws IOException {
- return getData(this.baseUrl + STORIES_ENDPOINT + "/" + issueId);
+ public String getStory(final String id) throws IOException {
+ return getData(this.baseUrl + STORIES_ENDPOINT + "/" + getStoryId(id));
+ }
+
+ public int getStoryId(final String issueId) throws IOException {
+ String taskJson = getTask(issueId);
+ JsonObject jobj = new Gson().fromJson(taskJson, JsonObject.class);
+ return jobj.get("story_id").getAsInt();
+ }
+
+ public String getTask(final String issueId) throws IOException {
+ return getData(this.baseUrl + TASKS_ENDPOINT + "/" + issueId);
+ }
+
+ public void setStatus(final String issueId, final String status)
+ throws IOException {
+ log.debug("PUT task with data: ({},{})", issueId, status);
+ final String url = baseUrl + TASKS_ENDPOINT + "/" + issueId;
+ final String json =
+ "{\"task_id\":\"" + issueId + "\",\"status\":\"" + status + "\"}";
+
+ putData(url, json);
+ }
+
+ public String getTaskStatus(final String issueId) throws IOException {
+ String taskJson = getTask(issueId);
+ JsonObject jobj = new Gson().fromJson(taskJson, JsonObject.class);
+ return jobj.get("status").getAsString();
+ }
+
+ public String getTaskNotes(final String issueId) throws IOException {
+ String taskJson = getTask(issueId);
+ JsonObject jobj = new Gson().fromJson(taskJson, JsonObject.class);
+ return jobj.get("link").getAsString();
}
public void addComment(final String issueId, final String comment)
throws IOException {
- log.debug("Posting comment with data: ({},{})", issueId, comment);
- final String url = baseUrl+STORIES_ENDPOINT+"/"+issueId+"/comments";
+ int story_id = getStoryId(issueId);
+ log.debug("Posting comment with data: ({},{})", story_id, comment);
+ final String url =
+ baseUrl + STORIES_ENDPOINT + "/" + story_id + "/comments";
final String escapedComment = comment.replace("\n", "\\n");
- final String json =
- "{\"story_id\":\"" + issueId + "\",\"content\":\"" +
- escapedComment + "\"}";
+ final String json = "{\"story_id\":\"" + issueId + "\",\"content\":\""
+ + escapedComment + "\"}";
postData(url, json);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
index 28ff6c8..2313c03 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacade.java
@@ -30,7 +30,6 @@
public class StoryboardItsFacade implements ItsFacade {
private final Logger log = LoggerFactory.getLogger(StoryboardItsFacade.class);
- private static final String GERRIT_CONFIG_USERNAME = "username";
private static final String GERRIT_CONFIG_PASSWORD = "password";
private static final String GERRIT_CONFIG_URL = "url";
@@ -40,12 +39,10 @@
public StoryboardItsFacade(@PluginName String pluginName,
@GerritServerConfig Config cfg) {
final String url = cfg.getString(pluginName, null, GERRIT_CONFIG_URL);
- final String username = cfg.getString(pluginName, null,
- GERRIT_CONFIG_USERNAME);
- final String password = cfg.getString(pluginName, null,
- GERRIT_CONFIG_PASSWORD);
+ final String password =
+ cfg.getString(pluginName, null, GERRIT_CONFIG_PASSWORD);
- this.client = new StoryboardClient(url, username, password);
+ this.client = new StoryboardClient(url, password);
}
@Override
@@ -73,16 +70,29 @@
@Override
public void addRelatedLink(final String issueId, final URL relatedUrl,
String description) throws IOException {
- addComment(issueId, "Related URL: " + createLinkForWebui(
- relatedUrl.toExternalForm(), description));
+ addComment(issueId, "Related URL: "
+ + createLinkForWebui(relatedUrl.toExternalForm(), description));
}
@Override
public void performAction(final String issueId, final String actionString) {
- // No custom actions at this point.
- //
- // Note that you can use hashtag names in comments to associate a task
- // with a new project.
+
+ try {
+ String actionName =
+ actionString.substring(0, actionString.indexOf(" ")).toLowerCase();
+ String actionValue =
+ actionString.substring(actionString.indexOf(" ") + 1).toLowerCase();
+ if (actionName.equals("set-status")) {
+ if (!client.getTaskStatus(issueId).toLowerCase().equals(actionValue)) {
+ log.info("Updating task " + issueId + " with status: " + actionValue);
+ client.setStatus(issueId, actionValue);
+ }
+ }
+ } catch (StringIndexOutOfBoundsException e) {
+ log.error("Error: Invalid action: " + actionString);
+ } catch (IOException e) {
+ log.error("Error: Failed to peform action: " + actionString);
+ }
}
@Override
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 5c0d532..389b896 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,15 +1,15 @@
-This @PLUGIN@ plugin is an [`its-base`][its-base] based plugin that integrates
-Gerrit and [`Storyboard`][storyboard]. The @PLUGIN@ allows users
-to configure how a storyboard story or task should be updated relative
-to Gerrit change updates. For example, it can be configured to
-automatically add comments to a story when a comment is entered to
-an associated Gerrit change. It can also be configured to update a
-story's status upon an update to an associated Gerrit change.
+This @PLUGIN@ plugin is an [`its-base`][its-base] based plugin that
+integrates Gerrit and [`Storyboard`][storyboard]. The @PLUGIN@ plugin
+allows users to configure how a storyboard story and task should be
+updated relative to Gerrit change updates. For example, it can be
+configured to automatically add comments to a story when a comment is
+entered to an associated Gerrit change. It can also be configured to
+update status on a task when an associated Gerrit change is updated.
-For details on how to install this plugin start with the
+For details on how to install and configure this plugin start with the
[Quick Install Guide][quick].
-[quick]: quick-install-guide.html
+[quick]: quick-install-guide.md
[its-base]: https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-base
[storyboard]: http://git.openstack.org/cgit/openstack-infra/storyboard
diff --git a/src/main/resources/Documentation/config-connectivity.md b/src/main/resources/Documentation/config-connectivity.md
index a138c82..36ff372 100644
--- a/src/main/resources/Documentation/config-connectivity.md
+++ b/src/main/resources/Documentation/config-connectivity.md
@@ -4,7 +4,7 @@
Please refer to the [quick install guide][quick] for info on how to
configure a connection.
-[quick]: quick-install-guide.html
+[quick]: quick-install-guide.md
[Back to @PLUGIN@ documentation index][index]
[index]: index.html
diff --git a/src/main/resources/Documentation/quick-install-guide.md b/src/main/resources/Documentation/quick-install-guide.md
index b749a0e..97327b6 100644
--- a/src/main/resources/Documentation/quick-install-guide.md
+++ b/src/main/resources/Documentation/quick-install-guide.md
@@ -2,18 +2,19 @@
===================
For general instructions on how to enable and configure an its plugin
-please refer to the general [configuration documentation][config-doc]
+please refer to the general [configuration documentation][config-doc].
Instructions in this document are specific to the @PLUGIN@ plugin.
Install Steps:
-1. Verify Storyboard [REST endpoint][rest-enabled].
-2. [Configure the connection][its-connection].
-3. [Associate a changes with stories][its-associate-change].
-4. [Configure the actions][its-actions] that the plugin will take on a Gerrit change.
-5. [Enable the @PLUGIN@ plugin][its-enable] for the Gerrit project.
+1. [Check Storyboard REST API availability][rest-enabled]
+2. [Connection Configuration][its-connection].
+3. [Verify access to Storyboard][access-enabled]
+4. [Associate Gerrit changes with Storyboard stories and tasks][its-associate-change].
+5. [Configure the actions][its-actions] that the plugin will take on a Gerrit change update.
6. [Install the plugin][its-install]
-7. Restart Gerrit
+7. [Enable the @PLUGIN@ plugin][its-enable] for the Gerrit project.
+8. [Testing][testing]
[rest-enabled]: #rest-enabled
<a name="rest-enabled">Checking REST API availability</a>
@@ -38,9 +39,9 @@
<a name="its-connection">Connection Configuration</a>
-----------------------------------------------------
-In order for @PLUGIN@ to connect to the REST service of your
+In order for the @PLUGIN@ plugin to connect to the REST service of your
Storyboard instance, the url and credentials are required in
-your site's `etc/gerrit.config` or `etc/secure.config` under
+your Gerrit site's `etc/gerrit.config` or `etc/secure.config` under
the `@PLUGIN@` section.
Example:
@@ -48,31 +49,47 @@
```
[@PLUGIN@]
url=https://my_storyboard_instance.com
- username=USERNAME_TO_CONNECT_TO_STORYBOARD
- password=AUTH_TOKEN_FOR_ABOVE_USERNAME
+ password=STORYBOARD_USER_AUTH_TOKEN
```
+[access-enabled]: #access-enabled
+<a name="access-enabled">Check Accessibility</a>
+---------------------------------------------------------
+
+This plugin uses the Storyboard REST endpoints to POST updates. Make sure
+that the STORYBOARD_USER_AUTH_TOKEN has access to update Storyboard stories
+and tasks. To verify this use the [Storyboard story API] from the Gerrit sever
+to post an update. If it fails to update the story you'll need to make the
+necessary changes to allow access between the Gerrit and Storyboard
+servers.
+
+
[its-associate-change]: #its-associate-change
<a name="its-associate-change">Associating Gerrit Changes</a>
-------------------------------------------------------------
-In order for @PLUGIN@ to associate a Gerrit change with
-a Storyboard story, a Gerrit commentlink needs to be
+In order for the @PLUGIN@ plugin to associate a Gerrit change with
+a Storyboard story and task, a Gerrit commentlink needs to be
defined in `etc/gerrit.config`
Example:
```
+[commentlink "story"]
+ match = "\\b[Ss]tory:? #?(\\d+)"
+ link = "http://my_storyboard_instance.com/#!/story/$1"
+ html = ""
[commentLink "@PLUGIN@"]
- match = [Ss][Tt][Oo][Rr][Yy][ ]*([1-9][0-9]*)
- html = "<a href=\"https://my_storyboard_instance.com/#!/story/$1\">story $1</a>"
+ match = "\\b[Tt]ask:? #?(\\d+)"
+ link = "task: $1"
+ html = ""
```
[its-actions]: #its-actions
<a name="its-actions">Configure its actions</a>
-----------------------------------------------
-The @PLUGIN@ plugin can take action when there are updates
+The @PLUGIN@ plugin can take actions when there are updates
to Gerrit changes. Users can define what events will trigger
which actions. To configure this a `etc/its/actions.config`
file is required.
@@ -84,18 +101,34 @@
[rule "update-comment"]
event-type = comment-added
action = add-velocity-comment inline $commenter-name commented on change ${its.formatLink($change-url, $subject)}
+
# add a comment only when a user leaves a -2 or a -1 vote on the Code-Review label on the associated Gerrit change.
[rule "comment-on-negative-vote"]
event-type = comment-added
approval-Code-Review = -2,-1
action = add-comment Boo-hoo, go away!
+
# add a standard comment when there is a status update to the associated Gerrit change.
[rule "comment-on-status-update"]
event-type = patchset-created,change-abandoned,change-restored,change-merged
action = add-standard-comment
-```
-More detailed information on actions is found the [rules documentation][rules-doc]
+# set storyboard task status to 'review' when a patch is uploaded or when a change is restored
+[rule "change-in-progress"]
+ event-type = patchset-created,change-restored
+ action = set-status REVIEW
+```
+*_NOTE_: A Gerrit restart is required to update these settings.
+
+### <a id="task-status"></a>TaskStatus
+Valid task status: TODO, REVIEW, INPROGRESS, MERGED, and INVALID
+
+[its-install]: #its-install
+<a name="its-install">Install the Plugin</a>
+-------------------------------------------------------
+
+In order to install the @PLUGIN@ plugin simply copy the built jar
+file into the `plugins` folder.
[its-enable]: #its-enable
<a name="its-enable">Enable the Plugin</a>
@@ -113,13 +146,31 @@
enabled = true
```
-[its-install]: #its-install
-<a name="its-install">Install the Plugin</a>
+[testing]: #testing
+<a name="testing">Testing the Plugin</a>
-------------------------------------------------------
-In order to install the @PLUGIN@ plugin simply copy the built jar
-file into the `plugins` folder.
+Create a new Gerrit change with a commit message that contains a reference
+to the Storyboard story and task.
-[config-common-doc]: config-common.html
-[config-doc]: config.html
-[rules-doc]: config-rulebase-common.html
+Example:
+
+```
+My change to test integration
+
+This is an example change to test storyboard integration.
+Story: 123
+Task: 1000
+Change-Id: I3912f42c371023eb8bd048a5b17b776801b405e2
+```
+
+Make an update to the Gerrit change (abandone, restore, submit, etc..),
+the @PLUGIN@ plugin should automatically update the corresponding story
+and task in Storyboard.
+
+SEE ALSO
+--------
+* More detailed information on actions is found in the [rules documentation][rules-doc]
+
+
+[Storyboard story API]: http://docs.openstack.org/infra/storyboard/webapi/v1.html#put--v1-stories
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
index 9364ad3..63caf65 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/storyboard/StoryboardItsFacadeTest.java
@@ -80,8 +80,6 @@
private void mockUnconnectableStoryboard() {
expect(serverConfig.getString("its-storyboard", null, "url"))
.andReturn("<no-url>").anyTimes();
- expect(serverConfig.getString("its-storyboard", null, "username"))
- .andReturn("none").anyTimes();
expect(serverConfig.getString("its-storyboard", null, "password"))
.andReturn("none").anyTimes();
}