
저번 글의 과정을 따라하신 분들께서는 깃허브 이슈에서 이상한 점을 느끼신 분들이 계실 것입니다.
바로 Jira에서의 "설명", 즉 "Description"이 깃허브 이슈에서 누락되는 것을 확인하실 수 있을 것입니다.
제가 추측하는 해당 문제 원인은 Jira가 새 이슈 생성 직후에 description 필드(설명)를 비동기적으로 저장하기 때문에 발생하는! 즉, 트리거(When: 업무 항목 만들어짐) 시점에는 아직 description이 비어 있어서 GitHub에 전달되지 않은 것으로 추측되고 있습니다. (반면, priority, duedate, labels, initiator.displayName 등은 즉시 저장되어 있어서 정상 표시되는 것으로 생각됩니다.)
그래서 해당 문제를 "Jira → GitHub : Jira Issue의 내용 변경 시 → GitHub Issue의 내용 업데이트" 기능에 병합하기로 계획을 변경했습니다ㅠㅠ 다양한 방법을 시도했지만 저의 역량은 여기까지였습니다...(이번 글에 추가된 CheckList 또한 "본문"형식이라 "설명"과 동일하게 누락되는 것이 정상이며 체크리스트는 크게 신경쓰시지 않으셔도 됩니다.)
해당 기능은 바로 다음 게시글에서 다루도록 하겠습니다!
다시 본론으로 돌아와 이번 글에서는 Jira의 여러 필드들의 내용을 GitHub의 이슈로 만들 때 아주 이쁘고 정형화된 형태로 만들 수 있도록 알려드리도록 하겠습니다.
저희는 지금부터 이쁘고 정형화된 이슈를 만들기 위한 이슈 템플릿을 정의해보도록 하겠습니다!
조직 내 특정 레포에서 .github 디렉터리 내부에 "ISSUE_TEMPLATE" 디렉터리 생성
-> 해당 디렉터리 내부에 3개의 이슈 템플릿 생성 [automation-story.md / automation-bug.md / automation-task.md]
// "Add file" 클릭하여 웹에서도 파일 작성 가능


제가 작성한 이슈 템플릿은 아래와 같습니다.
## 📄 이슈 개요 (Description)
> {{description}}
---
### ✅ To-Do CheckList
{{checklist}}
---
### 👤 담당자
{{initiator}}
### 🎯 우선순위
<ins>{{priority}}</ins>
### 📅 기한
*{{duedate}}*
### 🔗 Jira Link
[**{{key}}**]({{url}})
이제 깃허브 액션을 사용하여 사전에 정의한 이슈 템플릿에 Jira로부터 받은 Json 데이터들을 넣어줄 워크플로우를 작성하도록 하겠습니다.
조직 내 특정 레포에서 "Actions" -> "New workflow" -> "set up a workflow yourself" -> 스크립트문 작성



제가 작성한 스크립트문은 아래와 같습니다.
name: Create GitHub Issue from Jira
on:
repository_dispatch:
types: [jira-issue]
permissions:
contents: read
issues: write
jobs:
create-issue:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render template and create issue
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const p = context.payload.client_payload || {};
const kind = (p.kind || '').toLowerCase();
const esc = (s) => (s ?? '').toString();
const nz = (s, fallback='') => {
const v = (s ?? '').toString().trim();
return v.length ? v : fallback;
};
const decodeMaybe = (s) => {
const str = esc(s);
// Jira에서 urlEncode 했으면 % 기호 포함 → decodeURIComponent 적용
try {
return /%[0-9A-Fa-f]{2}/.test(str) ? decodeURIComponent(str) : str;
} catch { return str; }
};
// 1) 템플릿 선택
const mapping = {
story: '.github/ISSUE_TEMPLATE/automation-story.md',
bug: '.github/ISSUE_TEMPLATE/automation-bug.md',
task: '.github/ISSUE_TEMPLATE/automation-task.md'
};
const file = mapping[kind] || mapping.task;
const templatePath = path.join(process.cwd(), file);
if (!fs.existsSync(templatePath)) {
core.setFailed(`Template not found: ${file}`);
return;
}
const tpl = fs.readFileSync(templatePath, 'utf8');
// 2) 값 준비
const key = esc(p.key);
const url = esc(p.url);
const summary = esc(p.summary);
const description = esc(p.description);
const initiator = esc(p.initiator);
const priority = esc(p.priority);
const duedate = nz(p.duedate, '미정');
const checklist = decodeMaybe(p.checklist);
// 3) 템플릿 치환
let body = tpl
.replaceAll('{{issue.key}}', key)
.replaceAll('{{key}}', key)
.replaceAll('{{issue.url}}', url)
.replaceAll('{{url}}', url)
.replaceAll('{{issue.summary}}', summary)
.replaceAll('{{summary}}', summary)
.replaceAll('{{description}}', description)
.replaceAll('{{initiator}}', initiator)
.replaceAll('{{priority}}', priority)
.replaceAll('{{issue.duedate}}', duedate)
.replaceAll('{{duedate}}', duedate)
.replaceAll('{{checklist}}', checklist || '');
// 템플릿에 {{checklist}}가 없다면 섹션을 추가로 붙여줌
if (checklist && !tpl.includes('{{checklist}}')) {
body += `\n\n---\n\n## ✅ To-Do\n${checklist}\n`;
}
// 4) 라벨 결정
const allowed = new Set(['Feature','Refactor','Documentation']);
let labels = [];
if (kind === 'story') labels = ['Story'];
else if (kind === 'bug') labels = ['Bug'];
else {
let incoming = [];
if (Array.isArray(p.labels)) incoming = p.labels;
else if (typeof p.labels === 'string') {
try {
const parsed = JSON.parse(p.labels);
if (Array.isArray(parsed)) incoming = parsed;
} catch {
incoming = p.labels.split(',').map(s=>s.trim()).filter(Boolean);
}
}
labels = incoming.filter(v => allowed.has(v));
if (labels.length === 0) labels = ['Task'];
}
// 5) 이슈 생성
const title = `[${key}] ${summary}`;
const res = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels
});
core.info(`Created issue #${res.data.number}`);
다음으로 저희가 이전에 만들었던 규칙들의 "Then: 웹 요청 전송"의 "웹 요청 URL"과 "사용자 정의 데이터" 부분을 아래와 같이 모두[Stroy / Bug / Task] 수정해주시면 됩니다.
웹 요청 URL : 기존의 issues로 끝나는 주소를 끝에만 issues에서 dispatches로 수정
사용자 정의 데이터 :
{"event_type":"jira-issue","client_payload":{
"key":"{{issue.key}}",
"url":"{{issue.url}}",
"summary":"{{issue.summary}}",
"initiator":"{{initiator.displayName}}",
"priority":"{{issue.priority.name}}",
"duedate":"{{issue.duedate}}",
"description":"{{issue.description.htmlEscape}}",
"kind":"{{issue.issueType.name}}",
"labels":"{{issue.labels}}"
}}
여기서 "dispatches"와 "issues"의 차이는 간단히 말해서 아래와 같습니다.
dispatch = “GitHub Actions 실행시켜줘!”
issues = “GitHub Issue를 직접 만들어줘!”


다음 글에서는 "Jira → GitHub : Jira Issue의 내용 변경 시 → GitHub Issue의 내용 업데이트" 기능과 더불어 "설명"과 "체크리스트"에 대한 문제 처리를 진행해보도록 하겠습니다.