Heads up! ScriptRunner documentation is moving to docs.adaptavist.com. Adaptavist will keep this site up for a bit, but no future updates to documentation will be published here. ScriptRunner 6.20.0 will be the last release to link to scriptrunner.adaptavist.com for in-app help.

Example Scenario

Let’s say you have business logic that dictates that the component lead(s) must be added as watchers if the priority is Blocker.

You ignore the sensible option, just for the purpose of my example, and decide to write a script to add the component leads as watchers on the Start Progress transition.

Your script might look like:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue

// this comes to us in the binding... so this line is unnecessary other than to give my IDE "type" information
Issue issue = issue

// get some components we need
def watcherManager = ComponentAccessor.getWatcherManager()
def userUtil = ComponentAccessor.getUserUtil()

// would be better to use componentLead rather than lead, but not available until 6.3
issue.componentObjects*.lead.each {String username ->
    def applicationUser = userUtil.getUserByKey(username)
    watcherManager.startWatching(applicationUser, issue)
}

We will write a test for the above script which throws various combinations at it. The test will contain helper methods to help set up a project, workflow and workflow scheme.

The entire test class can be found below:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.project.AssigneeTypes
import com.atlassian.jira.project.Project
import com.atlassian.jira.project.type.ProjectTypeKey
import com.atlassian.jira.scheme.Scheme
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.workflow.JiraWorkflow
import com.onresolve.jira.groovy.GroovyFunctionPlugin
import com.onresolve.scriptrunner.canned.jira.utils.CustomScriptDelegate
import com.onresolve.scriptrunner.canned.jira.workflow.postfunctions.CustomScriptFunction
import com.opensymphony.workflow.loader.ActionDescriptor
import com.opensymphony.workflow.loader.DescriptorFactory
import spock.lang.Shared
import spock.lang.Specification

import static com.atlassian.jira.project.AssigneeTypes.PROJECT_DEFAULT

class TestAddComponentLeadsAsWatchers extends Specification {

    private static final String IN_PROGRESS = "In Progress"
    private static final String JOE_LEAD = "joelead"

    @Shared
    def projectService = ComponentAccessor.getComponent(ProjectService)

    @Shared
    def projectComponentManager = ComponentAccessor.projectComponentManager

    @Shared
    def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser

    @Shared
    def workflowManager = ComponentAccessor.workflowManager

    @Shared
    def workflowSchemeManager = ComponentAccessor.workflowSchemeManager

    @Shared
    def userService = ComponentAccessor.getComponent(UserService)

    @Shared
    def userManager = ComponentAccessor.userManager

    @Shared
    def userUtil = ComponentAccessor.userUtil

    def watcherManager = ComponentAccessor.watcherManager
    def issueService = ComponentAccessor.issueService
    def constantsManager = ComponentAccessor.constantsManager

    @Shared
    Project project

    /**
     * Create the workflow
     * add the post-function at the Start Progress step
     * Add some components, some with leads, some without
     */
    def setupSpec() {
        project = createTestSoftwareProject()

        def draftWorkflow = createDraftWorkflowForProject(project)

        addPostFunctionToDraftWorkflow(draftWorkflow, IN_PROGRESS, [
            "canned-script"                           : CustomScriptFunction.name,
            (CustomScriptDelegate.FIELD_SCRIPT_FILE)  : "com/acme/scriptrunner/scripts/AddComponentLeadsAsWatchers.groovy",
            "class.name"                              : GroovyFunctionPlugin.name
        ])

        publishDraftWorkflow(draftWorkflow)

        createUser(JOE_LEAD)

        projectComponentManager.create("Comp1", "has an assignee", JOE_LEAD, PROJECT_DEFAULT, project.id)
        projectComponentManager.create("Comp2", "no assignee", null, PROJECT_DEFAULT, project.id)
    }

    def cleanupSpec() {
        removeTestSoftwareProject()
        removeUser(JOE_LEAD)
    }

    def "test script with workflow transitions"() {
        given:
        def issue = createIssueWithComponents(project, componentNames)

        when:
        watcherManager.startWatching(currentUser, issue)
        issue = transitionIssue(issue, IN_PROGRESS)

        then:
        // do a comparison as a Set because we don't care about the order of the watcher names
        watcherManager.getCurrentWatcherUsernames(issue) as Set == expectedWatchers as Set

        // note the current user is always expected to be a watcher, as it's jira's behaviour
        // (provided you haven't turned off autowatch for the user running the test)
        // a useful improvement to this would be to set it

        where:
        componentNames     | expectedWatchers
        []                 | [currentUser.name]
        ["Comp1"]          | [currentUser.name, JOE_LEAD]
        ["Comp1", "Comp2"] | [currentUser.name, JOE_LEAD]
    }

    private Issue createIssueWithComponents(Project project, List<String> componentNames) {
        def inputParameters = new IssueInputParametersImpl().with {
            setProjectId(project.id)
            setSummary("my summary")
            setReporterId(currentUser.name)
            setIssueTypeId(constantsManager.allIssueTypeObjects.find { it.name == "Bug" }.id)
            setComponentIds(componentNames.collect { name -> projectComponentManager.findByComponentName(project.id, name).id } as Long[])
        }

        def createValidationResult = issueService.validateCreate(currentUser, inputParameters)
        assert !createValidationResult.errorCollection.hasAnyErrors()

        issueService.create(currentUser, createValidationResult).issue
    }

    private Issue transitionIssue(Issue issue, String actionName) {
        def workflow = getWorkflowForProject(project)
        def action = getActionInWorkflow(workflow, actionName)

        def transitionValidationResult = issueService.validateTransition(currentUser, issue.id, action.id, new IssueInputParametersImpl())
        issueService.transition(currentUser, transitionValidationResult).issue
    }

    private ApplicationUser createUser(String username) {
        def password = "password"
        def emailAddress = "${username}@example.com"

        def createUserRequest = UserService.CreateUserRequest.withUserDetails(currentUser, username, password, emailAddress, username)
        def validationResult = userService.validateCreateUser(createUserRequest)
        assert !validationResult.errorCollection.hasAnyErrors()

        userService.createUser(validationResult)
    }

    private void removeUser(String username) {
        def userToRemove = userManager.getUserByName(username)

        def validationResult = userService.validateDeleteUser(currentUser, userToRemove)
        assert !validationResult.errorCollection.hasAnyErrors()

        userUtil.removeUser(currentUser, userToRemove)
        assert !userUtil.userExists(userToRemove.name)
    }

    private Project createTestSoftwareProject() {
        def creationData = new ProjectCreationData.Builder().with {
            withName("Test Project")
            withKey("TEST")
            withLead(currentUser)
            withAssigneeType(AssigneeTypes.PROJECT_LEAD)
            withType(new ProjectTypeKey('software'))
            withProjectTemplateKey("com.pyxis.greenhopper.jira:basic-software-development-template")
        }

        def result = projectService.validateCreateProject(currentUser, creationData.build())
        assert !result.errorCollection.hasAnyErrors()

        projectService.createProject(result)
    }

    private void removeTestSoftwareProject() {
        def deleteProjectValidationResult = projectService.validateDeleteProject(currentUser, project.key)
        assert !deleteProjectValidationResult.errorCollection.hasAnyErrors()

        projectService.deleteProject(currentUser, deleteProjectValidationResult)

        def scheme = workflowSchemeManager.getSchemeFor(project)
        removeWorkflowSchemeIfUnused(scheme)
    }

    private void removeWorkflowSchemeIfUnused(Scheme scheme) {
        def projects = workflowSchemeManager.getProjects(scheme)
        if (!projects) {
            def workflows = workflowManager.getWorkflowsFromScheme(scheme).toUnique()
            workflowSchemeManager.deleteScheme(scheme.id)
            workflows.each { workflow ->
                def schemes = workflowSchemeManager.getSchemesForWorkflow(workflow)
                if (!schemes && !workflow.systemWorkflow) {
                    workflowManager.deleteWorkflow(workflow)
                }
            }
        }
    }

    private JiraWorkflow getWorkflowForProject(Project project) {
        def workflowName = getProjectWorkflowName(project)
        workflowManager.getWorkflow(workflowName)
    }

    private JiraWorkflow createDraftWorkflowForProject(Project project) {
        def workflowName = getProjectWorkflowName(project)
        def workflow = workflowManager.createDraftWorkflow(currentUser, workflowName)
        workflow
    }

    private void addPostFunctionToDraftWorkflow(JiraWorkflow draftWorkflow, String actionName, Map<String, String> args) {
        def action = getActionInWorkflow(draftWorkflow, actionName)

        def postFunction = DescriptorFactory.factory.createFunctionDescriptor()
        postFunction.type = 'class'
        postFunction.args << args

        action.unconditionalResult.postFunctions.add(postFunction)
    }

    private void publishDraftWorkflow(JiraWorkflow draftWorkflow) {
        workflowManager.updateWorkflow(currentUser, draftWorkflow)
        workflowManager.overwriteActiveWorkflow(currentUser, draftWorkflow.name)
    }

    private ActionDescriptor getActionInWorkflow(JiraWorkflow workflow, String actionName) {
        workflow.allActions.find { ad -> ad.name == actionName } as ActionDescriptor
    }

    private String getProjectWorkflowName(Project project) {
        def workflowScheme = workflowSchemeManager.getWorkflowSchemeObj(project)
        workflowScheme.actualDefaultWorkflow
    }
}

Then we write a single test. I used the Spock data table idiom. Whilst initially hard to setup, it’s very easy to add new test combinations.

In testing adding a component that doesn’t have a lead, I got an error from my script, and a test failure…​

ERROR [scriptrunner.jira.workflow.ScriptWorkflowFunction] Script function failed on issue: SRTESTPRJ-3, actionId: 4, file: examples/docs/AddComponentLeadsAsWatchers.groovy
groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method com.atlassian.jira.issue.watchers.DefaultWatcherManager#startWatching.
Cannot resolve which method to invoke for [null, class com.atlassian.jira.issue.IssueImpl] due to overlapping prototypes between:
    [interface com.atlassian.crowd.embedded.api.User, interface com.atlassian.jira.issue.Issue]
    [interface com.atlassian.jira.user.ApplicationUser, interface com.atlassian.jira.issue.Issue]
    at com.atlassian.jira.issue.watchers.WatcherManager$startWatching$1.call(Unknown Source)
    at examples.docs.AddComponentLeadsAsWatchers$_run_closure1.doCall(AddComponentLeadsAsWatchers.groovy:16)

This happened because I was passing null (the component without a lead) to startWatching, and so required a small change to the script to handle the case where the component lead is not set, namely:

...
if (applicationUser) {
    watcherManager.startWatching(applicationUser, issue)
}
...

Have questions? Visit the Atlassian Community to connect, share, and learn with other Atlassian users and experts, including Adaptavist staff.

Ask a question about ScriptRunner for JIRA, Bitbucket Server, or Confluence.

Want to learn more? Check out courses on Adaptavist Learn, an online platform to onboard and train new users for Atlassian solutions.