projectops 4.36.4 → 4.38.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 +17 -16
- package/package.json +1 -1
- package/src/cli/args.js +10 -5
- package/src/cli/help.js +8 -2
- package/src/commands/doctor.js +16 -0
- package/src/commands/full.js +6 -3
- package/src/commands/interactive.js +9 -4
- package/src/commands/version.js +6 -3
- package/src/commands/workflows.js +2 -0
- package/src/context.js +2 -1
- package/src/core/agent-guide.js +44 -0
- package/src/core/constants.js +28 -0
- package/src/core/copy/simple.js +3 -2
- package/src/core/copy/workflows.js +85 -7
- package/src/core/exclusions.js +2 -0
- package/src/core/fsutil.js +2 -2
- package/src/core/migrations/index.js +17 -2
- package/src/core/options-ask.js +5 -4
- package/src/core/options-schema.js +150 -0
- package/src/core/version-yml.js +135 -24
- package/src/core/wizard-env.js +8 -0
- package/src/i18n/doctor.en.js +4 -0
- package/src/i18n/doctor.ko.js +4 -0
- package/src/i18n/migrate.en.js +3 -0
- package/src/i18n/migrate.ko.js +3 -0
- package/src/i18n/summary.en.js +10 -0
- package/src/i18n/summary.ko.js +10 -0
- package/src/index.js +18 -6
- package/src/ui/summary.js +32 -0
package/src/core/version-yml.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
// version.yml 파싱·생성 (.sh create_version_yml 등가, 전체 재생성 전략 D4).
|
|
2
2
|
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터. .sh heredoc과 바이트 동일한 템플릿 문자열.
|
|
3
3
|
// 실측 기준: template_integrator.sh 2184~2354.
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { DEPLOY_TARGETS, LABEL_STYLES } from "./constants.js";
|
|
6
|
+
import { REPO_LANGUAGES } from "./repo-language.js";
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
// 값 목록으로 만든 매칭 패턴 — 목록에 언어·표기를 추가하면 파서가 저절로 읽는다.
|
|
9
|
+
// (예전엔 `(en|ko)` 가 박혀 있어 세 번째 언어를 저장하면 다음 업데이트에서 못 읽고 ko 로 되돌아갔다.)
|
|
10
|
+
const alt = (values) => values.map((v) => v.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")).join("|");
|
|
11
|
+
const LANGUAGE_RE = new RegExp(`^\\s+language:\\s*["']?(${alt(REPO_LANGUAGES)})["']?`);
|
|
12
|
+
const LABEL_STYLE_RE = new RegExp(`^\\s+label_style:\\s*["']?(${alt(LABEL_STYLES)})["']?`);
|
|
13
|
+
|
|
14
|
+
const HEADER_EN = `# ===================================================================
|
|
6
15
|
# Project version management file
|
|
7
16
|
# ===================================================================
|
|
8
17
|
#
|
|
@@ -18,11 +27,11 @@ const HEADER = `# ==============================================================
|
|
|
18
27
|
# Automatic version updates:
|
|
19
28
|
# - version_code: incremented by 1 on every build (independent of the rules below)
|
|
20
29
|
# - How far the version is bumped is decided by metadata.template.options.semver_auto
|
|
21
|
-
# semver_auto: true -> decided by the commit titles in the release range
|
|
30
|
+
# semver_auto: true -> decided by the commit titles in the release range (also when the key is missing)
|
|
22
31
|
# "title : feat! : body" or "feat!:" -> major (x+1.0.0)
|
|
23
32
|
# "title : feat : body" or "feat:" -> minor (x.y+1.0)
|
|
24
33
|
# anything else (fix/docs/chore/refactor/test) -> patch (x.y.z+1)
|
|
25
|
-
#
|
|
34
|
+
# semver_auto: false -> always patch +1
|
|
26
35
|
# - You may edit the version value by hand regardless of the rules above
|
|
27
36
|
#
|
|
28
37
|
# Files synced per project type:
|
|
@@ -45,6 +54,98 @@ const HEADER = `# ==============================================================
|
|
|
45
54
|
# ===================================================================
|
|
46
55
|
`;
|
|
47
56
|
|
|
57
|
+
// 한국어 레포(options.language: ko)는 주석도 한국어로 쓴다 (#811).
|
|
58
|
+
// 업데이트(--force 는 화면이 en 고정)마다 한국어 주석이 영문으로 바뀌어 version.yml diff 가 86줄이 됐다.
|
|
59
|
+
const HEADER_KO = `# ===================================================================
|
|
60
|
+
# 프로젝트 버전 관리 파일
|
|
61
|
+
# ===================================================================
|
|
62
|
+
#
|
|
63
|
+
# 이 파일은 다양한 프로젝트 타입에서 버전 정보를 중앙 관리하기 위한 파일입니다.
|
|
64
|
+
# GitHub Actions 워크플로우가 이 파일을 읽어 자동으로 버전을 관리합니다.
|
|
65
|
+
#
|
|
66
|
+
# 사용법:
|
|
67
|
+
# 1. version: "1.0.0" - 사용자에게 표시되는 버전
|
|
68
|
+
# 2. version_code: 1 - Play Store/App Store 빌드 번호 (1부터 자동 증가)
|
|
69
|
+
# 3. project_types: 프로젝트 타입 배열 — 첫 항목이 primary
|
|
70
|
+
# 4. project_paths: 타입별 프로젝트 폴더 (레포 루트 기준 상대경로, 모노레포용)
|
|
71
|
+
#
|
|
72
|
+
# 자동 버전 업데이트:
|
|
73
|
+
# - version_code: 매 빌드마다 자동으로 1씩 증가 (아래 판정과 무관)
|
|
74
|
+
# - 버전 승격 폭은 metadata.template.options.semver_auto 가 정합니다
|
|
75
|
+
# semver_auto: true -> 릴리스 구간 커밋 제목으로 판정 (키가 없어도 같다)
|
|
76
|
+
# "제목 : feat! : 내용" 또는 "feat!:" -> major (x+1.0.0)
|
|
77
|
+
# "제목 : feat : 내용" 또는 "feat:" -> minor (x.y+1.0)
|
|
78
|
+
# 그 외(fix/docs/chore/refactor/test) -> patch (x.y.z+1)
|
|
79
|
+
# semver_auto: false -> 항상 patch +1
|
|
80
|
+
# - 위 판정과 무관하게 version 값을 직접 수정해도 됩니다
|
|
81
|
+
#
|
|
82
|
+
# 프로젝트 타입별 동기화 파일:
|
|
83
|
+
# - spring: build.gradle (version = "x.y.z")
|
|
84
|
+
# - flutter: pubspec.yaml (version: x.y.z+i, buildNumber 포함)
|
|
85
|
+
# - react/node: package.json ("version": "x.y.z")
|
|
86
|
+
# - react-native: iOS Info.plist 또는 Android build.gradle
|
|
87
|
+
# - react-native-expo: app.json (expo.version)
|
|
88
|
+
# - python: pyproject.toml (version = "x.y.z")
|
|
89
|
+
# - basic/기타: version.yml 파일만 사용
|
|
90
|
+
#
|
|
91
|
+
# 연관된 워크플로우:
|
|
92
|
+
# - .github/workflows/PROJECT-VERSION-CONTROL.yaml
|
|
93
|
+
# - .github/workflows/PROJECT-README-VERSION-UPDATE.yaml
|
|
94
|
+
# - .github/workflows/PROJECT-RELEASE-CHANGELOG.yaml
|
|
95
|
+
#
|
|
96
|
+
# 주의사항:
|
|
97
|
+
# - project_types는 최초 설정 후 변경하지 마세요
|
|
98
|
+
# - 버전은 항상 높은 버전으로 자동 동기화됩니다
|
|
99
|
+
# ===================================================================
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
// 키 옆 한 줄 주석. 언어는 화면 언어가 아니라 레포 문구 언어(options.language)를 따른다.
|
|
103
|
+
const COMMENTS = {
|
|
104
|
+
en: {
|
|
105
|
+
versionCode: "app build number",
|
|
106
|
+
projectTypes: "array of types, first entry is primary, safe to edit by hand",
|
|
107
|
+
projectPaths: "project folder per type (relative to the repo root)",
|
|
108
|
+
mode: "install scope (full/version/workflows), used when re-running update mode",
|
|
109
|
+
intent: "project kind (app/library/both/none/manual), drives the deploy questions",
|
|
110
|
+
semverAuto: "decide the version bump from commit titles (false means always patch)",
|
|
111
|
+
projectsSync: "include the GitHub Projects status sync workflow",
|
|
112
|
+
closeOnRelease: "close issues labelled done when a release is merged (false or missing means do not close)",
|
|
113
|
+
language: "issue/PR template language (en | ko)",
|
|
114
|
+
labelStyle: "status label style (en: status: todo / ko: Korean names)",
|
|
115
|
+
appRelease: "whether releases go through App Store / Play Store review",
|
|
116
|
+
deploy: "deploy settings remembered by the wizard (non-sensitive, safe to edit by hand)",
|
|
117
|
+
excluded: "workflow files the update must never install (safe to edit by hand)",
|
|
118
|
+
},
|
|
119
|
+
ko: {
|
|
120
|
+
versionCode: "app build number",
|
|
121
|
+
projectTypes: "멀티타입 배열 — 첫 항목이 primary, 직접 편집 가능",
|
|
122
|
+
projectPaths: "타입별 프로젝트 폴더 (레포 루트 기준 상대경로)",
|
|
123
|
+
mode: "통합 범위(full/version/workflows) — 업데이트 모드 재실행 기준",
|
|
124
|
+
intent: "프로젝트 성격(app/library/both/none/manual) — 배포 질문 유도 기준",
|
|
125
|
+
semverAuto: "커밋 제목으로 버전 승격 폭 결정 (false면 항상 patch)",
|
|
126
|
+
projectsSync: "GitHub Projects 상태 동기화 워크플로우 포함 여부",
|
|
127
|
+
closeOnRelease: "릴리스 머지 때 완료 라벨 이슈를 닫는다 (false/미기재면 닫지 않음)",
|
|
128
|
+
language: "이슈/PR 템플릿 언어 (en | ko)",
|
|
129
|
+
labelStyle: "상태 라벨 표기 (en: status: todo / ko: 작업전)",
|
|
130
|
+
appRelease: "앱스토어·플레이스토어 심사로 이어지는 배포인가",
|
|
131
|
+
deploy: "마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)",
|
|
132
|
+
excluded: "업데이트가 절대 설치하지 않을 워크플로우 파일 (직접 수정 가능)",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const commentsFor = (lang) => COMMENTS[lang === "ko" ? "ko" : "en"];
|
|
136
|
+
|
|
137
|
+
// last_updated_by 결정 (#811): 실행한 git 사용자 → 기존 값 → template_integrator.
|
|
138
|
+
// 예전엔 업데이트마다 "template_integrator"로 덮어써 누가 바꿨는지가 사라졌다.
|
|
139
|
+
export function resolveUpdatedBy(existingContent = "", cwd = ".") {
|
|
140
|
+
try {
|
|
141
|
+
const name = execFileSync("git", ["config", "user.name"],
|
|
142
|
+
{ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
143
|
+
if (name) return name;
|
|
144
|
+
} catch { /* git 없음·설정 없음(CI) — 기존 값으로 */ }
|
|
145
|
+
const m = String(existingContent || "").match(/^\s+last_updated_by:\s*"([^"]*)"/m);
|
|
146
|
+
return m && m[1] ? m[1] : "template_integrator";
|
|
147
|
+
}
|
|
148
|
+
|
|
48
149
|
// metadata.template.options 상태머신 파싱 (.sh read_template_options L2361~2416 등가).
|
|
49
150
|
// 반환(#439 배포/publish 축): { deploy: string|null, publish: string[]|null, secretBackup: bool|null }
|
|
50
151
|
// deploy: 'docker-ssh'|'vercel'|'none', publish: ['nexus','npm','github-packages'] 부분집합. null=미기재.
|
|
@@ -57,7 +158,7 @@ const HEADER = `# ==============================================================
|
|
|
57
158
|
export function parseTemplateOptions(content) {
|
|
58
159
|
const out = { deploy: null, publish: null, secretBackup: null,
|
|
59
160
|
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null, aiPrSummary: null, projectsSync: null,
|
|
60
|
-
deployBranch: null, intent: null, semverAuto: null, appRelease: null, labelStyle: null, closeOnRelease: null, language: null };
|
|
161
|
+
deployBranch: null, intent: null, semverAuto: null, appRelease: null, labelStyle: null, closeOnRelease: null, language: null, excludedWorkflows: null };
|
|
61
162
|
// deploy_branch는 metadata 직속(#456) — template.options 밖이라 별도로 스캔한다.
|
|
62
163
|
for (const line of String(content || "").split("\n")) {
|
|
63
164
|
if (line.startsWith("#")) continue;
|
|
@@ -105,7 +206,7 @@ export function parseTemplateOptions(content) {
|
|
|
105
206
|
let m = line.match(/^\s+deploy:\s*(.+)/);
|
|
106
207
|
if (m) {
|
|
107
208
|
const v = strip(m[1]);
|
|
108
|
-
if (
|
|
209
|
+
if (DEPLOY_TARGETS.includes(v)) out.deploy = v;
|
|
109
210
|
continue;
|
|
110
211
|
}
|
|
111
212
|
m = line.match(/^\s+publish:\s*\[([^\]]*)\]/);
|
|
@@ -127,7 +228,7 @@ export function parseTemplateOptions(content) {
|
|
|
127
228
|
if (v === "false") out.secretBackup = false;
|
|
128
229
|
continue;
|
|
129
230
|
}
|
|
130
|
-
// semver 자동 승격(#546). null(미기재)
|
|
231
|
+
// semver 자동 승격(#546). null(미기재)이면 호출부가 ON(true)으로 해석한다 (#814). 명시적 false 만 끈다.
|
|
131
232
|
// 캡처를 true|false로 한정한다(intent와 같은 방식) — 값 뒤 인라인 주석이 값에 딸려오면
|
|
132
233
|
// strip()은 따옴표·공백만 걷어내므로 "true # ..."가 되어 매칭이 조용히 실패한다.
|
|
133
234
|
m = line.match(/^\s+semver_auto:\s*["']?(true|false)["']?/);
|
|
@@ -136,13 +237,13 @@ export function parseTemplateOptions(content) {
|
|
|
136
237
|
continue;
|
|
137
238
|
}
|
|
138
239
|
// 레포 문구 언어(#769) — en | ko. 미기재는 "기존 통합 레포" 신호라 호출부가 ko 로 해석한다.
|
|
139
|
-
m = line.match(
|
|
240
|
+
m = line.match(LANGUAGE_RE);
|
|
140
241
|
if (m) {
|
|
141
242
|
out.language = m[1];
|
|
142
243
|
continue;
|
|
143
244
|
}
|
|
144
245
|
// 상태 라벨 표기(#776) — en(영문 표준) | ko(기존 한글). 미기재는 "기존 통합 레포" 신호라 호출부가 ko로 해석한다.
|
|
145
|
-
m = line.match(
|
|
246
|
+
m = line.match(LABEL_STYLE_RE);
|
|
146
247
|
if (m) {
|
|
147
248
|
out.labelStyle = m[1];
|
|
148
249
|
continue;
|
|
@@ -159,6 +260,12 @@ export function parseTemplateOptions(content) {
|
|
|
159
260
|
out.closeOnRelease = m[1] === "true";
|
|
160
261
|
continue;
|
|
161
262
|
}
|
|
263
|
+
// 설치하지 않을 워크플로우 (#810) — 인라인 배열. 업데이트가 지운 파일을 되살리지 않게 하는 명시 수단이다.
|
|
264
|
+
m = line.match(/^\s+excluded_workflows:\s*\[([^\]]*)\]/);
|
|
265
|
+
if (m) {
|
|
266
|
+
out.excludedWorkflows = m[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
162
269
|
// 프로젝트 성격(#553) — 앱 심사로 이어지는 레포인가. 워크플로우와 스킬이 같은 값을 본다.
|
|
163
270
|
m = line.match(/^\s+app_release:\s*["']?(true|false)["']?/);
|
|
164
271
|
if (m) {
|
|
@@ -329,17 +436,19 @@ export function mergeDeployValues(existingContent, fresh) {
|
|
|
329
436
|
// today = "YYYY-MM-DD" (UTC)
|
|
330
437
|
// pathMarkers = Map<type, markerFilename> (project_paths 주석용)
|
|
331
438
|
// templateOptions = { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate } (template 블록)
|
|
332
|
-
export function buildVersionYml({ version, types = [], paths = new Map(), pathMarkers = new Map(), branch = "main", deployBranch = "", versionCode = 1, now, today, templateOptions = null, deployValues = new Map() }) {
|
|
439
|
+
export function buildVersionYml({ version, types = [], paths = new Map(), pathMarkers = new Map(), branch = "main", deployBranch = "", versionCode = 1, now, today, templateOptions = null, deployValues = new Map(), updatedBy = "template_integrator" }) {
|
|
440
|
+
const lang = templateOptions?.language === "ko" ? "ko" : "en";
|
|
441
|
+
const C = commentsFor(lang);
|
|
333
442
|
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(",")}]` : `["basic"]`;
|
|
334
443
|
|
|
335
|
-
let out =
|
|
444
|
+
let out = (lang === "ko" ? HEADER_KO : HEADER_EN) + "\n";
|
|
336
445
|
out += `version: "${version}"\n`;
|
|
337
|
-
out += `version_code: ${versionCode}
|
|
338
|
-
out += `project_types: ${typesJson} #
|
|
446
|
+
out += `version_code: ${versionCode} # ${C.versionCode}\n`; // 공백 1칸 — version_manager.py 가 쓰는 형태와 같아야 업데이트 diff 가 안 생긴다
|
|
447
|
+
out += `project_types: ${typesJson} # ${C.projectTypes}\n`;
|
|
339
448
|
|
|
340
449
|
// project_paths 블록. pathMarkers: Map<type, markerFilename> (있으면 " type: "path" # path/marker" 주석).
|
|
341
450
|
if (paths.size) {
|
|
342
|
-
out += `project_paths: #
|
|
451
|
+
out += `project_paths: # ${C.projectPaths}\n`;
|
|
343
452
|
for (const [t, p] of paths) {
|
|
344
453
|
const marker = pathMarkers.get(t) || "";
|
|
345
454
|
const pf = p === "." ? marker : (marker ? `${p}/${marker}` : p);
|
|
@@ -349,7 +458,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
349
458
|
|
|
350
459
|
out += `metadata:\n`;
|
|
351
460
|
out += ` last_updated: "${now}"\n`;
|
|
352
|
-
out += ` last_updated_by: "template_integrator"\n`;
|
|
461
|
+
out += ` last_updated_by: "${String(updatedBy || "template_integrator").replace(/"/g, "")}"\n`;
|
|
353
462
|
out += ` default_branch: "${branch}"\n`;
|
|
354
463
|
// deploy_branch: 릴리스 PR의 head 브랜치(#456). default_branch(레포 기본)와 별개 개념.
|
|
355
464
|
// 지정된 경우에만 출력 — 미지정이면 스킬이 develop로 폴백(하위호환).
|
|
@@ -361,7 +470,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
361
470
|
if (templateOptions) {
|
|
362
471
|
const { templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, optionsDate = today,
|
|
363
472
|
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true, aiPrSummary = true, intent = null, mode = null,
|
|
364
|
-
semverAuto = true, appRelease = null, labelStyle = null, closeOnRelease = null, projectsSync = null, language = null } = templateOptions;
|
|
473
|
+
semverAuto = true, appRelease = null, labelStyle = null, closeOnRelease = null, projectsSync = null, language = null, excludedWorkflows = null } = templateOptions;
|
|
365
474
|
const publishJson = `[${publishTargets.map((t) => `"${t}"`).join(",")}]`;
|
|
366
475
|
// intent(프로젝트 성격, #485) — 미지정이면 deploy/publish에서 역추론해 기록 (재통합 시 진입 질문 생략용)
|
|
367
476
|
const intentVal = intent || inferIntent(deployTarget, publishTargets) || "manual";
|
|
@@ -369,27 +478,29 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
369
478
|
out += ` source: "projectops"\n`;
|
|
370
479
|
out += ` version: "${templateVersion}"\n`;
|
|
371
480
|
// mode(#502) — 이 레포에 통합된 범위. 업데이트 모드가 같은 범위를 재실행하는 기준.
|
|
372
|
-
if (mode) out += ` mode: "${mode}" #
|
|
481
|
+
if (mode) out += ` mode: "${mode}" # ${C.mode}\n`;
|
|
373
482
|
out += ` integrated_date: "${optionsDate}"\n`;
|
|
374
483
|
out += ` last_update_date: "${optionsDate}"\n`;
|
|
375
484
|
out += ` options:\n`;
|
|
376
|
-
out += ` intent: "${intentVal}" #
|
|
485
|
+
out += ` intent: "${intentVal}" # ${C.intent}\n`;
|
|
377
486
|
out += ` deploy: "${deployTarget}"\n`;
|
|
378
487
|
out += ` publish: ${publishJson}\n`;
|
|
379
488
|
out += ` secret_backup: ${includeSecretBackup}\n`;
|
|
380
489
|
// semver 자동 승격(#546) — 릴리스 시 커밋 제목으로 major/minor/patch 결정. false면 항상 patch.
|
|
381
|
-
out += ` semver_auto: ${semverAuto} #
|
|
490
|
+
out += ` semver_auto: ${semverAuto} # ${C.semverAuto}\n`;
|
|
382
491
|
// Projects 보드 동기화(#716) — 미지정이면 키를 쓰지 않는다.
|
|
383
|
-
if (projectsSync !== null) out += ` projects_sync: ${projectsSync} #
|
|
492
|
+
if (projectsSync !== null) out += ` projects_sync: ${projectsSync} # ${C.projectsSync}\n`;
|
|
384
493
|
// 릴리스 시 완료 이슈 닫기(#771) — 미지정이면 키를 쓰지 않는다(기존 레포는 현행 유지).
|
|
385
|
-
if (closeOnRelease !== null) out += ` close_on_release: ${closeOnRelease} #
|
|
494
|
+
if (closeOnRelease !== null) out += ` close_on_release: ${closeOnRelease} # ${C.closeOnRelease}\n`;
|
|
386
495
|
// 상태 라벨 표기(#776) — 미지정이면 키를 쓰지 않는다(기존 레포 무변화).
|
|
387
496
|
// 레포 문구 언어(#769) — 미지정이면 키를 쓰지 않는다(기존 레포 무변화).
|
|
388
|
-
|
|
389
|
-
if (
|
|
497
|
+
// 설치 제외 목록(#810) — 전체 재생성이라 다시 쓰지 않으면 사용자가 적은 목록이 사라진다.
|
|
498
|
+
if (excludedWorkflows && excludedWorkflows.length) out += ` excluded_workflows: [${excludedWorkflows.map((f) => `"${f}"`).join(", ")}] # ${C.excluded}\n`;
|
|
499
|
+
if (language) out += ` language: ${language} # ${C.language}\n`;
|
|
500
|
+
if (labelStyle) out += ` label_style: ${labelStyle} # ${C.labelStyle}\n`;
|
|
390
501
|
// 앱 심사 배포 레포 여부(#553) — 미지정이면 키를 쓰지 않는다(기존 레포 무변화).
|
|
391
502
|
if (appRelease !== null) {
|
|
392
|
-
out += ` app_release: ${appRelease} #
|
|
503
|
+
out += ` app_release: ${appRelease} # ${C.appRelease}\n`;
|
|
393
504
|
}
|
|
394
505
|
out += ` code_review:\n`;
|
|
395
506
|
out += ` coderabbit: ${codeReviewCoderabbit}\n`;
|
|
@@ -405,7 +516,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
405
516
|
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
|
|
406
517
|
if (deployTypes.length) {
|
|
407
518
|
out += `\n`;
|
|
408
|
-
out += `deploy: # deploy
|
|
519
|
+
out += `deploy: # ${C.deploy}\n`;
|
|
409
520
|
for (const t of deployTypes) {
|
|
410
521
|
out += ` ${t}:\n`;
|
|
411
522
|
for (const [k, v] of deployValues.get(t)) out += ` ${k}: "${v}"\n`;
|
package/src/core/wizard-env.js
CHANGED
|
@@ -47,6 +47,13 @@ export function extractEnvValues(installedContent) {
|
|
|
47
47
|
return values;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// 예시용 기본값 (#810) — 그대로 배포되면 남의 도메인으로 요청이 간다.
|
|
51
|
+
// 사용자가 값을 고르지 않은 경로(--force·기본값 사용)에서는 채우지 않고 __KEY__ 를 남긴다.
|
|
52
|
+
// 그러면 설치 후 검증(verify.js)이 미치환으로 잡아 완료 화면에 "직접 채울 것"으로 보여준다.
|
|
53
|
+
export function isPlaceholderDefault(value) {
|
|
54
|
+
return /(^|\.)example\.(com|org|net)$/i.test(String(value || "").trim());
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
// resolver — .sh resolve_token 등가. 값 계산은 주입된 resolvers로 위임(순수성 유지).
|
|
51
58
|
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root" }
|
|
52
59
|
export function resolveToken(name, type, resolvers = {}) {
|
|
@@ -89,6 +96,7 @@ export function substituteEnv(content, opts = {}) {
|
|
|
89
96
|
let def = p.arg.startsWith("@") ? resolveToken(p.arg.slice(1), type, resolvers) : p.arg;
|
|
90
97
|
const chosen = values.get(p.key);
|
|
91
98
|
if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
|
|
99
|
+
else if (isPlaceholderDefault(def)) continue; // 예시값은 쓰지 않는다 — 토큰·마커 그대로 (#810)
|
|
92
100
|
else val = def;
|
|
93
101
|
// ask 키만 수집 (.sh wf_deploy_set — auto는 저장 안 함). deploy 블록용.
|
|
94
102
|
// #489 — 파일 본문은 아래 전역 토큰 치환을 거치므로 수집값도 동일 치환해
|
package/src/i18n/doctor.en.js
CHANGED
|
@@ -40,6 +40,10 @@ export default {
|
|
|
40
40
|
"doctor.pd.l1": "These run other workflows but permissions has no 'actions: write'.",
|
|
41
41
|
"doctor.pd.l2": "Even if the repository setting is Read and write, the API returns 403 without this declaration.",
|
|
42
42
|
"doctor.pd.l3": "Fix: add 'actions: write' to the permissions block of each file",
|
|
43
|
+
"doctor.legacy.name": "Retired workflows",
|
|
44
|
+
"doctor.legacy.purpose": "Old-generation files that run next to their replacements",
|
|
45
|
+
"doctor.legacy.l1": "They react to the same events as the new workflows, so every run happens twice. Delete them:",
|
|
46
|
+
"doctor.legacy.l2": "Or run `npx projectops --mode workflows --force --remove-legacy` to rename them to .bak.",
|
|
43
47
|
"doctor.bl.name": "Update baseline",
|
|
44
48
|
"doctor.bl.purpose": "Basis for telling your edits apart on the next update",
|
|
45
49
|
"doctor.bl.recorded": "{n} files recorded",
|
package/src/i18n/doctor.ko.js
CHANGED
|
@@ -40,6 +40,10 @@ export default {
|
|
|
40
40
|
"doctor.pd.l1": "다른 워크플로우를 실행하는데 permissions에 'actions: write'가 없습니다.",
|
|
41
41
|
"doctor.pd.l2": "저장소 설정이 Read and write여도 이 선언이 없으면 API가 403을 반환합니다.",
|
|
42
42
|
"doctor.pd.l3": "조치: 해당 파일의 permissions 블록에 'actions: write' 추가",
|
|
43
|
+
"doctor.legacy.name": "은퇴한 워크플로우",
|
|
44
|
+
"doctor.legacy.purpose": "신형과 함께 실행되는 구세대 파일",
|
|
45
|
+
"doctor.legacy.l1": "신형과 같은 이벤트에 반응해 모든 실행이 두 번 일어납니다. 지우려면:",
|
|
46
|
+
"doctor.legacy.l2": "또는 `npx projectops --mode workflows --force --remove-legacy` 로 .bak 으로 치웁니다.",
|
|
43
47
|
"doctor.bl.name": "업데이트 기준점",
|
|
44
48
|
"doctor.bl.purpose": "다음 업데이트가 내 수정을 구분할 근거",
|
|
45
49
|
"doctor.bl.recorded": "{n}개 파일 기록됨",
|
package/src/i18n/migrate.en.js
CHANGED
|
@@ -58,6 +58,9 @@ export default {
|
|
|
58
58
|
"migrate.confirm.found": "Found {n} old-generation deploy workflow(s). They may be live deployments, so they are left untouched:",
|
|
59
59
|
"migrate.confirm.newer": " (new: {name})",
|
|
60
60
|
"migrate.confirm.hint": " Delete the old files yourself once you have switched to the new workflows.",
|
|
61
|
+
"migrate.confirm.hintForce": " Delete the old files yourself once you have switched, or rerun with --remove-legacy to rename them to .bak.",
|
|
62
|
+
"migrate.confirm.ask": "Rename these {n} old workflow(s) to .bak so they stop running? (restore by removing .bak)",
|
|
63
|
+
"migrate.confirm.done": "✅ Renamed {n} old workflow(s) to .bak.",
|
|
61
64
|
"breaking.title": "BREAKING CHANGES (v{current} -> v{target}) - CRITICAL {critical}, WARNING {warnings}",
|
|
62
65
|
"breaking.askDetail": "Show the details (action steps) for each item?",
|
|
63
66
|
"breaking.seeJson": " Details and action steps: see .github/config/breaking-changes.json",
|
package/src/i18n/migrate.ko.js
CHANGED
|
@@ -58,6 +58,9 @@ export default {
|
|
|
58
58
|
"migrate.confirm.found": "⚠️ 구세대 배포 워크플로우 {n}개 발견 — 현역 배포일 수 있어 자동으로 건드리지 않습니다:",
|
|
59
59
|
"migrate.confirm.newer": " (신형: {name})",
|
|
60
60
|
"migrate.confirm.hint": " 신형 워크플로우로 전환을 마친 뒤 구 파일을 직접 삭제하세요.",
|
|
61
|
+
"migrate.confirm.hintForce": " 전환을 마친 뒤 구 파일을 직접 삭제하거나, --remove-legacy 로 다시 실행하면 .bak 으로 치웁니다.",
|
|
62
|
+
"migrate.confirm.ask": "구세대 워크플로우 {n}개를 .bak 으로 바꿔 실행을 멈출까요? (.bak 을 지우면 복원)",
|
|
63
|
+
"migrate.confirm.done": "✅ 구세대 워크플로우 {n}개를 .bak 으로 바꿨습니다.",
|
|
61
64
|
"breaking.title": "⚠️ BREAKING CHANGES (v{current} -> v{target}) - CRITICAL {critical}건, WARNING {warnings}건",
|
|
62
65
|
"breaking.askDetail": "각 항목의 상세 내용(조치 방법)을 볼까요?",
|
|
63
66
|
"breaking.seeJson": " 상세 내용, 조치 방법: .github/config/breaking-changes.json 참고",
|
package/src/i18n/summary.en.js
CHANGED
|
@@ -23,6 +23,16 @@ export default {
|
|
|
23
23
|
"done.keptHint1": "Copies of the new templates are in .github/.projectops/incoming/ (not tracked by git).",
|
|
24
24
|
"done.keptHint2": "Move only the changes you want into your workflows.",
|
|
25
25
|
"done.replacedTitle": "Could not tell whether {count} workflows were modified, so they were replaced with the new template and the originals saved as .bak",
|
|
26
|
+
"done.semverTitle": "Automatic version bump (semver_auto) is now ON for this repo",
|
|
27
|
+
"done.semverBody": "The next release picks the bump from commit titles: 'feat:' raises the minor version, 'type!:' the major one, anything else the patch.",
|
|
28
|
+
"done.semverOff": "To keep patch-only bumps, set metadata.template.options.semver_auto: false in version.yml.",
|
|
29
|
+
"done.notInstalledTitle": "{count} template workflow(s) were not installed",
|
|
30
|
+
"done.notInstalled.excluded": "listed in excluded_workflows",
|
|
31
|
+
"done.notInstalled.deleted-by-user": "you deleted it earlier",
|
|
32
|
+
"done.notInstalled.no-dev-branch": "only runs on {branch}, which this repo does not have",
|
|
33
|
+
"done.notInstalledHint": "To install one anyway, copy the file shown above. To stop being told, add it to options.excluded_workflows in version.yml.",
|
|
34
|
+
"done.legacyTitle": "{count} retired workflow(s) are still installed and run alongside their replacements",
|
|
35
|
+
"done.legacyHint": "Delete them once the new workflow works, or rerun with --remove-legacy to rename them to .bak.",
|
|
26
36
|
"done.replacedHint": "If you had edits, copy them over from the .bak files. (The update baseline is recorded by this run.)",
|
|
27
37
|
"done.utilTitle": "Utility modules:",
|
|
28
38
|
"done.spring.title": "Extra setup for Spring projects:",
|
package/src/i18n/summary.ko.js
CHANGED
|
@@ -23,6 +23,16 @@ export default {
|
|
|
23
23
|
"done.keptHint1": "새 템플릿 사본은 저장소에 추적되지 않는 .github/.projectops/incoming/ 에 있습니다.",
|
|
24
24
|
"done.keptHint2": "필요한 변경만 워크플로에 직접 옮겨 반영하세요.",
|
|
25
25
|
"done.replacedTitle": "수정 여부를 확인할 수 없어 {count}개 워크플로를 새 템플릿으로 교체하고 원본을 .bak 으로 백업했습니다",
|
|
26
|
+
"done.semverTitle": "이 레포의 자동 버전 승격(semver_auto)이 켜졌습니다",
|
|
27
|
+
"done.semverBody": "다음 릴리스부터 커밋 제목으로 올리는 폭을 정합니다: 'feat:'는 minor, 'type!:'는 major, 그 외는 patch.",
|
|
28
|
+
"done.semverOff": "patch만 올리고 싶으면 version.yml 의 metadata.template.options.semver_auto: false 로 두세요.",
|
|
29
|
+
"done.notInstalledTitle": "템플릿 워크플로우 {count}개를 설치하지 않았습니다",
|
|
30
|
+
"done.notInstalled.excluded": "excluded_workflows 에 적혀 있음",
|
|
31
|
+
"done.notInstalled.deleted-by-user": "예전에 직접 지운 파일",
|
|
32
|
+
"done.notInstalled.no-dev-branch": "{branch} 브랜치에서만 실행되는데 이 레포에는 그 브랜치가 없음",
|
|
33
|
+
"done.notInstalledHint": "그래도 쓰려면 위에 적힌 파일을 복사하세요. 안내를 그만 보려면 version.yml 의 options.excluded_workflows 에 추가하세요.",
|
|
34
|
+
"done.legacyTitle": "은퇴한 구세대 워크플로우 {count}개가 남아 신형과 함께 실행됩니다",
|
|
35
|
+
"done.legacyHint": "신형이 동작하는 걸 확인한 뒤 지우거나, --remove-legacy 로 다시 실행하면 .bak 으로 치웁니다.",
|
|
26
36
|
"done.replacedHint": "직접 수정한 내용이 있었다면 .bak 에서 옮겨 오세요. (업데이트 기준점은 이번 실행으로 새로 기록됩니다)",
|
|
27
37
|
"done.utilTitle": "유틸리티 모듈:",
|
|
28
38
|
"done.spring.title": "Spring 프로젝트 추가 설정:",
|
package/src/index.js
CHANGED
|
@@ -79,6 +79,14 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
79
79
|
if (opts.showVersion) { console.log(readPkgVersion()); return 0; }
|
|
80
80
|
if (opts.help) { console.log(helpText()); return 0; }
|
|
81
81
|
|
|
82
|
+
// options 모드 — 옵션 정본 출력. AI agent 가 문서를 뒤지지 않고 한 번에 읽는다. 읽기 전용·네트워크 없음.
|
|
83
|
+
if (opts.mode === "options") {
|
|
84
|
+
const { buildSchema, renderText } = await import("./core/options-schema.js");
|
|
85
|
+
const schema = buildSchema(readPkgVersion());
|
|
86
|
+
console.log(opts.json ? JSON.stringify(schema, null, 2) : renderText(schema));
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
// doctor 모드 (#558) — 읽기 전용 진단. 템플릿을 내려받지 않으므로 네트워크 없이도 동작한다.
|
|
83
91
|
if (opts.mode === "doctor") {
|
|
84
92
|
const { runDoctor } = await import("./commands/doctor.js");
|
|
@@ -245,12 +253,13 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
245
253
|
changelogProvider: migrateProvider(existing?.options?.changelogProvider) ?? "commit",
|
|
246
254
|
changelogBaseUrl: existing?.options?.changelogBaseUrl ?? "",
|
|
247
255
|
codeReviewCoderabbit: existing?.options?.codeReviewCoderabbit ?? false,
|
|
248
|
-
// semver 자동 승격(#546): 저장값
|
|
249
|
-
//
|
|
250
|
-
semverAuto: existing?.options?.semverAuto ??
|
|
256
|
+
// semver 자동 승격(#546, #814): 저장값(명시적 false 포함)은 존중하고, 키가 없으면 신규·기존 모두 켠다.
|
|
257
|
+
// 기존 레포에서 처음 켜질 때는 버전이 예고 없이 오르지 않게 완료 화면이 알린다 (semverAutoNewlyOn).
|
|
258
|
+
semverAuto: existing?.options?.semverAuto ?? true,
|
|
251
259
|
// 앱 심사 배포 레포 여부(#553): 저장값만 보존한다. 마법사가 묻지 않으므로 새로 켜지 않는다
|
|
252
260
|
// (사용자가 version.yml에 직접 쓰거나 스킬이 기록한 값을 그대로 유지).
|
|
253
261
|
appRelease: existing?.options?.appRelease ?? null,
|
|
262
|
+
excludedWorkflows: existing?.options?.excludedWorkflows ?? null, // #810 저장값 보존
|
|
254
263
|
// 상태 라벨 표기(#776): 플래그 > 저장값 > (신규 en / 기존 ko). 기존 레포의 라벨 이름은 업데이트만으로 바뀌지 않는다.
|
|
255
264
|
// 릴리스 시 완료 이슈 닫기(#771): 저장값 보존, 없으면 신규만 true. 기존 레포는 키를 만들지 않아 현행 유지.
|
|
256
265
|
closeOnRelease: existing?.options?.closeOnRelease ?? (existing ? null : true),
|
|
@@ -275,7 +284,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
275
284
|
});
|
|
276
285
|
trace.event("resolve", "semver-auto", String(context.semverAuto), {
|
|
277
286
|
reason: existing?.options?.semverAuto != null ? "stored value in version.yml kept"
|
|
278
|
-
: (existing ? "existing integrated repo,
|
|
287
|
+
: (existing ? "existing integrated repo without the key, turned on by default (announced in the summary)"
|
|
279
288
|
: "new integration, true"),
|
|
280
289
|
});
|
|
281
290
|
trace.event("resolve", "app-release", String(context.appRelease), {
|
|
@@ -313,7 +322,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
313
322
|
|
|
314
323
|
// 레거시 마이그레이션 (#470) — 워크플로우를 만지는 모드에서만. 비대화형은 safe 티어 자동 적용.
|
|
315
324
|
if (recordArtifacts) {
|
|
316
|
-
migrationsResult = await trace.stepAsync("legacy-migrations", () => runMigrations({ targetRoot: cwd }));
|
|
325
|
+
migrationsResult = await trace.stepAsync("legacy-migrations", () => runMigrations({ targetRoot: cwd, removeLegacy: opts.removeLegacy === true }));
|
|
317
326
|
for (const a of migrationsResult.applied ?? []) trace.event("legacy", a.action === "error" ? "error" : "neutralized", a.from ?? a.id ?? "", { to: a.to ?? "", id: a.id ?? "" });
|
|
318
327
|
for (const e of migrationsResult.confirmPending ?? []) trace.event("legacy", "leftover-old-gen", e.file, { replacement: e.replacedBy ?? "", reason: e.reason ?? "" });
|
|
319
328
|
}
|
|
@@ -352,7 +361,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
352
361
|
migrationGuidePath = appendGuideEntry(cwd, {
|
|
353
362
|
now, mode: opts.mode, types, repoName,
|
|
354
363
|
templateFrom: existing?.templateVersion || "", templateTo: context.templateVersion,
|
|
355
|
-
options: { deploy: deployTarget, publish: publishTargets, secretBackup: context.includeSecretBackup, coderabbit: context.codeReviewCoderabbit, changelogProvider: context.changelogProvider, intent, semverAuto: context.semverAuto , appRelease: context.appRelease, language: context.language, labelStyle: context.labelStyle, closeOnRelease: context.closeOnRelease, projectsSync: context.projectsSync },
|
|
364
|
+
options: { deploy: deployTarget, publish: publishTargets, secretBackup: context.includeSecretBackup, coderabbit: context.codeReviewCoderabbit, changelogProvider: context.changelogProvider, intent, semverAuto: context.semverAuto , appRelease: context.appRelease, language: context.language, labelStyle: context.labelStyle, closeOnRelease: context.closeOnRelease, projectsSync: context.projectsSync, excludedWorkflows: context.excludedWorkflows },
|
|
356
365
|
branches: { defaultBranch: branch, deployBranch: context.deployBranch || "develop", ready: null, created: null },
|
|
357
366
|
breaking: breakingReport, migrations: migrationsResult, orphans: { cleaned: [], pending: orphanPending },
|
|
358
367
|
events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
|
|
@@ -366,6 +375,9 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
366
375
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
367
376
|
skippedConflicts: result?.workflows?.skippedConflicts ?? [], // #654 병합 안내
|
|
368
377
|
replacedBak: result?.workflows?.replacedBak ?? [], // #673 기준점 없이 교체한 파일 안내
|
|
378
|
+
legacyLeftover: migrationsResult?.confirmPending ?? [], // #809 신·구 세대 동시 실행 안내
|
|
379
|
+
notInstalled: result?.workflows?.notInstalled ?? [], // #810 깔지 않은 워크플로우 안내
|
|
380
|
+
semverAutoNewlyOn: !!existing && existing?.options?.semverAuto == null && context.semverAuto === true, // 업데이트가 켠 사실 안내
|
|
369
381
|
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
370
382
|
// #569 — 고른 것만 안내하려면 선택값이 필요하다
|
|
371
383
|
aiPrSummary: context.aiPrSummary, codeReviewCoderabbit: context.codeReviewCoderabbit,
|
package/src/ui/summary.js
CHANGED
|
@@ -120,6 +120,38 @@ export function printSummary(ctx, targetRoot = ".", write = null) {
|
|
|
120
120
|
err(` ${t("done.replacedHint")}`);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// The update turned on automatic semver for a repo that did not have the key: tell them, because the next release can jump.
|
|
124
|
+
if (ctx?.semverAutoNewlyOn) {
|
|
125
|
+
err("");
|
|
126
|
+
err(` ${YELLOW}⚠️ ${t("done.semverTitle")}${NC}`);
|
|
127
|
+
err(` ${t("done.semverBody")}`);
|
|
128
|
+
err(` ${t("done.semverOff")}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Workflows left out on purpose (#810): deleted by the user, listed in excluded_workflows, or needing a branch this repo lacks.
|
|
132
|
+
const skippedNew = ctx?.notInstalled ?? [];
|
|
133
|
+
if (skippedNew.length > 0) {
|
|
134
|
+
err("");
|
|
135
|
+
err(` ${CYAN}ℹ️ ${t("done.notInstalledTitle", { count: skippedNew.length })}${NC}`);
|
|
136
|
+
for (const s of skippedNew) {
|
|
137
|
+
err(` ⏭️ ${s.filename} (${t(`done.notInstalled.${s.reason}`, { branch: ctx?.deployBranch || "develop" })})`);
|
|
138
|
+
if (s.incoming) err(` cp ${s.incoming} .github/workflows/${s.filename}`);
|
|
139
|
+
}
|
|
140
|
+
err(` ${t("done.notInstalledHint")}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Retired old-generation workflows still installed: they run next to their replacements (#809).
|
|
144
|
+
const legacy = ctx?.legacyLeftover ?? [];
|
|
145
|
+
if (legacy.length > 0) {
|
|
146
|
+
err("");
|
|
147
|
+
err(` ${YELLOW}⚠️ ${t("done.legacyTitle", { count: legacy.length })}${NC}`);
|
|
148
|
+
for (const e of legacy) {
|
|
149
|
+
err(` 🗑️ ${e.file}${e.replacedBy ? ` → ${e.replacedBy}` : ""}`);
|
|
150
|
+
err(` git rm .github/workflows/${e.file}`);
|
|
151
|
+
}
|
|
152
|
+
err(` ${t("done.legacyHint")}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
123
155
|
err("");
|
|
124
156
|
err(" 🔧 .github/scripts/");
|
|
125
157
|
err(" ├─ version_manager.sh");
|