[FIX] 캐치마인드 게임 라운드 전환 및 WebSocket 이벤트 처리 개선 #46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue-Jira Sync | |
| on: | |
| issues: | |
| types: [opened, reopened, edited, closed ] | |
| workflow_dispatch: | |
| inputs: | |
| process_all_open_issues: | |
| description: '수동 트리거로 기존 이슈 일괄 처리' | |
| type: boolean | |
| default: true | |
| permissions: | |
| issues: write | |
| contents: read | |
| jobs: | |
| sync-issue-to-jira: | |
| if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check Existing Jira Link | |
| id: check-jira | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number | |
| }); | |
| const hasJira = comments.data.some(c => c.body.includes('Jira:')); | |
| return hasJira; | |
| - name: Login to Jira | |
| if: steps.check-jira.outputs.result == 'false' | |
| uses: atlassian/gajira-login@v3 | |
| env: | |
| JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | |
| - name: Parse Issue Template | |
| if: steps.check-jira.outputs.result == 'false' | |
| id: parse | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const labels = issue.labels.map(l => l.name); | |
| const title = issue.title; | |
| const body = issue.body || ''; | |
| // 라벨 기반 Jira Type 결정 | |
| let jiraType = 'Task'; | |
| if (labels.includes('epic')) jiraType = 'Epic'; | |
| else if (labels.includes('story')) jiraType = 'Story'; | |
| else if (labels.includes('bug')) jiraType = 'Bug'; | |
| // task, change-request, spike → Task | |
| // 제목 prefix로 백업 판단 | |
| if (jiraType === 'Task') { | |
| if (/^\[EPIC\]/i.test(title)) jiraType = 'Epic'; | |
| else if (/^\[STORY\]/i.test(title)) jiraType = 'Story'; | |
| else if (/^\[BUG\]/i.test(title)) jiraType = 'Bug'; | |
| } | |
| // 템플릿 필드 파싱 (### 헤더 기반) | |
| const parseSection = (label) => { | |
| const regex = new RegExp(`### ${label}\\s*\\n([\\s\\S]*?)(?=###|$)`); | |
| const match = body.match(regex); | |
| return match ? match[1].trim() : ''; | |
| }; | |
| // 공통 + 템플릿별 필드 | |
| const fields = { | |
| // Epic | |
| goal: parseSection('목표'), | |
| scope: parseSection('범위 / Not-in-scope') || parseSection('작업 범위'), | |
| breakdown: parseSection('하위 스토리\\(체크리스트\\)'), | |
| milestone: parseSection('마일스톤'), | |
| // Story | |
| background: parseSection('배경'), | |
| ac: parseSection('수용 기준\\(AC\\)'), | |
| design: parseSection('디자인/문서 링크') || parseSection('디자인/계약 링크'), | |
| notes: parseSection('구현 메모/리스크'), | |
| epic: parseSection('연결된 Epic'), | |
| // Task | |
| parent: parseSection('연결된 Story/Epic'), | |
| done: parseSection('Done 기준'), | |
| // Change Request | |
| related: parseSection('영향받는 Epic/Story/Task'), | |
| change: parseSection('제안 변경 사항'), | |
| impact: parseSection('영향도'), | |
| decision: parseSection('결정/대안/근거\\(ADR 링크\\)'), | |
| // Spike | |
| timebox: parseSection('타임박스'), | |
| questions: parseSection('핵심 질문'), | |
| approach: parseSection('접근 방법'), | |
| deliverables: parseSection('산출물\\(요약/ADR/POC 링크\\)') | |
| }; | |
| // 템플릿 타입 판단 | |
| let templateType = 'task'; | |
| if (labels.includes('epic') || /^\[EPIC\]/i.test(title)) templateType = 'epic'; | |
| else if (labels.includes('story') || /^\[STORY\]/i.test(title)) templateType = 'story'; | |
| else if (labels.includes('change-request') || /^\[CR\]/i.test(title)) templateType = 'cr'; | |
| else if (labels.includes('spike') || /^\[SPIKE\]/i.test(title)) templateType = 'spike'; | |
| core.setOutput('jira_type', jiraType); | |
| core.setOutput('template_type', templateType); | |
| core.setOutput('goal', fields.goal); | |
| core.setOutput('scope', fields.scope); | |
| core.setOutput('breakdown', fields.breakdown); | |
| core.setOutput('background', fields.background); | |
| core.setOutput('ac', fields.ac); | |
| core.setOutput('design', fields.design); | |
| core.setOutput('parent', fields.parent); | |
| core.setOutput('change', fields.change); | |
| core.setOutput('impact', fields.impact); | |
| core.setOutput('timebox', fields.timebox); | |
| core.setOutput('questions', fields.questions); | |
| - name: Build Jira Description | |
| if: steps.check-jira.outputs.result == 'false' | |
| id: description | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const templateType = '${{ steps.parse.outputs.template_type }}'; | |
| let desc = `h3. GitHub Issue\n${{ github.event.issue.html_url }}\n\nh3. Author\n${{ github.event.issue.user.login }}\n\n`; | |
| switch (templateType) { | |
| case 'epic': | |
| desc += `h3. 목표\n${{ steps.parse.outputs.goal }}\n\n`; | |
| desc += `h3. 범위\n${{ steps.parse.outputs.scope }}\n\n`; | |
| desc += `h3. 하위 스토리\n${{ steps.parse.outputs.breakdown }}\n`; | |
| break; | |
| case 'story': | |
| desc += `h3. 배경\n${{ steps.parse.outputs.background }}\n\n`; | |
| desc += `h3. 수용 기준(AC)\n${{ steps.parse.outputs.ac }}\n\n`; | |
| desc += `h3. 디자인\n${{ steps.parse.outputs.design }}\n`; | |
| break; | |
| case 'cr': | |
| desc += `h3. 제안 변경 사항\n${{ steps.parse.outputs.change }}\n\n`; | |
| desc += `h3. 영향도\n${{ steps.parse.outputs.impact }}\n`; | |
| break; | |
| case 'spike': | |
| desc += `h3. 타임박스\n${{ steps.parse.outputs.timebox }}\n\n`; | |
| desc += `h3. 핵심 질문\n${{ steps.parse.outputs.questions }}\n`; | |
| break; | |
| default: // task | |
| desc += `h3. 연결된 Story/Epic\n${{ steps.parse.outputs.parent }}\n\n`; | |
| desc += `h3. 작업 범위\n${{ steps.parse.outputs.scope }}\n`; | |
| } | |
| core.setOutput('content', desc); | |
| - name: Create Jira Issue | |
| if: steps.check-jira.outputs.result == 'false' | |
| id: create-jira | |
| uses: atlassian/gajira-create@v3 | |
| with: | |
| project: MESP | |
| issuetype: ${{ steps.parse.outputs.jira_type }} | |
| summary: '[Issue-#${{ github.event.issue.number }}] ${{ github.event.issue.title }}' | |
| description: '${{ steps.description.outputs.content }}' | |
| - name: Add Jira Link Comment | |
| if: steps.check-jira.outputs.result == 'false' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const jiraKey = '${{ steps.create-jira.outputs.issue }}'; | |
| const jiraUrl = '${{ secrets.JIRA_BASE_URL }}/browse/' + jiraKey; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `Jira: [${jiraKey}](${jiraUrl})` | |
| }); | |
| # Issue 수정 시 Jira 업데이트 | |
| update-jira-on-edit: | |
| if: github.event_name == 'issues' && github.event.action == 'edited' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get Jira Key from Comments | |
| id: get-jira-key | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number | |
| }); | |
| for (const comment of comments.data) { | |
| const match = comment.body.match(/Jira: \[([A-Z]+-\d+)\]/); | |
| if (match) { | |
| core.setOutput('jira_key', match[1]); | |
| return match[1]; | |
| } | |
| } | |
| core.setOutput('jira_key', ''); | |
| return ''; | |
| - name: Parse Updated Issue | |
| if: steps.get-jira-key.outputs.jira_key != '' | |
| id: parse | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const labels = issue.labels.map(l => l.name); | |
| const title = issue.title; | |
| const body = issue.body || ''; | |
| const parseSection = (label) => { | |
| const regex = new RegExp(`### ${label}\\s*\\n([\\s\\S]*?)(?=###|$)`); | |
| const match = body.match(regex); | |
| return match ? match[1].trim() : '-'; | |
| }; | |
| let templateType = 'task'; | |
| if (labels.includes('epic') || /^\[EPIC\]/i.test(title)) templateType = 'epic'; | |
| else if (labels.includes('story') || /^\[STORY\]/i.test(title)) templateType = 'story'; | |
| else if (labels.includes('change-request') || /^\[CR\]/i.test(title)) templateType = 'cr'; | |
| else if (labels.includes('spike') || /^\[SPIKE\]/i.test(title)) templateType = 'spike'; | |
| const descContent = [ | |
| { type: 'paragraph', content: [{ type: 'text', text: `GitHub Issue: ${issue.html_url}` }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: `Author: ${issue.user.login}` }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: `Last Updated: ${new Date().toISOString()}` }] } | |
| ]; | |
| switch (templateType) { | |
| case 'epic': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '목표' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('목표') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '범위' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('범위 / Not-in-scope') }] } | |
| ); | |
| break; | |
| case 'story': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '배경' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('배경') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '수용 기준(AC)' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('수용 기준\\(AC\\)') }] } | |
| ); | |
| break; | |
| case 'cr': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '제안 변경 사항' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('제안 변경 사항') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '영향도' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('영향도') }] } | |
| ); | |
| break; | |
| case 'spike': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '타임박스' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('타임박스') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '핵심 질문' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('핵심 질문') }] } | |
| ); | |
| break; | |
| default: | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '연결된 Story/Epic' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('연결된 Story/Epic') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '작업 범위' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('작업 범위') }] } | |
| ); | |
| } | |
| core.setOutput('description', JSON.stringify(descContent)); | |
| - name: Update Jira Issue | |
| if: steps.get-jira-key.outputs.jira_key != '' | |
| uses: actions/github-script@v7 | |
| env: | |
| JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | |
| DESC_JSON: ${{ steps.parse.outputs.description }} | |
| with: | |
| script: | | |
| const jiraKey = '${{ steps.get-jira-key.outputs.jira_key }}'; | |
| const issue = context.payload.issue; | |
| const descContent = JSON.parse(process.env.DESC_JSON); | |
| const response = await fetch( | |
| `${process.env.JIRA_BASE_URL}/rest/api/3/issue/${jiraKey}`, | |
| { | |
| method: 'PUT', | |
| headers: { | |
| 'Authorization': `Basic ${Buffer.from(`${process.env.JIRA_USER_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| fields: { | |
| summary: `[Issue-#${issue.number}] ${issue.title}`, | |
| description: { | |
| type: 'doc', | |
| version: 1, | |
| content: descContent | |
| } | |
| } | |
| }) | |
| } | |
| ); | |
| if (response.ok) { | |
| console.log(`✓ Updated Jira ${jiraKey}`); | |
| } else { | |
| const error = await response.text(); | |
| console.log(`✗ Failed to update Jira ${jiraKey}: ${error}`); | |
| } | |
| # Issue 닫기 시 Jira 상태 변경 | |
| close-jira-on-issue-close: | |
| if: github.event_name == 'issues' && github.event.action == 'closed' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get Jira Key from Comments | |
| id: get-jira-key | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number | |
| }); | |
| for (const comment of comments.data) { | |
| const match = comment.body.match(/Jira: \[([A-Z]+-\d+)\]/); | |
| if (match) { | |
| core.setOutput('jira_key', match[1]); | |
| return match[1]; | |
| } | |
| } | |
| core.setOutput('jira_key', ''); | |
| return ''; | |
| - name: Get Jira Transitions | |
| if: steps.get-jira-key.outputs.jira_key != '' | |
| id: get-transitions | |
| uses: actions/github-script@v7 | |
| env: | |
| JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | |
| with: | |
| script: | | |
| const jiraKey = '${{ steps.get-jira-key.outputs.jira_key }}'; | |
| const response = await fetch( | |
| `${process.env.JIRA_BASE_URL}/rest/api/3/issue/${jiraKey}/transitions`, | |
| { | |
| headers: { | |
| 'Authorization': `Basic ${Buffer.from(`${process.env.JIRA_USER_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| } | |
| ); | |
| const data = await response.json(); | |
| console.log('Available transitions:', JSON.stringify(data.transitions, null, 2)); | |
| // "Done", "완료", "Closed" 등의 transition 찾기 | |
| const doneTransition = data.transitions.find(t => | |
| /done|완료|closed?|complete/i.test(t.name) | |
| ); | |
| if (doneTransition) { | |
| core.setOutput('transition_id', doneTransition.id); | |
| console.log(`Found transition: ${doneTransition.name} (${doneTransition.id})`); | |
| } else { | |
| core.setOutput('transition_id', ''); | |
| console.log('No matching transition found'); | |
| } | |
| - name: Transition Jira to Done | |
| if: steps.get-jira-key.outputs.jira_key != '' && steps.get-transitions.outputs.transition_id != '' | |
| uses: actions/github-script@v7 | |
| env: | |
| JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | |
| with: | |
| script: | | |
| const jiraKey = '${{ steps.get-jira-key.outputs.jira_key }}'; | |
| const transitionId = '${{ steps.get-transitions.outputs.transition_id }}'; | |
| const response = await fetch( | |
| `${process.env.JIRA_BASE_URL}/rest/api/3/issue/${jiraKey}/transitions`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Basic ${Buffer.from(`${process.env.JIRA_USER_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| transition: { id: transitionId } | |
| }) | |
| } | |
| ); | |
| if (response.ok) { | |
| console.log(`✓ Transitioned Jira ${jiraKey} to Done`); | |
| } else { | |
| const error = await response.text(); | |
| console.log(`✗ Failed to transition Jira ${jiraKey}: ${error}`); | |
| } | |
| - name: Add Close Comment to Jira | |
| if: steps.get-jira-key.outputs.jira_key != '' | |
| uses: actions/github-script@v7 | |
| env: | |
| JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | |
| with: | |
| script: | | |
| const jiraKey = '${{ steps.get-jira-key.outputs.jira_key }}'; | |
| const issue = context.payload.issue; | |
| await fetch( | |
| `${process.env.JIRA_BASE_URL}/rest/api/3/issue/${jiraKey}/comment`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Basic ${Buffer.from(`${process.env.JIRA_USER_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| body: { | |
| type: 'doc', | |
| version: 1, | |
| content: [ | |
| { | |
| type: 'paragraph', | |
| content: [ | |
| { type: 'text', text: `GitHub Issue closed by ${issue.user.login} at ${new Date().toISOString()}` } | |
| ] | |
| } | |
| ] | |
| } | |
| }) | |
| } | |
| ); | |
| # 기존 이슈 일괄 처리 | |
| sync-all-open-issues: | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Process All Open Issues | |
| uses: actions/github-script@v7 | |
| env: | |
| JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} | |
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | |
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | |
| with: | |
| script: | | |
| const issues = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| const realIssues = issues.data.filter(issue => !issue.pull_request); | |
| console.log(`Found ${realIssues.length} open issues`); | |
| for (const issue of realIssues) { | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number | |
| }); | |
| const hasJiraLink = comments.data.some(c => c.body.includes('Jira:')); | |
| if (hasJiraLink) { | |
| console.log(`Issue #${issue.number} already has Jira link, skipping`); | |
| continue; | |
| } | |
| console.log(`Processing Issue #${issue.number}: ${issue.title}`); | |
| const labels = issue.labels.map(l => l.name); | |
| const title = issue.title; | |
| const body = issue.body || ''; | |
| // Jira Type 결정 | |
| let jiraType = 'Task'; | |
| if (labels.includes('epic') || /^\[EPIC\]/i.test(title)) jiraType = 'Epic'; | |
| else if (labels.includes('story') || /^\[STORY\]/i.test(title)) jiraType = 'Story'; | |
| else if (labels.includes('bug') || /^\[BUG\]/i.test(title)) jiraType = 'Bug'; | |
| // 템플릿 타입 판단 | |
| let templateType = 'task'; | |
| if (labels.includes('epic') || /^\[EPIC\]/i.test(title)) templateType = 'epic'; | |
| else if (labels.includes('story') || /^\[STORY\]/i.test(title)) templateType = 'story'; | |
| else if (labels.includes('change-request') || /^\[CR\]/i.test(title)) templateType = 'cr'; | |
| else if (labels.includes('spike') || /^\[SPIKE\]/i.test(title)) templateType = 'spike'; | |
| // 섹션 파싱 | |
| const parseSection = (label) => { | |
| const regex = new RegExp(`### ${label}\\s*\\n([\\s\\S]*?)(?=###|$)`); | |
| const match = body.match(regex); | |
| return match ? match[1].trim() : '-'; | |
| }; | |
| // description 구성 | |
| const descContent = [ | |
| { type: 'paragraph', content: [{ type: 'text', text: `GitHub Issue: ${issue.html_url}` }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: `Author: ${issue.user.login}` }] } | |
| ]; | |
| switch (templateType) { | |
| case 'epic': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '목표' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('목표') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '범위' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('범위 / Not-in-scope') }] } | |
| ); | |
| break; | |
| case 'story': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '배경' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('배경') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '수용 기준(AC)' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('수용 기준\\(AC\\)') }] } | |
| ); | |
| break; | |
| case 'cr': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '제안 변경 사항' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('제안 변경 사항') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '영향도' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('영향도') }] } | |
| ); | |
| break; | |
| case 'spike': | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '타임박스' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('타임박스') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '핵심 질문' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('핵심 질문') }] } | |
| ); | |
| break; | |
| default: | |
| descContent.push( | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '연결된 Story/Epic' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('연결된 Story/Epic') }] }, | |
| { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '작업 범위' }] }, | |
| { type: 'paragraph', content: [{ type: 'text', text: parseSection('작업 범위') }] } | |
| ); | |
| } | |
| const jiraResponse = await fetch( | |
| `${process.env.JIRA_BASE_URL}/rest/api/3/issue`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Basic ${Buffer.from(`${process.env.JIRA_USER_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| fields: { | |
| project: { key: 'MESP' }, | |
| summary: `[Issue-#${issue.number}] ${issue.title}`, | |
| description: { | |
| type: 'doc', | |
| version: 1, | |
| content: descContent | |
| }, | |
| issuetype: { name: jiraType } | |
| } | |
| }) | |
| } | |
| ); | |
| const jiraData = await jiraResponse.json(); | |
| if (jiraData.key) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: `Jira: [${jiraData.key}](${process.env.JIRA_BASE_URL}/browse/${jiraData.key})` | |
| }); | |
| console.log(`Created Jira ${jiraData.key} for Issue #${issue.number}`); | |
| } else { | |
| console.log(`Failed to create Jira for Issue #${issue.number}:`, jiraData); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |