Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/github-jira-issue-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,4 @@ jobs:
}

await new Promise(resolve => setTimeout(resolve, 1000));
}
}
232 changes: 177 additions & 55 deletions .github/workflows/github-jira-pr-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,125 @@ on:
- develop
- main

# 수동 트리거 추가
workflow_dispatch:
inputs:
process_all_open_prs:
description: 'Process all open PRs'
type: boolean
default: true

permissions:
issues: write
pull-requests: write
contents: read

jobs:
sync-pr-to-jira:
if: github.event_name == 'pull_request_target'
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.pull_request.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: Determine Issue Type
id: issue-type
run: |
TITLE='${{ github.event.pull_request.title }}'

if echo "$TITLE" | grep -qiE "^\[EPIC\]"; then
echo "type=Epic" >> $GITHUB_OUTPUT
elif echo "$TITLE" | grep -qiE "^\[STORY\]"; then
echo "type=Story" >> $GITHUB_OUTPUT
elif echo "$TITLE" | grep -qiE "^fix:|^hotfix:"; then
echo "type=Bug" >> $GITHUB_OUTPUT
else
echo "type=Task" >> $GITHUB_OUTPUT
fi

- name: Convert to Jira Syntax
uses: peter-evans/jira2md@v1
id: md2jira
- name: Parse PR Template
if: steps.check-jira.outputs.result == 'false'
id: parse
uses: actions/github-script@v7
with:
input-text: |
h3. GitHub PR
${{ github.event.pull_request.html_url }}
script: |
const title = context.payload.pull_request.title;
const body = context.payload.pull_request.body || '';

h3. Author
${{ github.event.pull_request.user.login }}
// 제목에서 type 파싱: feat(scope): summary 또는 fix: summary
const typeMatch = title.match(/^(\w+)(?:\([^)]*\))?:/);
const type = typeMatch ? typeMatch[1].toLowerCase() : 'task';

h3. Branch
${{ github.head_ref }} -> ${{ github.base_ref }}
// type → Jira Issue Type 매핑
const typeMap = {
'feat': 'Story',
'feature': 'Story',
'fix': 'Bug',
'hotfix': 'Bug',
'bug': 'Bug',
'epic': 'Epic',
'docs': 'Task',
'chore': 'Task',
'refactor': 'Task',
'test': 'Task',
'style': 'Task',
'perf': 'Task'
};
const jiraType = typeMap[type] || 'Task';

// 본문에서 섹션 파싱
const sections = {};
const sectionNames = ['목적', '변경 요약', '수용 기준 검증', '브레이킹/마이그레이션', '테스트', '참조'];

for (const name of sectionNames) {
const regex = new RegExp(`${name}\\n([\\s\\S]*?)(?=\\n(?:${sectionNames.join('|')})|$)`);
const match = body.match(regex);
if (match) {
sections[name] = match[1].trim();
}
}

// refs #123 에서 이슈 번호 추출
const issueRef = title.match(/refs #(\d+)/i);
const relatedIssue = issueRef ? issueRef[1] : null;

----
${{ github.event.pull_request.body }}
mode: md2jira
core.setOutput('jira_type', jiraType);
core.setOutput('purpose', sections['목적'] || '');
core.setOutput('changes', sections['변경 요약'] || '');
core.setOutput('acceptance', sections['수용 기준 검증'] || '');
core.setOutput('related_issue', relatedIssue || '');

- name: Create Jira Issue
if: steps.check-jira.outputs.result == 'false'
id: create-jira
uses: atlassian/gajira-create@v3
with:
project: MESP
issuetype: ${{ steps.issue-type.outputs.type }}
issuetype: ${{ steps.parse.outputs.jira_type }}
summary: '[PR-#${{ github.event.pull_request.number }}] ${{ github.event.pull_request.title }}'
description: '${{ steps.md2jira.outputs.output-text }}'
description: |
h3. GitHub PR
${{ github.event.pull_request.html_url }}

h3. Author
${{ github.event.pull_request.user.login }}

h3. Branch
${{ github.head_ref }} → ${{ github.base_ref }}

h3. 목적
${{ steps.parse.outputs.purpose }}

h3. 변경 요약
${{ steps.parse.outputs.changes }}

h3. 수용 기준
${{ steps.parse.outputs.acceptance }}

- name: Add Jira Link Comment
if: steps.check-jira.outputs.result == 'false'
uses: actions/github-script@v7
with:
script: |
Expand All @@ -83,40 +139,44 @@ jobs:
body: `Jira: [${jiraKey}](${jiraUrl})`
});

# 추가: 열린 PR 일괄 처리
# 기존 PR 일괄 처리
sync-all-open-prs:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Get Open PRs
id: get-prs
- name: Process All Open PRs
uses: actions/github-script@v7
with:
script: |
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});
return prs.data;

- name: Login to Jira
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: Process Each PR
uses: actions/github-script@v7
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
with:
script: |
const prs = ${{ steps.get-prs.outputs.result }};
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});

console.log(`Found ${prs.data.length} open PRs`);

for (const pr of prs) {
// 이미 Jira 링크가 있는지 확인
const typeMap = {
'feat': 'Story',
'feature': 'Story',
'fix': 'Bug',
'hotfix': 'Bug',
'bug': 'Bug',
'epic': 'Epic',
'docs': 'Task',
'chore': 'Task',
'refactor': 'Task',
'test': 'Task',
'style': 'Task',
'perf': 'Task'
};

for (const pr of prs.data) {
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
Expand All @@ -130,5 +190,67 @@ jobs:
}

console.log(`Processing PR #${pr.number}: ${pr.title}`);
// 여기서 Jira 생성 로직 호출

// 제목에서 type 파싱
const typeMatch = pr.title.match(/^(\w+)(?:\([^)]*\))?:/);
const type = typeMatch ? typeMatch[1].toLowerCase() : 'task';
const jiraType = typeMap[type] || 'Task';

// 본문 파싱
const body = pr.body || '';
const sections = {};
const sectionNames = ['목적', '변경 요약', '수용 기준 검증'];

for (const name of sectionNames) {
const regex = new RegExp(`${name}\\n([\\s\\S]*?)(?=\\n(?:목적|변경 요약|수용 기준 검증|브레이킹|테스트|참조)|$)`);
const match = body.match(regex);
if (match) sections[name] = match[1].trim();
}

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: `[PR-#${pr.number}] ${pr.title}`,
description: {
type: 'doc',
version: 1,
content: [
{ type: 'paragraph', content: [{ type: 'text', text: `GitHub PR: ${pr.html_url}` }] },
{ type: 'paragraph', content: [{ type: 'text', text: `Author: ${pr.user.login}` }] },
{ type: 'paragraph', content: [{ type: 'text', text: `Branch: ${pr.head.ref} → ${pr.base.ref}` }] },
{ type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '목적' }] },
{ type: 'paragraph', content: [{ type: 'text', text: sections['목적'] || '-' }] },
{ type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: '변경 요약' }] },
{ type: 'paragraph', content: [{ type: 'text', text: sections['변경 요약'] || '-' }] }
]
},
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: pr.number,
body: `Jira: [${jiraData.key}](${process.env.JIRA_BASE_URL}/browse/${jiraData.key})`
});
console.log(`Created Jira ${jiraData.key} for PR #${pr.number}`);
} else {
console.log(`Failed to create Jira for PR #${pr.number}:`, jiraData);
}

await new Promise(resolve => setTimeout(resolve, 1000));
}