Skip to content

Commit

Permalink
[FIXED JENKINS-13652] Add ability to update issues by workflow actions.
Browse files Browse the repository at this point in the history
A new build step that takes a JQL query, and for each matching issue, performs
a configurable workflow step (such as "Resolve Issue").

Optionally adds a comment to each issue as it is updated.
  • Loading branch information
Joe Hansche committed May 30, 2012
1 parent 571fb36 commit 90c466a
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 1 deletion.
137 changes: 137 additions & 0 deletions src/main/java/hudson/plugins/jira/JiraIssueUpdateBuilder.java
@@ -0,0 +1,137 @@
package hudson.plugins.jira;

import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Result;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.FormValidation;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.xml.rpc.ServiceException;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

/**
* Build step that will mass-update all issues matching a JQL query, using the specified workflow
* action name (e.g., "Resolve Issue", "Close Issue").
*
* @author Joe Hansche <jhansche@myyearbook.com>
*/
public class JiraIssueUpdateBuilder extends Builder {
private final String jqlSearch;
private final String workflowActionName;
private final String comment;

@DataBoundConstructor
public JiraIssueUpdateBuilder(String jqlSearch, String workflowActionName, String comment) {
this.jqlSearch = Util.fixEmptyAndTrim(jqlSearch);
this.workflowActionName = Util.fixEmptyAndTrim(workflowActionName);
this.comment = Util.fixEmptyAndTrim(comment);
}

/**
* @return the jql
*/
public String getJqlSearch() {
return jqlSearch;
}

/**
* @return the workflowActionName
*/
public String getWorkflowActionName() {
return workflowActionName;
}

/**
* @return the comment
*/
public String getComment() {
return comment;
}

/**
* Performs the actual update based on job configuration.
*/
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
String realComment = Util.fixEmptyAndTrim(build.getEnvironment(listener).expand(comment));
String realJql = Util.fixEmptyAndTrim(build.getEnvironment(listener).expand(jqlSearch));

JiraSite site = JiraSite.get(build.getProject());

if (site == null) {
listener.getLogger().println(Messages.Updater_NoJiraSite());
build.setResult(Result.FAILURE);
return true;
}

listener.getLogger().println(Messages.JiraIssueUpdateBuilder_UpdatingWithAction(workflowActionName));
listener.getLogger().println("[JIRA] JQL: " + realJql);

try {
if (!site.progressMatchingIssues(realJql, workflowActionName, realComment, listener.getLogger())) {
listener.getLogger().println(Messages.JiraIssueUpdateBuilder_SomeIssuesFailed());
build.setResult(Result.UNSTABLE);
}
} catch (ServiceException e) {
listener.getLogger().println(Messages.JiraIssueUpdateBuilder_Failed());
e.printStackTrace(listener.getLogger());
return false;
}

return true;
}

@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}

/**
* Descriptor for {@link JiraIssueUpdateBuilder}.
*/
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
/**
* Performs on-the-fly validation of the form field 'Jql'.
*
* @param value This parameter receives the value that the user has typed.
* @return Indicates the outcome of the validation. This is sent to the browser.
*/
public FormValidation doCheckJqlSearch(@QueryParameter String value) throws IOException, ServletException {
if (value.length() == 0) {
return FormValidation.error(Messages.JiraIssueUpdateBuilder_NoJqlSearch());
}

return FormValidation.ok();
}

public FormValidation doCheckWorkflowActionName(@QueryParameter String value) {
if (Util.fixNull(value).trim().length() == 0) {
return FormValidation.error(Messages.JiraIssueUpdateBuilder_NoWorkflowAction());
}

return FormValidation.ok();
}

public boolean isApplicable(Class<? extends AbstractProject> klass) {
return true;
}

/**
* This human readable name is used in the configuration screen.
*/
public String getDisplayName() {
return Messages.JiraIssueUpdateBuilder_DisplayName();
}
}
}
80 changes: 80 additions & 0 deletions src/main/java/hudson/plugins/jira/JiraSession.java
Expand Up @@ -6,12 +6,15 @@
import hudson.plugins.jira.soap.RemoteGroup;
import hudson.plugins.jira.soap.RemoteIssue;
import hudson.plugins.jira.soap.RemoteIssueType;
import hudson.plugins.jira.soap.RemoteNamedObject;
import hudson.plugins.jira.soap.RemoteProject;
import hudson.plugins.jira.soap.RemoteProjectRole;
import hudson.plugins.jira.soap.RemoteStatus;
import hudson.plugins.jira.soap.RemoteValidationException;
import hudson.plugins.jira.soap.RemoteVersion;

import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
Expand Down Expand Up @@ -259,4 +262,81 @@ public void migrateIssuesToFixVersion(String projectKey, String version, String
service.updateIssue(token, issue.getKey(), new RemoteFieldValue[] { value });
}
}

/**
* Progresses the issue's workflow by performing the specified action. The issue's new status is returned.
*
* @param issueKey
* @param workflowActionName
* @param fields
* @return The new status
* @throws RemoteException
*/
public String progressWorkflowAction(String issueKey, String workflowActionName, RemoteFieldValue[] fields)
throws RemoteException {
LOGGER.fine("Progressing issue " + issueKey + " with workflow action: " + workflowActionName);
RemoteIssue issue = service.progressWorkflowAction(token, issueKey, workflowActionName, fields);
return getStatusById(issue.getStatus());
}

/**
* Returns the matching action id for a given action name.
*
* @param issueKey
* @param workflowAction
* @return The action id, or null if the action cannot be found.
* @throws RemoteException
*/
public String getActionIdForIssue(String issueKey, String workflowAction) throws RemoteException {
RemoteNamedObject[] actions = service.getAvailableActions(token, issueKey);

for (RemoteNamedObject action : actions) {
if (action.getName().equalsIgnoreCase(workflowAction)) {
return action.getId();
}
}

return null;
}

/**
* Returns the status name by status id.
*
* @param statusId
* @return
* @throws RemoteException
*/
public String getStatusById(String statusId) throws RemoteException {
String status = getKnownStatuses().get(statusId);

if (status == null) {
LOGGER.warning("JIRA status could not be found: " + statusId + ". Checking JIRA for new status types.");
knownStatuses = null;
// Try again, just in case the admin has recently added a new status. This should be a rare condition.
status = getKnownStatuses().get(statusId);
}

return status;
}

private HashMap<String, String> knownStatuses = null;

/**
* Returns all known statuses.
*
* @return
* @throws RemoteException
*/
private HashMap<String,String> getKnownStatuses() throws RemoteException {
if (knownStatuses == null) {
RemoteStatus[] statuses = service.getStatuses(token);
knownStatuses = new HashMap<String, String>(statuses.length);

for (RemoteStatus status : statuses) {
knownStatuses.put(status.getId(), status.getName());
}
}

return knownStatuses;
}
}
53 changes: 52 additions & 1 deletion src/main/java/hudson/plugins/jira/JiraSite.java
Expand Up @@ -10,8 +10,10 @@
import hudson.plugins.jira.soap.RemoteVersion;

import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -22,6 +24,7 @@
import java.util.regex.Pattern;

import javax.xml.rpc.ServiceException;

import org.kohsuke.stapler.DataBoundConstructor;

/**
Expand All @@ -40,7 +43,7 @@ public class JiraSite {
* See issue JENKINS-729, JENKINS-4092
*/
protected static final Pattern DEFAULT_ISSUE_PATTERN = Pattern.compile("([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\\.[^0-9]|\\.$|$)");

/**
* URL of JIRA, like <tt>http://jira.codehaus.org/</tt>.
* Mandatory. Normalized to end with '/'
Expand Down Expand Up @@ -439,6 +442,54 @@ public void migrateIssuesToFixVersion(String projectKey, String versionName, Str

session.migrateIssuesToFixVersion(projectKey, versionName, query);
}

/**
* Progresses all issues matching the JQL search, using the given workflow action. Optionally
* adds a comment to the issue(s) at the same time.
*
* @param jqlSearch
* @param workflowActionName
* @param comment
* @param console
* @throws IOException
* @throws ServiceException
*/
public boolean progressMatchingIssues(String jqlSearch, String workflowActionName, String comment, PrintStream console) throws IOException,
ServiceException {
JiraSession session = createSession();

if (session == null) {
console.println(Messages.Updater_FailedToConnect());
return false;
}

boolean success = true;
RemoteIssue[] issues = session.getIssuesFromJqlSearch(jqlSearch);

for (int i = 0; i < issues.length; i++) {
String issueKey = issues[i].getKey();

String actionId = session.getActionIdForIssue(issueKey, workflowActionName);

if (actionId == null) {
LOGGER.fine("Invalid workflow action " + workflowActionName + " for issue " + issueKey + "; issue status = " + issues[i].getStatus());
console.println(Messages.JiraIssueUpdateBuilder_UnknownWorkflowAction(issueKey, workflowActionName));
success = false;
continue;
}

String newStatus = session.progressWorkflowAction(issueKey, actionId, null);

console.println("[JIRA] Issue " + issueKey + " transitioned to \"" + newStatus
+ "\" due to action \"" + workflowActionName + "\".");

if (comment != null && !comment.isEmpty()) {
session.addComment(issueKey, comment, null, null);
}
}

return success;
}

private static final Logger LOGGER = Logger.getLogger(JiraSite.class.getName());
}
@@ -0,0 +1,11 @@
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry title="${%JQL Query}" field="jqlSearch">
<f:textbox/>
</f:entry>
<f:entry title="${%Workflow Action}" field="workflowActionName">
<f:textbox/>
</f:entry>
<f:entry title="${%Comment}" field="comment">
<f:textarea/>
</f:entry>
</j:jelly>
@@ -0,0 +1,5 @@
<div>
An optional comment to be added to the issue after updating the workflow. If left empty, no comment will be added.
<p />
This can contain <strong>$PARAM</strong> values which will be replaced by the build parameters.
</div>
@@ -0,0 +1,12 @@
<div>
Issues which match this JQL Query will be progressed using the specified workflow action.
<p />
This can contain <strong>$PARAM</strong> values which will be replaced by the build parameters.

<p>
Example:
<blockquote><pre>project = JENKINS and fixVersion = "$RELEASE_VERSION" and status not in (Resolved, Closed)</pre></blockquote>
or (e.g., combined with a JIRA Issue Parameter, selecting one issue from a JQL result set):
<blockquote><pre>issue = $ISSUE_ID</pre></blockquote>
</p>
</div>
@@ -0,0 +1,5 @@
<div>
The workflow action to be performed on the selected JIRA issues.
<p />
Be mindful of the issues being selected by the JQL query, because not all actions are valid for all issue statuses.
</div>
@@ -0,0 +1,8 @@
<div>
Performs a JIRA workflow action for every issue that matches the JQL query.
A common use might be to consider a ticket "confirmed" in the last build step
of a job, or to mark an issue as "merged" if the job is used to merge changes
from one SCM repository to another.
<p />
Optionally, include a comment that will be attached to those tickets that are modified as a result of this build step.
</div>
7 changes: 7 additions & 0 deletions src/main/resources/hudson/plugins/jira/Messages.properties
Expand Up @@ -11,3 +11,10 @@ Updater.NoRemoteAccess=The system configuration does not allow remote JIRA acces
Updater.Updating=Updating {0}
JiraReleaseVersionBuilder.DisplayName=Mark a JIRA Version as Released
JiraReleaseVersionMigrator.DisplayName=Move issues matching JQL to the specified version
JiraIssueUpdateBuilder.DisplayName=Progress JIRA issues by workflow action
JiraIssueUpdateBuilder.NoJqlSearch=Please set the JQL used to select the issues to update.
JiraIssueUpdateBuilder.NoWorkflowAction=A workflow action is required.
JiraIssueUpdateBuilder.UpdatingWithAction=[JIRA] Updating issues using workflow action {0}.
JiraIssueUpdateBuilder.Failed=[JIRA] An error occurred while progressing issues:
JiraIssueUpdateBuilder.UnknownWorkflowAction=[JIRA] Unable to update issue {0}: invalid workflow action "{1}".
JiraIssueUpdateBuilder.SomeIssuesFailed=[JIRA] At least one issue failed to update. See log above for more details.

0 comments on commit 90c466a

Please sign in to comment.