project-auto-wizard 0.12.2 → 0.13.1
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 +9 -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 +482 -107
- package/payload/version.yml.template +5 -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 +26 -60
- package/src/commands/dry-run.js +59 -5
- package/src/commands/full.js +135 -55
- package/src/commands/install-settings.js +51 -0
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +135 -69
- package/src/commands/purge.js +50 -21
- package/src/commands/status.js +18 -12
- package/src/commands/uninstall.js +22 -22
- 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/app-files.js +28 -0
- 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 +133 -111
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +177 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-doctor.js +49 -0
- package/src/core/flutter-hooks.js +29 -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-exec.js +67 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +136 -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 +128 -60
- 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
package/src/cli/help.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { TYPE_IDS } from "../core/types.js";
|
|
2
|
+
|
|
3
|
+
// 지원 타입 목록 — 레지스트리 순서대로 한 줄 40자 안쪽에서 줄바꿈하고, 이어지는 줄은 "지원: " 뒤에 맞춘다.
|
|
4
|
+
function typeListLines(width = 40) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
for (const id of TYPE_IDS) {
|
|
7
|
+
const last = lines.length - 1;
|
|
8
|
+
if (last >= 0 && lines[last].length + 1 + id.length <= width) lines[last] += ` ${id}`;
|
|
9
|
+
else lines.push(id);
|
|
10
|
+
}
|
|
11
|
+
return lines.join(`\n${" ".repeat(33)}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
// --help 텍스트.
|
|
2
15
|
export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-native 릴리스 자동화 설치 마법사
|
|
3
16
|
|
|
@@ -10,8 +23,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
10
23
|
uninstall = 완전 삭제(대화형 체크리스트, --force 시 --purge-*로 opt-in)
|
|
11
24
|
status = 설치 상태·드리프트 확인(읽기 전용). doctor = 환경 진단(읽기 전용)
|
|
12
25
|
-t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
|
|
13
|
-
지원:
|
|
14
|
-
react-native-expo node python basic go
|
|
26
|
+
지원: ${typeListLines()}
|
|
15
27
|
--project-version V 통합 대상의 초기 버전 (예: 1.0.0). 미지정 시 자동 감지
|
|
16
28
|
--paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
|
|
17
29
|
--main-branch B 릴리스 브랜치 (기본: 감지된 default branch)
|
|
@@ -23,7 +35,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
23
35
|
--ios-deploy-mode MODE iOS 배포 모드: store_only | store_prepare | store_submit (기본: store_only)
|
|
24
36
|
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
25
37
|
--copilot / --no-copilot Copilot으로 AI 요약 생성 (기본: 사용 안 함, GitHub Copilot AI Credits 소비)
|
|
26
|
-
--force full 실행에
|
|
38
|
+
--force full 실행에 필수, uninstall은 비대화형 삭제 (모든 확인 생략, 기본값 사용)
|
|
27
39
|
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/uninstall 지원)
|
|
28
40
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
|
29
41
|
--purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
@@ -33,7 +45,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
33
45
|
|
|
34
46
|
예시:
|
|
35
47
|
npx project-auto-wizard --mode full --force --type spring,react
|
|
36
|
-
npx project-auto-wizard --mode
|
|
48
|
+
npx project-auto-wizard --mode full --force --type flutter --paths "flutter=app"
|
|
37
49
|
npx project-auto-wizard --mode status
|
|
38
50
|
npx project-auto-wizard --mode doctor
|
|
39
51
|
npx project-auto-wizard --mode full --force --type node --dry-run
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// doctor 명령 — 로컬 환경 진단(읽기 전용, 규칙 기반). gh CLI에 위임해 원격 상태를 점검한다.
|
|
2
|
-
// AI 진단은 포함하지 않는다(스펙 §4에서 검토 후 기각 — 복잡도 대비 이득 낮음).
|
|
3
2
|
//
|
|
4
3
|
// 출력 설계 — `flutter doctor` 패턴을 차용한다.
|
|
5
4
|
// ① 항목 라벨에 purpose("무엇을 위한 설정인지")를 병기한다. `WORKFLOW_PAT`만 보고는 그게
|
|
@@ -12,22 +11,21 @@
|
|
|
12
11
|
// 않는다. 번역하면 설명은 읽히지만 정작 화면에서 그 항목을 찾지 못한다.
|
|
13
12
|
import { spawnSync } from "node:child_process";
|
|
14
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join
|
|
14
|
+
import { join } from "node:path";
|
|
16
15
|
import { A, paint, colorEnabled, visualWidth } from "../ui/ansi.js";
|
|
17
16
|
import { PATHS } from "../core/paths.js";
|
|
18
17
|
import { parseExisting } from "../core/version-yml.js";
|
|
19
|
-
import {
|
|
20
|
-
import { inferInstalledStores } from "../core/installed-stores.js";
|
|
18
|
+
import { hooksFor } from "../core/types.js";
|
|
21
19
|
|
|
22
20
|
const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
23
21
|
|
|
24
|
-
// 해결 가이드
|
|
25
|
-
// 해결 가이드 링크(README 앵커)"). README에 심은 영문 HTML 앵커를 가리킨다 — 한글 헤딩
|
|
22
|
+
// 해결 가이드 링크. 문서 사이트 페이지 안의 영문 HTML 앵커를 가리킨다 — 한글 헤딩
|
|
26
23
|
// 자동 앵커는 URL 인코딩되어 터미널에서 알아볼 수 없게 깨진다.
|
|
27
|
-
|
|
24
|
+
// 진단 출력이 한국어이므로 한국어 페이지(/ko/)로 연결한다.
|
|
25
|
+
export const DOCS_SITE_URL = "https://twin-fang.github.io/project-auto-wizard";
|
|
28
26
|
export const DOC = {
|
|
29
|
-
postInstall: `${
|
|
30
|
-
flutterStore: `${
|
|
27
|
+
postInstall: `${DOCS_SITE_URL}/ko/start/quickstart/#post-install`,
|
|
28
|
+
flutterStore: `${DOCS_SITE_URL}/ko/project-types/flutter/#flutter-store`,
|
|
31
29
|
};
|
|
32
30
|
|
|
33
31
|
export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
@@ -44,8 +42,13 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
44
42
|
note: ["이 폴더엔 아직 설치되지 않았습니다 — 지금 설치하면 됩니다."],
|
|
45
43
|
});
|
|
46
44
|
|
|
47
|
-
// Flutter 스토어 배포 파일
|
|
48
|
-
if (installed)
|
|
45
|
+
// 타입별 로컬 파일 점검(예: Flutter 스토어 배포 파일) — 로컬 파일만 보므로 gh 조회 결과와 무관하게 수행한다.
|
|
46
|
+
if (installed) {
|
|
47
|
+
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
48
|
+
for (const { hook } of hooksFor(existing.types, "doctorChecks")) {
|
|
49
|
+
for (const item of hook(cwd, existing, { docs: DOC })) add(item);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
49
52
|
|
|
50
53
|
const ghVersion = exec("gh", ["--version"]);
|
|
51
54
|
if (ghVersion.error || ghVersion.status !== 0) {
|
|
@@ -77,7 +80,8 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
77
80
|
|
|
78
81
|
const remote = exec("git", ["-C", cwd, "remote", "get-url", "origin"]);
|
|
79
82
|
const url = remote.status === 0 ? (remote.stdout || "").trim() : "";
|
|
80
|
-
|
|
83
|
+
// 레포 이름에는 점이 올 수 있다(user.github.io, next.js) — 끝의 .git만 떼어낸다.
|
|
84
|
+
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(\.git)?\/?$/);
|
|
81
85
|
if (!match) {
|
|
82
86
|
add({
|
|
83
87
|
name: "GitHub 원격", purpose: "점검 대상 레포 식별", status: "WARN",
|
|
@@ -129,7 +133,7 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
129
133
|
// 파이프라인을 끝까지 이어가므로, 없는 장애를 경고로 띄우지 않는다.
|
|
130
134
|
name: "WORKFLOW_PAT secret", label: "WORKFLOW_PAT", purpose: "자동 태그·Release 발행", status: "INFO",
|
|
131
135
|
note: [
|
|
132
|
-
"secret이 없어도 폴백이 자동으로 이어받아 태그·Release
|
|
136
|
+
"secret이 없어도 폴백이 자동으로 이어받아 태그·Release 발행과 main 배포 워크플로우 실행까지 진행됩니다 — 실제 병합 후 최대 ~20초 정도 더 걸릴 뿐입니다.",
|
|
133
137
|
"속도를 더 원한다면 PAT을 등록할 수 있습니다 — 반드시 개인 계정이 아닌 조직 bot/machine 계정으로 발급하세요 (scopes: repo, workflow).",
|
|
134
138
|
"등록: 레포 Settings → Secrets and variables → Actions → New repository secret · 이름은 WORKFLOW_PAT",
|
|
135
139
|
],
|
|
@@ -160,11 +164,18 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
160
164
|
});
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
// 실제 설정값을 보여준다 — 켜 둔 사용자에게 "꺼져 있다"고 안내하면 현재 상태를 오해하게 된다.
|
|
168
|
+
const copilotAi = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")).options.copilotAi : null;
|
|
169
|
+
const copilotState = copilotAi === true
|
|
170
|
+
? "켜져 있습니다 (version.yml의 copilot_ai: true)."
|
|
171
|
+
: copilotAi === false
|
|
172
|
+
? "꺼져 있습니다 (version.yml의 copilot_ai: false) — 켜려면 --copilot으로 다시 설치하거나 대화형 '수정하기 > Copilot AI 요약'을 쓰세요."
|
|
173
|
+
: "기본은 꺼져 있습니다 (version.yml의 copilot_ai: false).";
|
|
163
174
|
add({
|
|
164
175
|
name: "Copilot AI 요약", label: "Copilot AI 요약", purpose: "AI 릴리스 노트 생성(선택)", status: "INFO",
|
|
165
176
|
note: [
|
|
166
|
-
|
|
167
|
-
"켜면 GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이
|
|
177
|
+
copilotState,
|
|
178
|
+
`${copilotAi === true ? "" : "켜면 "}GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이 필요합니다.`,
|
|
168
179
|
"꺼져 있거나 사용할 수 없으면 규칙 기반 요약으로 자동 전환되므로 그대로 두셔도 됩니다.",
|
|
169
180
|
],
|
|
170
181
|
});
|
|
@@ -172,51 +183,6 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
172
183
|
return results;
|
|
173
184
|
}
|
|
174
185
|
|
|
175
|
-
const PLATFORM_ROW_NAME = { android: "Flutter Android 배포 파일", ios: "Flutter iOS 배포 파일" };
|
|
176
|
-
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // 감지 규칙은 ExportOptions.plist 템플릿·IOS-TESTFLIGHT 검증과 동일
|
|
177
|
-
const EXPORT_OPTIONS_REL = STORE_APP_FILES.ios.find((rel) => rel.endsWith("ExportOptions.plist"));
|
|
178
|
-
|
|
179
|
-
// Flutter 스토어 배포 진단 — 선택한 플랫폼의 필수 파일과 ExportOptions.plist 플레이스홀더.
|
|
180
|
-
// 스토어 시크릿 등록 여부는 이번 범위 밖이다. 반환: doctor 결과 행 배열.
|
|
181
|
-
function flutterStoreChecks(cwd) {
|
|
182
|
-
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
183
|
-
if (!existing.types.includes("flutter")) return [];
|
|
184
|
-
const saved = existing.options.flutterStore == null ? null : parseStoreList(existing.options.flutterStore);
|
|
185
|
-
// 저장값 없는 기존 설치는 설치된 스토어 워크플로우로 추론한다 (interactive와 같은 규칙).
|
|
186
|
-
const stores = saved ?? inferInstalledStores(join(cwd, PATHS.workflowsDir));
|
|
187
|
-
const flutterRoot = existing.paths.get("flutter") || ".";
|
|
188
|
-
const rows = [];
|
|
189
|
-
|
|
190
|
-
for (const platform of STORE_PLATFORMS.filter((p) => stores.includes(p))) {
|
|
191
|
-
const files = storeAppFilesFor([platform]).map((rel) => posix.join(flutterRoot, rel));
|
|
192
|
-
const missing = files.filter((file) => !existsSync(join(cwd, file)));
|
|
193
|
-
const head = { name: PLATFORM_ROW_NAME[platform], purpose: "fastlane 스토어 업로드에 필요한 파일" };
|
|
194
|
-
rows.push(missing.length
|
|
195
|
-
? {
|
|
196
|
-
...head, status: "WARN", value: `없는 파일: ${missing.join(", ")}`,
|
|
197
|
-
impact: ["이 파일이 없으면 스토어 배포 워크플로우가 fastlane 단계에서 실패합니다."],
|
|
198
|
-
actions: ["마법사를 다시 실행하면 없는 파일만 새로 만들어 줍니다 (이미 있는 파일은 덮어쓰지 않음)"],
|
|
199
|
-
doc: DOC.flutterStore,
|
|
200
|
-
}
|
|
201
|
-
: { ...head, status: "OK", value: `${files.length}개 있음` });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const plistPath = posix.join(flutterRoot, EXPORT_OPTIONS_REL);
|
|
205
|
-
if (stores.includes("ios") && existsSync(join(cwd, plistPath))) {
|
|
206
|
-
const head = { name: "ExportOptions.plist", purpose: "iOS 서명·내보내기 설정" };
|
|
207
|
-
const left = [...new Set(readFileSync(join(cwd, plistPath), "utf8").match(PLACEHOLDER_RE) ?? [])];
|
|
208
|
-
rows.push(left.length
|
|
209
|
-
? {
|
|
210
|
-
...head, status: "WARN", value: `채워지지 않은 값: ${left.join(", ")}`,
|
|
211
|
-
impact: ["채우지 않으면 IOS-TESTFLIGHT 워크플로우가 ExportOptions.plist 검증 단계에서 중단됩니다."],
|
|
212
|
-
actions: [`${plistPath} 의 플레이스홀더를 실제 값(Team ID · 번들 ID · 프로비저닝 프로파일 이름)으로 바꾸세요`],
|
|
213
|
-
doc: DOC.flutterStore,
|
|
214
|
-
}
|
|
215
|
-
: { ...head, status: "OK", value: "플레이스홀더 없음" });
|
|
216
|
-
}
|
|
217
|
-
return rows;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
186
|
const asLines = (v) => (Array.isArray(v) ? v : v ? [String(v)] : []);
|
|
221
187
|
const headOf = (r) => `${r.label || r.name}${r.purpose ? ` — ${r.purpose}` : ""}`;
|
|
222
188
|
|
package/src/commands/dry-run.js
CHANGED
|
@@ -4,11 +4,16 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { PATHS } from "../core/paths.js";
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
|
-
import {
|
|
7
|
+
import { planTypeAppFiles } from "../core/copy/app-files.js";
|
|
8
8
|
import { planUninstall } from "./uninstall.js";
|
|
9
|
-
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
|
+
import { planScripts } from "../core/copy/simple.js";
|
|
13
|
+
import { planVersionSection } from "../core/copy/readme.js";
|
|
14
|
+
import { BASELINE_PATH, readBaseline } from "../core/baseline.js";
|
|
15
|
+
import { cleanupWorkflows } from "./full.js";
|
|
16
|
+
import { planGitignore } from "../core/copy/gitignore.js";
|
|
12
17
|
|
|
13
18
|
function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
14
19
|
const { paths = new Map() } = context;
|
|
@@ -23,7 +28,8 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
|
23
28
|
const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
|
|
24
29
|
pathMarkers, extraTopLevel,
|
|
25
30
|
});
|
|
26
|
-
|
|
31
|
+
// 실제 설치와 같은 기준으로 비교한다 — 시각 줄만 다르면 설치도 파일을 다시 쓰지 않는다.
|
|
32
|
+
return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
// mode: "full" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
|
|
@@ -31,12 +37,25 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
31
37
|
if (mode === "uninstall") {
|
|
32
38
|
return { mode, uninstall: planUninstall(payloadRoot, targetRoot, context.uninstallSelection) };
|
|
33
39
|
}
|
|
40
|
+
const workflows = planWorkflows(context, payloadRoot, targetRoot);
|
|
41
|
+
// 실제 실행과 같은 정리 판정을 파일을 건드리지 않고 돌린다 — 배포 방식 변경·스토어 해제로 지워지거나
|
|
42
|
+
// .bak으로 옮겨질 파일이 미리보기에서 빠지면 안 된다. 비대화형 실행이 새로 쓰는 파일은 신규·자동 갱신분이다.
|
|
43
|
+
const justWritten = [...workflows.newFiles, ...workflows.upstreamOnly].map((f) => f.filename);
|
|
44
|
+
const cleanup = cleanupWorkflows(context, payloadRoot, targetRoot, readBaseline(targetRoot), { justWritten, dryRun: true });
|
|
45
|
+
// .bak이 생기면 실제 실행은 .gitignore에 백업 파일 항목을 보장한다. 비대화형 충돌은 기존 유지라 .bak을 만들지 않는다.
|
|
46
|
+
const backedUp = Object.values(cleanup).some((r) => r.backedUp.length > 0);
|
|
34
47
|
return {
|
|
35
48
|
mode,
|
|
36
|
-
workflows
|
|
49
|
+
workflows,
|
|
50
|
+
cleanup,
|
|
51
|
+
gitignore: backedUp ? planGitignore(targetRoot) : null,
|
|
37
52
|
// Flutter 스토어 배포 파일(Fastfile·ExportOptions.plist) — 이미 있는 파일은 덮어쓰지 않고 유지한다.
|
|
38
|
-
flutterApp:
|
|
53
|
+
flutterApp: planTypeAppFiles(context, payloadRoot, targetRoot),
|
|
39
54
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
55
|
+
// 실제 설치가 함께 바꾸는 파일 — 특히 기존 파일을 덮어쓰는 스크립트는 미리 보여줘야 한다.
|
|
56
|
+
scripts: planScripts(payloadRoot, targetRoot),
|
|
57
|
+
readme: planVersionSection(targetRoot),
|
|
58
|
+
baselineExists: existsSync(join(targetRoot, BASELINE_PATH)),
|
|
40
59
|
};
|
|
41
60
|
}
|
|
42
61
|
|
|
@@ -48,6 +67,7 @@ export function printDryRun(plan) {
|
|
|
48
67
|
for (const f of u.workflows) lines.push(` - ${f}`);
|
|
49
68
|
lines.push(`제거될 스크립트 (${u.scripts.length}개):`);
|
|
50
69
|
for (const f of u.scripts) lines.push(` - ${f}`);
|
|
70
|
+
for (const f of u.appFiles || []) lines.push(`제거될 Flutter 앱 파일: ${f}`);
|
|
51
71
|
if (u.readme) lines.push("제거될 항목: README.md 버전 섹션 (AUTO-VERSION-SECTION)");
|
|
52
72
|
if (u.gitignore) lines.push("제거될 항목: .gitignore 자동 추가 항목");
|
|
53
73
|
if (u.versionYml) lines.push("제거될 파일: version.yml");
|
|
@@ -60,6 +80,23 @@ export function printDryRun(plan) {
|
|
|
60
80
|
for (const f of w.changed) lines.push(` ~ ${f.filename} [${f.type}]`);
|
|
61
81
|
lines.push(`동일한 파일 (${w.unchanged.length}개, 변경 없음)`);
|
|
62
82
|
}
|
|
83
|
+
if (plan.cleanup) {
|
|
84
|
+
const reasons = { cleanup: "이전 배포 방식 정리", storeCleanup: "선택 해제된 스토어 워크플로우 정리", staleCleanup: "현재 버전에 없는 이전 워크플로우 정리" };
|
|
85
|
+
const removed = [];
|
|
86
|
+
const backedUp = [];
|
|
87
|
+
for (const [key, reason] of Object.entries(reasons)) {
|
|
88
|
+
for (const f of plan.cleanup[key]?.removed || []) removed.push(` - ${f} (${reason})`);
|
|
89
|
+
for (const f of plan.cleanup[key]?.backedUp || []) backedUp.push(` > ${f} → ${f}.bak (${reason}, 수정본이라 백업)`);
|
|
90
|
+
}
|
|
91
|
+
if (removed.length) lines.push(`삭제될 파일 (${removed.length}개):`, ...removed);
|
|
92
|
+
if (backedUp.length) lines.push(`.bak으로 옮겨질 파일 (${backedUp.length}개):`, ...backedUp);
|
|
93
|
+
}
|
|
94
|
+
if (plan.gitignore) {
|
|
95
|
+
const g = plan.gitignore;
|
|
96
|
+
lines.push(g.created
|
|
97
|
+
? `.gitignore: 새로 생성될 예정 (${g.added.join(", ")})`
|
|
98
|
+
: g.added.length ? `.gitignore: ${g.added.join(", ")} 추가될 예정` : ".gitignore: 변경 없음 (이미 있는 항목)");
|
|
99
|
+
}
|
|
63
100
|
if (plan.flutterApp && (plan.flutterApp.created.length || plan.flutterApp.kept.length)) {
|
|
64
101
|
const { created, kept } = plan.flutterApp;
|
|
65
102
|
lines.push(`Flutter 스토어 배포 파일 — 신규 (${created.length}개):`);
|
|
@@ -75,6 +112,23 @@ export function printDryRun(plan) {
|
|
|
75
112
|
// spring 등 deploy 블록이 있는 타입은 실제 설치 결과와 미리보기가 다를 수 있음을 안내한다.
|
|
76
113
|
lines.push(" (참고: 배포 설정 질문이 있는 타입(spring 등)은 deploy: 블록이 미리보기에 반영되지 않아 실제 설치와 다르게 보일 수 있습니다.)");
|
|
77
114
|
}
|
|
115
|
+
if (plan.scripts) {
|
|
116
|
+
const mark = { create: "+", overwrite: "~", unchanged: "=" };
|
|
117
|
+
const note = { create: "새로 생성", overwrite: "기존 파일을 새 버전으로 덮어씀 — 직접 고친 내용은 사라집니다", unchanged: "변경 없음" };
|
|
118
|
+
lines.push(`스크립트 (${PATHS.scriptsDir}/, ${plan.scripts.length}개 — 항상 새 버전으로 교체):`);
|
|
119
|
+
for (const s of plan.scripts) lines.push(` ${mark[s.action]} ${s.name} (${note[s.action]})`);
|
|
120
|
+
}
|
|
121
|
+
if (plan.readme) {
|
|
122
|
+
lines.push({
|
|
123
|
+
added: "README.md: 끝에 버전 섹션이 추가될 예정",
|
|
124
|
+
"skip-no-readme": "README.md: 파일이 없어 버전 섹션을 추가하지 않음",
|
|
125
|
+
"skip-marker": "README.md: 이미 버전 섹션이 있어 변경 없음",
|
|
126
|
+
"skip-version-line": "README.md: 이미 버전 줄이 있어 변경 없음",
|
|
127
|
+
}[plan.readme] || `README.md: ${plan.readme}`);
|
|
128
|
+
}
|
|
129
|
+
if (plan.baselineExists !== undefined) {
|
|
130
|
+
lines.push(`${BASELINE_PATH}: ${plan.baselineExists ? "갱신될 예정" : "새로 생성될 예정"} (다음 업데이트의 비교 기준)`);
|
|
131
|
+
}
|
|
78
132
|
}
|
|
79
133
|
lines.push("");
|
|
80
134
|
console.log(lines.join("\n"));
|
package/src/commands/full.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
// full 모드 오케스트레이터
|
|
1
|
+
// full 모드 오케스트레이터 — 대화형·비대화형이 확정한 context로 실제 설치를 수행한다.
|
|
2
2
|
// 복사 순서: workflows(+env 치환) → flutter 앱 파일 → version.yml → readme → scripts → gitignore(조건부)
|
|
3
3
|
// gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다.
|
|
4
|
-
// (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
|
-
import { writeText } from "../core/fsutil.js";
|
|
6
|
+
import { writeText, findUnwritable } from "../core/fsutil.js";
|
|
7
|
+
import { CliError } from "../core/errors.js";
|
|
8
8
|
import { PATHS } from "../core/paths.js";
|
|
9
|
-
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
|
-
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
12
|
+
import { addVersionSectionToReadme, README_STATUS_LABEL } from "../core/copy/readme.js";
|
|
13
13
|
import { copyWorkflows, computeBaselineEntries, makeSrcText } from "../core/copy/workflows.js";
|
|
14
|
-
import { copyScripts } from "../core/copy/simple.js";
|
|
15
|
-
import {
|
|
14
|
+
import { copyScripts, removeScriptBytecode } from "../core/copy/simple.js";
|
|
15
|
+
import { copyTypeAppFiles } from "../core/copy/app-files.js";
|
|
16
16
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
17
|
-
import { readBaseline, writeBaseline } from "../core/baseline.js";
|
|
18
|
-
import { scanUnsubstituted,
|
|
19
|
-
import { cleanupOtherDeployWorkflows, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
20
|
-
import {
|
|
17
|
+
import { readBaseline, writeBaseline, appFileHash } from "../core/baseline.js";
|
|
18
|
+
import { scanUnsubstituted, classifySecrets, narrowSecretsBySshAuth } from "../core/verify.js";
|
|
19
|
+
import { cleanupOtherDeployWorkflows, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
20
|
+
import { hooksFor } from "../core/types.js";
|
|
21
|
+
import { findStaleWorkflows, cleanupStaleWorkflows } from "../core/removal-plan.js";
|
|
21
22
|
import { log, maskValue } from "../core/logger.js";
|
|
22
23
|
|
|
23
24
|
// context: { version, types, paths:Map, branch, versionCode,
|
|
@@ -28,17 +29,28 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
28
29
|
force = true, now, today, templateVersion = "unknown",
|
|
29
30
|
includeSemverAuto } = context;
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
+
// 아무것도 쓰기 전에 권한부터 확인한다 — 도중에 멈추면 반쯤 설치된 상태가 남는다.
|
|
33
|
+
const blocked = findUnwritable(targetRoot,
|
|
34
|
+
[".", PATHS.workflowsDir, PATHS.scriptsDir, ".github/.wizard"],
|
|
35
|
+
[PATHS.versionFile, "README.md", ".gitignore", ".github/.wizard/baseline.json"]);
|
|
36
|
+
if (blocked.length) {
|
|
37
|
+
throw new CliError(`쓰기 권한이 없어 설치를 시작하지 않았습니다 (아무 파일도 바꾸지 않았습니다):\n${blocked.map((p) => ` - ${p}`).join("\n")}\n권한을 확인한 뒤 다시 실행하세요.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// project_paths 마커 계산.
|
|
32
41
|
// 대표 마커명이 아니라 그 폴더에 실제로 있는 파일을 쓴다 — build.gradle.kts만 있는 레포의
|
|
33
42
|
// version.yml에 "# build.gradle"이라고 적히면 감지 로그와 같은 종류의 거짓말이 된다.
|
|
34
43
|
const pathMarkers = new Map();
|
|
35
44
|
for (const [t, p] of paths) {
|
|
36
45
|
const marker = existingMarkerInDir(t, join(targetRoot, p || "."));
|
|
37
46
|
pathMarkers.set(t, marker);
|
|
38
|
-
|
|
47
|
+
// 파일이 실제로 없으면 사용자가 직접 고른 타입이다 — 대표 파일명을 근거로 적으면 감지된 것처럼 보인다.
|
|
48
|
+
const found = marker && existsSync(join(targetRoot, p || ".", marker));
|
|
49
|
+
log.info("detect", "type", `${t} (근거: ${found ? marker : "직접 선택"})`);
|
|
39
50
|
}
|
|
40
51
|
log.info("detect", "version", `${version}${context.versionSource ? ` (${context.versionSource})` : ""}`);
|
|
41
52
|
log.info("detect", "branch", `${branch}${context.branches ? ` | main=${context.branches.main} develop=${context.branches.develop} mode=${context.branches.mode}` : ""}`);
|
|
53
|
+
logChoices(context, types);
|
|
42
54
|
for (const a of context.envAnswers || []) {
|
|
43
55
|
log.info("prompt", a.isDefault ? "default" : "answer", `${a.key}=${maskValue(a.key, a.value)}`);
|
|
44
56
|
}
|
|
@@ -50,25 +62,36 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
50
62
|
|
|
51
63
|
// 1-1. Flutter 앱 파일(fastlane·ExportOptions) — 사용자가 값을 채워 쓰는 파일이라 없을 때만 만든다.
|
|
52
64
|
// 워크플로우 복사가 끝난 뒤에 실행한다(워크플로우가 이 파일들을 전제로 돈다).
|
|
53
|
-
const flutterApp =
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
const flutterApp = copyTypeAppFiles(context, payloadRoot, targetRoot, (tag, action, f) => {
|
|
66
|
+
log.info(tag, action, action === "keep" ? `${f} (기존 파일 유지)` : f);
|
|
67
|
+
});
|
|
56
68
|
|
|
57
69
|
// 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다.
|
|
58
70
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
59
|
-
const
|
|
71
|
+
const prevVy = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
72
|
+
const extraTopLevel = prevVy != null ? parseExisting(prevVy).extraTopLevel : [];
|
|
60
73
|
|
|
61
74
|
// 2. version.yml 생성 (payload/version.yml.template 렌더링 — 전체 재생성)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
// 날짜 줄만 달라지는 재실행은 다시 쓰지 않는다(멱등).
|
|
76
|
+
const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel });
|
|
77
|
+
if (prevVy == null || !sameIgnoringTimestamps(prevVy, vyText)) {
|
|
78
|
+
writeText(vyPath, vyText);
|
|
79
|
+
log.info("version", "write", `version.yml (v${version}, code=${versionCode})`);
|
|
80
|
+
} else {
|
|
81
|
+
log.info("version", "skip", `version.yml (v${version}, code=${versionCode}, 변경 없음)`);
|
|
82
|
+
}
|
|
66
83
|
|
|
67
84
|
// 3. README 버전 섹션
|
|
68
|
-
addVersionSectionToReadme(version, targetRoot);
|
|
85
|
+
const readme = addVersionSectionToReadme(version, targetRoot);
|
|
86
|
+
log.info("readme", readme === "added" ? "append" : "skip", README_STATUS_LABEL[readme] || readme);
|
|
69
87
|
|
|
70
|
-
// 4. scripts (payload/scripts/*.py → .github/scripts/)
|
|
71
|
-
copyScripts(payloadRoot, targetRoot);
|
|
88
|
+
// 4. scripts (payload/scripts/*.py → .github/scripts/) — 항상 덮어쓰므로 사용자 수정이 사라진 사실도 남긴다.
|
|
89
|
+
const scripts = copyScripts(payloadRoot, targetRoot);
|
|
90
|
+
for (const { name, action } of scripts) {
|
|
91
|
+
log.info("script", action, `${PATHS.scriptsDir}/${name}${action === "overwrite" ? " (기존 내용과 달라 새 버전으로 덮어씀)" : ""}`);
|
|
92
|
+
}
|
|
93
|
+
const bytecodeRemoved = removeScriptBytecode(targetRoot);
|
|
94
|
+
for (const rel of bytecodeRemoved) log.info("script", "remove", `${rel} (예전 버전이 커밋한 바이트코드 — 삭제를 함께 커밋하세요)`);
|
|
72
95
|
|
|
73
96
|
// 5. gitignore — 워크플로우 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
|
|
74
97
|
// 충돌 없는 설치(대부분의 최초 설치)는 .gitignore를 전혀 건드리지 않는다.
|
|
@@ -77,35 +100,28 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
77
100
|
// 6. 이전 배포 방식 정리 — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
|
|
78
101
|
// 옛 baseline이 살아 있는 지금이 "사용자가 손댔는가"를 판정할 수 있는 유일한 시점이다.
|
|
79
102
|
const previousBaseline = readBaseline(targetRoot);
|
|
80
|
-
const cleanup =
|
|
81
|
-
|
|
82
|
-
existsSync(join(targetRoot, PATHS.workflowsDir)) ? readdirSync(join(targetRoot, PATHS.workflowsDir)) : [],
|
|
83
|
-
context.deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
84
|
-
previousBaseline);
|
|
103
|
+
const { cleanup, storeCleanup, staleCleanup } = cleanupWorkflows(context, payloadRoot, targetRoot, previousBaseline,
|
|
104
|
+
{ justWritten: wfCounters.copiedFiles || [] });
|
|
85
105
|
// 지운 파일의 기준점은 baseline에서도 빼야 다음 실행에서 "사용자가 지웠다"로 오인하지 않는다.
|
|
86
|
-
for (const
|
|
106
|
+
for (const r of [cleanup, storeCleanup, staleCleanup]) {
|
|
107
|
+
for (const f of [...r.removed, ...r.backedUp]) delete previousBaseline?.files?.[f];
|
|
108
|
+
}
|
|
87
109
|
|
|
88
110
|
for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (이전 배포 방식 정리)`);
|
|
89
111
|
for (const f of cleanup.backedUp || []) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
90
|
-
|
|
91
|
-
// 6-1. 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 남아
|
|
92
|
-
// main push마다 계속 도는 것을 막는다. 규칙은 6과 같다(미수정 삭제, 수정본 .bak).
|
|
93
|
-
// 선택이 미결정(null)이거나 Flutter가 없으면 현행 동작 그대로 아무것도 지우지 않는다.
|
|
94
|
-
// Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
|
|
95
|
-
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
96
|
-
const storeCleanup = Array.isArray(context.flutterStore) && types.includes("flutter")
|
|
97
|
-
? cleanupDeselectedStoreWorkflows(
|
|
98
|
-
workflowsDir,
|
|
99
|
-
existsSync(workflowsDir) ? readdirSync(workflowsDir) : [],
|
|
100
|
-
context.flutterStore,
|
|
101
|
-
previousBaseline)
|
|
102
|
-
: { removed: [], backedUp: [] };
|
|
103
|
-
for (const f of [...storeCleanup.removed, ...storeCleanup.backedUp]) delete previousBaseline?.files?.[f];
|
|
104
112
|
for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (선택 해제된 스토어 워크플로우 정리)`);
|
|
105
113
|
for (const f of storeCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
114
|
+
for (const f of staleCleanup.removed) log.info("cleanup", "remove", `${f} (현재 버전에 없는 이전 워크플로우 정리)`);
|
|
115
|
+
for (const f of staleCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
116
|
+
const cleanupCount = (key) => [cleanup, storeCleanup, staleCleanup].reduce((n, r) => n + r[key].length, 0);
|
|
117
|
+
|
|
118
|
+
const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0 || storeCleanup.backedUp.length > 0
|
|
119
|
+
|| staleCleanup.backedUp.length > 0;
|
|
120
|
+
if (gitignoreUpdated) {
|
|
121
|
+
const gi = ensureGitignore(targetRoot);
|
|
122
|
+
log.info("gitignore", gi.created ? "create" : gi.added.length ? "append" : "skip",
|
|
123
|
+
gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : ".gitignore (이미 있는 항목)");
|
|
124
|
+
}
|
|
109
125
|
|
|
110
126
|
// 7. baseline 기록 — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
|
|
111
127
|
// env 치환까지 전부 끝난 뒤에 해시해야 디스크 내용이 최종형이다. 그래서 copyWorkflows 안이
|
|
@@ -117,8 +133,11 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
117
133
|
entries: computeBaselineEntries(
|
|
118
134
|
wfCounters.baselineTargets || new Map(),
|
|
119
135
|
join(targetRoot, PATHS.workflowsDir),
|
|
120
|
-
makeSrcText(context.branches || null, context.deployStyle || "")
|
|
136
|
+
makeSrcText(context.branches || null, context.deployStyle || ""),
|
|
137
|
+
deployValues),
|
|
121
138
|
previous: previousBaseline,
|
|
139
|
+
// 새로 만든 Flutter 앱 파일만 기록한다 — 기존 사용자 파일(kept)은 완전 삭제 대상이 아니다.
|
|
140
|
+
appFiles: new Map(flutterApp.created.map((rel) => [rel, appFileHash(readFileSync(join(targetRoot, rel), "utf8"))])),
|
|
122
141
|
});
|
|
123
142
|
|
|
124
143
|
// 8. 설치 후 검증 — 디스크에 실제로 쓰인 내용을 다시 읽어 확인한다.
|
|
@@ -127,26 +146,87 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
127
146
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
128
147
|
const managed = [...(wfCounters.baselineTargets || new Map()).keys()];
|
|
129
148
|
const unresolved = scanUnsubstituted(wfDir, managed);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
// 기본값이 있거나 둘 중 하나면 되는 secret은 필수와 나눠 안내한다.
|
|
150
|
+
const secretSets = classifySecrets(wfDir, managed);
|
|
151
|
+
const secrets = narrowSecretsBySshAuth(secretSets.required, firstDeployValue(deployValues, "SSH_AUTH_METHOD"));
|
|
152
|
+
const optionalSecrets = secretSets.optional;
|
|
134
153
|
|
|
135
154
|
// 9. 요약 — 파일 끝에 결과 블록을 붙인다. tail만 봐도 결과가 보이도록.
|
|
136
155
|
for (const u of unresolved) log.warn("verify", "unresolved", `${u.filename}:${u.line} ${u.token}`);
|
|
137
156
|
for (const [name, users] of secrets) log.info("verify", "secret", `${name} ← ${users.join(", ")}`);
|
|
157
|
+
for (const [name, users] of optionalSecrets) log.info("verify", "secret-opt", `${name} (선택) ← ${users.join(", ")}`);
|
|
158
|
+
// copiedFiles에는 자동 갱신분도 들어 있다 — 빼지 않으면 같은 파일이 설치와 자동 갱신에 두 번 잡힌다.
|
|
159
|
+
const autoUpdated = new Set(wfCounters.autoUpdated || []);
|
|
138
160
|
log.summary([
|
|
139
|
-
["설치", `${(wfCounters.copiedFiles || []).length}개 파일`],
|
|
140
|
-
["자동 갱신", `${
|
|
161
|
+
["설치", `${(wfCounters.copiedFiles || []).filter((f) => !autoUpdated.has(f)).length}개 파일`],
|
|
162
|
+
["자동 갱신", `${autoUpdated.size}개 (사용자 미수정)`],
|
|
141
163
|
["유지", `${(wfCounters.keptLocal || []).length}개 (사용자 수정본)`],
|
|
142
164
|
["변경 없음", `${(wfCounters.unchangedFiles || []).length}개`],
|
|
143
165
|
["백업 교체", `${wfCounters.backupAdded || 0}개 (.bak 생성)`],
|
|
166
|
+
// 배포 방식 변경·스토어 해제·옛 워크플로우 정리도 파일을 지우거나 .bak으로 옮긴다 — 충돌 백업과 따로 센다.
|
|
167
|
+
["정리", `삭제 ${cleanupCount("removed")}개, .bak 이동 ${cleanupCount("backedUp")}개`],
|
|
144
168
|
["미치환", `${unresolved.length}건${unresolved.length ? " ← 조치 필요" : ""}`],
|
|
145
169
|
["필요 Secret", `${secrets.size}개`],
|
|
146
170
|
["결과", unresolved.length ? `주의 (미치환 ${unresolved.length}건)` : "OK"],
|
|
147
171
|
]);
|
|
148
172
|
|
|
149
|
-
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, flutterApp };
|
|
173
|
+
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, staleCleanup, flutterApp, readme, scripts, bytecodeRemoved, optionalSecrets };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 워크플로우 정리 3종 — 실제 설치(runFull)와 --dry-run 미리보기가 같은 판정을 쓴다.
|
|
177
|
+
// 6 배포 방식 정리 — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
|
|
178
|
+
// 6-1 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 main push마다
|
|
179
|
+
// 계속 돈다. 선택이 미결정(null)이거나 Flutter가 없으면 아무것도 지우지 않는다.
|
|
180
|
+
// Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
|
|
181
|
+
// 6-2 payload에서 이름이 바뀌거나 빠진 옛 워크플로우 정리.
|
|
182
|
+
// 규칙은 모두 같다(미수정 삭제, 수정본 .bak). 세 대상은 파일명이 겹치지 않아 순서와 무관하게 판정이 같다.
|
|
183
|
+
// baseline은 정리 전의 것을 넘겨야 한다 — "사용자가 손댔는가"를 판정할 수 있는 유일한 기준이다.
|
|
184
|
+
// dryRun이면 판정만 하고 파일은 건드리지 않는다.
|
|
185
|
+
export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { justWritten = [], dryRun = false } = {}) {
|
|
186
|
+
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
187
|
+
const listDir = () => (existsSync(workflowsDir) ? readdirSync(workflowsDir) : []);
|
|
188
|
+
const cleanup = cleanupOtherDeployWorkflows(workflowsDir, listDir(),
|
|
189
|
+
context.deployStyle || DEFAULT_DEPLOY_STYLE, baseline,
|
|
190
|
+
{ available: payloadWorkflowNames(payloadRoot), justWritten, dryRun });
|
|
191
|
+
const storeCleanup = { removed: [], backedUp: [] };
|
|
192
|
+
for (const { hook } of hooksFor(context.types || [], "cleanupWorkflows")) {
|
|
193
|
+
const r = hook(workflowsDir, listDir(), context, baseline, { dryRun });
|
|
194
|
+
storeCleanup.removed.push(...r.removed);
|
|
195
|
+
storeCleanup.backedUp.push(...r.backedUp);
|
|
196
|
+
}
|
|
197
|
+
const staleCleanup = cleanupStaleWorkflows(targetRoot, findStaleWorkflows(payloadRoot, targetRoot, baseline), baseline, { dryRun });
|
|
198
|
+
return { cleanup, storeCleanup, staleCleanup };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 설치 결과를 가른 선택(배포 방식·자동 승격·Copilot·Flutter 옵션)을 남긴다 — 대화형에서 고른 값도
|
|
202
|
+
// 여기로 모이므로, 나중에 "왜 이렇게 설치됐나"를 로그만으로 따라갈 수 있다.
|
|
203
|
+
function logChoices(context, types) {
|
|
204
|
+
if (context.deployStyle) log.info("option", "deploy", context.deployStyle);
|
|
205
|
+
if (context.includeSemverAuto != null) log.info("option", "semver", context.includeSemverAuto ? "on" : "off");
|
|
206
|
+
if (context.includeCopilotAi != null) log.info("option", "copilot", context.includeCopilotAi ? "on" : "off");
|
|
207
|
+
if (types.includes("flutter")) {
|
|
208
|
+
const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || "없음") : "미결정(둘 다)";
|
|
209
|
+
log.info("option", "flutter",
|
|
210
|
+
`env=${context.envMode || "-"} stores=${stores} android=${context.androidDeployMode || "-"} ios=${context.iosDeployMode || "-"}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 완료 요약과 별도로 알려야 하는 사실 — 비대화형 실행이 조용히 넘어가면 사용자는 업데이트를 다 받았다고 믿는다.
|
|
215
|
+
// interactive: 대화형은 충돌을 사용자가 직접 골랐으므로 충돌 안내를 빼고 정리 결과만 알린다.
|
|
216
|
+
export function postInstallNotices(result, { interactive = false } = {}) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
const stale = result?.staleCleanup || { removed: [], backedUp: [] };
|
|
219
|
+
if (stale.removed.length || stale.backedUp.length) {
|
|
220
|
+
lines.push("현재 버전에 없는 이전 워크플로우를 정리했습니다 (이름이 바뀌었거나 제거된 파일):");
|
|
221
|
+
for (const f of stale.removed) lines.push(` • ${f} — 삭제 (손대지 않은 파일)`);
|
|
222
|
+
for (const f of stale.backedUp) lines.push(` • ${f} → ${f}.bak — 수정하신 내용이 있어 백업`);
|
|
223
|
+
}
|
|
224
|
+
const kept = interactive ? [] : result?.workflows?.conflictKept || [];
|
|
225
|
+
if (kept.length) {
|
|
226
|
+
lines.push(`충돌 ${kept.length}개 — 기존 파일을 유지했습니다 (업스트림 변경 미반영): ${kept.join(", ")}`);
|
|
227
|
+
lines.push(" 파일마다 고르려면 대화형(npx project-auto-wizard)으로 다시 실행하세요.");
|
|
228
|
+
}
|
|
229
|
+
return lines;
|
|
150
230
|
}
|
|
151
231
|
|
|
152
232
|
// deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 설치 설정 해석 단계 — 비대화형(index.js)과 대화형(interactive.js)이 공유한다.
|
|
2
|
+
// 값을 채우는 방법(플래그·질문)은 경로마다 다르지만, 타입·버전·배포 방식·옵션을 확정하고
|
|
3
|
+
// 설치 컨텍스트로 조립하는 규칙은 한 곳에 둬야 한쪽만 고쳐 두 모드의 결과가 갈라지지 않는다.
|
|
4
|
+
import { detectVersion, detectBuildNumber } from "../core/detect-fs.js";
|
|
5
|
+
import { createContext } from "../context.js";
|
|
6
|
+
import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, effectiveDeployStyle } from "../core/deploy-style.js";
|
|
7
|
+
|
|
8
|
+
// version.yml에 저장된 배포 방식 — 없거나 알 수 없는 값이면 "".
|
|
9
|
+
export const savedDeployStyle = (existing) =>
|
|
10
|
+
isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
11
|
+
|
|
12
|
+
// 배포 방식 — 명시값(플래그·질문 답변) → 저장값 → 기본값.
|
|
13
|
+
// 서버 배포 워크플로우가 없는 타입은 설치 결과에 영향이 없으므로 기록하지 않는다(null).
|
|
14
|
+
export function resolveDeployStyle({ payload, types, explicit = "", existing }) {
|
|
15
|
+
if (!hasServerDeployWorkflows(payload, types)) return null;
|
|
16
|
+
return explicit || savedDeployStyle(existing) || DEFAULT_DEPLOY_STYLE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 버전 — 기존 version.yml 최우선(재실행 시 덮어쓰기 방지) → 명시값 → 파일 감지.
|
|
20
|
+
// detectOpts는 감지 함수로 그대로 넘긴다(types·paths·warn·hint).
|
|
21
|
+
export function resolveVersion({ cwd, existing, explicit = "", ...detectOpts }) {
|
|
22
|
+
return existing?.version || explicit || detectVersion(cwd, detectOpts);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 빌드 번호 — 기존 값 보존, 신규 통합이면 프로젝트 파일에서 감지, 못 찾으면 1.
|
|
26
|
+
export function resolveVersionCode({ cwd, existing, types, paths }) {
|
|
27
|
+
return existing?.versionCode ?? detectBuildNumber(cwd, { types, paths }) ?? 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 확정된 값들을 설치 컨텍스트로 조립한다.
|
|
31
|
+
// deployStyle: resolveDeployStyle 결과(고른 값). 실제 설치되는 방식으로 바꿔 기록한다.
|
|
32
|
+
// flutterOptions: { envMode, stores, androidDeployMode, iosDeployMode } — 두 경로 모두 확정된 값.
|
|
33
|
+
// releaseOptions: { includeSemverAuto, includeCopilotAi } — resolveReleaseOptions 결과.
|
|
34
|
+
// extra: 경로별로만 쓰는 필드(envValues 등).
|
|
35
|
+
export function buildInstallContext({
|
|
36
|
+
payload, existing, templateVersion, types, deployStyle, flutterOptions, releaseOptions, ...rest
|
|
37
|
+
}) {
|
|
38
|
+
return createContext({
|
|
39
|
+
types,
|
|
40
|
+
...releaseOptions,
|
|
41
|
+
// 안내는 고른 값으로 하되, 기록은 실제로 설치되는 방식으로 한다.
|
|
42
|
+
deployStyle: effectiveDeployStyle(payload, types, deployStyle),
|
|
43
|
+
envMode: flutterOptions.envMode,
|
|
44
|
+
flutterStore: flutterOptions.stores,
|
|
45
|
+
androidDeployMode: flutterOptions.androidDeployMode,
|
|
46
|
+
iosDeployMode: flutterOptions.iosDeployMode,
|
|
47
|
+
previousTemplateVersion: existing?.templateVersion || "",
|
|
48
|
+
templateVersion,
|
|
49
|
+
...rest,
|
|
50
|
+
});
|
|
51
|
+
}
|