Atlassian marketplace app Scriptrunner

안녕하세요. 오픈소스컨설팅에서 고객사의 JIRA/Confluence 운영 및 서비스데스크업무를 담당하고 있는 Solution-Architect 3팀의 김경아 프로입니다.

운영 및 서비스데스크 업무를 하다보면 종종 아래와 같은 문의를 받습니다.

JIRA에서 User를 한번에 등록하려면 어떻게 해야하나요?

한번에 1000명 이상의 유저를 등록하고, 해당 user에게 맞게 group을 설정/변경하고 싶은데 어떻게 하나요?


해당 문의와 관련해서는 Jira 관리자 권한으로 ⚙ Administrator > User Management > Add user로 추가해주는 방법이 있습니다. 하지만, 해당 작업은 한번에 한명씩만 작업가능하기 때문에 복수의 인원을 작업할 때는 부적합니다. 때문에 오늘은 Script Runner라는 Add-on을 사용해서 자동적으로 Jira에 사용자를 추가하고, 추가한 사용자를 해당하는 그룹에 자동으로 할당하는 방법을 소개하고자 합니다.

ScriptRunner로 작업 하기 이전에 필요한 전제 조건으로 user의 정보를 보관할 프로젝트를 작성한 후에, 각 user의 정보를 issue로 등록되어야합니다.

☝️ issue를 하나씩 등록해도 되지만, 복수의 인원을 한번에 입력하기엔 시간이 오래 걸리기 때문에 계정 생성에 필요한 내용을 csv파일 형태로 작성합니다. 그리고 JIRA에서 Issues > import Issues from CSV 또는 ⚙ Administrator > system > (IMPORT AND EXPORT) External System Import 에서 작성한 csv 파일을 upload하시면 좀 더 수월하게 작업하실 수 있습니다.


ScriptRunner란 무엇인가?

ScriptRunner는 Jira/Confluecne에서 Groovy, JavaScript, Python 등 다양한 스크립트 언어를 사용하여 기능을 확장하고 커스터마이징 할 수 있는 Add-On으로 관리자 및 개발자들에게 여러가지 자동화 도구를 제공하여 플랫폼을 보다 유연하고 맞춤화된 환경으로 만들어 줍니다.
(⚙Administrators > ScriptRunner 또는 ⚙Administrators > Manage apps > ScriptRunner로 접속가능.
⚙Administrators > Find new apps > 검색 창에 “ScriptRunner”입력 후 Free trial / Buy now로 설치가능)

ScriptRunner에서 많이 사용하는 기능은 크게 아래의 4가지로 정의할 수 있습니다.

  1. Listener (이벤트 기반 스크립트 실행) : 이벤트(이슈 생성, 이슈 업데이트 등)가 발생할 때 특정 스크립트를 실행하여 원하는 작업을 수행할 수 있습니다.
  2. Workflows (워크플로우 확장) : 스크립트를 사용하여 Jira의 워크플로우를 확장하고 사용자 정의 기능을 추가할 수 있습니다. 예를 들어, 이슈 상태 변경 시 특정 작업을 자동으로 수행하도록 설정할 수 있습니다.
  3. Jobs (자동화된 이슈 처리) : ScriptRunner를 사용하여 이슈를 자동으로 처리하고 작업을 자동화할 수 있습니다. 예를 들어, 이슈 생성 시 특정 필드를 자동으로 채우거나 이슈 상태에 따라 이메일을 자동으로 발송할 수 있습니다.
  4. script Editor (사용자 정의 스크립트 함수) : ScriptRunner를 사용하여 자신만의 사용자 정의 함수를 작성하고 이를 스크립트에서 호출할 수 있습니다. 이를 통해 개발자는 자신의 비즈니스 규칙에 따라 Jira를 조작할 수 있습니다.

ScriptRunner로 user계정생성하기

(위와 같이 issue화면을 구성했습니다.
Summary에는 해당 user의 이름, User Name에는 jira의 user id, e-mail Address에는 해당 user의 이메일 정보,
필요시에 소속그룹 또는 팀 명 그리고 직책을 입력합니다.
사용자 등록여부의 default 값은 X 로 설정합니다)

– 사용자 정보의 issue 등록시에, 해당 user가 현재 존재하는 user인지 확인하기 위해서, “사용자 등록여부“ 의 값을 자동으로 세팅하도록 Listener 에서 issue Created, issue Updated시에 아래의 동작을 하도록 설정

import com.atlassian.jira.issue.Issue
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.query.Query
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.bc.user.UserService
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.user.ApplicationUser

def searchService = ComponentAccessor.getComponent(SearchService)
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def issueManager = ComponentAccessor.getIssueManager()
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def optionManager = ComponentAccessor.getOptionsManager()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

ApplicationUser adminUser = ComponentAccessor.getUserManager().getUserByName("admin");

MutableIssue issue = ComponentAccessor.getIssueManager().getIssueByCurrentKey(event.issue.getKey());

log.warn("issue : "+issue.getKey())
    
def getDisplayName = ""
def getUserName = ""
def getEmailAddress = ""
def getDepart = ""
def getPosition = ""
    
def getUserNameObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13900') // User Name Object
def getEmailObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13901') // e-mail Address Object
def getDepartObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13903') // 소속그룹/ 팀 명 Object
def getPositionObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13904') // 직책 Object
def userCheckObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13905') // 사용자 등록여부 Object


getDisplayName = issue.getSummary() // Jira에서 사용할 user의 이름(Display명)으로 사용할 Summary의 필드값을 가져옴
getUserName = issue.getCustomFieldValue(getUserNameObj).toString().trim() // jira에서 사용할 user Name의 필드값을 가져옴
getEmailAddress = issue.getCustomFieldValue(getEmailObj).toString().trim() // Jira에서 사용할 email address의 필드값을 가져옴
getDepart = issue.getCustomFieldValue(getDepartObj).toString() // 소속그룹/팀 명의 필드값을 가져옴
getPosition = issue.getCustomFieldValue(getPositionObj).toString() // 직책의 필드값을 가져옴    

def checkConfig = ComponentAccessor?.optionsManager?.getOptions(userCheckObj.getRelevantConfig(issue))

// the username of the new user - needs to be lowercase and unique - required
final String userName = getUserName

// The password for the new user - if empty a random password will be generated
final String password = "Pass1234!"

// The email address for the new user - required
final String emailAddress = getEmailAddress
    
// The display name for the new user - required
final String displayName = getDisplayName

// notifications are sent by default, set to false to not send a notification
final boolean sendNotification = false

def userService = ComponentAccessor.getComponent(UserService)

def newCreateRequest = UserService.CreateUserRequest.withUserDetails(adminUser, userName, password , emailAddress, displayName).sendNotification(sendNotification)
    
def createValidationResult = userService.validateCreateUser(newCreateRequest)
log.warn("createValidationResult : " + createValidationResult.valid)
    
if(createValidationResult.valid){
        
    def optionToSetCheck = checkConfig.find {(String)it.value == "✕" }
    issue.setCustomFieldValue(userCheckObj, optionToSetCheck)

}else{

    log.warn("기존에 존재하는 user입니다.")
    def optionToSetCheck = checkConfig.find {(String)it.value == "〇" }
    issue.setCustomFieldValue(userCheckObj, optionToSetCheck)

}

ComponentAccessor.issueManager.updateIssue(user, issue, EventDispatchOption.DO_NOT_DISPATCH, false)

– issue의 field값을 가져와서 해당 정보를 바탕으로 Jira의 user 등록하기

import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.query.Query
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.bc.user.UserService
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.user.ApplicationUser

def searchService = ComponentAccessor.getComponent(SearchService)
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def issueManager = ComponentAccessor.getIssueManager()
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def optionManager = ComponentAccessor.getOptionsManager()

ApplicationUser adminUser = ComponentAccessor.getUserManager().getUserByName("gakim");
String queryStr = ""
// 사용자 관리 프로젝트 프로젝트에서 사용자 등록여부가 X 인 issue를 대상으로 해당 job 실시
queryStr = "project = \"사용자 관리 프로젝트\" AND \"사용자 등록여부\" = \"✕\""
Query query = jqlQueryParser.parseQuery(queryStr);
log.warn("JQL : " + query)
SearchResults<Issue> results = searchService.search(adminUser, query, PagerFilter.getUnlimitedFilter());

results.getResults()?.each{ it ->

    MutableIssue issue = ComponentAccessor.getIssueManager().getIssueByCurrentKey(it.getKey());
    
    def getDisplayName = ""
    def getUserName = ""
    def getEmailAddress = ""
    def getDepart = ""
    def getPosition = ""
    
    def getUserNameObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13900') // User Name Object
    def getEmailObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13901') // e-mail Address Object
    def getDepartObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13903') // 소속그룹/ 팀 명 Object
    def getPositionObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13904') // 직책 Object


    getDisplayName = issue.getSummary().trim() // Jira에서 사용할 user의 이름(Display명)으로 사용할 Summary의 필드값을 가져옴
    getUserName = issue.getCustomFieldValue(getUserNameObj).toString().trim() // jira에서 사용할 user Name의 필드값을 가져옴
    getEmailAddress = issue.getCustomFieldValue(getEmailObj).toString().trim() // Jira에서 사용할 email address의 필드값을 가져옴
    getDepart = issue.getCustomFieldValue(getDepartObj).toString().trim() // 소속그룹/팀 명의 필드값을 가져옴
    getPosition = issue.getCustomFieldValue(getPositionObj).toString().trim() // 직책의 필드값을 가져옴

    // the username of the new user - needs to be lowercase and unique - required
    final String userName = getUserName

    // The password for the new user - if empty a random password will be generated
    final String password = "Pass1234!"  // 초기 비밀번호 = "Pass1234!"

    // The email address for the new user - required
    final String emailAddress = getEmailAddress
    
    // The display name for the new user - required
    final String displayName = getDisplayName

    // notifications are sent by default, set to false to not send a notification
    final boolean sendNotification = false

    def userService = ComponentAccessor.getComponent(UserService)

    def newCreateRequest = UserService.CreateUserRequest.withUserDetails(adminUser, userName, password , emailAddress, displayName).sendNotification(sendNotification)
    
    def createValidationResult = userService.validateCreateUser(newCreateRequest)
    log.warn("createValidationResult : " + createValidationResult.valid)
    assert createValidationResult.valid : createValidationResult.errorCollection
    
    if(createValidationResult.valid == true){
        // 만약 해당 User가 현재 등록되어있지 않은 user인 경우에 해당 User 추가
        log.warn("현재 추가할 user : ${issue.getSummary()}")
        try{                                                        
            userService.createUser(createValidationResult)
        }catch(Exception e){
            e.printStackTrace()
        }
    }else{
        log.warn("${userName}은 기존에 존재하는 user입니다.")
    }
    
}
실행 후, ⚙ > user Management에서 user가 추가되었는지 확인하기

ScriptRunner로 user의 group관리하기

– 유저는 만들어졌지만, group은 jira-software-users 밖에 들어있지 않기 때문에, 해당 user에게 맞는 group을 설정하기
전제조건 : 소속 그룹/팀 명으로 group이 작성되어 있을 것

import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.query.Query
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.bc.user.UserService
import com.atlassian.jira.user.ApplicationUser

def searchService = ComponentAccessor.getComponent(SearchService)
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def issueManager = ComponentAccessor.getIssueManager()
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def groupManager = ComponentAccessor.getGroupManager()

def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
String queryStr = ""
//사용자 관리 프로젝트에서 사용자등록여부의 값이 입력되지 않거나, X인 사용자를 대상으로 실행
queryStr = "project = USERMANAGE AND (\"사용자 등록여부\" = ✕  OR \"사용자 등록여부\" IS EMPTY)"
Query query = jqlQueryParser.parseQuery(queryStr);
log.warn("JQL : " + query)
SearchResults<Issue> results = searchService.search(user, query, PagerFilter.getUnlimitedFilter());

results.getResults()?.each{ it ->

    MutableIssue issue = ComponentAccessor.getIssueManager().getIssueByCurrentKey(it.getKey());
    
    def getDisplayName = ""
    def getUserName = ""
    def getEmailAddress = ""
    
    def getUserNameObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13900') // user Name
    def groupObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13903') // 소속그룹/팀 명
    def emailObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13901') // e-mail Address
    
    def emailValue = issue.getCustomFieldValue(emailObj) as String
    
    def groupName = ""
    
    groupName = issue.getCustomFieldValue(groupObj) as String
    
    def userName = issue.getCustomFieldValue(getUserNameObj) as String
    def setuser = ComponentAccessor.userManager.getUserByName(userName.toString())
    log.warn("set User : "+ComponentAccessor.userManager.getUserByName(userName))
    def group = groupManager.getGroup(groupName)
    assert group : "Could not find group with name $groupName"
    
    if(setuser != null && groupManager.getGroup(groupName) != null && !groupManager.getGroupsForUser(setuser).contains(groupName)){
        log.warn("setUser : "+setuser.getUsername()+"  ||  groupName : "+group.getName())
        groupManager.addUserToGroup(setuser, groupManager.getGroup(groupName))
    }else{
        log.warn("setUser : "+setuser.getUsername() +"  ||  groupName : "+group.getName())
        log.warn("error issue Key : "+issue.getKey())
    }
    
    
    if(emailValue){
        // email adress의 도메인으로 해당 user의 소속회사 확인 후, abcde외의 회사인 경우, external-users로 설정
        if(!emailValue.contains("@abcde.co.kr")){
            def partnerGroupName = "external-users"
            def partnerGroup = groupManager.getGroup(partnerGroupName)
            assert partnerGroup : "Could not find group with name $groupName"
            groupManager.addUserToGroup(setuser, groupManager.getGroup(partnerGroupName))
        }else{
            // email adress의 도메인으로 해당 user의 소속회사 확인 후, abcde외의 회사인 경우, abcde-users로 설정
            def abcdeGroupName = "abcde-users"
            def abcdeGroup = groupManager.getGroup(abcdeGroupName)
            assert abcdeGroup : "Could not find group with name $groupName"
            groupManager.addUserToGroup(setuser, groupManager.getGroup(abcdeGroupName))
        }
    }else{
        log.warn("${issue.getKey()}의 email이 등록되어있지 않습니다.")
    }
}
→ e-mail 주소의 도메인이 (abcde.co.kr)인 “홍길동“은 소속팀인 a팀의 그룹 및 abcde-user그룹이 추가됨
→ e-mail 주소의 도메인이 (abcde.co.kr)이 아닌 “김철수“는 소속팀인 b팀의 그룹 및 external-users그룹이 추가됨

– 사용자가 Jira에 등록이 되어있을 경우, 이슈의 사용자 등록여부의 값을 “〇“로 수정하도록 설정하기
(해당 작업은 하루에 한번씩 동작하도록 Custom scheduled job에 위치해서 중복 값을 방지하고자 필드로 설정 )

import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.query.Query
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.bc.user.UserService
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.issue.index.IssueIndexingService;
import com.atlassian.jira.util.ImportUtils;

def searchService = ComponentAccessor.getComponent(SearchService)
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def issueManager = ComponentAccessor.getIssueManager()
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def optionManager = ComponentAccessor.getOptionsManager()

ApplicationUser adminUser = ComponentAccessor.getUserManager().getUserByName("admin");
String queryStr = ""
//queryStr = "project = EMPNO AND \"사용자 등록여부\" is EMPTY OR \"사용자 등록여부\" = \"X\""
queryStr = "project = USERMANAGE"
Query query = jqlQueryParser.parseQuery(queryStr);
log.warn("JQL : " + query)
SearchResults<Issue> results = searchService.search(adminUser, query, PagerFilter.getUnlimitedFilter());

results.getResults()?.each{ it ->

    MutableIssue issue = ComponentAccessor.getIssueManager().getIssueByCurrentKey(it.getKey());
    
    def getDisplayName = ""
    def getUserName = ""
    def getEmailAddress = ""
    def getDepart = ""
    def getPosition = ""
    
    def getUserNameObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13900') // User Name Object
    def getEmailObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13901') // e-mail Address Object
    def getDepartObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13903') // 소속그룹/ 팀 명 Object
    def getPositionObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13904') // 직책 Object
    def validatorCheckObj = ComponentAccessor.customFieldManager.getCustomFieldObject('customfield_13905') // 사용자 등록여부

    def checkConfig = ComponentAccessor?.optionsManager?.getOptions(validatorCheckObj.getRelevantConfig(issue))
    
    getDisplayName = issue.getSummary().trim() // Jira에서 사용할 user의 이름(Display명)으로 사용할 Summary의 필드값을 가져옴
    getUserName = issue.getCustomFieldValue(getUserNameObj).toString().trim() // jira에서 사용할 user Name의 필드값을 가져옴
    getEmailAddress = issue.getCustomFieldValue(getEmailObj).toString().trim() // Jira에서 사용할 email address의 필드값을 가져옴
    getDepart = issue.getCustomFieldValue(getDepartObj).toString().trim() // 소속그룹/팀 명의 필드값을 가져옴
    getPosition = issue.getCustomFieldValue(getPositionObj).toString().trim() // 직책의 필드값을 가져옴
     
    // the username of the new user - needs to be lowercase and unique - required
    final String userName = getUserName

    // The password for the new user - if empty a random password will be generated
    final String password = "Pass1234!"  // 초기 비밀번호 = "Pass1234!"

    // The email address for the new user - required
    final String emailAddress = getEmailAddress
    
    // The display name for the new user - required
    final String displayName = getDisplayName

    // notifications are sent by default, set to false to not send a notification
    final boolean sendNotification = false

    def userService = ComponentAccessor.getComponent(UserService)

    def newCreateRequest = UserService.CreateUserRequest.withUserDetails(adminUser, userName, password , emailAddress, displayName).sendNotification(sendNotification)
    
    
    def createValidationResult = userService.validateCreateUser(newCreateRequest)
    log.warn("createValidationResult : " + createValidationResult.valid)
    
    if(createValidationResult.valid){
        def optionToSetCheck = checkConfig.find {(String)it.value == "✕" }
        issue.setCustomFieldValue(validatorCheckObj, optionToSetCheck)
    }else{
        log.warn("기존에 존재하는 user입니다.")
        def optionToSetCheck = checkConfig.find {(String)it.value == "〇" }
        issue.setCustomFieldValue(validatorCheckObj, optionToSetCheck)
    }
    
    //JQL에서의 값을 제대로 인식하기 위해서 indexing option 추가
    boolean isIndex = ImportUtils.isIndexIssues(); 
    ImportUtils.setIndexIssues(true);
    IssueIndexingService IssueIndexingService = (IssueIndexingService) ComponentAccessor.getComponent(IssueIndexingService.class);
    
    IssueIndexingService.reIndex(issue);

    ImportUtils.setIndexIssues(isIndex);

    ComponentAccessor.issueManager.updateIssue(adminUser, issue, EventDispatchOption.DO_NOT_DISPATCH, false)
}
→ User가 추가되지 않은 김영희는 “✕”인 상태 유지

마치며

이번 포스팅(ScriptRunner를 통한 유저관리 포스팅 #1) 에서는 user관리를 위한 유저의 계정생성 및 생성한 유저의 group에 추가하는 방법을 소개해보았습니다. #2에서는 해당 user의 최종로그인 기록 및 조직개편등의 이유로 해당 user의 full name을 변경해야 할 경우 등 좀 더 편리하고 유용하게 사용할 수 있는 scriptRunner활용법을 소개하도록 하겠습니다.

긴 글 읽어주셔서 감사합니다:)

Leave a Reply

Your email address will not be published. Required fields are marked *