[이제와서 시작하는 GitHub 마스터하기 - 심화편 #3] 고급 브랜치 전략과 릴리스 관리: 대규모 팀을 위한 워크플로우
[이제와서 시작하는 GitHub 마스터하기 - 심화편 #3] 고급 브랜치 전략과 릴리스 관리: 대규모 팀을 위한 워크플로우
들어가며
GitHub 마스터하기 심화편 세 번째 시간입니다. 이번에는 대규모 팀과 복잡한 프로젝트를 위한 고급 브랜치 전략과 릴리스 관리 방법을 깊이 있게 다룹니다. Git Flow, GitHub Flow, GitLab Flow부터 최신 Trunk-Based Development까지, 각 전략의 장단점과 실제 적용 방법을 알아보겠습니다.
1. 브랜치 전략 비교 분석
주요 브랜치 전략 개요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Git Flow:
브랜치:
- main/master: 프로덕션
- develop: 개발 통합
- feature/*: 기능 개발
- release/*: 릴리스 준비
- hotfix/*: 긴급 수정
장점:
- 명확한 릴리스 주기
- 병렬 개발 지원
- 버전 관리 용이
단점:
- 복잡한 워크플로우
- 많은 브랜치 관리
- CI/CD와 충돌 가능
GitHub Flow:
브랜치:
- main: 항상 배포 가능
- feature branches: 모든 작업
장점:
- 단순함
- 지속적 배포 적합
- 빠른 피드백
단점:
- 릴리스 관리 어려움
- 환경별 배포 복잡
GitLab Flow:
브랜치:
- main: 개발
- pre-production: 스테이징
- production: 프로덕션
- feature/*: 기능 개발
장점:
- 환경별 브랜치
- 유연한 배포
- 이슈 트래킹 통합
단점:
- 환경 관리 복잡
- 추가 인프라 필요
Trunk-Based Development:
브랜치:
- main/trunk: 메인 개발
- short-lived branches: 1-2일
장점:
- 지속적 통합
- 빠른 피드백
- 충돌 최소화
단점:
- 높은 팀 숙련도 필요
- Feature flags 필수
- 엄격한 테스트 필요
전략 선택 의사결정 트리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# branching_strategy_selector.py
class BranchingStrategySelector:
def __init__(self):
self.criteria = {}
def analyze_project(self, project_info):
"""프로젝트 특성 분석"""
self.criteria = {
'team_size': project_info.get('team_size', 0),
'release_frequency': project_info.get('release_frequency', 'monthly'),
'environments': project_info.get('environments', ['production']),
'team_experience': project_info.get('team_experience', 'medium'),
'project_type': project_info.get('project_type', 'web'),
'compliance_required': project_info.get('compliance_required', False),
'feature_toggles': project_info.get('feature_toggles', False)
}
def recommend_strategy(self):
"""최적 전략 추천"""
score = {
'git_flow': 0,
'github_flow': 0,
'gitlab_flow': 0,
'trunk_based': 0
}
# 팀 규모
if self.criteria['team_size'] > 20:
score['git_flow'] += 3
score['gitlab_flow'] += 2
elif self.criteria['team_size'] < 5:
score['github_flow'] += 3
score['trunk_based'] += 2
# 릴리스 빈도
if self.criteria['release_frequency'] == 'daily':
score['trunk_based'] += 3
score['github_flow'] += 2
elif self.criteria['release_frequency'] == 'monthly':
score['git_flow'] += 3
score['gitlab_flow'] += 2
# 환경 복잡도
if len(self.criteria['environments']) > 3:
score['gitlab_flow'] += 3
score['git_flow'] += 1
# 규정 준수
if self.criteria['compliance_required']:
score['git_flow'] += 2
score['gitlab_flow'] += 2
# Feature toggles
if self.criteria['feature_toggles']:
score['trunk_based'] += 3
# 최고 점수 전략 반환
return max(score, key=score.get), score
# 사용 예제
selector = BranchingStrategySelector()
selector.analyze_project({
'team_size': 15,
'release_frequency': 'weekly',
'environments': ['dev', 'staging', 'production'],
'team_experience': 'high',
'project_type': 'saas',
'compliance_required': True,
'feature_toggles': True
})
strategy, scores = selector.recommend_strategy()
print(f"Recommended strategy: {strategy}")
print(f"Scores: {scores}")
2. Git Flow 고급 구현
자동화된 Git Flow 워크플로우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/bin/bash
# git-flow-advanced.sh
# 설정
DEVELOP_BRANCH="develop"
MAIN_BRANCH="main"
VERSION_FILE="version.txt"
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 현재 버전 읽기
get_current_version() {
if [ -f "$VERSION_FILE" ]; then
cat "$VERSION_FILE"
else
echo "0.0.0"
fi
}
# 버전 증가
increment_version() {
local version=$1
local type=$2
IFS='.' read -ra PARTS <<< "$version"
local major="${PARTS[0]}"
local minor="${PARTS[1]}"
local patch="${PARTS[2]}"
case $type in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
esac
echo "$major.$minor.$patch"
}
# Feature 브랜치 시작
start_feature() {
local feature_name=$1
if [ -z "$feature_name" ]; then
echo -e "${RED}Error: Feature name required${NC}"
exit 1
fi
echo -e "${GREEN}Starting feature: $feature_name${NC}"
# develop에서 브랜치 생성
git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
git checkout -b "feature/$feature_name"
echo -e "${GREEN}Feature branch created: feature/$feature_name${NC}"
}
# Feature 완료
finish_feature() {
local current_branch=$(git rev-parse --abbrev-ref HEAD)
if [[ ! "$current_branch" =~ ^feature/ ]]; then
echo -e "${RED}Error: Not on a feature branch${NC}"
exit 1
fi
local feature_name=${current_branch#feature/}
echo -e "${GREEN}Finishing feature: $feature_name${NC}"
# 테스트 실행
if ! run_tests; then
echo -e "${RED}Tests failed. Aborting.${NC}"
exit 1
fi
# develop에 병합
git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
if git merge --no-ff "feature/$feature_name" -m "Merge feature '$feature_name' into develop"; then
echo -e "${GREEN}Feature merged successfully${NC}"
# 브랜치 삭제
git branch -d "feature/$feature_name"
git push origin --delete "feature/$feature_name"
# develop 푸시
git push origin "$DEVELOP_BRANCH"
else
echo -e "${RED}Merge failed. Please resolve conflicts.${NC}"
exit 1
fi
}
# Release 시작
start_release() {
local version_type=${1:-patch}
local current_version=$(get_current_version)
local new_version=$(increment_version "$current_version" "$version_type")
echo -e "${GREEN}Starting release: $new_version${NC}"
# develop에서 브랜치 생성
git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
git checkout -b "release/$new_version"
# 버전 파일 업데이트
echo "$new_version" > "$VERSION_FILE"
git add "$VERSION_FILE"
git commit -m "Bump version to $new_version"
echo -e "${GREEN}Release branch created: release/$new_version${NC}"
echo -e "${YELLOW}Please perform final testing and fixes on this branch${NC}"
}
# Release 완료
finish_release() {
local current_branch=$(git rev-parse --abbrev-ref HEAD)
if [[ ! "$current_branch" =~ ^release/ ]]; then
echo -e "${RED}Error: Not on a release branch${NC}"
exit 1
fi
local version=${current_branch#release/}
echo -e "${GREEN}Finishing release: $version${NC}"
# 최종 테스트
if ! run_tests; then
echo -e "${RED}Tests failed. Aborting.${NC}"
exit 1
fi
# main에 병합
git checkout "$MAIN_BRANCH"
git pull origin "$MAIN_BRANCH"
git merge --no-ff "$current_branch" -m "Merge release '$version'"
# 태그 생성
git tag -a "v$version" -m "Release version $version"
# develop에 병합
git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
git merge --no-ff "$current_branch" -m "Merge release '$version' into develop"
# 브랜치 삭제
git branch -d "$current_branch"
# 푸시
git push origin "$MAIN_BRANCH"
git push origin "$DEVELOP_BRANCH"
git push origin "v$version"
git push origin --delete "$current_branch"
echo -e "${GREEN}Release $version completed!${NC}"
}
# Hotfix 시작
start_hotfix() {
local current_version=$(get_current_version)
local hotfix_version=$(increment_version "$current_version" "patch")
echo -e "${GREEN}Starting hotfix: $hotfix_version${NC}"
# main에서 브랜치 생성
git checkout "$MAIN_BRANCH"
git pull origin "$MAIN_BRANCH"
git checkout -b "hotfix/$hotfix_version"
echo -e "${GREEN}Hotfix branch created: hotfix/$hotfix_version${NC}"
}
# 테스트 실행
run_tests() {
echo -e "${YELLOW}Running tests...${NC}"
# 프로젝트에 맞는 테스트 명령어
if [ -f "package.json" ]; then
npm test
elif [ -f "Gemfile" ]; then
bundle exec rspec
elif [ -f "pom.xml" ]; then
mvn test
else
echo -e "${YELLOW}No test framework detected${NC}"
return 0
fi
}
# 메인 명령어 처리
case "$1" in
feature)
case "$2" in
start)
start_feature "$3"
;;
finish)
finish_feature
;;
*)
echo "Usage: $0 feature {start|finish} [name]"
exit 1
;;
esac
;;
release)
case "$2" in
start)
start_release "$3"
;;
finish)
finish_release
;;
*)
echo "Usage: $0 release {start|finish} [major|minor|patch]"
exit 1
;;
esac
;;
hotfix)
case "$2" in
start)
start_hotfix
;;
finish)
finish_release # hotfix는 release와 동일한 프로세스
;;
*)
echo "Usage: $0 hotfix {start|finish}"
exit 1
;;
esac
;;
*)
echo "Usage: $0 {feature|release|hotfix} {start|finish} [options]"
exit 1
;;
esac
Git Flow 상태 모니터링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/env python3
# git_flow_monitor.py
import subprocess
import json
from datetime import datetime, timedelta
from collections import defaultdict
class GitFlowMonitor:
def __init__(self, repo_path='.'):
self.repo_path = repo_path
self.branches = self.get_all_branches()
def run_git_command(self, cmd):
"""Git 명령 실행"""
result = subprocess.run(
f"git {cmd}",
shell=True,
cwd=self.repo_path,
capture_output=True,
text=True
)
return result.stdout.strip()
def get_all_branches(self):
"""모든 브랜치 정보 수집"""
branches = {}
# 로컬 및 원격 브랜치
output = self.run_git_command("branch -a --format='%(refname:short)|%(committerdate:iso)|%(committername)'")
for line in output.split('\n'):
if line:
parts = line.split('|')
if len(parts) >= 3:
branch_name = parts[0].replace('origin/', '')
last_commit = datetime.fromisoformat(parts[1].replace(' +0000', '+00:00'))
author = parts[2]
branches[branch_name] = {
'last_commit': last_commit,
'author': author,
'age_days': (datetime.now(last_commit.tzinfo) - last_commit).days
}
return branches
def analyze_flow_health(self):
"""Git Flow 건강 상태 분석"""
health_report = {
'status': 'healthy',
'issues': [],
'warnings': [],
'metrics': {}
}
# 브랜치 분류
features = [b for b in self.branches if b.startswith('feature/')]
releases = [b for b in self.branches if b.startswith('release/')]
hotfixes = [b for b in self.branches if b.startswith('hotfix/')]
# 메트릭 계산
health_report['metrics'] = {
'active_features': len(features),
'active_releases': len(releases),
'active_hotfixes': len(hotfixes),
'total_branches': len(self.branches)
}
# 오래된 feature 브랜치 확인
old_features = [b for b in features if self.branches[b]['age_days'] > 30]
if old_features:
health_report['warnings'].append({
'type': 'stale_features',
'message': f"{len(old_features)} feature branches older than 30 days",
'branches': old_features
})
# 다중 release 브랜치 확인
if len(releases) > 1:
health_report['issues'].append({
'type': 'multiple_releases',
'message': f"{len(releases)} release branches active simultaneously",
'branches': releases
})
health_report['status'] = 'unhealthy'
# develop 브랜치 확인
if 'develop' not in self.branches:
health_report['issues'].append({
'type': 'missing_develop',
'message': "Develop branch not found"
})
health_report['status'] = 'unhealthy'
# 병합되지 않은 브랜치 확인
unmerged = self.find_unmerged_branches()
if len(unmerged) > 10:
health_report['warnings'].append({
'type': 'many_unmerged',
'message': f"{len(unmerged)} unmerged branches",
'count': len(unmerged)
})
return health_report
def find_unmerged_branches(self):
"""병합되지 않은 브랜치 찾기"""
merged = self.run_git_command("branch -a --merged develop")
all_branches = self.run_git_command("branch -a")
merged_set = set(merged.split('\n'))
all_set = set(all_branches.split('\n'))
return list(all_set - merged_set)
def generate_report(self):
"""상세 리포트 생성"""
health = self.analyze_flow_health()
report = f"""
Git Flow Health Report
======================
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Status: {health['status'].upper()}
Metrics:
--------
Active Features: {health['metrics']['active_features']}
Active Releases: {health['metrics']['active_releases']}
Active Hotfixes: {health['metrics']['active_hotfixes']}
Total Branches: {health['metrics']['total_branches']}
"""
if health['issues']:
report += "ISSUES:\n-------\n"
for issue in health['issues']:
report += f"❌ {issue['message']}\n"
if health['warnings']:
report += "\nWARNINGS:\n---------\n"
for warning in health['warnings']:
report += f"⚠️ {warning['message']}\n"
# 브랜치 상세 정보
report += "\nBranch Details:\n---------------\n"
for branch_type in ['feature/', 'release/', 'hotfix/']:
branches = [b for b in self.branches if b.startswith(branch_type)]
if branches:
report += f"\n{branch_type.upper()}:\n"
for branch in sorted(branches, key=lambda b: self.branches[b]['age_days'], reverse=True):
info = self.branches[branch]
report += f" - {branch}: {info['age_days']} days old (by {info['author']})\n"
return report
def export_metrics(self, format='json'):
"""메트릭 내보내기"""
data = {
'timestamp': datetime.now().isoformat(),
'health': self.analyze_flow_health(),
'branches': {
branch: {
'last_commit': info['last_commit'].isoformat(),
'author': info['author'],
'age_days': info['age_days']
}
for branch, info in self.branches.items()
}
}
if format == 'json':
return json.dumps(data, indent=2)
elif format == 'prometheus':
metrics = []
health = data['health']
metrics.append(f"git_flow_active_features {health['metrics']['active_features']}")
metrics.append(f"git_flow_active_releases {health['metrics']['active_releases']}")
metrics.append(f"git_flow_active_hotfixes {health['metrics']['active_hotfixes']}")
metrics.append(f"git_flow_health_status {1 if health['status'] == 'healthy' else 0}")
return '\n'.join(metrics)
# 사용 예제
monitor = GitFlowMonitor()
print(monitor.generate_report())
# Prometheus 메트릭 출력
print("\nPrometheus Metrics:")
print(monitor.export_metrics('prometheus'))
3. GitHub Flow 최적화
GitHub Flow 자동화 도구
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// github-flow-automation.js
const { Octokit } = require('@octokit/rest');
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
class GitHubFlowAutomation {
constructor(token, owner, repo) {
this.octokit = new Octokit({ auth: token });
this.owner = owner;
this.repo = repo;
}
async createFeatureBranch(branchName, description) {
// 1. main 브랜치 최신 커밋 가져오기
const { data: mainBranch } = await this.octokit.repos.getBranch({
owner: this.owner,
repo: this.repo,
branch: 'main'
});
const mainSha = mainBranch.commit.sha;
// 2. 새 브랜치 생성
await this.octokit.git.createRef({
owner: this.owner,
repo: this.repo,
ref: `refs/heads/${branchName}`,
sha: mainSha
});
// 3. 브랜치 보호 규칙 설정 (선택적)
if (branchName.startsWith('feature/')) {
await this.setupBranchProtection(branchName);
}
// 4. 이슈 생성 및 연결
const { data: issue } = await this.octokit.issues.create({
owner: this.owner,
repo: this.repo,
title: `Feature: ${description}`,
body: `## Feature Branch: ${branchName}\n\n${description}`,
labels: ['feature', 'in-progress']
});
console.log(`Created branch: ${branchName}`);
console.log(`Created issue: #${issue.number}`);
return { branch: branchName, issue: issue.number };
}
async createPullRequest(branchName, issueNumber) {
// 1. 브랜치 차이 확인
const { data: comparison } = await this.octokit.repos.compareCommitsWithBasehead({
owner: this.owner,
repo: this.repo,
basehead: `main...${branchName}`
});
if (comparison.total_commits === 0) {
throw new Error('No commits to create PR');
}
// 2. PR 템플릿 생성
const prBody = `
## Description
Closes #${issueNumber}
## Changes
${comparison.commits.map(c => `- ${c.commit.message}`).join('\n')}
## Checklist
- [ ] Tests pass
- [ ] Documentation updated
- [ ] Code review requested
- [ ] No breaking changes
## Screenshots (if applicable)
N/A
`;
// 3. PR 생성
const { data: pr } = await this.octokit.pulls.create({
owner: this.owner,
repo: this.repo,
title: `Feature: ${branchName.replace('feature/', '')}`,
head: branchName,
base: 'main',
body: prBody,
draft: false
});
// 4. 리뷰어 자동 할당
await this.assignReviewers(pr.number);
// 5. 라벨 추가
await this.octokit.issues.addLabels({
owner: this.owner,
repo: this.repo,
issue_number: pr.number,
labels: ['needs-review', 'feature']
});
return pr;
}
async assignReviewers(prNumber) {
// CODEOWNERS 파일 기반 리뷰어 할당
try {
const { data: codeowners } = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: '.github/CODEOWNERS'
});
const content = Buffer.from(codeowners.content, 'base64').toString();
const reviewers = this.parseCodeOwners(content);
await this.octokit.pulls.requestReviewers({
owner: this.owner,
repo: this.repo,
pull_number: prNumber,
reviewers: reviewers.slice(0, 2) // 최대 2명
});
} catch (error) {
console.log('CODEOWNERS not found, skipping auto-assignment');
}
}
async monitorPR(prNumber) {
const checkInterval = 60000; // 1분
const monitor = async () => {
const { data: pr } = await this.octokit.pulls.get({
owner: this.owner,
repo: this.repo,
pull_number: prNumber
});
// 체크 상태 확인
const { data: checks } = await this.octokit.checks.listForRef({
owner: this.owner,
repo: this.repo,
ref: pr.head.sha
});
const allChecksPassed = checks.check_runs.every(
check => check.status === 'completed' && check.conclusion === 'success'
);
// 리뷰 상태 확인
const { data: reviews } = await this.octokit.pulls.listReviews({
owner: this.owner,
repo: this.repo,
pull_number: prNumber
});
const approved = reviews.some(r => r.state === 'APPROVED');
console.log(`PR #${prNumber} Status:`);
console.log(`- Checks: ${allChecksPassed ? '✅' : '⏳'}`);
console.log(`- Reviews: ${approved ? '✅' : '⏳'}`);
console.log(`- Mergeable: ${pr.mergeable ? '✅' : '❌'}`);
// 자동 병합 조건 확인
if (allChecksPassed && approved && pr.mergeable) {
await this.autoMerge(prNumber);
return true; // 모니터링 종료
}
return false;
};
// 주기적 모니터링
const intervalId = setInterval(async () => {
const completed = await monitor();
if (completed) {
clearInterval(intervalId);
}
}, checkInterval);
}
async autoMerge(prNumber) {
try {
// Squash merge 실행
const { data: mergeResult } = await this.octokit.pulls.merge({
owner: this.owner,
repo: this.repo,
pull_number: prNumber,
merge_method: 'squash'
});
console.log(`✅ PR #${prNumber} merged successfully`);
// 브랜치 삭제
const { data: pr } = await this.octokit.pulls.get({
owner: this.owner,
repo: this.repo,
pull_number: prNumber
});
await this.octokit.git.deleteRef({
owner: this.owner,
repo: this.repo,
ref: `heads/${pr.head.ref}`
});
console.log(`🗑️ Branch ${pr.head.ref} deleted`);
} catch (error) {
console.error(`Failed to auto-merge PR #${prNumber}:`, error.message);
}
}
parseCodeOwners(content) {
// 간단한 CODEOWNERS 파서
const lines = content.split('\n');
const owners = new Set();
lines.forEach(line => {
if (line.trim() && !line.startsWith('#')) {
const parts = line.split(/\s+/);
parts.slice(1).forEach(owner => {
if (owner.startsWith('@')) {
owners.add(owner.substring(1));
}
});
}
});
return Array.from(owners);
}
}
// 사용 예제
async function runGitHubFlow() {
const automation = new GitHubFlowAutomation(
process.env.GITHUB_TOKEN,
'myorg',
'myrepo'
);
// 1. Feature 브랜치 생성
const { branch, issue } = await automation.createFeatureBranch(
'feature/user-authentication',
'Implement user authentication with OAuth2'
);
// 2. 개발 작업 (로컬에서)
await execAsync(`git fetch origin`);
await execAsync(`git checkout ${branch}`);
// ... 코드 작성 ...
await execAsync(`git add .`);
await execAsync(`git commit -m "feat: implement OAuth2 authentication"`);
await execAsync(`git push origin ${branch}`);
// 3. PR 생성
const pr = await automation.createPullRequest(branch, issue);
console.log(`Created PR: #${pr.number}`);
// 4. PR 모니터링 및 자동 병합
await automation.monitorPR(pr.number);
}
// 실행
runGitHubFlow().catch(console.error);
4. Trunk-Based Development 구현
Feature Flags를 활용한 TBD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# trunk_based_development.py
import json
import hashlib
import requests
from datetime import datetime
from abc import ABC, abstractmethod
class FeatureFlag:
"""Feature Flag 구현"""
def __init__(self, name, enabled=False, rollout_percentage=0, conditions=None):
self.name = name
self.enabled = enabled
self.rollout_percentage = rollout_percentage
self.conditions = conditions or {}
def is_enabled_for_user(self, user_id, attributes=None):
"""사용자별 활성화 여부 확인"""
if not self.enabled:
return False
# 조건 확인
if self.conditions:
if not self._check_conditions(attributes):
return False
# 점진적 롤아웃
if 0 < self.rollout_percentage < 100:
# 일관된 해싱으로 사용자 그룹 결정
hash_input = f"{self.name}:{user_id}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
user_percentage = hash_value % 100
return user_percentage < self.rollout_percentage
return True
def _check_conditions(self, attributes):
"""조건 확인"""
if not attributes:
return False
for key, expected_value in self.conditions.items():
if key not in attributes:
return False
if isinstance(expected_value, list):
if attributes[key] not in expected_value:
return False
else:
if attributes[key] != expected_value:
return False
return True
class FeatureFlagProvider(ABC):
"""Feature Flag Provider 인터페이스"""
@abstractmethod
def get_flag(self, flag_name):
pass
@abstractmethod
def update_flag(self, flag_name, config):
pass
class LocalFeatureFlagProvider(FeatureFlagProvider):
"""로컬 JSON 기반 Provider"""
def __init__(self, config_file='feature_flags.json'):
self.config_file = config_file
self.flags = self._load_flags()
def _load_flags(self):
try:
with open(self.config_file, 'r') as f:
data = json.load(f)
return {
name: FeatureFlag(name, **config)
for name, config in data.items()
}
except FileNotFoundError:
return {}
def get_flag(self, flag_name):
return self.flags.get(flag_name)
def update_flag(self, flag_name, config):
self.flags[flag_name] = FeatureFlag(flag_name, **config)
self._save_flags()
def _save_flags(self):
data = {
name: {
'enabled': flag.enabled,
'rollout_percentage': flag.rollout_percentage,
'conditions': flag.conditions
}
for name, flag in self.flags.items()
}
with open(self.config_file, 'w') as f:
json.dump(data, f, indent=2)
class TrunkBasedWorkflow:
"""Trunk-Based Development 워크플로우"""
def __init__(self, feature_flag_provider):
self.feature_flags = feature_flag_provider
self.deployment_pipeline = []
def add_feature(self, feature_name, implementation):
"""Feature flag로 보호된 기능 추가"""
flag = FeatureFlag(feature_name, enabled=False)
self.feature_flags.update_flag(feature_name, {
'enabled': False,
'rollout_percentage': 0,
'conditions': {}
})
def wrapped_implementation(*args, **kwargs):
user_id = kwargs.get('user_id', 'anonymous')
user_attributes = kwargs.get('user_attributes', {})
flag = self.feature_flags.get_flag(feature_name)
if flag and flag.is_enabled_for_user(user_id, user_attributes):
return implementation(*args, **kwargs)
else:
# 기존 동작 또는 대체 동작
return None
return wrapped_implementation
def deploy_feature(self, feature_name, strategy='canary'):
"""점진적 배포"""
stages = []
if strategy == 'canary':
stages = [
{'percentage': 1, 'duration': 3600}, # 1% for 1 hour
{'percentage': 5, 'duration': 7200}, # 5% for 2 hours
{'percentage': 25, 'duration': 14400}, # 25% for 4 hours
{'percentage': 50, 'duration': 28800}, # 50% for 8 hours
{'percentage': 100, 'duration': 0} # 100% (완료)
]
elif strategy == 'blue_green':
stages = [
{'percentage': 0, 'duration': 0},
{'percentage': 100, 'duration': 0}
]
return DeploymentPipeline(
feature_name,
stages,
self.feature_flags
)
class DeploymentPipeline:
"""배포 파이프라인"""
def __init__(self, feature_name, stages, feature_flags):
self.feature_name = feature_name
self.stages = stages
self.feature_flags = feature_flags
self.current_stage = 0
self.start_time = None
def start(self):
"""배포 시작"""
self.start_time = datetime.now()
self._apply_stage(0)
def _apply_stage(self, stage_index):
"""스테이지 적용"""
if stage_index >= len(self.stages):
print(f"✅ Deployment of {self.feature_name} completed!")
return
stage = self.stages[stage_index]
self.current_stage = stage_index
# Feature flag 업데이트
self.feature_flags.update_flag(self.feature_name, {
'enabled': True,
'rollout_percentage': stage['percentage'],
'conditions': {}
})
print(f"📊 Stage {stage_index + 1}: {stage['percentage']}% rollout")
# 다음 스테이지 스케줄링 (실제로는 별도 스케줄러 사용)
if stage['duration'] > 0:
print(f"⏱️ Next stage in {stage['duration']} seconds")
# 실제 구현에서는 스케줄러 사용
def rollback(self):
"""롤백"""
print(f"🔄 Rolling back {self.feature_name}")
self.feature_flags.update_flag(self.feature_name, {
'enabled': False,
'rollout_percentage': 0,
'conditions': {}
})
def get_metrics(self):
"""배포 메트릭"""
# 실제로는 모니터링 시스템과 통합
return {
'feature': self.feature_name,
'current_stage': self.current_stage,
'rollout_percentage': self.stages[self.current_stage]['percentage'],
'start_time': self.start_time,
'duration': (datetime.now() - self.start_time).total_seconds()
}
# 사용 예제
if __name__ == "__main__":
# Feature flag provider 초기화
provider = LocalFeatureFlagProvider()
# TBD 워크플로우 생성
workflow = TrunkBasedWorkflow(provider)
# 새 기능 추가
@workflow.add_feature('new_checkout_flow')
def new_checkout(user_id, cart_items, user_attributes=None):
print(f"New checkout flow for user {user_id}")
# 새로운 체크아웃 로직
return {'status': 'success', 'flow': 'new'}
# 기능 테스트
result = new_checkout(user_id='user123', cart_items=[])
print(f"Result: {result}") # None (비활성화 상태)
# 카나리 배포 시작
deployment = workflow.deploy_feature('new_checkout_flow', 'canary')
deployment.start()
# 다시 테스트
result = new_checkout(user_id='user123', cart_items=[])
print(f"Result after deployment: {result}")
Branch by Abstraction 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# branch_by_abstraction.py
from abc import ABC, abstractmethod
from typing import Dict, Any
import logging
class PaymentProcessor(ABC):
"""결제 처리 추상화"""
@abstractmethod
def process_payment(self, amount: float, payment_info: Dict[str, Any]) -> Dict[str, Any]:
pass
@abstractmethod
def refund_payment(self, transaction_id: str, amount: float) -> Dict[str, Any]:
pass
class LegacyPaymentProcessor(PaymentProcessor):
"""기존 결제 시스템"""
def process_payment(self, amount: float, payment_info: Dict[str, Any]) -> Dict[str, Any]:
logging.info(f"Processing payment with legacy system: ${amount}")
# 기존 결제 로직
return {
'status': 'success',
'transaction_id': f'legacy_{payment_info.get("order_id")}',
'processor': 'legacy'
}
def refund_payment(self, transaction_id: str, amount: float) -> Dict[str, Any]:
logging.info(f"Processing refund with legacy system: ${amount}")
return {
'status': 'success',
'refund_id': f'refund_{transaction_id}',
'processor': 'legacy'
}
class NewPaymentProcessor(PaymentProcessor):
"""새로운 결제 시스템"""
def process_payment(self, amount: float, payment_info: Dict[str, Any]) -> Dict[str, Any]:
logging.info(f"Processing payment with new system: ${amount}")
# 새로운 결제 로직
return {
'status': 'success',
'transaction_id': f'new_{payment_info.get("order_id")}',
'processor': 'new',
'additional_info': {
'fraud_score': 0.1,
'processing_time': 1.2
}
}
def refund_payment(self, transaction_id: str, amount: float) -> Dict[str, Any]:
logging.info(f"Processing refund with new system: ${amount}")
return {
'status': 'success',
'refund_id': f'refund_{transaction_id}',
'processor': 'new',
'instant_refund': True
}
class PaymentProcessorFactory:
"""결제 프로세서 팩토리"""
def __init__(self, feature_flags):
self.feature_flags = feature_flags
self.legacy_processor = LegacyPaymentProcessor()
self.new_processor = NewPaymentProcessor()
def get_processor(self, user_id: str = None, user_attributes: Dict = None) -> PaymentProcessor:
"""사용자별 적절한 프로세서 반환"""
flag = self.feature_flags.get_flag('new_payment_system')
if flag and flag.is_enabled_for_user(user_id, user_attributes):
return self.new_processor
else:
return self.legacy_processor
class PaymentService:
"""결제 서비스 (추상화 사용)"""
def __init__(self, processor_factory: PaymentProcessorFactory):
self.processor_factory = processor_factory
def process_order_payment(self, order_id: str, amount: float,
user_id: str, payment_info: Dict[str, Any]) -> Dict[str, Any]:
"""주문 결제 처리"""
# 적절한 프로세서 선택
processor = self.processor_factory.get_processor(
user_id=user_id,
user_attributes={'subscription_tier': payment_info.get('tier', 'free')}
)
# 결제 처리
result = processor.process_payment(amount, payment_info)
# 공통 후처리
self._log_payment_metrics(result)
self._send_notification(user_id, result)
return result
def _log_payment_metrics(self, result: Dict[str, Any]):
"""메트릭 로깅"""
processor_type = result.get('processor', 'unknown')
logging.info(f"Payment processed by {processor_type} system")
def _send_notification(self, user_id: str, result: Dict[str, Any]):
"""알림 전송"""
if result['status'] == 'success':
logging.info(f"Sending payment success notification to user {user_id}")
# 마이그레이션 전략
class PaymentSystemMigration:
"""결제 시스템 마이그레이션"""
def __init__(self, feature_flags, payment_service):
self.feature_flags = feature_flags
self.payment_service = payment_service
self.migration_stages = [
{
'name': 'testing',
'percentage': 0,
'conditions': {'subscription_tier': ['internal_test']},
'duration_days': 7
},
{
'name': 'beta_users',
'percentage': 1,
'conditions': {'subscription_tier': ['beta']},
'duration_days': 14
},
{
'name': 'premium_users',
'percentage': 10,
'conditions': {'subscription_tier': ['premium']},
'duration_days': 7
},
{
'name': 'gradual_rollout',
'percentage': 50,
'conditions': {},
'duration_days': 14
},
{
'name': 'full_rollout',
'percentage': 100,
'conditions': {},
'duration_days': 0
}
]
def execute_migration(self):
"""단계별 마이그레이션 실행"""
for stage in self.migration_stages:
print(f"\n🚀 Starting migration stage: {stage['name']}")
# Feature flag 업데이트
self.feature_flags.update_flag('new_payment_system', {
'enabled': True,
'rollout_percentage': stage['percentage'],
'conditions': stage['conditions']
})
# 모니터링 및 검증
if not self._validate_stage(stage):
print(f"❌ Validation failed for stage {stage['name']}")
self._rollback()
return False
print(f"✅ Stage {stage['name']} completed successfully")
print("\n🎉 Migration completed successfully!")
return True
def _validate_stage(self, stage):
"""스테이지 검증"""
# 실제로는 메트릭 확인, 에러율 체크 등
print(f"Validating stage {stage['name']}...")
return True
def _rollback(self):
"""롤백"""
print("🔄 Rolling back to legacy system")
self.feature_flags.update_flag('new_payment_system', {
'enabled': False,
'rollout_percentage': 0,
'conditions': {}
})
5. 릴리스 관리 자동화
Semantic Versioning 자동화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/usr/bin/env python3
# semantic_release.py
import re
import subprocess
import json
from datetime import datetime
from typing import List, Tuple, Dict
class SemanticVersioning:
"""Semantic Versioning 2.0.0 구현"""
VERSION_PATTERN = re.compile(r'^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\-\.]+))?(?:\+([a-zA-Z0-9\-\.]+))?$')
def __init__(self, current_version: str):
match = self.VERSION_PATTERN.match(current_version)
if not match:
raise ValueError(f"Invalid version format: {current_version}")
self.major = int(match.group(1))
self.minor = int(match.group(2))
self.patch = int(match.group(3))
self.prerelease = match.group(4)
self.metadata = match.group(5)
def bump(self, bump_type: str) -> str:
"""버전 증가"""
if bump_type == 'major':
self.major += 1
self.minor = 0
self.patch = 0
elif bump_type == 'minor':
self.minor += 1
self.patch = 0
elif bump_type == 'patch':
self.patch += 1
elif bump_type == 'prerelease':
if self.prerelease:
# 기존 prerelease 버전 증가
parts = self.prerelease.split('.')
if parts[-1].isdigit():
parts[-1] = str(int(parts[-1]) + 1)
else:
parts.append('1')
self.prerelease = '.'.join(parts)
else:
self.prerelease = 'rc.1'
else:
raise ValueError(f"Unknown bump type: {bump_type}")
return str(self)
def __str__(self):
version = f"{self.major}.{self.minor}.{self.patch}"
if self.prerelease:
version += f"-{self.prerelease}"
if self.metadata:
version += f"+{self.metadata}"
return version
class ConventionalCommitParser:
"""Conventional Commits 파서"""
COMMIT_PATTERN = re.compile(
r'^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?: (?P<description>.+?)(?:\n\n(?P<body>[\s\S]*?))?(?:\n\n(?P<footer>[\s\S]+))?$'
)
BREAKING_CHANGE_PATTERN = re.compile(r'BREAKING[\s-]CHANGE:', re.IGNORECASE)
TYPE_MAPPING = {
'feat': 'minor',
'fix': 'patch',
'docs': None,
'style': None,
'refactor': None,
'perf': 'patch',
'test': None,
'build': None,
'ci': None,
'chore': None,
'revert': 'patch'
}
def parse(self, commit_message: str) -> Dict:
"""커밋 메시지 파싱"""
match = self.COMMIT_PATTERN.match(commit_message.strip())
if not match:
return None
result = match.groupdict()
# Breaking change 확인
result['breaking'] = bool(
self.BREAKING_CHANGE_PATTERN.search(commit_message) or
commit_message.startswith('!:')
)
# 버전 영향도 결정
if result['breaking']:
result['version_bump'] = 'major'
else:
result['version_bump'] = self.TYPE_MAPPING.get(result['type'], None)
return result
class ReleaseAutomation:
"""릴리스 자동화"""
def __init__(self, repo_path: str = '.'):
self.repo_path = repo_path
self.commit_parser = ConventionalCommitParser()
def get_commits_since_last_tag(self) -> List[str]:
"""마지막 태그 이후 커밋 가져오기"""
try:
# 마지막 태그 찾기
last_tag = subprocess.check_output(
['git', 'describe', '--tags', '--abbrev=0'],
cwd=self.repo_path,
text=True
).strip()
except subprocess.CalledProcessError:
# 태그가 없으면 첫 커밋부터
last_tag = ''
# 커밋 로그 가져오기
if last_tag:
cmd = ['git', 'log', f'{last_tag}..HEAD', '--pretty=format:%H|%s|%b|%aN|%aE']
else:
cmd = ['git', 'log', '--pretty=format:%H|%s|%b|%aN|%aE']
output = subprocess.check_output(cmd, cwd=self.repo_path, text=True)
commits = []
for line in output.strip().split('\n'):
if line:
parts = line.split('|', 4)
commits.append({
'hash': parts[0],
'subject': parts[1],
'body': parts[2] if len(parts) > 2 else '',
'author': parts[3] if len(parts) > 3 else '',
'email': parts[4] if len(parts) > 4 else ''
})
return commits
def determine_version_bump(self, commits: List[Dict]) -> str:
"""커밋 기반 버전 범프 타입 결정"""
bump_priority = {'major': 3, 'minor': 2, 'patch': 1}
max_bump = None
for commit in commits:
full_message = f"{commit['subject']}\n\n{commit['body']}"
parsed = self.commit_parser.parse(full_message)
if parsed and parsed['version_bump']:
if not max_bump or bump_priority.get(parsed['version_bump'], 0) > bump_priority.get(max_bump, 0):
max_bump = parsed['version_bump']
return max_bump or 'patch'
def generate_changelog(self, commits: List[Dict], version: str) -> str:
"""변경 로그 생성"""
changelog = f"# Release {version} ({datetime.now().strftime('%Y-%m-%d')})\n\n"
# 커밋 분류
categories = {
'Features': [],
'Bug Fixes': [],
'Performance Improvements': [],
'Breaking Changes': [],
'Other Changes': []
}
for commit in commits:
full_message = f"{commit['subject']}\n\n{commit['body']}"
parsed = self.commit_parser.parse(full_message)
if not parsed:
categories['Other Changes'].append(commit)
continue
if parsed['breaking']:
categories['Breaking Changes'].append(commit)
elif parsed['type'] == 'feat':
categories['Features'].append(commit)
elif parsed['type'] == 'fix':
categories['Bug Fixes'].append(commit)
elif parsed['type'] == 'perf':
categories['Performance Improvements'].append(commit)
else:
categories['Other Changes'].append(commit)
# 변경 로그 작성
for category, items in categories.items():
if items:
changelog += f"## {category}\n\n"
for item in items:
scope = ''
if parsed := self.commit_parser.parse(f"{item['subject']}\n\n{item['body']}"):
if parsed.get('scope'):
scope = f"**{parsed['scope']}:** "
changelog += f"- {scope}{item['subject']} ([{item['hash'][:7]}]({item['hash']}))\n"
changelog += "\n"
# Contributors
contributors = set((commit['author'], commit['email']) for commit in commits)
if contributors:
changelog += "## Contributors\n\n"
for name, email in sorted(contributors):
changelog += f"- {name} <{email}>\n"
return changelog
def create_release(self, dry_run: bool = False):
"""릴리스 생성"""
# 현재 버전 가져오기
try:
current_version = subprocess.check_output(
['git', 'describe', '--tags', '--abbrev=0'],
cwd=self.repo_path,
text=True
).strip()
except subprocess.CalledProcessError:
current_version = 'v0.0.0'
# 커밋 분석
commits = self.get_commits_since_last_tag()
if not commits:
print("No commits since last release")
return
# 버전 결정
bump_type = self.determine_version_bump(commits)
version = SemanticVersioning(current_version)
new_version = version.bump(bump_type)
# 변경 로그 생성
changelog = self.generate_changelog(commits, new_version)
print(f"Current version: {current_version}")
print(f"New version: v{new_version}")
print(f"Bump type: {bump_type}")
print("\nChangelog:")
print(changelog)
if not dry_run:
# 태그 생성
subprocess.run(
['git', 'tag', '-a', f'v{new_version}', '-m', f'Release v{new_version}\n\n{changelog}'],
cwd=self.repo_path,
check=True
)
# CHANGELOG.md 업데이트
self._update_changelog_file(new_version, changelog)
print(f"\n✅ Release v{new_version} created!")
print("Run 'git push --tags' to publish")
else:
print("\n🔍 Dry run completed (no changes made)")
def _update_changelog_file(self, version: str, changelog: str):
"""CHANGELOG.md 파일 업데이트"""
changelog_file = os.path.join(self.repo_path, 'CHANGELOG.md')
if os.path.exists(changelog_file):
with open(changelog_file, 'r') as f:
existing_content = f.read()
else:
existing_content = "# Changelog\n\n"
# 새 내용 추가
new_content = existing_content.replace(
"# Changelog\n\n",
f"# Changelog\n\n{changelog}\n\n"
)
with open(changelog_file, 'w') as f:
f.write(new_content)
# Git에 추가
subprocess.run(
['git', 'add', 'CHANGELOG.md'],
cwd=self.repo_path,
check=True
)
subprocess.run(
['git', 'commit', '-m', f'docs: update changelog for v{version}'],
cwd=self.repo_path,
check=True
)
# CLI 인터페이스
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='Automated semantic release')
parser.add_argument('--dry-run', action='store_true', help='Perform dry run')
parser.add_argument('--path', default='.', help='Repository path')
args = parser.parse_args()
automation = ReleaseAutomation(args.path)
automation.create_release(dry_run=args.dry_run)
6. 브랜치 전략 마이그레이션
전략 변경 가이드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# branch_strategy_migration.py
import os
import subprocess
from typing import List, Dict
from datetime import datetime
class BranchStrategyMigration:
"""브랜치 전략 마이그레이션 도구"""
def __init__(self, repo_path: str = '.'):
self.repo_path = repo_path
def analyze_current_structure(self) -> Dict:
"""현재 브랜치 구조 분석"""
# 모든 브랜치 가져오기
branches = subprocess.check_output(
['git', 'branch', '-a'],
cwd=self.repo_path,
text=True
).strip().split('\n')
structure = {
'total_branches': len(branches),
'patterns': {
'git_flow': 0,
'github_flow': 0,
'custom': 0
},
'branches': []
}
# 패턴 분석
for branch in branches:
branch = branch.strip().replace('* ', '').replace('remotes/origin/', '')
if branch in ['main', 'master', 'develop']:
structure['patterns']['git_flow'] += 1
elif branch.startswith(('feature/', 'release/', 'hotfix/')):
structure['patterns']['git_flow'] += 1
elif '/' not in branch and branch not in ['main', 'master']:
structure['patterns']['github_flow'] += 1
else:
structure['patterns']['custom'] += 1
structure['branches'].append(branch)
# 현재 전략 추정
if structure['patterns']['git_flow'] > structure['patterns']['github_flow']:
structure['current_strategy'] = 'git_flow'
elif structure['patterns']['github_flow'] > structure['patterns']['git_flow']:
structure['current_strategy'] = 'github_flow'
else:
structure['current_strategy'] = 'unknown'
return structure
def migrate_to_trunk_based(self, dry_run: bool = True):
"""Trunk-Based Development로 마이그레이션"""
print("🚀 Migrating to Trunk-Based Development")
migration_plan = []
# 1. 현재 구조 분석
current = self.analyze_current_structure()
print(f"Current strategy: {current['current_strategy']}")
print(f"Total branches: {current['total_branches']}")
# 2. 활성 feature 브랜치 확인
active_features = [b for b in current['branches'] if b.startswith('feature/')]
if active_features:
print(f"\n⚠️ Found {len(active_features)} active feature branches")
migration_plan.append({
'action': 'merge_features',
'branches': active_features,
'description': 'Merge all active feature branches'
})
# 3. develop 브랜치 처리
if 'develop' in current['branches']:
migration_plan.append({
'action': 'merge_develop',
'description': 'Merge develop into main and remove'
})
# 4. 브랜치 정리 계획
cleanup_branches = [
b for b in current['branches']
if b not in ['main', 'master'] and
not b.startswith('feature/') and
not b.endswith('/HEAD')
]
if cleanup_branches:
migration_plan.append({
'action': 'cleanup_branches',
'branches': cleanup_branches,
'description': 'Remove unnecessary branches'
})
# 5. 설정 변경
migration_plan.append({
'action': 'update_settings',
'description': 'Update repository settings for TBD'
})
# 계획 출력
print("\n📋 Migration Plan:")
for i, step in enumerate(migration_plan, 1):
print(f"\n{i}. {step['description']}")
if 'branches' in step:
for branch in step['branches'][:5]: # 처음 5개만 표시
print(f" - {branch}")
if len(step['branches']) > 5:
print(f" ... and {len(step['branches']) - 5} more")
if not dry_run:
print("\n⚡ Executing migration...")
self._execute_migration_plan(migration_plan)
else:
print("\n🔍 Dry run completed (no changes made)")
# 마이그레이션 가이드 생성
self._generate_migration_guide()
def _execute_migration_plan(self, plan: List[Dict]):
"""마이그레이션 계획 실행"""
for step in plan:
if step['action'] == 'merge_features':
for branch in step['branches']:
print(f"Merging {branch}...")
# 실제 병합 로직
elif step['action'] == 'merge_develop':
print("Merging develop into main...")
# develop 병합 로직
elif step['action'] == 'cleanup_branches':
for branch in step['branches']:
print(f"Removing {branch}...")
# 브랜치 삭제 로직
elif step['action'] == 'update_settings':
print("Updating repository settings...")
self._update_repo_settings_for_tbd()
def _update_repo_settings_for_tbd(self):
"""TBD를 위한 저장소 설정 업데이트"""
# .github/settings.yml 생성
settings = """
# Trunk-Based Development Settings
branches:
- name: main
protection:
required_status_checks:
strict: true
contexts:
- continuous-integration
- tests
enforce_admins: false
required_pull_request_reviews:
required_approving_review_count: 1
dismiss_stale_reviews: true
require_code_owner_reviews: true
restrictions: null
allow_force_pushes: false
allow_deletions: false
labels:
- name: feature-flag
color: "1d76db"
description: "Feature flag required"
- name: ready-to-merge
color: "0e8a16"
description: "PR is ready for merge"
- name: do-not-merge
color: "d93f0b"
description: "PR should not be merged yet"
# Auto-delete head branches
delete_branch_on_merge: true
"""
os.makedirs('.github', exist_ok=True)
with open('.github/settings.yml', 'w') as f:
f.write(settings)
def _generate_migration_guide(self):
"""마이그레이션 가이드 생성"""
guide = """
# Trunk-Based Development Migration Guide
## New Workflow
1. **Creating Changes**
```bash
git checkout main
git pull origin main
git checkout -b short-lived-branch
# Make changes (small, incremental)
git commit -m "feat: add new feature"
git push origin short-lived-branch
- Feature Flags
- All new features must be behind feature flags
- Use the feature flag service for rollout control
- Remove flags only after full rollout
- Branch Lifetime
- Branches should live < 24 hours
- Large features: use Branch by Abstraction
- No long-lived feature branches
- Code Review
- PRs must be small (< 400 lines)
- Reviews should happen within 2 hours
- Use pair programming for complex changes
- Deployment
- Main branch is always deployable
- Automated deployment on merge
- Rollback via feature flags, not reverts
Tools and Scripts
./scripts/feature-flag.sh- Feature flag management./scripts/quick-pr.sh- Quick PR creation./scripts/health-check.sh- TBD health metrics
Resources
- Trunk-Based Development
- Feature Flags Best Practices
-
1 2 3 4
with open('TBD_MIGRATION_GUIDE.md', 'w') as f: f.write(guide) print("\n📚 Migration guide saved to TBD_MIGRATION_GUIDE.md")
사용 예제
if name == “main”: migration = BranchStrategyMigration()
1
2
3
4
5
6
# 현재 구조 분석
current = migration.analyze_current_structure()
print(f"Current branch strategy: {current['current_strategy']}")
# TBD로 마이그레이션 (dry run)
migration.migrate_to_trunk_based(dry_run=True) ```
마무리
고급 브랜치 전략과 릴리스 관리는 팀의 규모와 프로젝트의 특성에 따라 선택해야 합니다. 각 전략은 장단점이 있으며, 성공적인 구현을 위해서는 자동화와 팀의 규율이 필수적입니다.
핵심 포인트:
- 프로젝트 특성에 맞는 전략 선택
- 자동화를 통한 일관성 확보
- Feature Flags로 위험 관리
- 지속적인 모니터링과 개선
- 팀 교육과 문화 변화
올바른 브랜치 전략은 개발 속도를 높이고, 품질을 보장하며, 팀의 협업을 원활하게 만듭니다.
다음 심화편에서는 GitHub GraphQL API 마스터하기에 대해 다루겠습니다.
📚 GitHub 마스터하기 시리즈
🌱 기초편 (입문자)
💼 실전편 (중급자)
🚀 고급편 (전문가)
- GitHub Actions 입문
- Actions 고급 활용
- Webhooks와 API
- GitHub Apps 개발
- 보안 기능
- GitHub Packages
- Codespaces
- GitHub CLI
- 통계와 인사이트
🏆 심화편 (전문가+)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.