project-auto-wizard 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -2
- package/package.json +2 -2
- package/payload/scripts/changelog_manager.py +102 -4
- package/payload/scripts/version_manager.py +16 -2
- package/payload/version.yml.template +5 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +98 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +47 -10
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -15
- package/src/cli/args.js +5 -0
- package/src/cli/help.js +7 -1
- package/src/commands/doctor.js +73 -0
- package/src/commands/dry-run.js +77 -0
- package/src/commands/full.js +3 -2
- package/src/commands/interactive.js +13 -0
- package/src/commands/revert.js +22 -21
- package/src/commands/status.js +64 -0
- package/src/commands/version.js +3 -2
- package/src/context.js +1 -0
- package/src/core/copy/workflows.js +56 -0
- package/src/core/version-yml.js +10 -3
- package/src/index.js +36 -3
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> **One command DevOps** — `npx` 한 줄로 어떤 프로젝트든 GitHub-native AI 릴리스 자동화를 설치하는 마법사
|
|
4
4
|
|
|
5
|
+
- 새 프로젝트를 시작하면 코드를 작성하기 전부터 버전 관리, 배포 자동화, 변경 기록 작성처럼 먼저 정해야 할 일이 많다. project-auto-wizard는 이 준비 과정을 명령 한 번으로 구성하고, 프로젝트 상태 관리가 GitHub 안에서 자동으로 동작하도록 해 개발자가 기능 구현에 집중하도록 돕는 오픈소스 도구
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
npx project-auto-wizard
|
|
7
9
|
```
|
|
@@ -14,7 +16,7 @@ npx project-auto-wizard
|
|
|
14
16
|
[](package.json)
|
|
15
17
|
|
|
16
18
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
17
|
-
## 최신 버전 : v0.1.
|
|
19
|
+
## 최신 버전 : v0.1.6 (2026-07-27)
|
|
18
20
|
|
|
19
21
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
20
22
|
|
|
@@ -45,6 +47,32 @@ npx project-auto-wizard --mode full --force --type spring,react # CI에서 비
|
|
|
45
47
|
- **멀티타입**: `--type spring,react,python` — 한 레포에 여러 타입 공존
|
|
46
48
|
- **모노레포**: `--paths "flutter=app,react=client"` — 타입별 서브폴더 지정 (마커 파일 자동 감지)
|
|
47
49
|
|
|
50
|
+
### 질문 문구 커스터마이징
|
|
51
|
+
|
|
52
|
+
마법사가 묻는 질문의 라벨·도움말·예시 문구는 `.github/config/wizard-prompts.yml`을 만들어 재정의할 수 있습니다(설치되지 않는 파일이라 직접 만들어야 합니다). 타입별로 다른 문구를 쓰고 싶으면 `{type}.KEY` 형태로 오버라이드합니다.
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
PROJECT_NAME:
|
|
56
|
+
label: "프로젝트 이름이 뭔가요?"
|
|
57
|
+
help: "GitHub 레포 이름과 다르게 표시하고 싶을 때만 입력하세요."
|
|
58
|
+
|
|
59
|
+
flutter.APP_ARTIFACT_NAME:
|
|
60
|
+
label: "Flutter 앱 아티팩트 이름"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 타입별 워크플로우 구성
|
|
64
|
+
|
|
65
|
+
`spring`/`flutter`는 아래처럼 단일 CI 이상으로 깊게 구성되어 있습니다:
|
|
66
|
+
|
|
67
|
+
- **flutter**: Android(Firebase/Playstore/Selfhosted/TestAPK 배포), iOS(TestFlight/Test-TestFlight), CI, Lab 트리거까지 8종
|
|
68
|
+
- **spring**: 무중단 배포 2종(Nginx/Traefik) + 단일 서버 배포 + PR 프리뷰 + Nexus publish(opt-in)
|
|
69
|
+
- **react/next**: CI와 CI+CD 분리 구성
|
|
70
|
+
- **python**: CI / PR 프리뷰 / SimpleCICD
|
|
71
|
+
|
|
72
|
+
### 되돌리기(`--mode revert`)
|
|
73
|
+
|
|
74
|
+
`npx project-auto-wizard --mode revert`는 payload가 설치한 파일명과 **정확히 일치하는 것만** 제거합니다. 사용자가 직접 만든 워크플로우, `version.yml`, `README.md`, `.gitignore`는 건드리지 않습니다. 설치 시 충돌 처리로 생성된 `.bak`/`.template.yaml` 파생 파일도 함께 정리됩니다.
|
|
75
|
+
|
|
48
76
|
## API 키 0개 AI — 요약 엔진 체인
|
|
49
77
|
|
|
50
78
|
릴리스 노트는 4단 엔진 체인으로 생성됩니다. **어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
|
|
@@ -83,7 +111,7 @@ flowchart LR
|
|
|
83
111
|
```
|
|
84
112
|
npx project-auto-wizard [옵션]
|
|
85
113
|
|
|
86
|
-
-m, --mode MODE full | version | workflows (기본: 대화형)
|
|
114
|
+
-m, --mode MODE full | version | workflows | revert | status | doctor (기본: 대화형)
|
|
87
115
|
-t, --type CSV spring,react,... (미지정 시 자동 감지)
|
|
88
116
|
--project-version V 초기 버전 (미지정 시 자동 감지)
|
|
89
117
|
--paths "t=p,..." 모노레포 타입별 경로
|
|
@@ -92,9 +120,44 @@ npx project-auto-wizard [옵션]
|
|
|
92
120
|
--nexus Nexus 라이브러리 publish 워크플로우 포함
|
|
93
121
|
--secret-backup Secret 서버 백업 워크플로우 포함
|
|
94
122
|
--coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로
|
|
123
|
+
--semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함, --no-semver-auto로 끔)
|
|
124
|
+
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌
|
|
95
125
|
--force 전 질문 생략 (CI용)
|
|
96
126
|
```
|
|
97
127
|
|
|
128
|
+
## 설치 상태 확인 · 진단 · 미리보기
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx project-auto-wizard --mode status # 설치 상태·드리프트 확인 (읽기 전용)
|
|
132
|
+
npx project-auto-wizard --mode doctor # 환경 진단 (읽기 전용, 규칙 기반)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| 명령 | 내용 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `--mode status` | 설치된 버전·타입·브랜치 모드·옵션값과, 설치 시점 대비 사용자가 직접 수정한 워크플로우 파일 목록을 보여줍니다. 네트워크 접근 없음(로컬 파일 비교만) |
|
|
138
|
+
| `--mode doctor` | `version.yml` 설치 여부, `gh` CLI 설치/인증 상태, GitHub Actions workflow permissions, `WORKFLOW_PAT` secret 등록 여부, merge commit 허용 설정을 점검합니다. `gh api` 호출을 사용하므로 네트워크 접근이 발생합니다(규칙 기반 점검 — AI 진단 아님) |
|
|
139
|
+
|
|
140
|
+
`--dry-run`을 어떤 모드와도 함께 쓰면 실제로 파일을 바꾸지 않고 무엇이 바뀔지만 미리 보여줍니다(`full`/`version`/`workflows`/`revert` 전체 지원):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx project-auto-wizard --mode full --force --type node --dry-run
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 자동 semver 승격 (`--semver-auto`)
|
|
147
|
+
|
|
148
|
+
기본적으로 켜져 있습니다. 커밋 메시지 컨벤션(`feat:` → minor, `!` 브레이킹 마커 → major, 그 외 → patch)을 기반으로 다음 버전을 자동으로 계산합니다. 분류가 애매한 커밋은 AI 엔진 체인이 patch→minor 승격 여부를 판단합니다. 끄면 기존과 동일하게 항상 patch+1입니다.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx project-auto-wizard --semver-auto # 기본값, 명시 지정도 가능
|
|
152
|
+
npx project-auto-wizard --no-semver-auto # 항상 patch+1 (레거시 동작)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 자체 AI PR 요약봇
|
|
156
|
+
|
|
157
|
+
CodeRabbit을 쓰지 않는 레포를 위한 대안입니다. `--coderabbit`을 켜지 않으면(기본값) 릴리스 브랜치(`--main-branch`)를 대상으로 하는 PR이 열릴 때 API 키 0개 AI 엔진 체인으로 요약 코멘트를 자동으로 답니다. `--coderabbit`을 켜면 CodeRabbit이 PR 요약을 전담하고 이 봇은 no-op으로 빠집니다(중복 방지, 상호 배타적).
|
|
158
|
+
|
|
159
|
+
기본 설치(pr-flow) 기준으로 일상적인 기능 PR은 `develop`을 대상으로 열리므로, 이 봇은 develop→main 릴리스 PR에서만 실제로 동작합니다 — 해당 PR에서는 `AUTO-CHANGELOG-CONTROL`이 이미 같은 엔진으로 체인지로그 요약을 생성하므로, 이 봇은 그 요약을 PR 코멘트 형태로도 남겨주는 보조 역할입니다. 릴리스 브랜치 = 개발 브랜치인 trunk-based 모드에서는 모든 PR이 곧 릴리스 대상 브랜치를 향하므로 매 PR마다 동작합니다.
|
|
160
|
+
|
|
98
161
|
## 설치 후 확인할 것
|
|
99
162
|
|
|
100
163
|
| 항목 | 내용 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-auto-wizard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "One command DevOps: npx wizard that installs GitHub-native AI Release Automation into any project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "npm run test:node && npm run test:py",
|
|
20
20
|
"test:node": "node --test \"tests/node/**/*.test.js\"",
|
|
21
|
-
"test:py": "
|
|
21
|
+
"test:py": "python3 -m unittest discover -s tests/py -v"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
@@ -252,6 +252,12 @@ _TIER2_BUCKET_MAP = {
|
|
|
252
252
|
|
|
253
253
|
_FALLBACK_BUCKET_KEYS = ('feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'changes')
|
|
254
254
|
|
|
255
|
+
# 승격 폭 판단 전용 — 타입 뒤 `!` 마커(어떤 타입이든)는 breaking 신호.
|
|
256
|
+
# BREAKING CHANGE: 본문 푸터는 지원하지 않는다(커밋 수집이 제목 한 줄만
|
|
257
|
+
# 가져오는 구조라 본문에 접근 불가 — Conventional Commits 스펙 조항 13에
|
|
258
|
+
# 따르면 `!` 마커 단독으로도 표준을 만족하므로 이는 표준이 허용하는 부분집합).
|
|
259
|
+
_BREAKING_MARKER_RE = re.compile(r'^[a-zA-Z]+(\([^)]*\))?!:')
|
|
260
|
+
|
|
255
261
|
|
|
256
262
|
def classify_commits(lines: list[str]) -> dict:
|
|
257
263
|
"""
|
|
@@ -335,6 +341,81 @@ def render_fallback_md(classified: dict, version: str) -> str:
|
|
|
335
341
|
return "\n".join(lines).rstrip() + "\n"
|
|
336
342
|
|
|
337
343
|
|
|
344
|
+
def classify_bump_level(lines: list[str]) -> str:
|
|
345
|
+
"""커밋 제목 목록에서 semver 승격 폭을 규칙 기반으로 판단.
|
|
346
|
+
|
|
347
|
+
- 타입 뒤 `!` 마커 포함 -> major
|
|
348
|
+
- `feat:`(classify_commits의 feat 버킷과 동일 판정 기준) 포함 -> minor
|
|
349
|
+
- 그 외(매칭 실패 포함) -> patch
|
|
350
|
+
"""
|
|
351
|
+
for raw_line in lines:
|
|
352
|
+
line = raw_line.strip()
|
|
353
|
+
if not line or '[skip ci]' in line or line.startswith('Merge '):
|
|
354
|
+
continue
|
|
355
|
+
if _BREAKING_MARKER_RE.match(line):
|
|
356
|
+
return 'major'
|
|
357
|
+
classified = classify_commits(lines)
|
|
358
|
+
return 'minor' if classified.get('feat') else 'patch'
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
_BUMP_AI_PROMPT_PREFIX = (
|
|
362
|
+
"다음은 정해진 커밋 컨벤션을 따르지 않는 자유형식 커밋 메시지들이다.\n"
|
|
363
|
+
"이 중 사용자 대상 새로운 기능(feature) 추가로 보이는 것이 하나라도 있으면 정확히 MINOR라고만 답하고,\n"
|
|
364
|
+
"없으면 정확히 PATCH라고만 답해라. 다른 말은 절대 덧붙이지 마라.\n"
|
|
365
|
+
"커밋 목록:\n"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _ai_assisted_minor_upgrade(unclassified_lines: list[str]) -> bool:
|
|
370
|
+
"""규칙 분류가 patch일 때, 미분류 자유형식 커밋에 한해 AI에게 minor 업그레이드
|
|
371
|
+
여부만 보조 판단시킨다. AI는 절대 major를 만들 수 없다 — major는 항상 명시적
|
|
372
|
+
`!` 마커만 신뢰한다(classify_bump_level에서 이미 확정됨). 응답이 정확히
|
|
373
|
+
'MINOR'가 아니거나 호출이 실패하면 무조건 False(규칙 결과 patch 유지)."""
|
|
374
|
+
if not unclassified_lines:
|
|
375
|
+
return False
|
|
376
|
+
prompt = _BUMP_AI_PROMPT_PREFIX + "\n".join(f"- {line}" for line in unclassified_lines)
|
|
377
|
+
|
|
378
|
+
ai_api_key = os.environ.get('AI_API_KEY')
|
|
379
|
+
github_token = os.environ.get('GITHUB_TOKEN')
|
|
380
|
+
candidates = [
|
|
381
|
+
(ai_api_key, os.environ.get('AI_API_BASE_URL') or _AI_DEFAULT_BASE_URL, os.environ.get('AI_MODEL') or _AI_DEFAULT_MODEL),
|
|
382
|
+
(github_token, _AI_DEFAULT_BASE_URL, _AI_DEFAULT_MODEL),
|
|
383
|
+
]
|
|
384
|
+
for token, base_url, model in candidates:
|
|
385
|
+
if not token:
|
|
386
|
+
continue
|
|
387
|
+
try:
|
|
388
|
+
response = call_openai_compatible(base_url, token, model, prompt)
|
|
389
|
+
return response.strip() == 'MINOR'
|
|
390
|
+
except Exception as e:
|
|
391
|
+
print(f"[warn] bump AI assist failed: {e}", file=sys.stderr)
|
|
392
|
+
continue
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def cmd_classify_bump(commits_file: str) -> int:
|
|
397
|
+
"""커밋 목록 파일을 읽어 semver 승격 폭(major/minor/patch)을 stdout 마지막 줄에 출력.
|
|
398
|
+
|
|
399
|
+
규칙 우선(feat->minor, !마커->major, 그외->patch). 규칙 결과가 patch이고
|
|
400
|
+
분류 안 된 자유형식 커밋이 있으면, AI에게 patch->minor 업그레이드 여부만
|
|
401
|
+
보조 판단시킨다(major는 AI가 절대 만들 수 없음).
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
with open(commits_file, 'r', encoding='utf-8') as f:
|
|
405
|
+
commit_lines = [line.rstrip('\n').rstrip('\r') for line in f]
|
|
406
|
+
except Exception:
|
|
407
|
+
commit_lines = []
|
|
408
|
+
|
|
409
|
+
bump = classify_bump_level(commit_lines)
|
|
410
|
+
if bump == 'patch':
|
|
411
|
+
classified = classify_commits(commit_lines)
|
|
412
|
+
if _ai_assisted_minor_upgrade(classified.get('changes') or []):
|
|
413
|
+
bump = 'minor'
|
|
414
|
+
|
|
415
|
+
print(bump)
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
338
419
|
# ------------------------ 서브커맨드 구현부 ------------------------
|
|
339
420
|
|
|
340
421
|
def cmd_update_from_summary() -> int:
|
|
@@ -580,7 +661,7 @@ _AI_DEFAULT_BASE_URL = "https://models.github.ai/inference"
|
|
|
580
661
|
_AI_DEFAULT_MODEL = "openai/gpt-4o-mini"
|
|
581
662
|
|
|
582
663
|
|
|
583
|
-
def _build_ai_prompt(commit_lines: list[str], pr_title: str | None, version: str) -> str:
|
|
664
|
+
def _build_ai_prompt(commit_lines: list[str], pr_title: str | None, version: str, diff_stat: str | None = None) -> str:
|
|
584
665
|
"""AI에게 보낼 한국어 릴리즈 요약 프롬프트를 구성.
|
|
585
666
|
|
|
586
667
|
요청하는 출력 형식은 규칙 기반 폴백 렌더러(render_fallback_md)와 동일한
|
|
@@ -596,6 +677,9 @@ def _build_ai_prompt(commit_lines: list[str], pr_title: str | None, version: str
|
|
|
596
677
|
]
|
|
597
678
|
if pr_title:
|
|
598
679
|
parts.append(f"PR 제목: {pr_title}")
|
|
680
|
+
if diff_stat and diff_stat.strip():
|
|
681
|
+
parts.append("파일별 변경 요약:")
|
|
682
|
+
parts.append(diff_stat.strip())
|
|
599
683
|
parts.append("커밋 목록:")
|
|
600
684
|
parts.extend(f"- {line}" for line in commit_lines)
|
|
601
685
|
return "\n".join(parts)
|
|
@@ -623,7 +707,7 @@ def call_openai_compatible(base_url: str, token: str, model: str, prompt: str) -
|
|
|
623
707
|
return body["choices"][0]["message"]["content"]
|
|
624
708
|
|
|
625
709
|
|
|
626
|
-
def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title: str | None) -> int:
|
|
710
|
+
def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title: str | None, diff_stat_file: str | None = None) -> int:
|
|
627
711
|
"""커밋 목록을 읽어 AI(우선) 또는 규칙 기반 폴백으로 릴리즈 요약을 생성."""
|
|
628
712
|
try:
|
|
629
713
|
with open(commits_file, 'r', encoding='utf-8') as f:
|
|
@@ -631,6 +715,14 @@ def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title:
|
|
|
631
715
|
except Exception:
|
|
632
716
|
commit_lines = []
|
|
633
717
|
|
|
718
|
+
diff_stat = None
|
|
719
|
+
if diff_stat_file:
|
|
720
|
+
try:
|
|
721
|
+
with open(diff_stat_file, 'r', encoding='utf-8') as f:
|
|
722
|
+
diff_stat = f.read()
|
|
723
|
+
except Exception:
|
|
724
|
+
diff_stat = None
|
|
725
|
+
|
|
634
726
|
ai_api_key = os.environ.get('AI_API_KEY')
|
|
635
727
|
ai_base_url = os.environ.get('AI_API_BASE_URL') or _AI_DEFAULT_BASE_URL
|
|
636
728
|
ai_model = os.environ.get('AI_MODEL') or _AI_DEFAULT_MODEL
|
|
@@ -638,7 +730,7 @@ def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title:
|
|
|
638
730
|
|
|
639
731
|
engine = None
|
|
640
732
|
summary_text = None
|
|
641
|
-
prompt = _build_ai_prompt(commit_lines, pr_title, version)
|
|
733
|
+
prompt = _build_ai_prompt(commit_lines, pr_title, version, diff_stat)
|
|
642
734
|
|
|
643
735
|
if ai_api_key:
|
|
644
736
|
try:
|
|
@@ -699,6 +791,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
699
791
|
sub.add_parser('update-from-summary', help='PR body에서 CHANGELOG.json 갱신')
|
|
700
792
|
sub.add_parser('generate-md', help='CHANGELOG.json → CHANGELOG.md 생성')
|
|
701
793
|
|
|
794
|
+
p_classify_bump = sub.add_parser('classify-bump', help='커밋 목록으로 semver 승격 폭(major/minor/patch) 판단')
|
|
795
|
+
p_classify_bump.add_argument('--commits-file', required=True, help='커밋 제목 목록 파일 (한 줄당 1개)')
|
|
796
|
+
|
|
702
797
|
p_export = sub.add_parser('export', help='특정 버전 릴리즈 노트 추출')
|
|
703
798
|
p_export.add_argument('--version', required=True, help='버전 번호')
|
|
704
799
|
p_export.add_argument('--output', help='출력 파일 경로 (없으면 stdout)')
|
|
@@ -708,6 +803,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
708
803
|
p_ai_summary.add_argument('--version', required=True, help='버전 번호')
|
|
709
804
|
p_ai_summary.add_argument('--output', required=True, help='요약 결과를 저장할 파일 경로')
|
|
710
805
|
p_ai_summary.add_argument('--pr-title', help='PR 제목 (프롬프트 컨텍스트로 사용, 선택)')
|
|
806
|
+
p_ai_summary.add_argument('--diff-stat-file', help='git diff --stat 출력 파일 (프롬프트 컨텍스트 확장, 선택)')
|
|
711
807
|
|
|
712
808
|
args = parser.parse_args(argv)
|
|
713
809
|
|
|
@@ -717,8 +813,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
717
813
|
return cmd_generate_md()
|
|
718
814
|
if args.command == 'export':
|
|
719
815
|
return cmd_export_release_notes(args.version, args.output)
|
|
816
|
+
if args.command == 'classify-bump':
|
|
817
|
+
return cmd_classify_bump(args.commits_file)
|
|
720
818
|
if args.command == 'ai-summary':
|
|
721
|
-
return cmd_ai_summary(args.commits_file, args.version, args.output, args.pr_title)
|
|
819
|
+
return cmd_ai_summary(args.commits_file, args.version, args.output, args.pr_title, args.diff_stat_file)
|
|
722
820
|
return 2
|
|
723
821
|
|
|
724
822
|
|
|
@@ -246,6 +246,17 @@ def increment_patch(version):
|
|
|
246
246
|
return f"{major}.{minor}.{int(patch) + 1}"
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
def increment_version(version, bump="patch"):
|
|
250
|
+
"""bump: 'major'|'minor'|'patch'. 생략하면 기존과 동일하게 patch(increment_patch)로 동작."""
|
|
251
|
+
if bump == "major":
|
|
252
|
+
major, _minor, _patch = version.split(".")
|
|
253
|
+
return f"{int(major) + 1}.0.0"
|
|
254
|
+
if bump == "minor":
|
|
255
|
+
major, minor, _patch = version.split(".")
|
|
256
|
+
return f"{major}.{int(minor) + 1}.0"
|
|
257
|
+
return increment_patch(version)
|
|
258
|
+
|
|
259
|
+
|
|
249
260
|
def compare_versions(v1, v2):
|
|
250
261
|
"""Return 1 if v1>v2, -1 if v1<v2, 0 if equal."""
|
|
251
262
|
p1 = [int(x) for x in v1.split(".")]
|
|
@@ -553,7 +564,8 @@ def cmd_increment(args):
|
|
|
553
564
|
if not validate_version(current_version):
|
|
554
565
|
log(f"ERROR: invalid version format: {current_version}")
|
|
555
566
|
return 1
|
|
556
|
-
|
|
567
|
+
bump = getattr(args, "bump", None) or "patch"
|
|
568
|
+
new_version = increment_version(current_version, bump)
|
|
557
569
|
update_all_versions(new_version)
|
|
558
570
|
|
|
559
571
|
current_code = int(get_version_code())
|
|
@@ -587,7 +599,9 @@ def build_parser():
|
|
|
587
599
|
|
|
588
600
|
sub.add_parser("get")
|
|
589
601
|
sub.add_parser("get-code")
|
|
590
|
-
sub.add_parser("increment")
|
|
602
|
+
p_increment = sub.add_parser("increment")
|
|
603
|
+
p_increment.add_argument("--bump", choices=["major", "minor", "patch"], default="patch",
|
|
604
|
+
help="승격 폭 (기본 patch — 지정 안 하면 기존 동작과 동일)")
|
|
591
605
|
sub.add_parser("increment-code")
|
|
592
606
|
sub.add_parser("sync")
|
|
593
607
|
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
# Version format: x.y.z (semantic versioning)
|
|
3
3
|
# - patch: auto-incremented by the release workflows
|
|
4
4
|
# - version_code: monotonically increasing build number, bumped alongside version
|
|
5
|
-
# - major/minor:
|
|
6
|
-
# (
|
|
5
|
+
# - major/minor: auto-bumped when metadata.template.options.semver_auto is true
|
|
6
|
+
# (default) — feat: commits bump minor, a `!` breaking marker on any commit
|
|
7
|
+
# type bumps major. Set semver_auto: false to always patch-bump instead
|
|
8
|
+
# (then edit major/minor manually, e.g., 1.0.5 -> 1.1.0 or 2.0.0).
|
|
7
9
|
# ===
|
|
8
10
|
# Supported project types: spring, flutter, next, react, react-native,
|
|
9
11
|
# react-native-expo, node, python, basic
|
|
@@ -45,4 +47,5 @@ metadata:
|
|
|
45
47
|
nexus: {{OPT_NEXUS}}
|
|
46
48
|
secret_backup: {{OPT_SECRET_BACKUP}}
|
|
47
49
|
coderabbit: {{OPT_CODERABBIT}}
|
|
50
|
+
semver_auto: {{OPT_SEMVER_AUTO}}
|
|
48
51
|
{{DEPLOY}}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ===================================================================
|
|
2
|
+
# PROJECT-COMMON-AI-PR-SUMMARY.yaml
|
|
3
|
+
# Self-hosted AI PR summary bot — CodeRabbit-independent alternative.
|
|
4
|
+
# ===================================================================
|
|
5
|
+
#
|
|
6
|
+
# Posts an AI-generated summary comment on PRs targeting the default
|
|
7
|
+
# branch, using the same zero-API-key engine chain as the release
|
|
8
|
+
# changelog (user API key -> GitHub Models -> rule-based fallback).
|
|
9
|
+
# The prompt includes commit subjects plus a capped `git diff --stat`.
|
|
10
|
+
#
|
|
11
|
+
# Mutually exclusive with CodeRabbit by design: if version.yml
|
|
12
|
+
# metadata.template.options.coderabbit is true, this workflow no-ops
|
|
13
|
+
# immediately — CodeRabbit already covers PR summaries, and posting
|
|
14
|
+
# both on the same PR would be redundant/confusing.
|
|
15
|
+
#
|
|
16
|
+
# Independent from PROJECT-COMMON-AUTO-CHANGELOG-CONTROL: on release
|
|
17
|
+
# PRs both workflows may call the AI engine once each, and when
|
|
18
|
+
# semver_auto is on, AUTO-CHANGELOG-CONTROL's bump classification may
|
|
19
|
+
# add a third AI-assisted call to resolve an ambiguous patch->minor
|
|
20
|
+
# escalation (up to 3 LLM calls total). This is an accepted trade-off
|
|
21
|
+
# for simplicity — GitHub Models is free-tier and rule-based fallback
|
|
22
|
+
# is instant either way.
|
|
23
|
+
# ===================================================================
|
|
24
|
+
|
|
25
|
+
name: PROJECT-AI-PR-SUMMARY
|
|
26
|
+
|
|
27
|
+
on:
|
|
28
|
+
pull_request:
|
|
29
|
+
types: [opened, synchronize, reopened]
|
|
30
|
+
branches: ["{{MAIN_BRANCH}}"]
|
|
31
|
+
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
pull-requests: write
|
|
35
|
+
models: read
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
ai-pr-summary:
|
|
39
|
+
name: Post AI summary comment
|
|
40
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout PR head
|
|
44
|
+
uses: actions/checkout@v5
|
|
45
|
+
with:
|
|
46
|
+
fetch-depth: 0
|
|
47
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
48
|
+
|
|
49
|
+
- name: Read coderabbit option from version.yml
|
|
50
|
+
id: options
|
|
51
|
+
run: |
|
|
52
|
+
CODERABBIT=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?coderabbit:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
53
|
+
echo "coderabbit=$CODERABBIT" >> $GITHUB_OUTPUT
|
|
54
|
+
|
|
55
|
+
- name: Generate and post AI summary
|
|
56
|
+
if: steps.options.outputs.coderabbit != 'true'
|
|
57
|
+
env:
|
|
58
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
59
|
+
AI_API_KEY: ${{ secrets.AI_API_KEY }}
|
|
60
|
+
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
61
|
+
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
62
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
63
|
+
run: |
|
|
64
|
+
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
65
|
+
VERSION=$(python3 .github/scripts/version_manager.py get 2>/dev/null | tail -n 1)
|
|
66
|
+
VERSION=${VERSION:-unreleased}
|
|
67
|
+
|
|
68
|
+
git fetch origin {{MAIN_BRANCH}}
|
|
69
|
+
git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
|
|
70
|
+
git diff --stat "origin/{{MAIN_BRANCH}}...HEAD" > _full_diff_stat.txt
|
|
71
|
+
if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
|
|
72
|
+
{ head -49 _full_diff_stat.txt; tail -1 _full_diff_stat.txt; } > diff_stat.txt
|
|
73
|
+
else
|
|
74
|
+
cp _full_diff_stat.txt diff_stat.txt
|
|
75
|
+
fi
|
|
76
|
+
rm -f _full_diff_stat.txt
|
|
77
|
+
|
|
78
|
+
python3 .github/scripts/changelog_manager.py ai-summary \
|
|
79
|
+
--commits-file commits.txt \
|
|
80
|
+
--version "$VERSION" \
|
|
81
|
+
--output summary.md \
|
|
82
|
+
--pr-title "$PR_TITLE" \
|
|
83
|
+
--diff-stat-file diff_stat.txt
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
echo "🤖 **AI Summary (project-auto-wizard)**"
|
|
87
|
+
echo ""
|
|
88
|
+
cat summary.md
|
|
89
|
+
} > comment_body.md
|
|
90
|
+
|
|
91
|
+
python3 -c 'import json; print(json.dumps({"body": open("comment_body.md", encoding="utf-8").read()}))' > comment_payload.json
|
|
92
|
+
|
|
93
|
+
curl -sf -H "Authorization: token ${{ github.token }}" \
|
|
94
|
+
-H "Content-Type: application/json" \
|
|
95
|
+
-X POST \
|
|
96
|
+
-d @comment_payload.json \
|
|
97
|
+
"https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" > /dev/null \
|
|
98
|
+
|| echo "comment post failed — continuing (this workflow never blocks the PR)"
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
# ===================================================================
|
|
5
5
|
#
|
|
6
6
|
# Runs on release PRs ({{DEVELOP_BRANCH}} -> {{MAIN_BRANCH}}) and:
|
|
7
|
-
# 1. Confirms the release version
|
|
7
|
+
# 1. Confirms the release version and syncs every version file:
|
|
8
|
+
# - If version.yml metadata.template.options.semver_auto is true
|
|
9
|
+
# (default): classifies commits (feat -> minor, `!` marker -> major,
|
|
10
|
+
# else patch; AI-assisted patch->minor upgrade for unclassified
|
|
11
|
+
# commits) and bumps accordingly.
|
|
12
|
+
# - Otherwise: always patch+1 (legacy behavior).
|
|
8
13
|
# 2. Acquires a release summary:
|
|
9
14
|
# - If version.yml metadata.template.options.coderabbit is true:
|
|
10
15
|
# requests a CodeRabbit summary comment once and polls the PR body
|
|
@@ -12,6 +17,9 @@
|
|
|
12
17
|
# - Otherwise (or on poll timeout): generates the summary locally with
|
|
13
18
|
# changelog_manager.py ai-summary (engine chain: user API key ->
|
|
14
19
|
# GitHub Models -> rule-based fallback; always succeeds)
|
|
20
|
+
# - The AI prompt includes commit subjects plus a capped
|
|
21
|
+
# `git diff --stat` (file-level change summary, no diff body)
|
|
22
|
+
# for richer context without token-cost blowup.
|
|
15
23
|
# 3. Updates CHANGELOG.json / CHANGELOG.md and commits to the PR head
|
|
16
24
|
# branch with [skip ci]
|
|
17
25
|
# 4. Enables automerge (gh pr merge --auto --merge)
|
|
@@ -81,6 +89,27 @@ jobs:
|
|
|
81
89
|
echo "coderabbit=$CODERABBIT" >> $GITHUB_OUTPUT
|
|
82
90
|
echo "coderabbit option: $CODERABBIT"
|
|
83
91
|
|
|
92
|
+
- name: Read semver_auto option from version.yml
|
|
93
|
+
id: semver_options
|
|
94
|
+
run: |
|
|
95
|
+
SEMVER_AUTO=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?semver_auto:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
96
|
+
echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
|
|
97
|
+
echo "semver_auto option: $SEMVER_AUTO"
|
|
98
|
+
|
|
99
|
+
- name: Collect commits since last release
|
|
100
|
+
id: collect
|
|
101
|
+
run: |
|
|
102
|
+
git fetch origin {{MAIN_BRANCH}}
|
|
103
|
+
git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
|
|
104
|
+
git diff --stat "origin/{{MAIN_BRANCH}}...HEAD" > _full_diff_stat.txt
|
|
105
|
+
if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
|
|
106
|
+
{ head -49 _full_diff_stat.txt; tail -1 _full_diff_stat.txt; } > diff_stat.txt
|
|
107
|
+
else
|
|
108
|
+
cp _full_diff_stat.txt diff_stat.txt
|
|
109
|
+
fi
|
|
110
|
+
rm -f _full_diff_stat.txt
|
|
111
|
+
echo "commits to summarize: $(wc -l < commits.txt)"
|
|
112
|
+
|
|
84
113
|
- name: Request CodeRabbit summary (PR-body-init protection)
|
|
85
114
|
id: coderabbit_request
|
|
86
115
|
if: steps.options.outputs.coderabbit == 'true'
|
|
@@ -162,8 +191,13 @@ jobs:
|
|
|
162
191
|
echo "summary_found=false" >> $GITHUB_OUTPUT
|
|
163
192
|
fi
|
|
164
193
|
|
|
165
|
-
- name: Confirm release version (
|
|
194
|
+
- name: Confirm release version (bump + sync)
|
|
166
195
|
id: bump
|
|
196
|
+
env:
|
|
197
|
+
AI_API_KEY: ${{ secrets.AI_API_KEY }}
|
|
198
|
+
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
199
|
+
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
200
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
167
201
|
run: |
|
|
168
202
|
# Idempotency: if the PR head is already this workflow's own
|
|
169
203
|
# version-confirm commit (re-run / synchronize retrigger), do not
|
|
@@ -173,13 +207,19 @@ jobs:
|
|
|
173
207
|
NEW_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
174
208
|
echo "version already confirmed by a previous run — reusing $NEW_VERSION (no re-increment)"
|
|
175
209
|
else
|
|
176
|
-
|
|
210
|
+
if [ "${{ steps.semver_options.outputs.semver_auto }}" = "true" ]; then
|
|
211
|
+
BUMP=$(python3 .github/scripts/changelog_manager.py classify-bump --commits-file commits.txt | tail -n 1)
|
|
212
|
+
else
|
|
213
|
+
BUMP="patch"
|
|
214
|
+
fi
|
|
215
|
+
echo "bump level: $BUMP"
|
|
216
|
+
NEW_VERSION=$(python3 .github/scripts/version_manager.py increment --bump "$BUMP" | tail -n 1)
|
|
177
217
|
if [ -z "$NEW_VERSION" ]; then
|
|
178
218
|
echo "version bump failed"
|
|
179
219
|
exit 1
|
|
180
220
|
fi
|
|
181
221
|
python3 .github/scripts/version_manager.py sync
|
|
182
|
-
echo "release version confirmed: $NEW_VERSION"
|
|
222
|
+
echo "release version confirmed: $NEW_VERSION ($BUMP)"
|
|
183
223
|
fi
|
|
184
224
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
185
225
|
|
|
@@ -196,15 +236,12 @@ jobs:
|
|
|
196
236
|
run: |
|
|
197
237
|
VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
198
238
|
|
|
199
|
-
git fetch origin {{MAIN_BRANCH}}
|
|
200
|
-
git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
|
|
201
|
-
echo "commits to summarize: $(wc -l < commits.txt)"
|
|
202
|
-
|
|
203
239
|
python3 .github/scripts/changelog_manager.py ai-summary \
|
|
204
240
|
--commits-file commits.txt \
|
|
205
241
|
--version "$VERSION" \
|
|
206
242
|
--output summary.md \
|
|
207
|
-
--pr-title "$PR_TITLE"
|
|
243
|
+
--pr-title "$PR_TITLE" \
|
|
244
|
+
--diff-stat-file diff_stat.txt
|
|
208
245
|
|
|
209
246
|
# Feed the generated summary through the same channel the
|
|
210
247
|
# CodeRabbit path uses: update-from-summary reads ./pr_body.md
|
|
@@ -234,7 +271,7 @@ jobs:
|
|
|
234
271
|
VERSION="${{ steps.bump.outputs.new_version }}"
|
|
235
272
|
|
|
236
273
|
# Working files must not land in the repo
|
|
237
|
-
rm -f pr_body.md summary.md commits.txt
|
|
274
|
+
rm -f pr_body.md summary.md commits.txt diff_stat.txt
|
|
238
275
|
|
|
239
276
|
git add -A
|
|
240
277
|
|