Skip to content

Instantly share code, notes, and snippets.

@peteristhegreat
Last active September 10, 2020 18:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peteristhegreat/fbc1adae62ac1047761fc3aea496f9fd to your computer and use it in GitHub Desktop.
Save peteristhegreat/fbc1adae62ac1047761fc3aea496f9fd to your computer and use it in GitHub Desktop.
Clone a Jira issue from the Jira Rest API

Jira Software (Boards/Sprints etc) and Cloning Issues

The steps to clone an issue in jira are fairly complex. This example has a bash + curl + jq v1.6 tested on Jira Software running inside the jira docker container (tested on Jira v8.12).

This assumes that your favorite/default project is named TRAC. Change where appropriate.

Using the REST API Browser Plugin makes learning these calls much easier.

https://marketplace.atlassian.com/apps/1211542/atlassian-rest-api-browser?hosting=server&tab=overview

Helpful Links

https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#searching-for-issues-examples

https://jqplay.org/

https://community.atlassian.com/t5/Jira-questions/Clone-Issue-in-java-with-rest-api/qaq-p/932948

https://community.atlassian.com/t5/Jira-questions/Clone-issue-using-JIRA-Rest-api/qaq-p/339566

https://community.atlassian.com/t5/Answers-Developer-Questions/Jira-REST-Api-Clone-an-Issue/qaq-p/561045

https://stackoverflow.com/questions/53549596/how-do-i-clone-an-issue-in-jira

https://developer.atlassian.com/server/jira/platform/rest-apis/

Commercial Solutions

https://marketplace.atlassian.com/search?query=clone+issue

https://marketplace.atlassian.com/apps/1213072/repeating-issues?hosting=datacenter&tab=overview

Before running the scripts

  1. Create a custom field of type Radio button named Auto clone this issue with at least one option of When closed. Leave this as an optional field.
  2. Modify the screens for the field, and click the checkbox next to TRAC: Scrum Default Issue Screen.
  3. Create a Basic Authentication scheme or hook up the OAuth authentication for getting a token for API calls.
  4. Download jq 1.6 x64 into your docker container and make it executable.
  5. Run these scripts using bash not dash. (Maybe one day I'll learn to write POSIX compilant scripts instead of using bash-isms)
  6. Now create two scripts in your favorite language that can do curl like GET and POST calls, or download and use the two scripts below on this page.
  7. Go into Jira, modify or create an issue, and set Auto clone this issue to anything besides None and move the now "cloneable" issue into Done.
  8. Run the find_and_clone_issues.sh script and it should clone the issue and put it into the TODO of the active sprint on the board.

Pseudocode of scripts

Most of the GET and POST calls use rest/api/2 and a few calls with rest/agile/latest

For some reason rest/api/3 is missing a bunch of the endpoints I wanted to use.

get the custom field id for `Auto clone this issue`
get the custom field id for `Sprint`

get the list of all issues that have a value set for `Auto clone this issue`

for each cloneable issue in the list

  read the value of the fields: auto_clone_custom_field, key, project, srpint, board, issuelinks, status
  
  continue if the .fields.status.statusCategory.key is not done (aka the clone template is in todo or inprogress)
  
  get all the inward issue ids of the issuelinks of type.name `Cloners`
  
  for each existing clone (inwardIssue.key) in the issuelinks `Cloners`
  
    read just the status.statusCategory.key
    
    if it is not done, continue and skip cloning this cloneable issue
    
    # repeat for all existing clones found
  
  
  if all existing clones are in a done state, and the cloneable is in a done state, then proceed with a clone
  
  scrap a board_id by finding a board that matches the project_id of the clonable
  
  find a sprint_id by finding a sprint on the board_id with the state=active,future
  
  sort the results by startDate and pick the first as the sprint to add to
  
  get a copy of the entire cloneable issue
  strip out null elements
  strip out the following fields:
    comment
    id
    self
    key
    watches
    created
    creator
    votes
    updated
    subtasks
    reporter
    aggregateprogress
    resolution
    timetracking
    attachment
    flagged
    versions
    resolutiondate
    closedSprints
    workratio
    progress
    issuelinks
    worklog
    status
    sprint
    $auto_clone_custom_field_id

  Set the custom_field_id for sprint equal to the sprint_id as a number
  Set the project field as project_id as a number

  Prefix the summary with "CLONE - "

  Create an issue with the json

  Get the new issue key for the cloned issue
  
  Create an issueLink of type Cloners with the inward issue being the clone_issue_key and the outwardIssue being the cloneable_issue_key
  
  Post a comment on the cloneable issue summarizing the action for traceability.
  e.g.  {"body": "This issue has been cloned by $clone_issue_key by the auto clone script."}
    
  # repeat for all cloneable issues found
#!/usr/bin/env bash
source jira_utils.sh
default_board="TRAC"
default_sprint_id=1
echo "Looking up custom field ids needed"
auto_clone_custom_field_id="$(getapi2 field | jq -r '.[] | select(.name == "Auto clone this issue") | .id')"
sprint_custom_field_id="$(getapi2 field | jq -r '.[] | select(.name == "Sprint") | .id')"
# # https://confluence.atlassian.com/jirakb/creating-an-issue-in-a-sprint-using-the-jira-rest-api-875321726.html
# sprint_custom_field_id="$(echo "$issue_json_data" \
# | ./jq-linux64 -r '.fields | to_entries[]
# | select(.key | startswith("custom"))
# | select(.value | type == "array")
# | select(.value[0]
# | startswith("com.atlassian.greenhopper.service.sprint.Sprint"))
# | .key')"
# Find all issues where "Auto clone this issue" != null
issue_key_list=$(getapi2 search?jql=%22Auto%20clone%20this%20issue%22%21%3Dnull | jq -r '.issues[].key' | sort -n)
# print state of each autoclone issue and list of linked issues in each ticket.
for cloneable_issue_key in $issue_key_list; do
echo "Found cloneable issue: $cloneable_issue_key"
cloneable_info="$(getapi2 issue/$cloneable_issue_key?fields=$auto_clone_custom_field_id,key,project,sprint,board,issuelinks,status)"
cloneable_option="$(echo "$cloneable_info" | jq -r --arg auto_clone_custom_field_id "$auto_clone_custom_field_id" '.fields[$auto_clone_custom_field_id].value')"
cloneable_status="$(echo "$cloneable_info" | jq -r '.fields.status.statusCategory.key')"
if [ "$cloneable_option" = "Never" ]; then
# note that default of None returns null, and we are finding all the not-null entries
continue
fi
# get "Cloners" issue links for the cloneable, see if any of the clones are active (todo or in progress)
linked_key_list="$(echo "$cloneable_info" | jq -r '.fields.issuelinks[] | select(.type.name == "Cloners") | .inwardIssue.key')"
echo "Found linked \"is cloned by\" the following issues"
echo "$linked_key_list"
# if issue links are empty or they are all done, then proceed with cloning the issue
all_linked_issues_closed=true
if [ "$cloneable_status" != "done" ]; then
echo "Cloneable issue is not marked done. Giving up on cloning $cloneable_issue_key"
continue
fi
echo "Checking progress of linked issues"
for linked_issue_key in $linked_key_list; do
if [ $linked_issue_key = "null" ] ; then
continue
fi
issue_json_data="$(getapi2 issue/$linked_issue_key?fields=key,status | jq -r '.' )"
status="$(echo "$issue_json_data" | jq -r '.fields.status.statusCategory.key')"
# getapi2 status | jq -r '.[].statusCategory.key'
# in progress aka indeterminate
# todo aka new
# closed aka done
if [ $status != "done" ] ; then
echo "Found a linked issue not in a done state; giving up on cloning $cloneable_issue_key"
all_linked_issues_closed=false
pause
break;
fi
done # end of linked_issue_key
if $all_linked_issues_closed; then
# Make a new clone!
echo "All cloned issues are currently closed... creating a new clone..."
pause
issue_json_data="$(getapi2 issue/$cloneable_issue_key | jq -r '.')"
board_id=$(echo "$issue_json_data" | jq -r '.fields.sprint.originBoardId')
project_id=$(echo "$issue_json_data" | jq -r '.fields.project.id')
sprint_id=$(echo "$issue_json_data" | jq -r '.fields.sprint.id')
if [ "$board_id" == "null" ]; then
# search thru boards based on project
echo "Finding a board id that matches the project $project_id"
board_info="$(getagile board?projectKeyOrId=$project_id | jq -r '.values[0] | {id, name}')"
echo "$board_info"
board_id="$(echo "$board_info" | jq -r '.id')"
echo "$board_id"
fi
if [ "$sprint_id" == "null" ]; then
sprint_id=$default_sprint
echo "Finding the active and future sprints on a board and sorting by start date, and grabbing the first one"
sprint_info="$(getagile board/$board_id/sprint?state=future,active | jq -r '.values |= sort_by(.startDate) | .values[0] | {id, name}')"
echo "$sprint_info"
sprint_id="$(echo "$sprint_info" | jq -r '.id')"
echo "Using sprint_id=$sprint_id"
fi
# deny list additions
echo "Creating initial clone json data"
json_data="$(getapi2 issue/$cloneable_issue_key \
| ./jq-linux64 'delpaths([path(..?) as $p | select(getpath($p) == null) | $p])' \
| ./jq-linux64 -r --arg auto_clone_custom_field_id $auto_clone_custom_field_id \
'delpaths([["fields","comment"],
["fields","id"],
["fields","self"],
["fields","key"],
["fields","watches"],
["fields","created"],
["fields","creator"],
["fields","votes"],
["fields","updated"],
["fields","subtasks"],
["fields","reporter"],
["fields","customfield_10000"],
["fields","aggregateprogress"],
["fields","customfield_10100"],
["fields","resolution"],
["fields","timetracking"],
["fields","customfield_10106"],
["fields","attachment"],
["fields","flagged"],
["fields","versions"],
["fields","resolutiondate"],
["fields","closedSprints"],
["fields","workratio"],
["fields","progress"],
["fields","issuelinks"],
["fields","worklog"],
["fields","status"],
["fields","sprint"],
["fields", $auto_clone_custom_field_id ]
])')"
echo "Adding sprint, and project to the json data"
json_data_with_additions=$(echo "$json_data" | jq -r --arg sprint_id "$sprint_id" \
--arg project_id "$project_id" \
--arg sprint_custom_field_id "$sprint_custom_field_id" \
'.fields += { ($sprint_custom_field_id) : $sprint_id|tonumber , "project": { "id": $project_id|tonumber}}')
echo "Prefixing the summary field with \"CLONE - \""
json_data_with_additions="$(echo "$json_data_with_additions" | jq -r '.fields.summary |= ("CLONE - " + .)')"
echo "New Summary:"
echo "$json_data_with_additions" | jq -r '.fields.summary'
if false; then
echo -e "JSON Data for Clone: /n$json_data_with_additons\n"
fi
echo "Creating issue"
create_issue_resp=$(postapi2 issue "$json_data_with_additions")
echo "$create_issue_resp" | jq -r '. | {key}'
clone_issue_key=$(echo "$create_issue_resp" | jq -r '.key')
echo "Clone issue key: $clone_issue_key"
# Link back to the cloneable
echo "Linking cloneable to the clone"
echo " $clone_issue_key clones $cloneable_issue_key"
echo " aka $cloneable_issue_key is cloned by $clone_issue_key"
json_data="$(cat << HEREDOC
{
"type": {
"name": "Cloners"
},
"inwardIssue": {
"key": "$clone_issue_key"
},
"outwardIssue": {
"key": "$cloneable_issue_key"
}
}
HEREDOC
)"
issue_link_resp="$(postapi2 issueLink "$json_data")"
echo "$issue_link_resp" | jq -r '.'
echo "Marking cloneable $cloneable_issue_key with a comment of what has happened"
json_data="$(cat << HEREDOC
{
"body": "This issue has been cloned by $clone_issue_key by the auto clone script."
}
HEREDOC
)"
comment_resp="$(postapi2 issue/$cloneable_issue_key/comment "$json_data")"
echo "$comment_resp" | jq -r '. | {body,created}'
echo -e "/n/nDone cloning a jira issue"
fi
done # end of cloneable_issue_key
#!/usr/bin/env bash
# Leaving auth credentials in plain text or even in base64 encoding is not secure.
# Consider heavily restricting access to this script and make it execute only, without read, or switching to the stronger OAuth approach
# At a minimum only store the base64 version of the combination
username=user@company.com
password=badpassword
auth="$(echo -n "$username:$password" | base64 )"
url=localhost:8080
function pause(){
read -n 1 -r -s -p $'Press enter to continue...\n'
}
function postapi2(){
api_path="$1"
json_data_input="$2"
verb="${3:-POST}"
curl -sb -D- -H "Authorization: Basic $auth" \
-X $verb -H 'Content-Type: application/json' \
-d "$(echo "$json_data_input" | expand | jq -r '.')" \
http://$url/rest/api/2/$api_path
}
function postapi3(){
api_path="$1"
json_data_input="$2"
verb="${3:-POST}"
curl -sb -D- -H "Authorization: Basic $auth" \
-X $verb -H 'Content-Type: application/json' \
-d "$(echo "$json_data_input" | expand | jq -r '.')" \
http://$url/rest/api/3/$api_path
}
function postagile(){
api_path="$1"
json_data_input="$2"
verb="${3:-POST}"
curl -sb -D- -H "Authorization: Basic $auth" \
-X $verb -H 'Content-Type: application/json' \
-d "$(echo "$json_data_input" | expand | jq -r '.')" \
http://$url/rest/agile/latest/$api_path
# http://host:port/context/rest/api-name/api-version/resource-name
# https://jira.example.com/rest/agile/latest/board/123
}
function getapi2(){
api_path="$1"
query="$2"
curl -sb -D- -H "Authorization: Basic $auth" \
-X GET -H 'Content-Type: application/json' \
http://$url/rest/api/2/$api_path$query
}
function getapi3(){
api_path="$1"
query="$2"
curl -sb -D- -H "Authorization: Basic $auth" \
-X GET -H 'Content-Type: application/json' \
http://$url/rest/api/3/$api_path$query
}
function getagile(){
api_path="$1"
query="$2"
curl -sb -D- -H "Authorization: Basic $auth" \
-X GET -H 'Content-Type: application/json' \
http://$url/rest/agile/latest/$api_path$query
# http://host:port/context/rest/api-name/api-version/resource-name
# https://jira.example.com/rest/agile/latest/board/123
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment