Script Field Tips

It’s always safe to return null from a script, regardless of the searcher. If you don’t want the calculated value to appear you should return null.

The number searcher expects a java.lang.Double from the issue, so make sure you cast appropriately if you are using the number searcher. The previewer has some smarts to show you if you have screwed up. In this example I am returning the issue.id which is a Long.

I can fix this by using the script: issue.id as Double

The consequences of getting this wrong are relatively severe.

  1. If you do a full reindex the indexer will stop when it gets to this issue. If this happens either fix the script or disable the plugin, and reindex.

  2. You won’t be able to transition this issue anymore.

All this means is you should test well, preferably in a test instance. If you have a complex script add error handling so that any exceptions are caught and logged.

User Fields

If you use the User Picker (single user) or User Picker (multiple users) templates you need to return an ApplicationUser, or List of ApplicationUser in the multiuser case. For Example:

groovy
import com.atlassian.jira.user.ApplicationUsers ApplicationUsers.from(issue.reporter)

Stack Overflows

In a calculated custom field, you cannot under any circumstances try to get the value of the same calculated custom field, otherwise the same script will execute, which will call it again, and so on, leading to a stack overflow error. So you can’t do that, or call any method which will try to evaluate all of the custom fields on the issue. If you do this, you will crash your Jira.

JQL Searches in Script Fields

We do not recommend running JQL searches in scripted fields. The JQL search index is not available when running a full re-index, causing delays for re-indexes, including JQL search scripted fields.

Check Index Availability

Before a full re-index, you must ensure scripted fields containing JQL searches do not get indexed. Use indexLifecycleManager.isIndexAvailable() to check the index is available. For example:

/* The JQL search index is not available when running a "Lock Jira and rebuild index" re-index. If you try to run JQL searches, a 30-second index delay occurs for every attempt. To avoid this use indexLifecycleManager.isIndexAvailable() to check the index is available. Note: To add the values of scripted fields that use JQL searches to the JQL index, you must run a background re-index. */ import com.atlassian.jira.util.index.IndexLifecycleManager import org.apache.log4j.Logger import org.apache.log4j.Level import com.atlassian.jira.component.ComponentAccessor def log = Logger.getLogger(getClass()) log.setLevel(Level.INFO) def indexLifecycleManager = ComponentAccessor.getComponent(IndexLifecycleManager) // Use this if block to exit the script and return nothing when running a "Lock Jira and rebuild index" re-index if (!indexLifecycleManager.isIndexAvailable()) { def fieldName = getBinding()?.customField?.name log.info("The JQL index is not available for searching during a \"Lock Jira and rebuild index\" so this Script field [$fieldName] is not indexed.") return } // Run the rest of your scripted field logic below here.

Consider whether you need to do a JQL search in a script field and if you do ensure the field context is not wider than it need be, e.g. only apply to the relevant projects and issue types. The screens are irrelevant when considering a full re-index.

Reindex All Issue with Script Fields

After conducting a full re-index, run a background re-index to add the values of the fields that did not get re-indexed during the full lock re-index. To background reindex all issues containing script fields you can use the following script, which you could either run in the console or attach to the ReindexAllCompletedEvent using a script listener:

Jira 7

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.issue.search.SearchQuery
import com.atlassian.jira.issue.util.IssueIdsIssueIterable
import com.atlassian.jira.jql.builder.JqlQueryBuilder
import com.atlassian.jira.jql.query.IssueIdCollector
import com.atlassian.jira.task.context.LoggingContextSink
import com.atlassian.jira.task.context.PercentageContext
import org.apache.log4j.Level

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
def issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)
def issueManager = ComponentAccessor.getIssueManager()

/*
 Loop through all script fields and get their contexts
 Create a query and execute it
 Reindex all results
*/

def builder = JqlQueryBuilder.newBuilder().where().defaultOr()

def scriptFields = customFieldManager.getCustomFieldObjects().findAll {
    it.customFieldType.descriptor.completeKey == 'com.onresolve.jira.groovy.groovyrunner:scripted-field'
}

if (scriptFields.find {
    it.allProjects && it.allIssueTypes
}) {
    log.warn("A full background reindex is required at least one script field has global scope")
} else {
    scriptFields.each {
        def clauseBuilder = JqlQueryBuilder.newClauseBuilder().defaultAnd()

        if (!it.allProjects) {
            clauseBuilder.project(*it.associatedProjectObjects*.id).buildClause()
        }
        if (!it.allIssueTypes) {
            clauseBuilder.issueType(*it.associatedIssueTypes*.id).buildClause()
        }

        builder.addClause(clauseBuilder.buildClause())
    }
}

def query = builder.buildQuery()

def issueIdCollector = new IssueIdCollector()
searchProvider.search(SearchQuery.create(query, user), issueIdCollector)

def total = issueIdCollector.issueIds.size()
def issuesIdsIterable = new IssueIdsIssueIterable(issueIdCollector.issueIds*.toLong(), issueManager)
def loggingContextSink = new LoggingContextSink(log, "Indexing script field issues {0}% ($total total)", Level.DEBUG)
issueIndexingService.reIndexIssues(issuesIdsIterable, new PercentageContext(total, loggingContextSink))

Jira 8

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.issue.search.SearchProviderFactory
import com.atlassian.jira.issue.util.IssueIdsIssueIterable
import com.atlassian.jira.jql.builder.JqlQueryBuilder
import com.atlassian.jira.jql.query.IssueIdCollector
import com.atlassian.jira.task.context.LoggingContextSink
import com.atlassian.jira.task.context.PercentageContext
import com.atlassian.jira.issue.search.SearchQuery
import org.apache.log4j.Level

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
def issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)
def searchProviderFactory = ComponentAccessor.getComponent(SearchProviderFactory)
def issueManager = ComponentAccessor.getIssueManager()

/*
 Loop through all script fields and get their contexts
 Create a query and execute it
 Reindex all results
*/

def builder = JqlQueryBuilder.newBuilder().where().defaultOr()

def scriptFields = customFieldManager.getCustomFieldObjects().findAll {
    it.customFieldType.descriptor.completeKey == "com.onresolve.jira.groovy.groovyrunner:scripted-field"
}

if (scriptFields.find {
    it.allProjects && it.allIssueTypes
}) {
    log.warn("A full background reindex is required at least one script field has global scope")
} else {
    scriptFields.each {
        def clauseBuilder = JqlQueryBuilder.newClauseBuilder().defaultAnd()

        if (!it.allProjects) {
            clauseBuilder.project(*it.associatedProjectObjects*.id).buildClause()
        }
        if (!it.allIssueTypes) {
            clauseBuilder.issueType(*it.associatedIssueTypes*.id).buildClause()
        }

        builder.addClause(clauseBuilder.buildClause())
    }
}

def query = builder.buildQuery()

def issueIdCollector = new IssueIdCollector()

searchProvider.search(SearchQuery.create(query, user), issueIdCollector)

def total = issueIdCollector.issueIds.size()
def issuesIdsIterable = new IssueIdsIssueIterable(issueIdCollector.issueIds*.toLong(), issueManager)
def loggingContextSink = new LoggingContextSink(log, "Indexing script field issues {0}% ($total total)", Level.DEBUG)
issueIndexingService.reIndexIssues(issuesIdsIterable, new PercentageContext(total, loggingContextSink))


Previous Jira versions may throw a SearchUnavailableException. It’s possible that this only happens when indexes are corrupt. If this is the case disable ScriptRunner, do a full re-index, enable ScriptRunner, then re-index again.

Scripting

There is not much available in the binding for this one, just:

componentManager

Instance of com.atlassian.jira.ComponentManager This is here for backward compatibility. You should really use the ComponentAccessor instead.

log

logger so you can do eg log.warn("…​")

getCustomFieldValue

closure - a utility method to let you look you get the value of a custom field on this issue, by name or by id (as Long). Eg getCustomFieldValue("mytextfield")

Custom Templates

As of plugin version 2.0.4 you can write your own template. Select custom, and a textbox will drop down for you to enter the template.

Whatever you returned from the script part of the field will be available in $value. You could use a custom template if you want to display the field differently from the way it is stored, or the way one of the default templates displays it. As usual, experiment using the preview facility.

These are the items available in the velocity context:

Displaying Script Fields in Transition Screens

Jira does not display calculated custom fields on transition screens.

A possibility is to override one of the other custom fields, eg GenericTextCFType, however then you need to match the base class to the type of value that the calculated field produces, for instance a text or number or user type. This massively increases complexity and reduces flexibility.

My preferred solution therefore is to use javascript to do this.

The downside is that as there is no public API for the Jira frontend, so it’s possible that presentation issues will appear from one version to the next.

To make your calculated field appear on transition screens, paste the following javascript into the description (Admin → Field Configurations) of any field that appears on the transition screen(s) you care about, eg Resolution, NOT the calculated field itself. You only need to modify the two values in the mandatory config section.

<script type="text/javascript"> (function ($) { // ---------------------------------- MANDATORY CONFIG ---------------------------------- var fieldName = "Scripted Field" // display name - does not have to match the name of the field var fieldId = "customfield_14013" // field Id // ---------------------------------- END MANDATORY CONFIG ------------------------------ function addCalculatedField(e, context) { var $context = $(context); // if you want you can limit this to certain actions by checking to see if this value is in a list of action IDs if (!$("input[name='action']").val()) { return; } // multiple handlers can be added if you do an action, then cancel repeatedly if ($context.find("#scriptedfield_" + fieldId).length > 0) { return; } var issueKey = $("meta[name='ajs-issue-key']").attr("content"); if (!issueKey) { issueKey = $("#key-val").attr("rel"); // transition screens in full page mode } var paddingTop = AJS.$("meta[name='ajs-build-number']").attr("content") < 6000 ? "1" : "5"; var fieldGroupHtml = '<div class="field-group">' + '<label for="' + fieldId + '">' + fieldName + '</label>' + '<div style="padding-top: ' + paddingTop + 'px" id="scriptedfield_' + fieldId + '"> ' + '<span class="aui-icon aui-icon-wait">Loading, please wait</span></div>' + '</div> '; // Modify this select if you want to change the positioning of the displayed field $context.find("div.field-group:first").before(fieldGroupHtml); $.ajax({ type: "GET", "contentType": "application/json", url: AJS.params.baseURL + "/rest/api/2/issue/" + issueKey + "?expand=renderedFields&fields=" + fieldId, success: function (data) { if ("fields" in data && fieldId in data.fields) { var fieldValue = data.fields[fieldId]; $context.find("#scriptedfield_" + fieldId).empty().append(fieldValue); } else { $context.find("#scriptedfield_" + fieldId).empty().append("ERROR - bad field ID"); } }, error: function () { $context.find("#scriptedfield_" + fieldId).empty().append("ERROR"); } }); } JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) { addCalculatedField(e, context); }); })(AJS.$); </script>

Note that the plain value is returned, the renderer is not respected. If you know how to get the rendered value let me know. The REST API does not seem to provide this for calculated fields even with expand=renderedFields.

On this page