From 3a7212b984a207a66f5aafdf7cd03269faaa5654 Mon Sep 17 00:00:00 2001 From: aatarasoff Date: Tue, 14 Jun 2016 17:28:56 +0300 Subject: [PATCH 1/3] add jira issue update step for pipeline --- pom.xml | 2 +- .../jira/pipeline/IssueUpdateStep.java | 110 ++++++++++++++++ .../hudson/plugins/jira/Messages.properties | 1 + .../pipeline/IssueUpdateStep/config.jelly | 13 ++ .../jira/pipeline/IssueUpdateStepTest.java | 120 ++++++++++++++++++ 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java create mode 100644 src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly create mode 100644 src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java diff --git a/pom.xml b/pom.xml index 2230a8430..74b946de1 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ scm:git:git://github.com/jenkinsci/jira-plugin.git scm:git:git@github.com:jenkinsci/jira-plugin.git https://github.com/jenkinsci/jira-plugin - jira-2.2.1 + jira-2.2.1-forked diff --git a/src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java b/src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java new file mode 100644 index 000000000..42d923c64 --- /dev/null +++ b/src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java @@ -0,0 +1,110 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.Util; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraSite; +import hudson.plugins.jira.Messages; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Step for updating jira issue with workflow migration + * + * @author aatarasoff + */ +public class IssueUpdateStep extends AbstractStepImpl { + private final String jqlSearch; + private final String workflowActionName; + private final String comment; + + @DataBoundConstructor + public IssueUpdateStep(@Nonnull String jqlSearch, @Nonnull String workflowActionName, String comment) { + this.jqlSearch = Util.fixEmptyAndTrim(jqlSearch); + this.workflowActionName = Util.fixEmptyAndTrim(workflowActionName); + this.comment = Util.fixEmptyAndTrim(comment); + } + + public String getJqlSearch() { + return jqlSearch; + } + + public String getWorkflowActionName() { + return workflowActionName; + } + + public String getComment() { + return comment; + } + + @Extension(optional = true) + public static final class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(IssueUpdateStep.StepExecution.class); + } + + @Override + public String getFunctionName() { + return "jiraIssueUpdate"; + } + + @Override + public String getDisplayName() { + return Messages.IssueUpdateStep_Descriptor_DisplayName(); + } + } + + public static class StepExecution extends AbstractSynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 1L; + + @Inject + private transient IssueUpdateStep step; + + @StepContextParameter + private transient TaskListener listener; + + @StepContextParameter + private transient Run run; + + @Override + protected Void run() throws Exception { + JiraSite site = JiraSite.get(run.getParent()); + + if (site == null) { + listener.getLogger().println(Messages.Updater_NoJiraSite()); + run.setResult(Result.FAILURE); + } + + if (StringUtils.isNotEmpty(step.getWorkflowActionName())) { + listener.getLogger().println(Messages.JiraIssueUpdateBuilder_UpdatingWithAction(step.getWorkflowActionName())); + } + + listener.getLogger().println("[JIRA] JQL: " + step.getJqlSearch()); + + try { + if (!site.progressMatchingIssues(step.getJqlSearch(), step.workflowActionName, step.getComment(), listener.getLogger())) { + listener.getLogger().println(Messages.JiraIssueUpdateBuilder_SomeIssuesFailed()); + run.setResult(Result.UNSTABLE); + } + } catch (IOException e) { + listener.getLogger().println(Messages.JiraIssueUpdateBuilder_Failed()); + e.printStackTrace(listener.getLogger()); + run.setResult(Result.FAILURE); + } + + return null; + } + } +} diff --git a/src/main/resources/hudson/plugins/jira/Messages.properties b/src/main/resources/hudson/plugins/jira/Messages.properties index 4832e5a77..6f45ed839 100644 --- a/src/main/resources/hudson/plugins/jira/Messages.properties +++ b/src/main/resources/hudson/plugins/jira/Messages.properties @@ -32,3 +32,4 @@ JiraEnvironmentVariableBuilder.Updating=[JIRA] Setting {0} to {1}. CommentStep.Descriptor.DisplayName=JIRA: Add comment to the issue SearchIssuesStep.Descriptor.DisplayName=JIRA: Search issues IssueSelectorStep.Descriptor.DisplayName=JIRA: Issue selector +IssueUpdateStep.Descriptor.DisplayName=JIRA: Progress issues by workflow action \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly b/src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly new file mode 100644 index 000000000..324c375a7 --- /dev/null +++ b/src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java b/src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java new file mode 100644 index 000000000..1733d5a31 --- /dev/null +++ b/src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java @@ -0,0 +1,120 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraProjectProperty; +import hudson.plugins.jira.JiraSession; +import hudson.plugins.jira.JiraSite; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.PrintStream; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by aleksandr on 14.06.16. + */ +public class IssueUpdateStepTest { + @ClassRule + public static JenkinsRule jenkinsRule = new JenkinsRule(); + + @Inject + IssueUpdateStep.DescriptorImpl descriptor; + + @Before + public void setUp() { + jenkinsRule.getInstance().getInjector().injectMembers(this); + } + + @Test + public void configRoundTrip() throws Exception { + configRoundTrip("id=EXAMPLE-1", "Action 1", "comment"); + } + + private void configRoundTrip(String jqlSearch, String workflowActionName, String comment) throws Exception { + IssueUpdateStep configRoundTrip = new StepConfigTester(jenkinsRule) + .configRoundTrip(new IssueUpdateStep(jqlSearch, workflowActionName, comment)); + + assertEquals(jqlSearch, configRoundTrip.getJqlSearch()); + assertEquals(workflowActionName, configRoundTrip.getWorkflowActionName()); + assertEquals(comment, configRoundTrip.getComment()); + } + + @Test + public void testUpdateIssuesByJQL() throws Exception { + JiraSession session = mock(JiraSession.class); + JiraSite site = mock(JiraSite.class); + when(site.getSession()).thenReturn(session); + + final String jqlSearch = "id=KEY"; + final String workflowActionName = "Action 1"; + final String comment = "dsgsags"; + + final List assertCalledParams = new ArrayList(); + + Mockito.doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + String jqlSearchFromArgs = invocation.getArgumentAt(0, String.class); + String workflowActionNameFromArgs = invocation.getArgumentAt(1, String.class); + String commentFromArgs = invocation.getArgumentAt(2, String.class); + System.out.println("jqlSearch: " + jqlSearchFromArgs); + System.out.println("workflowActionName: " + workflowActionNameFromArgs); + System.out.println("comment: " + commentFromArgs); + assertThat(jqlSearchFromArgs, equalTo(jqlSearch)); + assertThat(workflowActionNameFromArgs, equalTo(workflowActionName)); + assertThat(commentFromArgs, equalTo(comment)); + assertCalledParams.addAll(Arrays.asList(invocation.getArguments())); + return null; + } + }).when(site).progressMatchingIssues(Mockito. anyObject(), Mockito. anyObject(), + Mockito. anyObject(), Mockito. anyObject()); + + Run mockRun = mock(Run.class); + Job mockJob = mock(Job.class); + when(mockRun.getParent()).thenReturn(mockJob); + + TaskListener mockTaskListener = mock(TaskListener.class); + when(mockTaskListener.getLogger()).thenReturn(mock(PrintStream.class)); + + JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); + when(jiraProjectProperty.getSite()).thenReturn(site); + when(mockJob.getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); + + Map r = new HashMap(); + r.put("jqlSearch", jqlSearch); + r.put("workflowActionName", workflowActionName); + r.put("comment", comment); + IssueUpdateStep step = (IssueUpdateStep) descriptor.newInstance(r); + + StepContext ctx = mock(StepContext.class); + when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); + when(ctx.get(Run.class)).thenReturn(mockRun); + when(ctx.get(TaskListener.class)).thenReturn(mockTaskListener); + + assertThat(assertCalledParams, hasSize(0)); + + IssueUpdateStep.StepExecution start = (IssueUpdateStep.StepExecution) step.start(ctx); + start.run(); + + assertThat(assertCalledParams, hasSize(4)); + } +} From 9f30e526c3f2015debdf7a431afa7249e7cf29bc Mon Sep 17 00:00:00 2001 From: aatarasoff Date: Tue, 14 Jun 2016 17:28:56 +0300 Subject: [PATCH 2/3] add jira issue update step for pipeline --- .../jira/pipeline/IssueUpdateStep.java | 110 ++++++++++++++++ .../hudson/plugins/jira/Messages.properties | 3 +- .../pipeline/IssueUpdateStep/config.jelly | 13 ++ .../jira/pipeline/IssueUpdateStepTest.java | 120 ++++++++++++++++++ 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java create mode 100644 src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly create mode 100644 src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java diff --git a/src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java b/src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java new file mode 100644 index 000000000..42d923c64 --- /dev/null +++ b/src/main/java/hudson/plugins/jira/pipeline/IssueUpdateStep.java @@ -0,0 +1,110 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.Util; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraSite; +import hudson.plugins.jira.Messages; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Step for updating jira issue with workflow migration + * + * @author aatarasoff + */ +public class IssueUpdateStep extends AbstractStepImpl { + private final String jqlSearch; + private final String workflowActionName; + private final String comment; + + @DataBoundConstructor + public IssueUpdateStep(@Nonnull String jqlSearch, @Nonnull String workflowActionName, String comment) { + this.jqlSearch = Util.fixEmptyAndTrim(jqlSearch); + this.workflowActionName = Util.fixEmptyAndTrim(workflowActionName); + this.comment = Util.fixEmptyAndTrim(comment); + } + + public String getJqlSearch() { + return jqlSearch; + } + + public String getWorkflowActionName() { + return workflowActionName; + } + + public String getComment() { + return comment; + } + + @Extension(optional = true) + public static final class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(IssueUpdateStep.StepExecution.class); + } + + @Override + public String getFunctionName() { + return "jiraIssueUpdate"; + } + + @Override + public String getDisplayName() { + return Messages.IssueUpdateStep_Descriptor_DisplayName(); + } + } + + public static class StepExecution extends AbstractSynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 1L; + + @Inject + private transient IssueUpdateStep step; + + @StepContextParameter + private transient TaskListener listener; + + @StepContextParameter + private transient Run run; + + @Override + protected Void run() throws Exception { + JiraSite site = JiraSite.get(run.getParent()); + + if (site == null) { + listener.getLogger().println(Messages.Updater_NoJiraSite()); + run.setResult(Result.FAILURE); + } + + if (StringUtils.isNotEmpty(step.getWorkflowActionName())) { + listener.getLogger().println(Messages.JiraIssueUpdateBuilder_UpdatingWithAction(step.getWorkflowActionName())); + } + + listener.getLogger().println("[JIRA] JQL: " + step.getJqlSearch()); + + try { + if (!site.progressMatchingIssues(step.getJqlSearch(), step.workflowActionName, step.getComment(), listener.getLogger())) { + listener.getLogger().println(Messages.JiraIssueUpdateBuilder_SomeIssuesFailed()); + run.setResult(Result.UNSTABLE); + } + } catch (IOException e) { + listener.getLogger().println(Messages.JiraIssueUpdateBuilder_Failed()); + e.printStackTrace(listener.getLogger()); + run.setResult(Result.FAILURE); + } + + return null; + } + } +} diff --git a/src/main/resources/hudson/plugins/jira/Messages.properties b/src/main/resources/hudson/plugins/jira/Messages.properties index 9b515711b..11f1dc80a 100644 --- a/src/main/resources/hudson/plugins/jira/Messages.properties +++ b/src/main/resources/hudson/plugins/jira/Messages.properties @@ -36,4 +36,5 @@ SearchIssuesStep.Descriptor.DisplayName=JIRA: Search issues IssueSelectorStep.Descriptor.DisplayName=JIRA: Issue selector JiraCreateIssueNotifier.DisplayName=JIRA: Create issue IssueSelector.ExplicitIssueSelector.DisplayName=Explicit selector -IssueSelector.JqlIssueSelector.DisplayName=JQL selector \ No newline at end of file +IssueSelector.JqlIssueSelector.DisplayName=JQL selector +IssueUpdateStep.Descriptor.DisplayName=JIRA: Progress issues by workflow action diff --git a/src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly b/src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly new file mode 100644 index 000000000..324c375a7 --- /dev/null +++ b/src/main/resources/hudson/plugins/jira/pipeline/IssueUpdateStep/config.jelly @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java b/src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java new file mode 100644 index 000000000..1733d5a31 --- /dev/null +++ b/src/test/java/hudson/plugins/jira/pipeline/IssueUpdateStepTest.java @@ -0,0 +1,120 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraProjectProperty; +import hudson.plugins.jira.JiraSession; +import hudson.plugins.jira.JiraSite; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.PrintStream; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by aleksandr on 14.06.16. + */ +public class IssueUpdateStepTest { + @ClassRule + public static JenkinsRule jenkinsRule = new JenkinsRule(); + + @Inject + IssueUpdateStep.DescriptorImpl descriptor; + + @Before + public void setUp() { + jenkinsRule.getInstance().getInjector().injectMembers(this); + } + + @Test + public void configRoundTrip() throws Exception { + configRoundTrip("id=EXAMPLE-1", "Action 1", "comment"); + } + + private void configRoundTrip(String jqlSearch, String workflowActionName, String comment) throws Exception { + IssueUpdateStep configRoundTrip = new StepConfigTester(jenkinsRule) + .configRoundTrip(new IssueUpdateStep(jqlSearch, workflowActionName, comment)); + + assertEquals(jqlSearch, configRoundTrip.getJqlSearch()); + assertEquals(workflowActionName, configRoundTrip.getWorkflowActionName()); + assertEquals(comment, configRoundTrip.getComment()); + } + + @Test + public void testUpdateIssuesByJQL() throws Exception { + JiraSession session = mock(JiraSession.class); + JiraSite site = mock(JiraSite.class); + when(site.getSession()).thenReturn(session); + + final String jqlSearch = "id=KEY"; + final String workflowActionName = "Action 1"; + final String comment = "dsgsags"; + + final List assertCalledParams = new ArrayList(); + + Mockito.doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + String jqlSearchFromArgs = invocation.getArgumentAt(0, String.class); + String workflowActionNameFromArgs = invocation.getArgumentAt(1, String.class); + String commentFromArgs = invocation.getArgumentAt(2, String.class); + System.out.println("jqlSearch: " + jqlSearchFromArgs); + System.out.println("workflowActionName: " + workflowActionNameFromArgs); + System.out.println("comment: " + commentFromArgs); + assertThat(jqlSearchFromArgs, equalTo(jqlSearch)); + assertThat(workflowActionNameFromArgs, equalTo(workflowActionName)); + assertThat(commentFromArgs, equalTo(comment)); + assertCalledParams.addAll(Arrays.asList(invocation.getArguments())); + return null; + } + }).when(site).progressMatchingIssues(Mockito. anyObject(), Mockito. anyObject(), + Mockito. anyObject(), Mockito. anyObject()); + + Run mockRun = mock(Run.class); + Job mockJob = mock(Job.class); + when(mockRun.getParent()).thenReturn(mockJob); + + TaskListener mockTaskListener = mock(TaskListener.class); + when(mockTaskListener.getLogger()).thenReturn(mock(PrintStream.class)); + + JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); + when(jiraProjectProperty.getSite()).thenReturn(site); + when(mockJob.getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); + + Map r = new HashMap(); + r.put("jqlSearch", jqlSearch); + r.put("workflowActionName", workflowActionName); + r.put("comment", comment); + IssueUpdateStep step = (IssueUpdateStep) descriptor.newInstance(r); + + StepContext ctx = mock(StepContext.class); + when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); + when(ctx.get(Run.class)).thenReturn(mockRun); + when(ctx.get(TaskListener.class)).thenReturn(mockTaskListener); + + assertThat(assertCalledParams, hasSize(0)); + + IssueUpdateStep.StepExecution start = (IssueUpdateStep.StepExecution) step.start(ctx); + start.run(); + + assertThat(assertCalledParams, hasSize(4)); + } +} From 0f9583cca98e48d6635c07d3a990f854660e1943 Mon Sep 17 00:00:00 2001 From: aatarasoff Date: Mon, 4 Jul 2016 18:20:17 +0300 Subject: [PATCH 3/3] add new pipeline steps --- .../hudson/plugins/jira/JiraRestService.java | 20 ++- .../java/hudson/plugins/jira/JiraSession.java | 12 ++ .../jira/pipeline/IssueFieldUpdateStep.java | 105 +++++++++++++ .../pipeline/IssueWorkflowActionStep.java | 115 ++++++++++++++ .../hudson/plugins/jira/Messages.properties | 4 + .../IssueFieldUpdateStep/config.jelly | 13 ++ .../IssueWorkflowActionStep/config.jelly | 10 ++ .../pipeline/IssueFieldUpdateStepTest.java | 123 +++++++++++++++ .../pipeline/IssueWorkflowActionStepTest.java | 140 ++++++++++++++++++ 9 files changed, 538 insertions(+), 4 deletions(-) create mode 100644 src/main/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStep.java create mode 100644 src/main/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStep.java create mode 100644 src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/config.jelly create mode 100644 src/main/resources/hudson/plugins/jira/pipeline/IssueWorkflowActionStep/config.jelly create mode 100644 src/test/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStepTest.java create mode 100644 src/test/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStepTest.java diff --git a/src/main/java/hudson/plugins/jira/JiraRestService.java b/src/main/java/hudson/plugins/jira/JiraRestService.java index 28f510a22..91e56efea 100644 --- a/src/main/java/hudson/plugins/jira/JiraRestService.java +++ b/src/main/java/hudson/plugins/jira/JiraRestService.java @@ -18,10 +18,7 @@ import com.atlassian.jira.rest.client.api.JiraRestClient; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.*; -import com.atlassian.jira.rest.client.api.domain.input.IssueInput; -import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder; -import com.atlassian.jira.rest.client.api.domain.input.TransitionInput; -import com.atlassian.jira.rest.client.api.domain.input.VersionInput; +import com.atlassian.jira.rest.client.api.domain.input.*; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Iterators; @@ -368,4 +365,19 @@ public String getBaseApiPath() { public Permissions getMyPermissions() throws RestClientException { return jiraRestClient.getMyPermissionsRestClient().getMyPermissions(null).claim(); } + + public boolean updateIssueFieldValue(String issueKey, String fieldName, String fieldValue) { + IssueField field = getIssue(issueKey).getFieldByName(fieldName); + FieldInput fieldInput = new FieldInput(field.getId(), fieldValue); + + IssueInput issueInput = IssueInput.createWithFields(fieldInput); + + try { + jiraRestClient.getIssueClient().updateIssue(issueKey, issueInput).get(timeout, TimeUnit.SECONDS); + return true; + } catch (Exception e) { + LOGGER.warning("jira rest client update issue field error. cause: " + e.getMessage()); + } + return false; + } } \ No newline at end of file diff --git a/src/main/java/hudson/plugins/jira/JiraSession.java b/src/main/java/hudson/plugins/jira/JiraSession.java index 4cd4a9a3d..df12156f9 100644 --- a/src/main/java/hudson/plugins/jira/JiraSession.java +++ b/src/main/java/hudson/plugins/jira/JiraSession.java @@ -410,4 +410,16 @@ public Version addVersion(String version, String projectKey) { */ public Permissions getMyPermissions(){ return service.getMyPermissions(); } + /** + * Update any issue field by given value + * + * @param issueKey + * @param fieldName + * @param fieldValue + */ + public boolean updateIssueFieldValue(String issueKey, String fieldName, String fieldValue) { + LOGGER.fine("Updating issue " + issueKey + " with field name: " + fieldName + " and value: " + fieldValue); + return service.updateIssueFieldValue(issueKey, fieldName, fieldValue); + } + } \ No newline at end of file diff --git a/src/main/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStep.java b/src/main/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStep.java new file mode 100644 index 000000000..0597d5b55 --- /dev/null +++ b/src/main/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStep.java @@ -0,0 +1,105 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.Util; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraSite; +import hudson.plugins.jira.Messages; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Step for updating jira issue with workflow migration + * + * @author aatarasoff + */ +public class IssueFieldUpdateStep extends AbstractStepImpl { + private final String issueKey; + private final String fieldName; + private final String fieldValue; + + @DataBoundConstructor + public IssueFieldUpdateStep(@Nonnull String issueKey, @Nonnull String fieldName, @Nonnull String fieldValue) { + this.issueKey = Util.fixEmptyAndTrim(issueKey); + this.fieldName = Util.fixEmptyAndTrim(fieldName); + this.fieldValue = Util.fixEmptyAndTrim(fieldValue); + } + + public String getIssueKey() { + return issueKey; + } + + public String getFieldName() { + return fieldName; + } + + public String getFieldValue() { + return fieldValue; + } + + @Extension(optional = true) + public static final class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(IssueFieldUpdateStep.StepExecution.class); + } + + @Override + public String getFunctionName() { + return "jiraIssueFieldUpdate"; + } + + @Override + public String getDisplayName() { + return Messages.IssueFieldUpdateStep_Descriptor_DisplayName(); + } + } + + public static class StepExecution extends AbstractSynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 1L; + + @Inject + private transient IssueFieldUpdateStep step; + + @StepContextParameter + private transient TaskListener listener; + + @StepContextParameter + private transient Run run; + + @Override + protected Void run() throws Exception { + JiraSite site = JiraSite.get(run.getParent()); + + if (site == null) { + listener.getLogger().println(Messages.Updater_NoJiraSite()); + run.setResult(Result.FAILURE); + } + + listener.getLogger().println("[JIRA] Update issue with key: " + step.getIssueKey()); + + try { + if (!site.getSession().updateIssueFieldValue(step.getIssueKey(), step.getFieldName(), step.getFieldValue())) { + listener.getLogger().println(Messages.IssueFieldUpdateStep_Failed()); + run.setResult(Result.UNSTABLE); + } + } catch (IOException e) { + listener.getLogger().println(Messages.IssueFieldUpdateStep_Failed()); + e.printStackTrace(listener.getLogger()); + run.setResult(Result.FAILURE); + } + + return null; + } + } +} diff --git a/src/main/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStep.java b/src/main/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStep.java new file mode 100644 index 000000000..c747de578 --- /dev/null +++ b/src/main/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStep.java @@ -0,0 +1,115 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.Util; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraSite; +import hudson.plugins.jira.Messages; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +/** + * Created by aleksandr on 01.07.16. + */ +public class IssueWorkflowActionStep extends AbstractStepImpl { + private final String issueKey; + private final String workflowActionName; + + @DataBoundConstructor + public IssueWorkflowActionStep(@Nonnull String issueKey, @Nonnull String workflowActionName) { + this.issueKey = Util.fixEmptyAndTrim(issueKey); + this.workflowActionName = Util.fixEmptyAndTrim(workflowActionName); + } + + public String getIssueKey() { + return issueKey; + } + + public String getWorkflowActionName() { + return workflowActionName; + } + + @Extension(optional = true) + public static final class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(IssueWorkflowActionStep.StepExecution.class); + } + + @Override + public String getFunctionName() { + return "jiraIssueWorkflowStep"; + } + + @Override + public String getDisplayName() { + return Messages.IssueWorkflowActionStep_Descriptor_DisplayName(); + } + } + + public static class StepExecution extends AbstractSynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 1L; + + @Inject + private transient IssueWorkflowActionStep step; + + @StepContextParameter + private transient TaskListener listener; + + @StepContextParameter + private transient Run run; + + @Override + protected Void run() throws Exception { + JiraSite site = JiraSite.get(run.getParent()); + + if (site == null) { + listener.getLogger().println(Messages.Updater_NoJiraSite()); + run.setResult(Result.FAILURE); + } + + listener.getLogger().println("[JIRA] Migrate issue with key: " + step.getIssueKey() + + " to step: " + step.getWorkflowActionName() + ); + + try { + long originalStatusId = site.getSession() + .getIssue(step.getIssueKey()) + .getStatus() + .getId(); + + Integer actionId = site.getSession().getActionIdForIssue(step.getIssueKey(), step.getWorkflowActionName()); + if (actionId == null) { + throw new RuntimeException("Workflow action does not exists"); + } + + site.getSession().progressWorkflowAction(step.getIssueKey(), actionId); + + long currentStatusId = site.getSession() + .getIssue(step.getIssueKey()) + .getStatus() + .getId(); + + if (originalStatusId == currentStatusId){ + listener.getLogger().println(Messages.IssueWorkflowActionStep_Failed()); + run.setResult(Result.FAILURE); + } + } catch (Exception e) { + listener.getLogger().println(Messages.IssueWorkflowActionStep_Failed()); + e.printStackTrace(listener.getLogger()); + run.setResult(Result.FAILURE); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/jira/Messages.properties b/src/main/resources/hudson/plugins/jira/Messages.properties index 11f1dc80a..c6bfb5ec2 100644 --- a/src/main/resources/hudson/plugins/jira/Messages.properties +++ b/src/main/resources/hudson/plugins/jira/Messages.properties @@ -38,3 +38,7 @@ JiraCreateIssueNotifier.DisplayName=JIRA: Create issue IssueSelector.ExplicitIssueSelector.DisplayName=Explicit selector IssueSelector.JqlIssueSelector.DisplayName=JQL selector IssueUpdateStep.Descriptor.DisplayName=JIRA: Progress issues by workflow action +IssueFieldUpdateStep.Descriptor.DisplayName=JIRA: Update issue field +IssueFieldUpdateStep.Failed=JIRA: Update issue field is failed +IssueWorkflowActionStep.Descriptor.DisplayName=JIRA: Progress single issue by workflow action +IssueWorkflowActionStep.Failed=JIRA: Progress issue is failed diff --git a/src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/config.jelly b/src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/config.jelly new file mode 100644 index 000000000..57ec977af --- /dev/null +++ b/src/main/resources/hudson/plugins/jira/pipeline/IssueFieldUpdateStep/config.jelly @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/jira/pipeline/IssueWorkflowActionStep/config.jelly b/src/main/resources/hudson/plugins/jira/pipeline/IssueWorkflowActionStep/config.jelly new file mode 100644 index 000000000..85ab4f1c3 --- /dev/null +++ b/src/main/resources/hudson/plugins/jira/pipeline/IssueWorkflowActionStep/config.jelly @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStepTest.java b/src/test/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStepTest.java new file mode 100644 index 000000000..3b15a84f1 --- /dev/null +++ b/src/test/java/hudson/plugins/jira/pipeline/IssueFieldUpdateStepTest.java @@ -0,0 +1,123 @@ +package hudson.plugins.jira.pipeline; + +import com.google.inject.Inject; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraProjectProperty; +import hudson.plugins.jira.JiraSession; +import hudson.plugins.jira.JiraSite; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.PrintStream; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by aleksandr on 04.07.16. + */ +public class IssueFieldUpdateStepTest { + @ClassRule + public static JenkinsRule jenkinsRule = new JenkinsRule(); + + @Inject + IssueFieldUpdateStep.DescriptorImpl descriptor; + + @Before + public void setUp() { + jenkinsRule.getInstance().getInjector().injectMembers(this); + } + + @Test + public void configRoundTrip() throws Exception { + configRoundTrip("EXAMPLE-1", "my-field", "Field Value"); + } + + private void configRoundTrip(String issueKey, String fieldName, String fieldValue) throws Exception { + IssueFieldUpdateStep configRoundTrip = new StepConfigTester(jenkinsRule) + .configRoundTrip(new IssueFieldUpdateStep(issueKey, fieldName, fieldValue)); + + assertEquals(issueKey, configRoundTrip.getIssueKey()); + assertEquals(fieldName, configRoundTrip.getFieldName()); + assertEquals(fieldValue, configRoundTrip.getFieldValue()); + } + + @Test + public void testUpdateIssueFieldByKey() throws Exception { + JiraSession session = mock(JiraSession.class); + JiraSite site = mock(JiraSite.class); + when(site.getSession()).thenReturn(session); + + final String issueKey = "EXAMPLE-1"; + final String fieldName = "my-field"; + final String fieldValue = "Field Value"; + + final List assertCalledParams = new ArrayList(); + + Mockito.doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + String issueKeyFromArgs = invocation.getArgumentAt(0, String.class); + String fieldNameFromArgs = invocation.getArgumentAt(1, String.class); + String fieldValueFromArgs = invocation.getArgumentAt(2, String.class); + System.out.println("issueKey: " + issueKeyFromArgs); + System.out.println("fieldName: " + fieldNameFromArgs); + System.out.println("fieldValue: " + fieldValueFromArgs); + assertThat(issueKeyFromArgs, equalTo(issueKey)); + assertThat(fieldNameFromArgs, equalTo(fieldName)); + assertThat(fieldValueFromArgs, equalTo(fieldValue)); + assertCalledParams.addAll(Arrays.asList(invocation.getArguments())); + return null; + } + }).when(session).updateIssueFieldValue( + Mockito. anyObject(), + Mockito. anyObject(), + Mockito. anyObject() + ); + + Run mockRun = mock(Run.class); + Job mockJob = mock(Job.class); + when(mockRun.getParent()).thenReturn(mockJob); + + TaskListener mockTaskListener = mock(TaskListener.class); + when(mockTaskListener.getLogger()).thenReturn(mock(PrintStream.class)); + + JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); + when(jiraProjectProperty.getSite()).thenReturn(site); + when(mockJob.getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); + + Map r = new HashMap(); + r.put("issueKey", issueKey); + r.put("fieldName", fieldName); + r.put("fieldValue", fieldValue); + IssueFieldUpdateStep step = (IssueFieldUpdateStep) descriptor.newInstance(r); + + StepContext ctx = mock(StepContext.class); + when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); + when(ctx.get(Run.class)).thenReturn(mockRun); + when(ctx.get(TaskListener.class)).thenReturn(mockTaskListener); + + assertThat(assertCalledParams, hasSize(0)); + + IssueFieldUpdateStep.StepExecution start = (IssueFieldUpdateStep.StepExecution) step.start(ctx); + start.run(); + + assertThat(assertCalledParams, hasSize(3)); + } +} diff --git a/src/test/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStepTest.java b/src/test/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStepTest.java new file mode 100644 index 000000000..35a53dd3c --- /dev/null +++ b/src/test/java/hudson/plugins/jira/pipeline/IssueWorkflowActionStepTest.java @@ -0,0 +1,140 @@ +package hudson.plugins.jira.pipeline; + +import com.atlassian.jira.rest.client.api.domain.Issue; +import com.atlassian.jira.rest.client.api.domain.Status; +import com.google.inject.Inject; +import hudson.model.Job; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.jira.JiraProjectProperty; +import hudson.plugins.jira.JiraSession; +import hudson.plugins.jira.JiraSite; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.PrintStream; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by aleksandr on 04.07.16. + */ +public class IssueWorkflowActionStepTest { + @ClassRule + public static JenkinsRule jenkinsRule = new JenkinsRule(); + + @Inject + IssueWorkflowActionStep.DescriptorImpl descriptor; + + @Before + public void setUp() { + jenkinsRule.getInstance().getInjector().injectMembers(this); + } + + @Test + public void configRoundTrip() throws Exception { + configRoundTrip("EXAMPLE-1", "action 1"); + } + + private void configRoundTrip(String issueKey, String workflowActionName) throws Exception { + IssueWorkflowActionStep configRoundTrip = new StepConfigTester(jenkinsRule) + .configRoundTrip(new IssueWorkflowActionStep(issueKey, workflowActionName)); + + assertEquals(issueKey, configRoundTrip.getIssueKey()); + assertEquals(workflowActionName, configRoundTrip.getWorkflowActionName()); + } + + @Test + public void testProgressIssue() throws Exception { + JiraSession session = mock(JiraSession.class); + JiraSite site = mock(JiraSite.class); + when(site.getSession()).thenReturn(session); + + final String issueKey = "EXAMPLE-1"; + final String workflowActionName = "action 1"; + final long originalActionId = 1L; + final long targetActionId = 2L; + + final List assertCalledParams = new ArrayList(); + + Mockito.doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + String issueKeyFromArgs = invocation.getArgumentAt(0, String.class); + Integer actionIdFromArgs = invocation.getArgumentAt(1, Integer.class); + System.out.println("issueKey: " + issueKeyFromArgs); + System.out.println("actionId: " + actionIdFromArgs); + assertThat(issueKeyFromArgs, equalTo(issueKey)); + assertThat(actionIdFromArgs, equalTo(Long.valueOf(targetActionId).intValue())); + assertCalledParams.addAll(Arrays.asList(invocation.getArguments())); + return null; + } + }).when(session).progressWorkflowAction( + Mockito. anyObject(), + Mockito. anyObject() + ); + + Run mockRun = mock(Run.class); + Job mockJob = mock(Job.class); + when(mockRun.getParent()).thenReturn(mockJob); + + TaskListener mockTaskListener = mock(TaskListener.class); + when(mockTaskListener.getLogger()).thenReturn(mock(PrintStream.class)); + + JiraProjectProperty jiraProjectProperty = mock(JiraProjectProperty.class); + when(jiraProjectProperty.getSite()).thenReturn(site); + when(mockJob.getProperty(JiraProjectProperty.class)).thenReturn(jiraProjectProperty); + + when(session.getActionIdForIssue(Mockito.eq(issueKey), Mockito.eq(workflowActionName))).thenReturn(2); + + Issue issue = mock(Issue.class); + Status status = mock(Status.class); + when(session.getIssue(Mockito.eq(issueKey))).thenReturn(issue); + when(issue.getStatus()).thenReturn(status); + + when(status.getId()).then(new Answer() { + private boolean firstTime = true; + + @Override + public Long answer(InvocationOnMock invocationOnMock) throws Throwable { + if (firstTime) { + firstTime = false; + return originalActionId; + } + return targetActionId; + } + }); + + Map r = new HashMap(); + r.put("issueKey", issueKey); + r.put("workflowActionName", workflowActionName); + IssueWorkflowActionStep step = (IssueWorkflowActionStep) descriptor.newInstance(r); + + StepContext ctx = mock(StepContext.class); + when(ctx.get(Node.class)).thenReturn(jenkinsRule.getInstance()); + when(ctx.get(Run.class)).thenReturn(mockRun); + when(ctx.get(TaskListener.class)).thenReturn(mockTaskListener); + + assertThat(assertCalledParams, hasSize(0)); + + IssueWorkflowActionStep.StepExecution start = (IssueWorkflowActionStep.StepExecution) step.start(ctx); + start.run(); + + assertThat(assertCalledParams, hasSize(2)); + } +}