project-auto-wizard 0.2.0 → 0.3.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 +15 -4
- package/package.json +1 -1
- 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/issue_helper.py +334 -0
- package/payload/scripts/truncate_release_notes.py +89 -0
- package/payload/version.yml.template +1 -0
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +47 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +4 -4
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +1 -1
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +1 -1
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +1 -1
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +6 -6
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +1 -1
- package/payload/workflows/spring/{PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml → nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml} +8 -2
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +8 -6
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +2 -2
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +8 -8
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +2 -2
- package/src/cli/args.js +9 -0
- package/src/cli/help.js +2 -1
- package/src/commands/dry-run.js +7 -16
- package/src/commands/full.js +75 -16
- package/src/commands/interactive.js +47 -6
- package/src/commands/uninstall.js +3 -1
- package/src/core/copy/simple.js +2 -2
- package/src/core/copy/workflows.js +32 -29
- package/src/core/deploy-style.js +90 -0
- package/src/core/detect-fs.js +36 -7
- package/src/core/detect.js +68 -3
- package/src/core/install-log.js +182 -0
- package/src/core/options-ask.js +5 -2
- package/src/core/paths-resolve.js +4 -8
- package/src/core/removal-plan.js +1 -1
- package/src/core/verify.js +86 -0
- package/src/core/version-yml.js +31 -4
- package/src/core/wizard-env.js +6 -3
- package/src/index.js +15 -2
- package/src/ui/env-plan.js +63 -33
- package/src/ui/prompts.js +41 -2
- package/src/ui/status-cards.js +7 -3
- package/src/ui/summary.js +56 -6
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 대화형 3지선(기존 파일 충돌)은 copyWorkflowsInteractive(async)가 결정 Map을 만들어
|
|
4
4
|
// 동기 엔진(copyWorkflows)에 hooks.decisions로 전달한다 — 기존 시그니처·force 동작 무변경.
|
|
5
5
|
import { join, basename } from "node:path";
|
|
6
|
+
import { deployFilter, isDeployWorkflow, activateDeployTrigger, DEFAULT_DEPLOY_STYLE } from "../deploy-style.js";
|
|
6
7
|
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
7
8
|
import { PATHS, PAYLOAD } from "../paths.js";
|
|
8
9
|
import { exists, writeText, listYamlFiles } from "../fsutil.js";
|
|
@@ -12,10 +13,12 @@ import { sha256, readBaseline } from "../baseline.js";
|
|
|
12
13
|
|
|
13
14
|
// 원본 텍스트 로더 — context.branches가 있으면 {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} 치환 적용.
|
|
14
15
|
// classify(unchanged 판정)와 실제 복사가 같은 치환본을 봐야 재실행 시 가짜 충돌이 없다.
|
|
15
|
-
export function makeSrcText(branches) {
|
|
16
|
+
export function makeSrcText(branches, deployStyle = DEFAULT_DEPLOY_STYLE) {
|
|
16
17
|
return (p) => {
|
|
17
18
|
const raw = readFileSync(p, "utf8");
|
|
18
|
-
|
|
19
|
+
const out = branches ? substitute(raw, branches) : raw;
|
|
20
|
+
// 고른 배포 방식의 CD는 push 트리거를 켜서 설치한다 — 설치했는데 안 도는 상태를 만들지 않는다.
|
|
21
|
+
return isDeployWorkflow(basename(p)) ? activateDeployTrigger(out) : out;
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -55,9 +58,10 @@ function renderVirtual(templateContent, envOpts) {
|
|
|
55
58
|
//
|
|
56
59
|
// baseline이 없는 기존 설치는 base 미상이라 upstreamOnly/localOnly 판정을 할 수 없고,
|
|
57
60
|
// 종전대로 unchanged/changed 2분류로 떨어진다(폴백). 그 실행에서 baseline이 심긴다.
|
|
58
|
-
function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null) {
|
|
61
|
+
function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null, filter = null) {
|
|
59
62
|
const result = { newFiles: [], unchanged: [], changed: [], upstreamOnly: [], localOnly: [], removed: [] };
|
|
60
63
|
for (const filename of listYamlFiles(srcDir)) {
|
|
64
|
+
if (filter && !filter(filename)) continue;
|
|
61
65
|
const src = join(srcDir, filename);
|
|
62
66
|
const dst = join(workflowsDir, filename);
|
|
63
67
|
const base = baseline?.files?.[filename] || null;
|
|
@@ -141,7 +145,8 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
141
145
|
counters.keptLocal = []; // 질문 없이 사용자 수정본을 유지한 파일 (업스트림 무변경)
|
|
142
146
|
counters.removedKept = []; // 사용자가 지웠고 되살리지 않은 파일
|
|
143
147
|
counters.restoredFiles = []; // 사용자가 지웠지만 복원하기로 한 파일
|
|
144
|
-
const
|
|
148
|
+
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
149
|
+
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
145
150
|
const baseline = readBaseline(targetRoot);
|
|
146
151
|
const baselineTargets = new Map(); // filename -> { srcPath, envOpts, wrote }
|
|
147
152
|
// values/useDefaults는 치환 경로에서만 의미 (renderVirtual은 useDefaults:true 강제 — 가상 비교 무손상)
|
|
@@ -160,7 +165,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
160
165
|
// (2~4) 타입별
|
|
161
166
|
for (const type of types) {
|
|
162
167
|
const asks = new Map();
|
|
163
|
-
copyWorkflowsForType(type, projectTypesDir, workflowsDir, { includeNexus, ...context, envOptsFor, collectAsks: asks, dirCtx }, counters);
|
|
168
|
+
copyWorkflowsForType(type, projectTypesDir, workflowsDir, { includeNexus, ...context, deployStyle, envOptsFor, collectAsks: asks, dirCtx }, counters);
|
|
164
169
|
if (asks.size) deployValues.set(type, asks);
|
|
165
170
|
}
|
|
166
171
|
|
|
@@ -171,6 +176,9 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
171
176
|
const dst = join(workflowsDir, filename);
|
|
172
177
|
if (existsSync(dst)) continue; // 이미 존재하면 스킵
|
|
173
178
|
writeText(dst, srcText(join(secretDir, filename)));
|
|
179
|
+
// 이 경로는 타입별 복사 루프 밖이라 env 치환 루프가 닿지 않는다. 여기서 직접 걸어주지
|
|
180
|
+
// 않으면 이 파일의 @wizard 마커가 통째로 무시돼 __PROJECT_NAME__ 같은 값이 그대로 설치된다.
|
|
181
|
+
configureEnv(dst, envOptsFor("common"));
|
|
174
182
|
counters.optionalCopied++;
|
|
175
183
|
counters.copied++;
|
|
176
184
|
counters.copiedFiles.push(filename);
|
|
@@ -232,14 +240,16 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
232
240
|
const { types = [], paths = new Map(), includeNexus = false, repoName = "", resolvers = {} } = context;
|
|
233
241
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
234
242
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
235
|
-
const
|
|
243
|
+
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
244
|
+
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
236
245
|
const baseline = readBaseline(targetRoot);
|
|
237
246
|
const branchMode = context.branches?.mode || "pr-flow";
|
|
238
247
|
const conflicts = []; // 엔진 처리 순서와 동일 (common → 타입 순회 → 직하위 → server-deploy)
|
|
239
248
|
const removed = [];
|
|
240
249
|
|
|
241
|
-
const
|
|
242
|
-
|
|
250
|
+
const keepDeploy = deployFilter(deployStyle);
|
|
251
|
+
const collect = (srcDir, envOpts, type, skipFile = () => false, filter = null) => {
|
|
252
|
+
const c = classify(srcDir, workflowsDir, envOpts, srcText, baseline, filter);
|
|
243
253
|
for (const f of c.changed) { if (!skipFile(f)) conflicts.push({ filename: f, type }); }
|
|
244
254
|
for (const f of c.removed) { if (!skipFile(f)) removed.push({ filename: f, type }); }
|
|
245
255
|
};
|
|
@@ -255,7 +265,7 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
255
265
|
const typeDir = join(projectTypesDir, type);
|
|
256
266
|
if (exists(typeDir)) collect(typeDir, envOpts, type);
|
|
257
267
|
const serverDeployDir = join(typeDir, "server-deploy");
|
|
258
|
-
if (exists(serverDeployDir) && !includeNexus) collect(serverDeployDir, envOpts, type);
|
|
268
|
+
if (exists(serverDeployDir) && !includeNexus) collect(serverDeployDir, envOpts, type, () => false, keepDeploy);
|
|
259
269
|
}
|
|
260
270
|
return { conflicts, removed };
|
|
261
271
|
}
|
|
@@ -281,7 +291,8 @@ export async function copyWorkflowsInteractive(context, payloadRoot, targetRoot
|
|
|
281
291
|
}
|
|
282
292
|
|
|
283
293
|
function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters) {
|
|
284
|
-
const { includeNexus, envOptsFor, collectAsks = null, dirCtx } = ctx;
|
|
294
|
+
const { includeNexus, deployStyle = "", envOptsFor, collectAsks = null, dirCtx } = ctx;
|
|
295
|
+
const keepDeploy = deployFilter(deployStyle);
|
|
285
296
|
const { srcText, baselineTargets } = dirCtx;
|
|
286
297
|
const typeDir = join(projectTypesDir, type);
|
|
287
298
|
const envOpts = envOptsFor(type);
|
|
@@ -301,29 +312,19 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
301
312
|
if (includeNexus) {
|
|
302
313
|
// Nexus 프로젝트 → 폴더째 제외 (복사 안 함)
|
|
303
314
|
} else {
|
|
304
|
-
const c = processDir(serverDeployDir, workflowsDir, envOpts, dirCtx, counters);
|
|
315
|
+
const c = processDir(serverDeployDir, workflowsDir, envOpts, dirCtx, counters, keepDeploy);
|
|
305
316
|
untouched.push(...c.unchanged, ...c.localOnly);
|
|
306
317
|
}
|
|
307
318
|
}
|
|
308
319
|
|
|
309
|
-
// nexus (opt-in)
|
|
320
|
+
// nexus (opt-in) — 라이브러리 publish 계열(Nexus + GitHub Packages).
|
|
321
|
+
// 다른 폴더와 같은 processDir을 쓴다. 종전에는 이 경로만 별도 로직으로 "존재하면 무조건
|
|
322
|
+
// .bak 후 교체"였는데, 그러면 사용자가 손댄 publish 워크플로우가 재실행마다 묻지도 않고
|
|
323
|
+
// 밀린다. baseline 3-way·충돌 3지선을 다른 워크플로우와 동일하게 적용한다.
|
|
310
324
|
const nexusDir = join(typeDir, "nexus");
|
|
311
325
|
if (exists(nexusDir) && includeNexus) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const dst = join(workflowsDir, filename);
|
|
315
|
-
const body = srcText(src);
|
|
316
|
-
if (existsSync(dst) && renderVirtual(body, envOpts) === readFileSync(dst, "utf8")) {
|
|
317
|
-
counters.skipped++;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (existsSync(dst)) { renameSync(dst, dst + ".bak"); counters.backupAdded++; }
|
|
321
|
-
writeText(dst, body);
|
|
322
|
-
counters.optionalCopied++;
|
|
323
|
-
counters.copied++;
|
|
324
|
-
counters.copiedFiles.push(filename);
|
|
325
|
-
baselineTargets.set(filename, { srcPath: src, envOpts, wrote: true });
|
|
326
|
-
}
|
|
326
|
+
const c = processDir(nexusDir, workflowsDir, envOpts, dirCtx, counters);
|
|
327
|
+
untouched.push(...c.unchanged, ...c.localOnly);
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
// env 치환 — 이 타입의 원본 디렉토리들에서 복사돼 존재하고, 손대지 않기로 한 것이 아닌 파일만
|
|
@@ -331,6 +332,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
331
332
|
if (!exists(srcDir)) continue;
|
|
332
333
|
for (const filename of listYamlFiles(srcDir)) {
|
|
333
334
|
const target = join(workflowsDir, filename);
|
|
335
|
+
if (srcDir === serverDeployDir && !keepDeploy(filename)) continue; // 안 고른 배포 방식
|
|
334
336
|
if (!existsSync(target)) continue; // 건너뛴 파일 제외
|
|
335
337
|
if (untouched.includes(filename)) continue; // unchanged/localOnly 제외
|
|
336
338
|
configureEnv(target, { ...envOpts, collectAsks }); // env 계획 values/useDefaults 포함
|
|
@@ -345,7 +347,8 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
345
347
|
const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {} } = context;
|
|
346
348
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
347
349
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
348
|
-
const
|
|
350
|
+
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
351
|
+
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
349
352
|
const baseline = readBaseline(targetRoot);
|
|
350
353
|
const branchMode = context.branches?.mode || "pr-flow";
|
|
351
354
|
// upstreamOnly/localOnly/removed는 baseline이 있을 때만 채워진다 (issue #69).
|
|
@@ -385,7 +388,7 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
385
388
|
|
|
386
389
|
const serverDeployDir = join(typeDir, "server-deploy");
|
|
387
390
|
if (exists(serverDeployDir) && !includeNexus) {
|
|
388
|
-
merge(classify(serverDeployDir, workflowsDir, envOpts, srcText, baseline), type);
|
|
391
|
+
merge(classify(serverDeployDir, workflowsDir, envOpts, srcText, baseline, deployFilter(deployStyle)), type);
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
const nexusDir = join(typeDir, "nexus");
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// 배포 방식 (이슈 #80) — 서버 배포 CD 워크플로우는 서로 대체재다.
|
|
2
|
+
// Nginx 무중단과 Traefik 무중단을 동시에 쓰는 경우는 없으므로 하나만 설치한다.
|
|
3
|
+
// 고른 것은 push 트리거까지 켜서 설치한다 — 설치했는데 안 도는 상태를 만들지 않는다.
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { sha256 } from "./baseline.js";
|
|
7
|
+
|
|
8
|
+
// 파일명 접미사로 식별한다 — 타입 접두사(PROJECT-SPRING- 등)는 타입마다 다르기 때문.
|
|
9
|
+
export const DEPLOY_STYLES = [
|
|
10
|
+
{ value: "simple", suffix: "-SIMPLE-CICD.yaml", label: "단일 서버 배포 — 컨테이너를 내렸다 올린다 (가장 단순, 짧은 다운타임)" },
|
|
11
|
+
{ value: "nginx", suffix: "-NONSTOP-NGINX-CICD.yaml", label: "무중단 배포 (Nginx) — nginx config의 proxy_pass 포트를 Blue/Green으로 토글" },
|
|
12
|
+
{ value: "traefik", suffix: "-NONSTOP-TRAEFIK-CICD.yaml", label: "무중단 배포 (Traefik) — Traefik 라우팅으로 Blue/Green 전환" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_DEPLOY_STYLE = "simple";
|
|
16
|
+
|
|
17
|
+
export const isDeployStyle = (v) => DEPLOY_STYLES.some((s) => s.value === v);
|
|
18
|
+
|
|
19
|
+
// 이 파일이 CD 본체인가 (= 택1 대상인가). PR 프리뷰는 배포 방식과 직교하는 축이라 제외한다.
|
|
20
|
+
export const isDeployWorkflow = (filename) => DEPLOY_STYLES.some((s) => filename.endsWith(s.suffix));
|
|
21
|
+
|
|
22
|
+
// 모르는 값은 기본값으로 수렴시킨다. 빈 접미사를 돌려주면 endsWith("")가 항상 참이라
|
|
23
|
+
// "전부 통과"가 되어, 잘못된 값이 조용히 CD 전부 설치로 새어나간다.
|
|
24
|
+
const suffixOf = (style) =>
|
|
25
|
+
(DEPLOY_STYLES.find((s) => s.value === style) ?? DEPLOY_STYLES.find((s) => s.value === DEFAULT_DEPLOY_STYLE)).suffix;
|
|
26
|
+
|
|
27
|
+
// 파일 필터 — 고른 방식의 CD만 통과. CD가 아닌 파일(PR 프리뷰·common 등)은 항상 통과.
|
|
28
|
+
export function deployFilter(style) {
|
|
29
|
+
const suffix = suffixOf(style);
|
|
30
|
+
return (filename) => !isDeployWorkflow(filename) || filename.endsWith(suffix);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 무중단 템플릿은 push 트리거가 주석 처리된 채 들어 있다(기본 배포가 단일 서버라서).
|
|
34
|
+
// 사용자가 그 방식을 고른 이상 트리거는 켜져 있어야 한다 — 안 그러면 설치해도 아무 일이
|
|
35
|
+
// 일어나지 않고 사용자가 YAML을 직접 고쳐야 한다.
|
|
36
|
+
//
|
|
37
|
+
// 첫 `on:` 블록 안에서 `# ` 두 글자만 떼므로 안쪽 들여쓰기 계층이 그대로 보존된다.
|
|
38
|
+
// 설명문 주석은 벗겨낸 내용이 push/branches/- 로 시작하지 않아 건드리지 않는다.
|
|
39
|
+
const TRIGGER_CONTENT = /^\s*(push:|branches:|- )/;
|
|
40
|
+
|
|
41
|
+
export function activateDeployTrigger(content) {
|
|
42
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
43
|
+
const lines = content.split(/\r?\n/);
|
|
44
|
+
let inOn = false;
|
|
45
|
+
let changed = false;
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
if (/^on:\s*$/.test(line)) { inOn = true; continue; }
|
|
49
|
+
if (!inOn) continue;
|
|
50
|
+
if (/^\S/.test(line)) break; // 최상위 키를 다시 만나면 on 블록 종료
|
|
51
|
+
const m = line.match(/^(\s*)# ?(.*)$/);
|
|
52
|
+
if (!m || !TRIGGER_CONTENT.test(m[2])) continue;
|
|
53
|
+
lines[i] = `${m[1]}${m[2]}`;
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
return changed ? lines.join(eol) : content;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 방식을 바꿔 재설치했을 때 이전 CD를 정리한다.
|
|
60
|
+
//
|
|
61
|
+
// 남겨두면 이전 방식의 push 트리거가 살아 있어 배포가 두 번 돈다. 그렇다고 사용자에게
|
|
62
|
+
// "직접 지우세요"라고 떠넘기면 설치가 끝나도 레포가 정상이 아닌 상태로 남는다. 마법사가
|
|
63
|
+
// 깐 파일은 마법사가 정리한다.
|
|
64
|
+
//
|
|
65
|
+
// 손대지 않은 것(baseline의 installed 해시와 동일) → 삭제. 물어볼 이유가 없다.
|
|
66
|
+
// 손댄 것 → .bak으로 옮긴다. 내용은 지키고 트리거만 죽인다.
|
|
67
|
+
//
|
|
68
|
+
// 반환: { removed:[], backedUp:[] } — 완료 화면·설치 기록에 그대로 보고한다.
|
|
69
|
+
export function cleanupOtherDeployWorkflows(workflowsDir, installedFilenames, style, baseline) {
|
|
70
|
+
const keep = deployFilter(style);
|
|
71
|
+
const removed = [];
|
|
72
|
+
const backedUp = [];
|
|
73
|
+
|
|
74
|
+
for (const filename of installedFilenames) {
|
|
75
|
+
if (!isDeployWorkflow(filename) || keep(filename)) continue;
|
|
76
|
+
const p = join(workflowsDir, filename);
|
|
77
|
+
if (!existsSync(p)) continue;
|
|
78
|
+
|
|
79
|
+
const known = baseline?.files?.[filename]?.installed;
|
|
80
|
+
const untouched = known && sha256(readFileSync(p, "utf8")) === known;
|
|
81
|
+
if (untouched) {
|
|
82
|
+
rmSync(p, { force: true });
|
|
83
|
+
removed.push(filename);
|
|
84
|
+
} else {
|
|
85
|
+
renameSync(p, `${p}.bak`);
|
|
86
|
+
backedUp.push(filename);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { removed, backedUp };
|
|
90
|
+
}
|
package/src/core/detect-fs.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, basename } from "node:path";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
|
-
import { detectTypesFromMarkers, detectVersionFromFiles, detectBuildNumberFromFiles } from "./detect.js";
|
|
6
|
+
import { detectTypesFromMarkers, detectVersionFromFiles, detectBuildNumberFromFiles, detectJdkFromFiles, resolveMarkers } from "./detect.js";
|
|
7
7
|
import { parseExisting } from "./version-yml.js";
|
|
8
8
|
|
|
9
9
|
const hasFile = (root) => (rel) => existsSync(join(root, rel));
|
|
@@ -28,11 +28,25 @@ export function detectTypes(root) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// 버전 감지 — .sh detect_version 순서. jq는 package.json 파싱에 쓰인 적이 없어 게이트를 제거했다(이슈 #22 L4).
|
|
31
|
-
|
|
31
|
+
// hint: 폴백 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다 — 이슈 #80).
|
|
32
|
+
export function detectVersion(root, { warn = (m) => console.error(m), hint } = {}) {
|
|
32
33
|
const read = readFile(root);
|
|
33
34
|
const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
|
|
34
35
|
const gitTag = gitOut(root, ["describe", "--tags", "--abbrev=0"]);
|
|
35
|
-
return detectVersionFromFiles({ read, readJson, gitTag, warn });
|
|
36
|
+
return detectVersionFromFiles({ read, readJson, gitTag, warn, hint });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 타입별 실제 마커 파일 (이슈 #77) — 감지 로그·설치 로그가 같은 근거 파일을 인용하도록.
|
|
40
|
+
export function detectMarkers(root, types = []) {
|
|
41
|
+
return resolveMarkers(types, hasFile(root));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 빌드 JDK 감지 (이슈 #82) — 배포 워크플로우 JAVA_VERSION 기본값에 실측값을 쓰기 위해.
|
|
45
|
+
// base: 모노레포에서 spring 프로젝트 루트 (레포 루트 기준 상대경로).
|
|
46
|
+
export function detectJdk(root, base = ".") {
|
|
47
|
+
const rel = base && base !== "." ? (r) => `${base}/${r}` : (r) => r;
|
|
48
|
+
const read = readFile(root);
|
|
49
|
+
return detectJdkFromFiles({ read: (r) => read(rel(r)) });
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
// 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에서 실제 빌드 번호를 읽는다 (이슈 #41).
|
|
@@ -65,23 +79,34 @@ export function detectRepoName(root) {
|
|
|
65
79
|
// Spring application*.yml 탐색 (.sh resolve_spring_app_yml_dir/path L2767~2780 등가)
|
|
66
80
|
// find {base} -path "*/src/main/resources/application*.yml" | head -1 의 fs 재귀 구현.
|
|
67
81
|
// 반환: root 기준 상대경로 (예: "server/src/main/resources/application.yml") 또는 "".
|
|
82
|
+
//
|
|
83
|
+
// .yaml도 인정한다 (이슈 #81). Spring은 .yml/.yaml을 모두 공식 지원하는데 종전 정규식이
|
|
84
|
+
// .yml만 봐서, application.yaml을 쓰는 프로젝트는 이 값이 빈 문자열이 되고 그 결과
|
|
85
|
+
// __APPLICATION_YML_DIR__ 가 치환되지 않은 채 설치됐다.
|
|
86
|
+
//
|
|
87
|
+
// 같은 디렉토리에서는 프로파일 없는 기본 파일(application.yml/.yaml)을 우선한다. 파일명 정렬만
|
|
88
|
+
// 쓰면 'application-dev.yml'이 'application.yml'보다 앞서(`-` < `.`) 프로파일 파일이 잡힌다.
|
|
68
89
|
export function findSpringAppYml(root, base = ".") {
|
|
69
90
|
const startRel = base === "." ? "" : base;
|
|
70
91
|
const PRUNE = new Set(["node_modules", ".git", "build", ".gradle", "target", ".idea"]);
|
|
92
|
+
const APP_YML = /^application(-[^/]*)?\.ya?ml$/;
|
|
71
93
|
let hit = "";
|
|
94
|
+
let hitIsBase = false;
|
|
72
95
|
const walk = (rel, depth) => {
|
|
73
|
-
if (
|
|
96
|
+
if (hitIsBase || depth > 8) return; // 기본 파일을 찾았으면 더 볼 필요가 없다
|
|
74
97
|
let entries;
|
|
75
98
|
try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return; }
|
|
76
99
|
// 정렬로 순회 순서 결정화 (find 순서 플랫폼 편차 제거)
|
|
77
100
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
78
|
-
if (
|
|
101
|
+
if (hitIsBase) return;
|
|
79
102
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
80
103
|
if (e.isDirectory()) {
|
|
81
104
|
if (PRUNE.has(e.name)) continue;
|
|
82
105
|
walk(childRel, depth + 1);
|
|
83
|
-
} else if (
|
|
84
|
-
|
|
106
|
+
} else if (APP_YML.test(e.name) && childRel.includes("src/main/resources/")) {
|
|
107
|
+
const isBase = /^application\.ya?ml$/.test(e.name);
|
|
108
|
+
// 첫 매치는 일단 채택하고, 이후 기본 파일이 나오면 그걸로 승격한다.
|
|
109
|
+
if (!hit || isBase) { hit = childRel; hitIsBase = isBase; }
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
112
|
};
|
|
@@ -95,6 +120,10 @@ export function makeResolvers(root, repoName, paths) {
|
|
|
95
120
|
const springBase = (t) => paths.get(t || "spring") || paths.get("spring") || ".";
|
|
96
121
|
return {
|
|
97
122
|
repo: () => repoName,
|
|
123
|
+
// 빌드 JDK (이슈 #82) — 배포 워크플로우 JAVA_VERSION의 기본값. 프로젝트 툴체인을 실측한다.
|
|
124
|
+
// ⚠️ 빈 문자열을 돌려주면 setEnvLine이 그 줄을 건너뛰어 __JAVA_VERSION__이 그대로 남는다
|
|
125
|
+
// (이슈 #81과 같은 실패 형태). 감지 실패 시 반드시 종전 기본값 21로 폴백한다.
|
|
126
|
+
jdk: (t) => detectJdk(root, springBase(t)) || "21",
|
|
98
127
|
"spring-app-yml-dir": (t) => {
|
|
99
128
|
const f = findSpringAppYml(root, springBase(t));
|
|
100
129
|
return f ? f.split("/").slice(0, -1).join("/") : "";
|
package/src/core/detect.js
CHANGED
|
@@ -36,7 +36,9 @@ const VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
36
36
|
|
|
37
37
|
// 버전 감지 (동작명세 §3.3) — 순서대로 첫 성공. read(relpath)=>string|null 주입.
|
|
38
38
|
// package.json은 이미 Node JSON.parse로 파싱을 마친 값이므로 jq 설치 여부와 무관하게 항상 사용한다(이슈 #22 L4).
|
|
39
|
-
|
|
39
|
+
// hint: 폴백 경고 뒤에 붙일 "그럼 어떻게 고치나" 한 줄. 대화형과 CLI가 서로 다른 방법을
|
|
40
|
+
// 안내해야 하므로(이슈 #80) 호출부가 정한다. 미지정 시 CLI 문구를 쓴다.
|
|
41
|
+
export function detectVersionFromFiles({ read, readJson, gitTag, warn, hint }) {
|
|
40
42
|
const pkg = readJson?.("package.json");
|
|
41
43
|
if (pkg?.version && VERSION_RE.test(pkg.version)) return pkg.version;
|
|
42
44
|
const grab = (content, re) => {
|
|
@@ -47,14 +49,29 @@ export function detectVersionFromFiles({ read, readJson, gitTag, warn }) {
|
|
|
47
49
|
return null;
|
|
48
50
|
};
|
|
49
51
|
let v;
|
|
50
|
-
|
|
52
|
+
const gradleRe = /version\s*=\s*["']?(\d+\.\d+\.\d+)/;
|
|
53
|
+
// Groovy DSL과 Kotlin DSL은 같은 문법(`version = "x.y.z"`)이라 정규식을 공유한다.
|
|
54
|
+
// .kts를 빼먹으면 Kotlin DSL Spring 프로젝트가 전부 0.0.1로 초기화된다 (이슈 #77).
|
|
55
|
+
if ((v = grab(read("build.gradle"), gradleRe))) return v;
|
|
56
|
+
if ((v = grab(read("build.gradle.kts"), gradleRe))) return v;
|
|
57
|
+
if ((v = versionFromPom(read("pom.xml")))) return v;
|
|
51
58
|
if ((v = grab(read("pubspec.yaml"), /^version:\s*(\d+\.\d+\.\d+)/))) return v;
|
|
52
59
|
if ((v = grab(read("pyproject.toml"), /version\s*=\s*["']?(\d+\.\d+\.\d+)/))) return v;
|
|
53
60
|
if (gitTag) { const t = String(gitTag).replace(/^v/, ""); if (VERSION_RE.test(t)) return t; }
|
|
54
|
-
|
|
61
|
+
const tail = hint ?? "--project-version으로 직접 지정하거나 version.yml을 확인하세요.";
|
|
62
|
+
warn?.(`⚠️ 버전을 자동 감지하지 못해 기본값 0.0.1을 사용합니다 — ${tail}`);
|
|
55
63
|
return "0.0.1";
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
// Maven pom.xml의 프로젝트 버전 (이슈 #77). <parent> 블록 안의 버전은 스프링 부트 BOM 버전이라
|
|
67
|
+
// 프로젝트 버전이 아니다 — 그 구간을 지운 뒤 첫 <version>을 읽는다.
|
|
68
|
+
export function versionFromPom(content) {
|
|
69
|
+
if (!content) return null;
|
|
70
|
+
const body = String(content).replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
71
|
+
const m = body.match(/<version>\s*(\d+\.\d+\.\d+)[^<]*<\/version>/);
|
|
72
|
+
return m ? m[1] : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
export function markerForType(type) {
|
|
59
76
|
return { flutter: "pubspec.yaml", "react-native-expo": "app.json", python: "pyproject.toml", spring: "build.gradle" }[type] || "package.json";
|
|
60
77
|
}
|
|
@@ -63,6 +80,54 @@ export function extraMarkers(type) {
|
|
|
63
80
|
return { python: ["setup.py", "requirements.txt"], spring: ["build.gradle.kts", "pom.xml"] }[type] || [];
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
// 그 타입을 감지하는 데 실제로 쓰인 파일 (이슈 #77). markerForType은 타입당 대표 파일 하나를
|
|
84
|
+
// 고정 반환하므로, build.gradle.kts만 있는 레포에서도 "build.gradle 발견"이라고 출력돼
|
|
85
|
+
// 같은 설치 로그 안에서 경로 확정 화면과 파일명이 어긋났다. has()로 실재하는 것을 고른다.
|
|
86
|
+
// 실재하는 후보가 없으면(감지 전 화면 등) 대표 파일을 쓴다.
|
|
87
|
+
export function resolveMarker(type, has) {
|
|
88
|
+
const candidates = [markerForType(type), ...extraMarkers(type)];
|
|
89
|
+
return candidates.find(has) ?? candidates[0];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 빌드 JDK 감지 (이슈 #82) — 배포 워크플로우의 JAVA_VERSION 기본값이 21로 고정돼 있어
|
|
93
|
+
// toolchain이 다른 프로젝트(예: 25)는 그대로 Enter를 누르면 러너 JDK와 어긋나 빌드가 깨진다.
|
|
94
|
+
// 빌드 번호를 프로젝트 파일에서 읽는 detectBuildNumberFromFiles와 같은 방식으로 실측한다.
|
|
95
|
+
// 반환: "21" 같은 메이저 버전 문자열, 못 찾으면 null.
|
|
96
|
+
export function detectJdkFromFiles({ read }) {
|
|
97
|
+
const pick = (content, patterns) => {
|
|
98
|
+
if (!content) return null;
|
|
99
|
+
for (const re of patterns) {
|
|
100
|
+
const m = String(content).match(re);
|
|
101
|
+
// JavaVersion.VERSION_1_8 처럼 1_8 표기는 8로 정규화한다.
|
|
102
|
+
if (m) return m[1] === "1_8" ? "8" : m[1].replace("1_", "");
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
const gradlePatterns = [
|
|
107
|
+
/JavaLanguageVersion\.of\((\d+)\)/, // toolchain (Gradle 권장 표기)
|
|
108
|
+
/JavaVersion\.VERSION_(\d+(?:_\d+)?)/, // sourceCompatibility = JavaVersion.VERSION_21
|
|
109
|
+
/(?:source|target)Compatibility\s*=?\s*["'](\d+)["']/, // sourceCompatibility = '17'
|
|
110
|
+
];
|
|
111
|
+
let v;
|
|
112
|
+
if ((v = pick(read("build.gradle.kts"), gradlePatterns))) return v;
|
|
113
|
+
if ((v = pick(read("build.gradle"), gradlePatterns))) return v;
|
|
114
|
+
if ((v = pick(read("pom.xml"), [
|
|
115
|
+
/<java\.version>\s*(\d+(?:\.\d+)?)\s*<\/java\.version>/,
|
|
116
|
+
/<maven\.compiler\.(?:source|release)>\s*(\d+(?:\.\d+)?)\s*<\//,
|
|
117
|
+
]))) return v.replace(/^1\./, "");
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 타입별 실제 마커 파일 맵 — 감지 로그·설치 로그가 같은 근거를 쓰도록 한 곳에서 만든다.
|
|
122
|
+
export function resolveMarkers(types = [], has) {
|
|
123
|
+
const out = new Map();
|
|
124
|
+
for (const t of types) {
|
|
125
|
+
if (t === "basic") continue;
|
|
126
|
+
out.set(t, resolveMarker(t, has));
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
66
131
|
// 빌드 번호 감지 (이슈 #41) — 신규 통합 시 pubspec.yaml/build.gradle/app.json에 이미 기록된
|
|
67
132
|
// 빌드 번호를 읽어 version_code가 항상 1로 초기화되는 걸 막는다. types 배열에서 먼저 매칭되는
|
|
68
133
|
// 첫 타입만 사용한다(다른 감지 로직의 types[0]=primary 관례와 동일). read(rel)=>string|null,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// 설치 로그 (이슈 #79) — 실행마다 "무엇을 어떤 값으로 설치했는지"를 레포에 한 건 남긴다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 필요한가: version.yml에는 버전·타입·경로·브랜치·옵션만 남는다. 감지 근거, 질문별 답변,
|
|
4
|
+
// 특히 환경설정 답변(도메인·포트·볼륨 경로)은 워크플로우 YAML 안으로 흩어질 뿐 한 곳에
|
|
5
|
+
// 기록되지 않아, 배포가 실패했을 때 "설치할 때 뭘로 답했더라"를 역추적할 방법이 없었다.
|
|
6
|
+
// 터미널 스크롤백은 에이전트가 볼 수 없고 다른 클론에도 남지 않는다.
|
|
7
|
+
//
|
|
8
|
+
// 위치는 .github/.wizard/ 아래 — 마법사가 이미 baseline.json을 두는 자기 메타데이터 폴더다.
|
|
9
|
+
// 새 최상위 폴더를 만들 이유가 없고, 삭제 모드에서도 경계가 이 폴더 하나로 끝난다.
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { writeText } from "./fsutil.js";
|
|
12
|
+
|
|
13
|
+
export const LOG_DIR = ".github/.wizard/logs";
|
|
14
|
+
|
|
15
|
+
// 값에 비밀이 들어갈 수 있는 키 — 현재 질문 항목에는 없지만(도메인·경로·포트·인증 '방식'),
|
|
16
|
+
// 앞으로 추가될 때 그냥 평문으로 커밋되지 않도록 처음부터 걸어둔다. 이 파일은 커밋 대상이다.
|
|
17
|
+
const SECRET_KEY_RE = /(PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL)/i;
|
|
18
|
+
const MASK = "***";
|
|
19
|
+
|
|
20
|
+
export function maskValue(key, value) {
|
|
21
|
+
// SSH_AUTH_METHOD처럼 "방식"만 담는 키는 비밀이 아니다 — 이름에 KEY가 들어가도 마스킹하지 않는다.
|
|
22
|
+
if (key === "SSH_AUTH_METHOD") return value;
|
|
23
|
+
return SECRET_KEY_RE.test(key) ? MASK : value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// "2026-08-12 18:15:30" → "20260812-181530". 파일명이 곧 정렬 키가 되도록.
|
|
27
|
+
export function stampFrom(now = "") {
|
|
28
|
+
const m = String(now).match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/);
|
|
29
|
+
if (!m) return "unknown";
|
|
30
|
+
return `${m[1]}${m[2]}${m[3]}-${m[4]}${m[5]}${m[6]}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function logFilename(now, action = "install") {
|
|
34
|
+
return `${stampFrom(now)}-${action}.md`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const yamlStr = (v) => `"${String(v ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
38
|
+
const yamlList = (arr) => `[${(arr || []).map(yamlStr).join(", ")}]`;
|
|
39
|
+
|
|
40
|
+
// 마크다운 렌더 — 상단은 기계가 읽는 front matter, 아래는 사람이 읽는 본문.
|
|
41
|
+
// 에이전트가 파싱만 해도 핵심을 얻고, 사람이 열면 그대로 읽힌다.
|
|
42
|
+
export function renderInstallLog(d = {}) {
|
|
43
|
+
const {
|
|
44
|
+
action = "install", at = "", templateVersion = "",
|
|
45
|
+
previousTemplateVersion = "", mode = "", types = [], markers = new Map(),
|
|
46
|
+
version = "", branch = "", branches = null, paths = new Map(),
|
|
47
|
+
options = {}, answers = [], result = {}, unresolved = [], secrets = new Map(),
|
|
48
|
+
warnings = [], cleanup = null,
|
|
49
|
+
} = d;
|
|
50
|
+
|
|
51
|
+
const L = [];
|
|
52
|
+
L.push("---");
|
|
53
|
+
L.push(`action: ${yamlStr(action)}`);
|
|
54
|
+
L.push(`at: ${yamlStr(at)}`);
|
|
55
|
+
L.push(`template_version: ${yamlStr(templateVersion)}`);
|
|
56
|
+
if (previousTemplateVersion) L.push(`previous_template_version: ${yamlStr(previousTemplateVersion)}`);
|
|
57
|
+
L.push(`mode: ${yamlStr(mode)}`);
|
|
58
|
+
L.push(`project_types: ${yamlList(types)}`);
|
|
59
|
+
L.push(`version: ${yamlStr(version)}`);
|
|
60
|
+
L.push(`default_branch: ${yamlStr(branch)}`);
|
|
61
|
+
if (branches) {
|
|
62
|
+
L.push(`branches: { main: ${yamlStr(branches.main)}, develop: ${yamlStr(branches.develop)}, mode: ${yamlStr(branches.mode)} }`);
|
|
63
|
+
}
|
|
64
|
+
L.push(`unresolved_count: ${unresolved.length}`);
|
|
65
|
+
L.push(`required_secrets: ${yamlList([...secrets.keys()])}`);
|
|
66
|
+
L.push("---");
|
|
67
|
+
L.push("");
|
|
68
|
+
L.push(`# 설치 로그 — ${at}`);
|
|
69
|
+
L.push("");
|
|
70
|
+
L.push("project-auto-wizard가 이 레포에 무엇을 설치했는지 남긴 기록입니다. 직접 편집하지 마세요.");
|
|
71
|
+
L.push("");
|
|
72
|
+
|
|
73
|
+
L.push("## 실행");
|
|
74
|
+
L.push("");
|
|
75
|
+
L.push("| 항목 | 값 |");
|
|
76
|
+
L.push("|---|---|");
|
|
77
|
+
L.push(`| 동작 | ${action === "install" ? "신규 설치" : action === "update" ? "업데이트" : action} |`);
|
|
78
|
+
L.push(`| 템플릿 버전 | ${previousTemplateVersion ? `${previousTemplateVersion} → ${templateVersion}` : templateVersion || "-"} |`);
|
|
79
|
+
L.push(`| 설치 모드 | ${mode || "-"} |`);
|
|
80
|
+
L.push("");
|
|
81
|
+
|
|
82
|
+
L.push("## 감지 결과");
|
|
83
|
+
L.push("");
|
|
84
|
+
L.push("| 항목 | 값 | 근거 |");
|
|
85
|
+
L.push("|---|---|---|");
|
|
86
|
+
for (const t of types) {
|
|
87
|
+
L.push(`| 타입 | ${t} | ${markers?.get?.(t) || "직접 선택"} |`);
|
|
88
|
+
}
|
|
89
|
+
L.push(`| 버전 | ${version} | ${d.versionSource || "자동 감지"} |`);
|
|
90
|
+
L.push(`| 브랜치 | ${branch} | git |`);
|
|
91
|
+
for (const [t, p] of paths) L.push(`| 경로 (${t}) | ${p} | |`);
|
|
92
|
+
L.push("");
|
|
93
|
+
if (warnings.length) {
|
|
94
|
+
L.push("감지 중 경고:");
|
|
95
|
+
L.push("");
|
|
96
|
+
for (const w of warnings) L.push(`- ${w}`);
|
|
97
|
+
L.push("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
L.push("## 선택 항목");
|
|
101
|
+
L.push("");
|
|
102
|
+
L.push("| 항목 | 값 |");
|
|
103
|
+
L.push("|---|---|");
|
|
104
|
+
L.push(`| 라이브러리 publish (Nexus·GitHub Packages) | ${options.nexus ? "포함" : "제외"} |`);
|
|
105
|
+
L.push(`| Secret 서버 백업 | ${options.secretBackup ? "포함" : "제외"} |`);
|
|
106
|
+
L.push(`| 자동 버전 승격 | ${options.semverAuto === false ? "사용 안 함" : "사용"} |`);
|
|
107
|
+
L.push(`| 서버 배포 방식 | ${options.deployStyle || "-"} |`);
|
|
108
|
+
L.push("");
|
|
109
|
+
|
|
110
|
+
L.push("## 환경설정 답변");
|
|
111
|
+
L.push("");
|
|
112
|
+
if (!answers.length) {
|
|
113
|
+
L.push("이 설치에서는 환경설정 질문이 없었습니다.");
|
|
114
|
+
} else {
|
|
115
|
+
L.push("`기본값` 열이 `예`면 질문에서 그대로 Enter를 누른 값입니다. 배포가 예상과 다르게 동작하면 여기부터 확인하세요.");
|
|
116
|
+
L.push("");
|
|
117
|
+
L.push("| 키 | 항목 | 값 | 기본값 | 사용처 |");
|
|
118
|
+
L.push("|---|---|---|---|---|");
|
|
119
|
+
for (const a of answers) {
|
|
120
|
+
L.push(`| \`${a.key}\` | ${a.label} | \`${maskValue(a.key, a.value)}\` | ${a.isDefault ? "예" : "아니오"} | ${a.scope || ""} |`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
L.push("");
|
|
124
|
+
|
|
125
|
+
L.push("## 설치 결과");
|
|
126
|
+
L.push("");
|
|
127
|
+
const sec = (title, items, fmt = (x) => `- \`${x}\``) => {
|
|
128
|
+
if (!items || !items.length) return;
|
|
129
|
+
L.push(`### ${title} (${items.length})`);
|
|
130
|
+
L.push("");
|
|
131
|
+
for (const it of items) L.push(fmt(it));
|
|
132
|
+
L.push("");
|
|
133
|
+
};
|
|
134
|
+
sec("새로 설치된 파일", result.copiedFiles);
|
|
135
|
+
sec("이전 배포 방식 정리 — 삭제", cleanup?.removed);
|
|
136
|
+
sec("이전 배포 방식 정리 — .bak 백업 (수정 내용 보존)", cleanup?.backedUp);
|
|
137
|
+
sec("건너뛴 파일", result.skippedFiles);
|
|
138
|
+
sec("백업 후 교체한 파일", result.backupFiles);
|
|
139
|
+
if (result.gitignoreUpdated) { L.push("`.gitignore`를 갱신했습니다 (충돌 백업 파일 무시 항목 추가)."); L.push(""); }
|
|
140
|
+
|
|
141
|
+
L.push("## 남은 할 일");
|
|
142
|
+
L.push("");
|
|
143
|
+
if (unresolved.length) {
|
|
144
|
+
L.push("### ⚠️ 값이 채워지지 않은 항목");
|
|
145
|
+
L.push("");
|
|
146
|
+
L.push("마법사가 값을 계산하지 못해 플레이스홀더가 그대로 남았습니다. **이 상태로는 해당 워크플로우가 정상 동작하지 않습니다.** 직접 채워 주세요.");
|
|
147
|
+
L.push("");
|
|
148
|
+
L.push("| 파일 | 줄 | 토큰 |");
|
|
149
|
+
L.push("|---|---|---|");
|
|
150
|
+
for (const u of unresolved) L.push(`| \`${u.filename}\` | ${u.line} | \`${u.token}\` |`);
|
|
151
|
+
L.push("");
|
|
152
|
+
}
|
|
153
|
+
if (secrets.size) {
|
|
154
|
+
L.push("### 등록해야 하는 GitHub Secret");
|
|
155
|
+
L.push("");
|
|
156
|
+
L.push("Settings > Secrets and variables > Actions 에서 등록합니다. 등록 전에는 해당 워크플로우가 실패합니다.");
|
|
157
|
+
L.push("");
|
|
158
|
+
L.push("| Secret | 사용하는 워크플로우 |");
|
|
159
|
+
L.push("|---|---|");
|
|
160
|
+
for (const [name, users] of secrets) L.push(`| \`${name}\` | ${users.map((u) => `\`${u}\``).join(", ")} |`);
|
|
161
|
+
L.push("");
|
|
162
|
+
}
|
|
163
|
+
if (!unresolved.length && !secrets.size) {
|
|
164
|
+
L.push("추가로 조치할 항목이 없습니다.");
|
|
165
|
+
L.push("");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return L.join("\n") + "\n";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 로그 기록. 실패해도 설치 자체는 성공으로 끝나야 하므로 예외를 삼키고 null을 돌려준다 —
|
|
172
|
+
// 로그를 못 남긴 것이 설치를 되돌릴 이유는 아니다.
|
|
173
|
+
export function writeInstallLog(targetRoot, data = {}) {
|
|
174
|
+
try {
|
|
175
|
+
const filename = logFilename(data.at, data.action || "install");
|
|
176
|
+
const rel = `${LOG_DIR}/${filename}`;
|
|
177
|
+
writeText(join(targetRoot, rel), renderInstallLog(data));
|
|
178
|
+
return { path: rel };
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/core/options-ask.js
CHANGED
|
@@ -82,8 +82,11 @@ export async function askAllOptionalWorkflows({
|
|
|
82
82
|
// ② Nexus: 각 타입의 nexus/ 폴더 (현재 spring만 존재, .sh L2719~2725)
|
|
83
83
|
for (const t of types) {
|
|
84
84
|
nexus = await askOptionalWorkflow({
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// GitHub Packages publish도 같은 '라이브러리 배포' 계열이라 이 질문이 함께 관장한다 (이슈 #80).
|
|
86
|
+
// 종전에는 Nexus만 묻고 GitHub Packages는 무조건 설치돼, "라이브러리 배포 필요 없다"고
|
|
87
|
+
// 답한 사용자에게 라이브러리 배포 워크플로우가 깔렸다.
|
|
88
|
+
dir: join(ptDir, t, "nexus"), icon: "📦", short: "라이브러리 publish (Nexus · GitHub Packages)",
|
|
89
|
+
desc: "라이브러리/모듈을 Maven 저장소(Nexus)나 GitHub Packages에 배포하는 워크플로우입니다. 일반 서버 배포가 아니라 라이브러리 프로젝트에만 필요합니다. 포함하면 서버 배포 워크플로우는 설치되지 않습니다.",
|
|
87
90
|
current: nexus, force, tty, io, forceAsk, say,
|
|
88
91
|
});
|
|
89
92
|
}
|