1. Introduce
새로운 기능을 개발할 때마다 반복되는 작업 중 하나가 바로 PR 본문 작성이다. PR 본문은 단순한 형식 문서가 아니라, 리뷰어가 변경 의도를 이해하고 코드 리뷰를 진행하기 위한 필수 커뮤니케이션 수단이다. 그럼에도 불구하고 실제 개발 과정에서는 기능 구현 이후 다시 변경 사항을 정리하고 문장으로 풀어내는 데 생각보다 많은 시간이 소요된다.
특히 변경 범위가 넓거나 여러 파일에 걸쳐 수정이 발생한 경우, PR 본문을 작성하기 위해 다시 diff를 훑고, 핵심 내용을 정리하는 작업은 생각보다 되게 귀찮은 작업이다. 결과적으로 PR 본문은 “최소한의 설명만 있는 상태”로 올라가거나, 리뷰어가 직접 코드를 열어보며 맥락을 추론해야 하는 상황이 반복된다.
최근에는 AI 활용이 보편화되면서, 이러한 불필요한 작업들을 자동화할 수 있지 않을까? 라는 고민이 자연스럽게 들었다. 이미 코드 변경 사항은 Git에 모두 기록되어 있고, PR을 생성하는 시점에는 변경 내역이 명확히 존재한다. 그렇다면 개발자가 다시 한 번 이를 정리하여 PR 본문으로 옮겨 적는 작업은, 충분히 AI에게 위임할 수 있는 영역이 아닐까?
해당 글에서는 이러한 문제 의식에서 출발하여 PR 생성 시점에 코드 변경 사항을 자동으로 분석하고, 일관된 형식의 PR 본문을 자동으로 생성하는 파이프라인을 어떻게 구현했는지 정리해보고자 한다.
27th-App-Team-1-BE/.github at develop · YAPP-Github/27th-App-Team-1-BE
Contribute to YAPP-Github/27th-App-Team-1-BE development by creating an account on GitHub.
github.com
모든 코드는 위에서 확인할 수 있습니다.
2. How It Works
📌 Github Actions
PR 본문 자동화를 설계하면서 가장 먼저 고민했던 것은 "이 흐름을 어디에서, 어떻게 실행할 것인가?" 였다.
이미 로컬 환경에서는 Claude Code나 MCP를 활용해 PR 본문을 생성할 수 있었지만, 이를 팀 단위로 확장하려면 몇가지 조건이 필요했다.
1. 팀원의 개인 환경에 의존하지 않을 것
2. 별도의 서버나 배포가 필요없을 것
3. 무료로 사용 가능한 것
4. 실행 여부를 명확히 제어할 수 있을 것
Github Actions는 이러한 조건들을 만족하였다. 거기다 Github와 자연스럽게 통합이 가능하기에 이를 이용하여 코드 변경사항을 트리거 하고, PR 작성을 자동화하는 파이프라인을 구축해보자.
📌 Trigger
1. Push Trigger
가장 단순한 방식은 push 이벤트를 기준으로 워크플로우를 실행하는 것이다.
브랜치에 커밋이 push될 때마다 GitHub Actions가 실행되고, 변경된 diff를 기반으로 AI를 호출해 PR 본문을 생성하거나 업데이트한다.
이 방식의 장점은 항상 최신 변경 사항이 PR 본문에 반영된다는 점이다.
하지만 push 빈도만큼 AI 호출이 발생하게 되고 이는 곧 API 비용 부담으로 이어진다. 팀 단위로 운영할 경우, 이 방식은 자동화의 편의성보다 비용 리스크가 더 크게 작용할 수 있다.
2. Draft PR Trigger
두 번째로 고려한 방식은 최초 push 시 draft PR을 자동 생성하고, 개발자가 Ready to review로 전환하는 시점에 다시 AI를 호출해 최종 PR 본문을 생성하는 구조다. 이 방식은 AI 호출 횟수를 최대 두 번으로 제한할 수 있다는 점에서, push 기반 방식의 비용 문제를 어느 정도 개선한다.
하지만 이 접근에는 또 다른 문제가 있었다.
push가 발생하는 순간 항상 draft PR이 생성되기 때문에, 개발 중간 단계의 브랜치라도 PR이 자동으로 쌓이게 된다. 결과적으로 불필요한 PR을 삭제하거나 관리해야 하는 부담이 생기며, PR이 무분별하게 산출물처럼 남는 문제가 발생한다.
3. Label Trigger
최종적으로 선택한 방식은 Label 기반 트리거다.
이 방식에서는 push 이벤트에 아무런 트리거도 걸지 않는다. 개발자는 기능 구현을 마친 뒤, 필요할 때 직접 PR을 생성한다. 그리고 PR에 `ai-generated` 라벨을 추가하는 순간, GitHub Actions 워크플로우가 실행된다.
이 구조를 통해 AI 호출 시점을 명확하게 통제할 수 있어 비용 관리가 용이하며 불필요한 PR이 생성되지 않는다라는 장점이 있다.
결과적으로 Label Trigger 방식은 자동화의 편의성과 팀 운영의 현실적인 요구(비용, 관리, 일관성)를 동시에 만족시키는 선택이었다.

마지막으로 정리해보자면
1. `ai-generated` 라벨이 붙은 PR이 생성되면 git actions가 trigger 된다.
2. `pr-auto-summary` workflow가 실행된다.
3. PR 메타데이터를 수집한다.
4. Git 변경 사항을 수집한다.
5. 수집한 데이터들을 기반으로 Open AI API를 호출한다.
3. How It’s Implemented
전체 구현은 GitHub Actions 워크플로우 와 AI 호출 로직을 분리한 `Composite Action` 두 가지로 구성되어 있다.
- Workflow (`.github/workflows/pr-auto-summary.yml`)
트리거 제어, PR 컨텍스트 수집, Git 데이터 수집, PR 본문 업데이트를 담당 - Composite Action (`.github/actions/generate-pr-summary/action.yml`)
프롬프트 조립, OpenAI 호출(재시도 포함), 응답 파싱을 담당
📌 초기 세팅 - trigger 조건 설정
먼저 워크플로우는 `pull_request` 이벤트와 수동 실행이 가능하도록 `workflow_dispatch` 를 모두 받도록 구성했다.
# .github/workflows/pr-auto-summary.yml
name: PR Auto Summary
on:
pull_request:
types: [opened, labeled]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to regenerate'
required: true
type: string
그 다음, 실제 job이 실행되는 조건을 `ai-generated` 라벨 기준으로 제한하였다.
Push 기반 트리거는 앞 섹션에서 설명한 문제로 인해 배제하였고, 코드에서는 아래 `if`가 핵심이다.
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.action == 'labeled'
&& github.event.label.name == 'ai-generated') ||
(github.event.action == 'opened'
&& contains(github.event.pull_request.labels.*.name, 'ai-generated'))
그리고 최소 권한으로 운영하기 위해 permissions도 명시했다.
permissions:
pull-requests: write
contents: read
- `pull-requests: write` : PR 본문을 업데이트하기 위해 필요
- `contents: read` : checkout과 diff 수집에 필요
📌 Checkout (diff 수집을 위한 준비)
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
diff를 추출하기 위해서는 기준 브랜치까지 포함해서 git 히스토리가 필요하며로 `fetch-depth`를 0으로 설정해주었다.
Step 1 ) PR 정보 수집
이 단계는 무엇을 비교할지를 결정하는 단계이다. 여기서 핵심은 실행 경로가 두 가지라는 점이다.
PR 이벤트로 실행될 때는 `github.event.pull_request.*` 가 존재하지만, `workflow_dispatch`로 실행되면 PR 이벤트 컨텍스트가 없다. 따라서 수동 실행일 때는 `gh pr view`로 PR 메타데이터를 조회하고, 그 안에서 브랜치 정보들을 추출해야한다.
- name: Get PR Info
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
HEAD_BRANCH=$(gh pr view "$PR_NUMBER" --json headRefName -q .headRefName)
BASE_BRANCH=$(gh pr view "$PR_NUMBER" --json baseRefName -q .baseRefName)
else
HEAD_BRANCH="$PR_HEAD_REF"
BASE_BRANCH="$PR_BASE_REF"
fi
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "branch_name=$HEAD_BRANCH" >> $GITHUB_OUTPUT
echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT
결과는 `GITHUB_OUTPUT으로 내보낸 다음 단계에서 재사용 하며 이후 diff 수집은 항상 `base_branch`를 기준으로 진행된다.
Step 2) Git 데이터 수집
이 단계는 AI가 PR 본문을 작성할 수 있도록 변경 사항을 정리하여 재료로 만드는 단계이다. 총 세 가지 단계로 변경사항을 정리한다.
1. 변경된 파일을 훑어 어떤 영역이 바뀌었는지 파악
2. diff를 보며 정확히 무엇이 바뀌었는지 확인
3. 변경 규모를 보고 설명을 얼마나 자세히 쓸지 조절
이렇게 수집된 데이터는 각각 파일로 저장되며, 이후 AI가 PR 본문을 생성할 때 입력 데이터로 사용된다.
- name: Collect Data
env:
BASE_BRANCH: ${{ steps.pr.outputs.base_branch }}
run: |
git diff --name-status origin/$BASE_BRANCH...HEAD > /tmp/files.txt
git diff origin/$BASE_BRANCH...HEAD > /tmp/diff.txt
git diff --shortstat origin/$BASE_BRANCH...HEAD > /tmp/stats.txt
각 파일의 역할은 다음과 같다.
1. `/tmp/files.txt`
- 어떤 파일이 추가, 수정, 삭제되었는지를 제공한다.
- AI가 기능이나 모듈 단위로 묶어 설명할 때 첫 단서로 쓰기 좋다.
2. `/tmp/diff.txt`
- 실제 코드의 변경 내용이다.
- 단순 요약이 아니라 무엇을 어떻게 바꿨는지를 설명하게 만드는 핵심 근거이다.
3. `/tmp/stats.txt`
-> 변경 규모를 한 줄로 요약해, AI가 PR 본문의 양을 조절하는데 도움을 준다.
여기서 비교 범위는 `origin/$BASE_BRANCH...HEAD` 기준이다. 이는 PR 관점에서 base 브랜치 대비 변경된 내용을 추출하는 방식이라, PR 화면에서 기대하는 diff와 기준이 잘 맞는다.
Step 3) Composite Action으로 PR 본문 생성
이제 Step 2에서 만든 세 파일을 `Composite Action`에 넘겨야 한다. AI 호출의 세부 구현을 워크플로우에다 직접 작성하면 유지보수도 어려워지고, 파일의 양이 많아져 프롬프트 조립과 API 호출 로직을 따로 분리하였다.
# .github/workflows/pr-auto-summary.yml
- name: Generate PR
id: ai
uses: ./.github/actions/generate-pr-summary
with:
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
stats_file: /tmp/stats.txt
files_file: /tmp/files.txt
diff_file: /tmp/diff.txt
생성한 파일들을 Composite Action에 넘겨주고, 나는 AI로 gpt-5-mini 를 사용할 예정이라 `OPENAI_API_KEY` 를 발급받고 시크릿값을 넘겨주었다.
outputs:
body:
description: "Generated PR body in markdown format"
value: "${{ steps.gen.outputs.body }}"
AI 호출 결과를 workflow로 다시 돌려주기 위해서 AI가 생성한 결과를 body 라는 output을 정의해주었다.
runs:
using: "composite"
steps:
- name: "Generate PR Markdown"
id: "gen"
shell: "bash"
env:
OPENAI_API_KEY: "${{ inputs.openai_api_key }}"
run: |
set -euo pipefail
cat > /tmp/prompt.txt << 'EOF'
# 실제 작성할 프롬포트를 작성
# 팀의 컨벤션, 규약 등을 작성
여기는 실제로 AI에게 전달할 프롬포트를 작성하는 부분이다. 나의 경우에는 PR 본문 구조와 작성 규칙을 명시하였으며, 출력은 Markdown만 허용하도록 하였다. 이를 통해 팀원마다 다른 프롬포트를 사용하는 문제를 시스템적으로 차단하였다.
응답에서 필요한 것은 오직 마크다운 텍스트뿐이기 때문에, Responses API의 중첩된 구조에서 텍스트만 추출해 하나의 문자열로 합친다.
이 결과는 가공 없이 그대로 Action의 `output(body)`으로 반환된다.
Step 4) PR 본문 업데이트
마지막 단계에서는 Composite Action이 반환한 PR 본문을 실제 PR에 반영한다.
- name: Update PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${{ steps.ai.outputs.body }}" > /tmp/pr_body.md
gh pr edit "${{ steps.pr.outputs.pr_number }}" --body-file /tmp/pr_body.md
본문을 파일로 저장한 뒤 --body-file 옵션을 사용하여 본문 길이가 길어지더라도 셸 이스케이프 문제 없이 안정적으로 적용할 수 있었다.
4. Notes
대규모 PR의 경우 `diff.txt`의 크기가 커지면서, OpenAI API 호출 시 사용되는 토큰 수에 직접적인 영향을 줄 수 있다.
또한 변경 내역이 많아질수록 프롬프트 자체가 과도하게 비대해져, 요약 품질이나 응답 시간에 영향을 줄 가능성도 있다.
이를 개선하기 위한 방법으로는 다음과 같은 선택지가 있다.
- 모든 프롬프트를 영어로 작성해 토큰 효율을 높이거나
- PR의 크기 제한을 팀 규칙으로 정의하거나
- 특정 라인 수를 초과할 경우 일부 파일만 프롬프트에 포함하거나
- 전체 diff 대신 요약된 변경 정보만 전달하는 방식
PR의 규모와 팀 환경에 맞춰 적절한 전략을 조합한다면, 해당 방식에서 더 개선할 수 있지 않을까 싶다. 추후 해당 워크플로우의 개선이 이루어진다면 다시 정리해보겠다.
'DevOps > Github Actions' 카테고리의 다른 글
| 나의 macOS를 GitHub Actions Runner로 만들기 (0) | 2026.01.17 |
|---|