ScriptRunner를 이용하여 Jira 이슈 생성시 설명(Description) 기본 값 설정하기

Needs: Jira 이슈 생성시 설명(Description) 항목 기본 값 설정하기

Jira를 사용하다 보면 이슈를 생성할 때 설명 항목에 자주 쓰는 내용들이 있습니다.
예를 들면, 현재 사용하는 프로젝트의 버그 이슈에서 버그에 대한 상세한 설명, 참고 사항 등이 있죠.
현재 사용하고 있는 프로젝트에서는 아래와 같은 내용을 버그 티켓 생성시 작성하고 있습니다.

예시: 버그 이슈 설명(Description) 내용

1
2
3
4
5
6
7
8
9
10
(QA 환경)
환경 설명
(상세 설명)
상세 설명
(참고)
참고 설명
(영상)
영상 링크
(기대 결과)
기대 결과 설명

이러한 내용을 항상 이슈 생성시 마다 작성하는 것도 번거롭고, 각 작성자가 입력하는 내용도 일정하지 않을 때가 있었습니다.
이런 상황들을 개선해보고자 기본 값, 템플릿을 설정할 수 있는지 확인해보았고 다른 Jira 앱(플러그인)들이 있지만 현재 설치되어있는 ScriptRunner로 개선해보았습니다.

ScriptRunner로 설명 항목 기본 값 설정 Flow

ScriptRunner에는 많은 기능들이 있지만 그 중에서 Behaviours 기능을 사용하여 특정 항목의 기본 값 설정을 해보겠습니다.
순서는 다음과 같이 진행합니다. (ScriptRunner 가이드 문서 - Setting Field Defaults에도 있는 내용이니 참고해주세요!)

ScriptRunner Behaviours

  1. Jira > Manage apps > Behaviours 이동
  2. Add Behaviours 추가
  3. Add Mapping - 프로젝트 및 이슈 타입 설정
  4. Fields 설정 > Initialiser 스크립트 설정

Add Behaviours 추가

첫 번째 설정으로 이동하는 단계는 스킵하고 Behaviours를 추가하는 것 부터 진행하겠습니다.
위에 있던 이미지 항목대로 behaviour 이름을 입력해주고 Add 버튼을 누르면 끝입니다.

Add Mapping - 프로젝트 및 이슈 타입 설정

  • Choose projects: 프로젝트는 기본 값 설정이 필요한 프로젝트를 선택하여 지정해주시면 됩니다.
  • Choose Issue types: 모든 이슈 타입 또는 특정 이슈 타입에 대해서 지정할 수 있습니다.

이 설정으로 behaviour가 동작할 프로젝트, 이슈 타입이 맵핑되었습니다.

Fields 설정 > Initialiser 스크립트 설정

Add Behaviours 화면에서 만든 behaviour 항목에 있는 Fields를 눌러 설정을 진행합니다.
처음에는 Initialiser Function이 없다고 나오는데 Create initialiser를 눌러 생성해주세요.
생성하고 나면 위와 같은 스크립트를 입력할 수 있는 항목을 볼 수 있습니다. 스크립트를 입력해볼까요?

스크립트

1
2
3
4
5
6
7
8
9
10
11
import com.atlassian.jira.component.ComponentAccessor
import static com.atlassian.jira.issue.IssueFieldConstants.DESCRIPTION;

if (getActionName() != "Create Issue") {
return // not the initial action, so don't set default values
}

def desc = getFieldById(DESCRIPTION);
if (!desc.getValue()) {
desc.setFormValue("test form");
}

스크립트는 심플해서 설명할 내용은 많지 않네요.

  • Create Issue 타입의 액션이 아니면 스크립트 실행 중지
  • getFieldById()를 가져오고 setFormValue() 함수를 통해 이슈 생성시 form에 기본 값을 입력

스크립트러너 가이드 문서에 있는 내용이 더 복잡한 내용을 담고 있어 다른 항목의 기본 값 설정시 참고하시면 좋을 것 같습니다.
기본 값 설정시 setFormValue() 함수에 string 값으로 설정해주시면 됩니다.
이미지와 스크립트가 다른 것은 설명 값이 있는지 유무를 확인하는 정도이니 설정시 참고부탁드립니다. 😀

설명 항목 기본 값 설정시 참고

설명 항목 기본 값 설정하면서 몇가지 참고할 만한 사항이 있습니다.

  • 기본적으로 설명 항목은 HTML 값을 인식하기에 줄바꿈이 필요할 경우 <br/>을 입력해서 설정해주시면 됩니다.
  • h1, h2와 같은 항목도 마찬가지로 <h2>, <h3>로 입력해서 설정해주시면 됩니다.
  • 만약 현재 Jira 시스템에 JEditor가 설치되어 사용중일 경우 JEditor 기능을 사용하여 기본 값을 설정해주셔야 합니다.
    • 이건 ScriptRunner 기능보다는 JEditor 내용이니 다른 포스트에서 다뤄보겠습니다.

마무리

ScriptRunner 기능 중에 Behaviours 기능을 이용하여 기본 값 설정을 해보았습니다.
사실 다른 Jira 앱을 통해서 쉽게 설정할 수 있는데 ScriptRunner는 코드를 입력해야하는 점이 어려울 수 있겠다는 생각이 드네요.
(그만큼 커스텀할 수 있는 것이 많아서 좋긴 하지만요. ㅎㅎ)

다른 항목에 대한 기본 값 설정은 가이드 문서를 참고해주시구요. 다른 유용한 기능이 있다면 포스팅해보겠습니다.
긴글 읽어주셔서 감사합니다. 👍

참고 문서 / 링크

Jira Bulk remove issue links (by link type)

Jira 이슈 링크(issue link) 삭제하기

관련 포스트: Bulk remove change Jira issue links
위 포스트에서는 이슈에 걸려있는 모든 링크를 삭제하는 것을 공유드렸었습니다.
이번 포스트에서는 이슈에 걸려있는 링크 중에 특정 타입의 링크를 삭제하는 방법을 공유해보겠습니다.

이슈 - 이슈 링크

Jira에서 이슈와 이슈를 연결해주는 이슈 링크,
전의 포스트에서는 링크를 그냥 다 삭제하는 포스트였기에 자세히 다루지는 않았지만 이번 포스트에서는 링크 타입에 대해 알아야 정확하게 삭제할 수 있기에 조금 더 자세히 설명해보고자 합니다.

Administration > issues > Issue linking 설정 화면

위와 같이 이슈 링크 속성에는 Name, Outward Description, Inward Description 항목이 있습니다.

한국어 버전으로는 이슈 링크 속성 이름이 명확한지 모르겠지만 다음과 같네요. 이름(Name), 가르키는 입장 설명(Outward Desc), 받는 입장 설명(Inward Desc)

Jira 시스템에 기본적으로 있는 이슈 링크 타입은 4개로 Blocks, Cloners, Duplicate, Relates 이렇게 구성되어 있습니다.
각각 링크 타입 항목들을 보면 이슈와 이슈를 연결했을 때 각 이슈에 보이는 링크의 이름이 다르게 보일 수 있도록 설정할 수 있습니다.

이슈 링크를 설정하고 어떻게 보이는지 아래의 문서 링크를 참고하시면 좋을 것 같습니다.
(EN) Jira Docs: linking issues

“이슈 - 이슈 링크”의 관계를 정리해보자면, 아래와 같이 볼 수 있겠습니다.

  • 이슈는 목적지
  • 이슈 링크는 출발지와 도착지 정보를 가지고 있는 화살표

이슈 링크 타입 확인해보자

개념적인 설명은 이만 마치고 스크립트를 통해 이슈에 있는 이슈 링크들을 한번 확인해보겠습니다.
우선 테스트를 위해 이슈에 설정한 이슈 링크들은 아래와 같습니다.

PUB-16 issue link 설정

링크된 항목들을 정리해보면 다음과 같습니다. 괄호 내용은 이슈 링크 이름입니다.

  • PUB-16 (blocks) PUB-1
  • PUB-16 (blocks) PUB-2
  • PUB-16 (blocks) PUB-3
  • PUB-16 (clones) PUB-5570
  • PUB-16 (duplicates) PUB-17
  • PUB-16 (is blocked by) PUB-4

PUB-16 이슈에 연결된 이슈 링크들을 확인하기 위한 스크립트 입니다.
스크립트를 ScriptRunner에 Script console에서 실행된 내용을 볼 수 있습니다.

스크립트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

def searchService = ComponentAccessor.getComponent(SearchService)
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def results = searchService.search(user, jqlQueryParser.parseQuery("key=PUB-16"), PagerFilter.getUnlimitedFilter())
def issueManager = ComponentAccessor.getIssueManager()
def issueLinkManager = ComponentAccessor.getIssueLinkManager()

results.getResults().each
{
def issue = issueManager.getIssueObject(it.id)
log.warn("Get InwardLinks")
def linkInwardList = issueLinkManager.getInwardLinks(issue.getId())
linkInwardList.each {
log.warn("${it.getIssueLinkType().getName()}, ${it.getSourceObject().getKey()}")
}
log.warn("Get OutwardLinks")
def linkOutwardList = issueLinkManager.getOutwardLinks(issue.getId())
linkOutwardList.each {
log.warn("${it.getIssueLinkType().getName()}, ${it.getDestinationObject().getKey()}")
}
}

스크립트 실행 결과

1
2
3
4
5
6
7
8
...: Get InwardLinks
...: Blocks, PUB-4
...: Duplicate, PUB-17
...: Get OutwardLinks
...: Blocks, PUB-1
...: Blocks, PUB-2
...: Blocks, PUB-3
...: Clones, PUB-5570

이슈 링크 타입에 따라 이슈 링크를 삭제해보자

이슈 링크 타입을 확인했으니 이슈 링크 타입에 따라 삭제할 수 있게 되었습니다.
사실 위 링크 확인 스크립트 코드에서 타입inward/outward 만 확인해서 이슈 링크 삭제 함수만 사용하면 우리가 원하는 링크 삭제가 가능합니다.

이슈 링크 타입 확인 & 삭제

이슈 링크 타입을 확인하는 것 자체는 어렵지 않습니다.
issueLinkObj.getIssueLinkType().getName()로 이름을 확인하면 되니까요.
다만 Inward, Outward 링크 타입을 확인해야하는데, 삭제할 때에 아래 옵션을 선택할 수 있도록 함수를 만들어야겠네요.

  • Inward 링크만
  • Outward 링크만
  • 모든 링크

삭제 함수도 간단합니다. 참고: Jira API Docs: IssueLinkManager.removeIssueLink()
아래 함수를 위에서 확인한 이슈 링크를 파라미터로 호출하면 링크 삭제가 가능합니다.

1
2
3
// issueLink - the issue link to remove
// remoteUser - needed for creation of change items
void removeIssueLink(IssueLink issueLink, ApplicationUser remoteUser)

스크립트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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

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

def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
// NOTE: jql options
def results = searchService.search(user, jqlQueryParser.parseQuery("key=PUBGTEST-3"), PagerFilter.getUnlimitedFilter())

// NOTE: remove link options
def removeLinkType = "Blocks"
def removeLinkIO = "all" // all, inward, outward

def removeInwardLinks = {MutableIssue issue ->
def linkInwardList = issueLinkManager.getInwardLinks(issue.getId())
linkInwardList.each {
def link = it
if (link.getIssueLinkType().getName() == removeLinkType) {
log.warn("removed links: ${link.getIssueLinkType().getName()}, ${link.getSourceObject().getKey()}")
issueLinkManager.removeIssueLink(link, user)
}
}
}
def removeOutwardLinks = {MutableIssue issue ->
def linkInwardList = issueLinkManager.getOutwardLinks(issue.getId())
linkInwardList.each {
def link = it
if (link.getIssueLinkType().getName() == removeLinkType) {
log.warn("removed links: ${it.getIssueLinkType().getName()}, ${it.getDestinationObject().getKey()}")
issueLinkManager.removeIssueLink(link, user)
}
}
}

results.getResults().each {
def issue = issueManager.getIssueObject(it.id)
if (removeLinkIO == "all" || removeLinkIO == "inward") {
log.warn("Get & Remove InwardLinks")
removeInwardLinks(issue)
}

if (removeLinkIO == "all" || removeLinkIO == "outward") {
log.warn("Get & Remove OutwardLinks")
removeOutwardLinks(issue)
}
}

스크립트 결과

현재 스크립트 상으로는 all(inward, outward), blocks 타입의 링크만 삭제하도록 구성되어 있어 실행시 blocks 타입의 링크가 모두 삭제됩니다.

활용

위 스크립트를 활용시에는 NOTE: 라고 표시된 내용에 링크 삭제가 필요한 이슈, 링크 타입을 알맞게 변경을 한 뒤에 사용하시면 됩니다.
이슈 링크 삭제 시에 주의해서 사용해주세요!

마무리

이번 내용은 약간 길어졌는데요. 포스트를 작성하면서 이슈와 이슈 링크의 관계, 이슈 링크의 속성 등에 대해서 공부할 수 있어서 좋았고 이 글을 보시는 다른분들도 스크립트를 이용하여 원하는 이슈 링크를 삭제할 수 있었으면 좋겠습니다.
감사합니다. 😀

Bulk remove change Jira issue links

Jira 이슈에 연결되어있는 이슈 링크 한번에 삭제하기

참고: https://community.atlassian.com/t5/Jira-questions/Bulk-remove-change-issue-links-in-Jira/qaq-p/659381

문제 인식

최근 특정 Jira 이슈에 연결된 이슈들을 삭제해야하는 일이 있었습니다.
(한 작업 이슈에 확인한 버그를 모두 링크를 달고 링크된 버그 이슈들을 보다가 링크 타입이 잘못된 것도 있는 등의 이슈가 있어 수정이 필요했습니다.)
Bulk로 링크들을 삭제하는 기능이 있는지 찾아보았는데 없더군요. 그래서 스크립트러너로 가능한지 찾아보았습니다.

하나하나에 커서를 놓고 지워야합니다

다행히, 당연하게도 이미 같은 고민을 했던 사람들이 있었고 아틀라시안 커뮤니티 채널에 해결방법이 있었습니다.
기본 Jira 기능에는 없지만 스크립트러너를 사용해서 가능하다는 것이었죠.

제가 현재 사용하고 있는 Jira 버전은 8.5여서 위 링크의 8.x 코드를 보면서 설명해보고자 합니다.
(위 링크에 7.13 버전에서 사용할 수 있는 코드도 있으니 참고해서 쓰시면 됩니다.)

문제 해결 방법 (ScriptRunner)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

def searchService = ComponentAccessor.getComponent(SearchService)
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def results = searchService.search(user, jqlQueryParser.parseQuery("Your JQL Filter"), PagerFilter.getUnlimitedFilter())
def issueManager = ComponentAccessor.getIssueManager()

results.getResults().each
{
def issue = issueManager.getIssueObject(it.id)
log.warn(issue.key)
// get all linked issues
log.warn(ComponentAccessor.issueLinkManager.getLinkCollection(issue, user).getAllIssues())
ComponentAccessor.issueLinkManager.removeIssueLinks(issue, user)
}

로그 관련한 코드는 설명 중 불필요하여 삭제했습니다. 실제 코드 동작하는데에 영향이 없습니다.
사실 코드 상으로는 크게 어려운 부분이 없습니다.

스크립트 Flow

  1. 검색, JQL 파서, 사용자, 이슈 컨트롤러를 정의한다. (검색 및 설정 준비 과정)
  2. JQL을 통해 이슈들을 가져온다.
  3. 각 이슈들의 모든 링크를 삭제한다.

issueLinkManager 인터페이스 설명은 jira/docs/api/8.5.1/…/IssueLinkManager 이 문서에서 볼 수 있습니다.
이 매니저에는 여러가지 함수들이 있습니다. 이슈 링크의 생성, 삭제, 조회, 변경 등이 가능합니다.

removeIssueLinks() 함수도 위 issueLinkManager에서 볼 수 있는데요.

1
2
3
4
5
6
int removeIssueLinks(Issue issue, ApplicationUser remoteUser)
// Parameters:
// issue - the Issue
// remoteUser - the remote user
// Returns:
// The total number of issuelinks deleted.

모든 링크를 삭제하고자하는 이슈와 삭제할 때 필요한 사용자를 받아 함수가 동작합니다.

스크립트 결과

맨 처음 이미지를 보여드렸던 이슈의 키를 JQL로 입력하고 실행해보겠습니다.
“key=PUBGTEST-112”로 입력하고 스크립트 콘솔에서 테스트 해보았습니다.

Logs:

1
2
2020-02-23 02:16:42,545 WARN [runner.AbstractScriptRunner]: PUBGTEST-112
2020-02-23 02:16:42,545 WARN [runner.AbstractScriptRunner]: [PUBGTEST-75, PUBGTEST-111, PUBGTEST-225]

Timing: Elapsed: 4949 ms / CPU time: 531 ms
동작하는데 생각보다 시간이 많이 걸리긴 하네요.
(약 5초 정도 걸리는 것으로 보이는데 링크 삭제가 오래걸리는 것인지 다른 것이 오래걸리는 것인지는 봐야겠습니다.)

실행 결과로는 이슈의 히스토리에 아래와 같이 남았습니다.
삭제 완료!

링크가 걸려있는 이슈의 모든 이슈 링크들이 삭제되었습니다! 👍

문제 해결!

제가 했던 하나의 이슈만 링크를 삭제할 수도 있고 JQL로 검색된 모든 이슈들의 모든 링크를 삭제할 수 있습니다.
다만 여기서 제가 원했던 것은 모든 링크의 삭제가 아니라 특정 타입의 링크들만 삭제하고 싶었던 것이어서
다음 포스트에서는 특정 링크 타입의 링크를 삭제하는 내용으로 한번 더 다뤄봐야겠습니다.

다음 포스트에서 만나요! :)

ScriptRunner Jira Issue Label 추가시 자동으로 watcher 추가

스크립트러너에서 Jira 이슈의 Label 추가에 따라 자동으로 특정 사람들을 와쳐(watcher)에 추가하는 방법을 다뤄봅니다.

시스템 테스트 환경

  • Jira: Jira Software 8.3.2
  • ScriptRunner: 5.6.1.1-jira8

참고 포스트

사용 시나리오

적용하는 방법 전에 제가 사용하고 있는 시나리오를 공유드립니다.
특정 이벤트에 따라 와쳐를 추가할 수 있으니 이벤트만 잘 고려해서 와쳐 추가 코드만 사용하셔도 괜찮습니다.

  1. 특정 레이블(Label)이 이슈에 추가됨
  2. 레이블이 추가된 이슈에 원하는 인원을 와쳐 필드에 입력함
  3. 와쳐 인원 입력은 복수의 인원이 가능해야함

여기서 특정 이벤트는 레이블 추가 입니다.
다른 이벤트의 예시로는 컴포넌트 추가, 특정 커스텀 필드 값의 변경 등 다양하게 조건을 변경해볼 수 있을 것 같네요.

적용

앞서 설명드린 시나리오대로 적용해보겠습니다.
바로 적용해보고 싶으신 분들을 위해 먼저 코드와 설정 화면을 보여드리겠습니다.

Listener - Custom Listener 선택

Custom Listener field 입력

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import com.atlassian.jira.component.ComponentAccessor;
import java.util.List;

def watcherManager = ComponentAccessor.getWatcherManager();
def userManager = ComponentAccessor.getUserManager();
// Add users to watcher field
def watchUsers = {usernames ->
usernames.each {
def user = userManager.getUserByKey(it.toString());
watcherManager.startWatching(user, event.issue);
}
}
// Remove users on watcher field
def unwatchUsers = {usernames ->
usernames.each {
def user = userManager.getUserByKey(it.toString());
watcherManager.stopWatching(user, event.issue);
}
}
def labelWatch = {String label, List users, String[] oldArr, String[] newArr, boolean isCaseSensitive ->
String[] tmpOldArr = oldArr.collect {String i ->
if (isCaseSensitive)
return i;
else
return i.toLowerCase();
};
String[] tmpNewArr = newArr.collect {String i ->
if (isCaseSensitive)
return i;
else
return i.toLowerCase();
}
String tmpLabel = isCaseSensitive ? label : label.toLowerCase();
if (tmpOldArr.contains(tmpLabel) == false && tmpNewArr.contains(tmpLabel) == true) {
watchUsers(users);
}
if (tmpOldArr.contains(tmpLabel) == true && tmpNewArr.contains(tmpLabel) == false) {
unwatchUsers(users);
}
}
def labelWatchCreated = {String label, List users, boolean isCaseSensitive ->
String[] labels = event.issue.labels as String[];
String[] tmpLabels = labels.collect {String i ->
if (isCaseSensitive)
return i;
else
return i.toLowerCase();
}
String tmpLabel = isCaseSensitive ? label : label.toLowerCase();
if (tmpLabels.contains(tmpLabel)) {
watchUsers(users);
}
}

def change = event?.getChangeLog()?.getRelated("ChildChangeItem")?.find {it.field == "labels"};

if (change) {
// Issue Updated case
String[] oldArr = change.oldstring.toString().tokenize();
String[] newArr = change.newstring.toString().tokenize();

labelWatch("m", ["m1", "m2", "m3"], oldArr, newArr, false);
labelWatch("b", ["b1", "b2", "b3", "b4", "b5"], oldArr, newArr, true);
} else {
// Issue Created case: ${event.issue} ${event.issue.labels}
labelWatchCreated("m", ["m1", "m2", "m3"], false);
labelWatchCreated("b", ["b1", "b2", "b3", "b4", "b5"], true);
}

참고

위와 같이 설정하고 Update 버튼을 눌러주시면 적용됩니다.
코드가 깔끔하지는 않으나 동작하고 있는 코드입니다. (코드 개선은 향후 조금씩 진행해보겠습니다.)
다만 적용 전에 각자에 맞게 설정해야하는 값은 아래와 같습니다.

  1. labelWatch()를 호출시 “m”은 레이블 이름을, []안에는 와쳐 필드에 추가하고자 하는 유저의 아이디들을 입력해주세요.
  2. labelWatchCreated()를 호출시 1번 항목과 동일하게 입력해주세요.
  • 이 함수를 추가하는 이유는 이슈 생성시 이벤트가 다르게 처리되기 때문입니다.
  • 이슈 생성시 이벤트를 실행시키지 않고자 한다면 해당 코드를 지워도 괜찮습니다.
  1. labelWatch(), labelWatchCreated() 함수 마지막 인자는 레이블의 대소문자를 구분할 것인지에 대한 변수입니다.
  • 레이블 값의 대소문자를 구분하고자한다면 true, 아니라면 false 값을 넣고 호출해주세요.

코드 설명

코드에 대한 설명 내용이 많지않지만 각 함수별로 짧게 설명드리겠습니다.

watchUsers(usernames) / unwatchUsers(usernames)

변수로 전달받은 유저들을 이벤트가 발생한 이슈의 와쳐 필드에 입력/삭제합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def watcherManager = ComponentAccessor.getWatcherManager();
def userManager = ComponentAccessor.getUserManager();
// Add users to watcher field
def watchUsers = {usernames ->
usernames.each {
def user = userManager.getUserByKey(it.toString());
watcherManager.startWatching(user, event.issue);
}
}
// Remove users on watcher field
def unwatchUsers = {usernames ->
usernames.each {
def user = userManager.getUserByKey(it.toString());
watcherManager.stopWatching(user, event.issue);
}
}
  • 유저를 의미하는 usernames이라는 string 배열 인수를 받아 처리합니다.
  • watcherManager에 있는 startWatching(), stopWatching() 함수로 와쳐를 추가하고 삭제합니다.

labelWatch (label, users, oldArr, newArr, isCaseSensitive)

레이블과 변경전 레이블 배열, 변경후 레이블 배열을 받아 레이블이 변경되었는지 확인합니다. 추가적으로 isCaseSensitive 인수를 통해 레이블의 대소문자를 구분하여 확인합니다.
확인 후, 받은 유저들을 이슈의 와쳐 필드에 추가 또는 삭제합니다.
(watchUsers(), unwatchUsers() 함수로)
이슈가 업데이트될 경우에만 호출되는 함수입니다. 생성시에는 아래에 있는 labelWatchCreated() 함수가 호출됩니다.

labelWatchCreated(label, users, isCaseSensitive)

이슈 생성시에는 변경된 사항이 없기에 issue Created 이벤트를 받아 처리해야했습니다.
추가 함수를 통해 이슈 생성시에도 와쳐가 추가될 수 있도록 만든 함수입니다.

labelWatch() 함수처럼 레이블과 유저를 받고 받은 레이블이 값에 있을 경우 유저를 이슈의 와쳐 필드에 추가합니다.

실행 코드

실행 코드는 위에서 설명한 함수를 사용하여 와쳐 추가를 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def change = event?.getChangeLog()?.getRelated("ChildChangeItem")?.find {it.field == "labels"};

if (change) {
// Issue Updated case
String[] oldArr = change.oldstring.toString().tokenize();
String[] newArr = change.newstring.toString().tokenize();

labelWatch("m", ["m1", "m2", "m3"], oldArr, newArr, false);
labelWatch("b", ["b1", "b2", "b3", "b4", "b5"], oldArr, newArr, true);
} else {
// Issue Created case: ${event.issue} ${event.issue.labels}
labelWatchCreated("m", ["m1", "m2", "m3"], false);
labelWatchCreated("b", ["b1", "b2", "b3", "b4", "b5"], true);
}

마무리

실제로 빠르게 적용해보고 사용해본 코드라 깔끔하지 않을 수 있지만
다른분들도 사용해볼 수 있도록 포스트해보았습니다.

업무에 도움이 될 수 있기를 바라며 다른 방법도 추후에 올려보겠습니다.
감사합니다.

ScriptRunner Label 다루기

스크립트러너에서 Jira 이슈의 Label 값을 수정하는 경우가 종종 있는데 어떻게 추가/수정하는지
이번 포스트에서 다뤄보겠습니다.

이번에도 Script Console에서 코드를 실행시켜보고 저는 어떤 케이스에서 사용하는지 설명드리겠습니다.

시스템 테스트 환경

스크립트러너는 Jira 버전에 영향을 좀 받아서 환경도 미리 알고 계셔야합니다.
(버전에 따라 내부 함수들이 변경됨에 따라 실제 코드도 조금씩 변경됩니다)

  • Jira: Jira Software 8.3.2
  • ScriptRunner: 5.6.1.1-jira8

Label 값 가져오기

Label 값을 가져오는 것은 쉽습니다.
이슈에서 기본적으로 사용하는 필드라서 다루기 쉬운 것이 장점입니다.

1
2
3
4
5
6
7
8
9
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.label.LabelManager;
import com.atlassian.jira.issue.MutableIssue;

LabelManager labelManager = ComponentAccessor.getComponent(LabelManager);
def issueMgr = ComponentAccessor.getIssueManager();
MutableIssue issue = issueMgr.getIssueObject("PUBGTEST-169");

labelManager.getLabels(issue.getId());

getLabels(Long issueId)

getLabels() 함수는 issueId 값을 받아 이슈의 label 값을 가져옵니다.
함수의 리턴 값은 label들을 포함한 배열로 리턴합니다.
(아래 결과 값은 이슈에 테스트로 Label 값을 입력한 것을 기반으로 스크립트 결과가 나온 것입니다.)

Result

[Promotion, art]

Label 값 추가하기

Label 값을 가져오는 것보다는 한가지 더 고려가 필요한 추가 입니다.
Label 값을 추가할 때에는 어떤 사용자가 값을 추가할 것인지 결정이 필요합니다.
먼저 코드를 보면서 설명을 드리겠습니다.

1
2
3
4
5
6
7
8
9
10
11
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.label.LabelManager;
import com.atlassian.jira.issue.MutableIssue;

def user = ComponentAccessor.getUserManager().getUserByKey("pubg-helper");
LabelManager labelManager = ComponentAccessor.getComponent(LabelManager);
def issueMgr = ComponentAccessor.getIssueManager();
MutableIssue issue = issueMgr.getIssueObject("PUBGTEST-169");

labelManager.addLabel(user, issue.getId(), 'test', false);
labelManager.getLabels(issue.getId());

코드 설명

앞서 Label 값을 가져오는 것과 비교해서 달라진 것은 아래 코드와 addLabel() 함수 입니다.
def user = ComponentAccessor.getUserManager().getUserByKey("pubg-helper");
위 코드는 Label을 추가할 사용자를 결정하기 위해 사용자 객체를 아이디로 가져온 것입니다.

ScriptRunner의 Listener 기능에서 Label 수정/추가 기능을 사용할 때,
위 코드를 통해 업데이트하는 사용자를 동적으로 변경할 수 있습니다.

addLabel(User remoteUser, Long issueId, String label, boolean sendNotification) 함수는 이슈에 Label을 하나 추가할 수 있는 함수입니다.
함수 레퍼런스: docs.atlassian.com/…/LabelManager/addLabel

Result

[Promotion, art, test]

Label 값 변경

앞서 Label 추가의 경우 하나씩 추가했던 것에 비해 Label 값을 변경하는 것은 LabelManager의 setLabels() 함수를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.label.LabelManager;
import com.atlassian.jira.issue.MutableIssue;

def user = ComponentAccessor.getUserManager().getUserByKey("pubg-helper");
LabelManager labelManager = ComponentAccessor.getComponent(LabelManager);
def issueMgr = ComponentAccessor.getIssueManager();
MutableIssue issue = issueMgr.getIssueObject("PUBGTEST-169");

labelManager.setLabels(user, issue.getId(), ['test'] as Set, false, true);
labelManager.getLabels(issue.getId());

labelManager.setLabels()

labelManager.setLabels() 함수는 Label 값을 추가, 삭제한다기보다는
새로운 값으로 재설정한다는 것으로 이해하시면 될 것 같습니다.

public Set<Label> setLabels (User remoteUser, Long issueId, Set<String> labels, boolean sendNotification, boolean causeChangeNotification)

여기서 중간에 labels 값에 Set으로 되어있는데 그냥 [] 배열로 넣을 경우 List 객체로 인식하는 문제가 생깁니다.
그래서 as Set으로 변경해준 것으로 이해해주시면 됩니다. (혹은 변수를 따로 Set으로 두고 입력하면 됩니다)

labelManager.setLabels() 함수 레퍼런스: docs.atlassian.com/…/LabelManager/setLabels

Result

[test]

마무리

Label 값을 가져오고 추가/설정하는 것은 어렵지 않은 것 같습니다. (커스텀 필드 수정에 비해서요)
다만 골라서 삭제하는 기능은 없어 setLabels와 기존에 있는 값을 비교해서 삭제하는 것으로 삭제할 수는 있어 보이네요. (그렇게 어려워 보이지는 않을 것 같긴 하네요.)

저는 특정 이벤트가 발생할 경우 Label을 추가하는 정도로만 Listener에 Custom Listener를 추가하여 사용하고 있습니다. 이 Label을 사용하여 보드를 만들어 사용하시는 분이 있는데 잘 사용하고 있다고 하네요. :)

다음에 다른 기능을 소개하는 포스트로 찾아뵙겠습니다.
감사합니다!