Getting Started

JIRA Extension Points

Administrative Functions

Copy Custom Field Values

For each issue returned by a query, this will copy values from one custom field to another. This is useful if you want to convert the type of a custom field.

If the two custom fields have different types, you may not be able to use this. The following conversions are handled:

  • Single to multi, eg single select to multi select, single user picker to multi user picker.

  • Multi to single, however only the first value will be retained.

  • Multi to text…​ the values are concatenated with a comma.

  • Short text to unlimited text

For reference, here is how you can script this functionality yourself to run in the script console:

// Define a JQL query for the issues on which you want to copy custom field values
def query = 'project = FOO'

// Here you can specify the names of the fields you want to copy from and into
def sourceFieldName = 'Assignee'
def targetFieldName = 'My User Field'

// We retrieve a list of all fields in this JIRA instance
def fields = get("/rest/api/2/field")
        .asObject(List)
assert fields.status == 200

List<Map> allFields = fields.body
// Now we lookup the field IDs
Map sourceField = allFields.find { it.name == sourceFieldName }
Map targetField = allFields.find { it.name == targetFieldName }

assert sourceField : "No field found with name '${sourceFieldName}'"
assert targetField : "No field found with name '${targetFieldName}'"

// Search for the issues we want to update
def searchReq = get("/rest/api/2/search")
        .queryString("jql", query)
        .queryString("startAt", 0)
        .queryString("maxResults", 100) // If we search for too many issues we'll reach the 30s script timeout
        .queryString("fields", "${sourceField.key},${targetField.key}")
        .queryString("expand", "names,schema")
        .asObject(Map)
assert searchReq.status == 200

Map searchResult = searchReq.body

// A useful helper method
def sanitiseValue(fieldValue) {
    // If we strip ids and self links out, we can set options by their values
    if (fieldValue instanceof Map) {
        fieldValue.remove('id')
        fieldValue.remove('self')
    }
    if (fieldValue instanceof List || fieldValue.class?.isArray()) {
        fieldValue.each {
            sanitiseValue(it)
        }
    }
}
// Each field type stores its value in a different way, we allow some conversion between types here
def getValue(sourceValue, String sourceType, String targetType) {
    if (sourceType == targetType) {
        return sourceValue
    }
    if (sourceType == 'option' && targetType == 'array') {
        return [sourceValue]
    }

    if (sourceType == 'option' && targetType == 'string') {
        return (sourceValue as Map).value
    }

    if (sourceType == 'array' && (targetType == 'option' || targetType == 'user')) {
        return (sourceValue as List)[0]
    }
    if (sourceType == 'array' && targetType == 'string') {
        return (sourceValue as List<Map>).collect { it.value }.join(',')
    }

    if (sourceType == 'string' && targetType == 'option') {
        return [value: sourceValue]
    }

    if (sourceType == 'string' && targetType == 'array') {
        return [[value:sourceValue]]
    }
    if (sourceType == 'user' && targetType == 'array') {
        return [sourceValue]
    }
    sourceValue
}

String sourceType = (sourceField.schema as Map).type
String targetType = (targetField.schema as Map).type
def count = 0
def errors = ''

// Now we iterate through the search results
searchResult.issues.each { Map issue ->
    def issueFields = issue.fields as Map
    def sourceFieldValue = issueFields[sourceField.key]

    if (sourceFieldValue) {
        // If there is a field value in the source field we try and convert it into a format that
        // the target field will understand
        sanitiseValue(sourceFieldValue)
        def updateDoc = [fields: [
                (targetField.key): getValue(sourceFieldValue, sourceType, targetType)
        ]]

        // Now we make the change, ignoring whether the field exists on the edit screen
        def resp = put("/rest/api/2/issue/${issue.key}")
                .queryString("overrideScreenSecurity", true)
                .header("Content-Type", "application/json")
                .body(updateDoc)
                .asObject(Map)
        if (resp.status > 299) {
            logger.error("Failed to update ${issue.key}: ${resp.statusText} - ${resp.body}")
            def errorMessages = (resp.body as Map).errorMessages as List<String>
            errors += "\nERROR: ${issue.key}: ${errorMessages?.join(',')}"
        } else {
            count++
        }
    }
}
logger.info("Updated '${targetFieldName}' on ${count} issues")
errors

Bulk Fix Resolutions

This will change the resolution on all issues returned by a query you provide. Search request result is limited to up 1000 issues. Resolutions can often be wrong after an incorrect import, or after modifying the workflow.

To use it you’d create a filter that selects all issues with the bad resolution, then choose filter and select the resolution you want to change it to and then Run.