From 1ac842cef430813993e2ea158c53b49ce3950589 Mon Sep 17 00:00:00 2001 From: arlen02-01 Date: Fri, 10 Apr 2026 13:47:12 +0900 Subject: [PATCH 1/4] chore: switch jira-github issue automation to branch-driven flow --- .../create-github-issue-from-jira-branch.yml | 173 ++++++++++++++++++ .github/workflows/create-jira-issue.yml | 102 ----------- 2 files changed, 173 insertions(+), 102 deletions(-) create mode 100644 .github/workflows/create-github-issue-from-jira-branch.yml delete mode 100644 .github/workflows/create-jira-issue.yml diff --git a/.github/workflows/create-github-issue-from-jira-branch.yml b/.github/workflows/create-github-issue-from-jira-branch.yml new file mode 100644 index 00000000..bfa3fb4c --- /dev/null +++ b/.github/workflows/create-github-issue-from-jira-branch.yml @@ -0,0 +1,173 @@ +name: Create GitHub Issue from Jira Branch + +on: + create: + +permissions: + contents: read + issues: write + +jobs: + create-github-issue: + if: github.event.ref_type == 'branch' + runs-on: ubuntu-latest + + steps: + - name: Extract Jira key from branch name + id: extract + shell: bash + run: | + BRANCH_NAME="${{ github.event.ref }}" + if [[ "$BRANCH_NAME" =~ ([A-Z][A-Z0-9]+-[0-9]+) ]]; then + echo "jira_key=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + else + echo "No Jira key found in branch: $BRANCH_NAME" + echo "jira_key=" >> "$GITHUB_OUTPUT" + fi + + - name: Stop when branch has no Jira key + if: steps.extract.outputs.jira_key == '' + run: echo "Skip - branch name does not include Jira issue key." + + - name: Fetch Jira issue summary and description + if: steps.extract.outputs.jira_key != '' + id: jira + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + shell: bash + run: | + set -euo pipefail + + RESPONSE=$(curl -sS --fail \ + -u "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ + -H "Accept: application/json" \ + "${JIRA_BASE_URL}/rest/api/3/issue/${JIRA_KEY}?fields=summary,description&expand=renderedFields") + + SUMMARY=$(echo "$RESPONSE" | jq -r '.fields.summary // empty') + DESCRIPTION_HTML=$(echo "$RESPONSE" | jq -r '.renderedFields.description // ""') + + if [[ -z "$SUMMARY" ]]; then + echo "Jira issue summary is empty for key: $JIRA_KEY" + exit 1 + fi + + { + echo "summary=$SUMMARY" + echo "description_html<> "$GITHUB_OUTPUT" + + - name: Check existing GitHub issue for Jira key + if: steps.extract.outputs.jira_key != '' + id: dedup + uses: actions/github-script@v7 + env: + JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + with: + script: | + const jiraKey = process.env.JIRA_KEY; + const repo = `${context.repo.owner}/${context.repo.repo}`; + const q = `repo:${repo} in:body "JIRA_KEY: ${jiraKey}"`; + const result = await github.rest.search.issuesAndPullRequests({ + q, + per_page: 10, + }); + + const existing = result.data.items.find((item) => !item.pull_request); + if (existing) { + core.info(`Issue already exists: #${existing.number}`); + core.setOutput('exists', 'true'); + core.setOutput('number', String(existing.number)); + } else { + core.setOutput('exists', 'false'); + } + + - name: Create GitHub issue + if: steps.extract.outputs.jira_key != '' && steps.dedup.outputs.exists != 'true' + id: create_issue + uses: actions/github-script@v7 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + JIRA_SUMMARY: ${{ steps.jira.outputs.summary }} + JIRA_DESCRIPTION_HTML: ${{ steps.jira.outputs.description_html }} + BRANCH_NAME: ${{ steps.extract.outputs.branch_name }} + with: + script: | + const jiraKey = process.env.JIRA_KEY; + const title = `[${jiraKey}] ${process.env.JIRA_SUMMARY}`; + const body = [ + `JIRA_KEY: ${jiraKey}`, + '', + `Jira: ${process.env.JIRA_BASE_URL}/browse/${jiraKey}`, + `Branch: \`${process.env.BRANCH_NAME}\``, + '', + '### Jira Description', + process.env.JIRA_DESCRIPTION_HTML || '(no description)', + ].join('\n'); + + const created = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + }); + + core.setOutput('number', String(created.data.number)); + + - name: Comment GitHub issue link back to Jira + if: steps.extract.outputs.jira_key != '' && steps.dedup.outputs.exists != 'true' + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + GH_ISSUE_NUMBER: ${{ steps.create_issue.outputs.number }} + GH_REPO: ${{ github.repository }} + GH_SERVER_URL: ${{ github.server_url }} + shell: bash + run: | + set -euo pipefail + + GH_ISSUE_URL="${GH_SERVER_URL}/${GH_REPO}/issues/${GH_ISSUE_NUMBER}" + + COMMENT_JSON=$(jq -n \ + --arg url "$GH_ISSUE_URL" \ + --arg no "$GH_ISSUE_NUMBER" \ + '{ + body: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "GitHub issue created: " }, + { + type: "text", + text: ("#" + $no), + marks: [ + { + type: "link", + attrs: { href: $url } + } + ] + } + ] + } + ] + } + }') + + curl -sS --fail \ + -u "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -X POST \ + --data "$COMMENT_JSON" \ + "${JIRA_BASE_URL}/rest/api/3/issue/${JIRA_KEY}/comment" > /dev/null diff --git a/.github/workflows/create-jira-issue.yml b/.github/workflows/create-jira-issue.yml deleted file mode 100644 index 95c635b8..00000000 --- a/.github/workflows/create-jira-issue.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Create Jira issue - -on: - issues: - types: - - opened - -permissions: - issues: write - contents: write - pull-requests: write - -jobs: - create-issue: - runs-on: ubuntu-latest - - steps: - # 1️⃣ Jira 로그인 - - 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 }} - - # 2️⃣ 현재 브랜치 기준 checkout - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: develop - - # 3️⃣ Issue Form 파싱 - - name: Parse issue form - uses: stefanbuck/github-issue-praser@v3 - id: issue-parser - with: - template-path: .github/ISSUE_TEMPLATE/issue-form.yml - - # 5️⃣ parentKey 검증 - - name: Validate parent key - if: ${{ steps.issue-parser.outputs.issueparser_parentKey == '' }} - run: | - echo "Parent key is empty. Failing workflow." - exit 1 - - # 6️⃣ Markdown → Jira 변환 - - name: Convert markdown to Jira format - uses: peter-evans/jira2md@v1 - id: md2jira - with: - input-text: | - ### Github Issue Link - - ${{ github.event.issue.html_url }} - - ${{ github.event.issue.body }} - mode: md2jira - - # 7️⃣ Jira 이슈 생성 - - name: Create Jira issue - id: create - uses: atlassian/gajira-create@v3 - with: - project: OT - issuetype: Task - summary: "${{ github.event.issue.title }}" - description: "${{ steps.md2jira.outputs.output-text }}" - fields: | - { - "parent": { - "key": "${{ steps.issue-parser.outputs.issueparser_parentKey }}" - } - } - - # 8️⃣ 브랜치 생성 (JiraKey-branchName) - - name: Create branch with Jira key - run: | - ISSUE_KEY="${{ steps.create.outputs.issue }}" - BRANCH_NAME="${{ steps.issue-parser.outputs.issueparser_branch }}" - FINAL_BRANCH="${ISSUE_KEY}-${BRANCH_NAME}" - - git config user.name "github-actions" - git config user.email "github-actions@github.com" - - git checkout -b "$FINAL_BRANCH" - git push origin "$FINAL_BRANCH" - - # 9️⃣ GitHub 이슈 제목에 Jira 키 붙이기 - - name: Update GitHub issue title - uses: actions-cool/issues-helper@v3 - with: - actions: update-issue - token: ${{ secrets.GITHUB_TOKEN }} - title: "[${{ steps.create.outputs.issue }}] ${{ github.event.issue.title }}" - - # 🔟 Jira 링크 코멘트 추가 - - name: Comment Jira 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: [${{ steps.create.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create.outputs.issue }})" From 7ca6f6ecc1e2344f8e469c9967c536a5e9021814 Mon Sep 17 00:00:00 2001 From: arlen02-01 Date: Fri, 10 Apr 2026 14:14:07 +0900 Subject: [PATCH 2/4] chore: align auto-created github issue template with jira fields --- .../create-github-issue-from-jira-branch.yml | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/create-github-issue-from-jira-branch.yml b/.github/workflows/create-github-issue-from-jira-branch.yml index bfa3fb4c..d3a76b92 100644 --- a/.github/workflows/create-github-issue-from-jira-branch.yml +++ b/.github/workflows/create-github-issue-from-jira-branch.yml @@ -49,17 +49,20 @@ jobs: SUMMARY=$(echo "$RESPONSE" | jq -r '.fields.summary // empty') DESCRIPTION_HTML=$(echo "$RESPONSE" | jq -r '.renderedFields.description // ""') + PARENT_KEY=$(echo "$RESPONSE" | jq -r '.fields.parent.key // ""') if [[ -z "$SUMMARY" ]]; then echo "Jira issue summary is empty for key: $JIRA_KEY" exit 1 fi + DELIMITER="EOF_$(date +%s)_$RANDOM" { echo "summary=$SUMMARY" - echo "description_html<> "$GITHUB_OUTPUT" - name: Check existing GitHub issue for Jira key @@ -72,13 +75,17 @@ jobs: script: | const jiraKey = process.env.JIRA_KEY; const repo = `${context.repo.owner}/${context.repo.repo}`; - const q = `repo:${repo} in:body "JIRA_KEY: ${jiraKey}"`; + const marker = `JIRA_KEY: ${jiraKey}`; + const q = `repo:${repo} "${jiraKey}"`; const result = await github.rest.search.issuesAndPullRequests({ q, - per_page: 10, + per_page: 20, }); - const existing = result.data.items.find((item) => !item.pull_request); + const existing = result.data.items.find((item) => + !item.pull_request && + (item.title.startsWith(`[${jiraKey}]`) || item.body?.includes(marker)) + ); if (existing) { core.info(`Issue already exists: #${existing.number}`); core.setOutput('exists', 'true'); @@ -95,20 +102,33 @@ jobs: JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} JIRA_KEY: ${{ steps.extract.outputs.jira_key }} JIRA_SUMMARY: ${{ steps.jira.outputs.summary }} + JIRA_PARENT_KEY: ${{ steps.jira.outputs.parent_key }} JIRA_DESCRIPTION_HTML: ${{ steps.jira.outputs.description_html }} BRANCH_NAME: ${{ steps.extract.outputs.branch_name }} with: script: | const jiraKey = process.env.JIRA_KEY; const title = `[${jiraKey}] ${process.env.JIRA_SUMMARY}`; + const summaryForBranch = (process.env.JIRA_SUMMARY || '') + .trim() + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9/_-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + const recommendedBranch = `${jiraKey}-${summaryForBranch || 'task'}`; + const parentKey = process.env.JIRA_PARENT_KEY || '없음'; const body = [ `JIRA_KEY: ${jiraKey}`, '', - `Jira: ${process.env.JIRA_BASE_URL}/browse/${jiraKey}`, - `Branch: \`${process.env.BRANCH_NAME}\``, + '### ️ 상위 작업 (Ticket Number)', + parentKey, '', - '### Jira Description', + '### 브랜치명 (Branch)', + `\`${recommendedBranch}\``, + '', + '### 상세 내용 (Description)', process.env.JIRA_DESCRIPTION_HTML || '(no description)', + '', ].join('\n'); const created = await github.rest.issues.create({ From fa87f14da8b372b6c1c769923515aa82438de998 Mon Sep 17 00:00:00 2001 From: arlen02-01 Date: Fri, 10 Apr 2026 14:45:08 +0900 Subject: [PATCH 3/4] fix: include parent field and use actual branch name in issue body --- .github/workflows/create-github-issue-from-jira-branch.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-github-issue-from-jira-branch.yml b/.github/workflows/create-github-issue-from-jira-branch.yml index d3a76b92..b1e05cbc 100644 --- a/.github/workflows/create-github-issue-from-jira-branch.yml +++ b/.github/workflows/create-github-issue-from-jira-branch.yml @@ -45,7 +45,7 @@ jobs: RESPONSE=$(curl -sS --fail \ -u "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ -H "Accept: application/json" \ - "${JIRA_BASE_URL}/rest/api/3/issue/${JIRA_KEY}?fields=summary,description&expand=renderedFields") + "${JIRA_BASE_URL}/rest/api/3/issue/${JIRA_KEY}?fields=summary,description,parent&expand=renderedFields") SUMMARY=$(echo "$RESPONSE" | jq -r '.fields.summary // empty') DESCRIPTION_HTML=$(echo "$RESPONSE" | jq -r '.renderedFields.description // ""') @@ -117,6 +117,7 @@ jobs: .replace(/^-|-$/g, ''); const recommendedBranch = `${jiraKey}-${summaryForBranch || 'task'}`; const parentKey = process.env.JIRA_PARENT_KEY || '없음'; + const actualBranch = process.env.BRANCH_NAME || recommendedBranch; const body = [ `JIRA_KEY: ${jiraKey}`, '', @@ -124,7 +125,7 @@ jobs: parentKey, '', '### 브랜치명 (Branch)', - `\`${recommendedBranch}\``, + `\`${actualBranch}\``, '', '### 상세 내용 (Description)', process.env.JIRA_DESCRIPTION_HTML || '(no description)', From 21f1599ff1c6b86bec593102af1ea5105f9b005e Mon Sep 17 00:00:00 2001 From: arlen02-01 Date: Fri, 10 Apr 2026 14:51:47 +0900 Subject: [PATCH 4/4] fix: harden jira automation with timeout and keyed concurrency --- .../create-github-issue-from-jira-branch.yml | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/.github/workflows/create-github-issue-from-jira-branch.yml b/.github/workflows/create-github-issue-from-jira-branch.yml index b1e05cbc..a1fd80a8 100644 --- a/.github/workflows/create-github-issue-from-jira-branch.yml +++ b/.github/workflows/create-github-issue-from-jira-branch.yml @@ -8,10 +8,12 @@ permissions: issues: write jobs: - create-github-issue: + extract-jira-key: if: github.event.ref_type == 'branch' runs-on: ubuntu-latest - + outputs: + jira_key: ${{ steps.extract.outputs.jira_key }} + branch_name: ${{ steps.extract.outputs.branch_name }} steps: - name: Extract Jira key from branch name id: extract @@ -24,25 +26,35 @@ jobs: else echo "No Jira key found in branch: $BRANCH_NAME" echo "jira_key=" >> "$GITHUB_OUTPUT" + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" fi - - name: Stop when branch has no Jira key - if: steps.extract.outputs.jira_key == '' - run: echo "Skip - branch name does not include Jira issue key." + create-github-issue: + needs: extract-jira-key + if: needs.extract-jira-key.outputs.jira_key != '' + runs-on: ubuntu-latest + concurrency: + group: jira-gh-issue-${{ github.repository }}-${{ needs.extract-jira-key.outputs.jira_key }} + cancel-in-progress: false + steps: - name: Fetch Jira issue summary and description - if: steps.extract.outputs.jira_key != '' id: jira env: JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + JIRA_KEY: ${{ needs.extract-jira-key.outputs.jira_key }} shell: bash run: | set -euo pipefail RESPONSE=$(curl -sS --fail \ + --connect-timeout 10 \ + --max-time 30 \ + --retry 3 \ + --retry-delay 2 \ + --retry-all-errors \ -u "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ -H "Accept: application/json" \ "${JIRA_BASE_URL}/rest/api/3/issue/${JIRA_KEY}?fields=summary,description,parent&expand=renderedFields") @@ -66,11 +78,10 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Check existing GitHub issue for Jira key - if: steps.extract.outputs.jira_key != '' id: dedup uses: actions/github-script@v7 env: - JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + JIRA_KEY: ${{ needs.extract-jira-key.outputs.jira_key }} with: script: | const jiraKey = process.env.JIRA_KEY; @@ -95,16 +106,16 @@ jobs: } - name: Create GitHub issue - if: steps.extract.outputs.jira_key != '' && steps.dedup.outputs.exists != 'true' + if: steps.dedup.outputs.exists != 'true' id: create_issue uses: actions/github-script@v7 env: JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + JIRA_KEY: ${{ needs.extract-jira-key.outputs.jira_key }} JIRA_SUMMARY: ${{ steps.jira.outputs.summary }} JIRA_PARENT_KEY: ${{ steps.jira.outputs.parent_key }} JIRA_DESCRIPTION_HTML: ${{ steps.jira.outputs.description_html }} - BRANCH_NAME: ${{ steps.extract.outputs.branch_name }} + BRANCH_NAME: ${{ needs.extract-jira-key.outputs.branch_name }} with: script: | const jiraKey = process.env.JIRA_KEY; @@ -116,18 +127,18 @@ jobs: .replace(/-+/g, '-') .replace(/^-|-$/g, ''); const recommendedBranch = `${jiraKey}-${summaryForBranch || 'task'}`; - const parentKey = process.env.JIRA_PARENT_KEY || '없음'; + const parentKey = process.env.JIRA_PARENT_KEY || 'none'; const actualBranch = process.env.BRANCH_NAME || recommendedBranch; const body = [ `JIRA_KEY: ${jiraKey}`, '', - '### ️ 상위 작업 (Ticket Number)', + '### Parent Ticket Number', parentKey, '', - '### 브랜치명 (Branch)', + '### Branch', `\`${actualBranch}\``, '', - '### 상세 내용 (Description)', + '### Description', process.env.JIRA_DESCRIPTION_HTML || '(no description)', '', ].join('\n'); @@ -142,12 +153,12 @@ jobs: core.setOutput('number', String(created.data.number)); - name: Comment GitHub issue link back to Jira - if: steps.extract.outputs.jira_key != '' && steps.dedup.outputs.exists != 'true' + if: steps.dedup.outputs.exists != 'true' env: JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_KEY: ${{ steps.extract.outputs.jira_key }} + JIRA_KEY: ${{ needs.extract-jira-key.outputs.jira_key }} GH_ISSUE_NUMBER: ${{ steps.create_issue.outputs.number }} GH_REPO: ${{ github.repository }} GH_SERVER_URL: ${{ github.server_url }} @@ -186,6 +197,11 @@ jobs: }') curl -sS --fail \ + --connect-timeout 10 \ + --max-time 30 \ + --retry 3 \ + --retry-delay 2 \ + --retry-all-errors \ -u "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \