projectops 4.6.1 → 4.8.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 +1 -1
- package/package.json +1 -1
- package/src/cli/args.js +4 -0
- package/src/cli/help.js +1 -0
- package/src/commands/doctor.js +35 -0
- package/src/commands/full.js +3 -3
- package/src/commands/interactive.js +8 -4
- package/src/commands/version.js +1 -1
- package/src/context.js +2 -1
- package/src/core/copy/simple.js +3 -1
- package/src/core/copy/workflows.js +25 -2
- package/src/core/options-ask.js +54 -35
- package/src/core/orphan-workflows.js +40 -2
- package/src/core/verify.js +1 -1
- package/src/core/version-yml.js +7 -2
- package/src/index.js +9 -3
- package/src/ui/summary.js +27 -3
package/README.md
CHANGED
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -17,6 +17,7 @@ export function parseArgs(argv) {
|
|
|
17
17
|
deployBranch: "", // 릴리스 PR head 브랜치 (#456): --deploy-branch, 빈 값=미지정
|
|
18
18
|
intent: null, // 프로젝트 성격 (#485): --intent app|library|both|none|manual, null=미설정(역추론)
|
|
19
19
|
includeSecretBackup: null,
|
|
20
|
+
aiPrSummary: null, // #566 — AI 변경 요약 워크플로우 포함 여부
|
|
20
21
|
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
21
22
|
force: false,
|
|
22
23
|
help: false,
|
|
@@ -101,6 +102,9 @@ export function parseArgs(argv) {
|
|
|
101
102
|
break;
|
|
102
103
|
case "--secret-backup": result.includeSecretBackup = true; break;
|
|
103
104
|
case "--no-secret-backup": result.includeSecretBackup = false; break;
|
|
105
|
+
// #566 — 비대화형에서도 켜고 끌 수 있어야 한다. 없으면 자동화 환경은 선택권이 없다.
|
|
106
|
+
case "--ai-summary": result.aiPrSummary = true; break;
|
|
107
|
+
case "--no-ai-summary": result.aiPrSummary = false; break;
|
|
104
108
|
case "--npm-publish":
|
|
105
109
|
process.stderr.write("⚠️ --npm-publish는 deprecated입니다. --publish npm 을 사용하세요.\n");
|
|
106
110
|
result.publishTargets = [...new Set([...(result.publishTargets ?? []), "npm"])];
|
package/src/cli/help.js
CHANGED
|
@@ -20,6 +20,7 @@ export const HELP_TEXT = `projectops — GitHub 프로젝트 자동화 템플릿
|
|
|
20
20
|
--publish CSV publish 타겟 csv: nexus,npm,github-packages (기본: 없음)
|
|
21
21
|
--deploy-branch NAME 릴리스 PR head 브랜치 (#456, 기본: develop). default_branch와 별개
|
|
22
22
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
23
|
+
--ai-summary / --no-ai-summary PR 변경 요약 워크플로우 포함/제외
|
|
23
24
|
--nexus / --npm-publish (deprecated — --publish nexus / --publish npm 사용)
|
|
24
25
|
--force 모든 확인 생략, 비대화형 기본값 사용
|
|
25
26
|
-v, --version projectops 버전 출력
|
package/src/commands/doctor.js
CHANGED
|
@@ -100,6 +100,27 @@ export function localChecks(cwd = ".") {
|
|
|
100
100
|
] : null,
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// AI 요약 키 (#569) — "등록했는데 되는 건가?"를 확인할 수단이 없었다.
|
|
104
|
+
// 로컬에서는 저장소 Secret을 읽을 수 없으므로, 어떤 이름을 쓰면 되는지와
|
|
105
|
+
// 등록하지 않아도 무방하다는 사실을 알려준다. 실제 등록 여부는 원격 점검이 본다.
|
|
106
|
+
const AI_KEY_NAMES = ["GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY"];
|
|
107
|
+
const hasSummaryWf = files.some((f) => /AI-PR-SUMMARY|RELEASE-CHANGELOG/.test(f));
|
|
108
|
+
if (hasSummaryWf) {
|
|
109
|
+
add({
|
|
110
|
+
name: "AI 요약 키", purpose: "릴리스 노트를 AI로 다듬을지 (선택)",
|
|
111
|
+
status: "INFO", value: "선택 사항",
|
|
112
|
+
detail: [
|
|
113
|
+
"등록하지 않아도 릴리스 노트는 나옵니다 — 커밋 내용을 분석해 만듭니다.",
|
|
114
|
+
"AI가 다듬은 문장을 원하면 아래 중 하나를 저장소 Secret에 등록하세요.",
|
|
115
|
+
" GEMINI_API_KEY 무료 · https://aistudio.google.com/apikey",
|
|
116
|
+
" GROQ_API_KEY 무료",
|
|
117
|
+
" MISTRAL_API_KEY 무료",
|
|
118
|
+
" OPENAI_API_KEY / ANTHROPIC_API_KEY 유료",
|
|
119
|
+
"등록한 것이 자동으로 쓰입니다. 별도 설정은 필요 없습니다.",
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
// 워크플로우 파일의 permissions 선언 점검 (#558).
|
|
104
125
|
// 저장소 설정(Settings > Actions)은 "요청할 수 있는 최대 범위"이고, 워크플로우의
|
|
105
126
|
// permissions 블록은 "실제로 요청한 범위"다. 둘은 다른 층이라 저장소 설정이 정상이어도
|
|
@@ -190,6 +211,20 @@ export async function remoteChecks(slug, token, requiredSecrets = []) {
|
|
|
190
211
|
detail: ["토큰에 Secret 조회 권한이 없어 확인하지 못했습니다."] });
|
|
191
212
|
} else {
|
|
192
213
|
const have = new Set((sec.data?.secrets || []).map((s) => s.name));
|
|
214
|
+
|
|
215
|
+
// AI 요약 키가 실제로 등록돼 있는지 (#569) — 있으면 어느 서비스인지까지 보여준다.
|
|
216
|
+
const AI_KEYS = { GEMINI_API_KEY: "Gemini", OPENAI_API_KEY: "OpenAI", ANTHROPIC_API_KEY: "Anthropic",
|
|
217
|
+
GROQ_API_KEY: "Groq", MISTRAL_API_KEY: "Mistral", MODEL_API_KEY: "구 이름(서비스 자동 추정)" };
|
|
218
|
+
const foundAi = Object.entries(AI_KEYS).filter(([n]) => have.has(n));
|
|
219
|
+
rows.push({
|
|
220
|
+
name: "AI 요약 키 등록", purpose: "릴리스 노트를 AI로 다듬을지 (선택)",
|
|
221
|
+
status: "INFO",
|
|
222
|
+
value: foundAi.length ? foundAi.map(([n, label]) => `${n} (${label})`).join(", ") : "없음 — 커밋 분석으로 동작",
|
|
223
|
+
detail: foundAi.length ? null : [
|
|
224
|
+
"등록하지 않아도 릴리스 노트는 정상적으로 나옵니다.",
|
|
225
|
+
"AI를 쓰려면 GEMINI_API_KEY(무료)를 등록하세요 — https://aistudio.google.com/apikey",
|
|
226
|
+
],
|
|
227
|
+
});
|
|
193
228
|
const missing = requiredSecrets.filter((n) => !have.has(n));
|
|
194
229
|
add({
|
|
195
230
|
name: "Secret 등록 여부", purpose: "배포에 필요한 값",
|
package/src/commands/full.js
CHANGED
|
@@ -23,8 +23,8 @@ import { verifyInstall } from "../core/verify.js";
|
|
|
23
23
|
export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
24
24
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
25
25
|
force = true, now, today, templateVersion = "unknown",
|
|
26
|
-
deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false,
|
|
27
|
-
changelogProvider = "
|
|
26
|
+
deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true,
|
|
27
|
+
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
28
28
|
deployBranch = "", intent = null, semverAuto = true , appRelease = null } = context;
|
|
29
29
|
|
|
30
30
|
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
@@ -43,7 +43,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
43
43
|
buildVersionYml({
|
|
44
44
|
version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
|
|
45
45
|
deployValues,
|
|
46
|
-
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate: today,
|
|
46
|
+
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, aiPrSummary, optionsDate: today,
|
|
47
47
|
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease },
|
|
48
48
|
})), { version, versionCode });
|
|
49
49
|
|
|
@@ -13,7 +13,7 @@ import { runBreakingCheck } from "../core/breaking-check.js";
|
|
|
13
13
|
import { runMigrations } from "../core/migrations/index.js";
|
|
14
14
|
import { detectOrphanWorkflows, applyOrphanCleanup } from "../core/orphan-workflows.js";
|
|
15
15
|
import { resolveProjectPaths, filterExcludedTypes } from "../core/paths-resolve.js";
|
|
16
|
-
import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets } from "../core/options-ask.js";
|
|
16
|
+
import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets, migrateProvider } from "../core/options-ask.js";
|
|
17
17
|
import { createRunTrace, MIGRATION_DIR } from "../core/run-trace.js";
|
|
18
18
|
import { appendGuideEntry } from "../core/migration-guide.js";
|
|
19
19
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
@@ -118,8 +118,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
118
118
|
let deployTarget = existing?.options?.deploy ?? "docker-ssh";
|
|
119
119
|
let publishTargets = existing?.options?.publish ?? [];
|
|
120
120
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
121
|
+
let aiPrSummary = existing?.options?.aiPrSummary ?? null; // null = 아직 안 물음 (#566)
|
|
121
122
|
let codeReviewCoderabbit = existing?.options?.codeReviewCoderabbit ?? true;
|
|
122
|
-
let changelogProvider = existing?.options?.changelogProvider ?? "
|
|
123
|
+
let changelogProvider = migrateProvider(existing?.options?.changelogProvider) ?? "commit";
|
|
123
124
|
let changelogBaseUrl = existing?.options?.changelogBaseUrl ?? "";
|
|
124
125
|
let deployBranch = existing?.options?.deployBranch ?? "develop"; // #456
|
|
125
126
|
let deployBranchReady = null; // #490 — 이번 실행에서 개발 브랜치 존재/생성이 확인됐는지
|
|
@@ -346,7 +347,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
346
347
|
// 고아 타입 워크플로우 정리 (#487) — 타입 변경으로 선택에서 빠진 타입의 잔존 워크플로우
|
|
347
348
|
const orphanReport = { cleaned: [], pending: [] }; // #493 — 가이드 기록용
|
|
348
349
|
if (mode === "full" || mode === "workflows") {
|
|
349
|
-
const orphans = detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types
|
|
350
|
+
const orphans = detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types,
|
|
351
|
+
// 껐는데 남아 계속 도는 워크플로우도 함께 잡는다 (#566)
|
|
352
|
+
options: { includeSecretBackup, aiPrSummary: aiPrSummary !== false, deployTarget } });
|
|
350
353
|
if (orphans.length > 0) {
|
|
351
354
|
io.note?.(
|
|
352
355
|
orphans.map((o) => `• ${o.filename} (${o.type} 타입 — 현재 미선택)`).join("\n"),
|
|
@@ -405,6 +408,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
405
408
|
mode, types, version, deployBranch, deployBranchReady, migrationGuidePath,
|
|
406
409
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
407
410
|
verification: result?.verification, // #549 설치 검증 결과
|
|
411
|
+
aiPrSummary, codeReviewCoderabbit, // #569 — 고른 것만 안내
|
|
408
412
|
logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
|
|
409
413
|
logFile: files?.logFile ?? null,
|
|
410
414
|
traceFile: files?.traceFile ?? null,
|
|
@@ -445,7 +449,7 @@ function summarize({ mode, types, version, branch, deployTarget, publishTargets,
|
|
|
445
449
|
lines.push(`배포 방식 : ${deployTarget || "docker-ssh"}`);
|
|
446
450
|
lines.push(`Publish : ${(publishTargets ?? []).join(",") || "없음"}`);
|
|
447
451
|
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
448
|
-
lines.push(`Changelog : ${changelogProvider || "
|
|
452
|
+
lines.push(`Changelog : ${changelogProvider || "commit"}`);
|
|
449
453
|
lines.push(`CodeRabbit 리뷰 : ${codeReviewCoderabbit ? "사용" : "미사용"}`);
|
|
450
454
|
}
|
|
451
455
|
return lines.join("\n");
|
package/src/commands/version.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
|
13
13
|
export function runVersion(context, tempDir, targetRoot = ".") {
|
|
14
14
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
15
15
|
now, today, templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false,
|
|
16
|
-
changelogProvider = "
|
|
16
|
+
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
17
17
|
deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null } = context;
|
|
18
18
|
|
|
19
19
|
const pathMarkers = new Map();
|
package/src/context.js
CHANGED
|
@@ -19,8 +19,9 @@ export function createContext(overrides = {}) {
|
|
|
19
19
|
deployTarget: null, // 'docker-ssh'(기본) | 'vercel' | 'none'
|
|
20
20
|
publishTargets: null, // ['nexus','npm','github-packages'] 부분집합
|
|
21
21
|
includeSecretBackup: null,
|
|
22
|
+
aiPrSummary: null, // #566 — AI 변경 요약 워크플로우 포함 여부
|
|
22
23
|
// changelog provider 축 (#455 — null=미설정)
|
|
23
|
-
changelogProvider: null, // '
|
|
24
|
+
changelogProvider: null, // 'commit'(기본) | 'coderabbit' | 'openai' | 'gemini' | 'claude' | 'ollama' | 'commit'
|
|
24
25
|
changelogBaseUrl: null, // ollama일 때만 값
|
|
25
26
|
codeReviewCoderabbit: null,
|
|
26
27
|
deployBranch: "", // 릴리스 PR head 브랜치 (#456). 빈 값=metadata.deploy_branch 미출력
|
package/src/core/copy/simple.js
CHANGED
|
@@ -20,8 +20,10 @@ export function copyScripts(tempDir, targetRoot = ".") {
|
|
|
20
20
|
"dispatch_downstream.py",
|
|
21
21
|
// AI PR SUMMARY 워크플로우가 요약 댓글을 작성·갱신할 때 호출 (#553).
|
|
22
22
|
"pr_summary_comment.py",
|
|
23
|
+
// 릴리스 노트가 어떤 경로로 만들어졌는지 알리고, AI를 못 썼을 때 대안을 안내 (#566).
|
|
24
|
+
"changelog_notice.py",
|
|
23
25
|
"changelog_providers/_common.py", "changelog_providers/ladder.py",
|
|
24
|
-
"changelog_providers/commit.py", "changelog_providers/
|
|
26
|
+
"changelog_providers/commit.py", "changelog_providers/copilot.py",
|
|
25
27
|
"changelog_providers/openai_compatible.py",
|
|
26
28
|
];
|
|
27
29
|
let copied = 0;
|
|
@@ -63,14 +63,14 @@ function classify(srcDir, workflowsDir, envOpts, baseline = null) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// copy_workflows 본체 (동기 — 기존 호출부 무변경).
|
|
66
|
-
// context: { types:[], paths:Map, deployTarget, publishTargets:[], includeSecretBackup, force, repoName, resolvers,
|
|
66
|
+
// context: { types:[], paths:Map, deployTarget, publishTargets:[], includeSecretBackup, aiPrSummary, force, repoName, resolvers,
|
|
67
67
|
// envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
|
|
68
68
|
// deployTarget(#439 택1): 'docker-ssh'(기본) | 'vercel' | 'none' — server-deploy는 docker-ssh일 때만,
|
|
69
69
|
// common/deploy/<target>/은 해당 타겟일 때 복사. publishTargets(#439 다중): 'nexus'|'npm'|'github-packages'.
|
|
70
70
|
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'> } — 기존 파일(changed) 충돌 결정.
|
|
71
71
|
// 반환: {copied, skipped, templateAdded, optionalCopied, copiedFiles[]} — copiedFiles는 실제 복사·교체된 파일명 (#473 요약용)
|
|
72
72
|
export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
73
|
-
const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
|
|
73
|
+
const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
|
|
74
74
|
const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
|
|
75
75
|
const trace = hooks.trace ?? null; // #494 — 실행 트레이스 (null-safe: 미주입이면 전 이벤트 no-op)
|
|
76
76
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
@@ -164,6 +164,29 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// (4.7) common/pr-summary — AI 변경 요약 (#566). 선택했을 때만 복사한다.
|
|
168
|
+
// 종전에는 common 본체에 있어 무조건 복사된 뒤 런타임에 스스로 빠졌고, 그 판단이
|
|
169
|
+
// ".coderabbit.yaml 존재"라 파일만 있고 앱이 없는 저장소에서는 아무도 요약하지 않았다.
|
|
170
|
+
const prSummaryDir = join(commonDir, "pr-summary");
|
|
171
|
+
if (exists(prSummaryDir) && aiPrSummary) {
|
|
172
|
+
for (const filename of listYamlFiles(prSummaryDir)) {
|
|
173
|
+
const src = join(prSummaryDir, filename);
|
|
174
|
+
const dst = join(workflowsDir, filename);
|
|
175
|
+
if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOptsFor("common"))) {
|
|
176
|
+
counters.skipped++;
|
|
177
|
+
trace?.event("copy", "skipped-unchanged", filename, { group: "pr-summary" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const backedUp = existsSync(dst);
|
|
181
|
+
if (backedUp) renameSync(dst, dst + ".bak");
|
|
182
|
+
copyFileSync(src, dst);
|
|
183
|
+
counters.optionalCopied++;
|
|
184
|
+
counters.copied++;
|
|
185
|
+
counters.copiedFiles.push(filename);
|
|
186
|
+
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: "pr-summary" });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
167
190
|
// (5) common/secret-backup — 있으면 무조건 스킵/신규만 복사
|
|
168
191
|
const secretDir = join(commonDir, "secret-backup");
|
|
169
192
|
if (exists(secretDir) && includeSecretBackup) {
|
package/src/core/options-ask.js
CHANGED
|
@@ -92,8 +92,19 @@ export function applicableTargets(types = []) {
|
|
|
92
92
|
publish: PUBLISH_TARGETS.filter((t) => types.some((ty) => (TYPE_PUBLISH_TARGETS[ty] ?? PUBLISH_TARGETS).includes(t))),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
95
|
+
// changelog 생성기 provider (#455, #566에서 재정비).
|
|
96
|
+
// 기본값은 commit — 외부 의존이 없어 어떤 환경에서도 결과가 나온다. AI를 쓰려면
|
|
97
|
+
// MODEL_API_KEY를 등록하면 사다리가 자동으로 집어 쓴다(마법사는 묻지 않는다).
|
|
98
|
+
// github-ai는 GitHub Models 종료(2026-07-30)로 제외됐다.
|
|
99
|
+
export const CHANGELOG_PROVIDERS = ["copilot", "coderabbit", "openai", "gemini", "claude", "groq", "mistral", "ollama", "commit"];
|
|
100
|
+
|
|
101
|
+
// 더 이상 동작하지 않는 저장값을 살아있는 값으로 옮긴다 (#566).
|
|
102
|
+
// 기존 저장소가 업데이트만 돌려도 죽은 설정에서 벗어나게 하는 유일한 경로다.
|
|
103
|
+
const RETIRED_PROVIDERS = { "github-ai": "commit" };
|
|
104
|
+
export function migrateProvider(saved) {
|
|
105
|
+
if (saved == null) return null;
|
|
106
|
+
return RETIRED_PROVIDERS[saved] ?? saved;
|
|
107
|
+
}
|
|
97
108
|
|
|
98
109
|
const isCancel = (v) => typeof v === "symbol";
|
|
99
110
|
|
|
@@ -140,7 +151,7 @@ const INTENT_ASKS_PUBLISH = { library: true, both: true, manual: true, app: fals
|
|
|
140
151
|
// current: { deploy: string|null, publish: string[]|null, secretBackup: bool|null } — CLI 명시값
|
|
141
152
|
// scope: null이면 전 축(초기 통합·전체 재질문). Set/배열이면 그 축만 forceAsk 대상 (#483 수정 메뉴 격리).
|
|
142
153
|
// 스코프 밖 축은 forceAsk여도 current/저장값을 그대로 유지하고 다시 묻지 않는다.
|
|
143
|
-
// 반환: { deploy, publish, secretBackup, codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch }
|
|
154
|
+
// 반환: { deploy, publish, secretBackup, codeReviewCoderabbit, aiPrSummary, changelogProvider, changelogBaseUrl, deployBranch }
|
|
144
155
|
export async function askAllOptionalWorkflows({
|
|
145
156
|
tempDir, types = [], current = {}, targetRoot = ".",
|
|
146
157
|
force = false, tty = true, io = {}, forceAsk = false, defaultBranch = "", scope = null,
|
|
@@ -153,6 +164,7 @@ export async function askAllOptionalWorkflows({
|
|
|
153
164
|
let publish = current.publish ?? null;
|
|
154
165
|
let secretBackup = current.secretBackup ?? null;
|
|
155
166
|
let codeReviewCoderabbit = current.codeReviewCoderabbit ?? null;
|
|
167
|
+
let aiPrSummary = current.aiPrSummary ?? null;
|
|
156
168
|
let changelogProvider = current.changelogProvider ?? null;
|
|
157
169
|
let changelogBaseUrl = current.changelogBaseUrl ?? null;
|
|
158
170
|
let deployBranch = current.deployBranch ?? null; // #456 릴리스 PR head 브랜치
|
|
@@ -187,6 +199,7 @@ export async function askAllOptionalWorkflows({
|
|
|
187
199
|
}
|
|
188
200
|
// #455 changelog/code_review 저장값 재사용
|
|
189
201
|
if (codeReviewCoderabbit === null && saved.codeReviewCoderabbit !== null) codeReviewCoderabbit = saved.codeReviewCoderabbit;
|
|
202
|
+
if (aiPrSummary === null && saved.aiPrSummary != null) aiPrSummary = saved.aiPrSummary;
|
|
190
203
|
if (changelogProvider === null && saved.changelogProvider !== null) changelogProvider = saved.changelogProvider;
|
|
191
204
|
if (changelogBaseUrl === null && saved.changelogBaseUrl !== null) changelogBaseUrl = saved.changelogBaseUrl;
|
|
192
205
|
// #456 deploy_branch 저장값 재사용
|
|
@@ -313,19 +326,40 @@ export async function askAllOptionalWorkflows({
|
|
|
313
326
|
}
|
|
314
327
|
}
|
|
315
328
|
|
|
316
|
-
// ──
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
// ── pr_comment: PR에 달 리뷰·요약 (#566에서 통합) ──
|
|
330
|
+
// 종전에는 CodeRabbit 여부만 예/아니오로 물었고, AI 변경 요약은 물어보지도 않은 채
|
|
331
|
+
// 항상 복사된 뒤 런타임에 스스로 빠졌다. 그 결과 "CodeRabbit도 안 달고 요약도 안 다는"
|
|
332
|
+
// 저장소가 생겼다. 둘은 같은 자리(PR 댓글)를 놓고 경쟁하므로 한 번에 고르게 한다.
|
|
333
|
+
if (ask("code-review") || codeReviewCoderabbit === null || aiPrSummary === null) {
|
|
334
|
+
if (force || !tty || typeof io.select !== "function") {
|
|
335
|
+
// 비대화형 기본값: CodeRabbit은 별도 앱 설치가 있어야 실제로 동작하므로
|
|
336
|
+
// 자동화 환경에서 켜봐야 의미가 없다. 설정 없이 바로 도는 요약만 켠다.
|
|
319
337
|
codeReviewCoderabbit = codeReviewCoderabbit ?? false;
|
|
338
|
+
aiPrSummary = aiPrSummary ?? true;
|
|
320
339
|
} else {
|
|
321
340
|
say("");
|
|
322
|
-
say("
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
say(
|
|
341
|
+
say("💬 PR에 어떤 댓글을 받으시겠어요?");
|
|
342
|
+
say(" 두 가지는 하는 일이 다르고 한도도 따로라, 같이 켜도 서로 방해하지 않습니다.");
|
|
343
|
+
say(" · 변경 요약 — 무엇이 바뀌었는지 (추가 설정 없이 바로 동작)");
|
|
344
|
+
say(" · 코드 리뷰 — 버그·개선점 지적 (CodeRabbit 앱 설치 필요, 공개 저장소 무료)");
|
|
345
|
+
const ans = await io.select({
|
|
346
|
+
message: "PR 댓글 방식을 선택하세요",
|
|
347
|
+
options: [
|
|
348
|
+
{ value: "both", label: "둘 다 (추천 · 무료로 쓸 수 있습니다)" },
|
|
349
|
+
{ value: "summary", label: "변경 요약만 (추가 설정 불필요)" },
|
|
350
|
+
{ value: "coderabbit", label: "코드 리뷰만 (CodeRabbit)" },
|
|
351
|
+
{ value: "none", label: "사용 안 함" },
|
|
352
|
+
],
|
|
353
|
+
});
|
|
354
|
+
// 취소·미응답은 추천값으로 — 다른 축과 같은 방어 패턴이다.
|
|
355
|
+
const PR_COMMENT_CHOICES = ["summary", "coderabbit", "both", "none"];
|
|
356
|
+
const pick = (!isCancel(ans) && PR_COMMENT_CHOICES.includes(ans)) ? ans : "summary";
|
|
357
|
+
codeReviewCoderabbit = pick === "coderabbit" || pick === "both";
|
|
358
|
+
aiPrSummary = pick === "summary" || pick === "both";
|
|
359
|
+
say(`PR 댓글: ${{ summary: "AI 변경 요약", coderabbit: "CodeRabbit", both: "둘 다", none: "사용 안 함" }[pick]}`);
|
|
326
360
|
// #481 — "사용"만으로는 안 붙는다. 앱 설치 + 레포 접근 권한이 있어야 실제로 리뷰가 달린다.
|
|
327
361
|
if (codeReviewCoderabbit) {
|
|
328
|
-
say(" ⚠️
|
|
362
|
+
say(" ⚠️ CodeRabbit은 추가 설정이 필요합니다:");
|
|
329
363
|
say(" 1) https://coderabbit.ai 접속 → GitHub으로 로그인");
|
|
330
364
|
say(" 2) CodeRabbit GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|
|
331
365
|
say(" (이 단계를 안 하면 워크플로우는 켜져도 PR에 리뷰 댓글이 달리지 않습니다)");
|
|
@@ -333,29 +367,12 @@ export async function askAllOptionalWorkflows({
|
|
|
333
367
|
}
|
|
334
368
|
}
|
|
335
369
|
|
|
336
|
-
// ── changelog:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
say("📝 릴리스 노트(changelog)는 1순위로 뭘로 만들까요?");
|
|
343
|
-
say(" GitHub AI는 설정 없이 바로 됩니다. 나머지는 나중에 GitHub Secret 등록이 필요할 수 있어요.");
|
|
344
|
-
// #481 — 하나만 골라야 하는 게 아니다. 고른 게 실패하면 자동 폴백하므로 안심하고 고르라고 안내.
|
|
345
|
-
say(" ✅ 고른 방식이 실패해도 자동으로 GitHub AI → 커밋 분석 순으로 폴백하니 릴리스 노트는 항상 생성됩니다.");
|
|
346
|
-
const ans = await io.select({
|
|
347
|
-
message: "1순위 changelog 생성기를 선택하세요 (실패 시 자동 폴백)",
|
|
348
|
-
options: [
|
|
349
|
-
{ value: "github-ai", label: "GitHub AI (추천 · 설정 불필요)" },
|
|
350
|
-
{ value: "coderabbit", label: "CodeRabbit" },
|
|
351
|
-
{ value: "openai", label: "OpenAI 호환 API (키 등록 필요)" },
|
|
352
|
-
{ value: "commit", label: "커밋 분석만 (AI 없음 · 최후 안전망)" },
|
|
353
|
-
],
|
|
354
|
-
});
|
|
355
|
-
changelogProvider = (!isCancel(ans) && CHANGELOG_PROVIDERS.includes(ans)) ? ans : (changelogProvider ?? "github-ai");
|
|
356
|
-
say(`changelog 생성기: ${changelogProvider}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
370
|
+
// ── changelog: 생성기를 묻지 않는다 (#566) ──
|
|
371
|
+
// 사용자의 목적은 "릴리스 노트가 잘 나오는 것"이지 provider 선택이 아니다. 무엇을 고를지
|
|
372
|
+
// 알 수 없는 질문이었고 기본값(github-ai)마저 죽어 있었다. 이제 워크플로우가 스스로
|
|
373
|
+
// 최선을 찾는다 — PR 본문 존중 → Copilot → 외부 AI(키 있으면) → 커밋 분석.
|
|
374
|
+
// 저장값은 그대로 두되 죽은 값만 살아있는 것으로 옮긴다.
|
|
375
|
+
changelogProvider = migrateProvider(changelogProvider) ?? "commit";
|
|
359
376
|
|
|
360
377
|
// ollama 선택 시에만 base_url 질문 (나머지 provider는 preset base_url 자동 — #455)
|
|
361
378
|
if (changelogProvider === "ollama" && (ask("changelog") || changelogBaseUrl === null || changelogBaseUrl === "")) {
|
|
@@ -408,7 +425,9 @@ export async function askAllOptionalWorkflows({
|
|
|
408
425
|
return {
|
|
409
426
|
deploy: finalDeploy, publish: finalPublish, secretBackup: secretBackup === true,
|
|
410
427
|
codeReviewCoderabbit: codeReviewCoderabbit === true,
|
|
411
|
-
|
|
428
|
+
// #566 — AI 변경 요약 워크플로우 포함 여부. 기본 true(추가 설정 없이 바로 동작).
|
|
429
|
+
aiPrSummary: aiPrSummary !== false,
|
|
430
|
+
changelogProvider: changelogProvider ?? "commit",
|
|
412
431
|
changelogBaseUrl: changelogBaseUrl ?? "",
|
|
413
432
|
deployBranch: deployBranch ?? "develop",
|
|
414
433
|
deployBranchReady, // #490 — true=존재/생성 확인됨, false=거절/실패, null=확인 안 함
|
|
@@ -25,8 +25,38 @@ function typeInventory(projectTypesDir, type) {
|
|
|
25
25
|
return files;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
|
|
28
|
+
// common/ 아래 "선택했을 때만 복사되는" 폴더들 (#566).
|
|
29
|
+
// 켜면 복사 엔진이 넣어주지만, 끄면 이미 있던 파일이 그대로 남아 계속 실행됐다.
|
|
30
|
+
// 축을 끈 사용자에게는 그게 곧 "껐는데 왜 도나"가 된다 — 여기서 고아로 잡는다.
|
|
31
|
+
// deploy는 타겟별 폴더라 선택된 타겟만 살리고 나머지를 대상으로 삼는다.
|
|
32
|
+
function commonOptionalOrphans(commonDir, opts) {
|
|
33
|
+
const { includeSecretBackup = false, aiPrSummary = true, deployTarget = "docker-ssh" } = opts;
|
|
34
|
+
const out = [];
|
|
35
|
+
|
|
36
|
+
const gated = [
|
|
37
|
+
["secret-backup", includeSecretBackup, "secret-backup"],
|
|
38
|
+
["pr-summary", aiPrSummary, "pr-summary"],
|
|
39
|
+
];
|
|
40
|
+
for (const [dir, enabled, label] of gated) {
|
|
41
|
+
if (enabled) continue;
|
|
42
|
+
const d = join(commonDir, dir);
|
|
43
|
+
if (!exists(d)) continue;
|
|
44
|
+
for (const f of listYamlFiles(d)) out.push({ filename: f, type: label });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// deploy/<target> — 선택된 타겟 외 폴더는 전부 고아 후보
|
|
48
|
+
const deployRoot = join(commonDir, "deploy");
|
|
49
|
+
if (exists(deployRoot)) {
|
|
50
|
+
for (const e of readdirSync(deployRoot, { withFileTypes: true })) {
|
|
51
|
+
if (!e.isDirectory() || e.name === deployTarget) continue;
|
|
52
|
+
for (const f of listYamlFiles(join(deployRoot, e.name))) out.push({ filename: f, type: `deploy/${e.name}` });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 선택 안 된 타입·옵션의 템플릿 워크플로우가 대상 레포에 실재하면 고아로 반환.
|
|
59
|
+
export function detectOrphanWorkflows({ tempDir, targetRoot = ".", selectedTypes = [], options = null }) {
|
|
30
60
|
const projectTypesDir = join(tempDir, PATHS.workflowsDir, PATHS.projectTypesDir);
|
|
31
61
|
if (!exists(projectTypesDir)) return [];
|
|
32
62
|
const selected = new Set(selectedTypes);
|
|
@@ -42,6 +72,14 @@ export function detectOrphanWorkflows({ tempDir, targetRoot = ".", selectedTypes
|
|
|
42
72
|
if (existsSync(join(workflowsDir, f))) orphans.push({ filename: f, type: e.name });
|
|
43
73
|
}
|
|
44
74
|
}
|
|
75
|
+
// common의 조건부 폴더 — options를 준 호출부에서만 검사한다(구 호출부 동작 보존).
|
|
76
|
+
if (options) {
|
|
77
|
+
const commonDir = join(projectTypesDir, "common");
|
|
78
|
+
for (const o of commonOptionalOrphans(commonDir, options)) {
|
|
79
|
+
if (keep.has(o.filename)) continue;
|
|
80
|
+
if (existsSync(join(workflowsDir, o.filename))) orphans.push(o);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
45
83
|
return orphans.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
46
84
|
}
|
|
47
85
|
|
package/src/core/verify.js
CHANGED
|
@@ -68,7 +68,7 @@ const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
|
|
|
68
68
|
|
|
69
69
|
// 없어도 워크플로우가 도는 secret — 폴백이 문서화돼 있다. 필수와 섞어 "등록해야 동작합니다"라고
|
|
70
70
|
// 하면 안내 자체를 못 믿게 되므로 분리한다.
|
|
71
|
-
// MODEL_API_KEY → 없으면
|
|
71
|
+
// MODEL_API_KEY → 등록하면 외부 AI 요약, 없으면 Copilot → 커밋 분석 (#455·#566 사다리)
|
|
72
72
|
// _GITHUB_PAT_TOKEN → 없으면 GITHUB_TOKEN으로 머지하고 후속 워크플로우를 직접 깨운다 (#551)
|
|
73
73
|
export const OPTIONAL_SECRETS = new Set(["MODEL_API_KEY", "_GITHUB_PAT_TOKEN"]);
|
|
74
74
|
|
package/src/core/version-yml.js
CHANGED
|
@@ -56,7 +56,7 @@ const HEADER = `# ==============================================================
|
|
|
56
56
|
// (options-ask.js가 이 함수를 import한다 — 순환 방지 위해 여기(version-yml)에 정의.)
|
|
57
57
|
export function parseTemplateOptions(content) {
|
|
58
58
|
const out = { deploy: null, publish: null, secretBackup: null,
|
|
59
|
-
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null,
|
|
59
|
+
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null, aiPrSummary: null,
|
|
60
60
|
deployBranch: null, intent: null, semverAuto: null, appRelease: null };
|
|
61
61
|
// deploy_branch는 metadata 직속(#456) — template.options 밖이라 별도로 스캔한다.
|
|
62
62
|
for (const line of String(content || "").split("\n")) {
|
|
@@ -83,6 +83,10 @@ export function parseTemplateOptions(content) {
|
|
|
83
83
|
if (inCodeReview) {
|
|
84
84
|
const cm = line.match(/^\s+coderabbit:\s*(.+)/);
|
|
85
85
|
if (cm) { const v = strip(cm[1]); if (v === "true") out.codeReviewCoderabbit = true; if (v === "false") out.codeReviewCoderabbit = false; continue; }
|
|
86
|
+
// #566 — AI 변경 요약 워크플로우 포함 여부. 키가 없으면 null(미설정)로 두어
|
|
87
|
+
// 기존 저장소가 업데이트할 때 마법사가 한 번 물어볼 수 있게 한다.
|
|
88
|
+
const am = line.match(/^\s+ai_summary:\s*(.+)/);
|
|
89
|
+
if (am) { const v = strip(am[1]); if (v === "true") out.aiPrSummary = true; if (v === "false") out.aiPrSummary = false; continue; }
|
|
86
90
|
}
|
|
87
91
|
if (inChangelog) {
|
|
88
92
|
const pm = line.match(/^\s+provider:\s*(.+)/);
|
|
@@ -313,7 +317,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
313
317
|
// template 옵션 블록 (.sh save_template_options 신규 추가 케이스). templateOptions 지정 시.
|
|
314
318
|
if (templateOptions) {
|
|
315
319
|
const { templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, optionsDate = today,
|
|
316
|
-
changelogProvider = "
|
|
320
|
+
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true, aiPrSummary = true, intent = null, mode = null,
|
|
317
321
|
semverAuto = true, appRelease = null } = templateOptions;
|
|
318
322
|
const publishJson = `[${publishTargets.map((t) => `"${t}"`).join(",")}]`;
|
|
319
323
|
// intent(프로젝트 성격, #485) — 미지정이면 deploy/publish에서 역추론해 기록 (재통합 시 진입 질문 생략용)
|
|
@@ -338,6 +342,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
338
342
|
}
|
|
339
343
|
out += ` code_review:\n`;
|
|
340
344
|
out += ` coderabbit: ${codeReviewCoderabbit}\n`;
|
|
345
|
+
out += ` ai_summary: ${aiPrSummary}\n`;
|
|
341
346
|
out += ` changelog:\n`;
|
|
342
347
|
out += ` provider: "${changelogProvider}"\n`;
|
|
343
348
|
out += ` base_url: "${changelogBaseUrl}"\n`;
|
package/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { detectOrphanWorkflows } from "./core/orphan-workflows.js";
|
|
|
17
17
|
import { createRunTrace, MIGRATION_DIR } from "./core/run-trace.js";
|
|
18
18
|
import { appendGuideEntry } from "./core/migration-guide.js";
|
|
19
19
|
import { resolveProjectPaths, markerForType } from "./core/paths-resolve.js";
|
|
20
|
-
import { applicableTargets } from "./core/options-ask.js";
|
|
20
|
+
import { applicableTargets, migrateProvider } from "./core/options-ask.js";
|
|
21
21
|
import { printBannerCompact } from "./ui/banner.js";
|
|
22
22
|
import { printSummary } from "./ui/summary.js";
|
|
23
23
|
import { runFull } from "./commands/full.js";
|
|
@@ -197,13 +197,15 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
197
197
|
deployTarget,
|
|
198
198
|
publishTargets,
|
|
199
199
|
includeSecretBackup: opts.includeSecretBackup ?? existing?.options?.secretBackup ?? false,
|
|
200
|
+
// #566 — CLI 값 우선, 없으면 저장값 보존, 그것도 없으면 true(설정 없이 바로 동작).
|
|
201
|
+
aiPrSummary: opts.aiPrSummary ?? existing?.options?.aiPrSummary ?? true,
|
|
200
202
|
// #502 — version 모드가 기존 full 통합 기록(mode)을 강등하지 않도록 (full이 우세)
|
|
201
203
|
recordMode: existing?.templateMode === "full" ? "full" : "version",
|
|
202
204
|
// 릴리스 배포 브랜치(#456): CLI 플래그 → version.yml 저장값 → 빈 값(미출력, 스킬이 develop 폴백)
|
|
203
205
|
deployBranch: opts.deployBranch || existing?.options?.deployBranch || "",
|
|
204
206
|
intent,
|
|
205
207
|
// changelog/code_review 축(#455): 비대화형은 저장값 → 기본값. null이 흘러 provider:"null"로 기록되던 버그 수정.
|
|
206
|
-
changelogProvider: existing?.options?.changelogProvider ?? "
|
|
208
|
+
changelogProvider: migrateProvider(existing?.options?.changelogProvider) ?? "commit",
|
|
207
209
|
changelogBaseUrl: existing?.options?.changelogBaseUrl ?? "",
|
|
208
210
|
codeReviewCoderabbit: existing?.options?.codeReviewCoderabbit ?? false,
|
|
209
211
|
// semver 자동 승격(#546): 저장값 → (기존 통합 레포면 false / 신규면 true).
|
|
@@ -276,7 +278,9 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
276
278
|
// 고아 타입 워크플로우 안내 (#487) — 비대화형은 자동 무해화 금지(배포 파이프라인일 수 있음), 안내만
|
|
277
279
|
if (recordArtifacts) {
|
|
278
280
|
const orphans = trace.step("orphan-scan",
|
|
279
|
-
() => detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types
|
|
281
|
+
() => detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types,
|
|
282
|
+
// 껐는데 남아 계속 도는 워크플로우도 함께 잡는다 (#566)
|
|
283
|
+
options: { includeSecretBackup: context.includeSecretBackup, aiPrSummary: context.aiPrSummary, deployTarget } }),
|
|
280
284
|
{ selectedTypes: types });
|
|
281
285
|
orphanPending = orphans.map((o) => o.filename);
|
|
282
286
|
for (const o of orphans) trace.event("orphan", "detected", o.filename, { type: o.type, action: "안내만(비대화형)" });
|
|
@@ -318,6 +322,8 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
318
322
|
mode: opts.mode, types, version, deployBranch: context.deployBranch, migrationGuidePath,
|
|
319
323
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
320
324
|
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
325
|
+
// #569 — 고른 것만 안내하려면 선택값이 필요하다
|
|
326
|
+
aiPrSummary: context.aiPrSummary, codeReviewCoderabbit: context.codeReviewCoderabbit,
|
|
321
327
|
logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
|
|
322
328
|
logFile: files?.logFile ?? null,
|
|
323
329
|
traceFile: files?.traceFile ?? null,
|
package/src/ui/summary.js
CHANGED
|
@@ -13,6 +13,7 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
13
13
|
const err = (s = "") => process.stderr.write(`${s}\n`);
|
|
14
14
|
// 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
|
|
15
15
|
const isTty = !!process.stderr.isTTY;
|
|
16
|
+
const BOLD = isTty ? "\u001b[1m" : "";
|
|
16
17
|
const YELLOW = isTty ? "\x1b[1;33m" : "";
|
|
17
18
|
const CYAN = isTty ? "\x1b[0;36m" : "";
|
|
18
19
|
const NC = isTty ? "\x1b[0m" : "";
|
|
@@ -190,9 +191,32 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
190
191
|
err(` → git checkout -b ${deployBranchName} && git push -u origin ${deployBranchName}`);
|
|
191
192
|
}
|
|
192
193
|
err("");
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// #569 — 선택한 것만 안내한다. 끈 기능의 설정법을 보여주면 무엇을 해야 하는지 흐려진다.
|
|
195
|
+
let step = 3;
|
|
196
|
+
if (ctx?.aiPrSummary !== false) {
|
|
197
|
+
err(` ${step}️⃣ (선택) AI가 다듬은 릴리스 노트 받기`);
|
|
198
|
+
err(" → 지금 상태로도 릴리스 노트는 나옵니다. 커밋 내용을 분석해 만들기 때문에");
|
|
199
|
+
err(" 아무 설정을 하지 않아도 됩니다. 아래는 문장을 더 매끄럽게 하고 싶을 때만 하세요.");
|
|
200
|
+
err("");
|
|
201
|
+
err(" ① Google AI Studio에서 키 발급 (무료 · 신용카드 불필요 · 2분)");
|
|
202
|
+
err(" https://aistudio.google.com/apikey");
|
|
203
|
+
err(" ② 이 저장소에 등록");
|
|
204
|
+
err(" Settings > Secrets and variables > Actions > New repository secret");
|
|
205
|
+
err(` ${BOLD}Name: GEMINI_API_KEY${NC} Secret: 발급받은 키(AIza... 로 시작)`);
|
|
206
|
+
err("");
|
|
207
|
+
err(" 💡 다른 서비스를 쓴다면 이름만 바꿔 등록하면 됩니다 — 등록한 것이 자동으로 쓰입니다.");
|
|
208
|
+
err(" OPENAI_API_KEY · ANTHROPIC_API_KEY · GROQ_API_KEY · MISTRAL_API_KEY");
|
|
209
|
+
err("");
|
|
210
|
+
step++;
|
|
211
|
+
}
|
|
212
|
+
if (ctx?.codeReviewCoderabbit === true) {
|
|
213
|
+
err(` ${step}️⃣ CodeRabbit 활성화`);
|
|
214
|
+
err(" → https://coderabbit.ai 로그인 → GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|
|
215
|
+
err(" → 이 단계를 안 하면 워크플로우는 켜져도 PR에 리뷰 댓글이 달리지 않습니다");
|
|
216
|
+
err(" → 공개 저장소는 무료입니다 (시간당 3회 리뷰 제한)");
|
|
217
|
+
err("");
|
|
218
|
+
step++;
|
|
219
|
+
}
|
|
196
220
|
err(SEPARATOR);
|
|
197
221
|
err("");
|
|
198
222
|
err(`${CYAN}📖 자세한 설정 방법은 다음 파일을 참고하세요:${NC}`);
|