project-auto-wizard 0.12.2 → 0.13.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.ja.md +165 -0
- package/README.ko.md +165 -0
- package/README.md +107 -286
- package/README.zh-CN.md +165 -0
- package/bin/project-auto-wizard.js +1 -2
- package/package.json +7 -3
- package/payload/config/breaking-changes.json +24 -0
- package/payload/config/wizard-prompts.yml +45 -0
- package/payload/scripts/changelog_manager.py +141 -46
- package/payload/scripts/issue_helper.py +34 -5
- package/payload/scripts/version_manager.py +397 -97
- package/payload/version.yml.template +4 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
- package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
- package/src/cli/args.js +33 -24
- package/src/cli/help.js +16 -4
- package/src/commands/doctor.js +17 -10
- package/src/commands/dry-run.js +54 -1
- package/src/commands/full.js +127 -50
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +124 -56
- package/src/commands/purge.js +59 -17
- package/src/commands/status.js +16 -1
- package/src/commands/uninstall.js +58 -13
- package/src/context.js +5 -5
- package/src/core/baseline.js +20 -5
- package/src/core/branches.js +19 -1
- package/src/core/branding.js +1 -1
- package/src/core/breaking-check.js +14 -19
- package/src/core/breaking.js +7 -3
- package/src/core/copy/gitignore.js +20 -12
- package/src/core/copy/readme.js +24 -5
- package/src/core/copy/simple.js +52 -13
- package/src/core/copy/workflows.js +90 -47
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +131 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-options.js +4 -3
- package/src/core/fsutil.js +23 -6
- package/src/core/logger.js +55 -19
- package/src/core/paths-resolve.js +50 -47
- package/src/core/paths.js +26 -1
- package/src/core/release-options.js +23 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +113 -0
- package/src/core/verify.js +85 -19
- package/src/core/version-yml.js +42 -8
- package/src/core/wizard-env.js +21 -14
- package/src/core/wizard-labels.js +34 -25
- package/src/index.js +122 -49
- package/src/ui/ansi.js +3 -1
- package/src/ui/banner.js +12 -8
- package/src/ui/env-plan.js +68 -44
- package/src/ui/prompts.js +22 -12
- package/src/ui/readline-engine.js +109 -22
- package/src/ui/status-cards.js +10 -5
- package/src/ui/summary.js +71 -33
- 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
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
// 워크플로우 복사 엔진
|
|
2
|
-
// 실측: template_integrator.sh 3398~3815.
|
|
1
|
+
// 워크플로우 복사 엔진 — common → 타입별 → server-deploy 순으로 분류·복사·치환한다.
|
|
3
2
|
// 대화형 3지선(기존 파일 충돌)은 copyWorkflowsInteractive(async)가 결정 Map을 만들어
|
|
4
3
|
// 동기 엔진(copyWorkflows)에 hooks.decisions로 전달한다 — 기존 시그니처·force 동작 무변경.
|
|
5
4
|
import { join, basename } from "node:path";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
deployFilter, isDeployWorkflow, activateDeployTrigger, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE, NO_DEPLOY_STYLE,
|
|
7
|
+
} from "../deploy-style.js";
|
|
7
8
|
import { storeWorkflowFilter } from "../flutter-options.js";
|
|
8
9
|
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
9
|
-
import { PATHS, PAYLOAD } from "../paths.js";
|
|
10
|
+
import { PATHS, PAYLOAD, typeWorkflowDirs } from "../paths.js";
|
|
10
11
|
import { exists, writeText, listYamlFiles } from "../fsutil.js";
|
|
11
12
|
import { substituteEnv } from "../wizard-env.js";
|
|
12
13
|
import { substitute } from "../branding.js";
|
|
13
14
|
import { sha256, readBaseline } from "../baseline.js";
|
|
15
|
+
import { parseDeployBlock } from "../version-yml.js";
|
|
14
16
|
import { log } from "../logger.js";
|
|
15
17
|
|
|
16
18
|
// 원본 텍스트 로더 — context.branches가 있으면 {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} 치환 적용.
|
|
@@ -24,37 +26,49 @@ export function makeSrcText(branches, deployStyle = DEFAULT_DEPLOY_STYLE) {
|
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
// trunk-based 모드에서 설치하지 않는 common
|
|
29
|
+
// trunk-based 모드에서 설치하지 않는 common 워크플로우.
|
|
28
30
|
// 릴리스 PR 흐름이 없으므로 RELEASE-PUBLISH 하나가 bump→changelog→tag→Release를 흡수한다.
|
|
29
31
|
const TRUNK_BASED_EXCLUDED = new Set([
|
|
30
32
|
"PROJECT-COMMON-VERSION-CONTROL.yaml",
|
|
31
33
|
"PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml",
|
|
32
34
|
]);
|
|
33
35
|
|
|
34
|
-
//
|
|
36
|
+
// version.yml deploy 블록에 저장된 배포 값 — Map<type, Map<key,value>>.
|
|
37
|
+
// 설치·분류·baseline이 같은 값을 기본값으로 써야 재실행 때 가짜 변경이 생기지 않는다.
|
|
38
|
+
export function readSavedDeployValues(targetRoot = ".") {
|
|
39
|
+
const p = join(targetRoot, PATHS.versionFile);
|
|
40
|
+
if (!existsSync(p)) return new Map();
|
|
41
|
+
return parseDeployBlock(readFileSync(p, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 타입 루트 디렉토리에 거는 파일 필터 — 배포 방식 필터와 Flutter 스토어 대상 선택을 합성한다.
|
|
35
45
|
// copyWorkflowsForType·surveyWorkflows·planWorkflows가 같은 함수를 써야 설치·충돌 조사·status/dry-run이
|
|
36
46
|
// 서로 다른 파일 집합을 보지 않는다. flutterStore가 배열이 아니면(null=미결정, 비대화형 기본) 스토어 필터는
|
|
37
47
|
// 걸지 않는다. 필터가 없어도 항상 함수를 돌려준다 — processDir은 null을 받지 못한다.
|
|
38
|
-
|
|
48
|
+
// go/python·react/next처럼 서버 배포 워크플로우가 타입 루트에 바로 있는 타입도 있어 배포 방식 필터를
|
|
49
|
+
// 항상 건다. available(payload 파일명 집합)이 있어야 고른 방식이 없는 타입을 단일 서버 배포로 대체한다.
|
|
50
|
+
export function buildTypeRootFilter(type, deployStyle, flutterStore, available = null) {
|
|
39
51
|
const filters = [];
|
|
40
|
-
if (deployStyle
|
|
52
|
+
if (deployStyle) filters.push(deployFilter(deployStyle, available));
|
|
41
53
|
if (type === "flutter" && Array.isArray(flutterStore)) filters.push(storeWorkflowFilter(flutterStore));
|
|
42
54
|
return (filename) => filters.every((keep) => keep(filename));
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
// 한 파일에 env 치환을 적용해 대상 파일을
|
|
57
|
+
// 한 파일에 env 치환을 적용해 대상 파일을 갱신.
|
|
46
58
|
// values/useDefaults: env 계획(promptEnvPlan) 결과 — 미지정이면 기본값 경로(현행 force 동작).
|
|
47
|
-
function configureEnv(targetPath, { type, projectPath = ".", repoName = "", resolvers = {}, collectAsks = null, values = new Map(), useDefaults = true }) {
|
|
59
|
+
function configureEnv(targetPath, { type, projectPath = ".", repoName = "", resolvers = {}, collectAsks = null, values = new Map(), useDefaults = true, savedValues = null }) {
|
|
48
60
|
const content = readFileSync(targetPath, "utf8");
|
|
49
61
|
if (!content.includes("@wizard")) return;
|
|
50
|
-
const out = substituteEnv(content, { type, useDefaults, values, projectPath, repoName, resolvers, collectAsks });
|
|
62
|
+
const out = substituteEnv(content, { type, useDefaults, values, projectPath, repoName, resolvers, collectAsks, savedValues });
|
|
51
63
|
writeFileSync(targetPath, out);
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
// payload 원본을 "
|
|
66
|
+
// payload 원본을 "이번 실행이 쓸 값으로 가상 치환한 최종형" — 저장된 deploy 값(없으면 기본값)과
|
|
67
|
+
// 이번에 답한 값을 반영한다. 기본값만 쓰면 기본값이 아닌 값으로 설치한 파일이 재실행마다 변경으로
|
|
68
|
+
// 잡혀, 다시 쓰이면서 답한 값이 기본값으로 되돌아간다.
|
|
55
69
|
// baseline.rendered와 비교해 "업스트림이 바뀌었는가"를 판정하는 데 쓴다.
|
|
56
70
|
function renderVirtual(templateContent, envOpts) {
|
|
57
|
-
return substituteEnv(templateContent, { ...envOpts,
|
|
71
|
+
return substituteEnv(templateContent, { useDefaults: true, ...envOpts, collectAsks: null });
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
// 분류 — 대상 워크플로우 디렉토리 기준. srcText: 브랜치 치환이 적용된 원본 로더 (makeSrcText).
|
|
@@ -107,7 +121,7 @@ function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null, filte
|
|
|
107
121
|
function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () => true) {
|
|
108
122
|
const { srcText, baseline, decisions, restoreRemoved, baselineTargets } = ctx;
|
|
109
123
|
const c = classify(srcDir, workflowsDir, envOpts, srcText, baseline);
|
|
110
|
-
const track = (f, wrote) => baselineTargets.set(f, { srcPath: join(srcDir, f), envOpts, wrote });
|
|
124
|
+
const track = (f, wrote, keepRendered = false) => baselineTargets.set(f, { srcPath: join(srcDir, f), envOpts, wrote, keepRendered });
|
|
111
125
|
const write = (f) => { writeText(join(workflowsDir, f), srcText(join(srcDir, f))); counters.copied++; counters.copiedFiles.push(f); track(f, true); };
|
|
112
126
|
|
|
113
127
|
for (const f of c.unchanged.filter(filter)) {
|
|
@@ -147,12 +161,16 @@ function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () =>
|
|
|
147
161
|
const decision = decisions.get(f);
|
|
148
162
|
applyDecision(decision, srcDir, workflowsDir, f, counters, srcText);
|
|
149
163
|
// 'backup'만 대상 파일 자체를 새로 쓴다. 'template'은 다른 파일명이고 'skip'은 기존 유지.
|
|
150
|
-
|
|
164
|
+
// skip은 업스트림 변경을 받지 않은 것이라 rendered를 예전 값으로 둔다 — 새 값으로 바꾸면 다음 실행에서
|
|
165
|
+
// "업스트림 무변경"으로 분류되어 그 변경을 영영 받을 수 없다.
|
|
166
|
+
const kept = decision !== "backup" && decision !== "template";
|
|
167
|
+
if (kept) counters.conflictKept.push(f);
|
|
168
|
+
track(f, decision === "backup", kept);
|
|
151
169
|
}
|
|
152
170
|
return c;
|
|
153
171
|
}
|
|
154
172
|
|
|
155
|
-
//
|
|
173
|
+
// 복사 엔진 본체 (동기 — 기존 호출부 무변경).
|
|
156
174
|
// context: { types:[], paths:Map, force, repoName, resolvers,
|
|
157
175
|
// envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
|
|
158
176
|
// flutterStore?:string[]|null } ← Flutter 스토어 대상 (null=필터 없음, 배열=선택된 플랫폼만)
|
|
@@ -177,13 +195,19 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
177
195
|
counters.keptLocal = []; // 질문 없이 사용자 수정본을 유지한 파일 (업스트림 무변경)
|
|
178
196
|
counters.removedKept = []; // 사용자가 지웠고 되살리지 않은 파일
|
|
179
197
|
counters.restoredFiles = []; // 사용자가 지웠지만 복원하기로 한 파일
|
|
198
|
+
counters.conflictKept = []; // 양쪽이 다 바뀌어 기존 파일을 유지한 파일 (업스트림 변경 미반영)
|
|
180
199
|
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
181
200
|
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
182
201
|
const baseline = readBaseline(targetRoot);
|
|
183
202
|
const baselineTargets = new Map(); // filename -> { srcPath, envOpts, wrote }
|
|
184
203
|
// values/useDefaults는 치환 경로에서만 의미 (renderVirtual은 useDefaults:true 강제 — 가상 비교 무손상)
|
|
185
|
-
const
|
|
186
|
-
const
|
|
204
|
+
const saved = readSavedDeployValues(targetRoot);
|
|
205
|
+
const envOptsFor = (type) => ({
|
|
206
|
+
type, projectPath: paths.get(type) || ".", repoName, resolvers, values: envValues, useDefaults: envUseDefaults,
|
|
207
|
+
savedValues: saved.get(type) || null,
|
|
208
|
+
});
|
|
209
|
+
const available = payloadWorkflowNames(payloadRoot);
|
|
210
|
+
const dirCtx = { srcText, baseline, decisions, restoreRemoved, baselineTargets, available };
|
|
187
211
|
|
|
188
212
|
// (1) common — 타입별과 동일 규칙 (README 계약).
|
|
189
213
|
// trunk-based 모드는 VERSION-CONTROL·AUTO-CHANGELOG 미설치 (RELEASE-PUBLISH 단독).
|
|
@@ -208,7 +232,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
208
232
|
// (2~4) 타입별
|
|
209
233
|
for (const type of types) {
|
|
210
234
|
const asks = new Map();
|
|
211
|
-
copyWorkflowsForType(type,
|
|
235
|
+
copyWorkflowsForType(type, payloadRoot, workflowsDir, { ...context, deployStyle, envOptsFor, collectAsks: asks, dirCtx }, counters);
|
|
212
236
|
if (asks.size) deployValues.set(type, asks);
|
|
213
237
|
}
|
|
214
238
|
|
|
@@ -218,12 +242,16 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
218
242
|
|
|
219
243
|
// copyWorkflows가 끝나고 env 치환까지 마친 뒤에 호출한다 — 그래야 디스크 내용이 최종형이다.
|
|
220
244
|
// entries: Map<filename, {installed:string|null, rendered:string}>
|
|
221
|
-
|
|
245
|
+
// savedDeploy: 이번 실행이 version.yml deploy 블록에 쓴 값(Map<type, Map<key,value>>). 다음 실행은 이 값을
|
|
246
|
+
// 기본값으로 분류하므로 rendered도 같은 값으로 계산해야 재실행이 "업스트림 변경"으로 오인되지 않는다.
|
|
247
|
+
export function computeBaselineEntries(baselineTargets, workflowsDir, srcText, savedDeploy = null) {
|
|
222
248
|
const entries = new Map();
|
|
223
249
|
for (const [filename, info] of baselineTargets) {
|
|
224
250
|
const dst = join(workflowsDir, filename);
|
|
225
251
|
if (!existsSync(dst)) continue;
|
|
226
|
-
const
|
|
252
|
+
const envOpts = savedDeploy ? { ...info.envOpts, savedValues: savedDeploy.get(info.envOpts.type) || null } : info.envOpts;
|
|
253
|
+
// keepRendered: 충돌로 기존 파일을 유지함 — 예전 rendered를 그대로 두도록 null로 넘긴다(writeBaseline이 병합).
|
|
254
|
+
const rendered = info.keepRendered ? null : sha256(renderVirtual(srcText(info.srcPath), envOpts));
|
|
227
255
|
// installed는 이번에 우리가 쓴 파일에만 채운다. 사용자 수정본을 installed로 기록하면
|
|
228
256
|
// "우리가 쓴 것"이라고 거짓말하는 셈이고, 다음 업데이트에서 그 파일이 조용히 덮인다.
|
|
229
257
|
entries.set(filename, { installed: info.wrote ? sha256(readFileSync(dst, "utf8")) : null, rendered });
|
|
@@ -231,13 +259,13 @@ export function computeBaselineEntries(baselineTargets, workflowsDir, srcText) {
|
|
|
231
259
|
return entries;
|
|
232
260
|
}
|
|
233
261
|
|
|
234
|
-
// changed(기존에 있고 내용이 바뀐) 파일 1개를 결정에 따라
|
|
262
|
+
// changed(기존에 있고 내용이 바뀐) 파일 1개를 결정에 따라 처리.
|
|
235
263
|
// 'skip'(기본): 기존 유지. 'backup': 기존→.bak 후 교체. 'template': 기존 유지 + 새 버전을 .template.yaml로.
|
|
236
264
|
function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcText) {
|
|
237
265
|
const src = join(srcDir, filename);
|
|
238
266
|
const dst = join(workflowsDir, filename);
|
|
239
267
|
if (decision === "backup") {
|
|
240
|
-
//
|
|
268
|
+
// 기존을 .bak으로 백업 후 새 버전으로 교체
|
|
241
269
|
renameSync(dst, dst + ".bak");
|
|
242
270
|
writeText(dst, srcText(src));
|
|
243
271
|
counters.copied++;
|
|
@@ -247,16 +275,19 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
|
|
|
247
275
|
return;
|
|
248
276
|
}
|
|
249
277
|
if (decision === "template") {
|
|
250
|
-
//
|
|
278
|
+
// `${이름}.template.yaml` — .yaml만 떼고 붙인다 (.yml은 그대로 뒤에 붙는다)
|
|
251
279
|
const templateName = (filename.endsWith(".yaml") ? filename.slice(0, -".yaml".length) : filename) + ".template.yaml";
|
|
252
|
-
writeText(join(workflowsDir, templateName), srcText(src)); // 기존 .template.yaml 덮어씀
|
|
280
|
+
writeText(join(workflowsDir, templateName), srcText(src)); // 기존 .template.yaml 덮어씀
|
|
253
281
|
counters.templateAdded++;
|
|
254
282
|
counters.copiedFiles.push(templateName);
|
|
255
283
|
log.info("copy", "template", `${filename} 유지 + ${templateName} 생성 (사용자 결정)`);
|
|
256
284
|
return;
|
|
257
285
|
}
|
|
258
|
-
counters.skipped++; // 'skip'/미지정/ESC → 기존 유지 (
|
|
259
|
-
|
|
286
|
+
counters.skipped++; // 'skip'/미지정/ESC → 기존 유지 (force 기본)
|
|
287
|
+
// 결정이 없으면 사용자가 고른 게 아니라 --force 기본값이다 — 로그가 사실과 달라지지 않게 구분한다.
|
|
288
|
+
log.info("copy", "skip", decision
|
|
289
|
+
? `${filename} (사용자 결정: 기존 유지, 업스트림 변경 미반영)`
|
|
290
|
+
: `${filename} (--force 기본값: 기존 유지, 업스트림 변경 미반영)`);
|
|
260
291
|
}
|
|
261
292
|
|
|
262
293
|
// 대화형 사전 조사 — 사람이 답해야 하는 것만 뽑는다.
|
|
@@ -273,10 +304,12 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
273
304
|
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
274
305
|
const baseline = readBaseline(targetRoot);
|
|
275
306
|
const branchMode = context.branches?.mode || "pr-flow";
|
|
307
|
+
const saved = readSavedDeployValues(targetRoot);
|
|
276
308
|
const conflicts = []; // 엔진 처리 순서와 동일 (common → 타입 순회 → 직하위 → server-deploy)
|
|
277
309
|
const removed = [];
|
|
278
310
|
|
|
279
|
-
const
|
|
311
|
+
const available = payloadWorkflowNames(payloadRoot);
|
|
312
|
+
const keepDeploy = deployFilter(deployStyle, available);
|
|
280
313
|
const collect = (srcDir, envOpts, type, skipFile = () => false, filter = null) => {
|
|
281
314
|
const c = classify(srcDir, workflowsDir, envOpts, srcText, baseline, filter);
|
|
282
315
|
for (const f of c.changed) { if (!skipFile(f)) conflicts.push({ filename: f, type }); }
|
|
@@ -285,17 +318,23 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
285
318
|
|
|
286
319
|
const commonDir = join(projectTypesDir, "common");
|
|
287
320
|
if (exists(commonDir)) {
|
|
288
|
-
collect(commonDir, {
|
|
321
|
+
collect(commonDir, {
|
|
322
|
+
type: "common", projectPath: ".", repoName, resolvers,
|
|
323
|
+
values: context.envValues || new Map(), useDefaults: context.envUseDefaults !== false,
|
|
324
|
+
}, "common",
|
|
289
325
|
(f) => branchMode === "trunk-based" && TRUNK_BASED_EXCLUDED.has(f));
|
|
290
326
|
}
|
|
291
327
|
|
|
292
328
|
for (const type of types) {
|
|
293
|
-
|
|
294
|
-
const
|
|
329
|
+
// 복사 엔진과 같은 값(이번 답변 포함)으로 분류해야 결정 목록이 실제 처리 대상과 맞는다.
|
|
330
|
+
const envOpts = {
|
|
331
|
+
type, projectPath: paths.get(type) || ".", repoName, resolvers, savedValues: saved.get(type) || null,
|
|
332
|
+
values: context.envValues || new Map(), useDefaults: context.envUseDefaults !== false,
|
|
333
|
+
};
|
|
334
|
+
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
295
335
|
if (exists(typeDir)) {
|
|
296
|
-
collect(typeDir, envOpts, type, () => false, buildTypeRootFilter(type, deployStyle, flutterStore));
|
|
336
|
+
collect(typeDir, envOpts, type, () => false, buildTypeRootFilter(type, deployStyle, flutterStore, available));
|
|
297
337
|
}
|
|
298
|
-
const serverDeployDir = join(typeDir, "server-deploy");
|
|
299
338
|
if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
|
|
300
339
|
collect(serverDeployDir, envOpts, type, () => false, keepDeploy);
|
|
301
340
|
}
|
|
@@ -323,27 +362,25 @@ export async function copyWorkflowsInteractive(context, payloadRoot, targetRoot
|
|
|
323
362
|
return copyWorkflows(context, payloadRoot, targetRoot, { decisions });
|
|
324
363
|
}
|
|
325
364
|
|
|
326
|
-
function copyWorkflowsForType(type,
|
|
365
|
+
function copyWorkflowsForType(type, payloadRoot, workflowsDir, ctx, counters) {
|
|
327
366
|
const { deployStyle = "", flutterStore = null, envOptsFor, collectAsks = null, dirCtx } = ctx;
|
|
328
|
-
const keepDeploy = deployFilter(deployStyle);
|
|
329
|
-
const keepTypeRoot = buildTypeRootFilter(type, deployStyle, flutterStore);
|
|
367
|
+
const keepDeploy = deployFilter(deployStyle, dirCtx.available);
|
|
368
|
+
const keepTypeRoot = buildTypeRootFilter(type, deployStyle, flutterStore, dirCtx.available);
|
|
330
369
|
const { srcText, baselineTargets } = dirCtx;
|
|
331
|
-
const typeDir =
|
|
370
|
+
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
332
371
|
const envOpts = envOptsFor(type);
|
|
333
372
|
// env 치환에서 제외할 파일 — 손대지 않기로 한 것들(unchanged/localOnly/유지된 삭제분)에
|
|
334
373
|
// 치환을 다시 걸면 사용자 수정본을 덮어쓰게 된다.
|
|
335
374
|
const untouched = [];
|
|
336
375
|
|
|
337
|
-
// 타입별 워크플로우 (직하위). go/python처럼
|
|
338
|
-
// 바로 있는 타입도
|
|
339
|
-
// 걸러야 한다. simple/nginx/traefik은 오늘과 동일하게 필터 없이 전부 복사한다.
|
|
376
|
+
// 타입별 워크플로우 (직하위). go/python·react/next처럼 서버 배포 워크플로우가 server-deploy 없이
|
|
377
|
+
// 타입 루트에 바로 있는 타입도 있어, 타입 루트에도 같은 배포 방식 필터를 건다.
|
|
340
378
|
if (exists(typeDir)) {
|
|
341
379
|
const c = processDir(typeDir, workflowsDir, envOpts, dirCtx, counters, keepTypeRoot);
|
|
342
380
|
untouched.push(...c.unchanged, ...c.localOnly);
|
|
343
381
|
}
|
|
344
382
|
|
|
345
383
|
// server-deploy
|
|
346
|
-
const serverDeployDir = join(typeDir, "server-deploy");
|
|
347
384
|
// "배포 안 함"이면 폴더째 제외 (복사 안 함)
|
|
348
385
|
if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
|
|
349
386
|
const c = processDir(serverDeployDir, workflowsDir, envOpts, dirCtx, counters, keepDeploy);
|
|
@@ -359,7 +396,12 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
359
396
|
if (srcDir === serverDeployDir && !keepDeploy(filename)) continue; // 안 고른 배포 방식
|
|
360
397
|
if (srcDir === typeDir && !keepTypeRoot(filename)) continue; // 타입 루트: 배제된 CD·안 고른 스토어 워크플로우
|
|
361
398
|
if (!existsSync(target)) continue; // 건너뛴 파일 제외
|
|
362
|
-
|
|
399
|
+
// unchanged/localOnly와 치환 마커가 이미 없는 유지본은 다시 치환하지 않는다. 대신 deploy 블록이
|
|
400
|
+
// 사라지지 않도록 저장값(없으면 기본값)으로 ask 값만 수집한다 — 파일은 건드리지 않는다.
|
|
401
|
+
if (untouched.includes(filename) || !readFileSync(target, "utf8").includes("@wizard")) {
|
|
402
|
+
if (collectAsks) substituteEnv(srcText(join(srcDir, filename)), { ...envOpts, useDefaults: true, collectAsks });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
363
405
|
configureEnv(target, { ...envOpts, collectAsks }); // env 계획 values/useDefaults 포함
|
|
364
406
|
}
|
|
365
407
|
}
|
|
@@ -375,6 +417,8 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
375
417
|
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
376
418
|
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
377
419
|
const baseline = readBaseline(targetRoot);
|
|
420
|
+
const saved = readSavedDeployValues(targetRoot);
|
|
421
|
+
const available = payloadWorkflowNames(payloadRoot);
|
|
378
422
|
const branchMode = context.branches?.mode || "pr-flow";
|
|
379
423
|
// upstreamOnly/localOnly/removed는 baseline이 있을 때만 채워진다.
|
|
380
424
|
const plan = { newFiles: [], unchanged: [], changed: [], upstreamOnly: [], localOnly: [], removed: [] };
|
|
@@ -397,15 +441,14 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
397
441
|
}
|
|
398
442
|
|
|
399
443
|
for (const type of types) {
|
|
400
|
-
const envOpts = { type, projectPath: paths.get(type) || ".", repoName, resolvers };
|
|
401
|
-
const typeDir =
|
|
444
|
+
const envOpts = { type, projectPath: paths.get(type) || ".", repoName, resolvers, savedValues: saved.get(type) || null };
|
|
445
|
+
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
402
446
|
if (exists(typeDir)) {
|
|
403
|
-
merge(classify(typeDir, workflowsDir, envOpts, srcText, baseline, buildTypeRootFilter(type, deployStyle, flutterStore)), type);
|
|
447
|
+
merge(classify(typeDir, workflowsDir, envOpts, srcText, baseline, buildTypeRootFilter(type, deployStyle, flutterStore, available)), type);
|
|
404
448
|
}
|
|
405
449
|
|
|
406
|
-
const serverDeployDir = join(typeDir, "server-deploy");
|
|
407
450
|
if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
|
|
408
|
-
merge(classify(serverDeployDir, workflowsDir, envOpts, srcText, baseline, deployFilter(deployStyle)), type);
|
|
451
|
+
merge(classify(serverDeployDir, workflowsDir, envOpts, srcText, baseline, deployFilter(deployStyle, available)), type);
|
|
409
452
|
}
|
|
410
453
|
}
|
|
411
454
|
|
package/src/core/deploy-style.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Nginx 무중단과 Traefik 무중단을 동시에 쓰는 경우는 없으므로 하나만 설치한다.
|
|
3
3
|
// 고른 것은 push 트리거까지 켜서 설치한다 — 설치했는데 안 도는 상태를 만들지 않는다.
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
6
6
|
import { sha256 } from "./baseline.js";
|
|
7
|
+
import { PAYLOAD, typeWorkflowDirs } from "./paths.js";
|
|
8
|
+
import { SINGLE_SERVER_CD_FILES } from "./types.js";
|
|
7
9
|
|
|
8
10
|
// 파일명 접미사로 식별한다 — 타입 접두사(PROJECT-SPRING- 등)는 타입마다 다르기 때문.
|
|
9
11
|
export const DEPLOY_STYLES = [
|
|
@@ -25,17 +27,81 @@ export const isDeployStyle = (v) => v === NO_DEPLOY_STYLE || DEPLOY_STYLES.some(
|
|
|
25
27
|
// 이 파일이 CD 본체인가 (= 택1 대상인가). PR 프리뷰는 배포 방식과 직교하는 축이라 제외한다.
|
|
26
28
|
export const isDeployWorkflow = (filename) => DEPLOY_STYLES.some((s) => filename.endsWith(s.suffix));
|
|
27
29
|
|
|
30
|
+
const PREVIEW_SUFFIX = "-PR-PREVIEW.yaml";
|
|
31
|
+
|
|
32
|
+
// 서버에 배포하는 워크플로우 전부 — CD 본체, 단일 CD(react·next), PR 프리뷰.
|
|
33
|
+
// "배포 안 함"은 이 전부를 모든 타입에서 똑같이 뺀다. 일부만 빼면 서버 Secret 요구가 남는다.
|
|
34
|
+
export const isServerDeployWorkflow = (filename) =>
|
|
35
|
+
isDeployWorkflow(filename) || SINGLE_SERVER_CD_FILES.has(filename) || filename.endsWith(PREVIEW_SUFFIX);
|
|
36
|
+
|
|
37
|
+
// 선택한 타입 중 서버 배포 워크플로우를 가진 타입이 있는가 — payload 파일로 판정한다.
|
|
38
|
+
// 없으면(node·flutter 등) 배포 방식은 설치 결과에 아무 영향이 없으므로 묻지도 기록하지도 않는다.
|
|
39
|
+
export function hasServerDeployWorkflows(payloadRoot, types = []) {
|
|
40
|
+
return types.some((type) => typeWorkflowDirs(payloadRoot, type)
|
|
41
|
+
.some((dir) => existsSync(dir) && readdirSync(dir).some(isServerDeployWorkflow)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 선택한 타입 중 무중단(nginx·traefik) 워크플로우가 있는 타입이 있는가 — 없으면 무중단 선택지를 보이지 않는다.
|
|
45
|
+
export function hasNonstopWorkflows(payloadRoot, types = []) {
|
|
46
|
+
const nonstop = DEPLOY_STYLES.filter((s) => s.value !== DEFAULT_DEPLOY_STYLE).map((s) => s.suffix);
|
|
47
|
+
return types.some((type) => typeWorkflowDirs(payloadRoot, type)
|
|
48
|
+
.some((dir) => existsSync(dir) && readdirSync(dir).some((f) => nonstop.some((sfx) => f.endsWith(sfx)))));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// payload/workflows/** 의 워크플로우 파일명 전체. 파일명은 PROJECT-{TYPE}- 접두사로 타입 간 유일하다.
|
|
52
|
+
export function payloadWorkflowNames(payloadRoot) {
|
|
53
|
+
const names = new Set();
|
|
54
|
+
const root = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
55
|
+
if (!existsSync(root)) return names;
|
|
56
|
+
for (const e of readdirSync(root, { recursive: true, withFileTypes: true })) {
|
|
57
|
+
if (e.isFile() && /\.ya?ml$/.test(e.name)) names.add(e.name);
|
|
58
|
+
}
|
|
59
|
+
return names;
|
|
60
|
+
}
|
|
61
|
+
|
|
28
62
|
// 모르는 값은 기본값으로 수렴시킨다. 빈 접미사를 돌려주면 endsWith("")가 항상 참이라
|
|
29
63
|
// "전부 통과"가 되어, 잘못된 값이 조용히 CD 전부 설치로 새어나간다.
|
|
30
64
|
const suffixOf = (style) =>
|
|
31
65
|
(DEPLOY_STYLES.find((s) => s.value === style) ?? DEPLOY_STYLES.find((s) => s.value === DEFAULT_DEPLOY_STYLE)).suffix;
|
|
66
|
+
const SIMPLE_SUFFIX = suffixOf(DEFAULT_DEPLOY_STYLE);
|
|
67
|
+
|
|
68
|
+
// 고른 방식의 CD가 없는 타입의 단일 서버 배포 CD인가 (예: nginx를 골랐는데 python·go에는 NONSTOP이 없다).
|
|
69
|
+
// 그런 타입은 단일 서버 배포로 대신 설치한다 — 설치 직후 지우거나 .bak으로 옮기면 배포가 사라진다.
|
|
70
|
+
function isFallbackSimple(filename, style, available) {
|
|
71
|
+
if (!available || !filename.endsWith(SIMPLE_SUFFIX)) return false;
|
|
72
|
+
const prefix = filename.slice(0, -SIMPLE_SUFFIX.length);
|
|
73
|
+
return !available.has(prefix + suffixOf(style));
|
|
74
|
+
}
|
|
32
75
|
|
|
33
76
|
// 파일 필터 — 고른 방식의 CD만 통과. CD가 아닌 파일(PR 프리뷰·common 등)은 항상 통과.
|
|
34
|
-
// "none"은 CD를 하나도 설치하지
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
// "none"은 서버 배포 워크플로우(CD·단일 CD·PR 프리뷰)를 하나도 설치하지 않는다.
|
|
78
|
+
// available: payload 워크플로우 파일명 집합(payloadWorkflowNames). 주면 고른 방식이 없는 타입은
|
|
79
|
+
// 단일 서버 배포로 대체한다. 없으면 접미사만으로 가른다.
|
|
80
|
+
export function deployFilter(style, available = null) {
|
|
81
|
+
if (style === NO_DEPLOY_STYLE) return (filename) => !isServerDeployWorkflow(filename);
|
|
37
82
|
const suffix = suffixOf(style);
|
|
38
|
-
return (filename) => !isDeployWorkflow(filename) || filename.endsWith(suffix)
|
|
83
|
+
return (filename) => !isDeployWorkflow(filename) || filename.endsWith(suffix)
|
|
84
|
+
|| isFallbackSimple(filename, style, available);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 고른 방식(nginx·traefik)의 워크플로우가 없어 단일 서버 배포로 대신 설치하는 타입 목록 — 안내용.
|
|
88
|
+
export function fallbackStyleTypes(payloadRoot, types = [], style) {
|
|
89
|
+
if (!style || style === NO_DEPLOY_STYLE || suffixOf(style) === SIMPLE_SUFFIX) return [];
|
|
90
|
+
return types.filter((type) => {
|
|
91
|
+
const files = typeWorkflowDirs(payloadRoot, type)
|
|
92
|
+
.filter((dir) => existsSync(dir)).flatMap((dir) => readdirSync(dir));
|
|
93
|
+
return files.some(isServerDeployWorkflow) && !files.some((f) => f.endsWith(suffixOf(style)));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 실제로 설치되는 배포 방식 — 고른 무중단 방식의 워크플로우가 선택한 타입 어디에도 없으면 전부 단일 서버
|
|
98
|
+
// 배포로 대신 설치되므로 simple이다. 고른 값을 그대로 기록하면 version.yml·status가 설치 상태와 어긋나고,
|
|
99
|
+
// 다음 실행도 그 값을 기본값으로 이어받는다. 한 타입이라도 그 방식이 있으면 고른 값을 유지한다.
|
|
100
|
+
export function effectiveDeployStyle(payloadRoot, types = [], style) {
|
|
101
|
+
if (!style || style === NO_DEPLOY_STYLE || suffixOf(style) === SIMPLE_SUFFIX) return style;
|
|
102
|
+
const installed = types.some((type) => typeWorkflowDirs(payloadRoot, type)
|
|
103
|
+
.some((dir) => existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(suffixOf(style)))));
|
|
104
|
+
return installed ? style : DEFAULT_DEPLOY_STYLE;
|
|
39
105
|
}
|
|
40
106
|
|
|
41
107
|
// 무중단 템플릿은 push 트리거가 주석 처리된 채 들어 있다(기본 배포가 단일 서버라서).
|
|
@@ -73,24 +139,30 @@ export function activateDeployTrigger(content) {
|
|
|
73
139
|
// 손대지 않은 것(baseline의 installed 해시와 동일) → 삭제. 물어볼 이유가 없다.
|
|
74
140
|
// 손댄 것 → .bak으로 옮긴다. 내용은 지키고 트리거만 죽인다.
|
|
75
141
|
//
|
|
142
|
+
// opts.available — payload 워크플로우 파일명 집합. 주면 그 안의 파일(마법사가 까는 파일)만 정리하고,
|
|
143
|
+
// 고른 방식이 없는 타입의 단일 서버 배포는 남긴다.
|
|
144
|
+
// opts.justWritten — 이번 실행에서 마법사가 방금 쓴 파일. 첫 설치처럼 baseline이 없어도 수정본으로 오인하지 않는다.
|
|
145
|
+
// opts.dryRun — 판정만 하고 파일은 건드리지 않는다. --dry-run 미리보기가 실제 실행과 같은 판정을 쓰게 한다.
|
|
76
146
|
// 반환: { removed:[], backedUp:[] } — 완료 화면·설치 기록에 그대로 보고한다.
|
|
77
|
-
export function cleanupOtherDeployWorkflows(workflowsDir, installedFilenames, style, baseline) {
|
|
78
|
-
const keep = deployFilter(style);
|
|
147
|
+
export function cleanupOtherDeployWorkflows(workflowsDir, installedFilenames, style, baseline, { available = null, justWritten = [], dryRun = false } = {}) {
|
|
148
|
+
const keep = deployFilter(style, available);
|
|
149
|
+
const written = new Set(justWritten);
|
|
79
150
|
const removed = [];
|
|
80
151
|
const backedUp = [];
|
|
81
152
|
|
|
82
153
|
for (const filename of installedFilenames) {
|
|
83
|
-
if (!
|
|
154
|
+
if (!isServerDeployWorkflow(filename) || keep(filename)) continue;
|
|
155
|
+
if (available && !available.has(filename)) continue; // 사용자가 만든 비슷한 이름의 워크플로우는 건드리지 않는다
|
|
84
156
|
const p = join(workflowsDir, filename);
|
|
85
157
|
if (!existsSync(p)) continue;
|
|
86
158
|
|
|
87
159
|
const known = baseline?.files?.[filename]?.installed;
|
|
88
|
-
const untouched = known && sha256(readFileSync(p, "utf8")) === known;
|
|
160
|
+
const untouched = written.has(filename) || (known && sha256(readFileSync(p, "utf8")) === known);
|
|
89
161
|
if (untouched) {
|
|
90
|
-
rmSync(p, { force: true });
|
|
162
|
+
if (!dryRun) rmSync(p, { force: true });
|
|
91
163
|
removed.push(filename);
|
|
92
164
|
} else {
|
|
93
|
-
renameSync(p, `${p}.bak`);
|
|
165
|
+
if (!dryRun) renameSync(p, `${p}.bak`);
|
|
94
166
|
backedUp.push(filename);
|
|
95
167
|
}
|
|
96
168
|
}
|