Rewriting Scripts for Cloud Hints and Tips
Mandatory script rewrite
Be aware that any migration from Server to Cloud (manual or through the Atlassian Cloud Migration Assistant) will require rewriting scripts.
Automatic migration from Server/Data Center to Cloud is not possible due to differences in the programming model and the API between the two products. The process of migration involves the manual rewriting of all scripts to make them compatible with the Cloud platform.
Every migration is different, but we have collated some useful hints and tips here to help you rewrite your scripts.
Before You Start
Platform Differences
Jira Cloud uses the Atlassian Connect framework whereas Jira Server/Data Center use the Atlassian Plugins framework, known as Plugins v2 (or P2). There are significant differences between the Connect and P2 frameworks which we have documented here. We recommend you familiarize yourself with these differences before you begin rewriting your scripts.
Feature Parity
Some features available in ScriptRunner for Jira Server/Data Center are not available in ScriptRunner for Jira Cloud due to differences in the platform (explained in the Platform Differences page). See our Feature Parity and Script Alternatives table before starting to rewrite your scripts to check which features have full or partial parity.
Don’t have the time or capacity to write scripts in-house? Get your Server scripts translated to Cloud without writing a single line of code yourself. Check out our Scripting Service.
Where to Start
As a general rule, we suggest you follow the steps set out below when rewriting your Server scripts for Cloud:
- List the Java API calls (classes and specific methods you consider important) used in the Server script with brief summary of what they do.
- Identify the types of fields that will need to be updated, and check these are available in Cloud.
- Look at Atlassian Jira Rest API and note down the API endpoints you expect to need as well as relevant body parameters.
Investigate how to update issues in Jira Cloud. The biggest challenge is usually working out the format required for each field type when sending the update JSON.
Investigation Process:- Create a test Jira Cloud instance here and install a trial version of ScriptRunner.
- Create a Test Jira project and a few issues, then navigate to the first new issue you created.
- On the Issue View page click the three dot menu in the top right and choose to See the old view then click Admin → Add field.
- Create the types of the fields we want to test with the Add field button and populate them with values for the current issue.
- Use a GET request to the
`/issue` endpoint
to see how the values are structured in JSON with a rest API tool like curl or Postman. - With this information, we can then plan the expected Body parameters required to update the same fields with a PUT request.
- Research the ScriptRunner for Jira Cloud style for writing the required HTTP calls and then rewrite your script.
Start by looking into the library we use in ScriptRunner for Jira Cloud, this is called Unirest. We auto-import this into the scripts, so you can simply call the
get()
,put()
,post()
methods mentioned in the Unirest documentation.Next, take a look at examples of ScriptRunner for Jira Cloud scripts in the Adaptavist Library. This will give you an idea of the style, and code for several common use cases.
Finally, for more working examples, go to the Script Console inside your ScriptRunner for Jira Cloud instance and browse through the example scripts in the Examples drop-down under the script input area.
Quick Tips
Here are are some quick tips that apply to a variety of use cases:
If you are unsure how an issue represents its selected values in a REST response, create a test issue and run a simple
GET
request for that issue, for example:https://YourCloudURL.atlassian.net/rest/api/2/issue/YourIssueKey
When translating ScriptRunner for Jira Server/Data Center scripts to ScriptRunner for Jira Cloud you will find that a lot of the complex objects can be ignored entirely. This is because complex Java methods with multiple params in Server can be replaced by fairly simple REST calls with structured body parameters in Cloud.
- You generally cannot run scripts as another user in Cloud (with the exception of the ScriptRunner add-on user). You can pass user account IDs as params to some REST calls, but the calls themselves will run as either the executing user or the ScriptRunner add-on user.
- You do not need to define REST request authentication headers in ScriptRunner for Jira Cloud scripts as these are already set up for you. The scripts will run as the user that triggers the script, or the ScriptRunner add-on user. These are controlled by a simple drop-down within the ScriptRunner script configuration UI.
- You can update most fields with string versions of the value rather than having to find option IDs as you would often have to in ScriptRunner for Server/Data Center scripts.
Commonly Used Atlassian Java API Endpoints and their Cloud Equivalents
This table maps some of the most commonly used Atlassian Java API's (in ScriptRunner for Jira Server/Data Center) to the closest Atlassian REST API endpoints (ScriptRunner for Jira Cloud) to guide you in script conversions.
Java API (Server) | REST API (Cloud) |
---|---|
IssueService | Getting issues: GET /rest/api/2/issue/{issueIdOrKey} Updating issues: PUT /rest/api/2/issue/{issueIdOrKey} |
issueService.newIssueInputParameters() | You don't need to do this in cloud, you just send a JSON structure of the fields you want to change in the PUT requests body parameters. For example:
|
issueService.validateUpdate | You don't need to do validation like this prior to updating issues in Jira Cloud as the endpoint itself will return an error if what you try to do is invalid. |
issueService.update | To update issues use: PUT /rest/api/2/issue/{issueIdOrKey} |
IssueManager | To get issues use: GET /rest/api/2/issue/{issueIdOrKey} |
jiraAuthenticationContext | Used by Server to get user who will run a Java function. This is not required in ScriptRunner for Jira Cloud as you choose to run scripts as the Current User or ScriptRunner Addon User only. You may need to pass user ID's in the REST API body parameters, but running a script as a user other than Current User or ScriptRunner Add-on User is not currently possible in Cloud. |
customFieldManager | |
optionsManager | Updating field options: Currently, these endpoints are all experimental for Jira Cloud so you may not be able to do exactly the same thing as in Jira Server. Refer to all the experimental endpoints here. |
ProjectManager | Get projects with a paginated search using project Key or Name: GET /rest/api/2/project/search Get a project by id or key: GET /rest/api/2/project/{projectIdOrKey} Update a project by id or key: PUT /rest/api/2/project/{projectIdOrKey} Delete a project by id or key: DELETE /rest/api/2/project/{projectIdOrKey} |
VersionManager | Get all versions for a project: GET /rest/api/2/project/{projectIdOrKey}/versions Create versions for a project: POST /rest/api/2/version Update versions within a project: PUT (Update versions) /rest/api/2/version/{id} Delete/Replace versions in a project: POST /rest/api/2/version/{id}/removeAndSwap |
WatcherManager | Get Watchers for an issue: GET /rest/api/2/issue/{issueIdOrKey}/watchers Add watchers to an issue: POST (Add wachers) /rest/api/2/issue/{issueIdOrKey}/watchers Delete watchers from an issue: DELETE /rest/api/2/issue/{issueIdOrKey}/watchers |
ProjectRoleservice / ProjectRoleManager | Control role actors PUT /rest/api/2/project/{projectIdOrKey}/role/{id} Get a projects roles GET /rest/api/2/project/{projectIdOrKey}/role Delete project roles DELETE /rest/api/2/role/{id} |
GroupManager | Create a group: POST /rest/api/2/group Delete a group: DELETE /rest/api/2/group Get members of a group GET /rest/api/2/group/member Add user to group POST /rest/api/2/group/user Remove user from group DELETE /rest/api/2/group/user |
UserManager | |
| Search with JQL using rest GET /rest/api/2/search If the JQL is too large for a query param use POST /rest/api/2/search |
| Get all issue types the executing user has permission to see GET /rest/api/2/issuetype |
PriorityManager | Get all Issue Priorities GET /rest/api/2/priority |
| You need the ID to get a security level so you have to follow this: |
Field Availability
All standard ScriptRunner for Jira field types are available in Cloud. The only caveat is that Project Picker fields are only available as single-select fields.
Cloud custom field and advanced field types are listed here.
Common Listener Events Availability
Below is a list of common listener events in ScriptRunner for Jira Server/Data Center and their availability in Cloud. More detail on what ScriptRunner can do with the supported Cloud events is documented here.
Server/DC Event | Available? | Notes |
---|---|---|
Issue Created | Y | |
Issue Updated | Y | |
Issue Deleted | Y | |
Issue Assigned | N | |
Issue Resolved | N | |
Issue Closed | N | |
Issue Reopened | N | |
Issue Moved | N | |
Issue Link Created | Y | |
Issue Link Deleted | Y | |
Issue Watcher Added | N | |
Issue Watcher Deleted | N | |
Issue Archived | N | Not a Cloud feature |
Comment Created | Y | |
Comment Updated | Y | |
Comment Deleted | Y | |
Project Created | Y | |
Project Updated | Y | |
Project Deleted | Y | |
Project Component events | N | |
Project Role events | N | |
Version Created | Y | |
Version Updated | Y | |
Version Deleted | Y | |
Version Moved | Y | |
Version Released | Y | |
Version unreleased | Y | |
User Created | Y | |
User Deleted | Y | |
User Edited | Y | Available as User Updated in Cloud |
Group Created | N | |
Group Updated | N | |
Group Deleted | N |
Common Operations
Here we have listed some common operations required in ScriptRunner for Jira Cloud scripts, switch between the tabs to show the Cloud or Server script. These operations can be utilized for many use cases.
Get field name to ID map
groovydef fieldNameToIdMap def fields = get('/rest/api/2/field') .header('Content-Type', 'application/json') .asObject(List) if (fields.status == 200){ fieldNameToIdMap = fields.body.collectEntries { [(it.name): it.id] } } else { return "Failed to generate fields map ${fields.status} ${fields.body}" }
groovy/* For Server/DC you will not likely need to get a map of field names to ID for custom fields as you can just use this simple method to get a collection CustomField objects and then filter by name as shown in the next example. */ import com.atlassian.jira.component.ComponentAccessor def fields = ComponentAccessor.customFieldManager.getCustomFieldObjects()
Get a single field ID with its name
groovyfinal customFieldName = 'TextFieldA' def fieldId def fields = get('/rest/api/2/field') .header('Content-Type', 'application/json') .asObject(List) if (fields.status == 200){ fieldId = fields.body.find { it.name == customFieldName }.id } else { return "Failed to generate fields map ${fields.status} ${fields.body}" }
groovyimport com.atlassian.jira.component.ComponentAccessor final FIELD_NAME = 'TextFieldA' def myFieldId = ComponentAccessor.customFieldManager.getCustomFieldObjects().find { it.name == FIELD_NAME }?.id
Update Fields
groovydef issueKey = 'TEST-2' def sendNotification = false // control if notification email is sent to all watchers def fieldNameToIdMap def fields = get('/rest/api/2/field') .header('Content-Type', 'application/json') .asObject(List) if (fields.status == 200){ fieldNameToIdMap = fields.body.collectEntries { [(it.name): it.id] } } else { return "Failed to generate fields map ${fields.status} ${fields.body}" } def result = put("/rest/api/2/issue/${issueKey}") //.queryString("overrideScreenSecurity", Boolean.TRUE) .header('Content-Type', 'application/json') .body([ notifyUsers: sendNotification, fields: [ (fieldNameToIdMap['SelectListA']): [value: "BBB"], //Single Select List (fieldNameToIdMap['MultiSelectListA']): [[value: "BBBB"], [value: "CCCC"]], //Multi Select List (fieldNameToIdMap['RadioButtonA']): [value: "Maybe"], // Radio Buttons (fieldNameToIdMap['CheckBoxA']): [[value: "Maybe"], [value: "No"]], // Checkboxes (fieldNameToIdMap['TextFieldA']): "QWERTY", // Text Field (fieldNameToIdMap['UserPickerA']): ["accountId": "5b9a84022d389f762bd0bd23"], // Single User Picker (fieldNameToIdMap['MultiUserPickerA']): [["accountId": "5b9a84022d389f762bd0bd23"]],// Multi User Picker (fieldNameToIdMap['DateTimePickerA']): "2021-10-16T15:41:00.000+0100", // Date Time Field (fieldNameToIdMap['DatePickerA']): "2021-10-11", // Date Field (fieldNameToIdMap['ProjectPickerA']): [key: "TP"], // Project Picker (fieldNameToIdMap['LabelFieldA']): ["here", "test"], // Labels field (fieldNameToIdMap['VersionPickerA']): [name: "testv1"], // Single Version Picker (fieldNameToIdMap['MultiVersionPickerA']): [[name: "testv1"], [name: "anotherv2"]], // Multi Version Picker (fieldNameToIdMap['GroupPickerA']): [name: "jira-software-users-mattdevtest"], // Single Group Picker (fieldNameToIdMap['MultiGroupPickerA']): [ //Multi-Group Picker [name:"jira-servicemanagement-users-mattdevtest"], [name:"jira-software-users-mattdevtest"], [name:"jira-workmanagement-users-mattdevtest"], ], ] ]) .asString() if (result.status == 204) { return 'Success' } else { return "${result.status}: ${result.body}" }
groovy// Example taken from https://library.adaptavist.com/entity/update-the-value-of-custom-fields-through-the-script-console with added multi-version picker example import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.event.type.EventDispatchOption import com.atlassian.jira.issue.fields.CustomField import groovy.transform.Field // the issue key to update @Field final String issueKey = "Test-1" // the name of a 'single select list' custom field final String selectList = "SelectListA" // the name of a 'multi select list' custom field final String multiSelectList = "MultiSelectA" // name of a 'radio button' custom field final String radioButtonField = "RadioButtons" // name of a 'check box' custom field final String checkboxField = "Checkboxes" // the name of a 'text field' custom field final String textField = "TextFieldA" // the name of a 'user picker' custom field final String userPicker = "UserPicker" // the name of a 'multi user picker' custom field final String multiUserPicker = "MultiUserPickerA" // the name of a 'group picker' custom field final String groupPicker = "GroupPicker" // the name of a 'multi group picker' custom field final String multiGroupPicker = "MultiGroupPicker" // the name of a 'date and time' custom field final String dateTimeField = "First DateTime" // the name of a 'date' custom field final String dateField = "Date" // the name of a 'project picker' custom field final String projectPickerField = "ProjectPicker" // the name of a 'label' picker custom field final String labelField = "LabelField" // name of a 'single version picker' custom field final String versionField = "VersionPicker" // name of a 'multi version picker' custom field final String multiVersionField = "VersionsPicker" // change to 'true' if you want to send an email if the update is successful final boolean sendMail = false def issueService = ComponentAccessor.issueService def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey) assert issue: "Could not find issue with key $issueKey" def issueInputParameters = issueService.newIssueInputParameters().with { // set custom fields with options (select lists, checkboxes, radio buttons) addCustomFieldValue(getSingleCustomFieldByName(selectList).id, *getOptionIdsForFieldByValue(selectList, "BBB")) addCustomFieldValue(getSingleCustomFieldByName(multiSelectList).id, *getOptionIdsForFieldByValue(multiSelectList, "BBB", "CCC")) addCustomFieldValue(getSingleCustomFieldByName(radioButtonField).id, *getOptionIdsForFieldByValue(radioButtonField, "Yes")) addCustomFieldValue(getSingleCustomFieldByName(checkboxField).id, *getOptionIdsForFieldByValue(checkboxField, "Maybe", "Yes")) // set text fields addCustomFieldValue(getSingleCustomFieldByName(textField).id, "New Value") // set user fields addCustomFieldValue(getSingleCustomFieldByName(userPicker).id, "admin") addCustomFieldValue(getSingleCustomFieldByName(multiUserPicker).id, "admin", "anuser") // set group fields addCustomFieldValue(getSingleCustomFieldByName(groupPicker).id, "jira-users") addCustomFieldValue(getSingleCustomFieldByName(multiGroupPicker).id, "jira-users", "jira-administrators") // set custom field of type date addCustomFieldValue(getSingleCustomFieldByName(dateTimeField).id, "04/Feb/12 8:47 PM") addCustomFieldValue(getSingleCustomFieldByName(dateField).id, "04/Feb/12") } // set project picker field def project = ComponentAccessor.projectManager.getProjectObjByKey("SSPA") assert project: "Could not find project" issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(projectPickerField).id, project.id.toString()) // set custom field of type label issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(labelField).id, "foo", "bar") // set custom field of type version picker def versionOne = ComponentAccessor.versionManager.getVersions(issue.projectObject).findByName("Version1") assert versionOne: "Could not find version" issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(versionField).id, versionOne.id.toString()) // set custom field of type multi-version picker def versionTwo = ComponentAccessor.versionManager.getVersions(issue.projectObject).findByName("Version2") assert versionTwo: "Could not find version" issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(multiVersionField).id, versionOne.id.toString(), versionTwo.id.toString()) def updateValidationResult = issueService.validateUpdate(loggedInUser, issue.id, issueInputParameters) assert updateValidationResult.valid: updateValidationResult.errorCollection def issueUpdateResult = issueService.update(loggedInUser, updateValidationResult, EventDispatchOption.ISSUE_UPDATED, sendMail) assert issueUpdateResult.valid: issueUpdateResult.errorCollection /** * Get a custom field given a custom field name. * If there are than one custom fields with the same name under the same Context then return the first one. * @param fieldName The name of the custom field * @param issue The issue to look for that custom field * @return the custom field, if that exists */ CustomField getSingleCustomFieldByName(String fieldName) { def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey) def customField = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName(fieldName) assert customField: "Could not find custom field with name $fieldName" customField } /** * Given a custom field name and option values, retrieve their ids as String * @param customFieldName The name of the custom field * @param values The values in order to get their ids * @return List < String > The ids of the given values */ List<String> getOptionIdsForFieldByValue(String customFieldName, String... values) { def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey) def customField = getSingleCustomFieldByName(customFieldName) ComponentAccessor.optionsManager.getOptions(customField.getRelevantConfig(issue)).findAll { it.value in values.toList() }*.optionId*.toString() }
Perform JQL Searches
groovydef query = 'project = TEST' def searchReq = get("/rest/api/2/search") .queryString("jql", query) .asObject(Map) assert searchReq.status == 200 // Save the search results as a Map Map searchResult = searchReq.body // print all the issue keys just to demonstrate what was found searchResult.issues*.key
groovy// Example from https://library.adaptavist.com/entity/perform-a-jql-search-in-scriptrunner-for-jira import com.atlassian.jira.bc.issue.search.SearchService import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.issue.search.SearchException import com.atlassian.jira.web.bean.PagerFilter import org.apache.log4j.Level // Set log level to INFO log.setLevel(Level.INFO) // The JQL query you want to search with final jqlSearch = "project = TEST" // Some components def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser def searchService = ComponentAccessor.getComponentOfType(SearchService) // Parse the query def parseResult = searchService.parseQuery(user, jqlSearch) if (!parseResult.valid) { log.error('Invalid query') return null } try { // Perform the query to get the issues def results = searchService.search(user, parseResult.query, PagerFilter.unlimitedFilter) def issues = results.results issues.each { log.info(it.key) } issues*.key } catch (SearchException e) { e.printStackTrace() null }