projectops 4.2.15 → 4.2.16
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 +1 -1
- package/package.json +1 -1
- package/src/commands/interactive.js +4 -1
- package/src/core/copy/workflows.js +13 -0
- package/src/core/options-ask.js +19 -9
- package/src/core/wizard-env.js +11 -4
- package/src/ui/env-plan.js +10 -6
- package/src/ui/summary.js +8 -2
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -89,6 +89,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
89
89
|
let changelogProvider = existing?.options?.changelogProvider ?? "github-ai";
|
|
90
90
|
let changelogBaseUrl = existing?.options?.changelogBaseUrl ?? "";
|
|
91
91
|
let deployBranch = existing?.options?.deployBranch ?? "develop"; // #456
|
|
92
|
+
let deployBranchReady = null; // #490 — 이번 실행에서 개발 브랜치 존재/생성이 확인됐는지
|
|
92
93
|
let intent = existing?.options?.intent ?? null; // #485 프로젝트 성격
|
|
93
94
|
const showOptional = mode === "full" || mode === "workflows";
|
|
94
95
|
const realTty = process.stdout.isTTY === true;
|
|
@@ -123,6 +124,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
123
124
|
changelogProvider = r.changelogProvider;
|
|
124
125
|
changelogBaseUrl = r.changelogBaseUrl;
|
|
125
126
|
deployBranch = r.deployBranch;
|
|
127
|
+
deployBranchReady = r.deployBranchReady ?? deployBranchReady; // #490
|
|
126
128
|
intent = r.intent;
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -186,6 +188,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
186
188
|
changelogProvider = r.changelogProvider;
|
|
187
189
|
changelogBaseUrl = r.changelogBaseUrl;
|
|
188
190
|
deployBranch = r.deployBranch;
|
|
191
|
+
deployBranchReady = r.deployBranchReady ?? deployBranchReady; // #490
|
|
189
192
|
intent = r.intent;
|
|
190
193
|
}
|
|
191
194
|
}
|
|
@@ -285,7 +288,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
285
288
|
|
|
286
289
|
// 완료 요약 (.sh print_summary L5438)
|
|
287
290
|
io.summary?.({
|
|
288
|
-
mode, types, version, deployBranch,
|
|
291
|
+
mode, types, version, deployBranch, deployBranchReady,
|
|
289
292
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
290
293
|
}, cwd);
|
|
291
294
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
@@ -18,6 +18,17 @@ function configureEnv(targetPath, { type, projectPath = ".", repoName = "", reso
|
|
|
18
18
|
writeFileSync(targetPath, out);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// util 버전 동기화 워크플로우 (#491) — .github/util/ 모듈(version.json)이 있는 레포에서만 의미가 있다.
|
|
22
|
+
// util이 없는 레포(예: spring 단독)에 복사하면 트리거가 영원히 안 걸리는 no-op 오염이 된다.
|
|
23
|
+
const UTIL_VERSION_SYNC = "PROJECT-COMMON-TEMPLATE-UTIL-VERSION-SYNC.yml";
|
|
24
|
+
|
|
25
|
+
// 이 통합에서 util 모듈이 존재하게 되는가 — 대상에 이미 있거나(업데이트),
|
|
26
|
+
// 선택 타입의 util이 템플릿에 있어 곧 복사될 예정(runFull 6단계)이면 true.
|
|
27
|
+
function utilSyncApplies(tempDir, targetRoot, types) {
|
|
28
|
+
if (exists(join(targetRoot, ".github", "util"))) return true;
|
|
29
|
+
return types.some((t) => exists(join(tempDir, ".github", "util", t)));
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
// 3분류 (신규/unchanged/changed) — 대상 워크플로우 디렉토리 기준.
|
|
22
33
|
function classify(srcDir, workflowsDir, envOpts) {
|
|
23
34
|
const result = { newFiles: [], unchanged: [], changed: [] };
|
|
@@ -62,6 +73,8 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
62
73
|
const commonDir = join(projectTypesDir, "common");
|
|
63
74
|
if (exists(commonDir)) {
|
|
64
75
|
for (const filename of listYamlFiles(commonDir)) {
|
|
76
|
+
// #491 — util 동기화 워크플로우는 util 모듈이 있(게 되)는 레포에만 복사
|
|
77
|
+
if (filename === UTIL_VERSION_SYNC && !utilSyncApplies(tempDir, targetRoot, types)) continue;
|
|
65
78
|
const src = join(commonDir, filename);
|
|
66
79
|
const dst = join(workflowsDir, filename);
|
|
67
80
|
if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOptsFor("common"))) {
|
package/src/core/options-ask.js
CHANGED
|
@@ -16,34 +16,39 @@ import { branchStatus, createBranch, pushBranch } from "./git-branch.js";
|
|
|
16
16
|
// 개발(배포) 브랜치 존재 확인 + 생성 제안 (#477) — 대화형 전용.
|
|
17
17
|
// 로컬에 없고 원격에도 없(거나 불명이)면 기본 브랜치에서 생성을 제안하고, 생성 후 push 여부도 묻는다.
|
|
18
18
|
// git 미설치·비레포·질문 불가(io.confirm 없음)면 조용히 통과 — 마법사 진행을 막지 않는다.
|
|
19
|
+
// 반환 ready (#490 — 완료 요약이 "브랜치 만들어라" 재지시를 접을지 판단):
|
|
20
|
+
// true = 브랜치가 이미 있거나 이번 실행에서 생성함 → 요약에서 생성 안내 불필요
|
|
21
|
+
// false = 없는데 사용자가 거절/생성 실패 → 안내 유지
|
|
22
|
+
// null = 확인 자체를 못 함(비레포·질문 불가) → 안내 유지(보수적)
|
|
19
23
|
export async function ensureDeployBranch({ targetRoot = ".", deployBranch = "", defaultBranch = "", io = {}, say = () => {} }) {
|
|
20
|
-
if (!deployBranch || typeof io.confirm !== "function") return { created: false, pushed: false };
|
|
24
|
+
if (!deployBranch || typeof io.confirm !== "function") return { created: false, pushed: false, ready: null };
|
|
21
25
|
const st = branchStatus(targetRoot, deployBranch);
|
|
22
|
-
if (!st.isRepo
|
|
26
|
+
if (!st.isRepo) return { created: false, pushed: false, ready: null };
|
|
27
|
+
if (st.local) return { created: false, pushed: false, ready: true };
|
|
23
28
|
if (st.remote === true) {
|
|
24
29
|
say(`ℹ️ '${deployBranch}' 브랜치가 원격에는 있고 로컬에 없습니다 — 필요 시 'git switch ${deployBranch}'로 가져오세요.`);
|
|
25
|
-
return { created: false, pushed: false };
|
|
30
|
+
return { created: false, pushed: false, ready: true };
|
|
26
31
|
}
|
|
27
32
|
say(`⚠️ 개발(릴리스 소스) 브랜치 '${deployBranch}'가 없습니다 — 릴리스(${deployBranch}→${defaultBranch || "기본 브랜치"} PR)가 동작하려면 필요합니다.`);
|
|
28
33
|
const mk = await io.confirm({ message: `${defaultBranch || "현재"} 브랜치에서 '${deployBranch}' 브랜치를 만들까요?`, initialValue: true });
|
|
29
34
|
if (mk !== true) {
|
|
30
35
|
say(`→ 건너뜁니다. 나중에 직접: git checkout -b ${deployBranch} && git push -u origin ${deployBranch}`);
|
|
31
|
-
return { created: false, pushed: false };
|
|
36
|
+
return { created: false, pushed: false, ready: false };
|
|
32
37
|
}
|
|
33
38
|
if (!createBranch(targetRoot, deployBranch, defaultBranch)) {
|
|
34
39
|
say(`⚠️ 브랜치 생성 실패 — 직접 실행해주세요: git branch ${deployBranch}${defaultBranch ? ` ${defaultBranch}` : ""}`);
|
|
35
|
-
return { created: false, pushed: false };
|
|
40
|
+
return { created: false, pushed: false, ready: false };
|
|
36
41
|
}
|
|
37
42
|
say(`✅ 로컬 브랜치 '${deployBranch}' 생성 완료 (checkout은 하지 않았습니다)`);
|
|
38
43
|
// #481 — push 질문에 브랜치명 명시 ("어느 브랜치를 push하는지" 불명확 방지)
|
|
39
44
|
const up = await io.confirm({ message: `원격(origin)에 '${deployBranch}' 브랜치도 push할까요?`, initialValue: true });
|
|
40
|
-
if (up !== true) return { created: true, pushed: false };
|
|
45
|
+
if (up !== true) return { created: true, pushed: false, ready: true };
|
|
41
46
|
if (pushBranch(targetRoot, deployBranch)) {
|
|
42
47
|
say(`✅ origin/${deployBranch} push 완료`);
|
|
43
|
-
return { created: true, pushed: true };
|
|
48
|
+
return { created: true, pushed: true, ready: true };
|
|
44
49
|
}
|
|
45
50
|
say(`⚠️ push 실패(자격/네트워크) — 직접 실행해주세요: git push -u origin ${deployBranch}`);
|
|
46
|
-
return { created: true, pushed: false };
|
|
51
|
+
return { created: true, pushed: false, ready: true };
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
// 재노출 — 파서 본체는 version-yml.js에 있다 (순환 import 방지: options-ask → version-yml 방향만 허용)
|
|
@@ -115,6 +120,7 @@ export async function askAllOptionalWorkflows({
|
|
|
115
120
|
let changelogProvider = current.changelogProvider ?? null;
|
|
116
121
|
let changelogBaseUrl = current.changelogBaseUrl ?? null;
|
|
117
122
|
let deployBranch = current.deployBranch ?? null; // #456 릴리스 PR head 브랜치
|
|
123
|
+
let deployBranchReady = null; // #490 — 이번 실행에서 브랜치 존재/생성이 확인됐는지 (null=확인 안 함)
|
|
118
124
|
let intent = current.intent ?? null; // #485 프로젝트 성격 (app/library/both/none/manual)
|
|
119
125
|
|
|
120
126
|
// basic 단독 타입은 서버 배포도 라이브러리 publish도 개념상 성립하지 않는다.
|
|
@@ -328,7 +334,9 @@ export async function askAllOptionalWorkflows({
|
|
|
328
334
|
deployBranch = (typeof ans === "string" && !isCancel(ans) && ans.trim()) ? ans.trim() : (deployBranch ?? "develop");
|
|
329
335
|
say(`개발(릴리스 소스) 브랜치: ${deployBranch}`);
|
|
330
336
|
// 브랜치 존재 확인 + 생성 제안 (#477) — 없으면 릴리스 파이프라인이 조용히 놀게 된다
|
|
331
|
-
|
|
337
|
+
// #490 — 결과(ready)를 완료 요약에 전달해 이미 생성/확인한 브랜치를 재지시하지 않는다
|
|
338
|
+
const br = await ensureDeployBranch({ targetRoot, deployBranch, defaultBranch, io, say });
|
|
339
|
+
deployBranchReady = br.ready;
|
|
332
340
|
}
|
|
333
341
|
}
|
|
334
342
|
|
|
@@ -349,6 +357,8 @@ export async function askAllOptionalWorkflows({
|
|
|
349
357
|
changelogProvider: changelogProvider ?? "github-ai",
|
|
350
358
|
changelogBaseUrl: changelogBaseUrl ?? "",
|
|
351
359
|
deployBranch: deployBranch ?? "develop",
|
|
360
|
+
deployBranchReady, // #490 — true=존재/생성 확인됨, false=거절/실패, null=확인 안 함
|
|
361
|
+
|
|
352
362
|
// #485 intent — 확정값 우선, 없으면 최종 deploy/publish에서 역추론(basic·비대화형 경로 보정)
|
|
353
363
|
intent: intent ?? inferIntent(finalDeploy, finalPublish) ?? "manual",
|
|
354
364
|
};
|
package/src/core/wizard-env.js
CHANGED
|
@@ -41,6 +41,13 @@ export function resolveToken(name, type, resolvers = {}) {
|
|
|
41
41
|
return typeof fn === "function" ? (fn(type) ?? "") : "";
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// 잔여 전역 토큰 치환 (.sh 3347~3351) — 파일 본문·수집값(#489)·카드 표시가 전부 같은 규칙을 쓴다.
|
|
45
|
+
// 파일에 실제 써지는 값과 version.yml deploy 블록 기억값이 어긋나지 않게 하는 단일 지점.
|
|
46
|
+
export function resolveGlobalTokens(s, repoName = "") {
|
|
47
|
+
if (typeof s !== "string" || (!s.includes("__PROJECT_NAME__") && !s.includes("__APP_ARTIFACT_NAME__"))) return s;
|
|
48
|
+
return s.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
// 파일 전체 치환 (configure_workflow_env 등가).
|
|
45
52
|
// content: 원본 워크플로우 텍스트. 반환: 치환된 텍스트.
|
|
46
53
|
// opts:
|
|
@@ -70,16 +77,16 @@ export function substituteEnv(content, opts = {}) {
|
|
|
70
77
|
if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
|
|
71
78
|
else val = def;
|
|
72
79
|
// ask 키만 수집 (.sh wf_deploy_set — auto는 저장 안 함). deploy 블록용.
|
|
73
|
-
|
|
80
|
+
// #489 — 파일 본문은 아래 전역 토큰 치환을 거치므로 수집값도 동일 치환해
|
|
81
|
+
// version.yml deploy 블록이 설치본과 항상 바이트 일치하게 한다.
|
|
82
|
+
if (collectAsks) collectAsks.set(p.key, resolveGlobalTokens(val, repoName));
|
|
74
83
|
}
|
|
75
84
|
lines[i] = setEnvLine(lines[i], p.key, val);
|
|
76
85
|
}
|
|
77
86
|
let out = lines.join(usesCRLF ? "\r\n" : "\n");
|
|
78
87
|
|
|
79
88
|
// 잔여 전역 토큰 (.sh 3347~3351)
|
|
80
|
-
|
|
81
|
-
out = out.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
|
|
82
|
-
}
|
|
89
|
+
out = resolveGlobalTokens(out, repoName);
|
|
83
90
|
|
|
84
91
|
// paths-anchor (.sh 3353~3360): 경로가 '.'이 아니면 주석 라인 전체를 paths 라인으로 교체
|
|
85
92
|
if (PATHS_ANCHOR_RE.test(out) && projectPath && projectPath !== ".") {
|
package/src/ui/env-plan.js
CHANGED
|
@@ -8,7 +8,7 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { stdin, stderr } from "node:process";
|
|
9
9
|
import { PATHS } from "../core/paths.js";
|
|
10
10
|
import { exists, listYamlFiles } from "../core/fsutil.js";
|
|
11
|
-
import { parseWizardLine, resolveToken } from "../core/wizard-env.js";
|
|
11
|
+
import { parseWizardLine, resolveToken, resolveGlobalTokens } from "../core/wizard-env.js";
|
|
12
12
|
import { loadWizardPrompts, wfField, workflowDisplayName } from "../core/wizard-labels.js";
|
|
13
13
|
import * as engine from "./readline-engine.js";
|
|
14
14
|
|
|
@@ -39,7 +39,7 @@ export function scopeString(usages = []) {
|
|
|
39
39
|
// 반환: { keys:[], defaults:Map<key,default>, typeDefaults:Map<"type|key",default>,
|
|
40
40
|
// usages:Map<key,[{type,workflowName}]> }
|
|
41
41
|
export function collectAsks(tempDir, types = [], opts = {}) {
|
|
42
|
-
const { resolvers = {}, deployTarget = "docker-ssh", publishTargets = [], prompts = null } = opts;
|
|
42
|
+
const { resolvers = {}, deployTarget = "docker-ssh", publishTargets = [], prompts = null, repoName = "" } = opts;
|
|
43
43
|
const baseDir = join(tempDir, PATHS.workflowsDir, PATHS.projectTypesDir);
|
|
44
44
|
const keys = [];
|
|
45
45
|
const defaults = new Map();
|
|
@@ -64,9 +64,13 @@ export function collectAsks(tempDir, types = [], opts = {}) {
|
|
|
64
64
|
const p = parseWizardLine(line); // KEY 정규식 [A-Z_]+ (.sh와 동일)
|
|
65
65
|
if (!p || p.action !== "ask") continue;
|
|
66
66
|
// 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴 (.sh _type_default 등가)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// #489 — __PROJECT_NAME__ 등 전역 토큰은 카드 표시 전에 미리 해석한다.
|
|
68
|
+
// 설치본(파일 복사 시 전역 치환)과 동일한 값을 보여주지 않으면
|
|
69
|
+
// 사용자가 "리터럴이 그대로 박히나?" 오해하게 된다.
|
|
70
|
+
const typeDefault = resolveGlobalTokens(
|
|
71
|
+
p.arg.startsWith("@") ? resolveToken(p.arg.slice(1), type, resolvers) : p.arg,
|
|
72
|
+
repoName,
|
|
73
|
+
);
|
|
70
74
|
typeDefaults.set(`${type}|${p.key}`, typeDefault);
|
|
71
75
|
if (!defaults.has(p.key)) { keys.push(p.key); defaults.set(p.key, typeDefault); }
|
|
72
76
|
const list = usages.get(p.key) || [];
|
|
@@ -140,7 +144,7 @@ export async function promptEnvPlan({
|
|
|
140
144
|
deployTarget = "docker-ssh", publishTargets = [], targetRoot = ".", repoName = "", log = defaultLog,
|
|
141
145
|
} = {}) {
|
|
142
146
|
const prompts = loadWizardPrompts(targetRoot, tempDir);
|
|
143
|
-
const asks = collectAsks(tempDir, types, { resolvers, deployTarget, publishTargets, prompts });
|
|
147
|
+
const asks = collectAsks(tempDir, types, { resolvers, deployTarget, publishTargets, prompts, repoName });
|
|
144
148
|
const defaults = asks.defaults;
|
|
145
149
|
|
|
146
150
|
// 수집 키 0개 → 질문 자체가 없음 (.sh `[ ${#WF_ASK_KEYS[@]} -eq 0 ]` 등가)
|
package/src/ui/summary.js
CHANGED
|
@@ -9,6 +9,7 @@ const SEPARATOR = "────────────────────
|
|
|
9
9
|
export function printSummary(ctx, targetRoot = ".") {
|
|
10
10
|
const { mode, types = [], version = "", counters = {} } = ctx || {};
|
|
11
11
|
const deployBranchName = ctx?.deployBranch || "develop"; // #477 — 설정된 배포 브랜치명으로 안내
|
|
12
|
+
const deployBranchReady = ctx?.deployBranchReady === true; // #490 — 마법사가 이번 실행에서 존재/생성 확인함
|
|
12
13
|
const err = (s = "") => process.stderr.write(`${s}\n`);
|
|
13
14
|
// 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
|
|
14
15
|
const isTty = !!process.stderr.isTTY;
|
|
@@ -139,8 +140,13 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
139
140
|
err(" → Secret Name: _GITHUB_PAT_TOKEN");
|
|
140
141
|
err(" → Scopes: repo, workflow");
|
|
141
142
|
err("");
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
// #490 — 마법사가 브랜치를 직접 생성(또는 존재 확인)했으면 같은 작업을 재지시하지 않는다
|
|
144
|
+
if (deployBranchReady) {
|
|
145
|
+
err(` 2️⃣ ✅ ${deployBranchName} 브랜치 준비 완료 — 마법사가 확인·생성했습니다 (추가 작업 불필요)`);
|
|
146
|
+
} else {
|
|
147
|
+
err(` 2️⃣ ${deployBranchName} 브랜치 생성 (아직 없다면)`);
|
|
148
|
+
err(` → git checkout -b ${deployBranchName} && git push -u origin ${deployBranchName}`);
|
|
149
|
+
}
|
|
144
150
|
err("");
|
|
145
151
|
err(" 3️⃣ CodeRabbit 활성화 (코드 리뷰를 켰다면)");
|
|
146
152
|
err(" → https://coderabbit.ai 로그인 → GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|