Getting Started

JIRA Extension Points

The Script Console is the place for running one-off ad hoc scripts, and for learning and experimenting with the JIRA REST API from ScriptRunner.

script console

Run As User

Code run from the Script Console can make requests back to JIRA using either the ScriptRunner Add-on user or the current user. See the Run As User section of Workflow Extensions for more information.

Samples

Take one of these samples and use it as a starting point for your own needs.

Get JIRA Version

Starting with a very simple script to read the JIRA version and display it in the console. API Reference.

get('/rest/api/2/serverInfo') (1)
        .queryString('doHealthCheck', 'true') (2)
        .asObject(Map) (3)
        .body (4)
        .version (5)
  1. This is a get request to the serverInfo resource

  2. Just as an example we see how to add a query string parameter of doHeathCheck and set it to true

  3. asObject(Map) makes the request and converts the response into a Map

  4. Calling .body on the result of asObject(Map) returns a Map representation of the JSON response

  5. We now read the version property of the resulting Map

Show Issue Counts for Projects Based on JQL

Now let’s perform a JQL search to find all stories and group them into projects to determine which projects have the most stories

Map<String, Object> searchResult = get('/rest/api/2/search')
        .queryString('jql', 'issuetype = Story')
        .queryString('fields', 'project')
        .asObject(Map)
        .body (1)

def issues = (List<Map<String, Object>>) searchResult.issues (2)

issues.groupBy { issue -> (3)
    ((Map<String, Map>) issue.fields).project.key
}.collectEntries { pKey, issueList -> (4)
    [(pKey): issueList.size()]
}.toString() (5)
  1. Perform the JQL search, specifying we are interested in the project field from the issue

  2. Grab all the issues from the search

  3. Take each issue and group them by project key

  4. The resulting list can be transformed into pairs of project key and the number of issues with that project key

  5. Turn the result into a String for display

Now the display of this isn’t great. It would be nice to output a html table so lets do that using Groovy’s MarkupBuilder.

Map<String, Object> searchResult = get('/rest/api/2/search')
        .queryString('jql', 'issuetype = Story')
        .queryString('fields', 'project')
        .asObject(Map)
        .body

def issues = (List<Map<String, Object>>) searchResult.issues

def mapping = issues.groupBy { issue -> (1)
    ((Map<String, Map>) issue.fields).project.key
}.collectEntries { pKey, issueList ->
    [(pKey): issueList.size()]
}

import static io.github.openunirest.http.Unirest.get

def writer = new StringWriter()
def builder = new MarkupBuilder(writer) (2)
builder.table(class: "aui") {
    thead {
        tr {
            th("Project Key")
            th("Count")
        }
    }
    tbody {
        mapping.each { projectKey, count ->
            tr {
                td {
                    b(projectKey)
                }
                td(count)
            }
        }
    }
}

return writer.toString()
  1. Up until this point everything is the same, except we assign the result to mapping

  2. A new MarkupBuild is created, note the import. The markup builder takes advantage of Groovy’s meta-programming so the static type checking will cause errors, this is nothing to be worried about.

Update an issue

Another common task is to update a field of an issue. In this case we set the summary to be a new summary

def issueKey = 'TP-1' (1)
def newSummary = 'Updated by a script'

def result = put("/rest/api/2/issue/${issueKey}") (2)
    //.queryString("overrideScreenSecurity", Boolean.TRUE) (3)
    .header('Content-Type', 'application/json') (4)
    .body([
        fields: [
                summary: newSummary
        ]
    ])
    .asString() (5)

if (result.status == 204) { (6)
    return 'Success'
} else {
    return "${result.status}: ${result.body}"
}
  1. Issue key and new summary to set

  2. Create the rest PUT request - see documentation

  3. You must pass overrideScreenSecurity=true if you are trying to amend fields that are not visible on the screen - Note you must use the Add-on user when setting overrideScreenSecurity=true

  4. Important to set the content type of the PUT, and then the body content as a Groovy Map

  5. Calling .asString() executes the put and parses the result as a string

  6. The REST request responds with a 204 (no content) so there is no point reading the body

Update a resolution on an issue

This example shows how you can update the resolution on an issue and could be used to set the resolution on an issue which was closed with no resolution specified.

// The issue to be updated
def issueKey = '<IssueKeyHere>' (1)

// The Name of the resolution to be set
def resolutionName = '<ResolutionNameHere>'

def result = put('/rest/api/2/issue/' + issueKey) (2)
        .queryString("overrideScreenSecurity", Boolean.TRUE) (3)
        .header('Content-Type', 'application/json') (4)
        .body([
        fields:[
                resolution:[name:resolutionName] (5)
        ]
])
        .asString() (6)

if (result.status == 204) { (7)
    return 'Success'
} else {
    return "${result.status}: ${result.body}"
}
  1. The issue key for the issue to be updated.

  2. Create the rest PUT request - see documentation

  3. Here we must pass the overrideScreenSecurity=true query string as we are updating a field that is not on a screen - Note: - you must use the Add-on user when setting overrideScreenSecurity=true.

  4. Important to set the content type of the PUT, and then the body content as a Groovy Map

  5. Pass in an array which contains the name of the resolution to be set.

  6. Calling .asString() executes the put and parses the result as a string

  7. The REST request responds with a 204 (no content) so there is no point reading the body

Bulk update multiple issue resolutions

This example extends the previous example and shows how you can update the resolution on multiple issues and could be used to set the resolution on issues which have been closed with no resolution specified.

// Define a JQL query to search for the issues on which you want to update the resolution
def query = '<JQLQueryHere>' (1)

// The Name of the resolution to be set
def resolutionName = '<ResolutionNameHere>'

// Search for the issues we want to update
def searchReq = get("/rest/api/2/search") (2)
        .queryString("jql", query)
        .queryString("fields", "resolution")
        .asObject(Map)

// Verify the search completed successfully
assert searchReq.status == 200

// Save the search results as a Map
Map searchResult = searchReq.body (3)

// Iterate through the search results and update the resolution for each issue returned
searchResult.issues.each { Map issue -> (4)
    // Log out what the value from the resolution field was for the original issue.
    logger.info("The original resolution was ${issue.fields.resolution ?: 'null'} for the ${issue.key} issue.")

    def result = put("/rest/api/2/issue/${issue.key}") (5)
            .queryString("overrideScreenSecurity", Boolean.TRUE) (6)
            .header('Content-Type', 'application/json') (7)
            .body([
            fields:[
                    resolution:[name:resolutionName] (8)
            ]
    ])
            .asString()  (9)

    // Log out the issues updated or which failed to update
    if (result.status == 204) { (10)
        logger.info("Resolution set to ${resolutionName} for the ${issue.key} issue")
    } else {
        logger.warn("Failed to set the resolution to ${resolutionName} on the ${issue.key} issue. ${result.status}: ${result.body}")
    }
}  // end of loop

return "Script Completed - Check the Logs tab for information on which issues were updated."
  1. The JQL search that will be used to return the list of issue keys.

  2. The rest call to execute the JQL search and return the issue keys

  3. Save the results returned from the JQL search to a map

  4. Loop over each of each issue key returned in the search results.

  5. Create the rest PUT request - see documentation

  6. Here we must pass the overrideScreenSecurity=true query string as we are updating a field that is not on a screen - Note: - you must use the Add-on user when setting overrideScreenSecurity=true.

  7. Important to set the content type of the PUT, and then the body content as a Groovy Map

  8. Pass in an array which contains the name of the resolution to be set.

  9. Calling .asString() executes the put and parses the result as a string

  10. The REST request responds with a 204 (no content) so there is no point reading the body

Adding a User or Group to a Project Role

Say we would like to add a user to a group. This can be quickly achieved using the following assuming that the user, group, project and role all exist

def accountId = '123456:12345a67-bbb1-12c3-dd45-678ee99f99g0'
def groupName = 'jira-core-users'
def projectKey = 'TP'
def roleName = 'Developers'

def roles = get("/rest/api/2/project/${projectKey}/role")
        .asObject(Map).body (1)

String developersUrl = roles[roleName] (2)

assert developersUrl != null

def result = post(developersUrl)
    .header('Content-Type', 'application/json')
    .body([
            user: [accountId], (3)
            group: [groupName]
    ])
    .asString()

assert result.status == 200
result.statusText
  1. First all the roles for the project are fetched

  2. Then the url for the specified role is found to use to post to

  3. In this case we have a group and a user to add, user and group must be arrays

Extracting the value from a select list custom field

This example shows how you can extract the value that has been specified inside a select list field on an issue.

// The issue key
def issueKey = '<IssueKeyHere>'

// Fetch the issue object from the key
def issue = get("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .asObject(Map)
        .body

// Get all the fields from the issue as a Map
def fields = issue.fields as Map

// Get the Custom field to get the option value from
def customField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
                (it as Map).name == '<CustomFieldNameHere>'
        } as Map

// Extract and store the option from the custom field
def value = (fields[customField.id] as Map).value

// Return the option value
return "The value of the select list from the ${customField.name} custom field is: ${value}"

Extracting the values from a multi select list custom field

This example shows how you can extract the values that have been specified inside a multi select list field on an issue.

// The issue key
def issueKey = '<IssueKeyHere>'

// Fetch the issue object from the key
def issue = get("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .asObject(Map)
        .body

// Get all the fields from the issue as a Map
def fields = issue.fields as Map

// Get the Custom field to get the option value from
def customField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
    (it as Map).name == '<CustomFieldNameHere>'
} as Map

// Extract and store the option from the custom field
def values = fields[customField.id] as List<Map>

// Get each of the values from the multi select list field and store them
def fieldValues = values.collect {
    it.value
}

// Return the option values
return "The values of the multi select list from the ${customField.name} custom field are: ${fieldValues}"

Extracting the value from a radio button custom field

This example shows how you can extract the value that has been specified inside a radio button field on an issue.

// The issue key
def issueKey = '<IssueKeyHere>'

// Fetch the issue object from the key
def issue = get("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .asObject(Map)
        .body

// Get all the fields from the issue as a Map
def fields = issue.fields as Map

// Get the Custom field to get the option value from
def customField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
    (it as Map).name == '<CustomFieldNameHere>'
} as Map

// Extract and store the option from the radio buttons custom field
def radioButtonValue = (fields[customField.id] as Map).value

// Return the option value
return "The value of the radio buttons from the ${customField.name} custom field is: ${radioButtonValue}"

Extracting the values from a checkbox custom field

This example shows how you can extract the values that have been specified inside a checkbox field on an issue.

// The issue key
def issueKey = '<IssueKeyHere>'

// Fetch the issue object from the key
def issue = get("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .asObject(Map)
        .body

// Get all the fields from the issue as a Map
def fields = issue.fields as Map

// Get the Custom field to get the option value from
def customField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
    (it as Map).name == '<CustomFieldNameHere>'
} as Map

// Extract and store the option from the custom field
def checkboxValues = fields[customField.id] as List<Map>

// Get each of the values from the checkbox field and store them
def checkboxFieldValues = checkboxValues.collect {
    it.value
}

// Return the option values
return "The values of the checkboxes from the ${customField.name} custom field are: ${checkboxFieldValues}"

This example shows how you can create a link to an external URL on an issue.

// The url for the link
def linkURL = '<LinkURLHere>'

// the title for the link
def linkTitle = '<LinkTitleHere>'

// The issue key
def issueKey = '<IssueKeyHere>'

// Create the link on the specified issue
def result = post("/rest/api/2/issue/${issueKey}/remotelink")
        .header('Content-Type', 'application/json')
        .body([
        object: [
                title:linkTitle,
                url:linkURL
        ]

])
        .asObject(String)

// Check if the link created succesfully
if (result.status == 201) {
    return "Remote link with name of ${linkTitle} which links to ${linkURL} created successfully"
} else {
    return "${result.status}: ${result.body}"
}

Flagging an issue as an impediment

This example shows how you can flag an issue as an impediment.

// Specify Issue Key here
def issueKey = '<IssueKeyHere>'

// Look up the custom field ID for the flagged field
def flaggedCustomField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
    (it as Map).name == 'Flagged'
} as Map

// Update the issue setting the flagged field
def result = put("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .body([
        fields:[
                // The format below specifies the Array format for the flagged field
                // More information on flagging an issue can be found in the documentation at:
                // https://confluence.atlassian.com/jirasoftwarecloud/flagging-an-issue-777002748.html
                (flaggedCustomField.id): [ // Initialise the Array
                                              [ // set the component value
                                                value: "Impediment",
                                              ],

                ]
        ]

])
        .asString()

// Check if the issue was updated correctly
if (result.status == 204) {
    return 'Success - Issue was updated by a script and issue flagged.'
} else {
    return "${result.status}: ${result.body}"
}

Bulk set Flag on multiple issues

This example extends the previous example and shows how you can set the Impediment flag on multiple issues.

// Define a JQL query to search for the issues on which you want to set the impediment flag
def query = '<JQLQueryHere>' (1)

// Look up the custom field ID for the flagged field
def flaggedCustomField = get("/rest/api/2/field") (2)
        .asObject(List)
        .body
        .find {
    (it as Map).name == 'Flagged'
} as Map

// Search for the issues we want to update
def searchReq = get("/rest/api/2/search") (3)
        .queryString("jql", query)
        .queryString("fields", "Flagged")
        .asObject(Map)

// Verify the search completed successfully
assert searchReq.status == 200

// Save the search results as a Map
Map searchResult = searchReq.body (4)

// Iterate through the search results and set the Impediment flag for each issue returned
searchResult.issues.each { Map issue -> (5)

    def result = put("/rest/api/2/issue/${issue.key}") (6)
            .queryString("overrideScreenSecurity", Boolean.TRUE) (7)
            .header('Content-Type', 'application/json') (8)
            .body([
            fields:[
                    // The format below specifies the Array format for the flagged field
                    // More information on flagging an issue can be found in the documentation at:
                    // https://confluence.atlassian.com/jirasoftwarecloud/flagging-an-issue-777002748.html
                    // Initialise the Array
                    (flaggedCustomField.id): [ (9)
                                                  [ // set the component value
                                                    value: "Impediment",
                                                  ],

                    ]
            ]
    ])
            .asString() (10)

    // Log out the issues updated or which failed to update
    if (result.status == 204) { (11)
        logger.info("The ${issue.key} issue was flagged as an Impediment. ")
    } else {
        logger.warn("Failed to set the Impediment flag on the ${issue.key} issue. ${result.status}: ${result.body}")
    }
}  // end of loop

return "Script Completed - Check the Logs tab for information on which issues were updated."
  1. The JQL search that will be used to return the list of issue keys.

  2. The rest call to look up the ID of the 'Flagged' custom field

  3. The rest call to execite the JQL search and return the issue keys

  4. Save the results returned from the JQL search to a map

  5. Loop over each of each issue key returned in the search results.

  6. Create the rest PUT request - see documentation

  7. Here we must pass the overrideScreenSecurity=true query string as we are updating a field that is not on a screen - Note: - you must use the Add-on user when setting overrideScreenSecurity=true.

  8. Important to set the content type of the PUT, and then the body content as a Groovy Map

  9. Pass in an array which contains the Impediment value to be set on the 'Flagged' field.

  10. Calling .asString() executes the put and parses the result as a string

  11. The REST request responds with a 204 (no content) so there is no point reading the body

Create a SubTask

This example shows how you can create a subtask below for a specified parent issue.

// Specify the key of the parent issue here
def parentKey = '<ParentIssueKeyHere>'

// Get the parent issue type
def issueResp = get("/rest/api/2/issue/${parentKey}") (1)
        .asObject(Map)
assert issueResp.status == 200

// get the body of the parent issue type
def issue = issueResp.body as Map

// Get the issue types for the instance
def typeResp = get('/rest/api/2/issuetype') (2)
        .asObject(List)
assert typeResp.status == 200
def issueTypes = typeResp.body as List<Map>


// Here we set the basic subtask issue details
def summary = "Subtask summary" // The summary to use for
def issueType = "Sub-task" // The Sub Task Issue Type to Use

// Get the sub task issue type to use
def issueTypeId = issueTypes.find { it.subtask && it.name == issueType }?.id (3)
assert issueTypeId : "No subtasks issue type found called '${issueType}'"

// Get the project to create the subtask in
def project = (issue.fields as Map).project

// Create the subtask
def resp = post("/rest/api/2/issue") (4)
        .header("Content-Type", "application/json")
        .body(
        fields: [
                project: project,
                issuetype: [
                        id: issueTypeId
                ],
                parent: [
                        id: issue.id
                ],
                summary: summary
        ])
        .asObject(Map)

// Get and validate the newly created subtask
def subtask = resp.body
assert resp.status >= 200 && resp.status < 300 && subtask && subtask.key != null

// If the sub task created successfully return a success message along with its key
if (resp.status == 201) { (5)
    return 'Success - Sub Task Created with the key of ' + resp.body.key.toString()
} else {
    return "${resp.status}: ${resp.body}"
}
  1. Get the issue object for the specified parent issue

  2. Get a list of all of the issue types for the instance

  3. Lookup the ID for the specified sub task issue type

  4. Create the sub task below the parent issue specified

  5. Validate that the sub task created successfully and display a message if it did.

Set Due Date Field Value

This example shows how you can set the due date field on an issue.

// Specify the issue key to update
def issueKey = '<IssueKeyHere>'

// Get today's date to set as the due date
def today = new Date()

// Update the issue
def result = put("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .body([
        fields:[
                // Set the due date to today's date
                duedate: today.format('yyyy-MM-dd') as String
        ]
])
        .asString()

// Validate the issue updated correctly
if (result.status == 204) {
    return "Success - The issue with the key of ${issueKey} has been updated with a new due date"
} else {
    return "${result.status}: ${result.body}"
}

Set Custom Date Field Value

This example shows how you can set a custom date picker field on an issue.

// Specify the issue key to update
def issueKey = '<IssueKeyHere>'

// Get today's date to set as the due date
def today = new Date()

// Specify the name of the date picker field to set
def datePickerFieldName = '<DatePickerFieldNameHere>'

// Get the Custom field to get the option value from
def customField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
            (it as Map).name == datePickerFieldName
        } as Map
// Check if the custom field returns a valid field and is not null
assert customField != null : "Cannot find custom field with name of: ${datePickerFieldName}"

// Update the issue
def result = put("/rest/api/2/issue/${issueKey}")
        .header('Content-Type', 'application/json')
        .body([
            fields:[
                // Set the custom date picker field date to today's date
                (customField.id): today.format('yyyy-MM-dd') as String
            ]
        ])
        .asString()

// Validate the issue updated correctly
if (result.status == 204) {
    return "Success - The issue with the key of ${issueKey} has been updated with a new date in the ${datePickerFieldName} field."
} else {
    return "${result.status}: ${result.body}"
}

Set Select List Field Value

This example shows how you can set the value of a single select list field on a issue.

// Specify the issue key to update
def issueKey = '<IssueKeyHere>'

// Specify the name of the select list field to set
def selectListFieldName = '<SelectListFieldNameHere>'

// Get the Custom field to get the option value from
def customField = get("/rest/api/2/field")
        .asObject(List)
        .body
        .find {
            (it as Map).name == selectListFieldName
        } as Map
// Check if the custom field returns a valid field and is not null
assert customField != null : "Cannot find custom field with name of: ${selectListFieldName}"

def result = put("/rest/api/2/issue/${issueKey}")
        // Uncomment the line below if you want to set a field which is not pressent on the screen. Note - If using this you must run the script as the ScriptRunner Add-On User.
        //.queryString("overrideScreenSecurity", Boolean.TRUE)
        .header('Content-Type', 'application/json')
        .body([
            fields: [
                (customField.id):[value: "<OptionValueHere>"] as Map
            ]
        ])
        .asString()

if (result.status == 204) {
    return "The ${customField.name} select list field was successfully updated on the ${issueKey} issue"
} else {
    return "${result.status}: ${result.body}"
}

Create a Confluence page with a Label

This example shows how you can create a page inside of a Confluence instance and add a label to the newly created page.

Note: This example requires that you have both ScriptRunner for Jira Cloud and ScriptRunner for Confluence Cloud installed. If you do not have ScriptRunner for Confluence Cloud installed then you will need to update this example to specify user credentials to access the Confluence instance.

// Specify the id of the parent page that the new page will be created under
def parentPageId = "<PageIDHere>"

//Specify a title for the new page
def pageTitle = "<PageTitleHere>"

// Specify the space key of the spcae that the new page will be created in
def spaceKey = "<SpaceKeyHere>"


// Specify the body of the page in storage format - below is some example storage format.
def storageFormat = """<h1>A page created by ScriptRunner</h1>
                        <p>The first line of my page.</p>
                        <p>The second line of my page</p>""" (1)

// Specify the body of the rest request
def body = [ (2)
             type: "page",
             title: pageTitle,
             space: [
                     key: spaceKey
             ],
             ancestors: [[
                                 id: parentPageId
                         ]],
             body:[
                     storage:[
                             value: storageFormat,
                             representation: "storage"
                     ]
             ]
]

//create confluence (cloud) page
def createPageResult = post("/wiki/rest/api/content") (3)
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .body(body)
        .asObject(Map)
// Assert that the new page created successfully
assert createPageResult.status == 200 : "Failed to create the page"


// Add some labels to the newly created Confluence page
def addLabels = post("/wiki/rest/api/content/${createPageResult.body.id}/label") (4)
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .body(
        [
                // Note you can specify a comma seperated lists of strings here if you wish to add multiple labels to a page.
                // An example of adding more than 1 label is "name"  : "ALabel,label2"
                "name"  : "<LabelNameHere>",
                "prefix": "global"
        ])
        .asString()
// Assert that the label was added to the page correctly
assert addLabels.status == 200 : "Failed to add labels to the page"

// Return the result of the created page
return "Confluence page created with the name of: ${pageTitle} \n" + createPageResult
  1. Here we specify the content to be added to the page using the 'Storage Format' for Confluence.

  2. Here we specify the body that is passed into the rest call to create the Confluence page.

  3. The rest API call to create the Confluence page..

  4. The rest API call to add a label to the newly created page.

Copy versions to a new Project

This example shows how you can copy a set of versions from one project to another project.

Note: This example will only copy versions where a version with the same name does not exist in the target project.

// Specify the master project  to get the versions form
def masterProjectKey = "<ProjectKeyHere>"

// Specify the key of the project to copy the version to
def projectToCopyVersionTo = "<ProjectKeyHere>"

// get the project versions
def versions = get("/rest/api/2/project/${masterProjectKey}/versions")
        .header('Content-Type', 'application/json')
        .asObject(List).body  (1)

// Loop over each version returned and create a version in the new project
versions.each {   (2)

    boolean archivedValue = it.archived
    boolean releasedValue = it.released

    // Get todays date as a string for any date properties which dont have a value set
    def today = new Date().format('yyyy-MM-dd').toString()

    // Declare some variables to store the start and release date values
    def startDateValue;
    def releaseDateValue

    // Get the start date  and if it is null set it to todays date as a start date is required when creating a version in a new project
    if (it.startDate != null) {
        startDateValue = it.startDate.toString()
    } else {
        startDateValue =  today
    }

    // Get the release date  and if it is null set it to todays date as a release date is required when creating a version in a new project
    if (it.releasedDate != null) {
        releaseDateValue = it.startDate.toString()
    } else {
        releaseDateValue = today
    }

    // Make the rest call to create the version
    logger.info("Copying the version with the name of: ${it.name.toString().replaceAll("\\[", "").replaceAll("\\]", "")}")
    def createVersion = post("/rest/api/2/version")
            .header("Content-Type", "application/json") (3)
            .body([
                "description": it.description.toString()?: "", // Get the desceription and pass an emtpy string if it is null
                "name": it.name.toString()?: "", // Get the name and pass an emtpy string if it is null
                "archived": archivedValue, // Get the archived value and pass an emtpy string if it is null
                "released": releasedValue, // Get the released  value and pass an emtpy string if it is null
                "startDate": startDateValue,
                "releaseDate": releaseDateValue,
                "project": projectToCopyVersionTo
            ]).asString()
}

return "Versions Copied. Check the 'Logs' tab for more details"
  1. Here we make a rest API call to get the versions to be copied as a List object.

  2. Here we loop over each version and ensure that we have the values in the correct format to pass to the rest call to create the version.

  3. Here we make a rest API call to create the current version.