projectops 4.44.2 → 4.45.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.md +3 -4
- package/package.json +1 -1
- package/src/commands/full.js +2 -2
- package/src/commands/interactive.js +26 -31
- package/src/commands/version.js +2 -2
- package/src/context.js +2 -0
- package/src/core/copy/workflows.js +29 -60
- package/src/core/options-ask.js +7 -3
- package/src/core/options-schema.js +8 -0
- package/src/core/orphan-workflows.js +7 -22
- package/src/core/resolve-options.js +91 -0
- package/src/core/version-yml.js +12 -2
- package/src/core/workflow-groups.js +44 -0
- package/src/index.js +20 -69
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
**Status: actively maintained.** npm `latest` is exactly what is on the `main` branch; work in progress on `develop` is never published. Versions can still ship several times a day. To stay on a version you have tested, run `npx projectops@<version>` ([releases](https://github.com/Cassiiopeia/projectops/releases)).
|
|
10
10
|
|
|
11
11
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
12
|
-
## Latest version : v4.
|
|
12
|
+
## Latest version : v4.45.0 (2026-10-10)
|
|
13
13
|
|
|
14
14
|
[View full version history](CHANGELOG.md)
|
|
15
15
|
|
|
@@ -49,7 +49,7 @@ This project automates your development workflow on two axes.
|
|
|
49
49
|
| Format every issue by hand | `/pro-github` writes and files an issue from the standard template in one step |
|
|
50
50
|
| Copy the issue URL into commit messages | `/pro-commit` completes the message from the issue context |
|
|
51
51
|
| Write PR descriptions and reports by hand | `/pro-report` analyzes the git diff and generates one |
|
|
52
|
-
| Re-type prompts for every code review or analysis |
|
|
52
|
+
| Re-type prompts for every code review or analysis | 17 Skills give consistent results without re-typing |
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -137,8 +137,7 @@ Open a PR from the development branch to `main` and it writes the release notes,
|
|
|
137
137
|
| `/pro-design-brief` | A design request for a screen you built before the mockup exists: state captures, alternatives, copy candidates, and must-keep rules as a board for the designer |
|
|
138
138
|
| `/pro-oss-consult` | Diagnose a GitHub repo as an open-source project: type detection, 6-axis scoring, maturity stage, and edits to About, topics, labels, and files only after approval. One repo or a whole account |
|
|
139
139
|
|
|
140
|
-
> Design, planning, and implementation
|
|
141
|
-
> `/pro-plan`, `/pro-analyze`, and `/pro-implement` are the previous generation of the same roles and only run when invoked explicitly. Together with the 17 in the tables above, that makes 20 in total.
|
|
140
|
+
> Design, planning, and implementation are handled by `superpowers` (brainstorming → writing-plans → executing-plans), not by this plugin.
|
|
142
141
|
|
|
143
142
|
### 📝 Documents and artifacts
|
|
144
143
|
|
package/package.json
CHANGED
package/src/commands/full.js
CHANGED
|
@@ -30,7 +30,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
30
30
|
force = true, now, today, templateVersion = "unknown",
|
|
31
31
|
deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true,
|
|
32
32
|
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
33
|
-
deployBranch = "", intent = null, semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = "en", excludedWorkflows = null, storeLocales = null } = context;
|
|
33
|
+
deployBranch = "", intent = null, semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = "en", excludedWorkflows = null, storeLocales = null, storeLocalesIos = null, storeLocalesPlay = null } = context;
|
|
34
34
|
|
|
35
35
|
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
36
36
|
const pathMarkers = new Map();
|
|
@@ -55,7 +55,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
55
55
|
...customYml(vyFile),
|
|
56
56
|
updatedBy: resolveUpdatedBy(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "", targetRoot), // #811
|
|
57
57
|
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, aiPrSummary, optionsDate: today,
|
|
58
|
-
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales },
|
|
58
|
+
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay },
|
|
59
59
|
})), { version, versionCode });
|
|
60
60
|
|
|
61
61
|
// 2. README 버전 섹션
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// io 주입으로 테스트 가능. 실제 실행은 src/ui/prompts.js 함수를 io로 넘긴다.
|
|
3
3
|
// 새 시각 층(banner/detectionLog/analysisCard/ideStatus/installKind/summary)과 저수준 엔진(engineIo)은
|
|
4
4
|
// io의 "옵셔널 멤버" — 스텁이 생략하면 해당 층만 건너뛰고 실행 계약은 동일하다.
|
|
5
|
-
import {
|
|
5
|
+
import { resolveOptions } from "../core/resolve-options.js";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { PATHS } from "../core/paths.js";
|
|
@@ -14,7 +14,7 @@ import { runBreakingCheck } from "../core/breaking-check.js";
|
|
|
14
14
|
import { runMigrations } from "../core/migrations/index.js";
|
|
15
15
|
import { detectOrphanWorkflows, applyOrphanCleanup } from "../core/orphan-workflows.js";
|
|
16
16
|
import { resolveProjectPaths, filterExcludedTypes } from "../core/paths-resolve.js";
|
|
17
|
-
import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets
|
|
17
|
+
import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets } from "../core/options-ask.js";
|
|
18
18
|
import { createRunTrace, MIGRATION_DIR } from "../core/run-trace.js";
|
|
19
19
|
import { appendGuideEntry } from "../core/migration-guide.js";
|
|
20
20
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
@@ -125,33 +125,26 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
125
125
|
let branch = detectDefaultBranch(cwd);
|
|
126
126
|
const repoName = detectRepoName(cwd);
|
|
127
127
|
const versionCode = existing?.versionCode ?? 1; // 기존 빌드번호 보존
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let
|
|
132
|
-
let
|
|
133
|
-
let
|
|
134
|
-
let
|
|
135
|
-
let
|
|
136
|
-
let
|
|
128
|
+
// 옵션 초기값은 비대화형과 같은 resolve-options.js 에서 받는다 (#851).
|
|
129
|
+
// 질문을 건너뛰는 모드(version 등)도 이 값이 그대로 기록되므로 타입 적용성 정리까지 거친 값이어야 한다.
|
|
130
|
+
const { values: init, sources: initSources } = resolveOptions({ existing, types, cwd });
|
|
131
|
+
let deployTarget = init.deployTarget;
|
|
132
|
+
let publishTargets = init.publishTargets;
|
|
133
|
+
let includeSecretBackup = init.includeSecretBackup;
|
|
134
|
+
let aiPrSummary = init.aiPrSummary;
|
|
135
|
+
let codeReviewCoderabbit = init.codeReviewCoderabbit;
|
|
136
|
+
let changelogProvider = init.changelogProvider;
|
|
137
|
+
let changelogBaseUrl = init.changelogBaseUrl;
|
|
138
|
+
let deployBranch = init.deployBranch; // #456 — 비어 있으면 기록하지 않는다. 질문에서 고른 값만 남긴다
|
|
137
139
|
let deployBranchReady = null; // #490 — 이번 실행에서 개발 브랜치 존재/생성이 확인됐는지
|
|
138
140
|
let deployBranchCreated = null; // #493 — 마법사가 직접 생성했는지 (가이드 기록용)
|
|
139
|
-
let intent =
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const closeOnRelease = existing?.options?.closeOnRelease ?? (existing ? null : true); // #771 묻지 않는다
|
|
147
|
-
const excludedWorkflows = existing?.options?.excludedWorkflows ?? null; // #810 저장값 보존
|
|
148
|
-
const storeLocales = existing?.options?.storeLocales ?? null; // #829 저장값 보존
|
|
149
|
-
// Projects 보드 동기화(#716): 묻지 않는다. 저장값 > 이미 설치돼 있으면 유지 > 신규는 제외 (index.js 와 같은 규칙).
|
|
150
|
-
const projectsSync = existing?.options?.projectsSync
|
|
151
|
-
?? existsSync(join(cwd, ".github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml"));
|
|
152
|
-
// 레포 문구 언어(#769): 저장값 > (신규 en / 기존 ko). 기존 레포에는 영문 전환을 한 번만 제안한다.
|
|
153
|
-
let language = resolveRepoLanguage({ flag: null, stored: existing?.options?.language, existing: !!existing });
|
|
154
|
-
let labelStyle = resolveLabelStyle({ flag: null, stored: existing?.options?.labelStyle, existing: !!existing });
|
|
141
|
+
let intent = init.intent; // #485 프로젝트 성격
|
|
142
|
+
// 묻지 않는 값 — 저장값 보존 규칙은 resolve-options.js 에 있다
|
|
143
|
+
const { semverAuto, appRelease, closeOnRelease, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay, projectsSync } = init;
|
|
144
|
+
const semverAutoNewlyOn = initSources.semverAutoNewlyOn;
|
|
145
|
+
// 레포 문구 언어(#769)·라벨 표기(#776): 기존 레포에는 영문 전환을 한 번만 제안한다(아래).
|
|
146
|
+
let language = init.language;
|
|
147
|
+
let labelStyle = init.labelStyle;
|
|
155
148
|
const showOptional = mode === "full" || mode === "workflows";
|
|
156
149
|
const realTty = process.stdout.isTTY === true;
|
|
157
150
|
// 기존 한글 라벨 레포에는 영문 표준 전환을 한 번만 제안한다(#776). 거절하면 한글을 저장해 다시 묻지 않는다.
|
|
@@ -192,6 +185,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
192
185
|
publishTargets = r.publish;
|
|
193
186
|
includeSecretBackup = r.secretBackup;
|
|
194
187
|
codeReviewCoderabbit = r.codeReviewCoderabbit;
|
|
188
|
+
aiPrSummary = r.aiPrSummary; // #851 — 빠져 있어 PR 요약 선택이 버려졌다
|
|
195
189
|
changelogProvider = r.changelogProvider;
|
|
196
190
|
changelogBaseUrl = r.changelogBaseUrl;
|
|
197
191
|
deployBranch = r.deployBranch;
|
|
@@ -254,7 +248,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
254
248
|
tempDir, types, targetRoot: cwd, defaultBranch: branch,
|
|
255
249
|
current: {
|
|
256
250
|
deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup,
|
|
257
|
-
codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent,
|
|
251
|
+
codeReviewCoderabbit, aiPrSummary, changelogProvider, changelogBaseUrl, deployBranch, intent,
|
|
258
252
|
},
|
|
259
253
|
force: false, tty: realTty, forceAsk: true, scope: [what],
|
|
260
254
|
io: {
|
|
@@ -268,6 +262,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
268
262
|
publishTargets = r.publish;
|
|
269
263
|
includeSecretBackup = r.secretBackup;
|
|
270
264
|
codeReviewCoderabbit = r.codeReviewCoderabbit;
|
|
265
|
+
aiPrSummary = r.aiPrSummary; // #851
|
|
271
266
|
changelogProvider = r.changelogProvider;
|
|
272
267
|
changelogBaseUrl = r.changelogBaseUrl;
|
|
273
268
|
deployBranch = r.deployBranch;
|
|
@@ -332,10 +327,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
332
327
|
const { now, today } = clock || utcNow();
|
|
333
328
|
const ctx = createContext({
|
|
334
329
|
mode, force: true, types, version, versionCode, branch, paths, deployTarget, publishTargets, includeSecretBackup,
|
|
335
|
-
codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales,
|
|
330
|
+
aiPrSummary, codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay,
|
|
336
331
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
337
332
|
// #502 — version 모드가 기존 full 기록을 강등하지 않도록 (full이 우세)
|
|
338
|
-
recordMode:
|
|
333
|
+
recordMode: init.recordMode,
|
|
339
334
|
});
|
|
340
335
|
ctx.templateVersion = templateVersion;
|
|
341
336
|
|
|
@@ -427,7 +422,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
427
422
|
migrationGuidePath = appendGuideEntry(cwd, {
|
|
428
423
|
now, mode, types, repoName,
|
|
429
424
|
templateFrom: existing?.templateVersion || "", templateTo: templateVersion,
|
|
430
|
-
options: { deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup, coderabbit: codeReviewCoderabbit, changelogProvider, intent, semverAuto , appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales },
|
|
425
|
+
options: { deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup, coderabbit: codeReviewCoderabbit, changelogProvider, intent, semverAuto , appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay },
|
|
431
426
|
branches: { defaultBranch: branch, deployBranch, ready: deployBranchReady, created: deployBranchCreated },
|
|
432
427
|
breaking: breakingReport, migrations: migrationsResult, orphans: orphanReport,
|
|
433
428
|
events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
|
package/src/commands/version.js
CHANGED
|
@@ -17,7 +17,7 @@ export function runVersion(context, tempDir, targetRoot = ".") {
|
|
|
17
17
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
18
18
|
now, today, templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false,
|
|
19
19
|
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
20
|
-
deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = null, excludedWorkflows = null, storeLocales = null } = context;
|
|
20
|
+
deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = null, excludedWorkflows = null, storeLocales = null, storeLocalesIos = null, storeLocalesPlay = null } = context;
|
|
21
21
|
|
|
22
22
|
const pathMarkers = new Map();
|
|
23
23
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
@@ -36,7 +36,7 @@ export function runVersion(context, tempDir, targetRoot = ".") {
|
|
|
36
36
|
// mode(#502): version 모드가 기존 full 통합 기록을 "version"으로 강등하지 않도록
|
|
37
37
|
// 호출부가 recordMode로 기존 값을 넘긴다 (full이 우세 — 업데이트 재실행 범위 축소 방지).
|
|
38
38
|
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate: today,
|
|
39
|
-
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, mode: recordMode, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales },
|
|
39
|
+
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, mode: recordMode, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay },
|
|
40
40
|
}));
|
|
41
41
|
addVersionSectionToReadme(version, targetRoot);
|
|
42
42
|
copyScripts(tempDir, targetRoot);
|
package/src/context.js
CHANGED
|
@@ -33,6 +33,8 @@ export function createContext(overrides = {}) {
|
|
|
33
33
|
labelStyle: null, // 상태 라벨 표기 (#776). null=미설정(신규 en / 기존 레포 ko)
|
|
34
34
|
excludedWorkflows: null, // 설치하지 않을 워크플로우 파일명 (#810). null=없음
|
|
35
35
|
storeLocales: null, // 스토어 릴리스 노트 언어 (#829). null=미설정 → 현행 동작 유지
|
|
36
|
+
storeLocalesIos: null, // App Store 만 언어 구성이 다를 때 (#829). null=공통 store_locales 를 쓴다
|
|
37
|
+
storeLocalesPlay: null, // Play 만 언어 구성이 다를 때 (#829)
|
|
36
38
|
appRelease: null, // 앱 심사 배포 레포인가 (#553). null=미설정(키 기록 안 함)
|
|
37
39
|
templateVersion: "",
|
|
38
40
|
tempDir: "",
|
|
@@ -15,6 +15,7 @@ import { substituteBranches } from "../branch-sub.js";
|
|
|
15
15
|
import { saveIncoming, lineDiffCounts } from "../incoming.js";
|
|
16
16
|
import { branchStatus } from "../git-branch.js";
|
|
17
17
|
import { PUBLISH_TARGETS } from "../constants.js";
|
|
18
|
+
import { commonOptionalGroups, serverDeployEnabled, SERVER_DEPLOY_DIR } from "../workflow-groups.js";
|
|
18
19
|
|
|
19
20
|
// on: 블록의 브랜치 필터가 전부 develop 뿐인가 (#810).
|
|
20
21
|
// 개발 브랜치가 없는 레포에 깔면 영영 실행되지 않는 워크플로우를 가린다.
|
|
@@ -229,78 +230,46 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
229
230
|
if (asks.size) deployValues.set(type, asks);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
// (4.5) common/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
// (4.5~5) common/ 아래 선택형 폴더 — deploy/<target>, pr-summary, projects-sync, secret-backup.
|
|
234
|
+
// 어느 폴더를 켜는지는 workflow-groups.js 한 곳이 정한다 (#851). 고아 감지도 같은 표를 쓴다.
|
|
235
|
+
// bak-replace: 템플릿과 같으면 건너뛰고, 다르면 .bak 으로 남기고 교체한다.
|
|
236
|
+
// new-only : 이미 있으면 건드리지 않는다. 새로 깔 때만 레포 이름을 채운다 (#729 — 고정값이면
|
|
237
|
+
// 모든 프로젝트가 서버의 같은 폴더에 Secret 을 덮어쓴다).
|
|
238
|
+
for (const group of commonOptionalGroups(commonDir, { includeSecretBackup, aiPrSummary, projectsSync, deployTarget })) {
|
|
239
|
+
const groupDir = join(commonDir, group.dir);
|
|
240
|
+
if (!group.enabled || !exists(groupDir)) continue;
|
|
241
|
+
for (const filename of listYamlFiles(groupDir)) {
|
|
242
|
+
const src = join(groupDir, filename);
|
|
237
243
|
const dst = join(workflowsDir, filename);
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
if (group.policy === "new-only") {
|
|
245
|
+
if (existsSync(dst)) continue;
|
|
246
|
+
if (skipNew(groupDir, filename, group.copyLabel)) continue;
|
|
247
|
+
copyFileSync(src, dst);
|
|
248
|
+
const raw = readFileSync(dst, "utf8");
|
|
249
|
+
const filled = resolveGlobalTokens(raw, repoName || "my-project");
|
|
250
|
+
if (filled !== raw) writeFileSync(dst, filled);
|
|
251
|
+
counters.optionalCopied++;
|
|
252
|
+
counters.copied++;
|
|
253
|
+
counters.copiedFiles.push(filename);
|
|
254
|
+
trace?.event("copy", "copied", filename, { group: group.copyLabel });
|
|
241
255
|
continue;
|
|
242
256
|
}
|
|
243
|
-
if (skipNew(commonDeployDir, filename, "common-deploy")) continue;
|
|
244
|
-
const backedUp = existsSync(dst);
|
|
245
|
-
if (backedUp) renameSync(dst, dst + ".bak");
|
|
246
|
-
copyFileSync(src, dst);
|
|
247
|
-
counters.optionalCopied++;
|
|
248
|
-
counters.copied++;
|
|
249
|
-
counters.copiedFiles.push(filename);
|
|
250
|
-
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: "common-deploy" });
|
|
251
|
-
if (backedUp) counters.replacedBak.push(filename); // #673
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// (4.7) common/pr-summary — AI 변경 요약 (#566). 선택했을 때만 복사한다.
|
|
256
|
-
// 종전에는 common 본체에 있어 무조건 복사된 뒤 런타임에 스스로 빠졌고, 그 판단이
|
|
257
|
-
// ".coderabbit.yaml 존재"라 파일만 있고 앱이 없는 저장소에서는 아무도 요약하지 않았다.
|
|
258
|
-
// (4.8) common/projects-sync — Projects 보드 동기화 (#716). 이것도 선택했을 때만 복사한다.
|
|
259
|
-
// Secret(_GITHUB_PAT_TOKEN)과 PROJECT_URL 변수를 등록해야 동작하는데, Projects 를 안 쓰는 레포가
|
|
260
|
-
// 많아 설치만 되고 설정되지 않은 채 남았다. 같은 폴더 방식이라 게이트와 고아 감지(orphan-workflows.js)를 함께 둔다.
|
|
261
|
-
for (const [groupName, groupEnabled] of [["pr-summary", aiPrSummary], ["projects-sync", projectsSync]]) {
|
|
262
|
-
const prSummaryDir = join(commonDir, groupName);
|
|
263
|
-
if (exists(prSummaryDir) && groupEnabled) {
|
|
264
|
-
for (const filename of listYamlFiles(prSummaryDir)) {
|
|
265
|
-
const src = join(prSummaryDir, filename);
|
|
266
|
-
const dst = join(workflowsDir, filename);
|
|
267
257
|
if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOptsFor("common"))) {
|
|
268
258
|
counters.skipped++;
|
|
269
|
-
trace?.event("copy", "skipped-unchanged", filename, { group:
|
|
259
|
+
trace?.event("copy", "skipped-unchanged", filename, { group: group.copyLabel });
|
|
270
260
|
continue;
|
|
271
261
|
}
|
|
272
|
-
if (skipNew(
|
|
262
|
+
if (skipNew(groupDir, filename, group.copyLabel)) continue;
|
|
273
263
|
const backedUp = existsSync(dst);
|
|
274
264
|
if (backedUp) renameSync(dst, dst + ".bak");
|
|
275
265
|
copyFileSync(src, dst);
|
|
276
266
|
counters.optionalCopied++;
|
|
277
267
|
counters.copied++;
|
|
278
268
|
counters.copiedFiles.push(filename);
|
|
279
|
-
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group:
|
|
269
|
+
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: group.copyLabel });
|
|
280
270
|
if (backedUp) counters.replacedBak.push(filename); // #673
|
|
281
271
|
}
|
|
282
272
|
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// (5) common/secret-backup — 있으면 무조건 스킵/신규만 복사
|
|
286
|
-
const secretDir = join(commonDir, "secret-backup");
|
|
287
|
-
if (exists(secretDir) && includeSecretBackup) {
|
|
288
|
-
for (const filename of listYamlFiles(secretDir)) {
|
|
289
|
-
const dst = join(workflowsDir, filename);
|
|
290
|
-
if (existsSync(dst)) continue; // 이미 존재하면 스킵
|
|
291
|
-
if (skipNew(secretDir, filename, "secret-backup")) continue;
|
|
292
|
-
copyFileSync(join(secretDir, filename), dst);
|
|
293
|
-
// PROJECT_NAME 이 고정값이면 모든 프로젝트가 서버의 같은 폴더에 Secret 을 덮어쓴다 (#729) —
|
|
294
|
-
// 다른 배포 워크플로처럼 레포 이름으로 채운다. 이름을 알 수 없으면 기존 기본값을 쓴다.
|
|
295
|
-
const raw = readFileSync(dst, "utf8");
|
|
296
|
-
const filled = resolveGlobalTokens(raw, repoName || "my-project");
|
|
297
|
-
if (filled !== raw) writeFileSync(dst, filled);
|
|
298
|
-
counters.optionalCopied++;
|
|
299
|
-
counters.copied++;
|
|
300
|
-
counters.copiedFiles.push(filename);
|
|
301
|
-
trace?.event("copy", "copied", filename, { group: "secret-backup" });
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
273
|
|
|
305
274
|
// (6) 브랜치 치환 post-pass (#477) — 표준과 다른 브랜치 전략일 때만 복사된 파일에 적용.
|
|
306
275
|
// isUnchanged 가상 비교에도 같은 branches가 들어가므로 다음 업데이트에서 재복사 churn이 없다.
|
|
@@ -411,8 +380,8 @@ export function listWorkflowConflicts(context, tempDir, targetRoot = ".") {
|
|
|
411
380
|
if (exists(typeDir)) {
|
|
412
381
|
for (const f of classify(typeDir, workflowsDir, envOpts, baseline).changed) conflicts.push({ filename: f, type });
|
|
413
382
|
}
|
|
414
|
-
const serverDeployDir = join(typeDir,
|
|
415
|
-
if (exists(serverDeployDir) && (deployTarget
|
|
383
|
+
const serverDeployDir = join(typeDir, SERVER_DEPLOY_DIR);
|
|
384
|
+
if (exists(serverDeployDir) && serverDeployEnabled(deployTarget)) {
|
|
416
385
|
for (const f of classify(serverDeployDir, workflowsDir, envOpts, baseline).changed) conflicts.push({ filename: f, type });
|
|
417
386
|
}
|
|
418
387
|
}
|
|
@@ -464,8 +433,8 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
464
433
|
}
|
|
465
434
|
|
|
466
435
|
// server-deploy — deploy=docker-ssh일 때만 포함 (#439)
|
|
467
|
-
const serverDeployDir = join(typeDir,
|
|
468
|
-
if (exists(serverDeployDir) && (deployTarget
|
|
436
|
+
const serverDeployDir = join(typeDir, SERVER_DEPLOY_DIR);
|
|
437
|
+
if (exists(serverDeployDir) && serverDeployEnabled(deployTarget)) {
|
|
469
438
|
const { newFiles, unchanged, changed, upstream } = classify(serverDeployDir, workflowsDir, envOpts, baseline);
|
|
470
439
|
for (const f of unchanged) { counters.skipped++; trace?.event("copy", "skipped-unchanged", f, { group: `${type}/server-deploy` }); }
|
|
471
440
|
for (const f of newFiles) { if (counters.skipNew(serverDeployDir, f, `${type}/server-deploy`)) continue; copyFileSync(join(serverDeployDir, f), join(workflowsDir, f)); counters.copied++; counters.copiedFiles.push(f); trace?.event("copy", "copied", f, { group: `${type}/server-deploy` }); }
|
package/src/core/options-ask.js
CHANGED
|
@@ -234,7 +234,10 @@ export async function askAllOptionalWorkflows({
|
|
|
234
234
|
if (askIntent) {
|
|
235
235
|
if (force || !tty || typeof io.select !== "function") {
|
|
236
236
|
// 비대화형: CLI intent 미지정이면 deploy/publish에서 역추론, 그래도 없으면 manual(기존 동작)
|
|
237
|
-
|
|
237
|
+
// 축이 아직 안 정해졌으면 비대화형(resolve-options.js)과 같은 기본 축으로 추론한다 (#851).
|
|
238
|
+
// 예전에는 null 로 추론해 "manual" 이 기록되어 같은 레포에서 두 경로의 결과가 갈렸다.
|
|
239
|
+
const defaultDeploy = applicable.deploy.includes("docker-ssh") ? "docker-ssh" : "none";
|
|
240
|
+
intent = intent ?? inferIntent(deploy ?? defaultDeploy, publish ?? []) ?? "manual";
|
|
238
241
|
} else {
|
|
239
242
|
say("");
|
|
240
243
|
say(msg("options.intent.intro"));
|
|
@@ -394,7 +397,8 @@ export async function askAllOptionalWorkflows({
|
|
|
394
397
|
// "배포 브랜치"가 아니다 — 배포가 도는 곳은 기본 브랜치(default) 쪽 개념. #482 참조.
|
|
395
398
|
if (ask("release-branch") || deployBranch === null) {
|
|
396
399
|
if (force || !tty || typeof io.text !== "function") {
|
|
397
|
-
|
|
400
|
+
// 묻지 못했으면 값을 지어내지 않는다 (#851) — 비어 있으면 기록하지 않고 스킬이 develop 으로 폴백한다.
|
|
401
|
+
deployBranch = deployBranch ?? "";
|
|
398
402
|
} else {
|
|
399
403
|
const base = defaultBranch || "main"; // git으로 감지된 기본 브랜치 (#481 동적 안내)
|
|
400
404
|
say("");
|
|
@@ -431,7 +435,7 @@ export async function askAllOptionalWorkflows({
|
|
|
431
435
|
aiPrSummary: aiPrSummary !== false,
|
|
432
436
|
changelogProvider: changelogProvider ?? "commit",
|
|
433
437
|
changelogBaseUrl: changelogBaseUrl ?? "",
|
|
434
|
-
deployBranch: deployBranch ?? "
|
|
438
|
+
deployBranch: deployBranch ?? "",
|
|
435
439
|
deployBranchReady, // #490 — true=존재/생성 확인됨, false=거절/실패, null=확인 안 함
|
|
436
440
|
deployBranchCreated, // #493 — true=이번 실행에서 마법사가 생성, null=확인 안 함
|
|
437
441
|
|
|
@@ -111,6 +111,12 @@ export const VERSION_YML = [
|
|
|
111
111
|
{ path: "metadata.template.options.store_locales", parserKey: "storeLocales", type: "string[]",
|
|
112
112
|
description: "Store release-note languages, first is the default (for example [\"ko-KR\", \"en-US\", \"ja-JP\", \"zh-CN\"]). Missing = one text for every store language, as before. Per-language text comes from store_notes in CHANGELOG.json; a language without text gets the default-language text (the stores reject an empty What's New).",
|
|
113
113
|
edit: "by hand" },
|
|
114
|
+
{ path: "metadata.template.options.store_locales_ios", parserKey: "storeLocalesIos", type: "string[]",
|
|
115
|
+
description: "Only when the App Store languages differ from store_locales: the App Store uses this list instead. fastlane deliver ACTIVATES every language folder the app version does not have yet, so a language the App Store listing lacks must not be in this list.",
|
|
116
|
+
edit: "by hand" },
|
|
117
|
+
{ path: "metadata.template.options.store_locales_play", parserKey: "storeLocalesPlay", type: "string[]",
|
|
118
|
+
description: "Only when the Play languages differ from store_locales: Google Play uses this list instead.",
|
|
119
|
+
edit: "by hand" },
|
|
114
120
|
{ path: "metadata.template.options.code_review.coderabbit", parserKey: "codeReviewCoderabbit", type: "boolean", description: "CodeRabbit config installed.", edit: "wizard" },
|
|
115
121
|
{ path: "metadata.template.options.code_review.ai_summary", parserKey: "aiPrSummary", type: "boolean", default: true, description: "AI PR summary workflow installed.", edit: "wizard (--ai-summary)" },
|
|
116
122
|
{ path: "metadata.template.options.changelog.provider", parserKey: "changelogProvider", type: "enum",
|
|
@@ -269,6 +275,8 @@ export const KEY_DETAILS = {
|
|
|
269
275
|
"metadata.template.options.app_release": { when: "Set true by hand for repos whose releases go through App Store / Play Store review.", effect: "The deploy skill (pro-changelog-deploy) treats releases as going through store review: release notes get a review warning banner and are checked more strictly, and the release PR always waits for manual approval instead of automerging.", omitted: "Not set. Never enabled automatically.", caution: "Do not set it on repos that do not ship to a store." },
|
|
270
276
|
"metadata.template.options.excluded_workflows": { when: "Add a file name to stop the update from ever installing that template workflow.", effect: "Listed files are skipped on install and update. Files you deleted yourself are also not restored; a copy of the template is kept in .github/.projectops/incoming/.", omitted: "[]", example: 'excluded_workflows: ["PROJECT-COMMON-TEMPLATE-UTIL-VERSION-SYNC.yml"]' },
|
|
271
277
|
"metadata.template.options.store_locales": { when: "Set it when the store listing has several languages and release notes must not be one Korean text copied into all of them.", effect: "Turns on per-language store release notes (Play Console 'Release notes', App Store 'What's New'). The first entry is the default language. Each other language uses the text in store_notes[<language>] of that version in CHANGELOG.json; a language without text gets the default-language text, because the stores reject an empty What's New.", omitted: "Off: the same text is used for every store language, as before.", example: 'store_locales: ["ko-KR", "en-US", "ja-JP", "zh-CN"]' },
|
|
278
|
+
"metadata.template.options.store_locales_ios": { when: "Set it only when the App Store listing has a different set of languages than Google Play.", effect: "The App Store uses this list instead of store_locales (same rules: first entry is the default language, a language without text gets the default-language text). Needed because fastlane deliver ACTIVATES every language folder the app version does not have, so a language the App Store listing lacks would be added to the app.", omitted: "The App Store uses store_locales.", example: 'store_locales_ios: ["ko-KR", "en-US"]' },
|
|
279
|
+
"metadata.template.options.store_locales_play": { when: "Set it only when Google Play has a different set of languages than the App Store.", effect: "Google Play uses this list instead of store_locales (same rules).", omitted: "Google Play uses store_locales.", example: 'store_locales_play: ["ko-KR", "en-US", "ja-JP"]' },
|
|
272
280
|
"metadata.template.options.code_review.coderabbit": { when: "Edit via --coderabbit / --no-coderabbit.", effect: "Whether .coderabbit.yaml is installed by --mode full. PR review bot only; unrelated to release notes.", omitted: "false" },
|
|
273
281
|
"metadata.template.options.code_review.ai_summary": { when: "Edit via --ai-summary.", effect: "Whether the AI PR summary workflow is installed.", omitted: "true" },
|
|
274
282
|
"metadata.template.options.changelog.provider": { when: "Leave the default. Set only to force one generator.", effect: "Release notes: the PR body is used as is when it already has notes; otherwise a registered AI key (GEMINI_API_KEY, GROQ_API_KEY, MISTRAL_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY) is used; otherwise the commit analysis, which always works and costs nothing. 'copilot' consumes Copilot premium requests, so it is only used when set here.", omitted: "commit (an AI key is still picked up automatically)." },
|
|
@@ -6,11 +6,12 @@ import { existsSync, renameSync, rmSync, readdirSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { PATHS } from "./paths.js";
|
|
8
8
|
import { exists, listYamlFiles } from "./fsutil.js";
|
|
9
|
+
import { commonOptionalGroups, SERVER_DEPLOY_DIR } from "./workflow-groups.js";
|
|
9
10
|
|
|
10
11
|
// 템플릿의 project-types/<type>/ 인벤토리 — 직하위 + server-deploy/ + publish/*/ (copy 엔진과 동일 범위)
|
|
11
12
|
function typeInventory(projectTypesDir, type) {
|
|
12
13
|
const typeDir = join(projectTypesDir, type);
|
|
13
|
-
const dirs = [typeDir, join(typeDir,
|
|
14
|
+
const dirs = [typeDir, join(typeDir, SERVER_DEPLOY_DIR)];
|
|
14
15
|
const pubRoot = join(typeDir, "publish");
|
|
15
16
|
if (exists(pubRoot)) {
|
|
16
17
|
for (const e of readdirSync(pubRoot, { withFileTypes: true })) {
|
|
@@ -28,30 +29,14 @@ function typeInventory(projectTypesDir, type) {
|
|
|
28
29
|
// common/ 아래 "선택했을 때만 복사되는" 폴더들 (#566).
|
|
29
30
|
// 켜면 복사 엔진이 넣어주지만, 끄면 이미 있던 파일이 그대로 남아 계속 실행됐다.
|
|
30
31
|
// 축을 끈 사용자에게는 그게 곧 "껐는데 왜 도나"가 된다 — 여기서 고아로 잡는다.
|
|
31
|
-
//
|
|
32
|
+
// 켜짐/꺼짐은 복사 엔진과 같은 표(workflow-groups.js)에서 읽는다 (#851).
|
|
32
33
|
function commonOptionalOrphans(commonDir, opts) {
|
|
33
|
-
const { includeSecretBackup = false, aiPrSummary = true, projectsSync = true, deployTarget = "docker-ssh" } = opts;
|
|
34
34
|
const out = [];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
["pr-summary", aiPrSummary, "pr-summary"],
|
|
39
|
-
["projects-sync", projectsSync, "projects-sync"],
|
|
40
|
-
];
|
|
41
|
-
for (const [dir, enabled, label] of gated) {
|
|
42
|
-
if (enabled) continue;
|
|
43
|
-
const d = join(commonDir, dir);
|
|
35
|
+
for (const group of commonOptionalGroups(commonDir, opts)) {
|
|
36
|
+
if (group.enabled) continue;
|
|
37
|
+
const d = join(commonDir, group.dir);
|
|
44
38
|
if (!exists(d)) continue;
|
|
45
|
-
for (const f of listYamlFiles(d)) out.push({ filename: f, type:
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// deploy/<target> — 선택된 타겟 외 폴더는 전부 고아 후보
|
|
49
|
-
const deployRoot = join(commonDir, "deploy");
|
|
50
|
-
if (exists(deployRoot)) {
|
|
51
|
-
for (const e of readdirSync(deployRoot, { withFileTypes: true })) {
|
|
52
|
-
if (!e.isDirectory() || e.name === deployTarget) continue;
|
|
53
|
-
for (const f of listYamlFiles(join(deployRoot, e.name))) out.push({ filename: f, type: `deploy/${e.name}` });
|
|
54
|
-
}
|
|
39
|
+
for (const f of listYamlFiles(d)) out.push({ filename: f, type: group.orphanLabel });
|
|
55
40
|
}
|
|
56
41
|
return out;
|
|
57
42
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// 설치 옵션 값을 정하는 단 한 곳 (#851).
|
|
2
|
+
//
|
|
3
|
+
// 예전에는 비대화형(src/index.js)과 대화형(src/commands/interactive.js)이 같은 옵션을
|
|
4
|
+
// 각자 정했다. 그 사이 규칙이 갈라져 대화형에서 PR 요약 선택이 버려지고(aiPrSummary),
|
|
5
|
+
// 질문을 건너뛰는 version 모드가 모바일 단독 레포에 deploy: docker-ssh 를 남겼다.
|
|
6
|
+
// 규칙은 모든 옵션에 같다: CLI 플래그 > version.yml 저장값 > 기본값.
|
|
7
|
+
// 대화형은 이 결과를 초기값으로 쓰고, 사용자가 답한 것만 그 위에 덮어쓴다.
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { applicableTargets, migrateProvider } from "./options-ask.js";
|
|
11
|
+
import { DEFAULT_CODE_REVIEW_CODERABBIT } from "./constants.js";
|
|
12
|
+
import { resolveLabelStyle } from "./label-style.js";
|
|
13
|
+
import { resolveRepoLanguage } from "./repo-language.js";
|
|
14
|
+
|
|
15
|
+
// 이미 설치돼 있으면 Projects 동기화를 켜진 것으로 보는 기준 파일 (#716)
|
|
16
|
+
export const PROJECTS_SYNC_WORKFLOW = ".github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml";
|
|
17
|
+
|
|
18
|
+
// 타입에 성립하지 않는 축 값을 정리한다 (#498). 모바일 앱·basic 단독은 두 축이 모두 비어
|
|
19
|
+
// intent 도 none 으로 확정된다. 질문을 거친 값에도 같은 규칙을 다시 적용할 수 있게 따로 둔다.
|
|
20
|
+
export function normalizeAxes({ deploy, publish, intent, types }) {
|
|
21
|
+
const applicable = applicableTargets(types);
|
|
22
|
+
const outDeploy = deploy !== "none" && !applicable.deploy.includes(deploy) ? "none" : deploy;
|
|
23
|
+
const outPublish = publish.filter((t) => applicable.publish.includes(t));
|
|
24
|
+
const noAxes = types.length > 0 && applicable.deploy.length === 0 && applicable.publish.length === 0;
|
|
25
|
+
return { deploy: outDeploy, publish: outPublish, intent: noAxes ? "none" : intent, applicable };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// flags: parseArgs 결과 중 옵션 관련 값 (없으면 {} — 대화형 초기값)
|
|
29
|
+
// existing: parseExisting 결과 또는 null
|
|
30
|
+
// 반환: { values, sources } — sources 는 trace 가 "왜 이 값인가"를 남길 근거다 (#561).
|
|
31
|
+
export function resolveOptions({ flags = {}, existing = null, types = [], cwd = "." }) {
|
|
32
|
+
const stored = existing?.options ?? {};
|
|
33
|
+
const applicable = applicableTargets(types);
|
|
34
|
+
|
|
35
|
+
// 프로젝트 성격(#485): CLI --intent → 저장값. deploy/publish 유도의 기준.
|
|
36
|
+
let intent = flags.intent ?? stored.intent ?? null;
|
|
37
|
+
// 배포/publish 축(#439): 플래그 → 저장값 → 기본값(적용 가능할 때만 docker-ssh — #498).
|
|
38
|
+
let deploy = flags.deployTarget ?? stored.deploy
|
|
39
|
+
?? (applicable.deploy.includes("docker-ssh") ? "docker-ssh" : "none");
|
|
40
|
+
let publish = flags.publishTargets ?? stored.publish ?? [];
|
|
41
|
+
// --intent 를 줬고 해당 축 플래그가 없으면 intent 가 축을 유도한다 (#485 비대화형)
|
|
42
|
+
if (flags.intent != null) {
|
|
43
|
+
if ((intent === "library" || intent === "none") && flags.deployTarget == null) deploy = "none";
|
|
44
|
+
if ((intent === "app" || intent === "none") && flags.publishTargets == null) publish = [];
|
|
45
|
+
}
|
|
46
|
+
const before = { deploy, publish: [...publish] };
|
|
47
|
+
({ deploy, publish, intent } = normalizeAxes({ deploy, publish, intent, types }));
|
|
48
|
+
|
|
49
|
+
const values = {
|
|
50
|
+
intent, deployTarget: deploy, publishTargets: publish,
|
|
51
|
+
includeSecretBackup: flags.includeSecretBackup ?? stored.secretBackup ?? false,
|
|
52
|
+
// #566 — 설정 없이 바로 동작하므로 기본 켜짐
|
|
53
|
+
aiPrSummary: flags.aiPrSummary ?? stored.aiPrSummary ?? true,
|
|
54
|
+
// #716 — 설정(Secret, PROJECT_URL) 없는 신규 레포에 깔면 알림만 쌓인다. 이미 깔린 레포는 유지.
|
|
55
|
+
projectsSync: flags.projectsSync ?? stored.projectsSync ?? existsSync(join(cwd, PROJECTS_SYNC_WORKFLOW)),
|
|
56
|
+
// #502 — version 모드가 기존 full 통합 기록을 강등하지 않게 (full 이 우세)
|
|
57
|
+
recordMode: existing?.templateMode === "full" ? "full" : "version",
|
|
58
|
+
// #456 — 비어 있으면 version.yml 에 쓰지 않는다 (스킬이 develop 으로 폴백). 사용자가 고르거나 플래그로 준 것만 기록.
|
|
59
|
+
deployBranch: flags.deployBranch || stored.deployBranch || "",
|
|
60
|
+
// #455 — null 이 흘러 provider:"null" 로 기록되던 버그 방지
|
|
61
|
+
changelogProvider: migrateProvider(stored.changelogProvider) ?? "commit",
|
|
62
|
+
changelogBaseUrl: stored.changelogBaseUrl ?? "",
|
|
63
|
+
codeReviewCoderabbit: flags.codeReviewCoderabbit ?? stored.codeReviewCoderabbit ?? DEFAULT_CODE_REVIEW_CODERABBIT,
|
|
64
|
+
// #546, #814 — 명시적 false 는 존중, 키가 없으면 신규·기존 모두 켠다
|
|
65
|
+
semverAuto: stored.semverAuto ?? true,
|
|
66
|
+
// 마법사가 묻지 않는 값은 저장값만 보존한다 (전체 재생성이라 안 넘기면 사라진다)
|
|
67
|
+
appRelease: stored.appRelease ?? null, // #553
|
|
68
|
+
excludedWorkflows: stored.excludedWorkflows ?? null, // #810
|
|
69
|
+
storeLocales: stored.storeLocales ?? null, // #829
|
|
70
|
+
storeLocalesIos: stored.storeLocalesIos ?? null,
|
|
71
|
+
storeLocalesPlay: stored.storeLocalesPlay ?? null,
|
|
72
|
+
// #771 — 신규만 켠다. 기존 레포는 키를 만들지 않아 현행 유지
|
|
73
|
+
closeOnRelease: stored.closeOnRelease ?? (existing ? null : true),
|
|
74
|
+
// #769, #776 — 신규 en / 기존 ko. 업데이트만으로 기존 레포 문구·라벨이 바뀌지 않는다
|
|
75
|
+
language: resolveRepoLanguage({ flag: flags.language ?? null, stored: stored.language, existing: !!existing }),
|
|
76
|
+
labelStyle: resolveLabelStyle({ flag: flags.labelStyle ?? null, stored: stored.labelStyle, existing: !!existing }),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const from = (flagVal, storedVal, flagName) =>
|
|
80
|
+
flagVal != null ? `cli-flag(${flagName})` : (storedVal ? "version.yml(stored value)" : "default");
|
|
81
|
+
const sources = {
|
|
82
|
+
intent: flags.intent != null ? "cli-flag(--intent)"
|
|
83
|
+
: (stored.intent ? "version.yml(stored value)" : "not given, inferred from deploy/publish"),
|
|
84
|
+
deploy: from(flags.deployTarget, stored.deploy, "--deploy"),
|
|
85
|
+
publish: from(flags.publishTargets, stored.publish, "--publish"),
|
|
86
|
+
before, applicable,
|
|
87
|
+
// 기존 레포에서 semver_auto 가 이번에 처음 켜졌는지 — 완료 화면이 알린다
|
|
88
|
+
semverAutoNewlyOn: !!existing && stored.semverAuto == null,
|
|
89
|
+
};
|
|
90
|
+
return { values, sources };
|
|
91
|
+
}
|
package/src/core/version-yml.js
CHANGED
|
@@ -117,6 +117,7 @@ const COMMENTS = {
|
|
|
117
117
|
deploy: "deploy settings remembered by the wizard (non-sensitive, safe to edit by hand)",
|
|
118
118
|
excluded: "workflow files the update must never install (safe to edit by hand)",
|
|
119
119
|
storeLocales: "store release-note languages, first is the default (missing = one text for every store language)",
|
|
120
|
+
storeLocalesStore: "same, only for this store when its languages differ from store_locales (optional)",
|
|
120
121
|
},
|
|
121
122
|
ko: {
|
|
122
123
|
versionCode: "app build number",
|
|
@@ -133,6 +134,7 @@ const COMMENTS = {
|
|
|
133
134
|
deploy: "마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)",
|
|
134
135
|
excluded: "업데이트가 절대 설치하지 않을 워크플로우 파일 (직접 수정 가능)",
|
|
135
136
|
storeLocales: "스토어 릴리스 노트 언어 목록, 첫 항목이 기본 언어 (미기재면 모든 스토어 언어에 같은 문구)",
|
|
137
|
+
storeLocalesStore: "이 스토어만 언어 구성이 store_locales 와 다를 때 쓰는 선택 키",
|
|
136
138
|
},
|
|
137
139
|
};
|
|
138
140
|
const commentsFor = (lang) => COMMENTS[lang === "ko" ? "ko" : "en"];
|
|
@@ -161,7 +163,7 @@ export function resolveUpdatedBy(existingContent = "", cwd = ".") {
|
|
|
161
163
|
export function parseTemplateOptions(content) {
|
|
162
164
|
const out = { deploy: null, publish: null, secretBackup: null,
|
|
163
165
|
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null, aiPrSummary: null, projectsSync: null,
|
|
164
|
-
deployBranch: null, intent: null, semverAuto: null, appRelease: null, labelStyle: null, closeOnRelease: null, language: null, excludedWorkflows: null, storeLocales: null };
|
|
166
|
+
deployBranch: null, intent: null, semverAuto: null, appRelease: null, labelStyle: null, closeOnRelease: null, language: null, excludedWorkflows: null, storeLocales: null, storeLocalesIos: null, storeLocalesPlay: null };
|
|
165
167
|
// deploy_branch는 metadata 직속(#456) — template.options 밖이라 별도로 스캔한다.
|
|
166
168
|
for (const line of String(content || "").split("\n")) {
|
|
167
169
|
if (line.startsWith("#")) continue;
|
|
@@ -275,6 +277,12 @@ export function parseTemplateOptions(content) {
|
|
|
275
277
|
out.storeLocales = m[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
276
278
|
continue;
|
|
277
279
|
}
|
|
280
|
+
// 스토어마다 지원 언어가 다를 때만 쓰는 선택 키 — 있으면 그 스토어는 공통 store_locales 대신 이것을 쓴다.
|
|
281
|
+
m = line.match(/^\s+store_locales_(ios|play):\s*\[([^\]]*)\]/);
|
|
282
|
+
if (m) {
|
|
283
|
+
out[m[1] === "ios" ? "storeLocalesIos" : "storeLocalesPlay"] = m[2].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
278
286
|
// 프로젝트 성격(#553) — 앱 심사로 이어지는 레포인가. 워크플로우와 스킬이 같은 값을 본다.
|
|
279
287
|
m = line.match(/^\s+app_release:\s*["']?(true|false)["']?/);
|
|
280
288
|
if (m) {
|
|
@@ -559,7 +567,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
559
567
|
if (templateOptions) {
|
|
560
568
|
const { templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, optionsDate = today,
|
|
561
569
|
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true, aiPrSummary = true, intent = null, mode = null,
|
|
562
|
-
semverAuto = true, appRelease = null, labelStyle = null, closeOnRelease = null, projectsSync = null, language = null, excludedWorkflows = null, storeLocales = null } = templateOptions;
|
|
570
|
+
semverAuto = true, appRelease = null, labelStyle = null, closeOnRelease = null, projectsSync = null, language = null, excludedWorkflows = null, storeLocales = null, storeLocalesIos = null, storeLocalesPlay = null } = templateOptions;
|
|
563
571
|
const publishJson = `[${publishTargets.map((t) => `"${t}"`).join(",")}]`;
|
|
564
572
|
// intent(프로젝트 성격, #485) — 미지정이면 deploy/publish에서 역추론해 기록 (재통합 시 진입 질문 생략용)
|
|
565
573
|
const intentVal = intent || inferIntent(deployTarget, publishTargets) || "manual";
|
|
@@ -587,6 +595,8 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
587
595
|
if (excludedWorkflows && excludedWorkflows.length) out += ` excluded_workflows: [${excludedWorkflows.map((f) => `"${f}"`).join(", ")}] # ${C.excluded}\n`;
|
|
588
596
|
// 스토어 릴리스 노트 언어(#829) — 미지정이면 키를 쓰지 않는다. 전체 재생성이라 다시 쓰지 않으면 사용자가 적은 값이 사라진다.
|
|
589
597
|
if (storeLocales && storeLocales.length) out += ` store_locales: [${storeLocales.map((l) => `"${l}"`).join(", ")}] # ${C.storeLocales}\n`;
|
|
598
|
+
if (storeLocalesIos && storeLocalesIos.length) out += ` store_locales_ios: [${storeLocalesIos.map((l) => `"${l}"`).join(", ")}] # App Store: ${C.storeLocalesStore}\n`;
|
|
599
|
+
if (storeLocalesPlay && storeLocalesPlay.length) out += ` store_locales_play: [${storeLocalesPlay.map((l) => `"${l}"`).join(", ")}] # Play: ${C.storeLocalesStore}\n`;
|
|
590
600
|
if (language) out += ` language: ${language} # ${C.language}\n`;
|
|
591
601
|
if (labelStyle) out += ` label_style: ${labelStyle} # ${C.labelStyle}\n`;
|
|
592
602
|
// 앱 심사 배포 레포 여부(#553) — 미지정이면 키를 쓰지 않는다(기존 레포 무변화).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 선택형 워크플로 그룹의 켜짐/꺼짐을 정하는 단 한 곳 (#851).
|
|
2
|
+
//
|
|
3
|
+
// 예전에는 복사(copy/workflows.js), 충돌 목록(listWorkflowConflicts), 고아 감지(orphan-workflows.js)가
|
|
4
|
+
// 같은 조건을 각자 들고 있었다. 한쪽만 고치면 "켰는데 안 깔림" 또는 "껐는데 계속 돎"이 된다 (#567).
|
|
5
|
+
// 새 조건부 폴더를 만들면 여기에 한 줄을 넣는다. 세 곳이 함께 따라온다.
|
|
6
|
+
import { readdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { exists } from "./fsutil.js";
|
|
9
|
+
|
|
10
|
+
// 서버 배포(<type>/server-deploy/)는 docker-ssh 일 때만 깐다 (#439)
|
|
11
|
+
export const SERVER_DEPLOY_DIR = "server-deploy";
|
|
12
|
+
export function serverDeployEnabled(deployTarget) {
|
|
13
|
+
return (deployTarget || "docker-ssh") === "docker-ssh";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// common/ 아래 선택형 폴더 목록. 순서가 복사 순서다 (기록·요약에 나오는 순서를 바꾸지 않는다).
|
|
17
|
+
// policy: "bak-replace" — 템플릿과 다르면 .bak 으로 남기고 교체
|
|
18
|
+
// "new-only" — 이미 있으면 건드리지 않는다 (secret-backup: 서버 경로를 사용자가 고쳐 쓴다)
|
|
19
|
+
// copyLabel / orphanLabel — trace 그룹 이름과 고아 안내 이름. 기존 기록과 같게 둔다.
|
|
20
|
+
export function commonOptionalGroups(commonDir, opts = {}) {
|
|
21
|
+
const { includeSecretBackup = false, aiPrSummary = true, projectsSync = true, deployTarget = "docker-ssh" } = opts;
|
|
22
|
+
const selectedDeploy = deployTarget || "docker-ssh";
|
|
23
|
+
const groups = [];
|
|
24
|
+
|
|
25
|
+
// deploy/<target> — 타입 비종속 배포 타겟(vercel 등). 고른 타겟 폴더만 켠다.
|
|
26
|
+
const deployRoot = join(commonDir, "deploy");
|
|
27
|
+
const deployNames = exists(deployRoot)
|
|
28
|
+
? readdirSync(deployRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
|
|
29
|
+
: [];
|
|
30
|
+
if (!deployNames.includes(selectedDeploy)) deployNames.push(selectedDeploy); // 폴더가 없으면 아래에서 exists 로 걸러진다
|
|
31
|
+
for (const name of deployNames) {
|
|
32
|
+
groups.push({ dir: join("deploy", name), enabled: name === selectedDeploy, policy: "bak-replace",
|
|
33
|
+
copyLabel: "common-deploy", orphanLabel: `deploy/${name}` });
|
|
34
|
+
}
|
|
35
|
+
groups.push(
|
|
36
|
+
// #566 — AI 변경 요약. 설정 없이 바로 동작
|
|
37
|
+
{ dir: "pr-summary", enabled: aiPrSummary === true, policy: "bak-replace", copyLabel: "pr-summary", orphanLabel: "pr-summary" },
|
|
38
|
+
// #716 — Projects 보드 동기화. Secret·변수를 등록해야 동작해 신규는 기본 꺼짐
|
|
39
|
+
{ dir: "projects-sync", enabled: projectsSync === true, policy: "bak-replace", copyLabel: "projects-sync", orphanLabel: "projects-sync" },
|
|
40
|
+
// Secret 서버 백업
|
|
41
|
+
{ dir: "secret-backup", enabled: includeSecretBackup === true, policy: "new-only", copyLabel: "secret-backup", orphanLabel: "secret-backup" },
|
|
42
|
+
);
|
|
43
|
+
return groups;
|
|
44
|
+
}
|
package/src/index.js
CHANGED
|
@@ -19,16 +19,13 @@ import { detectOrphanWorkflows } from "./core/orphan-workflows.js";
|
|
|
19
19
|
import { createRunTrace, MIGRATION_DIR } from "./core/run-trace.js";
|
|
20
20
|
import { appendGuideEntry } from "./core/migration-guide.js";
|
|
21
21
|
import { resolveProjectPaths, markerForType } from "./core/paths-resolve.js";
|
|
22
|
-
import {
|
|
23
|
-
import { DEFAULT_CODE_REVIEW_CODERABBIT } from "./core/constants.js";
|
|
22
|
+
import { resolveOptions } from "./core/resolve-options.js";
|
|
24
23
|
import { printBannerCompact } from "./ui/banner.js";
|
|
25
24
|
import { printSummary } from "./ui/summary.js";
|
|
26
25
|
import { runFull } from "./commands/full.js";
|
|
27
26
|
import { runVersion } from "./commands/version.js";
|
|
28
27
|
import { runWorkflows } from "./commands/workflows.js";
|
|
29
28
|
import { runIssues } from "./commands/issues.js";
|
|
30
|
-
import { resolveLabelStyle } from "./core/label-style.js";
|
|
31
|
-
import { resolveRepoLanguage } from "./core/repo-language.js";
|
|
32
29
|
import { runInteractive } from "./commands/interactive.js";
|
|
33
30
|
import { runSkills } from "./commands/skills.js";
|
|
34
31
|
|
|
@@ -191,84 +188,38 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
191
188
|
const { now, today } = clock || utcNow();
|
|
192
189
|
const tempDir = join(cwd, PATHS.tempDir);
|
|
193
190
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
// 적용 불가 타겟 조용한 정리 (#498) — 대화형과 동일 규칙. 타입에 성립하지 않는 축 값은
|
|
208
|
-
// 복사 결과가 동일하므로 경고 없이 none/교집합으로 정리한다 (모바일 앱/basic 단독 등).
|
|
209
|
-
const beforeCleanup = { deploy: deployTarget, publish: [...publishTargets] };
|
|
210
|
-
if (deployTarget !== "none" && !applicable.deploy.includes(deployTarget)) deployTarget = "none";
|
|
211
|
-
publishTargets = publishTargets.filter((t) => applicable.publish.includes(t));
|
|
212
|
-
if (types.length > 0 && applicable.deploy.length === 0 && applicable.publish.length === 0) intent = "none";
|
|
191
|
+
// 옵션 값은 resolve-options.js 한 곳에서 정한다 (#851) — 대화형과 같은 규칙.
|
|
192
|
+
const { values: resolved, sources } = resolveOptions({
|
|
193
|
+
flags: {
|
|
194
|
+
intent: opts.intent, deployTarget: opts.deployTarget, publishTargets: opts.publishTargets,
|
|
195
|
+
includeSecretBackup: opts.includeSecretBackup, aiPrSummary: opts.aiPrSummary, projectsSync: opts.projectsSync,
|
|
196
|
+
deployBranch: opts.deployBranch, codeReviewCoderabbit: opts.codeReviewCoderabbit,
|
|
197
|
+
language: opts.language, labelStyle: opts.labelStyle,
|
|
198
|
+
},
|
|
199
|
+
existing, types, cwd,
|
|
200
|
+
});
|
|
201
|
+
const { intent, deployTarget, publishTargets } = resolved;
|
|
202
|
+
const { before: beforeCleanup, applicable } = sources;
|
|
213
203
|
|
|
214
204
|
// 축 확정 근거 (#561) — "왜 이 값인가"가 가장 헷갈리는 자리다.
|
|
215
205
|
// CLI 플래그 / 저장값 / intent 유도 / 타입 적용성 정리 중 무엇이 이겼는지 남긴다.
|
|
216
|
-
trace.event("resolve", "intent", String(intent ?? "(unset)"), {
|
|
217
|
-
source: opts.intent != null ? "cli-flag(--intent)"
|
|
218
|
-
: (existing?.options?.intent ? "version.yml(stored value)" : "not given, inferred from deploy/publish"),
|
|
219
|
-
});
|
|
206
|
+
trace.event("resolve", "intent", String(intent ?? "(unset)"), { source: sources.intent });
|
|
220
207
|
trace.event("resolve", "deploy", deployTarget, {
|
|
221
|
-
source:
|
|
222
|
-
: (existing?.options?.deploy ? "version.yml(stored value)" : "default"),
|
|
208
|
+
source: sources.deploy,
|
|
223
209
|
applicableForTypes: applicable.deploy,
|
|
224
210
|
adjusted: beforeCleanup.deploy !== deployTarget
|
|
225
211
|
? `${beforeCleanup.deploy} → ${deployTarget} (not applicable to the selected types)` : null,
|
|
226
212
|
});
|
|
227
213
|
trace.event("resolve", "publish", publishTargets.join(",") || "(none)", {
|
|
228
|
-
source:
|
|
229
|
-
: (existing?.options?.publish ? "version.yml(stored value)" : "default"),
|
|
214
|
+
source: sources.publish,
|
|
230
215
|
applicableForTypes: applicable.publish,
|
|
231
216
|
adjusted: beforeCleanup.publish.join(",") !== publishTargets.join(",")
|
|
232
217
|
? `${beforeCleanup.publish.join(",") || "(none)"} → ${publishTargets.join(",") || "(none)"} (cleaned up, not applicable)` : null,
|
|
233
218
|
});
|
|
234
219
|
|
|
235
220
|
const context = createContext({
|
|
236
|
-
mode: opts.mode, force: true, types, version, versionCode, branch,
|
|
237
|
-
|
|
238
|
-
deployTarget,
|
|
239
|
-
publishTargets,
|
|
240
|
-
includeSecretBackup: opts.includeSecretBackup ?? existing?.options?.secretBackup ?? false,
|
|
241
|
-
// #566 — CLI 값 우선, 없으면 저장값 보존, 그것도 없으면 true(설정 없이 바로 동작).
|
|
242
|
-
aiPrSummary: opts.aiPrSummary ?? existing?.options?.aiPrSummary ?? true,
|
|
243
|
-
// Projects 보드 동기화(#716): CLI > 저장값 > 이미 설치돼 있으면 유지 > 신규는 제외.
|
|
244
|
-
// 설정(Secret, PROJECT_URL)이 없는 레포에 설치되면 라벨이 바뀔 때마다 알림만 쌓이기 때문이다.
|
|
245
|
-
// 이미 설치된 레포가 갱신만으로 연동을 잃지 않도록 파일이 있으면 켜진 것으로 본다.
|
|
246
|
-
projectsSync: opts.projectsSync ?? existing?.options?.projectsSync
|
|
247
|
-
?? existsSync(join(cwd, ".github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml")),
|
|
248
|
-
// #502 — version 모드가 기존 full 통합 기록(mode)을 강등하지 않도록 (full이 우세)
|
|
249
|
-
recordMode: existing?.templateMode === "full" ? "full" : "version",
|
|
250
|
-
// 릴리스 배포 브랜치(#456): CLI 플래그 → version.yml 저장값 → 빈 값(미출력, 스킬이 develop 폴백)
|
|
251
|
-
deployBranch: opts.deployBranch || existing?.options?.deployBranch || "",
|
|
252
|
-
intent,
|
|
253
|
-
// changelog/code_review 축(#455): 비대화형은 저장값 → 기본값. null이 흘러 provider:"null"로 기록되던 버그 수정.
|
|
254
|
-
changelogProvider: migrateProvider(existing?.options?.changelogProvider) ?? "commit",
|
|
255
|
-
changelogBaseUrl: existing?.options?.changelogBaseUrl ?? "",
|
|
256
|
-
// 플래그 > 저장값 > 기본(꺼짐). 쓰겠다고 명시하면 켜지고, 아무 말이 없을 때만 기본값이 적용된다.
|
|
257
|
-
codeReviewCoderabbit: opts.codeReviewCoderabbit ?? existing?.options?.codeReviewCoderabbit ?? DEFAULT_CODE_REVIEW_CODERABBIT,
|
|
258
|
-
// semver 자동 승격(#546, #814): 저장값(명시적 false 포함)은 존중하고, 키가 없으면 신규·기존 모두 켠다.
|
|
259
|
-
// 기존 레포에서 처음 켜질 때는 버전이 예고 없이 오르지 않게 완료 화면이 알린다 (semverAutoNewlyOn).
|
|
260
|
-
semverAuto: existing?.options?.semverAuto ?? true,
|
|
261
|
-
// 앱 심사 배포 레포 여부(#553): 저장값만 보존한다. 마법사가 묻지 않으므로 새로 켜지 않는다
|
|
262
|
-
// (사용자가 version.yml에 직접 쓰거나 스킬이 기록한 값을 그대로 유지).
|
|
263
|
-
appRelease: existing?.options?.appRelease ?? null,
|
|
264
|
-
excludedWorkflows: existing?.options?.excludedWorkflows ?? null, // #810 저장값 보존
|
|
265
|
-
storeLocales: existing?.options?.storeLocales ?? null, // #829 저장값 보존 (전체 재생성이라 안 넘기면 사라진다)
|
|
266
|
-
// 상태 라벨 표기(#776): 플래그 > 저장값 > (신규 en / 기존 ko). 기존 레포의 라벨 이름은 업데이트만으로 바뀌지 않는다.
|
|
267
|
-
// 릴리스 시 완료 이슈 닫기(#771): 저장값 보존, 없으면 신규만 true. 기존 레포는 키를 만들지 않아 현행 유지.
|
|
268
|
-
closeOnRelease: existing?.options?.closeOnRelease ?? (existing ? null : true),
|
|
269
|
-
// 레포 문구 언어(#769): 플래그 > 저장값 > (신규 en / 기존 ko). 기존 레포의 템플릿 언어는 업데이트만으로 바뀌지 않는다.
|
|
270
|
-
language: resolveRepoLanguage({ flag: opts.language, stored: existing?.options?.language, existing: !!existing }),
|
|
271
|
-
labelStyle: resolveLabelStyle({ flag: opts.labelStyle, stored: existing?.options?.labelStyle, existing: !!existing }),
|
|
221
|
+
mode: opts.mode, force: true, types, version, versionCode, branch, paths,
|
|
222
|
+
...resolved,
|
|
272
223
|
repoName,
|
|
273
224
|
// 실 resolver 4종 (.sh resolve_token 등가 — spring-app-yml 스텁 제거)
|
|
274
225
|
resolvers: makeResolvers(cwd, repoName, paths),
|
|
@@ -364,7 +315,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
364
315
|
migrationGuidePath = appendGuideEntry(cwd, {
|
|
365
316
|
now, mode: opts.mode, types, repoName,
|
|
366
317
|
templateFrom: existing?.templateVersion || "", templateTo: context.templateVersion,
|
|
367
|
-
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, storeLocales: context.storeLocales },
|
|
318
|
+
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, storeLocales: context.storeLocales, storeLocalesIos: context.storeLocalesIos, storeLocalesPlay: context.storeLocalesPlay },
|
|
368
319
|
branches: { defaultBranch: branch, deployBranch: context.deployBranch || "develop", ready: null, created: null },
|
|
369
320
|
breaking: breakingReport, migrations: migrationsResult, orphans: { cleaned: [], pending: orphanPending },
|
|
370
321
|
events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
|
|
@@ -380,7 +331,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
380
331
|
replacedBak: result?.workflows?.replacedBak ?? [], // #673 기준점 없이 교체한 파일 안내
|
|
381
332
|
legacyLeftover: migrationsResult?.confirmPending ?? [], // #809 신·구 세대 동시 실행 안내
|
|
382
333
|
notInstalled: result?.workflows?.notInstalled ?? [], // #810 깔지 않은 워크플로우 안내
|
|
383
|
-
semverAutoNewlyOn:
|
|
334
|
+
semverAutoNewlyOn: sources.semverAutoNewlyOn && context.semverAuto === true, // 업데이트가 켠 사실 안내
|
|
384
335
|
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
385
336
|
// #569 — 고른 것만 안내하려면 선택값이 필요하다
|
|
386
337
|
aiPrSummary: context.aiPrSummary, codeReviewCoderabbit: context.codeReviewCoderabbit,
|