Github Action으로 깃허브 이슈와 JIRA 이슈를 연동하는 방법에 대해 소개합니다.
이슈 관리
현재 제 프로젝트에서 이슈를 관리하는 방법은 다음과 같습니다.
-
JIRA의 이슈 유형 레벨은 3단계로 나뉩니다(Epic → Story/Chore/Bug → Task). 레벨 2에 해당하는 이슈는 Task라는 세부 작업으로 나누어집니다.
예를 들어, Story가 ‘사용자 로그인 페이지 구현’이라는 작업이라면, 해당 Story의 하위 작업인 Task는 ‘로그인 페이지 레이아웃 구현’, ‘입력 필드 유효성 검사’, ‘API 연동’으로 구분될 수 있습니다.
-
깃허브 이슈는 레벨 2에 해당하는 이슈만 생성하고 해당 브랜치에서 하위 Task 작업을 진행합니다.
-
Task에 해당하는 작업은 되도록 하나의 커밋으로 하며, 커밋 메시지에는 JIRA 이슈 키를 붙입니다.
팀원 수가 많고 큰 작업을 여러 명이 공유해야 하는 상황이었다면 Task도 브랜치를 생성하여 작업했겠지만, 현 프로젝트와는 맞지 않아 Task는 커밋의 이슈 넘버로만 구분하기로 했습니다.
아래 내용은 JIRA에 깃허브 레포가 연동되어 있다는 것을 전제로 합니다. 이슈 템플릿 작성 방법은 따로 설명하지 않습니다.
JIRA API 토큰 발급
JIRA API에 필요한 토큰을 미리 발급받습니다.
프로필 > 계정 관리 > 보안 > API 토큰 만들기 및 관리에서 토큰을 만들 수 있습니다.
토큰 이름과 만료일을 설정하고 만든 토큰을 복사해서 저장해놓습니다.
이슈 생성 자동화
목표
github 이슈를 생성하면 JIRA 이슈 생성
github 이슈 제목에 JIRA 이슈 넘버 추가
해당 이슈와 관련된 브랜치 생성
이슈 템플릿
현재 사용하고 있는 이슈 템플릿은 다음과 같습니다. 해당 템플릿으로 작성된 이슈를 기반으로 JIRA에도 이슈가 생성되도록 할 예정입니다.
주요 필드를 간단하게 설명하자면,
-
제목(title): 이슈 제목입니다. 해당 이슈 제목과 동일한 JIRA 이슈를 생성합니다.
-
⚡️ 이슈 유형(issue_type): JIRA에서 생성될 이슈 유형입니다. (e.g. Story, Chore…)
-
🎟️ 상위 에픽 Number(epic_number): JIRA의 상위 이슈(에픽) 번호입니다.
-
🏷️ 레이블(issue_label): JIRA 이슈 세부 사항에 기재될 레이블 목록입니다. (다중 선택)
-
🌿 브랜치명(branch_name): 해당 이슈를 작업할 브랜치 이름입니다. 작성한 이름으로 develop에서 분기된 브랜치가 생성됩니다.
-
📌 이슈 설명(issue_description): 간단한 이슈 설명입니다. JIRA 이슈 본문(설명)에 포함될 내용입니다.
*괄호 안의 이름은 각 필드의 id 값입니다.
워크플로 작성
.github/workflows
디렉터리에 워크플로 파일을 생성합니다.
이슈가 열릴 때 워크플로 실행을 트리거하고 jobs.<job_id>.env
에 작업에 사용될 환경 변수들을 정의합니다.
name: Create Jira issue
on:
issues:
types:
- opened
jobs:
issue-sync:
name: Create Jira issue
runs-on: ubuntu-latest
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
PROJECT_KEY: ${{ secrets.PROJECT_KEY }}
- JIRA_BASE_URL: 이슈를 연동할 JIRA URL.
<your-domain>.atlassian.net
형식 - JIRA_API_TOKEN: JIRA에서 발급받은 토큰값
- JIRA_USER_EMAIL: JIRA 계정 이메일
- PROJECT_KEY: JIRA 프로젝트 키
JIRA와의 연동은 Atlassian에서 제공하는 atlassian/gajira를 사용할 수도 있었지만, 해당 Actions가 Node.js 16 기반으로 구성되어 있고 현재는 유지 보수를 중단한 상태라 직접 JIRA API를 호출하는 방식으로 진행하였습니다.
먼저, 사전 확인을 위해 JIRA 로그인 검증을 수행합니다. —fail 옵션으로 로그인 실패 시 바로 워크플로를 중단합니다. 기준 브랜치는 develop으로 두고 이슈를 JSON 구조로 변환합니다.
steps:
- name: JIRA Login
run: |
curl --fail --request GET \
--url "https://${JIRA_BASE_URL}/rest/api/3/myself" \
--user "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \
--header "Accept: application/json"
- name: Checkout develop
uses: actions/checkout@v4
with:
ref: develop
- name: Issue Parser
uses: stefanbuck/github-issue-praser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/feature-report.yml
특정 필드 값은 issueparser_<field_id>로 접근할 수 있습니다.
예를 들어 id 값이 issue_type이라면, 해당 필드 값은 ${{ steps.issue-parser.outputs.issueparser_issue_type }}
입니다.
POST /rest/api/3/issue 를 호출하여 JIRA에 새로운 이슈를 생성합니다. fields 객체에는 JIRA 이슈에 반영될 필드 값을 포함합니다. 기본적으로 project
, issuetype
, summary
는 필수값입니다. 프로젝트에 따라 필수로 지정된 필드가 존재한다면 해당 필드 값도 포함해야 합니다.
필수 필드*
project.key
: 프로젝트 키 (e.g. ABC)issuetype.name
: 이슈 유형. GET /rest/api/3/issuetype으로 조회 가능summary
: 이슈 요약
선택 필드
- labels: 이슈 레이블 필드
- parent.key: 상위 이슈 번호
- description: 이슈 설명에 들어갈 내용
- name: Create JIRA issue
id: create-issue
run: |
LABELS=$(echo "${{ steps.issue-parser.outputs.issueparser_issue_label }}" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .)
response=$(curl --fail --silent --request POST \
--url "https://${JIRA_BASE_URL}/rest/api/3/issue" \
--user "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "{
\"fields\": {
\"issuetype\": {
\"name\": \"${{ steps.issue-parser.outputs.issueparser_issue_type }}\"
},
\"labels\": ${LABELS},
\"parent\": {
\"key\": \"${{ steps.issue-parser.outputs.issueparser_epic_number }}\"
},
\"project\": {
\"key\": \"${PROJECT_KEY}\"
},
\"summary\": \"[BE] ${{ github.event.issue.title }}\",
\"description\": {
\"version\": 1,
\"type\": \"doc\",
\"content\": [
{
\"type\": \"heading\",
\"attrs\": { \"level\": 3 },
\"content\": [
{
\"type\": \"text\",
\"text\": \"🌿 브랜치명\"
}
]
},
{
\"type\": \"paragraph\",
\"content\": [
{
\"type\": \"text\",
\"text\": \"${{ steps.issue-parser.outputs.issueparser_branch_name }}\"
}
]
},
{
\"type\": \"heading\",
\"attrs\": { \"level\": 3 },
\"content\": [
{
\"type\": \"text\",
\"text\": \"📌 이슈 설명\"
}
]
},
{
\"type\": \"paragraph\",
\"content\": [
{
\"type\": \"text\",
\"text\": \"${{ steps.issue-parser.outputs.issueparser_issue_description }}\"
}
]
}
]
}
}
}")
echo "Response: $response"
해당 요청의 response 예시는 아래와 같습니다.
{
"id": "10000",
"key": "ED-24",
"self": "https://your-domain.atlassian.net/rest/api/3/issue/10000",
...
}
여기서 key는 생성된 새 이슈의 이슈 번호입니다. 워크플로 다음 단계에서 사용해야 하므로 속성값을 추출하여 환경 변수로 설정했습니다.
issue_key=$(echo "$response" | jq -r '.key')
echo "key=$issue_key" >> $GITHUB_ENV
생성된 이슈 키(env.key)와 github issue에 작성한 브랜치명을 조합하여 브랜치를 자동으로 생성하도록 구성했습니다. 예를 들어, 작성된 브랜치명이 sign-in
이고 이슈 키가 AB-1
이라면, 브랜치 이름은 feature/AB-1_sign-in
형식으로 생성됩니다.
- name: Create branch with JIRA issue key
run: |
TICKET_NUMBER="${{ env.key }}"
BRANCH_NAME="${{ steps.issue-parser.outputs.issueparser_branch_name }}"
NEW_BRANCH_NAME="feature/${TICKET_NUMBER}_$(echo ${BRANCH_NAME} | sed 's/ /-/g')"
git checkout -b "${NEW_BRANCH_NAME}"
git push origin "${NEW_BRANCH_NAME}"
깃허브 이슈 구분을 위해 이슈 키를 자동으로 제목에 prefix로 달아줍니다. 작성한 이슈 제목이 ‘이슈 제목입니다’라면, 워크플로 진행 후에는 ‘[AB-1] 이슈 제목입니다’로 업데이트됩니다.
- name: Update issue title
uses: actions-cool/issues-helper@v3
with:
actions: 'update-issue'
token: ${{ secrets.GITHUB_TOKEN }}
title: '[${{ env.key }}] ${{ github.event.issue.title }}'
마지막으로 깃허브 이슈에 JIRA 이슈 링크를 댓글로 추가하는 작업을 진행합니다.
- name: Add comment with Jira issue link
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: 'Jira Issue Created: [${{ env.key }}](https://${{ secrets.JIRA_BASE_URL }}/browse/${{ env.key }})'
이슈 완료 자동화
목표
github 이슈가 닫히면 JIRA 이슈 상태를 완료로 변경
워크플로 작성
깃허브 이슈가 닫히면 자동으로 해당 이슈와 관련된 JIRA 이슈의 상태를 ‘완료’로 변경하는 워크플로를 생성합니다.
이슈가 닫힐 때 워크플로 실행을 트리거하고 환경 변수들을 정의합니다.
name: Close Jira issue
on:
issues:
types:
- closed
jobs:
close-issue:
name: Close Jira issue
runs-on: ubuntu-latest
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
이슈 제목에서 JIRA 이슈 키를 추출하여 env.JIRA_KEY
로 저장합니다.
steps:
- name: JIRA Login
run: |
curl --fail --request GET \
--url "https://${JIRA_BASE_URL}/rest/api/3/myself" \
--user "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \
--header "Accept: application/json"
- name: Extract JIRA ticket number from GitHub issue title
id: extract-key
run: |
ISSUE_TITLE="${{ github.event.issue.title }}"
JIRA_KEY=$(echo "$ISSUE_TITLE" | grep -oE '[A-Z]+-[0-9]+')
echo "JIRA_KEY=$JIRA_KEY" >> $GITHUB_ENV
JIRA 상태를 업데이트하기 위해 GET /rest/api/3/issue/{issueIdOrKey}/transitions을 호출하여 transition id 값을 얻습니다. 예시는 아래와 같습니다.
{
"expand": "transitions",
"transitions": [
// ...
{
"id": "31",
"name": "완료"
// ...
}
]
}
해당 프로젝트에서는 id: 31
이 ‘완료’ 상태에 해당합니다.
추출한 이슈 키를 넣어 POST /rest/api/3/issue/{issueIdOrKey}/transitions로 transition 값을 업데이트합니다.
- name: Close JIRA issue
if: env.JIRA_KEY != ''
run: |
curl --fail --silent --request POST \
--url "https://${JIRA_BASE_URL}/rest/api/3/issue/${{ env.JIRA_KEY }}/transitions" \
--user "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '{
"transition": {
"id": "31"
}
}'
이렇게 깃허브 이슈가 closed 상태로 전환되면,
JIRA 이슈도 자동으로 ‘완료’ 상태로 전환됩니다.
github repo에 환경 변수 추가
워크플로가 제대로 실행되려면 jobs.<job_id>.env
에 설정한 환경 변수들을 리포지토리에 추가해야 합니다.
settings > Secrets and variables > Actions에서 추가할 수 있습니다.
커밋 메시지 이슈 키 prefix 규칙 설정
이슈 추적을 위해 커밋 메시지에 JIRA 이슈 번호를 prefix로 포함하도록 설정합니다. 만약, 브랜치에 이슈 번호가 포함되어 있고 브랜치 내 모든 커밋에 같은 이슈 번호를 붙인다면 추출해서 사용하는 방법도 있습니다.
프로젝트에 husky
와 커밋 규칙을 검증하는 commitlint
를 설치하고 husky를 초기화합니다.
> yarn add -D husky @commitlint/{cli,config-conventional}
> npx husky init
commitlint.config.js
파일을 생성하고 아래와 같이 작성합니다.
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-empty': [0],
'type-empty': [0],
'header-custom-pattern': [2, 'always'],
},
plugins: [
{
rules: {
'header-custom-pattern': ({ header }) => {
const pattern =
/^\[WFP-\d+\]\s(feat|fix|docs|style|refactor|perf|test|chore|build):\s.+/;
const isValid = pattern.test(header);
return [
isValid,
isValid
? ''
: 'The header must follow the pattern: "[WFP-{number}] type: message"',
];
},
},
},
],
};
header-custom-pattern
은 커스텀 규칙입니다. 프로젝트 이슈 키가 WFP이므로 커밋 메시지 헤더에 [WFP-{number}]와 같은 prefix가 반드시 포함되도록 했습니다. (WFP 대신 본인의 프로젝트 키를 설정하면 됩니다)
@commitlint/config-conventional
가 제공하는 기본 규칙을 사용하면 커스텀 규칙과 충돌이 일어나므로 subject-empty와 type-empty는 비활성화합니다.
.husky/commit-msg
파일에 아래 내용을 추가하면 적용됩니다.
npx commitlint --edit
type이 feat, fix, docs, style, refactor, perf, test, chore, build 중 하나가 아니라면 오류가 발생합니다. 메시지가 존재하지 않는 경우도 해당됩니다.
[issue number] type: message 형식이 맞다면 커밋 메시지가 잘 적용됩니다.