workflow: add invoke-issue-restapi and invoke-project-restapi actions
The invoke-issue-restapi invokes RestAPI on the issue, it uses soy
template to generate the request.
action = invoke-issue-restapi method uri passCodes template
The invoke-project-restapi is similar but invokes project method.
These commands enables advanced integration with most of JIRA features.
For example if you would like to create a link in issues to review, use
the following action:
action = invoke-issue-restapi POST /remotelink 200,201 link
With the follwing `its/templates/link.soy` template:
{namespace etc.its.templates}
{template .link}
{@param changeUrl: string}
{@param subject: string}
{@param status: string}
{lb}
"globalId": "{$changeUrl}",
"application": {lb}
"type": "com.googlesource.gerrit",
"name": "Gerrit"
{rb},
"object": {lb}
"url": "{$changeUrl}",
"title": "{$subject}",
"icon": {lb}
"url16x16": "https://www.gerritcodereview.com/images/diffy_logo.png",
"title": "Review"
{rb},
"status": {lb}
{switch $status}
{case null}
"resolved": false
{case 'NEW'}
"resolved": false
{case 'SUBMITTED'}
"resolved": false
{case 'MERGED'}
"resolved": true
{case 'ABANDONED'}
"resolved": true
{/switch}
{rb}
{rb}
{rb}
{/template}
Change-Id: I617c110764d48ed9dd2162c5414945e0d7b9d90c
Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
index 5544df2..7c1c35d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
@@ -109,6 +109,50 @@
}
}
+ public void invokeProjectRestAPI(
+ JiraItsServerInfo server,
+ String projectKey,
+ String method,
+ String uri,
+ int[] passCodes,
+ String body)
+ throws IOException {
+
+ log.debug(
+ "Trying to invoke project {} restapi method {} uri {} status {} body {}",
+ projectKey,
+ method,
+ uri,
+ Arrays.toString(passCodes),
+ body);
+ apiBuilder.getProjects(server).sendPayload(method, projectKey + uri, body, passCodes);
+ log.debug("reatapi invoked project {}", projectKey);
+ }
+
+ public void invokeIssueRestAPI(
+ JiraItsServerInfo server,
+ String issueKey,
+ String method,
+ String uri,
+ int[] passCodes,
+ String body)
+ throws IOException {
+
+ if (issueExists(server, issueKey)) {
+ log.debug(
+ "Trying to invoke issue {} restapi method {} uri {} status {} body {}",
+ issueKey,
+ method,
+ uri,
+ Arrays.toString(passCodes),
+ body);
+ apiBuilder.getIssue(server).sendPayload(method, issueKey + uri, body, passCodes);
+ log.debug("reatapi invoked issue {}", issueKey);
+ } else {
+ log.error("Issue {} does not exist or no access permission", issueKey);
+ }
+ }
+
public void createVersion(JiraItsServerInfo server, String projectKey, String version)
throws IOException {
log.debug("Trying to create version {} on project {}", version, projectKey);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
index 3d59458..4cdda87 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
@@ -29,6 +29,8 @@
import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
import com.googlesource.gerrit.plugins.its.base.its.ItsFacadeFactory;
import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction;
+import com.googlesource.gerrit.plugins.its.jira.workflow.InvokeIssueRestAPI;
+import com.googlesource.gerrit.plugins.its.jira.workflow.InvokeProjectRestAPI;
import com.googlesource.gerrit.plugins.its.jira.workflow.MarkPropertyAsReleasedVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -62,6 +64,12 @@
bind(ItsConfig.class);
bind(JiraItsServerInfoProvider.class);
bind(CustomAction.class)
+ .annotatedWith(Exports.named(InvokeProjectRestAPI.ACTION_NAME))
+ .to(InvokeProjectRestAPI.class);
+ bind(CustomAction.class)
+ .annotatedWith(Exports.named(InvokeIssueRestAPI.ACTION_NAME))
+ .to(InvokeIssueRestAPI.class);
+ bind(CustomAction.class)
.annotatedWith(Exports.named(MarkPropertyAsReleasedVersion.ACTION_NAME))
.to(MarkPropertyAsReleasedVersion.class);
install(new ItsHookModule(pluginName, pluginCfgFactory));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java
index bcd10a5..c24ff49 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java
@@ -72,7 +72,7 @@
public T doGet(String spec, int passCode, int[] failCodes) throws IOException {
HttpURLConnection conn = openConnection(spec, "GET", false);
try {
- if (validateResponse(conn, passCode, failCodes)) {
+ if (validateResponse(conn, new int[] {passCode}, failCodes)) {
readIncomingData(conn);
}
return data;
@@ -117,10 +117,15 @@
private boolean sendPayload(String method, String spec, String jsonInput, int passCode)
throws IOException {
+ return sendPayload(method, spec, jsonInput, new int[] {passCode});
+ }
+
+ public boolean sendPayload(String method, String spec, String jsonInput, int[] passCodes)
+ throws IOException {
HttpURLConnection conn = openConnection(spec, method, true);
try {
writeBodyData(jsonInput, conn);
- return validateResponse(conn, passCode, null);
+ return validateResponse(conn, passCodes, null);
} finally {
conn.disconnect();
}
@@ -148,10 +153,10 @@
* IOException exception is thrown. If it was part of the list, then the actual response code is
* returned. returns true if valid response is returned, otherwise false
*/
- private boolean validateResponse(HttpURLConnection conn, int passCode, int[] failCodes)
+ private boolean validateResponse(HttpURLConnection conn, int[] passCodes, int[] failCodes)
throws IOException {
responseCode = conn.getResponseCode();
- if (responseCode == passCode) {
+ if (ArrayUtils.contains(passCodes, responseCode)) {
return true;
}
if ((failCodes == null) || (!ArrayUtils.contains(failCodes, responseCode))) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeIssueRestAPI.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeIssueRestAPI.java
new file mode 100644
index 0000000..f798d64
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeIssueRestAPI.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.jira.workflow;
+
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.ItsPath;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.its.base.workflow.ActionType;
+import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction;
+import com.googlesource.gerrit.plugins.its.jira.JiraClient;
+import com.googlesource.gerrit.plugins.its.jira.JiraItsFacade;
+import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfo;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class InvokeIssueRestAPI implements CustomAction {
+
+ private static final Logger log = LoggerFactory.getLogger(InvokeIssueRestAPI.class);
+
+ public static final String ACTION_NAME = "invoke-issue-restapi";
+
+ private final JiraClient jiraClient;
+ private final InvokeRestAPIParametersExtractor parametersExtractor;
+ private final SoyTemplateRenderer soyTemplateRenderer;
+
+ @Inject
+ public InvokeIssueRestAPI(
+ @ItsPath Path itsPath,
+ JiraClient jiraClient,
+ InvokeRestAPIParametersExtractor parametersExtractor) {
+ this.jiraClient = jiraClient;
+ this.parametersExtractor = parametersExtractor;
+ this.soyTemplateRenderer = new SoyTemplateRenderer(itsPath);
+ }
+
+ @Override
+ public void execute(
+ ItsFacade its, String issueKey, ActionRequest actionRequest, Map<String, String> properties)
+ throws IOException {
+ Optional<InvokeRestAPIParameters> _parameters =
+ parametersExtractor.extract(actionRequest, properties);
+ if (!_parameters.isPresent()) {
+ return;
+ }
+ InvokeRestAPIParameters parameters = _parameters.get();
+
+ if (!JiraItsFacade.class.isInstance(its)) {
+ throw new IllegalArgumentException("Incorrect facade type");
+ }
+ JiraItsFacade jits = JiraItsFacade.class.cast(its);
+ JiraItsServerInfo jiraItsServerInfo = jits.getJiraServerInstance();
+
+ jiraClient.invokeIssueRestAPI(
+ jiraItsServerInfo,
+ issueKey,
+ parameters.getMethod(),
+ parameters.getUri(),
+ parameters.getPassCodes(),
+ soyTemplateRenderer.render(parameters.getTemplate(), properties));
+ }
+
+ @Override
+ public ActionType getType() {
+ return ActionType.ISSUE;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeProjectRestAPI.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeProjectRestAPI.java
new file mode 100644
index 0000000..c2cec94
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeProjectRestAPI.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.jira.workflow;
+
+import com.google.gerrit.entities.Project;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.ItsPath;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.its.base.workflow.ActionType;
+import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction;
+import com.googlesource.gerrit.plugins.its.jira.JiraClient;
+import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfo;
+import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfoProvider;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class InvokeProjectRestAPI implements CustomAction {
+
+ private static final Logger log = LoggerFactory.getLogger(InvokeProjectRestAPI.class);
+
+ public static final String ACTION_NAME = "invoke-project-restapi";
+
+ private final JiraItsServerInfoProvider serverInfoProvider;
+ private final JiraClient jiraClient;
+ private final InvokeRestAPIParametersExtractor parametersExtractor;
+ private final SoyTemplateRenderer soyTemplateRenderer;
+
+ @Inject
+ public InvokeProjectRestAPI(
+ @ItsPath Path itsPath,
+ JiraItsServerInfoProvider serverInfoProvider,
+ JiraClient jiraClient,
+ InvokeRestAPIParametersExtractor parametersExtractor) {
+ this.serverInfoProvider = serverInfoProvider;
+ this.jiraClient = jiraClient;
+ this.parametersExtractor = parametersExtractor;
+ this.soyTemplateRenderer = new SoyTemplateRenderer(itsPath);
+ }
+
+ @Override
+ public void execute(
+ ItsFacade its, String itsProject, ActionRequest actionRequest, Map<String, String> properties)
+ throws IOException {
+ Optional<InvokeRestAPIParameters> _parameters =
+ parametersExtractor.extract(actionRequest, properties);
+ if (!_parameters.isPresent()) {
+ return;
+ }
+ InvokeRestAPIParameters parameters = _parameters.get();
+
+ Project.NameKey projectName = Project.nameKey(properties.get("project"));
+ JiraItsServerInfo jiraItsServerInfo = serverInfoProvider.get(projectName);
+
+ jiraClient.invokeProjectRestAPI(
+ jiraItsServerInfo,
+ itsProject,
+ parameters.getMethod(),
+ parameters.getUri(),
+ parameters.getPassCodes(),
+ soyTemplateRenderer.render(parameters.getTemplate(), properties));
+ }
+
+ @Override
+ public ActionType getType() {
+ return ActionType.PROJECT;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParameters.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParameters.java
new file mode 100644
index 0000000..ce5313a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParameters.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.jira.workflow;
+
+/** Parameters needed by {@link InvokeIssueRestAPI} and {@link InvokeProjectsRestAPI} actions */
+public class InvokeRestAPIParameters {
+
+ private final String method;
+ private final String uri;
+ private final int[] passCodes;
+ private final String template;
+
+ public InvokeRestAPIParameters(String method, String uri, int[] passCodes, String template) {
+ this.method = method;
+ this.uri = uri;
+ this.passCodes = passCodes;
+ this.template = template;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public int[] getPassCodes() {
+ return passCodes;
+ }
+
+ public String getTemplate() {
+ return template;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParametersExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParametersExtractor.java
new file mode 100644
index 0000000..b99141e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParametersExtractor.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.jira.workflow;
+
+import com.google.common.base.Strings;
+import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class InvokeRestAPIParametersExtractor {
+
+ private static final Logger log = LoggerFactory.getLogger(InvokeRestAPIParametersExtractor.class);
+
+ @Inject
+ public InvokeRestAPIParametersExtractor() {}
+
+ public Optional<InvokeRestAPIParameters> extract(
+ ActionRequest actionRequest, Map<String, String> properties) {
+ String[] parameters = actionRequest.getParameters();
+ if (parameters.length != 4) {
+ log.error(
+ "Wrong number of received parameters. Received parameters are {}. Three parameters are"
+ + " expected, method, uri, passCodes and template.",
+ Arrays.toString(parameters));
+ return Optional.empty();
+ }
+
+ String method = parameters[0];
+ if (Strings.isNullOrEmpty(method)) {
+ log.error("Received property id is blank");
+ return Optional.empty();
+ }
+ String uri = parameters[1];
+ if (Strings.isNullOrEmpty(uri)) {
+ log.error("Received property uri is blank");
+ return Optional.empty();
+ }
+ String passCodesStr = parameters[2];
+ if (Strings.isNullOrEmpty(passCodesStr)) {
+ log.error("Received property passCodes is blank");
+ return Optional.empty();
+ }
+ int[] passCodes = Arrays.stream(passCodesStr.split(",")).mapToInt(Integer::parseInt).toArray();
+ String template = parameters[3];
+ if (Strings.isNullOrEmpty(template)) {
+ log.error("Received property template is blank");
+ return Optional.empty();
+ }
+
+ return Optional.of(new InvokeRestAPIParameters(method, uri, passCodes, template));
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/SoyTemplateRenderer.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/SoyTemplateRenderer.java
new file mode 100644
index 0000000..3463be8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/SoyTemplateRenderer.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.its.jira.workflow;
+
+import com.google.common.io.CharStreams;
+import com.google.inject.ProvisionException;
+import com.google.template.soy.SoyFileSet;
+import com.google.template.soy.jbcsrc.api.SoySauce.Renderer;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SoyTemplateRenderer {
+
+ private static final Logger log = LoggerFactory.getLogger(SoyTemplateRenderer.class);
+
+ private final Path templateDir;
+
+ public SoyTemplateRenderer(Path itsPath) {
+ this.templateDir = itsPath.resolve("templates");
+ }
+
+ private String soyTextTemplate(
+ SoyFileSet.Builder builder, String template, Map<String, String> properties) {
+
+ Path templatePath = templateDir.resolve(template + ".soy");
+ String content;
+
+ try (Reader r = Files.newBufferedReader(templatePath, StandardCharsets.UTF_8)) {
+ content = CharStreams.toString(r);
+ } catch (IOException err) {
+ throw new ProvisionException(
+ "Failed to read template file " + templatePath.toAbsolutePath().toString(), err);
+ }
+
+ builder.add(content, templatePath.toAbsolutePath().toString());
+ Renderer renderer =
+ builder
+ .build()
+ .compileTemplates()
+ .renderTemplate("etc.its.templates." + template)
+ .setData(properties);
+ String rendered = renderer.renderText().get();
+ log.debug("Rendered template {} to:\n{}", templatePath, rendered);
+ return rendered;
+ }
+
+ public String render(String template, Map<String, String> properties) throws IOException {
+ return soyTextTemplate(SoyFileSet.builder(), template, properties);
+ }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 4827dfd..cb34003 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -274,6 +274,67 @@
Specific actions
----------------
+### invoke-issue-restapi
+
+The `invoke-issue-restapi` invokes RestAPI on the issue, it uses soy template to
+generate the request.
+
+```ini
+ action = invoke-issue-restapi method uri passCodes template
+```
+
+For example if you would like to create a link in issues to review, use the
+following action:
+
+```ini
+ action = invoke-issue-restapi POST /remotelink 200,201 link
+```
+
+With the follwing `its/templates/link.soy` template:
+
+```
+{namespace etc.its.templates}
+{template .link}
+ {@param changeUrl: string}
+ {@param subject: string}
+ {@param status: string}
+{lb}
+ "globalId": "{$changeUrl}",
+ "application": {lb}
+ "type": "com.googlesource.gerrit",
+ "name": "Gerrit"
+ {rb},
+ "object": {lb}
+ "url": "{$changeUrl}",
+ "title": "{$subject}",
+ "icon": {lb}
+ "url16x16": "https://www.gerritcodereview.com/images/diffy_logo.png",
+ "title": "Review"
+ {rb},
+ "status": {lb}
+ {switch $status}
+ {case null}
+ "resolved": false
+ {case 'NEW'}
+ "resolved": false
+ {case 'SUBMITTED'}
+ "resolved": false
+ {case 'MERGED'}
+ "resolved": true
+ {case 'ABANDONED'}
+ "resolved": true
+ {/switch}
+ {rb}
+ {rb}
+{rb}
+{/template}
+```
+
+### invoke-project-restapi
+
+The `invoke-project-restapi` is similar to `invoke-issue-restapi`, it invokes
+project method instead of issue method.
+
### mark-property-as-released-version
The `mark-property-as-released-version` action marks a version as released in