Skip to content

🚀 Deploy 20260111-v0.1.1 #4

🚀 Deploy 20260111-v0.1.1

🚀 Deploy 20260111-v0.1.1 #4

# ===================================================================
# 범용 프로젝트 자동 체인지로그 관리 워크플로우
# ===================================================================
#
# 이 워크플로우는 deploy 브랜치로 PR이 생성될 때 CodeRabbit AI의 리뷰를
# 자동으로 감지하고 파싱하여 CHANGELOG.json과 CHANGELOG.md를 업데이트합니다.
#
# 작동 방식:
# 1. deploy 브랜치로 PR 생성 시 트리거
# 2. PR 제목을 즉시 🚀 Deploy 형식으로 변경
# 3. CodeRabbit Summary 요청 후 최대 10분 대기
# 4. Summary 내용을 파싱하여 CHANGELOG 파일들 업데이트
# 5. PR 자동 머지 후 배포 트리거
#
# API Rate Limit 참고:
# - GITHUB_TOKEN: 1,000 요청/시간/레포 (Actions 내)
# - PAT: 5,000 요청/시간/계정
# - 이 워크플로우 예상 사용량: ~130회/실행
#
# ===================================================================
name: AUTO UPDATE PROJECT CHANGELOG
on:
pull_request_target:
types: [opened]
branches: ["deploy"]
permissions:
contents: write
pull-requests: write
jobs:
# Job 1: CodeRabbit Summary 감지 및 파싱
detect-and-parse:
name: CodeRabbit Summary 감지 및 파싱
runs-on: ubuntu-latest
outputs:
summary_found: ${{ steps.detect_summary.outputs.summary_found }}
version: ${{ steps.get_version.outputs.version }}
project_type: ${{ steps.get_version.outputs.project_type }}
steps:
- name: PR 본문 초기화
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
echo "🗑️ PR #$PR_NUMBER 본문 초기화 중..."
curl -s -H "Authorization: token ${{ secrets._GITHUB_PAT_TOKEN }}" \
-H "Content-Type: application/json" \
-X PATCH \
-d '{"body": ""}' \
"https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}"
echo "✅ PR 본문 초기화 완료"
- name: 저장소 체크아웃
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.event.repository.default_branch || 'main' }}
- name: 버전 스크립트 권한 설정
run: |
if [ -f ".github/scripts/version_manager.sh" ]; then
chmod +x .github/scripts/version_manager.sh
echo "✅ 버전 스크립트 권한 설정 완료"
fi
- name: 버전 정보 확인
id: get_version
run: |
VERSION="1.0.0"
PROJECT_TYPE="unknown"
# 버전 스크립트 사용
if [ -f ".github/scripts/version_manager.sh" ]; then
VERSION=$(./.github/scripts/version_manager.sh get | tail -n 1)
elif [ -f "version.yml" ]; then
VERSION=$(grep "^version:" version.yml | sed 's/version: *"\([^"]*\)".*/\1/')
# 폴백: 프로젝트 타입별 버전 파일
elif [ -f "build.gradle" ]; then
VERSION=$(grep "version = '" build.gradle | sed "s/version = '//" | sed "s/'//")
PROJECT_TYPE="spring"
elif [ -f "pubspec.yaml" ]; then
VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: *\([0-9.]*\).*/\1/')
PROJECT_TYPE="flutter"
elif [ -f "package.json" ]; then
VERSION=$(jq -r '.version' package.json)
PROJECT_TYPE="node"
elif [ -f "pyproject.toml" ]; then
VERSION=$(grep "^version = " pyproject.toml | sed 's/version = *"\([^"]*\)".*/\1/')
PROJECT_TYPE="python"
fi
# version.yml에서 프로젝트 타입 확인
if [ -f "version.yml" ] && [ "$PROJECT_TYPE" = "unknown" ]; then
PROJECT_TYPE=$(grep "^project_type:" version.yml | sed 's/project_type: *"\([^"]*\)".*/\1/' || echo "unknown")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "project_type=$PROJECT_TYPE" >> $GITHUB_OUTPUT
echo "✅ 버전: $VERSION, 프로젝트 타입: $PROJECT_TYPE"
- name: PR 제목 즉시 변경
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
VERSION="${{ steps.get_version.outputs.version }}"
TODAY=$(date '+%Y%m%d')
NEW_TITLE="🚀 Deploy ${TODAY}-v${VERSION}"
curl -s -H "Authorization: token ${{ secrets._GITHUB_PAT_TOKEN }}" \
-H "Content-Type: application/json" \
-X PATCH \
-d "{\"title\": \"${NEW_TITLE}\"}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}"
echo "✅ PR 제목 변경 완료: $NEW_TITLE"
- name: CodeRabbit Summary 요청
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
curl -s -H "Authorization: token ${{ secrets._GITHUB_PAT_TOKEN }}" \
-H "Content-Type: application/json" \
-X POST \
-d '{"body": "@coderabbitai summary"}' \
"https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments"
echo "✅ CodeRabbit Summary 요청 완료"
- name: CodeRabbit Summary 감지 (폴링)
id: detect_summary
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
MAX_ATTEMPTS=120 # 10분 = 120 * 5초
ATTEMPT=0
echo "🔍 PR #$PR_NUMBER에서 CodeRabbit Summary 감지 시작..."
echo "⏰ 최대 대기 시간: 10분 (5초마다 체크)"
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
ATTEMPT=$((ATTEMPT + 1))
echo "[$ATTEMPT/$MAX_ATTEMPTS] Summary 확인 중... ($(date '+%H:%M:%S'))"
# JSON API로 PR body 가져오기 (Public/Private 동일하게 동작)
PR_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
jq -r '.body // ""')
# 본문이 비어있는지 확인
if [ -z "$PR_BODY" ] || [ "$PR_BODY" = "null" ]; then
echo "⏳ PR 본문이 비어있습니다"
elif echo "$PR_BODY" | grep -q "Summary by CodeRabbit"; then
echo "✅ CodeRabbit Summary 발견!"
echo "$PR_BODY" > pr_body.md
echo "summary_found=true" >> $GITHUB_OUTPUT
break
else
echo "⏳ CodeRabbit Summary 아직 없음"
fi
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
sleep 5
fi
done
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
echo "⚠️ 10분 대기 후에도 Summary를 찾을 수 없습니다"
echo "summary_found=false" >> $GITHUB_OUTPUT
fi
- name: Summary 파일 업로드
if: steps.detect_summary.outputs.summary_found == 'true'
uses: actions/upload-artifact@v4
with:
name: pr-content
path: pr_body.md
retention-days: 1
# Job 2: CHANGELOG 업데이트
update-changelog:
name: CHANGELOG 업데이트
runs-on: ubuntu-latest
needs: detect-and-parse
if: needs.detect-and-parse.outputs.summary_found == 'true'
steps:
- name: 저장소 체크아웃
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.event.repository.default_branch || 'main' }}
- name: Git 설정
run: |
DEFAULT_BRANCH="${{ github.event.repository.default_branch || 'main' }}"
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git pull origin $DEFAULT_BRANCH
- name: Summary 파일 다운로드
uses: actions/download-artifact@v4
with:
name: pr-content
- name: CHANGELOG 업데이트
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
VERSION="${{ needs.detect-and-parse.outputs.version }}"
PROJECT_TYPE="${{ needs.detect-and-parse.outputs.project_type }}"
TODAY=$(date '+%Y-%m-%d')
TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%SZ')
echo "📝 CHANGELOG 업데이트 시작..."
echo "📋 프로젝트: $PROJECT_TYPE v$VERSION"
# Python 스크립트 실행 권한 설정
if [ -f ".github/scripts/changelog_manager.py" ]; then
chmod +x .github/scripts/changelog_manager.py
else
echo "❌ changelog_manager.py를 찾을 수 없습니다"
exit 1
fi
# 환경 변수 설정 후 Python 스크립트 실행
export VERSION="$VERSION"
export PROJECT_TYPE="$PROJECT_TYPE"
export TODAY="$TODAY"
export PR_NUMBER="$PR_NUMBER"
export TIMESTAMP="$TIMESTAMP"
python3 .github/scripts/changelog_manager.py update-from-summary
echo "📄 CHANGELOG.md 재생성 중..."
python3 .github/scripts/changelog_manager.py generate-md
- name: 변경사항 커밋 및 푸시
run: |
DEFAULT_BRANCH="${{ github.event.repository.default_branch || 'main' }}"
git add CHANGELOG.json CHANGELOG.md
if git diff --staged --quiet; then
echo "📝 변경사항이 없습니다"
else
REPO_NAME=$(basename "${{ github.repository }}")
VERSION="${{ needs.detect-and-parse.outputs.version }}"
git commit -m "$REPO_NAME 버전 관리 : docs : v$VERSION 릴리즈 문서 업데이트 (PR #${{ github.event.pull_request.number }})"
# Race Condition 방지: pull-rebase 후 push (최대 3회 재시도)
MAX_RETRIES=3
RETRY_COUNT=0
PUSH_SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "🔄 Push 시도 $RETRY_COUNT/$MAX_RETRIES..."
if git push origin HEAD:$DEFAULT_BRANCH; then
PUSH_SUCCESS=true
echo "✅ 변경사항 커밋 완료"
break
else
echo "⚠️ Push 실패, remote 변경사항 동기화 중..."
if git pull --rebase origin $DEFAULT_BRANCH; then
echo "✅ Rebase 성공, 다시 push 시도..."
else
echo "❌ Rebase 실패, 충돌 해결 필요"
git rebase --abort 2>/dev/null || true
exit 1
fi
fi
done
if [ "$PUSH_SUCCESS" = false ]; then
echo "❌ $MAX_RETRIES회 시도 후에도 push 실패"
exit 1
fi
fi
- name: 임시 파일 정리
run: rm -f pr_body.md
# Job 3: PR 머지 및 배포 트리거
merge-and-deploy:
name: PR 머지 및 배포 트리거
runs-on: ubuntu-latest
needs: [detect-and-parse, update-changelog]
if: needs.detect-and-parse.outputs.summary_found == 'true'
steps:
- name: 저장소 체크아웃
uses: actions/checkout@v4
with:
token: ${{ secrets._GITHUB_PAT_TOKEN }}
fetch-depth: 0
- name: Git 설정
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: PR 브랜치 최신화
env:
GH_TOKEN: ${{ secrets._GITHUB_PAT_TOKEN }}
run: |
PR_HEAD=$(gh pr view ${{ github.event.pull_request.number }} --json headRefName -q .headRefName)
PR_BASE=$(gh pr view ${{ github.event.pull_request.number }} --json baseRefName -q .baseRefName)
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 PR 정보:"
echo " • HEAD 브랜치: $PR_HEAD"
echo " • BASE 브랜치: $PR_BASE"
echo " • 목표: $PR_BASE 브랜치에 $PR_HEAD의 변경사항을 머지"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 원격 브랜치 최신화
echo "🔄 원격 브랜치 최신화 중..."
git fetch origin $PR_HEAD
git fetch origin $PR_BASE
# BASE 브랜치(deploy)로 체크아웃
echo "📦 $PR_BASE 브랜치로 체크아웃 중..."
git checkout $PR_BASE
git pull origin $PR_BASE
# HEAD 브랜치(main)의 최신 변경사항 다시 가져오기 (race condition 방지)
echo "🔄 $PR_HEAD 브랜치의 최신 변경사항 가져오는 중..."
git fetch origin $PR_HEAD
# HEAD 브랜치(main)의 변경사항을 BASE(deploy)에 머지
echo "🔀 $PR_HEAD의 변경사항을 $PR_BASE에 머지 중..."
if git merge --no-edit origin/$PR_HEAD; then
echo "✅ 머지 성공"
else
echo "⚠️ 머지 충돌 발생"
echo "충돌 파일:"
git diff --name-only --diff-filter=U || true
echo "❌ 머지 충돌로 인해 실패했습니다. 수동으로 해결 후 다시 시도하세요."
exit 1
fi
# deploy 브랜치에 푸시 (다른 워크플로우 트리거)
# Race Condition 방지: pull-rebase 후 push (최대 3회 재시도)
echo "🚀 $PR_BASE 브랜치에 푸시 중..."
MAX_RETRIES=3
RETRY_COUNT=0
PUSH_SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "🔄 Push 시도 $RETRY_COUNT/$MAX_RETRIES..."
if git push origin $PR_BASE; then
PUSH_SUCCESS=true
echo "✅ $PR_BASE 브랜치에 푸시 완료 (배포 워크플로우 트리거됨)"
break
else
echo "⚠️ Push 실패, remote 변경사항 동기화 중..."
if git pull --rebase origin $PR_BASE; then
echo "✅ Rebase 성공, 다시 push 시도..."
else
echo "❌ Rebase 실패, 충돌 해결 필요"
git rebase --abort 2>/dev/null || true
exit 1
fi
fi
done
if [ "$PUSH_SUCCESS" = false ]; then
echo "❌ $MAX_RETRIES회 시도 후에도 push 실패"
exit 1
fi
- name: 자동 PR Merge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
# PAT 토큰 존재 여부 확인
if [ -z "${{ secrets._GITHUB_PAT_TOKEN }}" ]; then
PAT_AVAILABLE="false"
else
PAT_AVAILABLE="true"
fi
# PR 상태 확인
PR_STATE=$(gh pr view $PR_NUMBER --json state,mergeable -q '.state + "," + (.mergeable | tostring)')
PR_STATE_VALUE=$(echo "$PR_STATE" | cut -d',' -f1)
PR_MERGEABLE=$(echo "$PR_STATE" | cut -d',' -f2)
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 PR 상태 확인:"
echo " • 상태: $PR_STATE_VALUE"
echo " • 병합 가능: $PR_MERGEABLE"
echo " • PAT 토큰 사용 가능: $PAT_AVAILABLE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ "$PR_STATE_VALUE" = "CLOSED" ] || [ "$PR_STATE_VALUE" = "MERGED" ]; then
echo "✅ PR이 이미 닫혔거나 병합되었습니다"
exit 0
fi
if [ "$PR_MERGEABLE" != "true" ]; then
echo "⚠️ PR을 GitHub API로 병합할 수 없습니다"
echo "💡 이미 'PR 브랜치 최신화' 단계에서 변경사항이 deploy 브랜치에 적용되었습니다"
echo "📝 PR 상태를 업데이트합니다..."
# PR에 댓글 추가
export GH_TOKEN=${{ secrets.GITHUB_TOKEN }}
gh pr comment $PR_NUMBER --body "✅ 변경사항이 자동으로 deploy 브랜치에 적용되었습니다. PR을 수동으로 닫아주세요."
echo "✅ PR 상태 업데이트 완료"
exit 0
fi
# 머지 성공 여부 추적 변수
MERGE_SUCCESS="false"
# 1차 시도: GITHUB_TOKEN으로 머지 시도 (--admin 없이)
echo ""
echo "🔀 1차 시도: GITHUB_TOKEN으로 PR 병합 중..."
export GH_TOKEN=${{ secrets.GITHUB_TOKEN }}
MERGE_OUTPUT=$(gh pr merge $PR_NUMBER --merge --delete-branch=false 2>&1)
MERGE_EXIT_CODE=$?
if [ $MERGE_EXIT_CODE -eq 0 ] && echo "$MERGE_OUTPUT" | grep -q "merged\|Merged"; then
echo "✅ GITHUB_TOKEN으로 PR 병합 성공"
MERGE_SUCCESS="true"
else
echo "⚠️ GITHUB_TOKEN으로 병합 실패"
echo " 출력: $MERGE_OUTPUT"
# PR 상태 재확인 (다른 프로세스가 머지했을 수 있음)
PR_STATE_CHECK=$(gh pr view $PR_NUMBER --json state -q .state)
if [ "$PR_STATE_CHECK" = "MERGED" ]; then
echo "✅ PR이 이미 머지되었습니다"
MERGE_SUCCESS="true"
fi
fi
# 2차 시도: PAT 토큰으로 재시도 (--admin 플래그 사용)
if [ "$MERGE_SUCCESS" = "false" ] && [ "$PAT_AVAILABLE" = "true" ]; then
echo ""
echo "🔀 2차 시도: PAT 토큰으로 PR 병합 중 (--admin 플래그 사용)..."
export GH_TOKEN=${{ secrets._GITHUB_PAT_TOKEN }}
MERGE_OUTPUT=$(gh pr merge $PR_NUMBER --merge --admin --delete-branch=false 2>&1)
MERGE_EXIT_CODE=$?
if [ $MERGE_EXIT_CODE -eq 0 ] && echo "$MERGE_OUTPUT" | grep -q "merged\|Merged"; then
echo "✅ PAT 토큰으로 PR 병합 성공"
MERGE_SUCCESS="true"
else
echo "❌ PAT 토큰으로도 병합 실패"
echo " 출력: $MERGE_OUTPUT"
# PR 상태 재확인
PR_STATE_CHECK=$(gh pr view $PR_NUMBER --json state -q .state)
if [ "$PR_STATE_CHECK" = "MERGED" ]; then
echo "✅ PR이 이미 머지되었습니다"
MERGE_SUCCESS="true"
fi
fi
fi
# 최종 실패 처리
if [ "$MERGE_SUCCESS" = "false" ]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "❌ PR 자동 병합 실패"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ "$PAT_AVAILABLE" = "false" ]; then
echo ""
echo "⚠️ PAT 토큰이 설정되어 있지 않습니다."
echo ""
echo "📋 해결 방법:"
echo "1. GitHub에서 Personal Access Token 생성"
echo " - https://github.com/settings/tokens"
echo " - 권한: repo (전체 권한)"
echo ""
echo "2. Repository Secrets에 등록"
echo " - Settings > Secrets and variables > Actions"
echo " - New repository secret"
echo " - Name: _GITHUB_PAT_TOKEN"
echo " - Value: 생성한 토큰"
echo ""
echo "또는"
echo ""
echo "3. Branch Protection Rules 조정"
echo " - Settings > Branches > Branch protection rules"
echo " - 'Allow specified actors to bypass required pull requests'"
echo " - GitHub Actions bot을 예외로 추가"
echo ""
else
echo ""
echo "⚠️ GITHUB_TOKEN과 PAT 토큰 모두로 병합에 실패했습니다."
echo ""
echo "📋 가능한 원인:"
echo "1. Branch Protection Rules가 너무 엄격함"
echo "2. PR에 충돌이 있음"
echo "3. 필수 상태 체크가 통과하지 않음"
echo "4. 필수 리뷰 승인이 없음"
echo ""
fi
# PR에 댓글 추가
export GH_TOKEN=${{ secrets.GITHUB_TOKEN }}
ERROR_COMMENT="❌ 자동 병합 실패. 수동으로 병합해주세요."
if [ "$PAT_AVAILABLE" = "false" ]; then
ERROR_COMMENT="❌ 자동 병합 실패: PAT 토큰이 설정되어 있지 않습니다. Settings > Secrets에 \`_GITHUB_PAT_TOKEN\`을 등록하거나 Branch Protection Rules에서 GitHub Actions bot을 예외로 추가해주세요."
fi
gh pr comment $PR_NUMBER --body "$ERROR_COMMENT"
exit 1
fi
echo ""
echo "✅ PR 자동 병합 완료"
- name: 배포 완료 알림
run: |
echo "🎉 체인지로그 업데이트 및 배포 트리거 완료!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 배포 정보:"
echo " • 버전: ${{ needs.detect-and-parse.outputs.version }}"
echo " • 프로젝트: ${{ needs.detect-and-parse.outputs.project_type }}"
echo " • PR: #${{ github.event.pull_request.number }}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"