package hudson.plugins.jira;
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 hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
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;
import org.kohsuke.stapler.StaplerRequest;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
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.
*
* @author Rupali Behera rupali@vertisinfotech.com
*/
public class JiraCreateIssueNotifier extends Notifier {
private static final Logger LOG = Logger.getLogger(JiraCreateIssueNotifier.class.getName());
private String projectKey;
private String testDescription;
private String assignee;
private String component;
private Long typeId;
private Long priorityId;
private Integer actionIdOnSuccess;
enum finishedStatuses {
Closed,
Done,
Resolved
}
@DataBoundConstructor
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() {
return projectKey;
}
public void setProjectKey(String projectKey) {
this.projectKey = projectKey;
}
public String getTestDescription() {
return testDescription;
}
public void setTestDescription(String testDescription) {
this.testDescription = testDescription;
}
public String getAssignee() {
return assignee;
}
public void setAssignee(String assignee) {
this.assignee = assignee;
}
public String getComponent() {
return component;
}
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;
}
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
String jobDirPath = build.getProject().getBuildDir().getPath();
String filename = jobDirPath + File.separator + "issue.txt";
EnvVars vars = build.getEnvironment(TaskListener.NULL);
Result currentBuildResult = build.getResult();
Result previousBuildResult = null;
AbstractBuild<?, ?> previousBuild = build.getPreviousBuild();
if (previousBuild != null) {
previousBuildResult = previousBuild.getResult();
}
if (currentBuildResult != Result.ABORTED && previousBuild != null) {
if (currentBuildResult == Result.FAILURE) {
currentBuildResultFailure(build, listener, previousBuildResult, filename, vars);
}
if (currentBuildResult == Result.SUCCESS) {
currentBuildResultSuccess(build, listener, previousBuildResult, filename, vars);
}
}
return true;
}
/**
* It creates a issue in the given project, with the given description, assignee,components and summary.
* The created issue ID is saved to the file at "filename".
*
* @param build
* @param filename
* @return issue id
* @throws IOException
* @throws InterruptedException
*/
private Issue createJiraIssue(AbstractBuild<?, ?> build, String filename) throws IOException, InterruptedException {
EnvVars vars = build.getEnvironment(TaskListener.NULL);
JiraSession session = getJiraSession(build);
String buildName = getBuildName(vars);
String summary = String.format("Build %s failed", buildName);
String description = String.format(
"%s\n\nThe build %s has failed.\nFirst failed run: %s",
(this.testDescription.equals("")) ? "No description is provided" : this.testDescription,
buildName,
getBuildDetailsString(vars)
);
Iterable<String> components = Splitter.on(",").trimResults().omitEmptyStrings().split(component);
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;
}
/**
* Returns the status of the issue.
*
* @param build
* @param id
* @return Status of the issue
* @throws IOException
*/
private Status getStatus(AbstractBuild<?, ?> build, String id) throws IOException {
JiraSession session = getJiraSession(build);
Issue issue = session.getIssueByKey(id);
return issue.getStatus();
}
/**
* Adds a comment to the existing issue.
*
* @param build
* @param listener
* @param id
* @param comment
* @throws IOException
*/
private void addComment(AbstractBuild<?, ?> build, BuildListener listener, String id, String comment) throws IOException {
JiraSession session = getJiraSession(build);
session.addCommentWithoutConstrains(id, comment);
listener.getLogger().println(String.format("[%s] Commented issue", id));
}
/**
* Returns the issue id
*
* @param filename
* @return
* @throws IOException
* @throws InterruptedException
*/
private String getIssue(String filename) throws IOException, InterruptedException {
String issueId = "";
BufferedReader br = null;
try {
String issue;
br = new BufferedReader(new FileReader(filename));
while ((issue = br.readLine()) != null) {
issueId = issue;
}
return issueId;
} catch (FileNotFoundException e) {
return null;
} finally {
if (br != null) {
br.close();
}
}
}
JiraSite getSiteForProject(AbstractProject<?, ?> project) {
return JiraSite.get(project);
}
/**
* Returns the jira session.
*
* @param build
* @return JiraSession
* @throws IOException
*/
private JiraSession getJiraSession(AbstractBuild<?, ?> build) throws IOException {
JiraSite site = getSiteForProject(build.getProject());
if (site == null) {
throw new IllegalStateException("JIRA site needs to be configured in the project " + build.getFullDisplayName());
}
JiraSession session = site.getSession();
if (session == null) {
throw new IllegalStateException("Remote access for JIRA isn't configured in Jenkins");
}
return session;
}
/**
* @param filename
*/
private void deleteFile(String filename) {
File file = new File(filename);
if (file.exists() && !file.delete()) {
LOG.warning("WARNING: couldn't delete file: " + filename);
}
}
/**
* write's the issue id in the file, which is stored in the Job's directory
*
* @param Filename
* @param issue
* @throws FileNotFoundException
*/
private void writeInFile(String Filename, Issue issue) throws FileNotFoundException {
PrintWriter writer = new PrintWriter(Filename);
writer.println(issue.getKey());
writer.close();
}
/**
* when the current build fails it checks for the previous build's result,
* creates jira issue if the result was "success" and adds comment if the result was "fail".
* It adds comment until the previously created issue is closed.
*/
private void currentBuildResultFailure(AbstractBuild<?, ?> build, BuildListener listener, Result previousBuildResult,
String filename, EnvVars vars) throws InterruptedException, IOException {
if (previousBuildResult == Result.FAILURE) {
String comment = String.format("Build is still failing.\nFailed run: %s", getBuildDetailsString(vars));
//Get the issue-id which was filed when the previous built failed
String issueId = getIssue(filename);
if (issueId != null) {
try {
//The status of the issue which was filed when the previous build failed
Status status = getStatus(build, issueId);
// Issue Closed, need to open new one
if ( status.getName().equalsIgnoreCase(finishedStatuses.Closed.toString()) ||
status.getName().equalsIgnoreCase(finishedStatuses.Resolved.toString()) ||
status.getName().equalsIgnoreCase(finishedStatuses.Done.toString()) ) {
listener.getLogger().println("The previous build also failed but the issue is closed");
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, listener, issueId, comment);
LOG.info(String.format("[%s] The previous build also failed, comment added.", issueId));
}
} catch (IOException e) {
LOG.warning(String.format("[%s] - error processing JIRA change: %s", issueId, e.getMessage()));
}
}
}
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());
LOG.warning("Error creating JIRA issue\n" + e.getMessage());
}
}
}
/**
* when the current build's result is "success",
* it checks for the previous build's result and adds comment until the previously created issue is closed.
*
* @param build
* @param previousBuildResult
* @param filename
* @param vars
* @throws InterruptedException
* @throws IOException
*/
private void currentBuildResultSuccess(AbstractBuild<?, ?> build, BuildListener listener, Result previousBuildResult,
String filename, EnvVars vars) throws InterruptedException, IOException {
if (previousBuildResult == Result.FAILURE || previousBuildResult == Result.SUCCESS) {
String comment = String.format("Previously failing build now is OK.\n Passed run: %s", getBuildDetailsString(vars));
String issueId = getIssue(filename);
//if issue exists it will check the status and comment or delete the file accordingly
if (issueId != null) {
try {
Status status = getStatus(build, issueId);
//if issue is in closed status
if ( status.getName().equalsIgnoreCase(finishedStatuses.Closed.toString()) ||
status.getName().equalsIgnoreCase(finishedStatuses.Resolved.toString()) ||
status.getName().equalsIgnoreCase(finishedStatuses.Done.toString()) ) {
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, listener, issueId, comment);
if (actionIdOnSuccess != null && actionIdOnSuccess > 0) {
progressWorkflowAction(build, issueId, actionIdOnSuccess);
}
}
} catch (IOException e) {
listener.error("Error updating JIRA issue " + issueId + " : " + e.getMessage());
LOG.warning("Error updating JIRA issue " + issueId + "\n" + e);
}
}
}
}
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.
*
* @param vars
* @return
*/
private String getBuildDetailsString(EnvVars vars){
final String buildURL = vars.get("BUILD_URL");
return String.format("[%s|%s] [console log|%s]", getBuildName(vars), buildURL, buildURL.concat("console"));
}
/**
* Returns build name in format BUILD#10
*
* @param vars
* @return String
*/
private String getBuildName(EnvVars vars){
final String jobName = vars.get("JOB_NAME");
final String buildNumber = vars.get("BUILD_NUMBER");
return String.format("%s #%s", jobName, buildNumber);
}
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public DescriptorImpl() {
super(JiraCreateIssueNotifier.class);
}
public FormValidation doCheckProjectKey(@QueryParameter String value) throws IOException {
if (value.length() == 0) {
return FormValidation.error("Please set the project key");
}
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);
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
@Override
public String getDisplayName() {
return Messages.JiraCreateIssueNotifier_DisplayName();
}
@Override
public String getHelpFile() {
return "/plugin/jira/help-jira-create-issue.html";
}
}
}