projectops 4.6.1 → 4.7.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/full.js +3 -3
- package/src/commands/interactive.js +7 -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 +50 -35
- package/src/core/orphan-workflows.js +40 -2
- package/src/core/version-yml.js +7 -2
- package/src/index.js +7 -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/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"),
|
|
@@ -445,7 +448,7 @@ function summarize({ mode, types, version, branch, deployTarget, publishTargets,
|
|
|
445
448
|
lines.push(`배포 방식 : ${deployTarget || "docker-ssh"}`);
|
|
446
449
|
lines.push(`Publish : ${(publishTargets ?? []).join(",") || "없음"}`);
|
|
447
450
|
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
448
|
-
lines.push(`Changelog : ${changelogProvider || "
|
|
451
|
+
lines.push(`Changelog : ${changelogProvider || "commit"}`);
|
|
449
452
|
lines.push(`CodeRabbit 리뷰 : ${codeReviewCoderabbit ? "사용" : "미사용"}`);
|
|
450
453
|
}
|
|
451
454
|
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,36 @@ 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은 외부 앱 설치가 필요하므로 끄고, 자체 요약만 켠다.
|
|
319
336
|
codeReviewCoderabbit = codeReviewCoderabbit ?? false;
|
|
337
|
+
aiPrSummary = aiPrSummary ?? true;
|
|
320
338
|
} else {
|
|
321
339
|
say("");
|
|
322
|
-
say("
|
|
323
|
-
const ans = await io.
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
say("💬 PR에 리뷰·요약 댓글을 어떻게 받으시겠어요?");
|
|
341
|
+
const ans = await io.select({
|
|
342
|
+
message: "PR 댓글 방식을 선택하세요",
|
|
343
|
+
options: [
|
|
344
|
+
{ value: "summary", label: "AI 변경 요약 (추천 · 추가 설정 불필요)" },
|
|
345
|
+
{ value: "coderabbit", label: "CodeRabbit 코드 리뷰 (외부 앱 설치 필요)" },
|
|
346
|
+
{ value: "both", label: "둘 다" },
|
|
347
|
+
{ value: "none", label: "사용 안 함" },
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
// 취소·미응답은 추천값으로 — 다른 축과 같은 방어 패턴이다.
|
|
351
|
+
const PR_COMMENT_CHOICES = ["summary", "coderabbit", "both", "none"];
|
|
352
|
+
const pick = (!isCancel(ans) && PR_COMMENT_CHOICES.includes(ans)) ? ans : "summary";
|
|
353
|
+
codeReviewCoderabbit = pick === "coderabbit" || pick === "both";
|
|
354
|
+
aiPrSummary = pick === "summary" || pick === "both";
|
|
355
|
+
say(`PR 댓글: ${{ summary: "AI 변경 요약", coderabbit: "CodeRabbit", both: "둘 다", none: "사용 안 함" }[pick]}`);
|
|
326
356
|
// #481 — "사용"만으로는 안 붙는다. 앱 설치 + 레포 접근 권한이 있어야 실제로 리뷰가 달린다.
|
|
327
357
|
if (codeReviewCoderabbit) {
|
|
328
|
-
say(" ⚠️
|
|
358
|
+
say(" ⚠️ CodeRabbit은 추가 설정이 필요합니다:");
|
|
329
359
|
say(" 1) https://coderabbit.ai 접속 → GitHub으로 로그인");
|
|
330
360
|
say(" 2) CodeRabbit GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|
|
331
361
|
say(" (이 단계를 안 하면 워크플로우는 켜져도 PR에 리뷰 댓글이 달리지 않습니다)");
|
|
@@ -333,29 +363,12 @@ export async function askAllOptionalWorkflows({
|
|
|
333
363
|
}
|
|
334
364
|
}
|
|
335
365
|
|
|
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
|
-
}
|
|
366
|
+
// ── changelog: 생성기를 묻지 않는다 (#566) ──
|
|
367
|
+
// 사용자의 목적은 "릴리스 노트가 잘 나오는 것"이지 provider 선택이 아니다. 무엇을 고를지
|
|
368
|
+
// 알 수 없는 질문이었고 기본값(github-ai)마저 죽어 있었다. 이제 워크플로우가 스스로
|
|
369
|
+
// 최선을 찾는다 — PR 본문 존중 → Copilot → 외부 AI(키 있으면) → 커밋 분석.
|
|
370
|
+
// 저장값은 그대로 두되 죽은 값만 살아있는 것으로 옮긴다.
|
|
371
|
+
changelogProvider = migrateProvider(changelogProvider) ?? "commit";
|
|
359
372
|
|
|
360
373
|
// ollama 선택 시에만 base_url 질문 (나머지 provider는 preset base_url 자동 — #455)
|
|
361
374
|
if (changelogProvider === "ollama" && (ask("changelog") || changelogBaseUrl === null || changelogBaseUrl === "")) {
|
|
@@ -408,7 +421,9 @@ export async function askAllOptionalWorkflows({
|
|
|
408
421
|
return {
|
|
409
422
|
deploy: finalDeploy, publish: finalPublish, secretBackup: secretBackup === true,
|
|
410
423
|
codeReviewCoderabbit: codeReviewCoderabbit === true,
|
|
411
|
-
|
|
424
|
+
// #566 — AI 변경 요약 워크플로우 포함 여부. 기본 true(추가 설정 없이 바로 동작).
|
|
425
|
+
aiPrSummary: aiPrSummary !== false,
|
|
426
|
+
changelogProvider: changelogProvider ?? "commit",
|
|
412
427
|
changelogBaseUrl: changelogBaseUrl ?? "",
|
|
413
428
|
deployBranch: deployBranch ?? "develop",
|
|
414
429
|
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/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: "안내만(비대화형)" });
|