Versions Compared

Key

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

...

Info
titlePlease note

The following scripts are provided as-is, no warranties attached. Use these scripts carefully.

Please feel free to adapt them to your needs. 

Note: We don't provide support for ScriptRunner; if you have doubts concerning its usage, please contact ScriptRunner's support.



Table of Contents

Validate

...

coverable issues upon making a transition

Sometimes you may need to assure that the coverable issue (e.g. requirement) is covered before transitioning it to some status, or before resolving it.

The following script validates the requirement based on the directly linked tests to the requirementit.


Info
titlePlease note

This solution is not perfect for several reasons, so please take the following notes into consideration:

  1. this should be implemented as a workflow condition (currently it's not possible with ScriptRunner on Jira Cloud)
  2. the tests association should be validated properly, based on all the direct linked tests and on the ones linked to "sub-requirements"

...

Code Block
languagegroovy
titleScript content
collapsetrue
Map<String, Object> searchResult = get('/rest/api/2/search')
        .queryString('jql', "key = ${issue.key} and hasLinks = 'is tested by'")
        .asObject(Map)
        .body 

// if no Tests linked, then force a certain transition (i.e. rollback it)
if (searchResult.issues.size == 0) {
    def res = post("/rest/api/2/issue/" + issue.key + "/transitions")
    .header("Content-Type", "application/json")
    .body([
        "transition": [
            "id": "301"
        ]
    ])
    .asString()
    //assert res.status == 204    
}


Reopen/transition linked Tests to a

...

covered issue

Whenever you change the specification of a requirement, for example, you most probably will need to review the Tests that you have already specified.

The following script tries to make a transition on all linked Tests to a requirement. You can hook it to a post-function on the transition of the requirementthe covered issue.


Info
titlePlease note

This solution is not perfect because the tests association should be validated properly, based on all the direcly and indirectly linked tests.

Code Block
languagegroovy
titlereopen_linked_tests.groovyScript content
collapsetrue
import comorg.atlassianapache.jiralog4j.issue.IssueLogger
import comorg.atlassianapache.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.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.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.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.ResolutionManager
import com.atlassian.jira.workflow.WorkflowTransitionUtil
import com.atlassian.jira.workflow.WorkflowTransitionUtilImpl
import com.atlassian.jira.util.JiraUtils
import com.atlassian.jira.bc.ServiceResultImpl
import com.atlassian.jira.bc.issue.IssueService.TransitionValidationResult
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.bc.issue.DefaultIssueService
import com.opensymphony.workflow.InvalidActionException
import com.atlassian.jira.workflow.IssueWorkflowManager



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 [] 
}


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

Issue issue = issue;

jql = "issue in requirementTests('${issue.key}')"
issues = getIssues(jql)


issues.each {
    WorkflowTransitionUtil workflowTransitionUtil = ( WorkflowTransitionUtil ) JiraUtils.loadComponent( WorkflowTransitionUtilImpl.class );
    MutableIssue tempissue = issueManager.getIssueObject(it.key)
    workflowTransitionUtil.setIssue(tempissue);
    workflowTransitionUtil.setUsername(serviceAccount.getUsername());
    
    def actionId = 3  // change it accordingly
    if (issueWorkflowManager.isValidAction(tempissue, actionId)){   
        workflowTransitionUtil.setAction(actionId);//Id of the status you want to transition to
        try {
            workflowTransitionUtil.progress();
        } catch (InvalidActionException e) {
            log.error("Caught exception trying to transition issue" + e.getMessage());
        }
    }
}

Show Tests Count for a requirement

In the following example, a "script field" is used to to show the total amount of linked Tests to a given requirement.

Code Block
languagegroovy
titletotal_linked_tests_to_requirement.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


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 []
}

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();

IssueManager iManager = ComponentAccessor.getIssueManager();
Issue issue = issue;

jql = "issue in requirementTests('${issue.key}')"
issues = getIssues(jql)
issues.size

...

log4j.Level

def transitionIssue(issueKey, transitionId){
    def res = post("/rest/api/2/issue/" + issueKey + "/transitions")
    .header("Content-Type", "application/json")
    .body([
        "transition": [
            "id": transitionId
        ]
    ])
    .asString()
}

def log = Logger.getLogger("com.example.script")
log.setLevel(org.apache.log4j.Level.DEBUG)


def result = get('/rest/api/2/issue/' + issue.key)
        .header('Content-Type', 'application/json')
        .asObject(Map)
if (result.status == 200){
    def issueLinks = (List<Map<String, Object>>) result.body.fields.issuelinks
    
    def mapping = issueLinks.groupBy { issueLink -> 
        ((Map<String, Map>) issueLink).type.inward
    }.collectEntries { linkName, linkItem ->
        [(linkName): linkItem.inwardIssue.key]
    }
    
    log.debug mapping['is tested by']
    def transitionId = 11 // Done=>TODO
    mapping['is tested by'].each { testKey ->
        transitionIssue(testKey, transitionId)
    }
} else {
    return "Failed to find issue: Status: ${result.status} ${result.body}"
}


You need to:

  1. edit the workflow transition of the "coverable issue" (e.g. story, requirement), by editing the related workflow
  2. add a ScriptRunner Post Function, in the Post Functions tab  


Image Added


Image Added

Show Tests Count for a requirement

In the following example, we'll create a text custom field to show the total amount of directly linked Tests to a given coverable issue.


Info
titlePlease note

ScriptRunner for Jira Cloud does not provide scripted field; thus, we have to create a standard text (single line) custom field and we'll update it periodically. For the later, we'll use a periodic job.



Code Block
languagegroovy
titleScript content
collapsetrue
import org.apache.log4j.Logger
import org.apache.log4j.Level

def transitionIssue(issueKey, transitionId){
    def res = post("/rest/api/2/issue/" + issue.key + "/transitions")
    .header("Content-Type", "application/json")
    .body([
        "transition": [
            "id": transitionId
        ]
    ])
    .asString()
}

def log = Logger.getLogger("com.example.script")

log.setLevel(org.apache.log4j.Level.DEBUG)

def issueKey = 'CALC-1'

def result = get('/rest/api/2/issue/' + issue.key)
        .header('Content-Type', 'application/json')
        .asObject(Map)
if (result.status == 200){
    //return result.body.fields.issuelinks[0]
    
    /*
    result.body.fields.issuelinks.each { ilink ->
        if (ilink.name == "Test") {
            ilink.inwardIssue.key
        }
    }
    */
    
    
    def issueLinks = (List<Map<String, Object>>) result.body.fields.issuelinks
    
    def mapping = issueLinks.groupBy { issueLink -> 
        ((Map<String, Map>) issueLink).type.inward
    }.collectEntries { linkName, linkList ->
        [(linkName): linkList.size()]
    }
    
        log.debug mapping['is tested by']
        
    def testsCountCF = "customfield_10027"
    def res = put("/rest/api/2/issue/${issue.key}") 
     .header("Content-Type", "application/json")
     .body([
         fields:[
         (testsCountCF): mapping['is tested by'].toString()
         ]
     ])
     .asString()
 

} else {
    return "Failed to find issue: Status: ${result.status} ${result.body}"
}


You need to:

  1. create a text (single line) custom field (e.g. "Tests Count")
  2. create a ScriptRunner scheduled job

Image Added

Image Added


Image Added


Image Added

References

In scenarios with CI implemented, you may want to trigger certain Jenkins "jobs" (i.e. project build) from a Test Plan and link back the result to Xray.

In this example, we're assuming that the list of automated tests that will be run is managed in the CI side, depending on the project configuration.

The results will be submitted back to Xray, if the project is configured to do so in Jenkins.

In order to add this option in Jira's UI, we'll need to add a custom "web item" that provide an action that will interact with a custom ScriptRunner endpoint, which will be the one doing the HTTP request to the Jenkins server, passing the Test Plan issue key. In order to submit the request to Jenkins, we need to obtain Jenkins username and respective API token along with the project specific authentication token.

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();
 	}
    
}

Image Removed

Example

ScripRunner configuration

Image Removed   Image Removed

Jenkins configuration

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

Image Removed 

At the project level, we need to enable remote build triggers, so we can obtain an "authentication token" to be used in the HTTP request afterwards.

Image Removed

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.

Image Removed  

The final task submits the results linking the Test Execution to the Test Plan passed as argument.

Image Removed

Trigger a Jenkins project build from a Test Plan, for the Tests contained in the Test Plan

This scenario is somehow similar to the previous one, except that the list of Tests that will be run in the CI side will be based on the Tests contained in the Test Plan.

The results will be submitted back to Xray, if the project is configured to do so in Jenkins.

In order to add this option in Jira's UI, we'll need to add a custom "web item" that provide an action that will interact with a custom ScriptRunner endpoint, which will be the one doing the HTTP request to the Jenkins server, passing the Test Plan issue key. In the ScriptRunner endpoint script, we'll obtain the list of Generic Tests (we're assuming that they will came from Junit, so they have a certain syntax in the Generic Test Definition field.

In order to submit the request to Jenkins, we need to obtain Jenkins username and respective API token along with the project specific authentication token.

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

ScripRunner configuration

Image Removed   Image Removed

Jenkins configuration

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

Image Removed 

At the project level, we need to enable remote build triggers, so we can obtain an "authentication token" to be used in the HTTP request afterwards.

Image Removed

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

Image Removed  

Maven is configured in order to run just the tests identified in the TESTLIST variable, using the "-Dtest" JVM option.

Image Removed 

The final task submits the results linking the Test Execution to the Test Plan passed as argument.

Image Removed

Trigger a Bamboo plan build from a Test Plan

In scenarios with CI implemented, you may want to trigger certain Bamboo "plans" (i.e. builds) from a Test Plan and link back the result to Xray.

In this example, we're assuming that the list of automated tests that will be run is managed in the CI side, depending on the plan configuration.

The results will be submitted back to Xray, if the project is configured to do so in Bamboo.

In order to add this option in Jira's UI, we'll need to add a custom "web item" that provide an action that will interact with a custom ScriptRunner endpoint, which will be the one doing the HTTP request to the Bamboo server, passing the Test Plan issue key. In order to submit the request to Bamboo we just need to use the credentials of some user.

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();
 	}
    
}

Image Removed

Example

ScripRunner configuration

Image Removed   Image Removed

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.

Image Removed

The final task submits the results linking the Test Execution to the Test Plan passed as argument.

Image Removed

Trigger a Bamboo plan/stage build from a Test Plan, for the Tests contained in the Test Plan

This scenario is somehow similar to the previous one, except that the list of Tests that will be run in the CI side will be based on the Tests contained in the Test Plan.

The results will be submitted back to Xray, if the project is configured to do so in Bamboo.

In order to add this option in Jira's UI, we'll need to add a custom "web item" that provide an action that will interact with a custom ScriptRunner endpoint, which will be the one doing the HTTP request to the Bamboo server, passing the Test Plan issue key. In the ScriptRunner endpoint script, we'll obtain the list of Generic Tests (we're assuming that they will came from Junit, so they have a certain syntax in the Generic Test Definition field.

In order to submit the request to Bamboo, we need to the credentials of some Bamboo user.

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

Image Removed   Image Removed   

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 TESTPLAN and TESTLIST variables, that in our case will be coming from Jira.

Image Removed

  

Maven is configured in order to run just the tests identified in the TESTLIST variable, using the "-Dtest" JVM option.

Image Removed 

The final task submits the results linking the Test Execution to the Test Plan passed as argument.

Image Removed

Extending REST API for interacting with requirement projects

In this example, we'll be creating some endpoints for obtaining the requirement projects and also for enabling or disabling requirement coverage for a certain project.

This makes use of ScriptRunner's custom REST API capabilities.

Code Block
languagegroovy
titlexray_custom_rest_api.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.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper;
import groovy.transform.BaseScript
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate 

 
ENTITY_NAME = "com.xpandit.raven";
ENTITY_ID = 12345678987654321L;
REQUIREMENT_PROJECTS_SETTING = "requirement-coverage.projects";
 
projectManager = ComponentAccessor.getProjectManager()
 
Object obtainRequirementProjectsIds() {
    JiraPropertySetFactory jiraPropertySetFactory = ComponentAccessor.getComponent(JiraPropertySetFactory.class);
    def setting = jiraPropertySetFactory.buildCachingPropertySet(ENTITY_NAME, ENTITY_ID, true);
    def requirementProjects = Eval.me(setting.getText(REQUIREMENT_PROJECTS_SETTING))
}
 
 
Object obtainRequirementProjects() {
    def requirementProjects = obtainRequirementProjectsIds()
    log.debug("requirementProjects: "+requirementProjects)
    def availableProjects = projectManager.getProjectObjects()
    availableProjects.findAll { it.id.toInteger() in requirementProjects}
}
 
boolean enableRequirementCoverageForProject(project){
    JiraPropertySetFactory jiraPropertySetFactory = ComponentAccessor.getComponent(JiraPropertySetFactory.class);
    def setting = jiraPropertySetFactory.buildCachingPropertySet(ENTITY_NAME, ENTITY_ID, true);
    projectList = obtainRequirementProjectsIds()
    if (!projectList.contains(project.id.toInteger())){
        setting.setText(REQUIREMENT_PROJECTS_SETTING,(projectList << project.id).toString())
    }
}
 
boolean disableRequirementCoverageForProject(project){
    JiraPropertySetFactory jiraPropertySetFactory = ComponentAccessor.getComponent(JiraPropertySetFactory.class);
    def setting = jiraPropertySetFactory.buildCachingPropertySet(ENTITY_NAME, ENTITY_ID, true);
    projectList = obtainRequirementProjectsIds()
    if (projectList.contains(project.id.toInteger())){
        projectList.removeAll{it == project.id.toInteger()}       
        setting.setText(REQUIREMENT_PROJECTS_SETTING,projectList.toString())
    }
}
 
 
 
boolean requirementCoverageEnabledForProject(project){
    JiraPropertySetFactory jiraPropertySetFactory = ComponentAccessor.getComponent(JiraPropertySetFactory.class);
    def setting = jiraPropertySetFactory.buildCachingPropertySet(ENTITY_NAME, ENTITY_ID, true);
    def requirementProjects = Eval.me(setting.getText(REQUIREMENT_PROJECTS_SETTING))
    (project.id.toInteger() in requirementProjects)
}
 



// curl -u admin:admin "http://yourjiraserver/rest/scriptrunner/latest/custom/getRequirementProjects"
requirementProjects( 
    httpMethod: "GET", groups: ["jira-administrators"] 
) { MultivaluedMap queryParams, String body -> 
    return Response.ok(new JsonBuilder(obtainRequirementProjects().collect{ [id: it.id, key: it.key, name: it.name] } ).toString() ).build() 
}


// curl -u admin:admin -X DELETE "http://yourjiraserver/rest/scriptrunner/latest/custom/requirementProjects/CALC"
requirementProjects(
    httpMethod: "DELETE", groups: ["jira-administrators"]
) {MultivaluedMap queryParams, String body, HttpServletRequest request ->
    try{
        def extraPath = getAdditionalPath(request)
        projectKey = extraPath.split("/")[1]
        log.debug("projectKey: "+projectKey)
        project = projectManager.getProjectObjByKey(projectKey)
        disableRequirementCoverageForProject(project)
    } catch (e) {
        return Response.serverError().entity([error: e.message]).build()
    }
    return Response.ok().build()
}


//curl -u admin:admin -X POST -d "@data.json" -H "Content-Type: application/json" "http://yourjiraserver/rest/scriptrunner/latest/custom/requirementProjects"
requirementProjects(
    httpMethod: "POST", groups: ["jira-administrators"]
) {MultivaluedMap queryParams, String body ->
    try{
        def jsonSlurper = new JsonSlurper()
        def object = jsonSlurper.parseText(body)
        log.debug("projectKey: "+object.key)
        def project 
        if (object.key) {
            project = projectManager.getProjectObjByKey(object.key)
        } else if (project.id ){
            project = projectManager.getProjectObjById(object.id)
        }
        enableRequirementCoverageForProject(project)
    } catch (e) {
        return Response.serverError().entity([error: e.message]).build()
    }
    return Response.ok().build()
}

Example of requests

Obtaining all projects with requirement coverage enabled

No Format
curl -u admin:admin "http://yourjiraserver/rest/scriptrunner/latest/custom/requirementProjects"
Code Block
languagejs
titleResponse
[  
   {  
      "id":10300,
      "key": "CALC",
      "name":"Calculator"
   },
   {  
      "id":10501,
      "key": "DEMO",
      "name":"Demonstration"
   }
]

Enabling requirement coverage for a project

No Format
curl -u admin:admin -X POST -d "@data.json" -H "Content-Type: application/json" "http://yourjiraserver/rest/scriptrunner/latest/custom/requirementProjects"
Code Block
languagejs
titledata.json
{
 "key": "CALC"
}

Disabling requirement coverage for a project

...