project-auto-wizard 0.11.0 → 0.12.0
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 +22 -15
- package/package.json +2 -2
- package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
- package/payload/scripts/changelog_manager.py +83 -30
- package/payload/version.yml.template +4 -0
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +30 -7
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +36 -8
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +25 -1
- package/src/cli/args.js +7 -0
- package/src/cli/help.js +2 -1
- package/src/commands/doctor.js +4 -3
- package/src/commands/interactive.js +9 -0
- package/src/commands/status.js +2 -1
- package/src/context.js +1 -0
- package/src/core/verify.js +1 -1
- package/src/core/version-yml.js +13 -4
- package/src/index.js +2 -0
- package/src/ui/summary.js +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# project-auto-wizard
|
|
2
2
|
|
|
3
|
-
> **One command DevOps** — `npx` 한 줄로 어떤 프로젝트든 GitHub-native AI
|
|
3
|
+
> **One command DevOps** — `npx` 한 줄로 어떤 프로젝트든 GitHub-native 릴리스 자동화(GitHub Copilot AI 요약은 선택)를 설치하는 마법사
|
|
4
4
|
|
|
5
5
|
- 새 프로젝트를 시작하면 코드를 작성하기 전부터 버전 관리, 배포 자동화, 변경 기록 작성처럼 먼저 정해야 할 일이 많다. project-auto-wizard는 이 준비 과정을 명령 한 번으로 구성하고, 프로젝트 상태 관리가 GitHub 안에서 자동으로 동작하도록 해 개발자가 기능 구현에 집중하도록 돕는 오픈소스 도구
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ npx project-auto-wizard
|
|
|
17
17
|
[](package.json)
|
|
18
18
|
|
|
19
19
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
20
|
-
## 최신 버전 : v0.
|
|
20
|
+
## 최신 버전 : v0.11.0 (2026-09-23)
|
|
21
21
|
|
|
22
22
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
23
23
|
|
|
@@ -38,7 +38,7 @@ npx project-auto-wizard --mode full --force --type spring,react # CI에서 비
|
|
|
38
38
|
| 축 | 내용 |
|
|
39
39
|
|---|---|
|
|
40
40
|
| ① **npx 마법사** | 마커 파일로 프로젝트 타입 자동 감지 — **10타입 + 멀티타입 + 모노레포 경로**까지. 질문은 최소한만 |
|
|
41
|
-
| ② **GitHub-native
|
|
41
|
+
| ② **GitHub-native Release Automation** | 릴리스 PR을 열면: 버전 확정 → **릴리스 노트 작성**(기본은 규칙 기반, GitHub Copilot AI는 선택) → CHANGELOG 갱신 → automerge → tag + GitHub Release. **API 키 0개** |
|
|
42
42
|
| ③ **타입별 CI/CD 워크플로우** | Spring(무중단 배포 포함)·Flutter(스토어 배포)·React·Next·Python·Go 등 타입에 맞는 GitHub Actions 자동 배치 |
|
|
43
43
|
|
|
44
44
|
### 지원 프로젝트 타입
|
|
@@ -183,18 +183,23 @@ npx project-auto-wizard --mode uninstall --force # 워크플로우·스
|
|
|
183
183
|
npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version # 완전 삭제
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
##
|
|
186
|
+
## 요약 엔진 체인
|
|
187
187
|
|
|
188
|
-
릴리스 노트는 3단 엔진 체인으로 생성됩니다.
|
|
188
|
+
릴리스 노트는 3단 엔진 체인으로 생성됩니다. **어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
|
|
189
|
+
|
|
190
|
+
> GitHub Models는 2026-07-30에 종료되어 더 이상 사용하지 않습니다.
|
|
189
191
|
|
|
190
192
|
```mermaid
|
|
191
193
|
flowchart LR
|
|
192
|
-
B["사용자 지정 AI<br/>(AI_API_KEY)"] -->|"
|
|
193
|
-
C -->|"
|
|
194
|
+
B["사용자 지정 AI<br/>(AI_API_KEY + AI_API_BASE_URL + AI_MODEL)"] -->|"미설정/실패"| C["GitHub Copilot CLI<br/>(선택, GITHUB_TOKEN)"]
|
|
195
|
+
C -->|"꺼짐/사용 불가/실패"| D["규칙 기반 fallback<br/>(항상 성공)"]
|
|
194
196
|
```
|
|
195
197
|
|
|
196
|
-
- 기본값은
|
|
197
|
-
-
|
|
198
|
+
- 기본값은 **규칙 기반 요약**입니다. 설치 마법사에서 Copilot을 켜면(`--copilot`, `version.yml`의 `copilot_ai: true`) Actions의 `GITHUB_TOKEN` + `permissions: copilot-requests: write`로 Copilot CLI가 요약을 생성합니다 — 별도 API 키는 필요 없습니다.
|
|
199
|
+
- **Copilot은 GitHub Copilot AI Credits를 소비합니다.** 개인 저장소는 저장소 소유자의 Copilot 좌석에, 조직 저장소는 조직에 과금되며 조직은 "Allow use of Copilot CLI billed to the organization" 정책을 켜야 합니다. 사용할 수 없으면 자동으로 규칙 기반 요약으로 전환됩니다. PR에 푸시할 때마다 요약이 새로 생성되므로 그만큼 크레딧이 소비됩니다.
|
|
200
|
+
- Copilot 모델은 저비용 소형 모델(`claude-haiku-4.5`)로 고정되어 있고, 저장소 변수 `COPILOT_MODEL`로 바꿀 수 있습니다.
|
|
201
|
+
- GitHub은 Copilot CLI를 `run` 스텝에서 직접 호출하기보다 Agentic Workflows를 쓰라고 권고하지만, 이 프로젝트는 직접 호출을 택했습니다. 프롬프트 입력이 PR 제목·커밋 메시지·`git diff --stat`뿐이고, 빈 임시 디렉터리에서 shell/write/url 도구와 내장 MCP를 모두 막은 텍스트 생성 전용으로 호출하며, 포크 PR은 기존 가드로 건너뛰어 프롬프트 인젝션 위험을 낮췄기 때문입니다.
|
|
202
|
+
- `AI_API_KEY`(**Secret**)와 `AI_API_BASE_URL`·`AI_MODEL`(**Variables**)을 **모두** 설정하면 OpenAI-호환 엔드포인트(Groq, Gemini 호환 모드, Ollama 등)를 최우선으로 사용합니다. 셋 중 하나라도 없으면 이 단계는 건너뜁니다.
|
|
198
203
|
- 규칙 fallback 3단: 프로젝트 컨벤션 → Conventional Commits → 무형식 bullet. 커밋 컨벤션이 없어도 동작.
|
|
199
204
|
|
|
200
205
|
## 릴리스 흐름
|
|
@@ -204,7 +209,7 @@ flowchart LR
|
|
|
204
209
|
subgraph pr-flow ["pr-flow (기본)"]
|
|
205
210
|
D1[develop push] --> PR[develop→main 릴리스 PR]
|
|
206
211
|
PR --> V["버전 확정 (patch+1)"]
|
|
207
|
-
V --> AI[
|
|
212
|
+
V --> AI[릴리스 노트]
|
|
208
213
|
AI --> CL[CHANGELOG.json/md 갱신]
|
|
209
214
|
CL --> AM[automerge]
|
|
210
215
|
AM --> TAG["tag vX.Y.Z + GitHub Release"]
|
|
@@ -236,6 +241,7 @@ npx project-auto-wizard [옵션]
|
|
|
236
241
|
--nexus 라이브러리 publish 워크플로우 포함 (Nexus + GitHub Packages)
|
|
237
242
|
--secret-backup Secret 서버 백업 워크플로우 포함
|
|
238
243
|
--semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함, --no-semver-auto로 끔)
|
|
244
|
+
--copilot Copilot으로 AI 요약 생성 (기본: 사용 안 함, GitHub Copilot AI Credits 소비, --no-copilot으로 끔)
|
|
239
245
|
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌
|
|
240
246
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
|
241
247
|
--purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
@@ -270,9 +276,10 @@ npx project-auto-wizard --mode doctor # 환경 진단 (읽기 전용, 규칙
|
|
|
270
276
|
secret이 없어도 폴백이 자동으로 이어받아 태그·Release까지 진행됩니다 — 실제 병합 후 최대 ~20초 정도 더 걸릴 뿐입니다.
|
|
271
277
|
속도를 더 원한다면 PAT을 등록할 수 있습니다 — 반드시 개인 계정이 아닌 조직 bot/machine 계정으로 발급하세요 (scopes: repo, workflow).
|
|
272
278
|
등록: 레포 Settings → Secrets and variables → Actions → New repository secret · 이름은 WORKFLOW_PAT
|
|
273
|
-
[i]
|
|
274
|
-
|
|
275
|
-
|
|
279
|
+
[i] Copilot AI 요약 — AI 릴리스 노트 생성(선택)
|
|
280
|
+
기본은 꺼져 있습니다 (version.yml의 copilot_ai: false).
|
|
281
|
+
켜면 GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이 필요합니다.
|
|
282
|
+
꺼져 있거나 사용할 수 없으면 규칙 기반 요약으로 자동 전환되므로 그대로 두셔도 됩니다.
|
|
276
283
|
|
|
277
284
|
✓ 문제를 찾지 못했습니다.
|
|
278
285
|
```
|
|
@@ -296,7 +303,7 @@ npx project-auto-wizard --no-semver-auto # 항상 patch+1 (레거시 동작)
|
|
|
296
303
|
|
|
297
304
|
### 자체 AI PR 요약봇
|
|
298
305
|
|
|
299
|
-
상용 PR 리뷰 SaaS 없이 동작하는 자체 요약봇입니다. 릴리스 브랜치(`--main-branch`)를 대상으로 하는 PR이 열릴 때
|
|
306
|
+
상용 PR 리뷰 SaaS 없이 동작하는 자체 요약봇입니다. 릴리스 브랜치(`--main-branch`)를 대상으로 하는 PR이 열릴 때 요약 엔진 체인(기본은 규칙 기반, Copilot은 선택)으로 요약 코멘트를 자동으로 답니다.
|
|
300
307
|
|
|
301
308
|
기본 설치(pr-flow) 기준으로 일상적인 기능 PR은 `develop`을 대상으로 열리므로, 이 봇은 develop→main 릴리스 PR에서만 실제로 동작합니다 — 해당 PR에서는 `AUTO-CHANGELOG-CONTROL`이 이미 같은 엔진으로 체인지로그 요약을 생성하므로, 이 봇은 그 요약을 PR 코멘트 형태로도 남겨주는 보조 역할입니다. 릴리스 브랜치 = 개발 브랜치인 trunk-based 모드에서는 모든 PR이 곧 릴리스 대상 브랜치를 향하므로 매 PR마다 동작합니다.
|
|
302
309
|
|
|
@@ -308,7 +315,7 @@ npx project-auto-wizard --no-semver-auto # 항상 patch+1 (레거시 동작)
|
|
|
308
315
|
|---|---|
|
|
309
316
|
| **`WORKFLOW_PAT` secret** (선택 — 속도 최적화용) | 없어도 `GITHUB_TOKEN` 폴백이 automerge부터 Release 발행까지 자동으로 이어갑니다(실제 병합 후 최대 ~20초 추가). 더 빠르게 하고 싶다면 Settings → Secrets → Actions에 `WORKFLOW_PAT` (scopes: `repo`, `workflow`) 등록 — 반드시 개인 계정이 아닌 조직 bot/machine 계정으로 발급하세요 |
|
|
310
317
|
| **Workflow permissions** | Settings → Actions → Workflow permissions: **Read and write** |
|
|
311
|
-
| **
|
|
318
|
+
| **Copilot AI 요약** (선택) | 기본 꺼짐. 켜려면 마법사에서 선택하거나 `version.yml`의 `copilot_ai`를 `true`로 — AI Credits가 소비되며 조직은 "Allow use of Copilot CLI billed to the organization" 정책이 필요합니다. 사용할 수 없으면 자동으로 규칙 fallback |
|
|
312
319
|
|
|
313
320
|
## 설계 원칙
|
|
314
321
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-auto-wizard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "One command DevOps: npx wizard that installs GitHub-native
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "One command DevOps: npx wizard that installs GitHub-native Release Automation (optional GitHub Copilot AI summaries) into any project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -27,7 +27,9 @@ import html
|
|
|
27
27
|
import json
|
|
28
28
|
import os
|
|
29
29
|
import re
|
|
30
|
+
import subprocess
|
|
30
31
|
import sys
|
|
32
|
+
import tempfile
|
|
31
33
|
import traceback
|
|
32
34
|
import urllib.error
|
|
33
35
|
import urllib.request
|
|
@@ -123,7 +125,7 @@ def _parse_markdown_sections(md_content: str) -> dict:
|
|
|
123
125
|
"""
|
|
124
126
|
섹션 파서: `### 카테고리` 헤딩 + `- 항목` 불릿 형식.
|
|
125
127
|
|
|
126
|
-
AI 엔진 체인(사용자 지정 API →
|
|
128
|
+
AI 엔진 체인(사용자 지정 API → Copilot CLI)과 규칙 기반 폴백이
|
|
127
129
|
동일하게 생성하는 형식이므로 1순위로 시도한다.
|
|
128
130
|
"""
|
|
129
131
|
detected: dict[str, dict] = {}
|
|
@@ -389,21 +391,19 @@ def _ai_assisted_minor_upgrade(unclassified_lines: list[str]) -> bool:
|
|
|
389
391
|
return False
|
|
390
392
|
prompt = _BUMP_AI_PROMPT_PREFIX + "\n".join(f"- {line}" for line in unclassified_lines)
|
|
391
393
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
(ai_api_key, os.environ.get('AI_API_BASE_URL') or _AI_DEFAULT_BASE_URL, os.environ.get('AI_MODEL') or _AI_DEFAULT_MODEL),
|
|
396
|
-
(github_token, _AI_DEFAULT_BASE_URL, _AI_DEFAULT_MODEL),
|
|
397
|
-
]
|
|
398
|
-
for token, base_url, model in candidates:
|
|
399
|
-
if not token:
|
|
400
|
-
continue
|
|
394
|
+
settings = _user_api_settings()
|
|
395
|
+
if settings:
|
|
396
|
+
api_key, base_url, model = settings
|
|
401
397
|
try:
|
|
402
|
-
|
|
403
|
-
return response.strip() == 'MINOR'
|
|
398
|
+
return call_openai_compatible(base_url, api_key, model, prompt).strip() == 'MINOR'
|
|
404
399
|
except Exception as e:
|
|
405
400
|
print(f"[warn] bump AI assist failed: {e}", file=sys.stderr)
|
|
406
|
-
|
|
401
|
+
|
|
402
|
+
if _copilot_enabled():
|
|
403
|
+
try:
|
|
404
|
+
return call_copilot_cli(prompt).strip() == 'MINOR'
|
|
405
|
+
except Exception as e:
|
|
406
|
+
print(f"[warn] bump AI assist (copilot) failed: {e}", file=sys.stderr)
|
|
407
407
|
return False
|
|
408
408
|
|
|
409
409
|
|
|
@@ -694,8 +694,67 @@ def cmd_export_release_notes(version: str, output_path: str | None) -> int:
|
|
|
694
694
|
|
|
695
695
|
# ------------------------ ai-summary 엔진 체인 ------------------------
|
|
696
696
|
|
|
697
|
-
|
|
698
|
-
|
|
697
|
+
_COPILOT_DEFAULT_MODEL = "claude-haiku-4.5"
|
|
698
|
+
_COPILOT_TIMEOUT_SECONDS = 90
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _user_api_settings() -> tuple[str, str, str] | None:
|
|
702
|
+
"""사용자 지정 AI 티어 설정. AI_API_KEY와 AI_API_BASE_URL, AI_MODEL이 모두 있어야 한다.
|
|
703
|
+
|
|
704
|
+
키만 있고 URL·모델이 비어 있으면 그 키를 어디로도 보내지 않고 경고 후 건너뛴다
|
|
705
|
+
(종료된 기본 엔드포인트으로 사용자 키가 흘러가던 문제 방지)."""
|
|
706
|
+
api_key = os.environ.get('AI_API_KEY')
|
|
707
|
+
if not api_key:
|
|
708
|
+
return None
|
|
709
|
+
base_url = os.environ.get('AI_API_BASE_URL')
|
|
710
|
+
model = os.environ.get('AI_MODEL')
|
|
711
|
+
if not base_url or not model:
|
|
712
|
+
print(
|
|
713
|
+
"::warning::AI_API_KEY가 설정됐지만 AI_API_BASE_URL/AI_MODEL이 없어 사용자 API 티어를 건너뜁니다",
|
|
714
|
+
file=sys.stderr,
|
|
715
|
+
)
|
|
716
|
+
return None
|
|
717
|
+
return api_key, base_url, model
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _copilot_enabled() -> bool:
|
|
721
|
+
"""version.yml의 copilot_ai가 켜져 있고(워크플로우가 COPILOT_AI로 전달) 토큰이 있을 때만 True."""
|
|
722
|
+
return os.environ.get('COPILOT_AI', '').strip().lower() == 'true' and bool(os.environ.get('GITHUB_TOKEN'))
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def call_copilot_cli(prompt: str) -> str:
|
|
726
|
+
"""Copilot CLI를 텍스트 생성 전용으로 호출해 응답 텍스트를 반환.
|
|
727
|
+
|
|
728
|
+
프롬프트에 필요한 정보가 이미 다 들어 있으므로 에이전트 기능은 전부 막는다:
|
|
729
|
+
빈 임시 디렉터리에서 실행하고, shell/write/url 도구를 거부하며, 내장 MCP와
|
|
730
|
+
커스텀 지침 로딩을 끈다.
|
|
731
|
+
실패(비정상 종료·타임아웃·CLI 없음)는 예외로 올려 호출부가 fallback한다."""
|
|
732
|
+
model = os.environ.get('COPILOT_MODEL') or _COPILOT_DEFAULT_MODEL
|
|
733
|
+
with tempfile.TemporaryDirectory() as workdir:
|
|
734
|
+
result = subprocess.run(
|
|
735
|
+
[
|
|
736
|
+
'copilot', '-p', prompt, '-s',
|
|
737
|
+
'--no-ask-user', '--no-color', '--no-custom-instructions', '--disable-builtin-mcps',
|
|
738
|
+
'--deny-tool=shell', '--deny-tool=write', '--deny-tool=url',
|
|
739
|
+
'--model', model,
|
|
740
|
+
],
|
|
741
|
+
cwd=workdir, stdin=subprocess.DEVNULL,
|
|
742
|
+
capture_output=True, text=True, timeout=_COPILOT_TIMEOUT_SECONDS,
|
|
743
|
+
)
|
|
744
|
+
if result.returncode != 0:
|
|
745
|
+
raise RuntimeError(f"copilot exited with {result.returncode}: {result.stderr.strip()[:200]}")
|
|
746
|
+
return result.stdout
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _is_valid_copilot_summary(text: str) -> bool:
|
|
750
|
+
"""프롬프트가 요구한 Markdown 형식인지 최소한만 검사한다.
|
|
751
|
+
|
|
752
|
+
섹션 헤딩('### ')이 하나도 없거나 코드펜스로 시작하는(통째로 감싼) 응답은 릴리스 노트로
|
|
753
|
+
쓰지 않는다."""
|
|
754
|
+
stripped = text.strip()
|
|
755
|
+
if not stripped or stripped.startswith('```'):
|
|
756
|
+
return False
|
|
757
|
+
return any(line.startswith('### ') for line in stripped.splitlines())
|
|
699
758
|
|
|
700
759
|
|
|
701
760
|
def _build_ai_prompt(commit_lines: list[str], pr_title: str | None, version: str, diff_stat: str | None = None) -> str:
|
|
@@ -760,16 +819,13 @@ def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title:
|
|
|
760
819
|
except Exception:
|
|
761
820
|
diff_stat = None
|
|
762
821
|
|
|
763
|
-
ai_api_key = os.environ.get('AI_API_KEY')
|
|
764
|
-
ai_base_url = os.environ.get('AI_API_BASE_URL') or _AI_DEFAULT_BASE_URL
|
|
765
|
-
ai_model = os.environ.get('AI_MODEL') or _AI_DEFAULT_MODEL
|
|
766
|
-
github_token = os.environ.get('GITHUB_TOKEN')
|
|
767
|
-
|
|
768
822
|
engine = None
|
|
769
823
|
summary_text = None
|
|
770
824
|
prompt = _build_ai_prompt(commit_lines, pr_title, version, diff_stat)
|
|
771
825
|
|
|
772
|
-
|
|
826
|
+
settings = _user_api_settings()
|
|
827
|
+
if settings:
|
|
828
|
+
ai_api_key, ai_base_url, ai_model = settings
|
|
773
829
|
try:
|
|
774
830
|
candidate = call_openai_compatible(ai_base_url, ai_api_key, ai_model, prompt)
|
|
775
831
|
if candidate and candidate.strip():
|
|
@@ -780,19 +836,16 @@ def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title:
|
|
|
780
836
|
except Exception as e:
|
|
781
837
|
print(f"[warn] user-api failed: {e}", file=sys.stderr)
|
|
782
838
|
|
|
783
|
-
if summary_text is None and
|
|
839
|
+
if summary_text is None and _copilot_enabled():
|
|
784
840
|
try:
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
# (커스텀 모델명은 models.github.ai에서 404).
|
|
788
|
-
candidate = call_openai_compatible(_AI_DEFAULT_BASE_URL, github_token, _AI_DEFAULT_MODEL, prompt)
|
|
789
|
-
if candidate and candidate.strip():
|
|
841
|
+
candidate = call_copilot_cli(prompt)
|
|
842
|
+
if _is_valid_copilot_summary(candidate):
|
|
790
843
|
summary_text = candidate
|
|
791
|
-
engine = "
|
|
844
|
+
engine = "copilot"
|
|
792
845
|
else:
|
|
793
|
-
print("[warn]
|
|
846
|
+
print("[warn] copilot failed: empty or not in the requested Markdown format", file=sys.stderr)
|
|
794
847
|
except Exception as e:
|
|
795
|
-
print(f"[warn]
|
|
848
|
+
print(f"[warn] copilot failed: {e}", file=sys.stderr)
|
|
796
849
|
|
|
797
850
|
if summary_text is None:
|
|
798
851
|
classified = classify_commits(commit_lines)
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
# (default) — feat: commits bump minor, a `!` breaking marker on any commit
|
|
7
7
|
# type bumps major. Set semver_auto: false to always patch-bump instead
|
|
8
8
|
# (then edit major/minor manually, e.g., 1.0.5 -> 1.1.0 or 2.0.0).
|
|
9
|
+
# - copilot_ai: opt-in (default false) — true lets the release/PR summary
|
|
10
|
+
# workflows call GitHub Copilot CLI, which consumes GitHub Copilot AI Credits.
|
|
11
|
+
# Falls back to rule-based summaries whenever Copilot is off or unavailable.
|
|
9
12
|
# ===
|
|
10
13
|
# Supported project types: spring, flutter, next, react, react-native,
|
|
11
14
|
# react-native-expo, node, python, basic, go
|
|
@@ -47,6 +50,7 @@ metadata:
|
|
|
47
50
|
nexus: {{OPT_NEXUS}}
|
|
48
51
|
secret_backup: {{OPT_SECRET_BACKUP}}
|
|
49
52
|
semver_auto: {{OPT_SEMVER_AUTO}}
|
|
53
|
+
copilot_ai: {{OPT_COPILOT_AI}}
|
|
50
54
|
deploy_style: "{{OPT_DEPLOY_STYLE}}" # simple | nginx | traefik | none (서버 배포 워크플로우)
|
|
51
55
|
{{FLUTTER_OPTIONS}}
|
|
52
56
|
{{DEPLOY}}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
# Self-hosted AI PR summary bot — no third-party service required.
|
|
5
5
|
# ===================================================================
|
|
6
6
|
#
|
|
7
|
-
# Posts
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Posts a summary comment on PRs targeting the default branch, using the
|
|
8
|
+
# same engine chain as the release changelog (user API key -> Copilot CLI,
|
|
9
|
+
# opt-in via version.yml copilot_ai -> rule-based fallback).
|
|
10
10
|
# The prompt includes commit subjects plus a capped `git diff --stat`.
|
|
11
11
|
#
|
|
12
12
|
# On release PRs (head branch == {{DEVELOP_BRANCH}}) this job is skipped.
|
|
@@ -30,7 +30,11 @@ on:
|
|
|
30
30
|
permissions:
|
|
31
31
|
contents: read
|
|
32
32
|
pull-requests: write
|
|
33
|
-
|
|
33
|
+
copilot-requests: write
|
|
34
|
+
|
|
35
|
+
concurrency:
|
|
36
|
+
group: ai-pr-summary-${{ github.event.pull_request.number }}
|
|
37
|
+
cancel-in-progress: true
|
|
34
38
|
|
|
35
39
|
jobs:
|
|
36
40
|
ai-pr-summary:
|
|
@@ -46,6 +50,20 @@ jobs:
|
|
|
46
50
|
fetch-depth: 0
|
|
47
51
|
ref: ${{ github.event.pull_request.head.ref }}
|
|
48
52
|
|
|
53
|
+
- name: Read copilot_ai option from version.yml
|
|
54
|
+
id: copilot_options
|
|
55
|
+
run: |
|
|
56
|
+
# scoped to the metadata: -> template: -> options: chain so an
|
|
57
|
+
# unrelated "copilot_ai:" key can never hijack the value
|
|
58
|
+
COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
59
|
+
echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
|
|
60
|
+
echo "copilot_ai option: $COPILOT_AI"
|
|
61
|
+
|
|
62
|
+
- name: Install Copilot CLI
|
|
63
|
+
if: steps.copilot_options.outputs.copilot_ai == 'true'
|
|
64
|
+
continue-on-error: true
|
|
65
|
+
run: npm install -g @github/copilot@1.0.88
|
|
66
|
+
|
|
49
67
|
- name: Generate and post AI summary
|
|
50
68
|
env:
|
|
51
69
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
@@ -53,6 +71,8 @@ jobs:
|
|
|
53
71
|
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
54
72
|
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
55
73
|
GITHUB_TOKEN: ${{ github.token }}
|
|
74
|
+
COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
|
|
75
|
+
COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
|
|
56
76
|
run: |
|
|
57
77
|
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
58
78
|
VERSION=$(python3 .github/scripts/version_manager.py get 2>/dev/null | tail -n 1)
|
|
@@ -68,17 +88,20 @@ jobs:
|
|
|
68
88
|
fi
|
|
69
89
|
rm -f _full_diff_stat.txt
|
|
70
90
|
|
|
71
|
-
python3 .github/scripts/changelog_manager.py ai-summary \
|
|
91
|
+
SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
|
|
72
92
|
--commits-file commits.txt \
|
|
73
93
|
--version "$VERSION" \
|
|
74
94
|
--output summary.md \
|
|
75
95
|
--pr-title "$PR_TITLE" \
|
|
76
|
-
--diff-stat-file diff_stat.txt
|
|
96
|
+
--diff-stat-file diff_stat.txt | tail -n 1)
|
|
97
|
+
ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
|
|
77
98
|
|
|
78
99
|
{
|
|
79
|
-
echo "
|
|
100
|
+
echo "📋 **PR Summary (project-auto-wizard)**"
|
|
80
101
|
echo ""
|
|
81
102
|
cat summary.md
|
|
103
|
+
echo ""
|
|
104
|
+
echo "<sub>engine: ${ENGINE}</sub>"
|
|
82
105
|
} > comment_body.md
|
|
83
106
|
|
|
84
107
|
python3 -c 'import json; print(json.dumps({"body": open("comment_body.md", encoding="utf-8").read()}))' > comment_payload.json
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
# commits) and bumps accordingly.
|
|
13
13
|
# - Otherwise: always patch+1 (legacy behavior).
|
|
14
14
|
# 2. Generates the release summary locally with changelog_manager.py
|
|
15
|
-
# ai-summary (engine chain: user API key ->
|
|
16
|
-
# fallback; always succeeds
|
|
15
|
+
# ai-summary (engine chain: user API key -> Copilot CLI, opt-in via
|
|
16
|
+
# version.yml copilot_ai -> rule-based fallback; always succeeds)
|
|
17
17
|
# - The AI prompt includes commit subjects plus a capped
|
|
18
18
|
# `git diff --stat` (file-level change summary, no diff body)
|
|
19
19
|
# for richer context without token-cost blowup.
|
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
# - secrets.WORKFLOW_PAT: PAT used for the merge. Without it the merge is
|
|
35
35
|
# performed with GITHUB_TOKEN, and GitHub will NOT trigger downstream
|
|
36
36
|
# {{MAIN_BRANCH}}-push workflows (e.g. RELEASE-PUBLISH) for that merge.
|
|
37
|
-
# - secrets.AI_API_KEY (+ vars.AI_API_BASE_URL / vars.AI_MODEL
|
|
38
|
-
# summary engine. Falls back to
|
|
37
|
+
# - secrets.AI_API_KEY (+ vars.AI_API_BASE_URL / vars.AI_MODEL, all three
|
|
38
|
+
# required together): preferred summary engine. Falls back to Copilot CLI
|
|
39
|
+
# (only when version.yml copilot_ai is true; optional vars.COPILOT_MODEL),
|
|
40
|
+
# then to rule-based.
|
|
39
41
|
#
|
|
40
42
|
# Issue #90: without WORKFLOW_PAT, a wait-for-merge-and-trigger-release job
|
|
41
43
|
# polls until the automerge actually lands, then wakes RELEASE-PUBLISH via
|
|
@@ -60,7 +62,7 @@ on:
|
|
|
60
62
|
permissions:
|
|
61
63
|
contents: write
|
|
62
64
|
pull-requests: write
|
|
63
|
-
|
|
65
|
+
copilot-requests: write
|
|
64
66
|
|
|
65
67
|
jobs:
|
|
66
68
|
changelog-and-merge:
|
|
@@ -94,6 +96,20 @@ jobs:
|
|
|
94
96
|
echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
|
|
95
97
|
echo "semver_auto option: $SEMVER_AUTO"
|
|
96
98
|
|
|
99
|
+
- name: Read copilot_ai option from version.yml
|
|
100
|
+
id: copilot_options
|
|
101
|
+
run: |
|
|
102
|
+
# scoped to the metadata: -> template: -> options: chain so an
|
|
103
|
+
# unrelated "copilot_ai:" key can never hijack the value
|
|
104
|
+
COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
105
|
+
echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
|
|
106
|
+
echo "copilot_ai option: $COPILOT_AI"
|
|
107
|
+
|
|
108
|
+
- name: Install Copilot CLI
|
|
109
|
+
if: steps.copilot_options.outputs.copilot_ai == 'true'
|
|
110
|
+
continue-on-error: true
|
|
111
|
+
run: npm install -g @github/copilot@1.0.88
|
|
112
|
+
|
|
97
113
|
- name: Collect commits since last release
|
|
98
114
|
id: collect
|
|
99
115
|
run: |
|
|
@@ -149,6 +165,8 @@ jobs:
|
|
|
149
165
|
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
150
166
|
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
151
167
|
GITHUB_TOKEN: ${{ github.token }}
|
|
168
|
+
COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
|
|
169
|
+
COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
|
|
152
170
|
run: |
|
|
153
171
|
# Idempotency: if the PR head is already this workflow's own
|
|
154
172
|
# version-confirm commit (re-run / synchronize retrigger), do not
|
|
@@ -175,6 +193,7 @@ jobs:
|
|
|
175
193
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
176
194
|
|
|
177
195
|
- name: Generate summary with the AI engine chain
|
|
196
|
+
id: summary
|
|
178
197
|
env:
|
|
179
198
|
# PR title goes through the env block — never inline-interpolate
|
|
180
199
|
# untrusted text into the shell string (quote injection).
|
|
@@ -183,28 +202,37 @@ jobs:
|
|
|
183
202
|
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
184
203
|
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
185
204
|
GITHUB_TOKEN: ${{ github.token }}
|
|
205
|
+
COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
|
|
206
|
+
COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
|
|
186
207
|
run: |
|
|
187
208
|
VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
188
209
|
|
|
189
|
-
python3 .github/scripts/changelog_manager.py ai-summary \
|
|
210
|
+
SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
|
|
190
211
|
--commits-file commits.txt \
|
|
191
212
|
--version "$VERSION" \
|
|
192
213
|
--output summary.md \
|
|
193
214
|
--pr-title "$PR_TITLE" \
|
|
194
|
-
--diff-stat-file diff_stat.txt
|
|
215
|
+
--diff-stat-file diff_stat.txt | tail -n 1)
|
|
216
|
+
ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
|
|
217
|
+
echo "summary engine: $ENGINE"
|
|
218
|
+
echo "engine=$ENGINE" >> $GITHUB_OUTPUT
|
|
195
219
|
|
|
196
220
|
# update-from-summary reads ./pr_body.md
|
|
197
221
|
cp summary.md pr_body.md
|
|
198
222
|
|
|
199
223
|
- name: Post AI summary comment
|
|
200
224
|
continue-on-error: true
|
|
225
|
+
env:
|
|
226
|
+
ENGINE: ${{ steps.summary.outputs.engine }}
|
|
201
227
|
run: |
|
|
202
228
|
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
203
229
|
|
|
204
230
|
{
|
|
205
|
-
echo "
|
|
231
|
+
echo "📋 **PR Summary (project-auto-wizard)**"
|
|
206
232
|
echo ""
|
|
207
233
|
cat summary.md
|
|
234
|
+
echo ""
|
|
235
|
+
echo "<sub>engine: ${ENGINE:-unknown}</sub>"
|
|
208
236
|
} > comment_body.md
|
|
209
237
|
|
|
210
238
|
python3 -c 'import json; print(json.dumps({"body": open("comment_body.md", encoding="utf-8").read()}))' > comment_payload.json
|
|
@@ -59,7 +59,7 @@ on:
|
|
|
59
59
|
|
|
60
60
|
permissions:
|
|
61
61
|
contents: write
|
|
62
|
-
|
|
62
|
+
copilot-requests: write
|
|
63
63
|
|
|
64
64
|
jobs:
|
|
65
65
|
release:
|
|
@@ -160,6 +160,28 @@ jobs:
|
|
|
160
160
|
echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
|
|
161
161
|
echo "semver_auto option: $SEMVER_AUTO"
|
|
162
162
|
|
|
163
|
+
- name: Read copilot_ai option from version.yml
|
|
164
|
+
id: copilot_options
|
|
165
|
+
if: >-
|
|
166
|
+
steps.gate.outputs.proceed == 'true' &&
|
|
167
|
+
steps.mode.outputs.mode == 'trunk-based' &&
|
|
168
|
+
github.event_name == 'push'
|
|
169
|
+
run: |
|
|
170
|
+
# scoped to the metadata: -> template: -> options: chain so an
|
|
171
|
+
# unrelated "copilot_ai:" key can never hijack the value
|
|
172
|
+
COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
173
|
+
echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
|
|
174
|
+
echo "copilot_ai option: $COPILOT_AI"
|
|
175
|
+
|
|
176
|
+
- name: Install Copilot CLI
|
|
177
|
+
if: >-
|
|
178
|
+
steps.gate.outputs.proceed == 'true' &&
|
|
179
|
+
steps.mode.outputs.mode == 'trunk-based' &&
|
|
180
|
+
github.event_name == 'push' &&
|
|
181
|
+
steps.copilot_options.outputs.copilot_ai == 'true'
|
|
182
|
+
continue-on-error: true
|
|
183
|
+
run: npm install -g @github/copilot@1.0.88
|
|
184
|
+
|
|
163
185
|
- name: Trunk-based version bump + changelog
|
|
164
186
|
if: >-
|
|
165
187
|
steps.gate.outputs.proceed == 'true' &&
|
|
@@ -170,6 +192,8 @@ jobs:
|
|
|
170
192
|
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
171
193
|
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
172
194
|
GITHUB_TOKEN: ${{ github.token }}
|
|
195
|
+
COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
|
|
196
|
+
COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
|
|
173
197
|
run: |
|
|
174
198
|
# Idempotency: on a re-run of a failed job the checked-out HEAD may
|
|
175
199
|
# already be this workflow's own docs commit — never bump twice.
|
package/src/cli/args.js
CHANGED
|
@@ -15,6 +15,7 @@ export function parseArgs(argv) {
|
|
|
15
15
|
includeNexus: null, // null=미설정
|
|
16
16
|
includeSecretBackup: null,
|
|
17
17
|
includeSemverAuto: null, // --semver-auto / --no-semver-auto (기본 true — 미지정 시 다운스트림에서 해석)
|
|
18
|
+
includeCopilotAi: null, // --copilot / --no-copilot (기본 false — AI Credits를 소비하는 opt-in)
|
|
18
19
|
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
19
20
|
mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
|
|
20
21
|
developBranch: "", // 개발 브랜치 (--develop-branch). 빈값=develop
|
|
@@ -139,6 +140,12 @@ export function parseArgs(argv) {
|
|
|
139
140
|
case "--no-semver-auto":
|
|
140
141
|
if (seenFlags.has("--semver-auto")) throw new CliError("--semver-auto와 --no-semver-auto는 동시에 지정할 수 없습니다");
|
|
141
142
|
seenFlags.add("--no-semver-auto"); result.includeSemverAuto = false; break;
|
|
143
|
+
case "--copilot":
|
|
144
|
+
if (seenFlags.has("--no-copilot")) throw new CliError("--copilot과 --no-copilot은 동시에 지정할 수 없습니다");
|
|
145
|
+
seenFlags.add("--copilot"); result.includeCopilotAi = true; break;
|
|
146
|
+
case "--no-copilot":
|
|
147
|
+
if (seenFlags.has("--copilot")) throw new CliError("--copilot과 --no-copilot은 동시에 지정할 수 없습니다");
|
|
148
|
+
seenFlags.add("--no-copilot"); result.includeCopilotAi = false; break;
|
|
142
149
|
case "--paths": result.pathsCsv = args.shift() ?? ""; break;
|
|
143
150
|
case "--main-branch": {
|
|
144
151
|
const v = args.shift();
|
package/src/cli/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// --help 텍스트.
|
|
2
|
-
export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-native
|
|
2
|
+
export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-native 릴리스 자동화 설치 마법사
|
|
3
3
|
|
|
4
4
|
사용법:
|
|
5
5
|
npx project-auto-wizard [옵션]
|
|
@@ -24,6 +24,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
24
24
|
--nexus / --no-nexus 라이브러리 publish 워크플로우 포함/제외 (Nexus + GitHub Packages)
|
|
25
25
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
26
26
|
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
27
|
+
--copilot / --no-copilot Copilot으로 AI 요약 생성 (기본: 사용 안 함, GitHub Copilot AI Credits 소비)
|
|
27
28
|
--force full 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
|
|
28
29
|
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/uninstall 지원)
|
|
29
30
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
package/src/commands/doctor.js
CHANGED
|
@@ -161,10 +161,11 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
add({
|
|
164
|
-
name: "
|
|
164
|
+
name: "Copilot AI 요약", label: "Copilot AI 요약", purpose: "AI 릴리스 노트 생성(선택)", status: "INFO",
|
|
165
165
|
note: [
|
|
166
|
-
"
|
|
167
|
-
"
|
|
166
|
+
"기본은 꺼져 있습니다 (version.yml의 copilot_ai: false).",
|
|
167
|
+
"켜면 GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이 필요합니다.",
|
|
168
|
+
"꺼져 있거나 사용할 수 없으면 규칙 기반 요약으로 자동 전환되므로 그대로 두셔도 됩니다.",
|
|
168
169
|
],
|
|
169
170
|
});
|
|
170
171
|
|
|
@@ -88,6 +88,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
88
88
|
let includeNexus = existing?.options?.nexus ?? false;
|
|
89
89
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
90
90
|
let includeSemverAuto = existing?.options?.semverAuto ?? null;
|
|
91
|
+
let includeCopilotAi = existing?.options?.copilotAi ?? null;
|
|
91
92
|
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (nexus/secret_backup과 같은 규약).
|
|
92
93
|
let deployStyle = isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
93
94
|
const showOptional = mode === "full";
|
|
@@ -153,11 +154,18 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
153
154
|
const y2 = await io.askYesNo("자동 버전 승격을 사용하시겠습니까? (커밋 타입에 따라 major/minor/patch 자동 결정)", true);
|
|
154
155
|
includeSemverAuto = y2 === true;
|
|
155
156
|
}
|
|
157
|
+
|
|
158
|
+
// Copilot AI 요약 — AI Credits를 소비하므로 opt-in(기본 No). 저장값 있으면 재질문 생략.
|
|
159
|
+
if (mode === "full" && includeCopilotAi === null) {
|
|
160
|
+
const y3 = await io.askYesNo("Copilot으로 AI 요약을 생성하시겠습니까? (GitHub Copilot AI Credits가 소비되며, 사용할 수 없으면 자동으로 규칙 기반 요약으로 전환됩니다)", false);
|
|
161
|
+
includeCopilotAi = y3 === true;
|
|
162
|
+
}
|
|
156
163
|
}
|
|
157
164
|
// 질문이 실제로 나온 경우(위 full 모드 질문) 답변을 그대로 존중.
|
|
158
165
|
// 질문이 안 나온 경우(version/workflows 모드) — 기존 설치는 안전하게 false로 폴백,
|
|
159
166
|
// 완전 신규 설치만 true(기존 설계) 유지 — CLI 경로(index.js)와 동일한 안전 정책.
|
|
160
167
|
includeSemverAuto = includeSemverAuto === null ? (existing ? false : true) : includeSemverAuto !== false;
|
|
168
|
+
includeCopilotAi = includeCopilotAi === true;
|
|
161
169
|
|
|
162
170
|
// 확인/수정 루프 — ESC는 '머무르기' (.sh L1877~1881: 명시적 '아니오'만 종료)
|
|
163
171
|
let paths = new Map();
|
|
@@ -278,6 +286,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
278
286
|
mode, force: true, types, version, versionCode, branch, branches, paths,
|
|
279
287
|
includeNexus, includeSecretBackup,
|
|
280
288
|
includeSemverAuto,
|
|
289
|
+
includeCopilotAi,
|
|
281
290
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
282
291
|
// 설치 로그(#79)·완료 요약(#80)이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
283
292
|
markers, envAnswers, detectWarnings,
|
package/src/commands/status.js
CHANGED
|
@@ -69,8 +69,9 @@ export function printStatus(status) {
|
|
|
69
69
|
}
|
|
70
70
|
const boolLabel = (v) => (v === null ? "미설정(기본 false)" : v);
|
|
71
71
|
const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
|
|
72
|
+
const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
|
|
72
73
|
const flutterLabels = status.types.includes("flutter") ? flutterOptionLabels(status.options) : "";
|
|
73
|
-
lines.push(`옵션 : nexus=${boolLabel(status.options.nexus)} secret_backup=${boolLabel(status.options.secretBackup)} semver_auto=${semverAutoLabel}${flutterLabels}`);
|
|
74
|
+
lines.push(`옵션 : nexus=${boolLabel(status.options.nexus)} secret_backup=${boolLabel(status.options.secretBackup)} semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${flutterLabels}`);
|
|
74
75
|
if (status.modifiedFiles.length) {
|
|
75
76
|
lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
|
|
76
77
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
package/src/context.js
CHANGED
|
@@ -27,6 +27,7 @@ export function createContext(overrides = {}) {
|
|
|
27
27
|
includeNexus: null, // null=미설정, true/false=명시
|
|
28
28
|
includeSecretBackup: null,
|
|
29
29
|
includeSemverAuto: null, // null=미설정(다운스트림에서 true로 해석), true/false=명시
|
|
30
|
+
includeCopilotAi: null, // null=미설정(다운스트림에서 false로 해석), true/false=명시 — Copilot AI 요약 opt-in
|
|
30
31
|
// Flutter 옵션 (이슈 #131) — Flutter 타입이 없는 프로젝트에서는 전부 무시된다.
|
|
31
32
|
// 결정은 src/core/flutter-options.js의 resolveFlutterOptions가 한다.
|
|
32
33
|
envMode: "", // "dart-define" | "dotenv". ""=미결정 → 템플릿 기본값(dart-define)
|
package/src/core/verify.js
CHANGED
|
@@ -43,7 +43,7 @@ export function scanUnsubstituted(workflowsDir, filenames = []) {
|
|
|
43
43
|
const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
|
|
44
44
|
// 없어도 워크플로우가 도는 secret — 폴백이 문서화돼 있다. 필수와 섞어 "등록해야 동작합니다"라고
|
|
45
45
|
// 하면 안내 자체를 못 믿게 되므로 분리한다.
|
|
46
|
-
// AI_API_KEY → 없으면
|
|
46
|
+
// AI_API_KEY → 없으면 Copilot(opt-in, copilot_ai) → 규칙 fallback
|
|
47
47
|
// WORKFLOW_PAT → 없으면 GITHUB_TOKEN
|
|
48
48
|
export const OPTIONAL_SECRETS = new Set(["AI_API_KEY", "WORKFLOW_PAT"]);
|
|
49
49
|
const SECRET_RE = /secrets\.([A-Z][A-Z0-9_]*)/g;
|
package/src/core/version-yml.js
CHANGED
|
@@ -44,13 +44,13 @@ const FLUTTER_OPTION_KEYS = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
// metadata.template.options 상태머신 파싱 (.sh read_template_options L2361~2416 등가).
|
|
47
|
-
// 반환: { nexus: bool|null, secretBackup: bool|null, semverAuto: bool|null, deployStyle: string|null,
|
|
47
|
+
// 반환: { nexus: bool|null, secretBackup: bool|null, semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
|
|
48
48
|
// envMode/flutterStore/androidDeployMode/iosDeployMode: string|null } — null=미기재.
|
|
49
49
|
// 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
|
|
50
50
|
// (options-ask.js가 이 함수를 import한다 — 순환 방지 위해 여기(version-yml)에 정의.)
|
|
51
51
|
export function parseTemplateOptions(content) {
|
|
52
52
|
const out = {
|
|
53
|
-
nexus: null, secretBackup: null, semverAuto: null, deployStyle: null,
|
|
53
|
+
nexus: null, secretBackup: null, semverAuto: null, copilotAi: null, deployStyle: null,
|
|
54
54
|
envMode: null, flutterStore: null, androidDeployMode: null, iosDeployMode: null,
|
|
55
55
|
};
|
|
56
56
|
// 값 정규화: 따옴표 제거 + 트림 (.sh tr -d '"' | tr -d "'" | xargs 등가)
|
|
@@ -88,6 +88,13 @@ export function parseTemplateOptions(content) {
|
|
|
88
88
|
if (v === "false") out.semverAuto = false;
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
|
+
m = line.match(/^\s+copilot_ai:\s*(.+)/);
|
|
92
|
+
if (m) {
|
|
93
|
+
const v = strip(m[1]);
|
|
94
|
+
if (v === "true") out.copilotAi = true;
|
|
95
|
+
if (v === "false") out.copilotAi = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
91
98
|
// 들여쓰기 0~4칸의 다른 키 → options 섹션 종료 (.sh L2404~2408)
|
|
92
99
|
if (/^\s{0,4}[a-z_]+:/.test(line)) { inOptions = false; inTemplate = false; }
|
|
93
100
|
}
|
|
@@ -204,7 +211,7 @@ export function buildVersionYml({
|
|
|
204
211
|
const b = branches || { main: branch || "main", develop: "develop", mode: "pr-flow" };
|
|
205
212
|
const {
|
|
206
213
|
templateVersion = "unknown", includeNexus = false, includeSecretBackup = false,
|
|
207
|
-
includeSemverAuto = true, deployStyle = "", optionsDate = today,
|
|
214
|
+
includeSemverAuto = true, includeCopilotAi = false, deployStyle = "", optionsDate = today,
|
|
208
215
|
} = templateOptions || {};
|
|
209
216
|
|
|
210
217
|
// project_paths 블록 (full-line 토큰 {{PROJECT_PATHS}} — 없으면 라인 제거)
|
|
@@ -244,6 +251,7 @@ export function buildVersionYml({
|
|
|
244
251
|
MAIN_BRANCH: b.main, DEVELOP_BRANCH: b.develop, BRANCH_MODE: b.mode,
|
|
245
252
|
OPT_NEXUS: String(includeNexus), OPT_SECRET_BACKUP: String(includeSecretBackup),
|
|
246
253
|
OPT_SEMVER_AUTO: String(includeSemverAuto),
|
|
254
|
+
OPT_COPILOT_AI: String(includeCopilotAi),
|
|
247
255
|
OPT_DEPLOY_STYLE: String(deployStyle || ""),
|
|
248
256
|
};
|
|
249
257
|
|
|
@@ -273,7 +281,7 @@ export function buildVersionYml({
|
|
|
273
281
|
export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [] }) {
|
|
274
282
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
275
283
|
now, today, templateVersion = "unknown", branches = null,
|
|
276
|
-
includeNexus = false, includeSecretBackup = false, includeSemverAuto, deployStyle,
|
|
284
|
+
includeNexus = false, includeSecretBackup = false, includeSemverAuto, includeCopilotAi, deployStyle,
|
|
277
285
|
envMode, flutterStore, androidDeployMode, iosDeployMode } = context;
|
|
278
286
|
return buildVersionYml({
|
|
279
287
|
templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
|
|
@@ -282,6 +290,7 @@ export function renderVersionYml(context, templateText, { pathMarkers, deployVal
|
|
|
282
290
|
templateOptions: {
|
|
283
291
|
templateVersion, includeNexus, includeSecretBackup,
|
|
284
292
|
includeSemverAuto: includeSemverAuto !== false,
|
|
293
|
+
includeCopilotAi: includeCopilotAi === true,
|
|
285
294
|
deployStyle: deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
286
295
|
optionsDate: today,
|
|
287
296
|
},
|
package/src/index.js
CHANGED
|
@@ -290,6 +290,8 @@ async function runInner(argv, {
|
|
|
290
290
|
// workflows-only 재실행) 조용히 true로 켜지면 애매한 커밋 하나로 major가 승격될 위험이 있다 —
|
|
291
291
|
// 기존 설치는 false로 안전하게 폴백, 완전 신규 설치만 true(기존 설계) 유지.
|
|
292
292
|
includeSemverAuto: opts.includeSemverAuto ?? existing?.options?.semverAuto ?? (existing ? false : true),
|
|
293
|
+
// Copilot AI 요약은 AI Credits를 소비하는 opt-in — 신규·기존 설치 모두 명시하지 않으면 false다.
|
|
294
|
+
includeCopilotAi: opts.includeCopilotAi ?? existing?.options?.copilotAi ?? false,
|
|
293
295
|
repoName,
|
|
294
296
|
// 실 resolver 4종 (.sh resolve_token 등가)
|
|
295
297
|
resolvers: makeResolvers(cwd, repoName, paths, flutterOptions),
|
package/src/ui/summary.js
CHANGED
|
@@ -29,7 +29,7 @@ export function printSummary(ctx) {
|
|
|
29
29
|
case "full":
|
|
30
30
|
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
31
31
|
err(" ✅ README.md 자동 버전 업데이트");
|
|
32
|
-
err(" ✅ GitHub Actions 워크플로우 (
|
|
32
|
+
err(" ✅ GitHub Actions 워크플로우 (릴리스 자동화 포함)");
|
|
33
33
|
if (gitignoreUpdated) err(" ✅ .gitignore 백업 파일 제외 항목 (*.bak/*.template.yaml)");
|
|
34
34
|
break;
|
|
35
35
|
case "version":
|
|
@@ -37,7 +37,7 @@ export function printSummary(ctx) {
|
|
|
37
37
|
err(" ✅ README.md 자동 버전 업데이트");
|
|
38
38
|
break;
|
|
39
39
|
case "workflows":
|
|
40
|
-
err(" ✅ GitHub Actions 워크플로우 (
|
|
40
|
+
err(" ✅ GitHub Actions 워크플로우 (릴리스 자동화 포함)");
|
|
41
41
|
break;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -48,13 +48,13 @@ export function printSummary(ctx) {
|
|
|
48
48
|
if (branches.mode === "trunk-based") {
|
|
49
49
|
err(` 🌿 ${branches.main} 단일 브랜치 (trunk-based) — RELEASE-PUBLISH 하나가 버전확정→체인지로그→tag→Release를 순차 처리`);
|
|
50
50
|
} else {
|
|
51
|
-
err(` 🌿 개발 ${branches.develop} → 릴리스 ${branches.main} (pr-flow) — 릴리스 PR에서
|
|
51
|
+
err(` 🌿 개발 ${branches.develop} → 릴리스 ${branches.main} (pr-flow) — 릴리스 PR에서 버전확정·체인지로그·automerge`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
if (mode === "full" || mode === "workflows") {
|
|
55
55
|
err("");
|
|
56
56
|
err("릴리스 노트 요약 엔진:");
|
|
57
|
-
err(" 🤖 AI_API_KEY(선택) →
|
|
57
|
+
err(" 🤖 AI_API_KEY(선택) → Copilot(선택·AI Credits 소비, 기본 꺼짐) → 규칙 fallback — 릴리스는 절대 막히지 않음");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
err("");
|