ScriptRunner: Custom Field 다루기

스크립트러너에서 Jira 이슈의 커스텀 필드를 다루는 경우가 많습니다.
저는 어떻게 스크립트러너에서 커스텀 필드의 값을 가져오고, 수정하고 있는지 다뤄보겠습니다.

ScriptRunner란? (복습)

기존에 있는 포스트를 보시면 좋을 것 같습니다.
그중에 2번째 포스트에서 설명했던 Script Console에서 실습해보겠습니다.

시스템 테스트 환경

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

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

커스텀 필드(Custom Field) 값 가져오기

간단하게 커스텀 필드 값을 가져오는 코드를 보면서 설명하겠습니다.
코드는 groovy로 작성하며 Script Console에서 테스트해보았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// import
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.MutableIssue;

// functions
String getCustomFieldValue(MutableIssue issue, String cfName) {
def customFieldManager = ComponentAccessor.getCustomFieldManager();
def cfObj = customFieldManager.getCustomFieldObjectsByName(cfName)[0];
String cfValue = issue.getCustomFieldValue(cfObj);
return cfValue;
}

def issueMgr = ComponentAccessor.getIssueManager();
MutableIssue issue = issueMgr.getIssueObject("PUBGTEST-169");
getCustomFieldValue(issue, "Translation");

복사 & 붙여넣기 하시고 이슈 키, 커스텀 필드 이름만 바꿔서 입력해주시면
해당 이슈의 커스텀 필드 값을 가져올 수 있는 코드입니다.
코드에서 중요한 부분만 간단하게 보고 커스텀 필드 값 수정으로 넘어가겠습니다.

getCustomFieldValue(issue, cfName) 함수

이슈 오브젝트커스텀 필드 이름을 받아 커스텀 필드 값을 반환하는 함수입니다.

  1. CustomFieldManager를 사용하여 커스텀 필드 오브젝트를 가져옵니다.
  • 배열로 가져오는 함수(getCustomFieldObjectsByName())를 사용하였습니다.
  • getCustomFieldObjectByName()도 있지만 배열로 가져오는 함수를 권장하고 있습니다.
  • 같은 이름의 커스텀 필드가 있을 수 있기에 배열로 가져오는 함수를 권장하는 것 같네요.
  1. issue에 있는 함수 getCustomFieldValue()로 커스텀 필드 오브젝트를 이용해 커스텀 필드 값을 가져옵니다.
  2. 커스텀 필드 값을 리턴합니다.

코드 실행 결과

Script Console 실행 결과도 보여드리겠습니다.

콘솔에서는 코드 실행 후 마지막에 있는 오브젝트의 값을 보여줍니다.
log에서 출력한 내용은 logs 탭에서 볼 수 있습니다.

커스텀 필드(Custom 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
// import
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.MutableIssue;

// functions
String getCustomFieldValue(MutableIssue issue, String cfName) {
def customFieldManager = ComponentAccessor.getCustomFieldManager();
def cfObj = customFieldManager.getCustomFieldObjectsByName(cfName)[0];
String cfValue = issue.getCustomFieldValue(cfObj);
return cfValue;
}
def setCustomFieldValue(MutableIssue issue, String cfName, String str) {
def issueMgr = ComponentAccessor.getIssueManager();
def customFieldManager = ComponentAccessor.getCustomFieldManager();
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();
def cfObj = customFieldManager.getCustomFieldObjectsByName(cfName)[0];
issue.setCustomFieldValue(cfObj, str);
issueMgr.updateIssue(user, issue, EventDispatchOption.DO_NOT_DISPATCH, false);
}

def issueMgr = ComponentAccessor.getIssueManager();
MutableIssue issue = issueMgr.getIssueObject("PUBGTEST-169");
setCustomFieldValue(issue, "Translation", "TEST");
getCustomFieldValue(issue, "Translation");

앞서 값을 가져오는 코드와 마찬가지로 “TEST”에 있는 값을 바꾸면 적용해보실 수 있습니다.
다만 주의하실 점은 커스텀 필드의 타입이 텍스트 타입이어야합니다.
Label, Checkbox 등 다른 타입일 경우에는 동작하지 않는 코드입니다.

setCustomFieldValue(issue, cfName, str)

값을 가져오는 코드와 다른 부분만 설명드리겠습니다.

  • issueMgr: 이슈 매니저로 이슈 업데이트를 위해 함수 내에 선언합니다.
  • usr: 이슈 업데이트를 하려면 업데이트하는 사용자 오브젝트가 필요하여 선언합니다.
  • issue.setCustomFieldValue(cfObj, str);
    • 이 함수를 통해 커스텀 필드 값이 변경됩니다.
    • 이 코드만 실행할 경우, 실제 반영은 되지 않습니다. (함수 동작시에만 변경됨)
  • issueMgr.updateIssue()
    • 이 함수를 통해 앞에서 setCustomFieldValue()에서 변경된 내용이 반영됩니다.

커스텀 필드 수정과 관련한 질문은 Google에 검색만 하더라도 많이 나옵니다.
(첫번째 글을 보시는 것을 추천합니다.)

코드 실행 결과

실제로 TEST로 변경된 것을 볼 수 있습니다.

마무리

이렇게 스크립트러너로 Jira 이슈의 커스텀 필드를 다뤄보았습니다.
처음에는 어려울 수 있지만 조금씩 다뤄보면 그렇게 어렵지 않습니다.
쉽게 따라하고 적용해보실 수 있는 내용으로 추가 포스트해보겠습니다.

감사합니다. :)

Jira 8.0 & ScriptRunner로 JQL issue 가져오기

Jira 8.0 & ScriptRunner로 JQL issue 가져오기

최근에 ScriptRunner로 JQL 결과 이슈 가져오기 스크립트를 찾아보고 실행해보려 했는데 잘 안되서 포스트를 작성해보았습니다.
(다른분들도 같은 문제를 겪을 것 같아서..!)

스크립트러너 문서에서의 코드는 링크에서 확인하시면 좋을 것 같습니다.
ScriptRunner Doc: running-a-jql-query

위 링크에 있는 코드에서 몇몇 Deprecated 코드를 수정하면 다음과 같은 코드로 정리해볼 수 있습니다.

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
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.search.SearchProvider;
import com.atlassian.jira.jql.parser.JqlQueryParser;
import com.atlassian.jira.web.bean.PagerFilter;

def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser);
def searchProvider = ComponentAccessor.getComponent(SearchProvider);
def issueManager = ComponentAccessor.getIssueManager();
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();

// edit this query to suit
def query = jqlQueryParser.parseQuery("project = JRA and assignee = currentUser()");

def results = searchProvider.search(query, user, PagerFilter.getUnlimitedFilter());

log.debug("Total issues: ${results.total}");

results.getIssues().each {documentIssue ->
log.debug(documentIssue.key);

// if you need a mutable issue you can do:
def issue = issueManager.getIssueObject(documentIssue.id);

// do something to the issue...
log.debug(issue.summary);
}

문제의 시작

그런데..
이 코드를 Script Console에서 실행해보면 아래와 같은 에러가 발생합니다.

1
2
3
groovy.lang.MissingMethodException: No signature of method: com.atlassian.jira.issue.search.providers.LuceneSearchProvider.search() is applicable for argument types: (com.atlassian.query.QueryImpl, com.atlassian.jira.user.DelegatingApplicationUser, com.atlassian.jira.web.bean.PagerFilter) values: [{project = "JRA"} AND {assignee = currentUser()}, pineoc(pineoc), ...]
Possible solutions: search(com.atlassian.jira.issue.search.SearchQuery, com.atlassian.jira.web.bean.PagerFilter), search(com.atlassian.jira.issue.search.SearchQuery, com.atlassian.jira.web.bean.PagerFilter, java.util.Set), search(com.atlassian.jira.issue.search.SearchQuery, org.apache.lucene.search.Collector), each(groovy.lang.Closure)
at Script1071.run(Script1071.groovy:14)

에러를 요약해보자면..

  • LuceneSearchProvider에 그런 메서드가 없다.
  • search(.issue.search.SearchQuery, .web.bean.PagerFilter) 요런 형식이어야하는데
  • search(.query.QueryImpl, .user.DelegatingApplicationUser, .web.bean.PagerFilter) 요렇게 부르고 있어서 안되는거다.
  • 파라미터 형식이 너무 길어서 생략하겠습니다. (com.atlassian.jira 등이 앞에 붙어있었습니다.)

에러 내용만 봤을 때는 잘못된 파라미터로 인해 함수를 호출하지 못한다고 합니다.
공식 가이드 문서로 보고 진행했는데 에러가 나서 잠시 당황했지만 파라미터를 확인해보았습니다.

문제 파악

searchProvidersearch() 함수에는 다음과 같은 파라미터가 들어가야한다고 합니다.
JIRA API docs(8.0.2)-LuceneSearchProvider

  • com.atlassian.jira.issue.search.SearchQuery
  • com.atlassian.jira.web.bean.PagerFilter

근데 가이드 문서에서 사용했던 함수에는 아래와 같은 파라미터를 사용했습니다.
JIRA API docs(7.0.5)-LuceneSearchProvider

  • Query query
  • ApplicationUser searcher
  • PagerFilter pager

위 처럼 찾아보는 과정에서 LuceneSearch 쪽 업데이트가 있어 함수 변경이 있었습니다.
그 업데이트가 Jira 8.0 버전으로 올라가면서 반영된 것 같습니다.
ScriptRunner는 Jira 8.0을 지원한다고 했지만 문서는 아직 지원하지 않는 것 같네요. (…)

참고

문제 해결 시도

그래도 결과를 찾아봐야하니까 문제를 해결해봅니다.

아래와 같이 코드를 수정하여 스크립트 콘솔에 적용해보았습니다.
변경된 내용 및 이유도 함께 적어둡니다.
(바로 적용해볼 수 있는 코드는 아래에 있으니 바로 사용해보시면 될 것 같습니다.)

  • searchProvider.search() 함수도 앞서 설명한 것 처럼 메서드를 확인하여 변경했다.
  • results.getIssues() 함수는 Jira 8.0.x에 더 이상 존재하지 않았다.
    • SearchResults에 함수에는 getResults() 함수가 존재한다.
    • getResults() 함수는 List를 반환하는데 DocumentWithId List를 반환한다. (참고 링크 - search 함수)
    • DocumentWithId 클래스에는 getDocument() 함수가 있는데 이 함수로 Document에 접근할 수 있다.
    • Document 오브젝트는 org.apache.lucene.document.Document 메서드를 사용하여 데이터를 접근할 수 있다. (참고 링크 - Document 메서드)
    • Document.getField(“key”) 로 접근하여 이슈의 키를 얻을 수 있다. (ex: JRA-1)
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
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.jql.parser.JqlQueryParser;
import com.atlassian.jira.issue.search.SearchProvider;
import com.atlassian.jira.web.bean.PagerFilter;
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.search.SearchQuery;

def findIssues(String jqlQuery) {
def issueManager = ComponentAccessor.issueManager;
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser);
def searchProvider = ComponentAccessor.getComponent(SearchProvider);
def query = jqlQueryParser.parseQuery(jqlQuery);
def searchQuery = SearchQuery.create(query, user);

// search 함수를 type 에 맞게 수정해주었다.
def results = searchProvider.search(searchQuery, PagerFilter.getUnlimitedFilter());
log.warn "issue cnt: ${results.getTotal()}"; // query로 얻은 이슈의 수를 확인한다.
results.getResults().collect { res ->
def doc = res.getDocument();
def key = doc.get("key");
def issue = ComponentAccessor.getIssueManager().getIssueObject(key);
return issue;
}
}
def jqlQuery = "project=JRA AND issuetype=Bug";
def issues = findIssues(jqlQuery);

참고

정리

다소 문제 해결에 애를 먹기는 했지만 해결은 했습니다.
이 글을 쓰고나서 ScriptRunner 쪽에 문서 최신화좀 해달라고 요청해야겠네요.
문서 최신화가 어려운 것은 알지만 다들 삽질할 것 같은데 피드백 얼른 남겨야겠네요.

종종 스크립트러너로 할 수 있는 기능들을 포스팅 해보겠습니다. :)

ScriptRunner 소개 4

ScriptRunner 소개 #4

지난 #3 소개 글에서는 Script Fragments 기능까지 보았습니다.
얼마 남지 않았지만 너무 길어져서 #4로 나눴습니다.

  • Escalation Services
  • Script JQL Functions

길고 길었던 ScriptRunner 소개를 마무리하러 가보겠습니다! :)

Escalation Services

https://scriptrunner.adaptavist.com/latest/jira/escalation-service.html
설명은 아래와 같이 나와있는데 요약해보겠습니다.

Automate issue escalation with escalation services. Use a JQL query to define processes for modifying issues based on elapsed time. Specify actions (such as transitions) to occur at defined intervals after a specified time has passed.

에스컬레이션 서비스를 통해 문제 에스컬레이션 자동화 JQL 쿼리를 사용하여 경과된 시간을 기준으로 문제를 수정하는 프로세스를 정의하십시오. 지정된 시간이 경과한 후 정의된 간격으로 발생할 작업(예: 전환)을 지정하십시오. - with Papago

JQL로 검색한 쿼리들을 일정 시간마다 특정 값을 설정하거나 transition(전환) 할 수 있는 기능입니다.
예를 들면, 2주 동안 변경이 없는 이슈들의 상태를 변경할 수 있겠죠. (혹은 특정 필드를 수정하거나)


입력해야하는 항목은 아래와 같이 많지 않습니다.

  • Description: 설정에 대한 설명을 적습니다.
  • JQL Query: 변경하고자하는 이슈를 가져오기 위해 JQL을 작성합니다.
  • AS User: 어떤 유저로 변경할 것인지 설정합니다.
  • Interval/CRON Expression: 얼마나 자주 실행할 것인지 설정합니다. 기본적으로 분 단위로 설정할 수 있습니다.
    • CRON Expression에 대한 것은 링크에서 참고하여 작성해볼 수 있습니다.
  • Action: Transition을 설정할 수 있습니다. 반드시 설정해야하는 항목은 아닙니다.
  • Additional issue actions: 댓글을 달거나 Resolution을 설정할 수 있습니다. groovy 코드로 작성해야합니다.
    • Show examples를 보고 작성해보는 것을 추천합니다.
  • Transition Options: Permissions, Validators, Conditions 확인하는 것을 건너뛸 것인지 설정합니다.

수시로 확인해서 상태를 업데이트하거나 이슈의 항목을 수정해야하는 일이 있다면 사용하기 좋을 것 같습니다.
다만 As User로 설정한 유저의 권한을 잘 확인하여 transition할 수 있는 권한이 있는지 확인해주세요.
(방금 테스트해보았는데 권한없는 동작을 하려할 경우 아예 변경이 진행되지 않습니다.)

Script JQL Functions

https://scriptrunner.adaptavist.com/latest/jira/jql-functions.html
이 항목은 커스텀한 JQL 함수를 추가할 수 있다기 보다 현재 사용할 수 있는 JQL Function들을 보여줍니다.

커스텀한 함수를 추가할 수는 있지만 JIRA 페이지에서 바로 추가할 수 있지는 않습니다.
https://scriptrunner.adaptavist.com/latest/jira/custom-jql-functions.html


쭉 내려보면 많은 함수들을 볼 수 있습니다.
함수에 대한 더 자세한 내용을 보려면 위에 있는 jql-functions 링크를 참고하세요.

ScriptRunner 소개 마무리

4개의 포스트로 소개 문서를 마무리하고자 합니다.
많은 내용들을 찾아보고 테스트 삼아 만들어보느라 시간이 오래걸리기도 했네요.
설정하면서 많이 배웠고 실제 프로젝트에서도 적용해볼만한 내용들이 많았습니다.
다른 JIRA를 사용하는 분들께도 필요한 내용이 있으면 다른 포스트에서 짧게 사용할만한 것들
남겨두겠습니다. :)

긴 글 읽어주셔서 감사합니다. (_ _)

ScriptRunner 소개 3

ScriptRunner 소개 #3

지난 #2 소개 글에서는 Script Fields 기능까지 보았고
그 다음에 있는 아래 리스트의 기능들을 보겠습니다.

  • REST Endpoints
  • Script Fragments
  • Escalation Services
  • Script JQL Functions

소개 글을 쓰는 도중에 JIRA 시스템 버전이 7에서 8로 올라가는 과정에서 UI, 기능이 약간 변경되었습니다.
버전 변경에 따라 큰 차이는 없겠지만 참고해주세요. :)

  • JIRA version: 7.13 -> 8.0
  • ScriptRunner: 5.5.2 -> 5.5.3

REST Endpoints

https://scriptrunner.adaptavist.com/latest/jira/rest-endpoints.html
ScriptRunner 공식 문서를 보면 아래와 같은 용도로 사용할 수 있다고 합니다.

  • use in dashboard gadgets
    • 대시보드 가젯으로 사용
  • receive notifications from external systems
    • 외부 시스템으로부터 알림 받기
  • plug gaps in the official REST API
    • 공식 REST API 갭을 보완(매꾸기)
  • allow all your XHRs to proxy through to other systems
    • 모든 XHR이 다른 시스템으로 프록시되도록 허용

Create Rest Endpoint 버튼을 통해 만들 수 있습니다.


위 화면처럼 inline script 입력 창에 코드를 입력하여 REST API를 구현합니다.
공식 문서와 Show examples에서는 Simple 'get' 코드를 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate // 1

doSomething( // 2
httpMethod: "GET", groups: ["jira-administrators"]) { // 3
MultivaluedMap queryParams, String body -> // 4
return Response.ok(new JsonBuilder([abc: 42]).toString()).build(); // 5
}

코드의 설명 내용은 공식 문서에 나와있으나 짧게 설명해보겠습니다. (영어로 되어있어 한국어로 설명할 겸..!)

  1. 이 줄이 코드는 스크립트가 엔드포인트로 사용할 수 있도록 하는 코드입니다. 엔드포인트를 사용하고자 한다면 꼭 필요한 코드입니다.
  2. REST 엔드포인트의 이름입니다. URL에서 사용할 이름으로 이 코드에서는 doSomething으로 사용될 것입니다.
  3. 엔드포인트의 설정으로 HTTP GET, 권한 그룹은 jira-administrators 그룹이 사용할 수 있는 엔드포인트가 됩니다.
  4. 메서드 바디에 제공될 파라미터 입니다.
  5. 작성된 값은 메서드 바디에 포함되어 javax.ws.rs.core.Response 객체를 반환할 것 입니다.

이렇게 추가하게되면 아래의 링크에 접근하여 데이터를 받을 수 있습니다.
<jira_base_url>/rest/scriptrunner/latest/custom/doSomething
curl로 테스트해보면 아래와 같이 결과 값이 나올 수 있겠습니다.
(admin:admin는 username:password 값을 입력하면 됩니다.)

1
2
> curl -u admin:admin http://localhost:8080/jira/rest/scriptrunner/latest/custom/doSomething
> {"abc":42}

예시

예시: ScriptRunner REST Endpoints Examples

  • Create Priority Object
  • Get the user making the request

문서에 코드도 같이 포함되어 있으니 링크 참고하셔서 설정하면 되겠습니다.
특정 URL로 요청해서 시스템 설정을 하거나 데이터를 가져오거나 할 수 있는 기능으로
REST Endpoints 소래를 마무리할 수 있겠습니다.

Script Fragments

https://scriptrunner.adaptavist.com/latest/jira/CustomisingUI.html
설정화면에 있는 설명을 그대로 가져와보겠습니다.

Use script fragments to customise the user interface and add new functionality to a Jira instance. Use customisable built-in script fragments to display web items, web panels and web sections or to include JavaScript and CSS resources using web resources. All script fragments can be customised, or use the raw XML module to create a fully custom web fragment.

간단히 설명하자면 필요한 Web items, panels, section을 만들 수 있다는 것입니다.
XML 설정이나 다른 리소스가 필요할 수는 있으나 팝업, 패널 등을 사용자가 원하는대로 만들 수 있는 기능입니다.


설정을 할 수 있는 메뉴를 하나하나 알아보겠습니다.

Raw xml module

XmlModuleItem Docs
xml 코드를 입력하여 원하는 위치에 메뉴를 보여주거나, 버튼들을 보여줄 수 있습니다.
아래 코드로 JIRA 상단 메뉴의 Projects > View all projects > My Projects를 볼 수 있습니다.

1
2
3
4
5
6
7
8
<web-item key='link-to-myprojects' name='ScriptRunner generated web item - link-to-myprojects' section='browse_link/project_view_all' weight='50'>
<label>My Projects</label>
<link linkId='link-to-myprojects'></link>
<tooltip>Show only my projects</tooltip>
<icon height="16" width="16">
<link>/images/icons/print.gif</link>
</icon>
</web-item>

XML 입력창에 입력만 하면 되긴 하지만 JIRA UI 아이템들이 어떻게 배치되어있는지
알고 작성해야하는 것이 어렵긴합니다.

sectionsystem.top.navigation.bar이 있는지, <web-item> 태그가 있는지 알기 쉽지 않지만
일단 예제로 따라해보고 다른 web-item, web-section 등의 태그를 알아가야할 것 같습니다.

참고 외부 링크

Install web resource

Web Resource Docs
이 기능은 Javascript, CSS 리소스를 사용할 수 있도록 하는 기능입니다.
우선 설정을 하려면 아래와 같이 환경 설정을 해야합니다. (경로는 다를 수 있습니다.)

1
JVM_REQUIRED_ARGS='-Dplugin.resource.directories=/app/home/scripts -Dother.properties...'

설정 후, 해당 폴더에 JS, CSS를 넣고 Resouces 입력창에 경로를 넣어줍니다.

커스텀한 리소스를 사용할 수 있다는 점에서는 좋은 것 같지만
잘못하면 많이 망가질 수 있다는 생각이 들긴하는 기능입니다.

Create a custom web section

Web Section Docs
웹 섹션(Web section)은 새로운 로케이션 또는 섹션, web-item을 추가할 때 사용할 수 있습니다.

web-item에서 web-section을 사용하고자 한다면,
web-section 만들 때 사용했던 key 값을 사용해야합니다.

Constrained create issue dialog

Create Constrained Issue
문서 설명 그대로 가져오자면, Create Constrained Issue -> 제한된 문제 만들기!
제한된 설정 안에서 이슈를 만들 수 있도록 설정할 수 있습니다.

문서에 나온 설명에 따르면,

This can be useful to workflow designers. Imagine a workflow where at one transition the user is required to create a new linked issue. Typically this is done by having a self or any to any transition which will created the linked issue with a post-function. Then the user is required to edit the linked issue further.

Workflow를 디자인, 만들때 유용한 기능입니다.
예를 들면 특정 트랜지션에서 이슈를 링크해야한다던지, 특정 값을 입력해야하는 옵션을 추가할 수 있습니다.

문서에서는 Behaviours와 함께 사용해서 구성한 메뉴를 눌렀을 경우,
새로운 이슈를 만들때 특정 필드를 읽기전용으로 설정하고 필드 값도 미리 설정할 수 있는 것을 보여줍니다.

TODO

이 기능은 설정할 내용이 좀 있어서 나중에 포스트로 깊게 다루는게 좋을 것 같습니다.

Custom web item

Web Item
A web-item is a button or link that will appear at your chosen location.
웹 아이템은 버튼 또는 링크로 설정한 위치에서 메뉴로 사용할 수 있습니다.

간단한 예시로 JIRA 상단에 네비게이션 바에 Search the Web 링크를 만들어봅니다.

issue에서 More Actions 메뉴에도 링크를 만들어봅니다.

결과화면은 아래와 같이 나옵니다.

Condition 조건에 따라 웹 아이템이 나오기에 Condition 설정도 필요합니다.
프로젝트로 한정하거나 이슈의 특정 값인지 조건을 사용할 수 있습니다.

차근차근 테스트 삼아 저도 웹 아이템 하나 만들어보겠습니다.

1. web-item을 하나 만들어봅니다.

  • Menu text는 버튼에 들어갈 텍스트니 적당히 적어줍니다.
  • Weight 값은 숫자가 작을수록 우선순위가 높습니다.
  • Condition 값은 어떤 조건에 버튼이 나올지 설정할 수 있습니다.
  • Do what에는 저는 dialog를 띄우는 테스트를 할 것이라 Run code and display dialog를 선택했습니다.
  • Link는 REST Endpoint 기능에서 만든 URL을 입력해줍니다. 다음 과정에서 만드는 방법을 보겠습니다.

2. REST Endpoint 추가

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
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript

import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate

showDialog { MultivaluedMap queryParams ->
def dialog =
"""<section role="dialog" id="sr-dialog" class="aui-layer aui-dialog2 aui-dialog2-medium" aria-hidden="true" data-aui-remove-on-hide="true">
<header class="aui-dialog2-header">
<h2 class="aui-dialog2-header-main">Some dialog</h2>
<a class="aui-dialog2-header-close">
<span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>
</a>
</header>
<div class="aui-dialog2-content">
<p>This is a dialog...</p>
</div>
<footer class="aui-dialog2-footer">
<div class="aui-dialog2-footer-actions">
<button id="dialog-close-button" class="aui-button aui-button-link">Close</button>
</div>
<div class="aui-dialog2-footer-hint">Some hint here if you like</div>
</footer>
</section>
"""

Response.ok().type(MediaType.TEXT_HTML).entity(dialog.toString()).build()
}
  • 위 코드를 inline script 항목에 넣어줍니다.
    • 함수 이름으로 REST endpoint가 만들어질 것인데 showDialog로 만들어질 예정입니다.
  • 생성!
  • 다시 Web-item 만든 곳으로 돌아가서 아래 주소를 입력해줍니다.
    • /rest/scriptrunner/latest/custom/showDialog
    • 1번 단계에서 이미 이미지를 보고 입력하셨다면 함수 이름에 맞게 입력했는지 확인하면 끝!

3. 결과


  • Web 버튼을 누르면 2번째 이미지 처럼 다이얼로그가 나옵니다!
  • 다이얼로그에 필요한 정보는 차차 넣을 수 있도록하면 되겠습니다.
  • 문서에서는 web resource 까지 가서 close 버튼에 뭔가 더 해보라고 하지만 여기까지 하겠습니다. :)

Custom web item provider

Web Item Provider Built-In Script
문서 상에 예로 사용할 수 있는 것으로 설명을 시작하는게 좋을 것 같습니다.
Examples of a web item provider might be:

  • 현재 사용자가 가지고 있는 이슈 중, 액티브 스프린트에 있는 상위 5개의 이슈를 보여줍니다.
    • Show the top 5 issues for the current user in active sprints in the Issues top-level menu
  • 현재 이슈의 서브태스크 각각에 대한 web item을 More Actions 메뉴에서 보여줍니다.
    • Show a web item in the More Actions menu for each of the current issue’s subtasks

예시: My Top Issues

https://scriptrunner.adaptavist.com/5.5.3.1-jira8/jira/fragments/WebItemProvider.html#_example_my_top_issues

JIRA 상단의 네비게이션 바 메뉴에서 issues를 눌렀을 때,
MY TOP ISSUES 섹션에 스프린트에 있는 자신에게 할당된 이슈 상위 5개가 보입니다.
예시 링크에 있는대로 진행하시면 원하는 결과를 보실 수 있을 겁니다. (스프린트를 사용한다는 가정하에 말이죠)

Show a web panel

Web Panel
커스텀 웹 패널을 만들고 그 영역에 정보를 담을 수 있도록 설정할 수 있습니다.

  • Location : 설정 코멘트
  • Key: UI상 ID가 필요하여 사용하는 것으로 보입니다.
  • Menu text: 보이는 메뉴의 타이틀 문구로 사용됩니다.
  • Weight: UI 우선순의 값으로, 양수로 설정할 수 있으며 입력하지 않아도 됩니다.
  • Condition: 해당 패널이 보이는 조건을 설정할 수 있습니다.
  • Provider class/script: 패널에 들어갈 데이터를 넣어주는 코드를 작성할 수 있습니다.
1
2
3
writer.write("<div style='background-color: yellow; text-align: center'>" +
"This project is going to be deprecrated. Please create" +
" all issues in the WIDGET project</div>");

위 이미지 상에서 설정한대로 설정할 경우 이슈 오른쪽 패널에는 아래처럼 보여집니다.

부모 이슈의 설명(Description) 값을 가져와서 서브태스크에 보여주는 방법도 있습니다.
WebPanel.html#_parent_issue_example

잘 사용하면 필요한 정보를 적재적소에 보여줄 수 있을 것 같지만,
잘못 사용하면 난잡해질 수 있겠다는 생각이 드네요. 위의 예시를 살펴보고 세팅해보면 좋을 것 같습니다.
패널 뿐만 아니라 JIRA 이곳 저곳에 정보를 넣을 수 있으니 잘 사용하면 좋을 것 같습니다. :)

Hide system or plugin UI element

Hide UI Element Build-In Script
JIRA에서 특정 프로젝트나 특정 이슈에서 UI 요소를 숨길 수 있는 기능입니다.
예시로는 아래와 같은 것을 할 수 있습니다.

  • Move, Clone 기능을 특정 프로젝트에서 이슈 Operation 목록에서 보이지 않게 함
  • source control이 필요없는 프로젝트에서 Development panel을 보이지 않게 함

설정을 위해 입력해야하는 값은 많지 않습니다.

  • Note: script note, 이 설정의 설명을 적는 곳입니다.
  • Hide what: 어떤 요소를 숨길지 설정합니다. 설정할 수 있는 요소는 web items 들로 리스트는 굉장히 많습니다.
    • com.atlassian.jira.plugin.system.issueoperations:* - 이슈 관련 UI 요소
    • com.pyxis.greenhopper.jira:board-* - board 관련 UI 요소
  • Condition: 어떤 조건에서 UI를 숨길 것인지 조건을 설정합니다.
    • 프로젝트, 이슈 등을 조건으로 설정할 수 있습니다.

이 설정을 사용하기전에 다른 프로젝트에 영향이 있을지 주의가 필요할 것 같습니다.
잘못하면 다른 구성원에게 필요한 기능이 보이지 않게될 수 있을테니까요.

Planning board context menu item

Board Context Menu Item
Web Item 기능 중에 하나라고 볼 수 있습니다.
스크럼, 칸반 보드에서 오른쪽 버튼을 눌렀을 때 나오는 드롭다운에 메뉴를 추가할 수 있는 기능입니다.

`Ready for release` 메뉴를 추가한 모습

가이드 문서에는 메뉴를 누르면 간단한 동작을 하는 것을 보여줍니다.

  1. 스크럼 보드에서 액티브 스프린트에 있는 이슈를 Ready for release 메뉴를 누르면,
  2. REST endpoint로 등록한 /rest/scriptrunner/latest/custom/ghAddLabel 로 이슈 정보를 보낸다.
  3. REST endpoint 등록된 함수에서 이슈의 label 필드에 “red” 입력한다.
  4. flag으로 오른쪽 위에 노티를 보여준다.
1
2
3
4
5
6
7
8
// ...
def flag = [
type: 'success',
title: "Label Added",
close: 'auto',
body : "This issue has been approved for release"
]
Response.ok(JsonOutput.toJson(flag)).build()

Condition은 복수의 이슈를 받을 수 있기에 List 객체를 받을 수 있게 되어있습니다.
예시 코드는 아래와 같이 볼 수 있습니다.

1
2
3
4
5
6
// All selected issues are resolved
issues.every { it.resolution }
// Selected issues in same project
issues*.projectObject.unique().size() == 1
// Only a single issue is selected
issues.size() == 1

Do what에는 사용할 수 있는 동작이 많습니다.
link로 데이터를 보내서 추가 동작을 설정하거나, 수정, 할당 등을 할 수 있습니다.

마무리

REST Endpoints, Script Fragment 내용이 많다보니 또 포스트 내용이 넘쳤네요.
다음 #4 소개 포스트에서 남은 Escalation Services, Script JQL Functions 에 대해 소개해보겠습니다.

ScriptRunner 소개 2

ScriptRunner 소개 #2

지난 글에서는 Behaviours를 보았고 다음 내용인 콘솔, 리스너 등을 보겠습니다.

  • Administration > Add-ons > ScriptRunner
    • Script Console
    • Built-in Scripts
    • Script Listeners
    • Script Fields
    • REST Endpoints
    • Script Fragments
    • Escalation Services
    • Script JQL Functions

Script Console

Script Console은 실시간으로 스크립트를 작성하고 결과를 볼 수 있는 곳 입니다.

img

실제로 스크립트를 작성해보고 실행해본 모습입니다.

img

  1. 스크립트를 작성하는 곳 입니다.
  • 자동 완성 기능은 없지만 없는 변수나 없는 함수 등은 오류를 보여줍니다.
  1. 스크립트를 작성한 뒤에 Run 버튼으로 실행할 수 있습니다.
  2. Result / Logs / Timing 값을 각각 볼 수 있습니다.
  • Result: 스크립트에서 나온 결과물을 볼 수 있습니다.
  • Logs: 스크립트에서 출력한 log들을 볼 수 있습니다.
  • Timing: Elapsed: 208 ms / CPU time: 78 ms 와 같은 내용을 볼 수 있습니다.

아래는 의도적으로 함수명을 다르게 입력하면 볼 수 있는 에러 화면 입니다.

img

정적 타입 체크 밖에 해주지 못하지만 최소한의 체크는 할 수 있기에
스크립트 작성은 할 수 있습니다.

Built-in Scripts

ScriptRunner 에 이미 설정(구현)되어 있는 스크립트들을 사용할 수 있는 메뉴입니다.
아래 참고 문서에서 사용할 수 있는 스크립트 리스트를 볼 수 있습니다.
참고 문서: https://scriptrunner.adaptavist.com/latest/jira/builtin-scripts.html

img

<아래에 더 많은 스크립트가 있습니다>

위 화면의 스크립트 중 “Bulk Fix Resolutions” 스크립트를 확인해보겠습니다.
이 스크립트는 Filter에 있는 티켓들의 Resolution을 변경할 수 있습니다.
참고 링크: Bulk Fix Resolutions

img

JQL을 바로 작성하는 것은 아니며 기존에 만들어둔 Filter를 사용하여 동작하는 스크립트 입니다.
그 외에 다른 스크립트의 목록은 아래와 같습니다.

Listeners

Listeners는 JIRA에서 일어나는 이벤트에 따라 스크립트를 동작하게 합니다.
프로젝트, 특정 이슈, 특정 이벤트에 따라 동작할 수 있도록 설정할 수 있습니다.
참고 문서: https://scriptrunner.adaptavist.com/latest/jira/listeners.html

img

<여러가지 리스너를 추가할 수 있습니다>

이미 있는 리스너 타입을 사용해도 되고
아예 이벤트를 받는 처음부터 스크립트 마무리까지 관리하는 Custom listener를 사용할 수도 있습니다.

이미 있는 리스너 타입 하나를 한번 보겠습니다.
https://scriptrunner.adaptavist.com/5.4.39/jira/builtin-scripts.html
Create a sub-task: Create a sub-task, Will Optionally reopen a matching sub-task

img

<특정 이벤트에서 sub-task를 만들 수 있는 리스너 입니다>

Create a sub-task Listener를 등록하는 화면의 각각 입력 필드에 대해 알아보겠습니다.

  • Note: Listener 등록시 해당 설명으로 Listener 리스트에서 설명을 볼 수 있습니다.
  • Project(s): 해당 이벤트가 발생하는 프로젝트를 설정합니다.
    • 프로젝트는 자동 완성되어 입력할 수 있습니다.
  • Events: 어떤 이벤트를 받아서 처리할 것인지 이벤트를 설정합니다.
    • All Issue Events, Issue Created, Issue Updated 등의 이벤트를 추가할 수 있습니다.
  • Condition: 해당 이벤트가 발생했을 경우, True / False를 판단할 스크립트를 설정합니다.
    • Show examples를 누르면 예시 코드를 볼 수 있습니다.
    • Priority changed to Major (Listeners only) - Listeners 에서만 동작하는 코드로 priority가 Major로 변경되었을 경우 True를 반환하는 스크립트 입니다.
    • 그 외에 많은 예제 스크립트가 있으니 참고하여 작성하면 됩니다.
  • Target Issue Type: Sub-task 카테고리의 이슈 타입을 설정합니다.
  • Subtask Summary: 생성할 Sub-task 제목(Summary)를 설정합니다.
  • Fields to copy: All / None / Custom 으로 상위 태스크의 필드 값을 복사할 것인지 설정합니다.
  • As User: 어떤 유저로 티켓 생성을 할 것인지 설정합니다.
    • 값이 없을 경우 현재 유저로 설정합니다.
  • Additional issue action: Sub-task 생성 이후 타겟 이슈(부모 이슈)에 대해 추가적인 작업을 하도록 설정합니다.
    • Condition과는 달리 추가적인 작업을 할 수 있도록 하는 스크립트 입력 필드입니다.
    • 제목을 바꾸거나 팝업 메세지를 따로 띄우거나 할 수 있습니다.
    • Show examples를 누르면 예시 코드를 볼 수 있습니다.
  • Subtask Action: 이미 생성된 Subtask가 있을 경우 transition을 진행할 것인지 설정합니다.
    • 주의할 점은 적용하고자하는 프로젝트의 서브 태스크가 어떤 워크플로우를 쓰는가를 확인하여 적용해야 합니다.

Create a sub-task 외에 다른 listener 타입이 있으며 아예 커스텀한 listener도 만들 수 있습니다.
Custom listener의 경우 다음과 같이 만들 수 있습니다.

img

이 listener는 앞서 설명드린 create a sub-task 와 프로젝트, 이벤트 필드 설정하는 것은 같지만
이벤트를 받고 처리하는 방식이 다릅니다.

발생한 이벤트에서 각종 데이터를 받아 처리해야하는 listener 타입 입니다.
listener에 필요한 listener 타입이 없다면 커스텀하게 만들어서 사용하면 됩니다.

// Listener 중 Remote Custom listener는 JIRA에 등록되어있는 application에도 scriptRunner를 설치해야 사용할 수 있습니다.

Script Fields

Script fields는 아래와 같이 설명이 나와있습니다.

A script field is a custom field that allows you to automatically display a value according to the results of a ScriptRunner script.

요약하자면, “스크립트러너의 스크립트 결과를 보여주는 Custom field“ 입니다.
참고 문서: https://scriptrunner.adaptavist.com/latest/jira/scripted-fields.html

img

다음 글에서는 남은 기능을 소개해보겠습니다.

  • REST Endpoints
  • Script Fragments
  • Escalation Services
  • Script JQL Functions