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
|
@@ -86,7 +86,7 @@ env:
|
|
|
86
86
|
|
|
87
87
|
# ☁️ Spring Boot 설정
|
|
88
88
|
SPRING_PROFILE: "prod"
|
|
89
|
-
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask
|
|
89
|
+
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
|
|
90
90
|
GRADLE_OPTS: "-Dspring.profiles.active=prod"
|
|
91
91
|
|
|
92
92
|
# 📁 경로 설정
|
|
@@ -94,13 +94,15 @@ env:
|
|
|
94
94
|
DOCKERFILE_PATH: "./Dockerfile"
|
|
95
95
|
|
|
96
96
|
# 🌐 포트 및 컨테이너 설정
|
|
97
|
+
# BLUE/GREEN은 서버에서 번갈아 뜨는 두 컨테이너의 호스트 포트다 — 다른 서비스와 겹치면 배포가 실패한다.
|
|
97
98
|
CONTAINER_INTERNAL_PORT: "8080"
|
|
98
|
-
BLUE_PORT: "8080
|
|
99
|
-
GREEN_PORT: "8081
|
|
99
|
+
BLUE_PORT: "__BLUE_PORT__" # @wizard ask:8080
|
|
100
|
+
GREEN_PORT: "__GREEN_PORT__" # @wizard ask:8081
|
|
100
101
|
|
|
101
102
|
# 🔧 Nginx 설정
|
|
102
|
-
NGINX_RP_CONF
|
|
103
|
-
|
|
103
|
+
# NGINX_RP_CONF 경로가 틀리면 무중단 전환(proxy_pass 포트 토글) 자체가 동작하지 않는다.
|
|
104
|
+
NGINX_RP_CONF: "__NGINX_RP_CONF__" # @wizard ask:/etc/nginx/sites-enabled/__PROJECT_NAME__.conf
|
|
105
|
+
NGINX_BACKUP_DIR: "__NGINX_BACKUP_DIR__" # @wizard ask:/volume1/projects/nginx/backups
|
|
104
106
|
NGINX_BACKUP_KEEP: "10"
|
|
105
107
|
|
|
106
108
|
# 📦 볼륨 마운트 설정
|
|
@@ -114,7 +116,7 @@ env:
|
|
|
114
116
|
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
115
117
|
|
|
116
118
|
# 🔌 SSH 연결 설정
|
|
117
|
-
SSH_PORT: "2022
|
|
119
|
+
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
118
120
|
SSH_COMMAND_TIMEOUT: "600s"
|
|
119
121
|
SSH_CONNECTION_TIMEOUT: "60s"
|
|
120
122
|
|
|
@@ -82,7 +82,7 @@ env:
|
|
|
82
82
|
|
|
83
83
|
# ☁️ Spring Boot 설정
|
|
84
84
|
SPRING_PROFILE: "prod"
|
|
85
|
-
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask
|
|
85
|
+
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
|
|
86
86
|
GRADLE_OPTS: "-Dspring.profiles.active=prod"
|
|
87
87
|
|
|
88
88
|
# 📁 경로 설정
|
|
@@ -109,7 +109,7 @@ env:
|
|
|
109
109
|
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
110
110
|
|
|
111
111
|
# 🔌 SSH 연결 설정
|
|
112
|
-
SSH_PORT: "2022
|
|
112
|
+
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
113
113
|
SSH_COMMAND_TIMEOUT: "600s"
|
|
114
114
|
SSH_CONNECTION_TIMEOUT: "60s"
|
|
115
115
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
# 📊 리소스 네이밍 규칙:
|
|
23
23
|
# - 컨테이너: {PROJECT_NAME}-pr-{PR번호}
|
|
24
24
|
# - 이미지: {DOCKERHUB_USERNAME}/{PROJECT_NAME}:pr-{PR번호}
|
|
25
|
-
# - 도메인: {PROJECT_NAME}-pr-{PR번호}.pr.
|
|
25
|
+
# - 도메인: {PROJECT_NAME}-pr-{PR번호}.pr.example.com
|
|
26
26
|
# - Preview URL: http://{도메인}:8079
|
|
27
27
|
#
|
|
28
28
|
# ===================================================================
|
|
@@ -37,9 +37,9 @@ env:
|
|
|
37
37
|
PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
|
|
38
38
|
|
|
39
39
|
# Spring Boot 빌드 설정 (프로젝트별 수정 필요)
|
|
40
|
-
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask
|
|
40
|
+
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
|
|
41
41
|
GRADLE_BUILD_CMD: './gradlew clean build -x test -Dspring.profiles.active=prod'
|
|
42
|
-
JAR_PATH:
|
|
42
|
+
JAR_PATH: "__JAR_PATH__" # @wizard ask:build/libs/*.jar
|
|
43
43
|
APPLICATION_YML_PATH: "__APPLICATION_YML_PATH__" # @wizard auto:spring-app-yml-path
|
|
44
44
|
|
|
45
45
|
# Docker 설정 (프로젝트별 수정 필요)
|
|
@@ -52,7 +52,7 @@ env:
|
|
|
52
52
|
|
|
53
53
|
# Traefik & Preview 도메인 설정 (환경 구축 후 수정 금지)
|
|
54
54
|
TRAEFIK_NETWORK: traefik-network
|
|
55
|
-
PREVIEW_DOMAIN_SUFFIX: pr.
|
|
55
|
+
PREVIEW_DOMAIN_SUFFIX: "__PREVIEW_DOMAIN_SUFFIX__" # @wizard ask:pr.example.com
|
|
56
56
|
PREVIEW_PORT: '8079'
|
|
57
57
|
|
|
58
58
|
# 🔐 SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
|
|
@@ -61,7 +61,7 @@ env:
|
|
|
61
61
|
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
62
62
|
|
|
63
63
|
# SSH 포트 (서버 포트 환경 구축 후 수정 금지)
|
|
64
|
-
SSH_PORT:
|
|
64
|
+
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
65
65
|
|
|
66
66
|
# Issue Helper 댓글 마커 (프로젝트별 수정 필요)
|
|
67
67
|
# Issue 댓글에서 브랜치명을 추출할 때 사용
|
|
@@ -1814,8 +1814,8 @@ jobs:
|
|
|
1814
1814
|
if: steps.check_branch.outputs.exists == 'true'
|
|
1815
1815
|
uses: docker/login-action@v3
|
|
1816
1816
|
with:
|
|
1817
|
-
username: ${{ secrets.
|
|
1818
|
-
password: ${{ secrets.
|
|
1817
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
1818
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
1819
1819
|
|
|
1820
1820
|
- name: Docker 이미지 빌드 & 푸시
|
|
1821
1821
|
if: steps.check_branch.outputs.exists == 'true'
|
|
@@ -1823,7 +1823,7 @@ jobs:
|
|
|
1823
1823
|
run: |
|
|
1824
1824
|
DOCKER_START=$(date +%s)
|
|
1825
1825
|
CONTAINER_SUFFIX="${{ steps.extract.outputs.container_suffix }}"
|
|
1826
|
-
IMAGE_TAG="${{ secrets.
|
|
1826
|
+
IMAGE_TAG="${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${CONTAINER_SUFFIX}"
|
|
1827
1827
|
docker build -t $IMAGE_TAG .
|
|
1828
1828
|
docker push $IMAGE_TAG
|
|
1829
1829
|
DOCKER_END=$(date +%s)
|
|
@@ -79,7 +79,7 @@ env:
|
|
|
79
79
|
|
|
80
80
|
# ☁️ Spring Boot 설정
|
|
81
81
|
SPRING_PROFILE: "prod"
|
|
82
|
-
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask
|
|
82
|
+
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
|
|
83
83
|
GRADLE_OPTS: "-Dspring.profiles.active=prod"
|
|
84
84
|
|
|
85
85
|
# 📁 경로 설정
|
|
@@ -108,7 +108,7 @@ env:
|
|
|
108
108
|
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
109
109
|
|
|
110
110
|
# 🔌 SSH 연결 설정
|
|
111
|
-
SSH_PORT: "
|
|
111
|
+
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
112
112
|
SSH_COMMAND_TIMEOUT: "600s" # SSH 명령 실행 타임아웃
|
|
113
113
|
SSH_CONNECTION_TIMEOUT: "60s" # SSH 연결 타임아웃
|
|
114
114
|
|
package/src/cli/args.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
|
|
2
2
|
import { VALID_TYPES, VALID_MODES } from "../context.js";
|
|
3
|
+
import { DEPLOY_STYLES, isDeployStyle } from "../core/deploy-style.js";
|
|
3
4
|
|
|
4
5
|
// argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
|
|
5
6
|
export function parseArgs(argv) {
|
|
@@ -14,6 +15,7 @@ export function parseArgs(argv) {
|
|
|
14
15
|
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
15
16
|
mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
|
|
16
17
|
developBranch: "", // 개발 브랜치 (--develop-branch). 빈값=develop
|
|
18
|
+
deployStyle: "", // 서버 배포 방식 (--deploy-style). 빈값=version.yml 저장값 → simple
|
|
17
19
|
force: false,
|
|
18
20
|
help: false,
|
|
19
21
|
showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
|
|
@@ -75,6 +77,13 @@ export function parseArgs(argv) {
|
|
|
75
77
|
case "--keep-changelog": result.keepChangelog = true; break;
|
|
76
78
|
case "--keep-workflows": result.keepWorkflows = true; break;
|
|
77
79
|
case "--keep-scripts": result.keepScripts = true; break;
|
|
80
|
+
case "--deploy-style": {
|
|
81
|
+
const v = args.shift();
|
|
82
|
+
if (!isDeployStyle(v)) {
|
|
83
|
+
throw new CliError(`--deploy-style 값이 올바르지 않습니다: ${v ?? "(없음)"} (${DEPLOY_STYLES.map((s) => s.value).join(" | ")})`);
|
|
84
|
+
}
|
|
85
|
+
result.deployStyle = v; break;
|
|
86
|
+
}
|
|
78
87
|
case "--nexus":
|
|
79
88
|
if (seenFlags.has("--no-nexus")) throw new CliError("--nexus와 --no-nexus는 동시에 지정할 수 없습니다");
|
|
80
89
|
seenFlags.add("--nexus"); result.includeNexus = true; break;
|
package/src/cli/help.js
CHANGED
|
@@ -16,7 +16,8 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
16
16
|
--paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
|
|
17
17
|
--main-branch B 릴리스 브랜치 (기본: 감지된 default branch)
|
|
18
18
|
--develop-branch B 개발 브랜치 (기본: develop). 릴리스 브랜치와 같으면 trunk-based 모드
|
|
19
|
-
--
|
|
19
|
+
--deploy-style STYLE 서버 배포 방식: simple | nginx | traefik (기본: simple)
|
|
20
|
+
--nexus / --no-nexus 라이브러리 publish 워크플로우 포함/제외 (Nexus + GitHub Packages)
|
|
20
21
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
21
22
|
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
22
23
|
--force full 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
|
package/src/commands/dry-run.js
CHANGED
|
@@ -5,31 +5,22 @@ import { join } from "node:path";
|
|
|
5
5
|
import { PATHS } from "../core/paths.js";
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
7
|
import { planUninstall } from "./uninstall.js";
|
|
8
|
-
import {
|
|
8
|
+
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
9
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
10
|
-
import {
|
|
10
|
+
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
11
11
|
|
|
12
12
|
function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
13
|
-
const {
|
|
14
|
-
now, today, templateVersion = "unknown",
|
|
15
|
-
includeNexus = false, includeSecretBackup = false,
|
|
16
|
-
includeSemverAuto } = context;
|
|
13
|
+
const { paths = new Map() } = context;
|
|
17
14
|
const pathMarkers = new Map();
|
|
18
|
-
for (const [t] of paths) pathMarkers.set(t,
|
|
15
|
+
for (const [t, p] of paths) pathMarkers.set(t, existingMarkerInDir(t, join(targetRoot, p || ".")));
|
|
19
16
|
|
|
20
17
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
21
18
|
const existingRaw = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
22
19
|
const extraTopLevel = existingRaw !== null ? parseExisting(existingRaw).extraTopLevel : [];
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
extraTopLevel,
|
|
28
|
-
templateOptions: {
|
|
29
|
-
templateVersion, includeNexus, includeSecretBackup,
|
|
30
|
-
includeSemverAuto: includeSemverAuto !== false,
|
|
31
|
-
optionsDate: today,
|
|
32
|
-
},
|
|
21
|
+
// 실제 설치와 같은 렌더 함수를 쓴다 — 각자 조립하면 옵션이 늘 때마다 미리보기가 어긋난다.
|
|
22
|
+
const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
|
|
23
|
+
pathMarkers, extraTopLevel,
|
|
33
24
|
});
|
|
34
25
|
return { existed: existingRaw !== null, changed: existingRaw !== wouldBe };
|
|
35
26
|
}
|
package/src/commands/full.js
CHANGED
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
// gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다 — issue #7.
|
|
4
4
|
// (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { writeText } from "../core/fsutil.js";
|
|
8
8
|
import { PATHS } from "../core/paths.js";
|
|
9
|
-
import {
|
|
9
|
+
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
|
-
import {
|
|
11
|
+
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
12
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
13
13
|
import { copyWorkflows, computeBaselineEntries, makeSrcText } from "../core/copy/workflows.js";
|
|
14
14
|
import { copyScripts } from "../core/copy/simple.js";
|
|
15
15
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
16
16
|
import { readBaseline, writeBaseline } from "../core/baseline.js";
|
|
17
|
+
import { scanUnsubstituted, collectRequiredSecrets, narrowSecretsBySshAuth } from "../core/verify.js";
|
|
18
|
+
import { cleanupOtherDeployWorkflows, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
19
|
+
import { writeInstallLog } from "../core/install-log.js";
|
|
17
20
|
|
|
18
21
|
// context: { version, types, paths:Map, branch, versionCode, includeNexus, includeSecretBackup,
|
|
19
22
|
// force, repoName, resolvers, now, today }
|
|
@@ -24,9 +27,11 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
24
27
|
includeNexus = false, includeSecretBackup = false,
|
|
25
28
|
includeSemverAuto } = context;
|
|
26
29
|
|
|
27
|
-
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가
|
|
30
|
+
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가).
|
|
31
|
+
// 대표 마커명이 아니라 그 폴더에 실제로 있는 파일을 쓴다 — build.gradle.kts만 있는 레포의
|
|
32
|
+
// version.yml에 "# build.gradle"이라고 적히면 감지 로그와 같은 종류의 거짓말이 된다 (이슈 #77).
|
|
28
33
|
const pathMarkers = new Map();
|
|
29
|
-
for (const [t] of paths) pathMarkers.set(t,
|
|
34
|
+
for (const [t, p] of paths) pathMarkers.set(t, existingMarkerInDir(t, join(targetRoot, p || ".")));
|
|
30
35
|
|
|
31
36
|
// 1. 워크플로우 복사 (+ env 치환) — deploy 블록에 쓸 ask 값을 수집한다.
|
|
32
37
|
// hooks.decisions: 대화형 충돌 3지선 결정 Map (미지정=skip — 현행 force 동작)
|
|
@@ -39,12 +44,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
39
44
|
|
|
40
45
|
// 2. version.yml 생성 (payload/version.yml.template 렌더링 — 전체 재생성 전략 D4)
|
|
41
46
|
writeText(join(targetRoot, PATHS.versionFile),
|
|
42
|
-
|
|
43
|
-
templateText: readVersionYmlTemplate(payloadRoot),
|
|
44
|
-
version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
|
|
45
|
-
deployValues, extraTopLevel,
|
|
46
|
-
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
|
|
47
|
-
}));
|
|
47
|
+
renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel }));
|
|
48
48
|
|
|
49
49
|
// 3. README 버전 섹션
|
|
50
50
|
addVersionSectionToReadme(version, targetRoot);
|
|
@@ -54,10 +54,23 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
54
54
|
|
|
55
55
|
// 5. gitignore — 워크플로우 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
|
|
56
56
|
// 충돌 없는 설치(대부분의 최초 설치)는 .gitignore를 전혀 건드리지 않는다 — issue #7.
|
|
57
|
-
const
|
|
57
|
+
const gitignoreUpdated0 = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0;
|
|
58
|
+
|
|
59
|
+
// 6. 이전 배포 방식 정리 (이슈 #80) — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
|
|
60
|
+
// 옛 baseline이 살아 있는 지금이 "사용자가 손댔는가"를 판정할 수 있는 유일한 시점이다.
|
|
61
|
+
const previousBaseline = readBaseline(targetRoot);
|
|
62
|
+
const cleanup = cleanupOtherDeployWorkflows(
|
|
63
|
+
join(targetRoot, PATHS.workflowsDir),
|
|
64
|
+
existsSync(join(targetRoot, PATHS.workflowsDir)) ? readdirSync(join(targetRoot, PATHS.workflowsDir)) : [],
|
|
65
|
+
context.deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
66
|
+
previousBaseline);
|
|
67
|
+
// 지운 파일의 기준점은 baseline에서도 빼야 다음 실행에서 "사용자가 지웠다"로 오인하지 않는다.
|
|
68
|
+
for (const f of [...cleanup.removed, ...cleanup.backedUp]) delete previousBaseline?.files?.[f];
|
|
69
|
+
|
|
70
|
+
const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0;
|
|
58
71
|
if (gitignoreUpdated) ensureGitignore(targetRoot);
|
|
59
72
|
|
|
60
|
-
//
|
|
73
|
+
// 7. baseline 기록 (issue #69) — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
|
|
61
74
|
// env 치환까지 전부 끝난 뒤에 해시해야 디스크 내용이 최종형이다. 그래서 copyWorkflows 안이
|
|
62
75
|
// 아니라 여기서 기록한다.
|
|
63
76
|
// 기존 baseline은 병합 대상 — 이번에 건드리지 않은 파일의 기준점을 잃지 않는다.
|
|
@@ -67,9 +80,55 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
67
80
|
entries: computeBaselineEntries(
|
|
68
81
|
wfCounters.baselineTargets || new Map(),
|
|
69
82
|
join(targetRoot, PATHS.workflowsDir),
|
|
70
|
-
makeSrcText(context.branches || null)),
|
|
71
|
-
previous:
|
|
83
|
+
makeSrcText(context.branches || null, context.deployStyle || "")),
|
|
84
|
+
previous: previousBaseline,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 8. 설치 후 검증 (이슈 #81, #80) — 디스크에 실제로 쓰인 내용을 다시 읽어 확인한다.
|
|
88
|
+
// 미치환 플레이스홀더가 남았는지, 어떤 Secret이 있어야 워크플로우가 도는지.
|
|
89
|
+
// 설치를 실패시키지는 않는다 — 사실을 알려주는 것이 목적이고, 판단은 사용자 몫이다.
|
|
90
|
+
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
91
|
+
const managed = [...(wfCounters.baselineTargets || new Map()).keys()];
|
|
92
|
+
const unresolved = scanUnsubstituted(wfDir, managed);
|
|
93
|
+
const secrets = narrowSecretsBySshAuth(
|
|
94
|
+
collectRequiredSecrets(wfDir, managed),
|
|
95
|
+
firstDeployValue(deployValues, "SSH_AUTH_METHOD"),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// 9. 설치 로그 (이슈 #79) — 이 실행에서 무엇을 어떤 값으로 설치했는지 레포에 남긴다.
|
|
99
|
+
// 실패해도 설치는 성공으로 끝난다.
|
|
100
|
+
const installLog = writeInstallLog(targetRoot, {
|
|
101
|
+
action: context.previousTemplateVersion ? "update" : "install",
|
|
102
|
+
at: now || today || "",
|
|
103
|
+
templateVersion,
|
|
104
|
+
previousTemplateVersion: context.previousTemplateVersion || "",
|
|
105
|
+
mode: context.mode || "full",
|
|
106
|
+
types, markers: context.markers || new Map(), version,
|
|
107
|
+
versionSource: context.versionSource || "",
|
|
108
|
+
branch, branches: context.branches, paths,
|
|
109
|
+
options: {
|
|
110
|
+
nexus: includeNexus,
|
|
111
|
+
secretBackup: includeSecretBackup,
|
|
112
|
+
semverAuto: includeSemverAuto !== false,
|
|
113
|
+
deployStyle: context.deployStyle || "",
|
|
114
|
+
},
|
|
115
|
+
answers: context.envAnswers || [],
|
|
116
|
+
warnings: context.detectWarnings || [],
|
|
117
|
+
result: {
|
|
118
|
+
copiedFiles: wfCounters.copiedFiles || [],
|
|
119
|
+
gitignoreUpdated,
|
|
120
|
+
},
|
|
121
|
+
unresolved, secrets, cleanup,
|
|
72
122
|
});
|
|
73
123
|
|
|
74
|
-
return { workflows: wfCounters, gitignoreUpdated };
|
|
124
|
+
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, installLog, cleanup };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.
|
|
128
|
+
function firstDeployValue(deployValues, key) {
|
|
129
|
+
for (const [, asks] of deployValues) {
|
|
130
|
+
const v = asks.get(key);
|
|
131
|
+
if (v) return v;
|
|
132
|
+
}
|
|
133
|
+
return "";
|
|
75
134
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
|
|
8
|
-
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber } from "../core/detect-fs.js";
|
|
8
|
+
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber, detectMarkers } from "../core/detect-fs.js";
|
|
9
9
|
import { parseExisting } from "../core/version-yml.js";
|
|
10
10
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
11
11
|
import { resolveProjectPaths } from "../core/paths-resolve.js";
|
|
@@ -14,6 +14,7 @@ import { askAllOptionalWorkflows } from "../core/options-ask.js";
|
|
|
14
14
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
15
15
|
import { surveyWorkflows } from "../core/copy/workflows.js";
|
|
16
16
|
import { createContext, VALID_TYPES } from "../context.js";
|
|
17
|
+
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
17
18
|
import { runFull } from "./full.js";
|
|
18
19
|
import { runUninstallFlow } from "./uninstall.js";
|
|
19
20
|
import * as prompts from "../ui/prompts.js";
|
|
@@ -68,19 +69,43 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
68
69
|
if (!proceed) { io.cancelMessage?.("통합을 안전하게 취소했습니다."); return 0; }
|
|
69
70
|
|
|
70
71
|
// full/version/workflows — 감지 (version은 기존 version.yml 최우선)
|
|
72
|
+
// 감지 경고는 즉시 찍지 않고 모았다가 감지 박스 안에서 출력한다 — 종전에는 경고가 박스보다
|
|
73
|
+
// 먼저 나와 앞선 질문에 대한 경고처럼 보였다 (이슈 #80). 안내 문구도 대화형용으로 바꾼다.
|
|
74
|
+
const detectWarnings = [];
|
|
71
75
|
let types = detectTypes(cwd);
|
|
72
|
-
let version = (existing?.version) || detectVersion(cwd
|
|
76
|
+
let version = (existing?.version) || detectVersion(cwd, {
|
|
77
|
+
warn: (m) => detectWarnings.push(m),
|
|
78
|
+
hint: "다음 화면의 '수정하기 > 버전'에서 바로 고칠 수 있습니다.",
|
|
79
|
+
});
|
|
73
80
|
let branch = detectDefaultBranch(cwd);
|
|
74
81
|
const repoName = detectRepoName(cwd);
|
|
75
82
|
// 선택 워크플로우 초기값: version.yml 저장 옵션 (.sh read_template_options L2361 등가)
|
|
76
83
|
let includeNexus = existing?.options?.nexus ?? false;
|
|
77
84
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
78
85
|
let includeSemverAuto = existing?.options?.semverAuto ?? null;
|
|
86
|
+
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (nexus/secret_backup과 같은 규약).
|
|
87
|
+
let deployStyle = isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
79
88
|
const showOptional = mode === "full";
|
|
80
89
|
const realTty = process.stdout.isTTY === true;
|
|
81
90
|
|
|
82
|
-
// 층2 — 감지 로그 (#446)
|
|
83
|
-
|
|
91
|
+
// 층2 — 감지 로그 (#446). markers = 실제로 존재를 확인한 파일 (이슈 #77).
|
|
92
|
+
let markers = detectMarkers(cwd, types);
|
|
93
|
+
io.detectionLog?.({ types, version, branch, markers, warnings: detectWarnings });
|
|
94
|
+
|
|
95
|
+
// 타입 확정 (이슈 #78) — 감지는 추정이므로 다른 질문보다 먼저 확인받는다. 종전에는 확정 UI가
|
|
96
|
+
// '수정하기 > 프로젝트 타입' 두 단계 뒤에 숨어 있어, 타입이 틀린 채로 설치가 끝나는 일이 많았다.
|
|
97
|
+
// 타입이 뒤에 나올 질문(선택 워크플로우·경로·env)의 범위를 정하므로 순서상 여기가 맞다.
|
|
98
|
+
// 저장값이 있는 업데이트 설치와 비대화형에서는 묻지 않는다 — 기존 동작 그대로.
|
|
99
|
+
if (showOptional && !existing?.types?.length) {
|
|
100
|
+
const picked = await io.confirmTypes({ types, markers });
|
|
101
|
+
if (!isCancel(picked) && Array.isArray(picked) && picked.length) {
|
|
102
|
+
const next = picked.filter((x) => VALID_TYPES.includes(x));
|
|
103
|
+
if (next.length) {
|
|
104
|
+
types = next;
|
|
105
|
+
markers = detectMarkers(cwd, types);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
84
109
|
|
|
85
110
|
// 선택 워크플로우(Nexus/Secret) 질문 (.sh ask_all_optional_workflows L2707 — full/workflows만)
|
|
86
111
|
if (showOptional) {
|
|
@@ -93,6 +118,12 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
93
118
|
includeNexus = r.nexus;
|
|
94
119
|
includeSecretBackup = r.secretBackup;
|
|
95
120
|
|
|
121
|
+
// 서버 배포 방식 (이슈 #80). Nexus(라이브러리)를 고르면 server-deploy가 통째로 빠지므로 묻지 않는다.
|
|
122
|
+
if (!includeNexus && !isDeployStyle(deployStyle)) {
|
|
123
|
+
const picked = await io.selectDeployStyle();
|
|
124
|
+
deployStyle = isDeployStyle(picked) ? picked : DEFAULT_DEPLOY_STYLE; // ESC = 기본값
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
// 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
|
|
97
128
|
// version.yml을 쓰지 않는 workflows 모드에서는 답변이 무의미하므로 full에서만 질문한다.
|
|
98
129
|
if (mode === "full" && includeSemverAuto === null) {
|
|
@@ -187,14 +218,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
187
218
|
|
|
188
219
|
// @wizard env 계획 질문 (.sh wf_prompt_env_plan L3220 — full/workflows만)
|
|
189
220
|
const resolvers = makeResolvers(cwd, repoName, paths);
|
|
190
|
-
let envValues = new Map(), envUseDefaults = true;
|
|
221
|
+
let envValues = new Map(), envUseDefaults = true, envAnswers = [];
|
|
191
222
|
if (showOptional) {
|
|
192
223
|
const plan = await promptEnvPlan({
|
|
193
224
|
payloadRoot: payload, types, io: io.engineIo ?? null, force: false,
|
|
194
|
-
resolvers, includeNexus, targetRoot: cwd, repoName,
|
|
225
|
+
resolvers, includeNexus, includeSecretBackup, deployStyle, targetRoot: cwd, repoName,
|
|
195
226
|
});
|
|
196
227
|
envValues = plan.values;
|
|
197
228
|
envUseDefaults = plan.useDefaults;
|
|
229
|
+
envAnswers = plan.answers || []; // 완료 요약·설치 로그가 같은 답변 데이터를 쓴다 (#79, #80)
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
const { now, today } = clock || utcNow();
|
|
@@ -203,6 +235,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
203
235
|
includeNexus, includeSecretBackup,
|
|
204
236
|
includeSemverAuto,
|
|
205
237
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
238
|
+
// 설치 로그(#79)·완료 요약(#80)이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
239
|
+
markers, envAnswers, detectWarnings,
|
|
240
|
+
deployStyle: deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
241
|
+
previousTemplateVersion: existing?.templateVersion || "",
|
|
206
242
|
});
|
|
207
243
|
ctx.templateVersion = templateVersion;
|
|
208
244
|
|
|
@@ -258,6 +294,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
258
294
|
mode, types, version, versionCode, branches,
|
|
259
295
|
copiedFiles: result?.workflows?.copiedFiles ?? [],
|
|
260
296
|
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
297
|
+
answers: envAnswers,
|
|
298
|
+
unresolved: result?.unresolved ?? [],
|
|
299
|
+
secrets: result?.secrets ?? new Map(),
|
|
300
|
+
installLogPath: result?.installLog?.path ?? "",
|
|
301
|
+
cleanup: result?.cleanup ?? null,
|
|
261
302
|
});
|
|
262
303
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
263
304
|
return 0;
|
|
@@ -44,7 +44,9 @@ export function runUninstall(context, payloadRoot, targetRoot, selection) {
|
|
|
44
44
|
|
|
45
45
|
// ── 대화형 체크리스트 흐름 ────────────────────────────────────────────
|
|
46
46
|
const ITEM_DEFS = [
|
|
47
|
-
|
|
47
|
+
// .github/.wizard 에는 baseline.json과 설치 로그(.wizard/logs)가 함께 들어 있다 —
|
|
48
|
+
// 워크플로우를 지우면 함께 사라지므로 라벨에 명시한다 (이슈 #79).
|
|
49
|
+
{ key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml) + 설치 기록 (.github/.wizard)" },
|
|
48
50
|
{ key: "scripts", label: "스크립트 (.github/scripts/*.py)" },
|
|
49
51
|
{ key: "readme", label: "README.md 버전 섹션 (AUTO-VERSION-SECTION)" },
|
|
50
52
|
{ key: "gitignore", label: ".gitignore 자동 추가 항목" },
|
package/src/core/copy/simple.js
CHANGED
|
@@ -6,9 +6,9 @@ import { chmodSync } from "node:fs";
|
|
|
6
6
|
import { PATHS, PAYLOAD } from "../paths.js";
|
|
7
7
|
import { exists, copyFileSync } from "../fsutil.js";
|
|
8
8
|
|
|
9
|
-
// version_manager.py, changelog_manager.py 무조건 덮어쓰기 (+chmod — Windows에선 무의미하나 무해).
|
|
9
|
+
// version_manager.py, changelog_manager.py, truncate_release_notes.py 무조건 덮어쓰기 (+chmod — Windows에선 무의미하나 무해).
|
|
10
10
|
export function copyScripts(payloadRoot, targetRoot = ".") {
|
|
11
|
-
const scripts = ["version_manager.py", "changelog_manager.py"];
|
|
11
|
+
const scripts = ["version_manager.py", "changelog_manager.py", "truncate_release_notes.py", "issue_helper.py"];
|
|
12
12
|
let copied = 0;
|
|
13
13
|
for (const s of scripts) {
|
|
14
14
|
const src = join(payloadRoot, PAYLOAD.scriptsDir, s);
|