Hi - I'm evaluating Bulk Clone Professional, and need to clone our "template" Epic. This Epic contains a handful of Tasks ("Issues in Epic"). Each of these Tasks in the Epic have a bunch of Sub-Tasks. How do I clone this entire structure? When I clone this "template" Epic using Bulk Clone Professional, I only specify this single Epic as needing to be cloned, because I want to end up with a cloned Epic that contains the same (cloned) Tasks as the source Epic contains, and have each of these (cloned) Tasks point to (cloned) Sub-Tasks.
So in the pic attached, the 5 "Issues in Epic" would also be cloned, and listed in the cloned Epic's "Issues in Epic" section. Also, I need each of the Sub-Tasks (of each Task) to also be cloned. So a full Epic clone.
Thanks!
Ok, I figured it out. Instead of assuming that just cloning the "top level" Epic, I need to use JQL (project = MIG and "Epic Link" = MIG-84 or issuekey = MIG-84) - where MIG-84 is the Epic - and clone the Epic and all of the "Issues in Epic" together. Very cool plug-in.
Where do you place the JQL? The Backlog area is the only place you can multi-select (I think) and if I do a JQL search, I am taken to a Search results page where I cannot multi-select.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi Kristina - this is Fred, posting by mistake from my manager's account (oops ;_0 ) ... so I go to my project, and click on "view all issues and filters" in the upper right. Make sure the "Advanced" link is clicked, and create a filter that selects the epic you want to clone - along with all of the tasks in that epic. So for me, it's:
project = MIG AND "Epic Link" = MIG-84 OR issuekey = MIG-84 ORDER BY issuetype ASC, issuekey ASC
Then I save that filter. When you're ready to clone that epic (with its tasks and task sub-tasks), run that filter (by going to your project, clicking on "view all issues and filters", and clicking on the filter you just saved).
You'll now see the epics and tasks in that epic. You won't see sub-tasks but they're cloned automatically with each task. Now just follow the add-on instructions (go to tools -> bulk tools, change all X issues, and in bulk operations select all of them, click next, then select the add-on "clone issues", and you're off and running).
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Logged in as myself (Fred) now - let me know if you have any other questions on this!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks - trying it out. Template Cloner is SO much simpler: https://marketplace.atlassian.com/plugins/com.vilisoft.jira.plugins.template-cloner/server/overview
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
I was able to create JQL and see the list I wanted. I don't see tools > bulk tools etc. I'm trying to follow http://www.lbconsultinggroup.org/bulkclone-professional-documentation/ which recommends going to Backlog...I don't want that.
Couldn't see how to save the filter except to save it to a local file.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
When you build up the filter (using either 'basic' or 'advanced'), there is a "Save As" button that you can click to save that filter.
We chose Bulk Clone Pro because we could clone Epics and their hierarchies across many different JIRA Projects.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks for all of your help. Our needs were simpler; may need this in the future though...
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@Fred Lunau FYI - we ended up going with Jira Automation with a trigger on Epic creation to create all of the issues "below" the Epic. It had more flexibility to create if/then scenarios for our agency model with multiple products plus the ability to suffix or prefix the issue names. I needed a coder to assist with the setup though.
Found out that we couldn't use the Epic cloning tool anymore because it would trigger Jira Automation. Oops. I'm sure there's a checkbox for that somewhere.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi,
Try out our FAQ here: http://www.lbconsultinggroup.org/bulk-clone-professional-frequently-asked-questions-faq/
Or product documentation here: http://www.lbconsultinggroup.org/bulkclone-professional-documentation/
Should be up to date.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi,
I needed the same some time ago. I got a solution with 2 scriptrunner REST Endpoints called sequentially. The first is called from a custom web item (Script Fragments) in the more-menu of an issue. It shows a dialog where you can set the summary/epic name of the new epic. After clicking submit, the second endpoint is called.
Here are the scripts for the two REST Endpoints:
Dialog
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
@BaseScript CustomEndpointDelegate delegate
showDialog() { MultivaluedMap queryParams ->
//get issueKey from parameters of REST Endpoint call
def issueKey = queryParams.getFirst("issueKey") as String
String BASE_URL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL);
//definition of Dialog with some Java Script
def dialog =
"""<script>
//On dialog the user can insert the summary/epic name of the new epic
//After clicking submit button, spinner is displayed and second REST Endpoint is called with new summary value
AJS.\$('[id="dialog-submit-button"]').click(function(){
var newSum = AJS.\$("#text-input").val()
if(newSum!=""){
AJS.\$("#text-input").parent().find(".error").hide()
var spinning = false;
if(!spinning){
AJS.\$(this).text('Cloning...');
AJS.\$('.button-spinner').spin();
spinning = true;
AJS.\$("#dialog-submit-button").attr("disabled",true)
}
var request = new XMLHttpRequest();
request.open("GET","${BASE_URL}/rest/scriptrunner/latest/custom/cloneEpicWithIssues?issueKey=${issueKey}&newSum="+newSum);
request.send();
//Keep the dialog open for 15s, then close it and reload the source epic
setTimeout(function(){
AJS.\$('#dialog-close-button').trigger('click');
function loadUrl(newLocation){
window.location.href = newLocation;
};
loadUrl('${BASE_URL}/browse/${issueKey}');
},15000);
}else{
//show error message if field for new summary is empty
AJS.\$("#text-input").next().append('<div class="error">Summary for new Epic is required</div>')
}
});
</script>
//Dialog definition, got from scriptrunner documentation and adapted
<section role="dialog" id="sr-dialog" class="aui-layer aui-dialog2 aui-dialog2-medium" aria-hidden="true" data-aui-remove-on-hide="true">
<header class="aui-dialog2-header">
<h2 class="aui-dialog2-header-main">Clone Epic with Issues in Epic</h2>
<a class="aui-dialog2-header-close">
<span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>
</a>
</header>
<div class="aui-dialog2-content">
<p>Please insert Summary of new Epic</p>
<form class="aui">
<div class="field-group">
<label for="text-input">New Epic Name<span class="aui-icon icon-required">required</span></label>
<input class="text" type="text" id="text-input" name="text-input" title="summary">
<div class="description">Please insert Name of new Epic</div>
</div>
</form>
</div>
<footer class="aui-dialog2-footer">
<div class="aui-dialog2-footer-actions">
<button id="dialog-submit-button" class="aui-button aui-button-primary">Submit</button>
<span style="padding-left: 15px; padding-bottom: 10px; display: inline-block;" class="button-spinner"></span>
<button id="dialog-close-button" class="aui-button aui-button-link">Cancel</button>
</div>
</footer>
</section>
"""
//Show dialog
Response.ok().type(MediaType.TEXT_HTML).entity(dialog.toString()).build()
}
Clone Epic:
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.sal.api.user.UserManager
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.Response
import org.apache.log4j.Category
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.user.util.UserUtil
import com.atlassian.jira.config.properties.APKeys
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
import java.sql.Timestamp;
import org.apache.log4j.Logger;
@BaseScript CustomEndpointDelegate delegate
cloneEpicWithIssues(httpMethod: "GET") { MultivaluedMap queryParams,body, HttpServletRequest request ->
// Variable definition
def issueKey = queryParams.getFirst("issueKey") as String // issuekey from request parameters
def newSummary = queryParams.getFirst("newSum") as String // newSummary value from request parameters
def epic_link_field="customfield_12739";
def epic_name_field="customfield_12738";
def issueMgr = ComponentAccessor.getIssueManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def linkMgr = ComponentAccessor.getIssueLinkManager()
def issueFactory = ComponentAccessor.getIssueFactory()
Issue issue = issueMgr.getIssueObject(issueKey)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
Timestamp Ts = new Timestamp(System.currentTimeMillis());
CustomField epicLink = customFieldManager.getCustomFieldObject(epic_link_field);
CustomField epicName = customFieldManager.getCustomFieldObject(epic_name_field);
//Get current issues in epic -> links from and to source epic
List<IssueLink> outwardLinks = linkMgr.getOutwardLinks(issue.getId())
List<IssueLink> inwardLinks = linkMgr.getInwardLinks(issue.getId())
List<Issue> outwardLinkedIssues = [] as List<Issue>;
List<Issue> inwardLinkedIssues = [] as List<Issue>;
outwardLinks.each{
outwardLinkedIssues.add(it.getDestinationObject())
}
inwardLinks.each{
inwardLinkedIssues.add(it.getSourceObject())
}
//sorting of epic links - not neccessary, was requirement from users
outwardLinkedIssues.sort{a,b -> a.getId()<=>b.getId()}
outwardLinkedIssues = outwardLinkedIssues.reverse()
inwardLinkedIssues.sort{a,b -> a.getId()<=>b.getId()}
inwardLinkedIssues = inwardLinkedIssues.reverse();
ModifiedValue mVal;
MutableIssue clonedIssue;
MutableIssue clonedEpic;
try {
//Clone epic with all fields
def toClone = issueFactory.cloneIssueWithAllFields(issue)
clonedEpic = issueMgr.createIssueObject(issue.reporter, toClone) as MutableIssue
//set current system time as created date, otherwise it would have created date from source epic
//issue.store() is deprecated but issueManager.updateIssue didn't work, don't know why
clonedEpic.setCreated(Ts)
clonedEpic.store();
//Set new summary and epic name (same in this case)
clonedEpic.setSummary(newSummary)
clonedEpic.store();
mVal = new ModifiedValue(clonedEpic.getCustomFieldValue(epicName), newSummary);
epicName.updateValue(null, clonedEpic, mVal, new DefaultIssueChangeHolder());
//if source epic has outward links, clone each linked issue and set epic link to new epic
//For sub-tasks, this is unfortunately not working. Therefore they are excluded
if(!outwardLinks.isEmpty()){
outwardLinkedIssues.each{
if(it.issueType.name!="Sub-task"){
toClone=issueFactory.cloneIssueWithAllFields(it)
clonedIssue = issueMgr.createIssueObject(issue.reporter, toClone) as MutableIssue
clonedIssue.setCreated(Ts)
clonedIssue.store();
mVal = new ModifiedValue(clonedIssue.getCustomFieldValue(epicLink), clonedEpic);
epicLink.updateValue(null, clonedIssue, mVal, new DefaultIssueChangeHolder());
}
}
}
if(!inwardLinks.isEmpty()){
inwardLinkedIssues.each{
if(it.issueType.name!="Sub-task"){
toClone=issueFactory.cloneIssueWithAllFields(it)
clonedIssue = issueMgr.createIssueObject(issue.reporter, toClone) as MutableIssue
clonedIssue.setCreated(Ts)
clonedIssue.store();
mVal = new ModifiedValue(clonedIssue.getCustomFieldValue(epicLink), clonedEpic);
epicLink.updateValue(null, clonedIssue, mVal, new DefaultIssueChangeHolder());
}
}
}
}catch(e){
return Response.serverError().entity([error: e.message]).build();
}
String BASE_URL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL);
//show success message and load new epic - doesn't work in some cases, don't know why
String clonedMessage = """
Navigating to created issue <a href="${BASE_URL}/browse/${clonedEpic.getKey()}">${clonedEpic.getKey()}</a>...
<script>
function loadUrl(newLocation){
window.location.href = newLocation;
};
loadUrl('${BASE_URL}/browse/${clonedEpic.getKey()}');
</script>
"""
UserMessageUtil.success(clonedMessage)
}
Maybe not the best solution, but it is working for us. At least useful as a workaround until Atlassian maybe is providing such a function in the standard JIRA application in the future.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hello,
Can you please share the Custom Web Item configuration?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hello,
Getting below error while executing this code. Could you please help me in fixing this?
2019-01-25 10:36:38,866 ERROR [common.UserCustomScriptEndpoint]: ************************************************************************************* 2019-01-25 10:36:38,873 ERROR [common.UserCustomScriptEndpoint]: Script endpoint failed on method: GET cloneEpicWithIssues java.lang.NullPointerException: Cannot invoke method getId() on null object at Script5$_run_closure1.doCall(Script5.groovy:48) at com.onresolve.scriptrunner.runner.rest.common.UserCustomScriptEndpoint.doEndpoint(UserCustomScriptEndpoint.groovy:375) at com.onresolve.scriptrunner.runner.rest.common.UserCustomScriptEndpoint.getUserEndpoint(UserCustomScriptEndpoint.groovy:256)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi,
here is a screenshot of the web item configuration.
I think you get the error message as you didn't provide the issue. In the configuration you do that in the "Link" field.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hello Jonas,
Thanks for your reply.
I configured in the same way. But the issue is not created. Could you please help me with this? Is this something related to condition?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi,
do you have a REST Endpoint configured that is called "showDialog"? It is called in the URL with the issue as parameter. It is defined in the first code snippet above. This dialog calls the second Endpoint if you click on "Submit" so it is necessary for this to work that both Endpoint exist.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Yes and No error though. When I click on Submit it is redirected to the same source issue and when I search for new epic, unable to find.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi @Nikhil
ok, understood.
The ApplicationUser appuser part was just an old script part which isn't used anymore. It was for testing purposes with checks the current user. But it isn't actually used anymore.
ApplicationUser appUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();
Best regards,
Jonas
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi @Nikhil
ok, this seems like the screen just isn't displayed correctly.
Problem is that I can't really help here as we actually don't use this code for cloning epics with issues. We currently use the free app "Epic Clone" which is doing exactly the same.
So, maybe that would be an alternative for you as well.
Best,
Jonas
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi @Jonas M
Thanks for the ScriptRunner code it helped me form a solution to our needs. I have since modified the code due to the ScriptRunnner breaking change 5.6.7 Some_Groovy_Classes_Backwards-incompatible , as cloneIssue is no longer possible with versions of ScriptRunner >=5.6.7
Our remit was similar but we just needed to capture and restrict a few extra fields. So my dialogue captures:
The script then has to clone the epic and all ticket linked to the epic, removing all:
and the only actual copied fields are the original description and a custom field called "Support Subject" ( custom field ID: "customfield_10501" ).
I specifically add the text "CLONED:" to the beginning of all the issues linked to the epic as opposed to using the word "CLONE:" so that I can easily identify issues cloned with the script rather than those cloned using the Jira method.
I also slightly altered the dialogue to give feedback whilst cloning. Here is what we have:
Dialog:
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.customfields.manager.OptionsManager
import com.atlassian.jira.config.properties.APKeys
@BaseScript CustomEndpointDelegate delegate
v3showDialogCloneEpic( httpMethod: "GET", groups: ["jira-users"] ) { MultivaluedMap queryParams ->
//get issueKey from parameters of REST Endpoint call
def issueKey = queryParams.getFirst("issueKey") as String
if ( issueKey == null ) {
return Response.status(Response.Status.NOT_FOUND).entity("No issue entered").build()
}
String BASE_URL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL);
CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
def CustomField customerFieldList = customFieldManager.getCustomFieldObjectsByName('Customer')[0]
def OptionsManager optionsManager = ComponentAccessor.getOptionsManager()
IssueManager issueMgr = ComponentAccessor.getIssueManager()
MutableIssue issue = issueMgr.getIssueObject(issueKey)
def fieldConfig = customerFieldList.getRelevantConfig(issue)
def allAvailableCustomers = optionsManager.getOptions(fieldConfig)
//definition of Dialog with some Java Script
def dialog =
"""
<script>
function loadUrl(newLocation) {
AJS.\$("#dialog-close-button").trigger('click');
window.location.href = newLocation;
}
//On dialog the user can insert the summary/epic name of the new epic
//After clicking submit button, spinner is displayed and second REST Endpoint is called with new summary and name values
function submitClone() {
var newSum = AJS.\$("#text-input-sum").val()
var newName = AJS.\$("#text-input-name").val()
var newDueDate = AJS.\$("#escalus-input-date").val()
var newCustomer = AJS.\$("#escalus-customer-select").val()
if(newSum != "" && newName != "" && newDueDate != "" && newCustomer != ""){
var spinning = false;
if(!spinning){
AJS.\$('#dialog-submit-button').text('Cloning...');
AJS.\$('.button-spinner').spin();
spinning = true;
AJS.\$("#dialog-submit-button").attr("disabled",true)
}
var request = new XMLHttpRequest();
request.open("GET","${BASE_URL}/rest/scriptrunner/latest/custom/v4CloneEpicAndLinkedIssues?issueId=${issueKey}&summary="+newSum+"&name="+newName+"&due="+newDueDate+"&customer="+newCustomer);
request.responseType = 'json'
request.send();
request.onload = function() {
if (request.status == 200) {
var responseObj = request.response
loadUrl('${BASE_URL}/browse/'+responseObj.newEpic)
} else {
loadUrl('${BASE_URL}/browse/${issueKey}');
}
}
} else {
//show error message if field for new summary is empty
AJS.messages.error(".aui", {
title: '',
body: '<p>All fields are required before you can submit</p>',
fadeout: true
});
}
};
</script>
//Dialog definition, got from scriptrunner documentation and adapted
<section role="dialog" id="sr-dialog" class="aui-layer aui-dialog2 aui-dialog2-medium" aria-hidden="true" data-aui-remove-on-hide="true">
<header class="aui-dialog2-header">
<h2 class="aui-dialog2-header-main">Clone Epic with Issues in Epic</h2>
<a class="aui-dialog2-header-close">
<span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>
</a>
</header>
<div class="aui-dialog2-content">
<p>Please insert Summary & Name of new Epic</p>
<form class="aui">
<div class="field-group">
<label for="text-input-sum">New Epic Summary<span class="aui-icon icon-required">required</span></label>
<input class="text" type="text" id="text-input-sum" name="text-input-sum" title="summary">
</div>
<div class="field-group">
<label for="text-input-name">New Epic Name<span class="aui-icon icon-required">required</span></label>
<input class="text" type="text" id="text-input-name" name="text-input-name" title="name">
</div>
<div class="field-group">
<label for="escalus-input-date">Due Date<span class="aui-icon icon-required">required</span></label>
<input id="escalus-input-date" name="escalus-input-date" type="date" />
</div>
<div class="field-group">
<label for="escalus-customer-select">New Epic Customer<span class="aui-icon icon-required">required</span></label>
<input list="escalus-customers" id="escalus-customer-select" name="escalus-customer-select" />
<datalist id="escalus-customers">
</datalist>
</div>
</form>
</div>
<footer class="aui-dialog2-footer">
<div class="aui-dialog2-footer-actions">
<button id="dialog-submit-button" class="aui-button aui-button-primary" onclick="submitClone()">Submit</button>
<span style="padding-left: 15px; padding-bottom: 10px; display: inline-block;" class="button-spinner"></span>
<button id="dialog-close-button" class="aui-button aui-button-link">Cancel</button>
</div>
</footer>
</section>
<script>
var addOptions = '${allAvailableCustomers}'.substring(1, '${allAvailableCustomers}'.length-1).split(', ');
var customerlist = document.getElementById("escalus-customers");
if(customerlist.options.length == 0) {
addOptions.forEach(function(newOption) {
var option = document.createElement("option");
option.value = newOption;
customerlist.appendChild(option);
})
};
</script>
"""
//Show dialog
Response.ok().type(MediaType.TEXT_HTML).entity(dialog.toString()).build()
}
Clone Epic:
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import groovy.transform.Field
import org.apache.log4j.Category
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import java.sql.Timestamp
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.config.properties.APKeys
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
import com.onresolve.scriptrunner.canned.jira.workflow.postfunctions.CloneIssue
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import com.atlassian.jira.issue.IssueFactory
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.util.ImportUtils
import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.issue.customfields.option.Option
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.onresolve.scriptrunner.canned.jira.utils.ConditionUtils
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
Category log = Category.getInstance("com.onresolve.jira.groovy")
log.setLevel(org.apache.log4j.Level.INFO)
@BaseScript CustomEndpointDelegate delegate
v4CloneEpicAndLinkedIssues( httpMethod: "GET", groups: ["jira-users"] )
{ MultivaluedMap queryParams ->
String contextIssue = queryParams.getFirst("issueId")
String newSummary = queryParams.getFirst("summary")
String newName = queryParams.getFirst("name")
String unformatedDate = queryParams.getFirst("due")
Timestamp newDueDate = new Timestamp(Date.parse('yyyy-MM-dd', unformatedDate).getTime())
String newCustomer = queryParams.getFirst("customer")
if ( contextIssue == null ) {
return Response.status(Response.Status.NOT_FOUND).entity("No issue entered").build()
}
log.info(">>> Cloning EPIC: ${contextIssue} with data => Summary: ${newSummary}, EpicName: ${newName}, Due: ${newDueDate}, Customer: ${newCustomer}")
def epicQuery = jqlQueryParser.parseQuery("project IN (IMP) AND key = ${contextIssue}")
// def issuesQuery = jqlQueryParser.parseQuery("project IN (TIMP) AND key in (TIMP-2, TIMP-48) ORDER BY key ASC") // reduced second query for debuging
def issuesQuery = jqlQueryParser.parseQuery("project IN (IMP) AND 'Epic Link' = ${contextIssue} ORDER BY Rank ASC")
def epicSearch = searchService.search(user, epicQuery, PagerFilter.getUnlimitedFilter())
def issueSearch = searchService.search(user, issuesQuery, PagerFilter.getUnlimitedFilter())
def newEpic = cloneOriginalIssues(epicSearch, user, contextIssue, null, newSummary, newName, newDueDate, newCustomer)
def searchTotal = cloneOriginalIssues(issueSearch, user, contextIssue, newEpic, null, newName, newDueDate, newCustomer)
String BASE_URL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL)
//show success message and load new epic - doesn't work in some cases, don't know why
String json = '{"newEpic": "' + newEpic.getKey() + '" }'
return Response.ok(json, "application/json").build();
String clonedMessage = """
Navigating to created issue <a href="${BASE_URL}/browse/${newEpic.getKey()}">${newEpic.getKey()}</a>...
<script>
clearTimeout(myTimeout);
AJS.\$('#dialog-close-button').trigger('click');
function loadUrl(newLocation){
window.location.href = newLocation;
};
loadUrl('${BASE_URL}/browse/${newEpic.getKey()}');
</script>
"""
}
// get option list for radio button, checkbox and select custom fields
def List<Option> getOptions(Issue issue, CustomField customField, List<String> optionList) {
def config = customField.getRelevantConfig(issue)
def options = ComponentAccessor.getOptionsManager().getOptions(config)
def optionsToSelect = options.findAll { it.value in optionList }
return optionsToSelect // not sure about this as untested and missing from source solution
}
// get just the first option of options for cascading select
def Map<String, Object> getCascadingOptions(Issue issue, CustomField customField, String selectedOption) {
def parentOptionObj = getOptions(issue, customField, [selectedOption]).get(0) as Option
Map<String,Object> newValues = new HashMap<>()
newValues.put(null, parentOptionObj)
return newValues
}
Issue cloneOriginalIssues(SearchResults searchTransform, ApplicationUser user, String contextIssue, Issue newEpic, String summary, String newEpicName, Timestamp newDueDate, String newCustomer) {
IssueManager issueManager = ComponentAccessor.getIssueManager()
def issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)
CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
IssueService issueService = ComponentAccessor.getComponent(IssueService)
def cloneResult
def MutableIssue updateIssue
searchTransform.results.each { documentIssue ->
def oldIssue = issueManager.getIssueObject(documentIssue.id)
def additionalCode
Category log = Category.getInstance("com.onresolve.jira.groovy")
log.setLevel(org.apache.log4j.Level.INFO)
if(summary == null) {
additionalCode = """
checkLink = {link -> false}
checkAttachment = {attachment -> false}
"""
} else {
additionalCode = """
issue.setSummary("CLONED: epic")
checkLink = {link -> false}
checkAttachment = {attachment -> false}
"""
}
Map<String, Object> inputs = [
(ConditionUtils.FIELD_ADDITIONAL_SCRIPT): [additionalCode, ""],
(CloneIssue.FIELD_COPY_COMMENTS) : false,
(CloneIssue.FIELD_LINK_TYPE) : null,
(CloneIssue.SKIP_EPIC_LINKS) : false,
(CloneIssue.FIELD_SELECTED_FIELDS) : ["description","customfield_10501"],
] as Map<String, Object>
def executionContext = [
issue: oldIssue,
]
def cloningIssue = ScriptRunnerImpl.scriptRunner.createBean(CloneIssue)
cloneResult = cloningIssue.execute(inputs, executionContext)
updateIssue = issueManager.getIssueObject((String) cloneResult.newIssue)
def locale = ComponentAccessor.getLocaleManager().getLocaleFor(user)
def watcherManager = ComponentAccessor.getWatcherManager()
def watchers = watcherManager.getWatchers(updateIssue, locale)
watchers.each { watcher ->
watcherManager.stopWatching(watcher, updateIssue)
}
// set up new required fields for all issues
CustomField customer = customFieldManager.getCustomFieldObjectsByName('Customer')[0]
updateIssue.setCustomFieldValue(customer, getCascadingOptions(updateIssue, customer, newCustomer))
updateIssue.setDueDate(newDueDate)
if (newEpic == null) {
// this is the new epic so reset the epic name
CustomField epicName = customFieldManager.getCustomFieldObjectsByName('Epic Name')[0]
updateIssue.setCustomFieldValue(epicName, newEpicName)
updateIssue.setSummary("${summary}")
} else {
// this issue is not an epic so set up a new epic link
CustomField epicLink = customFieldManager.getCustomFieldObjectsByName('Epic Link')[0]
updateIssue.setCustomFieldValue(epicLink, newEpic)
}
issueManager.updateIssue(user, updateIssue, EventDispatchOption.DO_NOT_DISPATCH, false)
boolean wasIndexing = ImportUtils.isIndexIssues();
ImportUtils.setIndexIssues(true);
issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)
issueIndexingService.reIndex(updateIssue)
log.warn(">>> Cloned ${oldIssue} --> ${updateIssue.toString()}")
}
return updateIssue
}
Also found this resource invaluable:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi sir,
I would like to ask if you can provide scrrenshots what will be the output based on the script that you provided. If you can also scrrenshot the Rest Endpoint configuration and web item .
Appreciate your help on this.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Folks, if you have the latest version of the JIRA CLI installed (I have Atlassian CLI 8.0.0), then this command below will clone everything listed below:
jira --action cloneIssues --jql "project=ABC and 'Epic Link' = ABC-123 or IssueKey = ABC-123" --copySubtasks --copyLinks
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hello,
Can you please share the custom web item configurations?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi everyone,
You can use Clone Plus for Jira to clone an epic, its issues, and their subtasks with estimates also. Here is a quick guide on how to do so: link
I'm the product manager for the app.
If you have questions or feedback about Clone Plus for Jira, please get in touch with us directly.
Regards,
Nishanth T
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
We have support for this also in Bulk Clone Professional since version 4.7.10 and works perfectly with scriptrunner. For more examples see here: https://www.lbconsultinggroup.org/bulkclone-professional-documentation/
scroll down to ”Jira Software Section” and you find examples both for Epics and Porfolio Parent Links
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi,
This is EPIC CLONE free app and it worked perfectly.
by IJ-Solutionsfor Jira Server 7.6.0 - 8.2.3
https://marketplace.atlassian.com/apps/1220519/epic-clone?hosting=server&tab=overview
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi Fred
But is the EPIC-Link also changed?
JQL: key = TEST-1756 OR "EPIC Link"= TEST-1756 ORDER BY issuetype ASC
The issues in the list are from different projects.
Then I do the Clone and select all the issues.
I select the following issues in the 3rd step:
Everything looks fine - except the Epic Link. This is still to the old Epic.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Yes, I noticed that that is the behavior for non-admins. When I ran it as an admin it copied the links correctly into the new EPIC. This is a serious detraction of the cloning tool, IMO.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Online forums and learning are now in one easy-to-use experience.
By continuing, you accept the updated Community Terms of Use and acknowledge the Privacy Policy. Your public name, photo, and achievements may be publicly visible and available in search engines.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.