How to use Xray GraphQL in ScriptRunner on Jira Cloud

Xray Test Management for Jira and ScriptRunner for Jira are very popular apps used by many organizations to enhance their processes. Xray is known for its testing features while ScriptRunner is known as tool used for scripting and automating repetitive tasks.

Why use Xray:

  • Supports full testing life cycle: planning, design, execution and reporting
  • Seamless integration with Jira, supporting both manual and automated tests
  • Compatible with CI tools such as Jenkins and Bamboo
  • Consistent test management with Jira issue types, customizable workflows, and attributes
  • Supports importing automated tests and results from frameworks like TestNG, JUnit, and NUnit

Why use ScriptRunner:

  • Gain greater control over issue transitions with custom Workflow extensions, defining actions and conditions
  • Automate tasks like issue creation and report emailing using Scheduled Jobs
  • Use Script Listeners to trigger predefined actions based on events
  • Keep teams aligned with real-time calculated data through Scripted Fields
  • Enhance your search capabilities with advanced JQL functions via Enhanced Search

Using Xray GraphQL

To start working with Xray GraphQL, you need to create a Client ID and Client Secret for authorization. This can be done by navigating to Xray settingsAPI KeysCreate new token for your user. Be sure to copy both the Client Secret and Client ID as you’ll need them shortly.

Once you have these credentials, you can retrieve the token required for authorization when calling Xray GraphQL. Below is a simple code snippet for retrieving and printing the token in ScriptRunner’s Script Console:

def clientId = "your_client_id"
def clientSecret = "your_client_secret"


def token_Result = post('https://xray.cloud.getxray.app/api/v2/authenticate')
        .header('Content-Type', 'application/json')
        .body([
                client_id: clientId,
                client_secret: clientSecret
        ])
        .asObject(Map)

if (tokenResult.status == 200) {
    def token = token_Result.headers["x-access-token"][0]
    println(token)
} else {
    println("Failed to retrieve token. Status: " + tokenResult.status)
}

NOTE: If you’re creating an automation script, make sure to place this code at the beginning of your script. This ensures that a new token is generated every time the script runs. Xray tokens typically have a limited lifespan, so it’s essential to generate a fresh token to maintain access to the Xray GraphQL API.

Once you have retrieved the token, you can start making GraphQL requests. For example, to retrieve details about a Test Execution by its issue ID, you can use the following query:

def query = """{
    getTestExecution(issueId: "${issue_id}") {
        issueId
        tests(limit: 100) {
            total
            start
            limit
            results {
                issueId
                testType {
                    name
                }
            }
        }
    }
}"""

def execResult = post('https://xray.cloud.getxray.app/api/v2/graphql')
            .header('Content-Type', 'application/json')
            .header('Authorization', 'Bearer ' + token)
            .body([
                query: query
            ])
            .asObject(Map)

if (execResult.status == 200) {
    println(execResult.body)
} else {
    println("Failed to retrieve test execution. Status: "    +execResult.status)
}

Make sure to check the syntax of your GraphQL query carefully, as a misplaced character or missing bracket can cause the request to fail. Printing the result helps you verify if the correct data is returned.

If you want to test updating data via GraphQL, such as changing the status of a test run, you can use a GraphQL mutation. Here’s an example of how to update the status of a test run:

def mutation = """mutation {
    updateTestRunStatus(id: "${testRunId}", status: "UNBLOCKED")
}"""

def result = post('https://xray.cloud.getxray.app/api/v2/graphql')
      .header('Content-Type', 'application/json')
      .header('Authorization', 'Bearer ' + token)
      .body([
          query: mutation
      ])
      .asObject(Map)

if (result.status == 200) {
    println(execResult.body)
} else {
    println("Failed to update test run status. Status: " + result.status)
}

NOTE: When working with mutations (updates) in GraphQL, it’s crucial to ensure that the mutation syntax is correct. Always test mutations carefully and check the results returned to confirm that the update was successful.

Real life example

The following script automatically unblocks test runs (by changing their statuses from BLOCKED or FAILED to UNBLOCKED) for test executions that are linked to issues which have been transitioned to the “Done” status (i.e., the issue is closed).

// Authentication
def clientId = "your_client_id"
def clientSecret = "your_client_secret"
def issueKey = issue.key

// Fetching token
def tokenResult = post('https://xray.cloud.getxray.app/api/v2/authenticate')
        .header('Content-Type', 'application/json')
        .body([
            client_id: clientId,
            client_secret: clientSecret
        ])
        .asObject(Map)

def token = tokenResult.headers["x-access-token"][0]
println("Token: " + token)

// Fetch issue links
def result = get('/rest/api/2/issue/' + issueKey)
        .header('Content-Type', 'application/json')
        .asObject(Map)

def issueLinks = result.body.fields.issuelinks

if(issueLinks.size() > 0) {
    issueLinks.each { link ->
        def linkedIssueType = ""
        def linkedIssueId = ""
        
        if (link.inwardIssue) {
            linkedIssueType = link.inwardIssue.fields.issuetype.name
            linkedIssueId = link.inwardIssue.id
        } else if (link.outwardIssue) {
            linkedIssueType = link.outwardIssue.fields.issuetype.name
            linkedIssueId = link.outwardIssue.id
        }

        if(linkedIssueType == "Test Execution") {
            checkAndUpdateTestExecutionsAndRuns(linkedIssueId, token, issueKey)
        }
    }
}

// Function to check and update test executions
def checkAndUpdateTestExecutionsAndRuns(String issueId, String token, String issueKey) {
    println("Test Execution Issue ID: " + issueId)

    def query = """{
        getTestExecution(issueId: "${issueId}") {
            issueId
            tests(limit: 100) {
                total
                start
                limit
                results {
                    issueId
                    testType {
                        name
                    }
                }
            }
        }
    }"""

    def execResult = post('https://xray.cloud.getxray.app/api/v2/graphql')
            .header('Content-Type', 'application/json')
            .header('Authorization', 'Bearer ' + token)
            .body([query: query])
            .asObject(Map)

    def executions = execResult.body.data.getTestExecution.tests.results
    println("Executions: " + executions)

    if(executions.size() > 0) {
        executions.each { execution ->
            println("Test Run Issue ID: " + execution.issueId)
            checkAndUpdateTestRun(issueId, execution.issueId, token, issueKey)
        }
    }
}

// Function to check and update test run status
def checkAndUpdateTestRun(String exeIssueId, String tesRunIssueId, String token, String issueKey) {
    def query = """{
        getTestRun(testIssueId: "${tesRunIssueId}", testExecIssueId: "${exeIssueId}") {
            id
            status {
                name
            }
            defects
            steps {
                defects
            }
        }
    }"""

    def testRunResult = post('https://xray.cloud.getxray.app/api/v2/graphql')
            .header('Content-Type', 'application/json')
            .header('Authorization', 'Bearer ' + token)
            .body([query: query])
            .asObject(Map)

    def testRunStatus = testRunResult.body.data.getTestRun.status.name
    println("Test Run Status: " + testRunStatus)
    def testRunId = testRunResult.body.data.getTestRun.id
    println("Test Run ID: " + testRunId)

    if(testRunStatus in ["FAILED", "BLOCKED"]) {
        handleDefects(testRunResult.body.data.getTestRun.defects, token, testRunId, issueKey)
        handleStepDefects(testRunResult.body.data.getTestRun.steps, token, testRunId, issueKey)
    }
}

// Function to handle defects in test runs
def handleDefects(def defects, String token, String testRunId, String issueKey) {
    if (defects.size() > 0) {
        defects.each { defectId ->
            def defectIssueKey = getIssueKey(defectId)
            if (defectIssueKey == issueKey) {
                updateTestRunStatus(testRunId, token)
            }
        }
    }
}

// Function to handle defects in test steps
def handleStepDefects(def steps, String token, String testRunId, String issueKey) {
    steps.each { step ->
        if (step.defects.size() > 0) {
            step.defects.each { stepDefectId ->
                def stepDefectIssueKey = getIssueKey(stepDefectId)
                if (stepDefectIssueKey == issueKey) {
                    updateTestRunStatus(testRunId, token)
                }
            }
        }
    }
}

// Function to get issue key from the issue ID
def getIssueKey(String issueId) {
    def result = get('/rest/api/2/issue/' + issueId)
            .header('Content-Type', 'application/json')
            .asObject(Map)
    return result.body.key
}

// Function to update test run status
def updateTestRunStatus(String testRunId, String token) {
    def mutation = """mutation {
        updateTestRunStatus(id: "${testRunId}", status: "UNBLOCKED")
    }"""

    def result = post('https://xray.cloud.getxray.app/api/v2/graphql')
            .header('Content-Type', 'application/json')
            .header('Authorization', 'Bearer ' + token)
            .body([query: mutation])
            .asObject(Map)

    if (result.status == 200) {
        println("Test run status has been successfully updated to  UNBLOCKED")
    } else {
        println("Failed to update test run status")
    }
}

Keep in mind

In addition to the notes mentioned throughout the article, consider these potential issues when working with Xray and ScriptRunner:

  • ScriptRunner timeout – Script executions have a 240-second time limit. If your script exceeds this limit, it will be terminated. Aim to optimize your queries and limit results to ensure faster execution. Link to documentation.
  • Xray limits – Xray imposes rate limits of 60 calls per minute (300 per 5 minutes) for Xray Standard and 200 calls per minute (1000 per 5 minutes) for Xray Enterprise. To avoid reaching these limits, minimize unnecessary calls, and avoid running similar scripts with overlapping triggers. Link to documentation
  • IP alowlisting – If your Jira instance uses IP allowlisting, make sure to include the IP addresses for both Xray and ScriptRunner to avoid connection issues.
  • Permissions – ScriptRunner requires the appropriate permissions to access and process issues, especially in projects with issue security schemes. Ensure that ScriptRunner has the necessary permissions across all relevant projects.

Reach out!

I hope this article was helpful to you! If you have any additional questions, want to dive deeper into the topic, or have ideas for improvement, I’d love to hear from you.

You can find links to my LinkedIn profile and email at the bottom of the page, or feel free to reach out via the Contact page.