projectops 4.2.12 → 4.2.13
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/commands/interactive.js +4 -7
- package/src/core/options-ask.js +61 -29
- package/src/ui/prompts.js +6 -1
- package/src/ui/summary.js +2 -2
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import { parseExisting } from "../core/version-yml.js";
|
|
|
12
12
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
13
13
|
import { runMigrations } from "../core/migrations/index.js";
|
|
14
14
|
import { resolveProjectPaths } from "../core/paths-resolve.js";
|
|
15
|
-
import { askAllOptionalWorkflows } from "../core/options-ask.js";
|
|
15
|
+
import { askAllOptionalWorkflows, OPTION_AXES } from "../core/options-ask.js";
|
|
16
16
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
17
17
|
import { listWorkflowConflicts } from "../core/copy/workflows.js";
|
|
18
18
|
import { createContext, VALID_TYPES } from "../context.js";
|
|
@@ -159,15 +159,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
159
159
|
} else if (what === "branch") {
|
|
160
160
|
const b = await io.askText("기본 브랜치", branch);
|
|
161
161
|
if (!isCancel(b) && b) branch = b;
|
|
162
|
-
} else if (what
|
|
163
|
-
//
|
|
162
|
+
} else if (OPTION_AXES.includes(what)) {
|
|
163
|
+
// #483 — 선택한 축 하나만 재질문 (scope). 나머지 옵션은 현재값 그대로 유지.
|
|
164
164
|
const r = await askAllOptionalWorkflows({
|
|
165
165
|
tempDir, types, targetRoot: cwd, defaultBranch: branch,
|
|
166
166
|
current: {
|
|
167
167
|
deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup,
|
|
168
168
|
codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch,
|
|
169
169
|
},
|
|
170
|
-
force: false, tty: realTty, forceAsk: true,
|
|
170
|
+
force: false, tty: realTty, forceAsk: true, scope: [what],
|
|
171
171
|
io: {
|
|
172
172
|
confirm: ({ message, initialValue }) => io.askYesNo(message, initialValue),
|
|
173
173
|
select: io.engineIo?.select,
|
|
@@ -182,9 +182,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
182
182
|
changelogProvider = r.changelogProvider;
|
|
183
183
|
changelogBaseUrl = r.changelogBaseUrl;
|
|
184
184
|
deployBranch = r.deployBranch;
|
|
185
|
-
} else if (what === "secret") {
|
|
186
|
-
const y = await io.askYesNo("Secret 백업 워크플로우를 포함할까요?", includeSecretBackup);
|
|
187
|
-
if (!isCancel(y)) includeSecretBackup = y === true;
|
|
188
185
|
}
|
|
189
186
|
}
|
|
190
187
|
}
|
package/src/core/options-ask.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function ensureDeployBranch({ targetRoot = ".", deployBranch = "",
|
|
|
24
24
|
say(`ℹ️ '${deployBranch}' 브랜치가 원격에는 있고 로컬에 없습니다 — 필요 시 'git switch ${deployBranch}'로 가져오세요.`);
|
|
25
25
|
return { created: false, pushed: false };
|
|
26
26
|
}
|
|
27
|
-
say(`⚠️
|
|
27
|
+
say(`⚠️ 개발(릴리스 소스) 브랜치 '${deployBranch}'가 없습니다 — 릴리스(${deployBranch}→${defaultBranch || "기본 브랜치"} PR)가 동작하려면 필요합니다.`);
|
|
28
28
|
const mk = await io.confirm({ message: `${defaultBranch || "현재"} 브랜치에서 '${deployBranch}' 브랜치를 만들까요?`, initialValue: true });
|
|
29
29
|
if (mk !== true) {
|
|
30
30
|
say(`→ 건너뜁니다. 나중에 직접: git checkout -b ${deployBranch} && git push -u origin ${deployBranch}`);
|
|
@@ -35,7 +35,8 @@ export async function ensureDeployBranch({ targetRoot = ".", deployBranch = "",
|
|
|
35
35
|
return { created: false, pushed: false };
|
|
36
36
|
}
|
|
37
37
|
say(`✅ 로컬 브랜치 '${deployBranch}' 생성 완료 (checkout은 하지 않았습니다)`);
|
|
38
|
-
|
|
38
|
+
// #481 — push 질문에 브랜치명 명시 ("어느 브랜치를 push하는지" 불명확 방지)
|
|
39
|
+
const up = await io.confirm({ message: `원격(origin)에 '${deployBranch}' 브랜치도 push할까요?`, initialValue: true });
|
|
39
40
|
if (up !== true) return { created: true, pushed: false };
|
|
40
41
|
if (pushBranch(targetRoot, deployBranch)) {
|
|
41
42
|
say(`✅ origin/${deployBranch} push 완료`);
|
|
@@ -85,15 +86,23 @@ async function askOptionalWorkflow({ dir, icon, short, desc, current, force, tty
|
|
|
85
86
|
return include;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// 옵션 질문의 축 키 (#483 수정 스코프 단위) — 수정 메뉴가 이 단위로 한 축만 재질문한다.
|
|
90
|
+
export const OPTION_AXES = ["deploy", "publish", "code-review", "changelog", "release-branch", "secret"];
|
|
91
|
+
|
|
88
92
|
// 배포/publish 축 + Secret 백업을 순서대로 질문 (.sh ask_all_optional_workflows 등가).
|
|
89
93
|
// tempDir: 템플릿 다운로드 루트 — project-types는 {tempDir}/.github/workflows/project-types
|
|
90
94
|
// current: { deploy: string|null, publish: string[]|null, secretBackup: bool|null } — CLI 명시값
|
|
91
|
-
//
|
|
95
|
+
// scope: null이면 전 축(초기 통합·전체 재질문). Set/배열이면 그 축만 forceAsk 대상 (#483 수정 메뉴 격리).
|
|
96
|
+
// 스코프 밖 축은 forceAsk여도 current/저장값을 그대로 유지하고 다시 묻지 않는다.
|
|
97
|
+
// 반환: { deploy, publish, secretBackup, codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch }
|
|
92
98
|
export async function askAllOptionalWorkflows({
|
|
93
99
|
tempDir, types = [], current = {}, targetRoot = ".",
|
|
94
|
-
force = false, tty = true, io = {}, forceAsk = false, defaultBranch = "",
|
|
100
|
+
force = false, tty = true, io = {}, forceAsk = false, defaultBranch = "", scope = null,
|
|
95
101
|
}) {
|
|
96
102
|
const say = io.log || ((m) => process.stderr.write(`${m}\n`));
|
|
103
|
+
// 축별 재질문 여부: 전역 forceAsk이고, scope가 없거나 그 축을 포함할 때만 강제 질문.
|
|
104
|
+
const scopeSet = scope == null ? null : new Set(scope);
|
|
105
|
+
const ask = (axis) => forceAsk && (scopeSet === null || scopeSet.has(axis));
|
|
97
106
|
let deploy = current.deploy ?? null;
|
|
98
107
|
let publish = current.publish ?? null;
|
|
99
108
|
let secretBackup = current.secretBackup ?? null;
|
|
@@ -139,36 +148,47 @@ export async function askAllOptionalWorkflows({
|
|
|
139
148
|
if (deploy === null) deploy = "none";
|
|
140
149
|
if (publish === null) publish = [];
|
|
141
150
|
} else {
|
|
142
|
-
|
|
151
|
+
const willAskDeploy = ask("deploy") || deploy === null;
|
|
152
|
+
const willAskPublish = ask("publish") || publish === null;
|
|
153
|
+
// #480 — deploy·publish는 서로 다른 두 축이다. 둘 다 물을 참이면 먼저 큰 그림을 한 번 안내한다.
|
|
154
|
+
// (수정 메뉴로 한 축만 고칠 때는 맥락이 이미 명확하므로 생략)
|
|
155
|
+
if (willAskDeploy && willAskPublish && tty && typeof io.select === "function") {
|
|
156
|
+
say("");
|
|
157
|
+
say("🧭 배포는 두 가지가 따로 있습니다 — 서로 독립이라 각각 답하시면 됩니다:");
|
|
158
|
+
say(" 1) 실행물 배포 — 서버/호스팅에 올려 돌리는 것 (Docker, Vercel …)");
|
|
159
|
+
say(" 2) 라이브러리 배포(publish) — 남이 가져다 쓰게 레지스트리에 내는 것 (Nexus, npm …)");
|
|
160
|
+
say(" 먼저 (1) 실행물 배포부터 물어보고, 이어서 (2) 라이브러리 배포를 물어봅니다.");
|
|
161
|
+
}
|
|
162
|
+
if (willAskDeploy) {
|
|
143
163
|
if (force || !tty || typeof io.select !== "function") {
|
|
144
164
|
deploy = deploy ?? "docker-ssh";
|
|
145
165
|
} else {
|
|
146
166
|
say("");
|
|
147
|
-
say("🚀
|
|
148
|
-
say(" 서버·호스팅에 올릴 계획이 있으면 고르고,
|
|
167
|
+
say("🚀 (1) 실행물(서버/앱)을 어디에 올리나요?");
|
|
168
|
+
say(" 서버·호스팅에 올릴 계획이 있으면 고르고, 없으면 '서버에 올리지 않음'으로 두세요.");
|
|
149
169
|
const ans = await io.select({
|
|
150
|
-
message: "배포 방식을 선택하세요",
|
|
170
|
+
message: "실행물 배포 방식을 선택하세요",
|
|
151
171
|
options: [
|
|
152
172
|
{ value: "docker-ssh", label: "Docker + SSH 서버 배포 (기본)" },
|
|
153
173
|
{ value: "vercel", label: "Vercel" },
|
|
154
|
-
{ value: "none", label: "
|
|
174
|
+
{ value: "none", label: "서버에 올리지 않음 (빌드 검증만 · 라이브러리는 다음에서 선택)" },
|
|
155
175
|
],
|
|
156
176
|
});
|
|
157
177
|
deploy = (!isCancel(ans) && DEPLOY_TARGETS.includes(ans)) ? ans : (deploy ?? "docker-ssh");
|
|
158
|
-
say(
|
|
178
|
+
say(`실행물 배포: ${deploy}`);
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
// ── ③ publish 타겟 (다중 선택) ──
|
|
163
|
-
if (
|
|
183
|
+
if (willAskPublish) {
|
|
164
184
|
if (force || !tty || typeof io.multiselect !== "function") {
|
|
165
185
|
publish = publish ?? [];
|
|
166
186
|
} else {
|
|
167
187
|
say("");
|
|
168
|
-
say("📦
|
|
169
|
-
say("
|
|
188
|
+
say("📦 (2) 이 프로젝트를 남이 가져다 쓰는 라이브러리로도 배포(publish)하나요?");
|
|
189
|
+
say(" (1) 실행물 배포와 별개입니다. 해당되는 걸 고르고, 라이브러리 배포를 안 하면 아무것도 고르지 말고 Enter.");
|
|
170
190
|
const ans = await io.multiselect({
|
|
171
|
-
message: "
|
|
191
|
+
message: "라이브러리 배포 타겟 (없으면 선택 없이 Enter = 배포 안 함)",
|
|
172
192
|
options: [
|
|
173
193
|
{ value: "nexus", label: "사내 Maven(Nexus) 라이브러리 배포" },
|
|
174
194
|
{ value: "npm", label: "공개 npmjs 패키지 배포 (NPM_TOKEN)" },
|
|
@@ -180,13 +200,13 @@ export async function askAllOptionalWorkflows({
|
|
|
180
200
|
publish = (!isCancel(ans) && Array.isArray(ans))
|
|
181
201
|
? ans.filter((t) => PUBLISH_TARGETS.includes(t))
|
|
182
202
|
: (publish ?? []);
|
|
183
|
-
say(
|
|
203
|
+
say(publish.length ? `라이브러리 배포: ${publish.join(", ")}` : "라이브러리 배포: 안 함");
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
// ── code_review: CodeRabbit AI 코드 리뷰 (changelog와 무관 — #455) ──
|
|
189
|
-
if (
|
|
209
|
+
if (ask("code-review") || codeReviewCoderabbit === null) {
|
|
190
210
|
if (force || !tty || typeof io.confirm !== "function") {
|
|
191
211
|
codeReviewCoderabbit = codeReviewCoderabbit ?? false;
|
|
192
212
|
} else {
|
|
@@ -195,24 +215,33 @@ export async function askAllOptionalWorkflows({
|
|
|
195
215
|
const ans = await io.confirm({ message: "CodeRabbit AI 코드 리뷰 사용", initialValue: false });
|
|
196
216
|
codeReviewCoderabbit = (ans === true && !isCancel(ans));
|
|
197
217
|
say(`CodeRabbit 코드 리뷰: ${codeReviewCoderabbit ? "사용" : "미사용"}`);
|
|
218
|
+
// #481 — "사용"만으로는 안 붙는다. 앱 설치 + 레포 접근 권한이 있어야 실제로 리뷰가 달린다.
|
|
219
|
+
if (codeReviewCoderabbit) {
|
|
220
|
+
say(" ⚠️ 실제로 리뷰가 붙으려면 추가 설정이 필요합니다:");
|
|
221
|
+
say(" 1) https://coderabbit.ai 접속 → GitHub으로 로그인");
|
|
222
|
+
say(" 2) CodeRabbit GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|
|
223
|
+
say(" (이 단계를 안 하면 워크플로우는 켜져도 PR에 리뷰 댓글이 달리지 않습니다)");
|
|
224
|
+
}
|
|
198
225
|
}
|
|
199
226
|
}
|
|
200
227
|
|
|
201
228
|
// ── changelog: 릴리스 노트 생성기 (기본 커서 = github-ai — #455) ──
|
|
202
|
-
if (
|
|
229
|
+
if (ask("changelog") || changelogProvider === null) {
|
|
203
230
|
if (force || !tty || typeof io.select !== "function") {
|
|
204
231
|
changelogProvider = changelogProvider ?? "github-ai";
|
|
205
232
|
} else {
|
|
206
233
|
say("");
|
|
207
|
-
say("📝 릴리스 노트(changelog)는 뭘로 만들까요?");
|
|
234
|
+
say("📝 릴리스 노트(changelog)는 1순위로 뭘로 만들까요?");
|
|
208
235
|
say(" GitHub AI는 설정 없이 바로 됩니다. 나머지는 나중에 GitHub Secret 등록이 필요할 수 있어요.");
|
|
236
|
+
// #481 — 하나만 골라야 하는 게 아니다. 고른 게 실패하면 자동 폴백하므로 안심하고 고르라고 안내.
|
|
237
|
+
say(" ✅ 고른 방식이 실패해도 자동으로 GitHub AI → 커밋 분석 순으로 폴백하니 릴리스 노트는 항상 생성됩니다.");
|
|
209
238
|
const ans = await io.select({
|
|
210
|
-
message: "changelog 생성기를 선택하세요",
|
|
239
|
+
message: "1순위 changelog 생성기를 선택하세요 (실패 시 자동 폴백)",
|
|
211
240
|
options: [
|
|
212
241
|
{ value: "github-ai", label: "GitHub AI (추천 · 설정 불필요)" },
|
|
213
242
|
{ value: "coderabbit", label: "CodeRabbit" },
|
|
214
243
|
{ value: "openai", label: "OpenAI 호환 API (키 등록 필요)" },
|
|
215
|
-
{ value: "commit", label: "커밋 분석만 (AI 없음)" },
|
|
244
|
+
{ value: "commit", label: "커밋 분석만 (AI 없음 · 최후 안전망)" },
|
|
216
245
|
],
|
|
217
246
|
});
|
|
218
247
|
changelogProvider = (!isCancel(ans) && CHANGELOG_PROVIDERS.includes(ans)) ? ans : (changelogProvider ?? "github-ai");
|
|
@@ -221,7 +250,7 @@ export async function askAllOptionalWorkflows({
|
|
|
221
250
|
}
|
|
222
251
|
|
|
223
252
|
// ollama 선택 시에만 base_url 질문 (나머지 provider는 preset base_url 자동 — #455)
|
|
224
|
-
if (changelogProvider === "ollama" && (
|
|
253
|
+
if (changelogProvider === "ollama" && (ask("changelog") || changelogBaseUrl === null || changelogBaseUrl === "")) {
|
|
225
254
|
if (force || !tty || typeof io.text !== "function") {
|
|
226
255
|
changelogBaseUrl = changelogBaseUrl ?? "";
|
|
227
256
|
} else {
|
|
@@ -233,18 +262,21 @@ export async function askAllOptionalWorkflows({
|
|
|
233
262
|
changelogBaseUrl = "";
|
|
234
263
|
}
|
|
235
264
|
|
|
236
|
-
// ──
|
|
237
|
-
//
|
|
238
|
-
|
|
265
|
+
// ── 릴리스 소스(개발) 브랜치 (#456 필드 deploy_branch — 이름과 달리 "개발 브랜치"다, #482) ──
|
|
266
|
+
// 이 값은 릴리스 PR(개발→기본)의 head, 즉 개발한 걸 모아 기본 브랜치로 올리는 브랜치다.
|
|
267
|
+
// "배포 브랜치"가 아니다 — 배포가 도는 곳은 기본 브랜치(default) 쪽 개념. #482 참조.
|
|
268
|
+
if (ask("release-branch") || deployBranch === null) {
|
|
239
269
|
if (force || !tty || typeof io.text !== "function") {
|
|
240
270
|
deployBranch = deployBranch ?? "develop";
|
|
241
271
|
} else {
|
|
272
|
+
const base = defaultBranch || "main"; // git으로 감지된 기본 브랜치 (#481 동적 안내)
|
|
242
273
|
say("");
|
|
243
|
-
say("🌿
|
|
244
|
-
say(
|
|
245
|
-
|
|
274
|
+
say("🌿 개발한 코드를 모아서 배포로 올리는 '개발 브랜치'는 무엇인가요?");
|
|
275
|
+
say(` 감지된 기본(배포) 브랜치는 '${base}'입니다. 개발은 보통 그 앞단 브랜치(예: develop)에서 모아 올립니다.`);
|
|
276
|
+
say(" 특별한 이유가 없으면 develop 그대로 두세요.");
|
|
277
|
+
const ans = await io.text({ message: "개발(릴리스 소스) 브랜치", initialValue: deployBranch ?? "develop" });
|
|
246
278
|
deployBranch = (typeof ans === "string" && !isCancel(ans) && ans.trim()) ? ans.trim() : (deployBranch ?? "develop");
|
|
247
|
-
say(
|
|
279
|
+
say(`개발(릴리스 소스) 브랜치: ${deployBranch}`);
|
|
248
280
|
// 브랜치 존재 확인 + 생성 제안 (#477) — 없으면 릴리스 파이프라인이 조용히 놀게 된다
|
|
249
281
|
await ensureDeployBranch({ targetRoot, deployBranch, defaultBranch, io, say });
|
|
250
282
|
}
|
|
@@ -256,7 +288,7 @@ export async function askAllOptionalWorkflows({
|
|
|
256
288
|
secretBackup = await askOptionalWorkflow({
|
|
257
289
|
dir: join(ptDir, "common", "secret-backup"), icon: "🔐", short: "Secret 서버 백업",
|
|
258
290
|
desc: "GitHub Secret에 저장한 설정 파일을 SSH로 서버에 업로드·이력관리하는 워크플로우입니다.",
|
|
259
|
-
current: secretBackup, force, tty, io, forceAsk, say,
|
|
291
|
+
current: secretBackup, force, tty, io, forceAsk: ask("secret"), say,
|
|
260
292
|
});
|
|
261
293
|
|
|
262
294
|
return {
|
package/src/ui/prompts.js
CHANGED
|
@@ -39,7 +39,12 @@ export async function editMenu({ showOptional = false } = {}) {
|
|
|
39
39
|
{ value: "branch", label: "기본 브랜치" },
|
|
40
40
|
];
|
|
41
41
|
if (showOptional) {
|
|
42
|
-
|
|
42
|
+
// #483 — 항목별 격리: 한 축만 골라 그 축만 재질문한다 (통짜 "배포/Publish 방식" 분해)
|
|
43
|
+
options.push({ value: "deploy", label: "배포 방식 (서버 실행물)" });
|
|
44
|
+
options.push({ value: "publish", label: "라이브러리 배포(publish) 타겟" });
|
|
45
|
+
options.push({ value: "code-review", label: "CodeRabbit 코드 리뷰" });
|
|
46
|
+
options.push({ value: "changelog", label: "릴리스 노트(changelog) 생성기" });
|
|
47
|
+
options.push({ value: "release-branch", label: "릴리스 소스(개발) 브랜치" });
|
|
43
48
|
options.push({ value: "secret", label: "Secret 백업 포함 여부" });
|
|
44
49
|
}
|
|
45
50
|
options.push({ value: "done", label: "모두 맞음, 계속" });
|
package/src/ui/summary.js
CHANGED
|
@@ -142,8 +142,8 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
142
142
|
err(` 2️⃣ ${deployBranchName} 브랜치 생성 (아직 없다면)`);
|
|
143
143
|
err(` → git checkout -b ${deployBranchName} && git push -u origin ${deployBranchName}`);
|
|
144
144
|
err("");
|
|
145
|
-
err(" 3️⃣ CodeRabbit 활성화");
|
|
146
|
-
err(" → https://coderabbit.ai
|
|
145
|
+
err(" 3️⃣ CodeRabbit 활성화 (코드 리뷰를 켰다면)");
|
|
146
|
+
err(" → https://coderabbit.ai 로그인 → GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|
|
147
147
|
err("");
|
|
148
148
|
err(SEPARATOR);
|
|
149
149
|
err("");
|