ํ์ฌ Git-flow ์ ๋ต์ ์ฌ์ฉํ๊ณ ์๋ค.
feature ๋ธ๋์น์์ ์์
์ ๋๋ด๊ณ develop ๋ธ๋์น์ ๋จธ์ง๋ฅผ ํ๊ณ ๋์ develop ๋ธ๋์น๋ฅผ main ๋ธ๋์น๋ก PR์ ์์ฑํด์ผํ๋๋ฐ ์ด ๋ถ๋ถ์์ ์ ํด์ง ๊ท์น๋ ์๊ณ , ๋งค๋ฒ develop PR๊ณผ ๋น์ทํ ๋ด์ฉ์ ์์ฑํด์ ๋ฒ๊ฑฐ๋ก์๋ง ๋๋ผ๊ณ ์์๋ค.
๊ทธ๋์ main์ผ๋ก ์ฌ๋ฆฌ๋ PR์ AI์๊ฒ ๋งก๊ฒจ๋ณด๋ ๊ฒ ์ข์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ ํ๋ค.
AI๊ฐ PR์ ์์ฑํด์ฃผ๋ ํ๋ก์ฐ๋ ์๋์ ๊ฐ๋ค.
develop์ ํธ์๊ฐ ๋์ ๋ main์ ๋จธ์งํ๋ PR์ ์๋์ผ๋ก ์์ฑํด์ฃผ๋ ๋ฐฉ๋ฒ๋ ์์ง๋ง,
์ฐ๋ฆฌ ํ๋ก์ ํธ ํน์ฑ์ ์ด ๋ฐฉ๋ฒ๋ณด๋จ ๊ฐ๋ฐ์๊ฐ PR์ ์์ฑํ์ ๋ PR ๋ด์ฉ์ ์๋์ผ๋ก ์์ ํด์ฃผ๋๊ฒ ์ ์ ํ๋ค๊ณ ๋ณด์ ๊ทธ๋ ๊ฒ ์งํํ๋ค.
1. main PR ์์ฑ
2. AI๊ฐ PR ๋ด์ฉ ์์
2-1. ์ปค๋ฐ์์ Jira ํฐ์ผ ๋ฒํธ๋ฅผ ์ถ์ถํ์ฌ Jira API๋ฅผ ํตํด ์ด์ ๋ด์ฉ ๊ฐ์ ธ์ค๊ธฐ
2-2. Jira ์ด์ ๋ด์ฉ๊ณผ git diff ๋ด์ฉ์ ์ฐธ๊ณ ํ์ฌ AI์๊ฒ PR ๋ด์ฉ ์์ฒญํ๊ธฐ
๋ํ, git diff ๋ง ๋ณด์ง ์๊ณ Jira API ํธ์ถ์ ํตํด Jira ํฐ์ผ ์ด์์ ๋ด์ฉ์ ํจ๊ป ์๋ ค์ฃผ์ด ๋ด์ฉ์ ์ ํ์ฑ์ ์ฌ๋ ธ๋ค.
ํ์คํ diff ๋ง ๋ณด๋ ๊ฒ๋ณด๋จ ์ปค๋ฐ์ ๋ฐฐ๊ฒฝ์ ์๊ฒ ๋๋ ๋ด์ฉ์ด ๋ ์ข์์ง ๊ฒ์ ๋ณผ ์ ์์๋ค.
OpenAI API๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ API Key๊ฐ ํ์ํ๋ค.
๋๋ ์ด๋ฏธ API Key๊ฐ ์์ด์ ๋ ๋ฐ๊ธ๋ฐ์ง ์์๋ค.
Jira API๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ Jira์์ API Token์ ๋ฐ๊ธ๋ฐ์์ผ ํ๋ค.
๋จผ์ , ์ฐ์ธก ์๋จ์ ํ๋กํ ์ด๋ฏธ์ง๋ฅผ ๋๋ฌ Account settings๋ฅผ ๋๋ฅธ๋ค.

๊ทธ ํ, ์๋จ ํญ ์ค ๋ณด์์ ๋ค์ด๊ฐ๋ค.
ํ๋จ์ ๋ณด๋ฉด API ํ ํฐ ์น์
์ด ์๋ค. API ํ ํฐ ๋ง๋ค๊ธฐ ๋ฐ ๊ด๋ฆฌ๋ฅผ ๋๋ฅธ๋ค.

๊ทธ๋ฌ๋ฉด ์์ฑํ API ํ ํฐ ๋ชฉ๋ก์ด ๋์จ๋ค. ๋๋ ์ด๋ฏธ PR ์
๋ฐ์ดํธ์ฉ ํ ํฐ์ ๋ง๋ค์ด๋์๋ค.
์๋ค๋ฉด API ํ ํฐ ๋ง๋ค๊ธฐ๋ก ํ ํฐ์ ์์ฑํ๋ค.

๊ฐ๋จํ๊ฒ ์ด๋ฆ๊ณผ ๋ง๋ฃ ๋ ์ง๋ง ์ ํํ๋ฉด ๋๋ค.

์ด๋ ๊ฒ ๋ฐ๊ธ๋ฐ์ OpenAI API Key์ Jira API Token์ ๋ ํฌ์งํ ๋ฆฌ ํ๊ฒฝ ๋ณ์์ ๋ฃ์ด์ค๋ค.
๋ ํฌ์งํ ๋ฆฌ Settings > Secrets and variables > Actions ์์ ์ค์ ๊ฐ๋ฅํ๋ค.

๋ค์์ ์ค์ ๋ก ํน์ ํ๋์ด ๋ฐ์ํ์ ๋ ๋์ํ workflow๋ฅผ ์์ฑํ๋ค.
workflow name๊ณผ ์ธ์ ์ด workflow๋ฅผ ์คํํ ๊ฑด์ง ์ค์ ํด์ค๋ค.
PR์ด ์์ฑ๋ ๋, ์ฝ๋๊ฐ ์ถ๊ฐ๋ ๋ ์คํํ๋๋ก ์ค์ ํ๋ค.
name: AI PR Updater
# main PR์ด ์์ฑ๋ ๋(opend), ์ฝ๋๊ฐ ์ถ๊ฐ๋ ๋(synchronize) ์คํ
on:
pull_request:
types: [opened, synchronize]
branches:
- main
์ปค๋ฐ์ ๋ชจ๋ ํ์คํ ๋ฆฌ๋ฅผ ๊ฐ์ ธ์จ ํ git diff ๋ช ๋ น์ด๋ก ํ์ฌ ๋ฒ์ ๊ณผ ๊ณผ๊ฑฐ ๋ฒ์ ์ Diff๋ฅผ ์ถ์ถํ์ฌ pr_diff.txt ํ์ผ๋ก ์ ์ฅํ๋ค.
jobs:
update-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Diff
id: get_diff
run: |
git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > pr_diff.txt
์ฐ๋ฆฌ ํ๋ก์ ํธ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ Poetry๋ฅผ ํตํด ๊ด๋ฆฌํ๊ณ ์๊ธฐ ๋๋ฌธ์(openai ํฌํจ) poetry ์ค์น ํ install ๋ฐ๋๋ค.
- name: Install Poetry
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12.1'
cache: 'poetry'
- name: Install dependencies
run: poetry install --no-interaction --no-root
AI ์ฌ์ฉ์ ํ์ํ ์ ๋ณด๋ค์ env๋ก ์ค์ ํ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋ค. (์คํฌ๋ฆฝํธ ๋ด์ฉ์ ์๋ ๋์จ๋ค.)
๊ทธ ํ, AI ์๋ต๊ฐ์ผ๋ก PR ๋ด์ฉ์ ์์ ํด์ค๋ค.
PR ๋ด์ฉ์ ์์ ํ๊ธฐ ์ํ ๊ถํ๋ ๋ถ์ฌํด์ค๋ค.
# **AI๋ฅผ ํตํด PR ๋ด์ฉ ์์ฑ (์คํฌ๋ฆฝํธ์์ ์ฌ์ฉํ ํ๊ฒฝ๋ณ์๋ฅผ env๋ก ์ค์ )**
- name: Generate AI Summary
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_EMAIL_ADDRESS: ${{ secrets.JIRA_EMAIL_ADDRESS }}
run: |
poetry run python .github/scripts/ai_summary.py
# PR ๋ด์ฉ ์์ (gh pr edit ...)
- name: Update PR Description
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr edit ${{ github.event.pull_request.number }} --body-file ai_summary.txt
# ์ฐ๊ธฐ ๊ถํ ๋ถ์ฌ
permissions:
contents: read
pull-requests: write
์ต์ข ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
name: AI PR Updater
# main PR์ด ์์ฑ๋ ๋(opend), ์ฝ๋๊ฐ ์ถ๊ฐ๋ ๋(synchronize) ์คํ
on:
pull_request:
types: [opened, synchronize]
branches:
- main
jobs:
update-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
# ํ์ฌ ๋ฒ์ ๊ณผ ๊ณผ๊ฑฐ ๋ฒ์ ์ Diff๋ฅผ ์ถ์ถํ์ฌ *pr_diff.txt* ํ์ผ๋ก ์ ์ฅ
- name: Get Diff
id: get_diff
run: |
git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > pr_diff.txt
# openai ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ poetry๋ก ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ poetry ์ค์น
- name: Install Poetry
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12.1'
cache: 'poetry'
- name: Install dependencies
run: poetry install --no-interaction --no-root
# **AI๋ฅผ ํตํด PR ๋ด์ฉ ์์ฑ (์คํฌ๋ฆฝํธ์์ ์ฌ์ฉํ ํ๊ฒฝ๋ณ์๋ฅผ env๋ก ์ค์ )**
- name: Generate AI Summary
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_EMAIL_ADDRESS: ${{ secrets.JIRA_EMAIL_ADDRESS }}
run: |
poetry run python .github/scripts/ai_summary.py
# PR ๋ด์ฉ ์์ (gh pr edit ...)
- name: Update PR Description
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr edit ${{ github.event.pull_request.number }} --body-file ai_summary.txt
# ์ฐ๊ธฐ ๊ถํ ๋ถ์ฌ
permissions:
contents: read
pull-requests: write
Generate AI Summary ๋จ๊ณ์์ ์ธ๊ธํ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ ์ฐจ๋ก๋ค.
์ฐ๋ฆฌ๋ ์ปค๋ฐ๋ฉ์์ง ๋งจ ์์ prefix๋ก Jira ํฐ์ผ ๋ฒํธ๋ฅผ ๋ถ์ด๋ ๊ท์น์ด ์๋ค.
์ด๋ฅผ ์ด์ฉํด์ ์ปค๋ฐ๋ฉ์์ง์ Jira ํฐ์ผ ๋ฒํธ๋ฅผ ์ถ์ถํ๊ณ , ํด๋น ๋ฒํธ๋ก Jira API๋ฅผ ํธ์ถํ์ฌ ์ด์ ๋ด์ฉ์ ๊ฐ์ ธ์, ํด๋น ์ด์ ๋ด์ฉ๊ณผ Diff ๊ธฐ๋ฐ์ผ๋ก AI๊ฐ PR ๋ด์ฉ์ ์์ฑํด์ฃผ๋๋ก ์์ฑํ๋ค.
์ต์ข ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
main ํจ์
import os
import re
import subprocess
from openai import OpenAI
import sys
import requests
from requests.auth import HTTPBasicAuth
DIFF_FILE = 'pr_diff.txt'
MODEL_NAME = 'gpt-4-turbo'
SUMMARY_FILE = 'ai_summary.txt'
JIRA_BASE_URL = 'base_url'
DEFAULT_MESSAGE = "No text"
def generate_pr_summary():
# ์ง๋ผ ํฐ์ผ ๋ฒํธ ์ถ์ถ ํ ์ด์ ๋ด์ฉ์ ๊ฐ์ ธ์จ๋ค
ticket_id = get_jira_ticket_id()
jira_description, jira_summary = DEFAULT_MESSAGE, DEFAULT_MESSAGE
if ticket_id:
jira_description, jira_summary = get_jira_issue_details(ticket_id=ticket_id)
# yml ํ์ผ์์ ์ค์ ํด๋ ํ๊ฒฝ๋ณ์ ๊ฐ์ ๊ฐ์ ธ์จ๋ค
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
print("Error: OPENAI_API_KEY is not set.")
sys.exit(1)
client = OpenAI(api_key=api_key)
# diff ํ์ผ ์ฝ๊ธฐ
try:
with open(DIFF_FILE, "r", encoding="utf-8") as f:
diff_text = f.read()
except FileNotFoundError:
print("Error: pr_diff.txt not found.")
sys.exit(1)
# ํ๋กฌํํธ๋ฅผ ์ค์ ํด์ค๋ค.
# ์์ด๋ก๋ง ์์ฑํ๋๋ ๊ฐ๋์ฉ ์์ด๋ก PR ๋ด์ฉ์ ์ ์ด์ฃผ๋ ์ด์๊ฐ ์์ด์ ์์ด์ ํ๊ตญ์ด๋ฅผ ๊ฐ์ด ์ฌ์ฉํ๋ค.
system_prompt = ("You are a professional Senior Software Engineer."
"You must communicate only in Korean and use Markdown for formatting.")
user_prompt = f"""
Analyze the provided Jira ticket information and Git Diff to write a concise GitHub Pull Request (PR) description.
[Context Data]
- Jira Ticket ID: {ticket_id}
- Jira Summary: {jira_summary}
- Jira Description: {jira_description}
- Git Diff:
{diff_text}
[Response Guidelines - ๋ฐ๋์ ํ๊ตญ์ด๋ก ์์ฑํ๋ฉฐ ๋งํฌ๋ค์ด ํ์์ ์ฌ์ฉํ์ธ์]
1. **Jira ์์
์์ฝ**: ์ ๊ณต๋ Jira ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ด ํฐ์ผ์ ์์
๋ด์ฉ์ ํต์ฌ ์์ฃผ๋ก ์์ฝํฉ๋๋ค.
- ์ง๋ผ ์ ๋ณด๊ฐ ๋ถ์กฑํ๋ฉด Git Diff๋ฅผ ๋ถ์ํ์ฌ ํด๋น ์์
์ด ์ ํ์ํ์ง ์ ์ถํด์ ์์ฑํ์ธ์.
2. **PR Overview**: ๋ณ๊ฒฝ ์ฌํญ์ ํต์ฌ(What)์ ํ ๋ฌธ์ฅ์ผ๋ก ์์ฝํฉ๋๋ค.
3. **Detailed Changes**: ๊ฐ ํ์ผ๋ณ ๋ณ๊ฒฝ์ ์ **๋งํฌ๋ค์ด ๋ชฉ๋กํ(Bullet points)**์ผ๋ก ์์ฑํฉ๋๋ค.
- ์ค์ํ ๋ก์ง ๋ณํ๋ฅผ ์ฐ์ ์ค๋ช
ํ์ธ์.
- **GROUP MINOR CHANGES**: ์ฌ์ํ ์ค์ ์ด๋ ์์กด์ฑ ๋ณ๊ฒฝ์ ํ ์ค์ ๋ชฉ๋ก์ผ๋ก ๋ฌถ์ด์ ์์ฝํ์ธ์.
- ํ์: `1. ํ์ผ๋ช
: ๋ณ๊ฒฝ ์ฌํญ ์ค๋ช
`
4. **Reviewer's Notes**: ๋ฆฌ๋ทฐ์ด๊ฐ ์ฃผ์ ๊น๊ฒ ํ์ธํด์ผ ํ ์ฌํญ์ ๊ธฐ์ ํฉ๋๋ค.
[Output Style & Rules]
- **Language**: MUST be Korean (ํ๊ตญ์ด).
- **Format**: Use clear Markdown (Headers, Bullets, Bold text, `Code Backticks`).
- **Visual**: **๊ฐ ์น์
์ ๋ชฉ ์์ ์ ์ ํ ์ด๋ชจํฐ์ฝ์ ์ถ๊ฐํ์ฌ ๊ฐ๋
์ฑ์ ๋์ด์ธ์.**
- **Tone**: Professional and formal.
- No introductory phrases. Start with the content immediately.
"""
# Create summary
try:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.2,
)
summary = response.choices[0].message.content
# response๋ก ๋ฐ์ ๊ฒฐ๊ณผ๋ฅผ ai_summary.txtํ์ผ๋ก ์ ์ฅํ๋ค.
with open(SUMMARY_FILE, "w", encoding="utf-8") as f:
f.write("## ๐ค AI PR ๋ถ์ ๊ฒฐ๊ณผ\n\n")
f.write(summary)
f.write("\n\n---\n*์ด ์์ฝ์ ChatGPT์ ์ํด ์๋ ์์ฑ๋์์ต๋๋ค.*")
except Exception as e:
print(f"Error during AI generation: {e}")
sys.exit(1)
if __name__ == "__main__":
generate_pr_summary()
Jira ํฐ์ผ ๋ฒํธ ์ถ์ถ ํ ์ด์ ๋ด์ฉ์ ๊ฐ์ ธ์ค๋ ํจ์
def get_jira_issue_details(ticket_id):
api_token = os.getenv("JIRA_API_TOKEN")
email_address = os.getenv("JIRA_EMAIL_ADDRESS")
if not api_token or not email_address:
print("Error: JIRA_API_TOKEN or JIRA_EMAIL_ADDRESS is not set.")
return DEFAULT_MESSAGE, DEFAULT_MESSAGE
try:
url = f"{JIRA_BASE_URL}/rest/api/2/issue/{ticket_id}"
auth = HTTPBasicAuth(email_address, api_token)
response = requests.get(url, auth=auth)
if response.status_code != 200:
print(f"Error: Failed to get Jira issue {ticket_id}. Status: {response.status_code}")
return DEFAULT_MESSAGE, DEFAULT_MESSAGE
data = response.json()
description = str(data['fields'].get('description', DEFAULT_MESSAGE))[:1000]
summary = data['fields'].get('summary', DEFAULT_MESSAGE)
return description, summary
except Exception:
return DEFAULT_MESSAGE, DEFAULT_MESSAGE
def get_jira_ticket_id():
try:
# develop ๋ธ๋์น์ ๋ง์ง๋ง ์ปค๋ฐ์ PR ๋จธ์งํ ์ปค๋ฐ์ด๊ธฐ ๋๋ฌธ์ ํ๋ ์คํตํ๊ณ ๊ทธ ๋ค์ ์ปค๋ฐ์ ๊ฐ์ ธ์จ๋ค
commit_msg = subprocess.check_output(["git", "log", "-1", "--skip=1", "--pretty=%B"]).decode()
match = re.search(r"\[([A-Z]+-\d+)\]", commit_msg)
return match.group(1) if match else None
except Exception:
return None
main์ PR์ ์์ฑํด๋ณธ ๊ฒฐ๊ณผ, ๊ฐ๋
์ฑ ์๊ณ ๊ฐ๋จํ๊ฒ PR ๋ด์ฉ์ ์์ฝํด์ฃผ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
(ํ๋กฌํํธ ๊ฐ์ ๋ฐ ์ปค๋ฐ ๊ฐ์ ธ์ค๋ ๋ก์ง ์์ ์ ํ ์์
์ ๋ํด ChatGPT๊ฐ ์จ์ค PR ๋ด์ฉ์ด๋ค.)

AI๋ฅผ ํตํด ๋ฒ๊ฑฐ๋ก์ด ์์
์ ์๋ํ๋ก ์ ํํ๋ ์ข์ ๊ฒฝํ์ด์๋ค.
์๋ํํ ์ ์๋ ๋ค๋ฅธ ๋ถ๋ถ์ด ๋ ์์์ง ์๊ฐํด๋ณด์์ผ๊ฒ ๋ค.
ํด๋ก๋๋ก ๋ญ ๋ ํ ์ ์์์ง ๊ณ ๋ฏผ์ด ๋๋ค๋ฉด
claude-code-achievements๋ฅผ ์ฌ์ฉํด๋ณด์์.