Skip to content

Commit b3bf9c4

Browse files
sumi-0011claude
andauthored
feat: dev → main Release PR 자동 생성 워크플로우 추가 (git-goods#371)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ca72da commit b3bf9c4

3 files changed

Lines changed: 496 additions & 0 deletions

File tree

.github/workflows/release-pr.yml

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: Release PR
2+
3+
on:
4+
push:
5+
branches: [dev]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
jobs:
12+
release-pr:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Check for changes
21+
id: check
22+
run: |
23+
COMMIT_COUNT=$(git rev-list --count origin/main..origin/dev 2>/dev/null || echo "0")
24+
echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT"
25+
if [ "$COMMIT_COUNT" -eq 0 ]; then
26+
echo "No commits between main and dev. Skipping."
27+
fi
28+
29+
- name: Parse commits and generate body
30+
if: steps.check.outputs.commit_count != '0'
31+
id: generate
32+
run: |
33+
COMMITS=$(git log origin/main..origin/dev --no-merges --format="%s")
34+
35+
FEAT=""
36+
FIX=""
37+
REFACTOR=""
38+
STYLE=""
39+
DOCS=""
40+
CHORE=""
41+
42+
while IFS= read -r line; do
43+
[ -z "$line" ] && continue
44+
45+
# prefix 제거하여 메시지 추출
46+
msg=$(echo "$line" | sed 's/^[a-z]*\(([^)]*)\)\?[!]\?: *//')
47+
48+
case "$line" in
49+
feat:*|feat\(*) FEAT="${FEAT}- ${msg}"$'\n' ;;
50+
fix:*|fix\(*) FIX="${FIX}- ${msg}"$'\n' ;;
51+
refactor:*|refactor\(*) REFACTOR="${REFACTOR}- ${msg}"$'\n' ;;
52+
style:*|style\(*) STYLE="${STYLE}- ${msg}"$'\n' ;;
53+
docs:*|docs\(*) DOCS="${DOCS}- ${msg}"$'\n' ;;
54+
chore:*|chore\(*) CHORE="${CHORE}- ${msg}"$'\n' ;;
55+
*) CHORE="${CHORE}- ${line}"$'\n' ;;
56+
esac
57+
done <<< "$COMMITS"
58+
59+
# 버전 계산: 최근 RELEASE 커밋에서 현재 버전 추출
60+
CURRENT_VERSION=$(git log --all --oneline --grep="\[RELEASE\]" --format="%s" | head -1 | sed 's/.*v\([0-9]*\.[0-9]*\.[0-9]*\).*/\1/')
61+
if [ -z "$CURRENT_VERSION" ]; then
62+
CURRENT_VERSION="0.0.0"
63+
fi
64+
65+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
66+
67+
if [ -n "$FEAT" ]; then
68+
MINOR=$((MINOR + 1))
69+
PATCH=0
70+
else
71+
PATCH=$((PATCH + 1))
72+
fi
73+
74+
NEXT_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
75+
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
76+
77+
# PR 본문 생성
78+
BODY="## [RELEASE] $NEXT_VERSION"
79+
NEWLINE=$'\n'
80+
81+
if [ -n "$FEAT" ]; then
82+
BODY="${BODY}${NEWLINE}${NEWLINE}### 기능${NEWLINE}${FEAT}"
83+
fi
84+
85+
if [ -n "$FIX" ]; then
86+
BODY="${BODY}${NEWLINE}${NEWLINE}### 버그 수정${NEWLINE}${FIX}"
87+
fi
88+
89+
if [ -n "$REFACTOR" ]; then
90+
BODY="${BODY}${NEWLINE}${NEWLINE}### 리팩토링${NEWLINE}${REFACTOR}"
91+
fi
92+
93+
if [ -n "$STYLE" ]; then
94+
BODY="${BODY}${NEWLINE}${NEWLINE}### 스타일${NEWLINE}${STYLE}"
95+
fi
96+
97+
if [ -n "$DOCS" ]; then
98+
BODY="${BODY}${NEWLINE}${NEWLINE}### 문서${NEWLINE}${DOCS}"
99+
fi
100+
101+
if [ -n "$CHORE" ]; then
102+
BODY="${BODY}${NEWLINE}${NEWLINE}### 기타${NEWLINE}${CHORE}"
103+
fi
104+
105+
# multiline output
106+
{
107+
echo "body<<BODY_EOF"
108+
echo "$BODY"
109+
echo "BODY_EOF"
110+
} >> "$GITHUB_OUTPUT"
111+
112+
- name: Create or update PR
113+
if: steps.check.outputs.commit_count != '0'
114+
env:
115+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
116+
run: |
117+
NEXT_VERSION="${{ steps.generate.outputs.next_version }}"
118+
TITLE="[RELEASE] $NEXT_VERSION"
119+
120+
# 기존 열린 PR 확인
121+
EXISTING_PR=$(gh pr list --base main --head dev --state open --json number --jq '.[0].number // empty')
122+
123+
if [ -n "$EXISTING_PR" ]; then
124+
echo "Updating existing PR #$EXISTING_PR"
125+
gh pr edit "$EXISTING_PR" --title "$TITLE" --body "${{ steps.generate.outputs.body }}"
126+
else
127+
echo "Creating new PR"
128+
gh pr create --base main --head dev --title "$TITLE" --body "${{ steps.generate.outputs.body }}"
129+
fi
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Release PR 자동화 구현 계획
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** dev 브랜치에 push될 때 릴리즈 노트 형식의 dev → main PR을 자동 생성/업데이트하는 GitHub Actions 워크플로우 구현
6+
7+
**Architecture:** 단일 GitHub Actions 워크플로우 파일 내 인라인 쉘 스크립트. 커밋 로그 파싱 → 타입별 분류 → semver 계산 → gh CLI로 PR 생성/업데이트.
8+
9+
**Tech Stack:** GitHub Actions, Bash, gh CLI
10+
11+
---
12+
13+
## 파일 구조
14+
15+
- **Create:** `.github/workflows/release-pr.yml` — 워크플로우 정의 및 전체 로직
16+
17+
---
18+
19+
### Task 1: 워크플로우 기본 구조 작성
20+
21+
**Files:**
22+
- Create: `.github/workflows/release-pr.yml`
23+
24+
- [ ] **Step 1: 워크플로우 트리거 및 권한 정의**
25+
26+
```yaml
27+
name: Release PR
28+
29+
on:
30+
push:
31+
branches: [dev]
32+
33+
permissions:
34+
contents: read
35+
pull-requests: write
36+
37+
jobs:
38+
release-pr:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- name: Checkout
42+
uses: actions/checkout@v4
43+
with:
44+
fetch-depth: 0
45+
```
46+
47+
`fetch-depth: 0`은 `main..dev` 커밋 비교를 위해 전체 히스토리 필요.
48+
49+
- [ ] **Step 2: 커밋 차이 확인 step 추가**
50+
51+
checkout 이후에 추가:
52+
53+
```yaml
54+
- name: Check for changes
55+
id: check
56+
run: |
57+
COMMIT_COUNT=$(git rev-list --count origin/main..origin/dev 2>/dev/null || echo "0")
58+
echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT"
59+
if [ "$COMMIT_COUNT" -eq 0 ]; then
60+
echo "No commits between main and dev. Skipping."
61+
fi
62+
```
63+
64+
이후 모든 step에 조건 추가: `if: steps.check.outputs.commit_count != '0'`
65+
66+
- [ ] **Step 3: 커밋**
67+
68+
```bash
69+
git add .github/workflows/release-pr.yml
70+
git commit -m "feat: Release PR 자동화 워크플로우 기본 구조 추가"
71+
```
72+
73+
---
74+
75+
### Task 2: 커밋 파싱 및 분류 로직
76+
77+
**Files:**
78+
- Modify: `.github/workflows/release-pr.yml`
79+
80+
- [ ] **Step 1: 커밋 파싱 step 추가**
81+
82+
check step 이후에 추가:
83+
84+
```yaml
85+
- name: Parse commits and generate body
86+
if: steps.check.outputs.commit_count != '0'
87+
id: generate
88+
run: |
89+
# 커밋 로그 수집 (Merge 커밋 제외)
90+
COMMITS=$(git log origin/main..origin/dev --oneline --no-merges --format="%s")
91+
92+
# 타입별 분류
93+
FEAT=""
94+
FIX=""
95+
REFACTOR=""
96+
STYLE=""
97+
DOCS=""
98+
CHORE=""
99+
100+
while IFS= read -r line; do
101+
[ -z "$line" ] && continue
102+
case "$line" in
103+
feat:*|feat\(*) FEAT="$FEAT
104+
- ${line#feat: }" ;;
105+
fix:*|fix\(*) FIX="$FIX
106+
- ${line#fix: }" ;;
107+
refactor:*|refactor\(*) REFACTOR="$REFACTOR
108+
- ${line#refactor: }" ;;
109+
style:*|style\(*) STYLE="$STYLE
110+
- ${line#style: }" ;;
111+
docs:*|docs\(*) DOCS="$DOCS
112+
- ${line#docs: }" ;;
113+
*) CHORE="$CHORE
114+
- ${line#chore: }" ;;
115+
esac
116+
done <<< "$COMMITS"
117+
118+
# 버전 계산: 최근 RELEASE 커밋에서 현재 버전 추출
119+
CURRENT_VERSION=$(git log --all --oneline --grep="\[RELEASE\]" --format="%s" | head -1 | sed 's/.*v\([0-9]*\.[0-9]*\.[0-9]*\).*/\1/')
120+
if [ -z "$CURRENT_VERSION" ]; then
121+
CURRENT_VERSION="0.0.0"
122+
fi
123+
124+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
125+
126+
if [ -n "$FEAT" ]; then
127+
MINOR=$((MINOR + 1))
128+
PATCH=0
129+
else
130+
PATCH=$((PATCH + 1))
131+
fi
132+
133+
NEXT_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
134+
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
135+
136+
# PR 본문 생성
137+
BODY="## [RELEASE] $NEXT_VERSION"
138+
139+
if [ -n "$FEAT" ]; then
140+
BODY="$BODY
141+
142+
### 기능
143+
$FEAT"
144+
fi
145+
146+
if [ -n "$FIX" ]; then
147+
BODY="$BODY
148+
149+
### 버그 수정
150+
$FIX"
151+
fi
152+
153+
if [ -n "$REFACTOR" ]; then
154+
BODY="$BODY
155+
156+
### 리팩토링
157+
$REFACTOR"
158+
fi
159+
160+
if [ -n "$STYLE" ]; then
161+
BODY="$BODY
162+
163+
### 스타일
164+
$STYLE"
165+
fi
166+
167+
if [ -n "$DOCS" ]; then
168+
BODY="$BODY
169+
170+
### 문서
171+
$DOCS"
172+
fi
173+
174+
if [ -n "$CHORE" ]; then
175+
BODY="$BODY
176+
177+
### 기타
178+
$CHORE"
179+
fi
180+
181+
# multiline output
182+
{
183+
echo "body<<BODY_EOF"
184+
echo "$BODY"
185+
echo "BODY_EOF"
186+
} >> "$GITHUB_OUTPUT"
187+
```
188+
189+
- [ ] **Step 2: 커밋**
190+
191+
```bash
192+
git add .github/workflows/release-pr.yml
193+
git commit -m "feat: 커밋 파싱, 타입별 분류 및 버전 계산 로직 추가"
194+
```
195+
196+
---
197+
198+
### Task 3: PR 생성/업데이트 로직
199+
200+
**Files:**
201+
- Modify: `.github/workflows/release-pr.yml`
202+
203+
- [ ] **Step 1: PR 생성/업데이트 step 추가**
204+
205+
generate step 이후에 추가:
206+
207+
```yaml
208+
- name: Create or update PR
209+
if: steps.check.outputs.commit_count != '0'
210+
env:
211+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
212+
run: |
213+
NEXT_VERSION="${{ steps.generate.outputs.next_version }}"
214+
TITLE="[RELEASE] $NEXT_VERSION"
215+
BODY="${{ steps.generate.outputs.body }}"
216+
217+
# 기존 열린 PR 확인
218+
EXISTING_PR=$(gh pr list --base main --head dev --state open --json number --jq '.[0].number // empty')
219+
220+
if [ -n "$EXISTING_PR" ]; then
221+
echo "Updating existing PR #$EXISTING_PR"
222+
gh pr edit "$EXISTING_PR" --title "$TITLE" --body "$BODY"
223+
else
224+
echo "Creating new PR"
225+
gh pr create --base main --head dev --title "$TITLE" --body "$BODY"
226+
fi
227+
```
228+
229+
- [ ] **Step 2: 커밋**
230+
231+
```bash
232+
git add .github/workflows/release-pr.yml
233+
git commit -m "feat: PR 자동 생성/업데이트 로직 추가"
234+
```
235+
236+
---
237+
238+
### Task 4: 최종 검증 및 통합 커밋
239+
240+
**Files:**
241+
- Verify: `.github/workflows/release-pr.yml`
242+
243+
- [ ] **Step 1: YAML 문법 검증**
244+
245+
```bash
246+
# yamllint이 없으면 간단히 파싱 테스트
247+
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release-pr.yml'))" && echo "YAML valid"
248+
```
249+
250+
- [ ] **Step 2: 워크플로우 전체 파일 리뷰**
251+
252+
완성된 `.github/workflows/release-pr.yml` 전체를 읽고 다음을 확인:
253+
- 들여쓰기가 올바른지
254+
- step 간 output 참조가 일치하는지 (`steps.check.outputs.commit_count`, `steps.generate.outputs.next_version`, `steps.generate.outputs.body`)
255+
- `if` 조건이 모든 후속 step에 적용되었는지
256+
257+
- [ ] **Step 3: 최종 커밋 (필요 시)**
258+
259+
문제가 발견되어 수정한 경우:
260+
261+
```bash
262+
git add .github/workflows/release-pr.yml
263+
git commit -m "fix: Release PR 워크플로우 수정"
264+
```

0 commit comments

Comments
 (0)