Skip to content

Commit

Permalink
JENKINS-31164: Configurable issue priorityId, typeId and actionIdOnSu…
Browse files Browse the repository at this point in the history
…ccess

Motivation:

When Jenkins creates a JIRA ticket the priority and type ids are hardcoded and may break ticket creation as noted in JENKINS-31164.

Modifications:

Allow the admin to configure issue priority id and type id from the Jenkins job configuration.
Allow the admin to configure actionId on success which allows the plugin to automatically resolve or close tickets.

Result:

Support for JIRA where the type ids are different and greater flexibility when setting up workflows.
  • Loading branch information
johnou committed Nov 26, 2016
1 parent 37d7f69 commit 0d70110
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 66 deletions.
137 changes: 89 additions & 48 deletions src/main/java/hudson/plugins/jira/JiraCreateIssueNotifier.java
@@ -1,15 +1,13 @@
package hudson.plugins.jira;

import com.atlassian.jira.rest.client.api.domain.BasicComponent;
import com.atlassian.jira.rest.client.api.domain.Component;
import com.atlassian.jira.rest.client.api.domain.Issue;
import com.atlassian.jira.rest.client.api.domain.IssueType;
import com.atlassian.jira.rest.client.api.domain.Priority;
import com.atlassian.jira.rest.client.api.domain.Status;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
Expand All @@ -20,6 +18,7 @@
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
Expand All @@ -31,11 +30,10 @@
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

import static hudson.plugins.jira.JiraRestService.BUG_ISSUE_TYPE_ID;

/**
* When a build fails it creates jira issues.
* Repeated failures does not create a new issue but update the existing issue until the issue is closed.
Expand All @@ -50,6 +48,9 @@ public class JiraCreateIssueNotifier extends Notifier {
private String testDescription;
private String assignee;
private String component;
private Long typeId;
private Long priorityId;
private Integer actionIdOnSuccess;

enum finishedStatuses {
Closed,
Expand All @@ -58,13 +59,22 @@ enum finishedStatuses {
}

@DataBoundConstructor
public JiraCreateIssueNotifier(String projectKey, String testDescription, String assignee, String component) {
public JiraCreateIssueNotifier(String projectKey, String testDescription, String assignee, String component, Long typeId,
Long priorityId, Integer actionIdOnSuccess) {
if (projectKey == null) throw new IllegalArgumentException("Project key cannot be null");
this.projectKey = projectKey;

this.testDescription = testDescription;
this.assignee = assignee;
this.component = component;
this.typeId = typeId;
this.priorityId = priorityId;
this.actionIdOnSuccess = actionIdOnSuccess;
}

@Deprecated
public JiraCreateIssueNotifier(String projectKey, String testDescription, String assignee, String component) {
this(projectKey, testDescription, assignee, component, null, null, null);
}

public String getProjectKey() {
Expand Down Expand Up @@ -99,6 +109,18 @@ public void setComponent(String component) {
this.component = component;
}

public Long getTypeId() {
return typeId;
}

public Long getPriorityId() {
return priorityId;
}

public Integer getActionIdOnSuccess() {
return actionIdOnSuccess;
}

@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
Expand Down Expand Up @@ -165,7 +187,17 @@ private Issue createJiraIssue(AbstractBuild<?, ?> build, String filename) throws
);
Iterable<String> components = Splitter.on(",").trimResults().omitEmptyStrings().split(component);

Issue issue = session.createIssue(projectKey, description, assignee, components, summary);
Long type = typeId;
if (type == null || type == 0) { // zero is default / invalid selection
LOG.info("Returning default issue type id " + BUG_ISSUE_TYPE_ID);
type = BUG_ISSUE_TYPE_ID;
}
Long priority = priorityId;
if (priority != null && priority == 0) {
priority = null; // remove invalid priority selection
}

Issue issue = session.createIssue(projectKey, description, assignee, components, summary, type, priority);

writeInFile(filename, issue);
return issue;
Expand All @@ -190,47 +222,15 @@ private Status getStatus(AbstractBuild<?, ?> build, String id) throws IOExceptio
* Adds a comment to the existing issue.
*
* @param build
* @param listener
* @param id
* @param comment
* @throws IOException
*/
private void addComment(AbstractBuild<?, ?> build, String id, String comment) throws IOException {

private void addComment(AbstractBuild<?, ?> build, BuildListener listener, String id, String comment) throws IOException {
JiraSession session = getJiraSession(build);
session.addCommentWithoutConstrains(id, comment);
}

/**
* Returns an Array of componets given by the user
*
* @param build
* @param component
* @return Array of component
* @throws IOException
*/
private List<BasicComponent> getJiraComponents(AbstractBuild<?, ?> build, String component) throws IOException {

if (Util.fixEmpty(component) == null) {
return Collections.emptyList();
}

JiraSession session = getJiraSession(build);
List<Component> availableComponents = session.getComponents(projectKey);

//converting the user input as a string array
Splitter splitter = Splitter.on(",").trimResults().omitEmptyStrings();
List<String> inputComponents = Lists.newArrayList(splitter.split(component));
int numberOfComponents = inputComponents.size();

final List<BasicComponent> jiraComponents = new ArrayList<BasicComponent>(numberOfComponents);

for (final BasicComponent availableComponent : availableComponents) {
if (inputComponents.contains(availableComponent.getName())) {
jiraComponents.add(availableComponent);
}
}

return jiraComponents;
listener.getLogger().println(String.format("[%s] Commented issue", id));
}

/**
Expand Down Expand Up @@ -339,9 +339,9 @@ private void currentBuildResultFailure(AbstractBuild<?, ?> build, BuildListener
deleteFile(filename);
Issue issue = createJiraIssue(build, filename);
LOG.info(String.format("[%s] created.", issue.getKey()));

listener.getLogger().println("Build failed, created JIRA issue " + issue.getKey());
}else {
addComment(build, issueId, comment);
addComment(build, listener, issueId, comment);
LOG.info(String.format("[%s] The previous build also failed, comment added.", issueId));
}
} catch (IOException e) {
Expand All @@ -353,6 +353,7 @@ private void currentBuildResultFailure(AbstractBuild<?, ?> build, BuildListener
if (previousBuildResult == Result.SUCCESS || previousBuildResult == Result.ABORTED) {
try {
Issue issue = createJiraIssue(build, filename);
LOG.info(String.format("[%s] created.", issue.getKey()));
listener.getLogger().println("Build failed, created JIRA issue " + issue.getKey());
} catch (IOException e) {
listener.error("Error creating JIRA issue : " + e.getMessage());
Expand Down Expand Up @@ -390,8 +391,11 @@ private void currentBuildResultSuccess(AbstractBuild<?, ?> build, BuildListener
LOG.info(String.format("%s is closed", issueId));
deleteFile(filename);
} else {
LOG.info(String.format("%s is not Closed, comment was added.", issueId));
addComment(build, issueId, comment);
LOG.info(String.format("%s is not closed, comment was added.", issueId));
addComment(build, listener, issueId, comment);
if (actionIdOnSuccess != null && actionIdOnSuccess > 0) {
progressWorkflowAction(build, issueId, actionIdOnSuccess);
}
}

} catch (IOException e) {
Expand All @@ -403,6 +407,11 @@ private void currentBuildResultSuccess(AbstractBuild<?, ?> build, BuildListener
}
}

private void progressWorkflowAction(AbstractBuild<?, ?> build, String issueId, Integer actionId) throws IOException {
JiraSession session = getJiraSession(build);
session.progressWorkflowAction(issueId, actionId);
}

/**
* Returns build details string in wiki format, with hyperlinks.
*
Expand Down Expand Up @@ -439,6 +448,38 @@ public FormValidation doCheckProjectKey(@QueryParameter String value) throws IOE
return FormValidation.ok();
}

public ListBoxModel doFillPriorityIdItems() {
ListBoxModel items = new ListBoxModel().add(""); // optional field
for (JiraSite site : JiraProjectProperty.DESCRIPTOR.getSites()) {
try {
JiraSession session = site.getSession();
if (session != null) {
for (Priority priority : session.getPriorities()) {
items.add("[" + site.getName() + "] " + priority.getName(), String.valueOf(priority.getId()));
}
}
} catch (IOException ignore) {
}
}
return items;
}

public ListBoxModel doFillTypeIdItems() {
ListBoxModel items = new ListBoxModel().add(""); // optional field
for (JiraSite site : JiraProjectProperty.DESCRIPTOR.getSites()) {
try {
JiraSession session = site.getSession();
if (session != null) {
for (IssueType type : session.getIssueTypes()) {
items.add("[" + site.getName() + "] " + type.getName(), String.valueOf(type.getId()));
}
}
} catch (IOException ignore) {
}
}
return items;
}

@Override
public JiraCreateIssueNotifier newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return req.bindJSON(JiraCreateIssueNotifier.class, formData);
Expand Down
25 changes: 24 additions & 1 deletion src/main/java/hudson/plugins/jira/JiraRestService.java
Expand Up @@ -36,6 +36,8 @@
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
Expand All @@ -59,6 +61,8 @@ public class JiraRestService {
*/
public static final String BASE_API_PATH = "rest/api/2";

static final long BUG_ISSUE_TYPE_ID = 1L;

private final URI uri;

private final JiraRestClient jiraRestClient;
Expand Down Expand Up @@ -142,6 +146,15 @@ public List<IssueType> getIssueTypes() {
}
}

public List<Priority> getPriorities() {
try {
return Lists.newArrayList(jiraRestClient.getMetadataClient().getPriorities().get(timeout, TimeUnit.SECONDS));
} catch (Exception e) {
LOGGER.log(WARNING, "jira rest client get priorities error. cause: " + e.getMessage(), e);
return Collections.emptyList();
}
}

public List<String> getProjectsKeys() {
Iterable<BasicProject> projects = Collections.emptyList();
try {
Expand Down Expand Up @@ -222,13 +235,23 @@ public void releaseVersion(String projectKey, Version version) {
}
}

@Deprecated
public BasicIssue createIssue(String projectKey, String description, String assignee, Iterable<String> components, String summary) {
return createIssue(projectKey, description, assignee, components, summary, BUG_ISSUE_TYPE_ID, null);
}

public BasicIssue createIssue(String projectKey, String description, String assignee, Iterable<String> components, String summary,
@Nonnull Long issueTypeId, @Nullable Long priorityId) {
IssueInputBuilder builder = new IssueInputBuilder();
builder.setProjectKey(projectKey)
.setDescription(description)
.setIssueTypeId(1L) // BUG
.setIssueTypeId(issueTypeId)
.setSummary(summary);

if (priorityId != null) {
builder.setPriorityId(priorityId);
}

if (!assignee.equals(""))
builder.setAssigneeName(assignee);
if (Iterators.size(components.iterator()) > 0){
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/hudson/plugins/jira/JiraSession.java
Expand Up @@ -14,6 +14,9 @@
import hudson.plugins.jira.model.JiraIssueField;
import org.apache.commons.lang.StringUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static org.apache.commons.lang.StringUtils.isNotEmpty;

/**
Expand Down Expand Up @@ -186,6 +189,16 @@ public List<IssueType> getIssueTypes() {
return service.getIssueTypes();
}

/**
* Get all priorities
*
* @return An array of priorities
*/
public List<Priority> getPriorities() {
LOGGER.fine("Fetching priorities");
return service.getPriorities();
}

@Deprecated
public boolean existsIssue(String id) {
return site.existsIssue(id);
Expand Down Expand Up @@ -274,7 +287,7 @@ public void replaceFixVersion(String projectKey, String fromVersion, String toVe
}
}

LOGGER.fine("Replaceing version in issue: " + issue.getKey());
LOGGER.fine("Replacing version in issue: " + issue.getKey());
service.updateIssue(issue.getKey(), Lists.newArrayList(newVersions));
}
}
Expand Down Expand Up @@ -391,8 +404,13 @@ private HashMap<Long, String> getKnownStatuses() {
* @param summary
* @return The issue id
*/
@Deprecated
public Issue createIssue(String projectKey, String description, String assignee, Iterable<String> components, String summary) {
final BasicIssue basicIssue = service.createIssue(projectKey, description, assignee, components, summary);
return createIssue(projectKey, description, assignee, components, summary, null, null);
}

public Issue createIssue(String projectKey, String description, String assignee, Iterable<String> components, String summary, @Nonnull Long issueTypeId, @Nullable Long priorityId) {
final BasicIssue basicIssue = service.createIssue(projectKey, description, assignee, components, summary, issueTypeId, priorityId);
return service.getIssue(basicIssue.getKey());
}

Expand Down
@@ -1,14 +1,23 @@
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry title="${%Jira Project Key}" field="projectKey">
<f:entry title="${%Jira Project Key}" field="projectKey">
<f:textbox/>
</f:entry>
<f:entry title="${%Assignee}" field="assignee">
<f:textbox/>
</f:entry>
<f:entry title="${%Description Of Test}" field="testDescription">
<f:textarea/>
</f:entry>
<f:entry title="${%Assignee}" field="assignee">
<f:textbox/>
</f:entry>
<f:entry title="${%Description Of Test}" field="testDescription">
<f:textarea/>
</f:entry>
<f:entry title="${%Component Name}" field="component">
<f:textbox/>
<f:textbox/>
</f:entry>
<f:entry title="${%Issue Priority}" field="priorityId">
<f:select/>
</f:entry>
<f:entry title="${%Issue Type}" field="typeId">
<f:select/>
</f:entry>
<f:entry title="${%Action Id On Success}" field="actionIdOnSuccess">
<f:number/>
</f:entry>
</j:jelly>
@@ -0,0 +1,4 @@
<div>
<strong>Optional</strong> <br/>
The JIRA issue status will transition with the configured workflow action id when the build status returns success, for no transition set '0'.
</div>

0 comments on commit 0d70110

Please sign in to comment.