Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
languagegroovy
titletrigger_jenkins_build_restapi_endpoint.groovy
collapsetrue
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import groovy.json.JsonSlurper;
import groovy.json.StreamingJsonBuilder;
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
 
@BaseScript CustomEndpointDelegate delegate
 
triggerJenkinsBuild(httpMethod: "GET") { MultivaluedMap queryParams ->
 
	def issueId = queryParams.getFirst("issueId") as String // use the issueId to retrieve this issue
	 
	def flag = [
	type : 'success',
	title: "Build scheduled",
	close: 'auto',
	body : "A new build has been scheduled related with "+issueId
	]
   

	URL url;
	def jobName = "java-junit-calc"                         // could come from a CF in the Test Plan
	def jenkinsHostPort = "192.168.56.102:8081"             // could be defined elsewhere
	def token = "iFBDOBhNhaxL4T9ass93HRXun2JF161Z"  		// could also come from a CF in the Test Plan
	def username = "admin"									// probably, would need to be stored elsewhere
	def password = "fa02840152aa2e4da3d8db933ec708d6"		// probably, would need to be stored elsewhere
	def baseURL = "http://${jenkinsHostPort}/job/${jobName}/buildWithParameters?token=${token}&TESTPLAN=$issueId"

	url = new URL(baseURL);
	def body_req = []

	def authString = "${username}:${password}".bytes.encodeBase64().toString()

	URLConnection connection = url.openConnection();
	connection.requestMethod = "POST"
	connection.doOutput = true
	connection.addRequestProperty("Authorization", "Basic ${authString}")
	connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8")
	connection.outputStream.withWriter("UTF-8") { new StreamingJsonBuilder(it, body_req) }
	connection.connect();
	log.debug(connection.getResponseCode())
	log.debug(connection.getResponseMessage())

    
	if (connection.getResponseCode() == 201) {
 		Response.ok(JsonOutput.toJson(flag)).build()
 	} else {
 		//Response.status(Response.Status.NOT_FOUND).entity("Problem scheduling job!").build();
 	}
    
}


Example

...

ScriptRunner configuration

   

Jenkins configuration

In Jenkins, we need to generate an API token for some user, which can be done from the profile settings page.

...

Code Block
languagegroovy
titletrigger_jenkins_build_restapi_endpoint_with_testlist.groovy
collapsetrue
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.builder.JqlQueryBuilder
import com.atlassian.jira.user.util.UserUtil
import com.atlassian.jira.user.util.UserManager;
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.bc.issue.search.SearchService;
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.web.bean.PagerFilter;
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.user.UserPropertyManager
import com.atlassian.jira.propertyset.JiraPropertySetFactory;
import com.google.common.collect.ImmutableMap;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.module.propertyset.PropertySetManager;
import com.atlassian.jira.util.BuildUtils
import com.atlassian.jira.util.BuildUtilsInfo
import com.atlassian.jira.util.BuildUtilsInfoImpl
import com.atlassian.plugin.PluginAccessor
import com.atlassian.plugin.PluginManager
import com.atlassian.jira.bc.license.JiraLicenseService
import com.atlassian.jira.bc.license.JiraLicenseServiceImpl
import org.apache.log4j.Level
import org.apache.log4j.Logger
import com.atlassian.jira.issue.IssueManager
import com.opensymphony.workflow.InvalidInputException

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import groovy.json.JsonSlurper;
import groovy.json.StreamingJsonBuilder;
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
 
@BaseScript CustomEndpointDelegate delegate


issueManager = ComponentAccessor.getIssueManager()
searchService =  ComponentAccessor.getComponent(SearchService.class);
serviceAccount = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
customFieldManager = ComponentAccessor.getCustomFieldManager()

Object getIssues(jqlQuery){
    // A list of GenericValues representing issues
    List<Issue> searchResults = null;

    SearchService.ParseResult parseResult =  searchService.parseQuery(serviceAccount, jqlQuery);

    if (parseResult.isValid()) {
        // throws SearchException
        SearchResults results = searchService.search(serviceAccount, parseResult.getQuery(), PagerFilter.getUnlimitedFilter());
        searchResults = results.getIssues();
        return searchResults;
    } 
            
     return [] 
}


Object getFieldValue(issue,customField) {
    //def cField = customFieldManager.getCustomFieldObject(customField)
    def cField = customFieldManager.getCustomFieldObjectByName(customField)
    def cFieldValue = issue.getCustomFieldValue(cField)
    return cFieldValue
}

String replaceLast(String string, String substring, String replacement)
{
  int index = string.lastIndexOf(substring);
  if (index == -1)
    return string;
  return string.substring(0, index) + replacement + string.substring(index+substring.length());
}


triggerJenkinsBuildWithTestList(httpMethod: "GET") { MultivaluedMap queryParams ->
 
	// the details of getting and modifying the current issue are ommitted for brevity
	def issueId = queryParams.getFirst("issueId") as String // use the issueId to retrieve this issue
	

	def flag = [
	type : 'success',
	title: "Build scheduled",
	close: 'auto',
	body : "A new build has been scheduled related with "+issueId
	]
   

	URL url;
	def jobName = "java-junit-calc-triggered"               // could be defined in a CF in the Test Plan
	def jenkinsHostPort = "192.168.56.102:8081"             // could be defined elsewhere
	def token = "iFBDOBhNhaxL4T9ass93HRXun2JF161Z"  		// could also come from a CF in the Test Plan
	def username = "admin"									// probably, would need to be stored elsewhere
	def password = "fa02840152aa2e4da3d8db933ec708d6"		// probably, would need to be stored elsewhere
	//def baseURL = "http://${username}:${password}@${jenkinsHostPort}/job/${jobName}/build?token=${token}"
	//def baseURL = "http://${jenkinsHostPort}/job/${jobName}/build?token=${token}"

	jql = "issue in testPlanTests('${issueId}') and \"Test Type\" = Generic"
	issues = getIssues(jql)
    // we're assuming that we have Junit based Tests.. so we need to do some conversion beforehand, so maven can process the list of tests to be run
	def testlist = issues.collect { getFieldValue(it,"Generic Test Definition")}
	def testlist2 = testlist.collect { replaceLast(it,".","%23") }
    
	def baseURL = "http://${jenkinsHostPort}/job/${jobName}/buildWithParameters?token=${token}&TESTPLAN=${issueId}&TESTLIST=${testlist2.join(',')}"
	url = new URL(baseURL);
	def body_req = []

	def authString = "${username}:${password}".bytes.encodeBase64().toString()

	URLConnection connection = url.openConnection();
	connection.requestMethod = "POST"
	connection.doOutput = true
	connection.addRequestProperty("Authorization", "Basic ${authString}")
	connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8")
	connection.outputStream.withWriter("UTF-8") { new StreamingJsonBuilder(it, body_req) }
	connection.connect();
	//connection.getContent();
	log.debug(connection.getResponseCode())
	log.debug(connection.getResponseMessage())

    
	if (connection.getResponseCode() == 201) {
 		Response.ok(JsonOutput.toJson(flag)).build()
 	} else {
 		//Response.status(Response.Status.NOT_FOUND).entity("Problem scheduling job!").build();
 	}
    
}


Example

...

ScriptRunner configuration

   

Jenkins configuration

In Jenkins, we need to generate an API token for some user, which can be done from the profile settings page.

...

Code Block
languagegroovy
titletrigger_bamboo_build_restapi_endpoint.groovy
collapsetrue
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import groovy.json.JsonSlurper;
import groovy.json.StreamingJsonBuilder;
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import java.nio.charset.StandardCharsets

@BaseScript CustomEndpointDelegate delegate
 
triggerBambooBuild(httpMethod: "GET") { MultivaluedMap queryParams ->
 
	def issueId = queryParams.getFirst("issueId") as String // use the issueId to retrieve this issue
	 
	def flag = [
	type : 'success',
	title: "Build scheduled",
	close: 'auto',
	body : "A new build has been scheduled related with "+issueId
	]
   

	URL url;
	// curl --user admin:admin -X POST -d "default&ExecuteAllStages=true" http://yourbambooserver/rest/api/latest/queue/XRAY-JUNITCALC
	def projectKey = "XRAY"									// could come from a CF in the Test Plan
    def planKey = "JUNITCALC"								// could come from a CF in the Test Plan
	def bambooHostPort = "192.168.56.102:8085"				// could be defined elsewhere
	def username = "admin"									// probably, would need to be stored elesewhere
	def password = "admin"									// probably, would need to be stored elesewhere
	def baseURL = "http://${bambooHostPort}/rest/api/latest/queue/${projectKey}-${planKey}"
    String urlParameters  = "default&ExecuteAllStages=true&bamboo.TESTPLAN=${issueId}";
    byte[] postData       = urlParameters.getBytes( StandardCharsets.UTF_8 );
    int    postDataLength = postData.length;
    
	url = new URL(baseURL);
	def body_req = []

	def authString = "${username}:${password}".bytes.encodeBase64().toString()

	URLConnection connection = url.openConnection();
	connection.requestMethod = "POST"
	connection.doOutput = true
	connection.addRequestProperty("Authorization", "Basic ${authString}")
    connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded"); 
    connection.setRequestProperty( "charset", "utf-8");
    connection.setRequestProperty( "Content-Length", Integer.toString( postDataLength ));
    connection.setUseCaches( false );

    DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
    wr.write( postData );

	connection.connect();
	//connection.getContent();
	log.debug(connection.getResponseCode())
	log.debug(connection.getResponseMessage())

    
	if (connection.getResponseCode() == 200) {
 		Response.ok(JsonOutput.toJson(flag)).build()
 	} else {
 		//Response.status(Response.Status.NOT_FOUND).entity("Problem scheduling job!").build();
 	}
    
}


Example

ScripRunner configuration


   

Bamboo configuration

The project itself is a normal one; the only thing relevant to mention is that this project is a parameterized one, so it receives a TESTPLAN variable, that in our case will be coming from Jira.

...

Code Block
languagegroovy
titletrigger_bamboo_build_restapi_endpoint_with_testlist.groovy
collapsetrue
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.builder.JqlQueryBuilder
import com.atlassian.jira.user.util.UserUtil
import com.atlassian.jira.user.util.UserManager;
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.bc.issue.search.SearchService;
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.web.bean.PagerFilter;
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.user.UserPropertyManager
import com.atlassian.jira.propertyset.JiraPropertySetFactory;
import com.google.common.collect.ImmutableMap;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.module.propertyset.PropertySetManager;
import com.atlassian.jira.util.BuildUtils
import com.atlassian.jira.util.BuildUtilsInfo
import com.atlassian.jira.util.BuildUtilsInfoImpl
import com.atlassian.plugin.PluginAccessor
import com.atlassian.plugin.PluginManager
import com.atlassian.jira.bc.license.JiraLicenseService
import com.atlassian.jira.bc.license.JiraLicenseServiceImpl
import org.apache.log4j.Level
import org.apache.log4j.Logger
import com.atlassian.jira.issue.IssueManager
import com.opensymphony.workflow.InvalidInputException



import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import groovy.json.JsonSlurper;
import groovy.json.StreamingJsonBuilder;
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import java.nio.charset.StandardCharsets

@BaseScript CustomEndpointDelegate delegate

issueManager = ComponentAccessor.getIssueManager()
searchService =  ComponentAccessor.getComponent(SearchService.class);
serviceAccount = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
customFieldManager = ComponentAccessor.getCustomFieldManager()


Object getIssues(jqlQuery){
    // A list of GenericValues representing issues
    List<Issue> searchResults = null;

    SearchService.ParseResult parseResult =  searchService.parseQuery(serviceAccount, jqlQuery);

    if (parseResult.isValid()) {
        // throws SearchException
        SearchResults results = searchService.search(serviceAccount, parseResult.getQuery(), PagerFilter.getUnlimitedFilter());
        searchResults = results.getIssues();
        return searchResults;
    } 
            
     return [] 
}

Object getFieldValue(issue,customField) {
    def cField = customFieldManager.getCustomFieldObjectByName(customField)
    def cFieldValue = issue.getCustomFieldValue(cField)
    return cFieldValue
}

String replaceLast(String string, String substring, String replacement)
{
  int index = string.lastIndexOf(substring);
  if (index == -1)
    return string;
  return string.substring(0, index) + replacement + string.substring(index+substring.length());
}
 
triggerBambooBuildWithTestList(httpMethod: "GET") { MultivaluedMap queryParams ->

	def issueId = queryParams.getFirst("issueId") as String // use the issueId to retrieve this issue
	 
	def flag = [
	type : 'success',
	title: "Build scheduled",
	close: 'auto',
	body : "A new build has been scheduled related with "+issueId
	]
   

	jql = "issue in testPlanTests('${issueId}') and \"Test Type\" = Generic"
	issues = getIssues(jql)
    // // we're assuming that we have Junit based Tests.. so we need to do some conversion beforehand, so maven can process the list of tests to be run
	def testlist = issues.collect { getFieldValue(it,"Generic Test Definition")}
    def testlist2 = testlist.collect { replaceLast(it,".","%23") }

	URL url;
	// curl --user admin:admin -X POST -d "default&ExecuteAllStages=true&bamboo.TESTLIST=com.xpand.java.CalcTest#CanAddNumbers" http://yourbambooserver/rest/api/latest/queue/XRAY-JUNITCALCPARAMS

	def projectKey = "XRAY"									// could come from a CF in the Test Plan
    def planKey = "JUNITCALCPARAMS"							// could come from a CF in the Test Plan
    def stage = "default"                                   // could be hardcoded or come from a CF in the Test Plan
	def bambooHostPort = "192.168.56.102:8085"				// could be defined elsewhere
	def username = "admin"									// probably, would need to be stored elsewhere
	def password = "admin"									// probably, would need to be stored elsewhere
	def baseURL = "http://${bambooHostPort}/rest/api/latest/queue/${projectKey}-${planKey}"
    String urlParameters  = "${stage}&ExecuteAllStages=true&bamboo.TESTPLAN=${issueId}&bamboo.TESTLIST=${testlist2.join(',')}";
    byte[] postData       = urlParameters.getBytes( StandardCharsets.UTF_8 );
    int    postDataLength = postData.length;
    
	url = new URL(baseURL);
	def body_req = []

	def authString = "${username}:${password}".bytes.encodeBase64().toString()

	URLConnection connection = url.openConnection();
	connection.requestMethod = "POST"
	connection.doOutput = true
	connection.addRequestProperty("Authorization", "Basic ${authString}")
    connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded"); 
    connection.setRequestProperty( "charset", "utf-8");
    connection.setRequestProperty( "Content-Length", Integer.toString( postDataLength ));
    connection.setUseCaches( false );

    DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
    wr.write( postData );

	connection.connect();
	//connection.getContent();
	log.debug(connection.getResponseCode())
	log.debug(connection.getResponseMessage())

    
	if (connection.getResponseCode() == 200) {
 		Response.ok(JsonOutput.toJson(flag)).build()
 	} else {
 		//Response.status(Response.Status.NOT_FOUND).entity("Problem scheduling job!").build();
 	}
    
}


Example

ScripRunner configuration


      

Bamboo configuration

...

  • receive the source issue key that triggered the synchronization request (e.g. Test Plan, Test Execution, Sub Test Execution)
  • obtain the linked Test Sets, using JQL
    • obtain the Tests on each Test Set, using JQL
    • submit a REST API request to Xray specific endpoints to add the Test to the entity (e.g Test Plan)

ScripRunner configuration


     




Code Block
languagegroovy
titlesynchTestsFromRelatedTestSets_restapi_endpoint.groovy
collapsetrue
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import groovy.json.JsonSlurper;
import groovy.json.StreamingJsonBuilder;
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import java.nio.charset.StandardCharsets
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.builder.JqlQueryBuilder
import com.atlassian.jira.user.util.UserUtil
import com.atlassian.jira.user.util.UserManager;
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.bc.issue.search.SearchService;
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.web.bean.PagerFilter;
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.user.UserPropertyManager
import com.atlassian.jira.propertyset.JiraPropertySetFactory;
import com.google.common.collect.ImmutableMap;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.module.propertyset.PropertySetManager;
import com.atlassian.jira.util.BuildUtils
import com.atlassian.jira.util.BuildUtilsInfo
import com.atlassian.jira.util.BuildUtilsInfoImpl
import com.atlassian.plugin.PluginAccessor
import com.atlassian.plugin.PluginManager
import com.atlassian.jira.bc.license.JiraLicenseService
import com.atlassian.jira.bc.license.JiraLicenseServiceImpl
import org.apache.log4j.Level
import org.apache.log4j.Logger
import com.atlassian.jira.issue.IssueManager
import groovy.json.StreamingJsonBuilder
 



@BaseScript CustomEndpointDelegate delegate
 

boolean associateTestsToXrayIssue(endpoint, issueKey,listOfTestKeys){
    def jiraBaseUrl = com.atlassian.jira.component.ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
    def endpointUrl = "${jiraBaseUrl}/rest/raven/1.0/api/${endpoint}/${issueKey}/test"
    
    log.debug("issueKey: "+issueKey)
    log.debug("listOfTestKeys: "+listOfTestKeys)
    log.debug("jirabaseurl: "+jiraBaseUrl)
    log.debug("endpoint: "+endpointUrl)
    url = new URL(endpointUrl);
    def body_req = [ "add": listOfTestKeys ]

    // you should use a specific user for this purpose
    username = "admin"
    password = "admin"
    def authString = "${username}:${password}".bytes.encodeBase64().toString()

    URLConnection connection = url.openConnection();
    connection.requestMethod = "POST"
    connection.doOutput = true
    connection.addRequestProperty("Authorization", "Basic ${authString}")
    connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8")
    connection.outputStream.withWriter("UTF-8") { new StreamingJsonBuilder(it, body_req) }
    connection.connect();
    log.debug(connection.getResponseCode())
    log.debug(connection.getResponseMessage())

    if (connection.getResponseCode() == 200) {
        // OK
        return true;
    } else {
        // error
        return false;
    }
}

boolean associateTestsToTestPlan(testPlanKey, listOfTestKeys){
 return associateTestsToXrayIssue("testplan", testPlanKey, listOfTestKeys)
}

boolean associateTestsToTestExecution(testExecutionKey, listOfTestKeys){
 return associateTestsToXrayIssue("testexec", testExecutionKey, listOfTestKeys)
}



Object getIssues(jqlQuery){
    // A list of GenericValues representing issues
    List<Issue> searchResults = null;
    SearchService.ParseResult parseResult =  searchService.parseQuery(serviceAccount, jqlQuery);
  
    if (parseResult.isValid()) {
        // throws SearchException
        SearchResults results = searchService.search(serviceAccount, parseResult.getQuery(), PagerFilter.getUnlimitedFilter());
        searchResults = results.getIssues();
        return searchResults;
    }
  
     return []
}


synchTestsFromRelatedTestSets(httpMethod: "GET") { MultivaluedMap queryParams ->
    // issue_key may refer to a Test Plan or to a Test Execution
    def issue_key = queryParams.getFirst("issueId") as String // use the issueId to retrieve this issue
    
 
    projectManager = ComponentAccessor.getProjectManager()
    componentManager = ComponentManager.getInstance();
    searchService =  ComponentAccessor.getComponent(SearchService.class);
    issueManager = ComponentAccessor.getIssueManager()
    customFieldManager = ComponentAccessor.getCustomFieldManager()
    userUtil = ComponentAccessor.getUserUtil();
    serviceAccount = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
    pluginAccessor = componentManager.getPluginAccessor();
    
    MutableIssue issue = issueManager.getIssueObject(issue_key)

    Logger.getLogger("com.onresolve").setLevel(Level.DEBUG)

    // assume that Test Plan/Execution is linked to Test Sets using the issue link "relates to"; customize if needed
    jql = "issue in linkedIssues('${issue_key}', 'relates to')"
    testset_issues = getIssues(jql)
    def had_errors = false
    def success = false
    
    testset_issues.each {
        // process only "Test Set" issues
        if (it.issueType.name == "Test Set") {
            jql = "issue in testSetTests('${it.key}')"
            issues = getIssues(jql)
            test_keys = issues.collect{ it.key }
            //log.debug(test_keys)
            if (issue.issueType.name == "Test Plan") {
                success = associateTestsToTestPlan(issue_key, test_keys)
            } else if ((issue.issueType.name == "Sub Test Execution") || (issue.issueType.name == "Test Execution")) {
            	success = associateTestsToTestExecution(issue_key, test_keys)
            }
            if (!success) {
                had_errors = true
            }
        }
    }    
    
    def flag = []
    if (success) {
    	flag = [
            type : 'success',
            title: "Test Sets Synchronization",
            close: 'auto',
            body : "Tests have been synchronized for " + issue_key
    	]
        Response.ok(JsonOutput.toJson(flag)).build() 
    } else {
    	flag = [
            type : 'success',
            title: "Test Sets Synchronization",
            close: 'auto',
            body : "Tests have been synchronized for " + issue_key
    	] 
        Response.serverError().entity([error: "some Tests were no synchronized"]).build()
    }
	
}

...

As an example, you may have an internal documentation/Confluence space with some valuable information concerning Xray usage in your organization.

ScripRunner configuration

Adding an entry to the top menu is easy.

...