project-auto-wizard 0.1.6 → 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 CHANGED
@@ -16,7 +16,7 @@ npx project-auto-wizard
16
16
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
17
17
 
18
18
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
19
- ## 최신 버전 : v0.1.4 (2026-07-09)
19
+ ## 최신 버전 : v0.1.6 (2026-07-27)
20
20
 
21
21
  [전체 버전 기록 보기](CHANGELOG.md)
22
22
 
@@ -47,6 +47,32 @@ npx project-auto-wizard --mode full --force --type spring,react # CI에서 비
47
47
  - **멀티타입**: `--type spring,react,python` — 한 레포에 여러 타입 공존
48
48
  - **모노레포**: `--paths "flutter=app,react=client"` — 타입별 서브폴더 지정 (마커 파일 자동 감지)
49
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
+
50
76
  ## API 키 0개 AI — 요약 엔진 체인
51
77
 
52
78
  릴리스 노트는 4단 엔진 체인으로 생성됩니다. **어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
@@ -85,7 +111,7 @@ flowchart LR
85
111
  ```
86
112
  npx project-auto-wizard [옵션]
87
113
 
88
- -m, --mode MODE full | version | workflows (기본: 대화형)
114
+ -m, --mode MODE full | version | workflows | revert | status | doctor (기본: 대화형)
89
115
  -t, --type CSV spring,react,... (미지정 시 자동 감지)
90
116
  --project-version V 초기 버전 (미지정 시 자동 감지)
91
117
  --paths "t=p,..." 모노레포 타입별 경로
@@ -94,9 +120,44 @@ npx project-auto-wizard [옵션]
94
120
  --nexus Nexus 라이브러리 publish 워크플로우 포함
95
121
  --secret-backup Secret 서버 백업 워크플로우 포함
96
122
  --coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로
123
+ --semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함, --no-semver-auto로 끔)
124
+ --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌
97
125
  --force 전 질문 생략 (CI용)
98
126
  ```
99
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
+
100
161
  ## 설치 후 확인할 것
101
162
 
102
163
  | 항목 | 내용 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.6",
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": "python -m unittest discover -s tests/py -v"
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
- new_version = increment_patch(current_version)
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: NOT bumped automatically edit this file manually
6
- # (e.g., 1.0.5 -> 1.1.0 or 2.0.0)
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 (patch +1 and sync of every version file)
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 (patch +1 and sync)
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
- NEW_VERSION=$(python3 .github/scripts/version_manager.py increment | tail -n 1)
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
 
@@ -13,10 +13,16 @@
13
13
  # exits cleanly. Version + changelog were already confirmed in the PR.
14
14
  #
15
15
  # - trunk-based: every direct push to {{MAIN_BRANCH}} is a release.
16
- # This job bumps the version (patch +1 and sync), summarizes the commits
17
- # since the last tag with the ai-summary engine chain (user API ->
18
- # GitHub Models -> rule-based fallback), updates CHANGELOG.json /
19
- # CHANGELOG.md, and commits everything back with [skip ci].
16
+ # This job bumps the version and syncs it, summarizes the commits since
17
+ # the last tag with the ai-summary engine chain (user API -> GitHub
18
+ # Models -> rule-based fallback, diff-stat-enriched prompt), updates
19
+ # CHANGELOG.json / CHANGELOG.md, and commits everything back with
20
+ # [skip ci]. Same as pr-flow's AUTO-CHANGELOG-CONTROL:
21
+ # - If version.yml metadata.template.options.semver_auto is true
22
+ # (default): classifies commits (feat -> minor, `!` marker -> major,
23
+ # else patch; AI-assisted patch->minor upgrade for unclassified
24
+ # commits) and bumps accordingly.
25
+ # - Otherwise: always patch+1 (legacy behavior).
20
26
  #
21
27
  # Common tail (both modes): recovery-aware idempotency guard (tag and
22
28
  # release checked separately so a half-published release can be finished
@@ -102,6 +108,19 @@ jobs:
102
108
  echo "not a release-confirm push in pr-flow mode — skipping (clean exit)"
103
109
  fi
104
110
 
111
+ - name: Read semver_auto option from version.yml
112
+ id: semver_options
113
+ if: >-
114
+ steps.gate.outputs.proceed == 'true' &&
115
+ steps.mode.outputs.mode == 'trunk-based' &&
116
+ github.event_name == 'push'
117
+ run: |
118
+ # scoped to the metadata: -> template: -> options: chain so an
119
+ # unrelated "semver_auto:" key can never hijack the value
120
+ 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")
121
+ echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
122
+ echo "semver_auto option: $SEMVER_AUTO"
123
+
105
124
  - name: Trunk-based version bump + changelog
106
125
  if: >-
107
126
  steps.gate.outputs.proceed == 'true' &&
@@ -123,28 +142,48 @@ jobs:
123
142
  exit 0
124
143
  fi
125
144
 
126
- NEW_VERSION=$(python3 .github/scripts/version_manager.py increment | tail -n 1)
127
- if [ -z "$NEW_VERSION" ]; then
128
- echo "version bump failed"
129
- exit 1
130
- fi
131
- python3 .github/scripts/version_manager.py sync
132
- echo "release version: $NEW_VERSION"
133
-
134
- # Commits since the last tag (fall back to full history on first release)
145
+ # Commits (and diff-stat) since the last tag — this is "what's in
146
+ # this release" for trunk-based pushes. Fall back to the full
147
+ # history / a diff against the empty tree on the first-ever
148
+ # release (no previous tag to diff against).
135
149
  LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
136
150
  if [ -n "$LAST_TAG" ]; then
137
151
  echo "collecting commits since $LAST_TAG"
138
152
  git log --pretty=%s "${LAST_TAG}..HEAD" > commits.txt
153
+ DIFF_BASE="$LAST_TAG"
139
154
  else
140
155
  echo "no previous tag — collecting the full history"
141
156
  git log --pretty=%s > commits.txt
157
+ DIFF_BASE="4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git empty tree hash
142
158
  fi
159
+ git diff --stat "${DIFF_BASE}..HEAD" > _full_diff_stat.txt
160
+ if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
161
+ { head -49 _full_diff_stat.txt; tail -1 _full_diff_stat.txt; } > diff_stat.txt
162
+ else
163
+ cp _full_diff_stat.txt diff_stat.txt
164
+ fi
165
+ rm -f _full_diff_stat.txt
166
+
167
+ if [ "${{ steps.semver_options.outputs.semver_auto }}" = "true" ]; then
168
+ BUMP=$(python3 .github/scripts/changelog_manager.py classify-bump --commits-file commits.txt | tail -n 1)
169
+ else
170
+ BUMP="patch"
171
+ fi
172
+ echo "bump level: $BUMP"
173
+
174
+ NEW_VERSION=$(python3 .github/scripts/version_manager.py increment --bump "$BUMP" | tail -n 1)
175
+ if [ -z "$NEW_VERSION" ]; then
176
+ echo "version bump failed"
177
+ exit 1
178
+ fi
179
+ python3 .github/scripts/version_manager.py sync
180
+ echo "release version: $NEW_VERSION ($BUMP)"
143
181
 
144
182
  python3 .github/scripts/changelog_manager.py ai-summary \
145
183
  --commits-file commits.txt \
146
184
  --version "$NEW_VERSION" \
147
- --output summary.md
185
+ --output summary.md \
186
+ --diff-stat-file diff_stat.txt
148
187
 
149
188
  # Same input channel as AUTO-CHANGELOG-CONTROL:
150
189
  # update-from-summary reads ./pr_body.md
@@ -165,7 +204,7 @@ jobs:
165
204
  python3 .github/scripts/changelog_manager.py update-from-summary
166
205
  python3 .github/scripts/changelog_manager.py generate-md
167
206
 
168
- rm -f pr_body.md summary.md commits.txt
207
+ rm -f pr_body.md summary.md commits.txt diff_stat.txt
169
208
 
170
209
  git add -A
171
210
  if git diff --staged --quiet; then