[Automation] Jira → GitHub (Jira Issue 생성 시 GitHub 자동 Issue 생성-내용 상세화)

김동섭·2025년 10월 16일

Automation

목록 보기
4/8
post-thumbnail

저번 글의 과정을 따라하신 분들께서는 깃허브 이슈에서 이상한 점을 느끼신 분들이 계실 것입니다.
바로 Jira에서의 "설명", 즉 "Description"이 깃허브 이슈에서 누락되는 것을 확인하실 수 있을 것입니다.

제가 추측하는 해당 문제 원인은 Jira가 새 이슈 생성 직후에 description 필드(설명)를 비동기적으로 저장하기 때문에 발생하는! 즉, 트리거(When: 업무 항목 만들어짐) 시점에는 아직 description이 비어 있어서 GitHub에 전달되지 않은 것으로 추측되고 있습니다. (반면, priority, duedate, labels, initiator.displayName 등은 즉시 저장되어 있어서 정상 표시되는 것으로 생각됩니다.)

쉽게 말해서

  1. 사용자가 “이슈 생성” 버튼 클릭
  2. Jira는 기본 필드만 먼저 저장 → 이 시점에 issue key, summary, type만 존재
  3. description 필드는 비동기 요청으로 별도로 저장
  4. Automation의 “Issue created” 트리거는 [2] 에서 바로 실행됨 → 그래서 description은 비어 있음
    즉, 사용자가 아무리 “이슈 생성 시 description까지 한 번에 입력”해도, Jira의 백엔드에서 트리거 → Webhook 전송 타이밍이 description 저장보다 먼저라서 데이터가 누락되게 됨

그래서 해당 문제를 "Jira → GitHub : Jira Issue의 내용 변경 시 → GitHub Issue의 내용 업데이트" 기능에 병합하기로 계획을 변경했습니다ㅠㅠ 다양한 방법을 시도했지만 저의 역량은 여기까지였습니다...(이번 글에 추가된 CheckList 또한 "본문"형식이라 "설명"과 동일하게 누락되는 것이 정상이며 체크리스트는 크게 신경쓰시지 않으셔도 됩니다.)
해당 기능은 바로 다음 게시글에서 다루도록 하겠습니다!


다시 본론으로 돌아와 이번 글에서는 Jira의 여러 필드들의 내용을 GitHub의 이슈로 만들 때 아주 이쁘고 정형화된 형태로 만들 수 있도록 알려드리도록 하겠습니다.

0. 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}})

1. 깃허브 이슈 생성 자동화 by GitHub Actions

이제 깃허브 액션을 사용하여 사전에 정의한 이슈 템플릿에 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}`);

2. Jira - Automation Rule 수정

다음으로 저희가 이전에 만들었던 규칙들의 "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를 직접 만들어줘!”


3. 테스트 결과


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

profile
이것저것 모두 적어보는 공간

0개의 댓글