projectops 4.2.46 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/commands/full.js +11 -1
- package/src/commands/workflows.js +10 -1
- package/src/core/copy/simple.js +2 -0
- package/src/core/verify.js +109 -0
- package/src/core/version-yml.js +8 -3
- package/src/index.js +1 -0
- package/src/ui/summary.js +44 -4
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
> 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
|
|
8
8
|
|
|
9
9
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
10
|
-
## 최신 버전 : v4.2.
|
|
10
|
+
## 최신 버전 : v4.2.47 (2026-09-15)
|
|
11
11
|
|
|
12
12
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
| 기존 방식 | Projectops |
|
|
26
26
|
|----------|---------------------|
|
|
27
|
-
| 버전 수동 관리, 태그 직접 생성 | 릴리스
|
|
27
|
+
| 버전 수동 관리, 태그 직접 생성 | 릴리스 시 커밋 내용에 맞는 버전 자동 증가 + 태그 생성 |
|
|
28
28
|
| 체인지로그 직접 작성 (30분+) | CodeRabbit AI가 PR마다 자동 생성 |
|
|
29
29
|
| CI/CD 처음부터 설정 | 프로젝트 타입별 워크플로우 즉시 구성 |
|
|
30
30
|
| 이슈 매번 형식 맞춰 작성 (5분+) | `/pro-github` 한 번에 표준 템플릿 생성 + 등록 |
|
|
@@ -67,7 +67,7 @@ flowchart TD
|
|
|
67
67
|
flowchart TD
|
|
68
68
|
A([develop 푸시]) --> B[개발 통합]
|
|
69
69
|
B --> C[develop→main 릴리스 PR]
|
|
70
|
-
C --> D["PR 내 버전 확정<br
|
|
70
|
+
C --> D["PR 내 버전 확정<br/>커밋 기반 승격 + 태그 + AI 체인지로그"]
|
|
71
71
|
D --> E[자동 머지]
|
|
72
72
|
E --> F["main push → CI/CD 배포<br/>Flutter / Spring / React 등"]
|
|
73
73
|
F --> G([완료])
|
|
@@ -131,7 +131,7 @@ npx projectops --mode skills
|
|
|
131
131
|
| 기능 | 설명 | 문서 |
|
|
132
132
|
|------|------|------|
|
|
133
133
|
| **Agent Skills** | Claude Code, Cursor, Gemini CLI, Codex CLI에서 쓰는 17종 AI DevOps Skills | [상세](docs/SKILLS.md) |
|
|
134
|
-
| **버전 자동화** | 릴리스
|
|
134
|
+
| **버전 자동화** | 릴리스 시 커밋 제목으로 major/minor/patch 판정 + Git 태그 | [상세](docs/VERSION-CONTROL.md) |
|
|
135
135
|
| **AI 체인지로그** | provider 사다리(CodeRabbit/GitHub Models/OpenAI 계열/commit) 기반 CHANGELOG 자동 생성 | [상세](docs/CHANGELOG-AUTOMATION.md) |
|
|
136
136
|
| **PR Preview** | 댓글 한 줄로 임시 서버 배포, 닫으면 자동 삭제 | [상세](docs/PR-PREVIEW.md) |
|
|
137
137
|
| **이슈 자동화** | 브랜치명/커밋 메시지 자동 제안, QA 이슈 생성 | [상세](docs/ISSUE-AUTOMATION.md) |
|
package/package.json
CHANGED
package/src/commands/full.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { copyUtilModules } from "../core/copy/util.js";
|
|
16
16
|
import { copyCoderabbit } from "../core/copy/coderabbit.js";
|
|
17
17
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
18
|
+
import { verifyInstall } from "../core/verify.js";
|
|
18
19
|
|
|
19
20
|
// context: { version, types, paths:Map, branch, versionCode, deployTarget, publishTargets, includeSecretBackup,
|
|
20
21
|
// force, repoName, resolvers, now, today }
|
|
@@ -64,5 +65,14 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
64
65
|
ensureGitignore(targetRoot);
|
|
65
66
|
copySetupGuide(tempDir, targetRoot);
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
// 9. 설치 후 검증 (#549) — 디스크에 쓰인 최종 결과물을 다시 읽는다.
|
|
69
|
+
// 치환은 파일 단위로 흩어져 일어나고 auto 토큰은 resolver 결과에 의존하므로,
|
|
70
|
+
// 최종 내용을 보는 것이 실제 배포될 것과 같은 것을 보는 유일한 방법이다.
|
|
71
|
+
const verification = verifyInstall(targetRoot);
|
|
72
|
+
hooks.trace?.emit?.("verify", "scan", {
|
|
73
|
+
unresolved: verification.unresolved.length,
|
|
74
|
+
secrets: verification.secrets.size,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { workflows: wfCounters, verification };
|
|
68
78
|
}
|
|
@@ -8,6 +8,7 @@ import { copyWorkflows } from "../core/copy/workflows.js";
|
|
|
8
8
|
import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
|
|
9
9
|
import { copyUtilModules } from "../core/copy/util.js";
|
|
10
10
|
import { convertLegacySingularType } from "../core/version-yml.js";
|
|
11
|
+
import { verifyInstall } from "../core/verify.js";
|
|
11
12
|
|
|
12
13
|
export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
13
14
|
const { types = [], force = true } = context;
|
|
@@ -31,7 +32,15 @@ export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
31
32
|
copyConfigFolder(tempDir, targetRoot);
|
|
32
33
|
for (const t of types) copyUtilModules(tempDir, t, { force }, targetRoot);
|
|
33
34
|
copySetupGuide(tempDir, targetRoot);
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
// 설치 후 검증 (#549) — 워크플로우만 설치하는 모드라 오히려 더 필요하다.
|
|
37
|
+
const verification = verifyInstall(targetRoot);
|
|
38
|
+
hooks.trace?.emit?.("verify", "scan", {
|
|
39
|
+
unresolved: verification.unresolved.length,
|
|
40
|
+
secrets: verification.secrets.size,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return { workflows: wf, verification };
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
// 기존 version.yml에서 deploy: 블록을 제거하고 새로 append (.sh update_version_yml_deploy 멱등).
|
package/src/core/copy/simple.js
CHANGED
|
@@ -16,6 +16,8 @@ export function copyScripts(tempDir, targetRoot = ".") {
|
|
|
16
16
|
"changelog_manager.py",
|
|
17
17
|
"truncate_release_notes.sh", "truncate_release_notes.py",
|
|
18
18
|
"issue_helper.py",
|
|
19
|
+
// 릴리스 워크플로우가 PAT 없이 머지한 뒤 후속 워크플로우를 깨울 때 호출 (#551).
|
|
20
|
+
"dispatch_downstream.py",
|
|
19
21
|
"changelog_providers/_common.py", "changelog_providers/ladder.py",
|
|
20
22
|
"changelog_providers/commit.py", "changelog_providers/github_ai.py",
|
|
21
23
|
"changelog_providers/openai_compatible.py",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// 설치 후 검증 (#549) — 설치된 워크플로우를 다시 읽어 "이대로 돌아가는가"를 본다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 설치 전이 아니라 후인가: 치환은 파일 단위로 흩어져 일어나고 auto 토큰은 resolver 결과에
|
|
4
|
+
// 의존한다. 최종 디스크 내용을 보는 것이 실제로 배포될 것과 같은 것을 보는 유일한 방법이다.
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { PATHS } from "./paths.js";
|
|
8
|
+
|
|
9
|
+
// 치환 대상이 아닌 토큰 — 워크플로우 스크립트 안의 heredoc 구분자다. 값이 아니라 문법이므로
|
|
10
|
+
// 미치환 검사에서 제외한다. (예: cat <<'__SUH_FILE_CONTENT_EOF__')
|
|
11
|
+
const SENTINEL_RE = /^__SUH_[A-Z0-9_]*__$/;
|
|
12
|
+
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g;
|
|
13
|
+
|
|
14
|
+
// 주석으로 죽어 있는 줄 — 실행되지 않으므로 검사 대상이 아니다.
|
|
15
|
+
// 템플릿에는 "[선택] ..." 예시 스텝이 통째로 주석 처리돼 들어 있는데, 이걸 세면
|
|
16
|
+
// 쓰지도 않는 Secret을 "등록하세요"라고 안내하게 된다.
|
|
17
|
+
const isCommented = (line) => /^\s*#/.test(line);
|
|
18
|
+
|
|
19
|
+
// 검사 대상 파일 목록. 호출부가 명시하지 않으면 설치된 워크플로우 전체를 스캔한다
|
|
20
|
+
// (복사 결과 목록을 조합해 넘기는 것보다, 최종 디스크 상태를 그대로 보는 쪽이 목적에 맞다).
|
|
21
|
+
function listWorkflowFiles(workflowsDir, filenames) {
|
|
22
|
+
if (filenames && filenames.length) return filenames;
|
|
23
|
+
if (!existsSync(workflowsDir)) return [];
|
|
24
|
+
try {
|
|
25
|
+
return readdirSync(workflowsDir, { withFileTypes: true })
|
|
26
|
+
.filter((e) => e.isFile() && /\.ya?ml$/.test(e.name))
|
|
27
|
+
.map((e) => e.name)
|
|
28
|
+
.sort();
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLines(workflowsDir, filename) {
|
|
35
|
+
const p = join(workflowsDir, filename);
|
|
36
|
+
if (!existsSync(p)) return null;
|
|
37
|
+
try {
|
|
38
|
+
return readFileSync(p, "utf8").split(/\r?\n/);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 미치환 플레이스홀더 스캔.
|
|
45
|
+
// auto 토큰 계산이 실패해도(예: application.yaml을 못 찾아 경로가 빈 문자열) 그 줄을 건드리지
|
|
46
|
+
// 않고 넘어가므로, __APPLICATION_YML_DIR__ 이 그대로 남은 워크플로우가 "설치 성공"으로 끝난다.
|
|
47
|
+
// 문제는 배포 시점에야 그 이름의 디렉터리가 만들어지며 드러난다.
|
|
48
|
+
//
|
|
49
|
+
// 반환: [{ filename, line, token, text }] — 사람이 바로 고칠 수 있게 줄 번호까지 준다.
|
|
50
|
+
export function scanUnsubstituted(workflowsDir, filenames = []) {
|
|
51
|
+
const found = [];
|
|
52
|
+
for (const filename of listWorkflowFiles(workflowsDir, filenames)) {
|
|
53
|
+
const lines = readLines(workflowsDir, filename);
|
|
54
|
+
if (!lines) continue;
|
|
55
|
+
lines.forEach((text, i) => {
|
|
56
|
+
if (isCommented(text)) return;
|
|
57
|
+
for (const token of text.match(PLACEHOLDER_RE) || []) {
|
|
58
|
+
if (SENTINEL_RE.test(token)) continue;
|
|
59
|
+
found.push({ filename, line: i + 1, token, text: text.trim() });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// GITHUB_TOKEN은 Actions가 자동 주입하므로 사용자가 등록할 대상이 아니다.
|
|
67
|
+
const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
|
|
68
|
+
|
|
69
|
+
// 없어도 워크플로우가 도는 secret — 폴백이 문서화돼 있다. 필수와 섞어 "등록해야 동작합니다"라고
|
|
70
|
+
// 하면 안내 자체를 못 믿게 되므로 분리한다.
|
|
71
|
+
// MODEL_API_KEY → 없으면 GitHub Models(무료) → commit 규칙 fallback (#455 provider 사다리)
|
|
72
|
+
// _GITHUB_PAT_TOKEN → 없으면 GITHUB_TOKEN으로 머지하고 후속 워크플로우를 직접 깨운다 (#551)
|
|
73
|
+
export const OPTIONAL_SECRETS = new Set(["MODEL_API_KEY", "_GITHUB_PAT_TOKEN"]);
|
|
74
|
+
|
|
75
|
+
const SECRET_RE = /secrets\.([A-Z_][A-Z0-9_]*)/g;
|
|
76
|
+
|
|
77
|
+
// 설치된 워크플로우가 요구하는 GitHub Secret 목록.
|
|
78
|
+
// 완료 화면이 아무것도 안내하지 않는 바람에, 배포 워크플로우가 실제로 필요로 하는
|
|
79
|
+
// SERVER_HOST·SSH_KEY 같은 값이 드러나지 않았다. 설치 직후 상태로는 배포가 돌지 않는데
|
|
80
|
+
// 그 사실을 알 방법이 없다.
|
|
81
|
+
//
|
|
82
|
+
// 반환: Map<secretName, string[] 그 secret을 쓰는 파일명> (이름 오름차순)
|
|
83
|
+
export function collectRequiredSecrets(workflowsDir, filenames = []) {
|
|
84
|
+
const out = new Map();
|
|
85
|
+
for (const filename of listWorkflowFiles(workflowsDir, filenames)) {
|
|
86
|
+
const lines = readLines(workflowsDir, filename);
|
|
87
|
+
if (!lines) continue;
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
if (isCommented(line)) continue;
|
|
90
|
+
for (const m of line.matchAll(SECRET_RE)) {
|
|
91
|
+
const name = m[1];
|
|
92
|
+
if (AUTO_SECRETS.has(name) || OPTIONAL_SECRETS.has(name)) continue;
|
|
93
|
+
if (!out.has(name)) out.set(name, []);
|
|
94
|
+
const users = out.get(name);
|
|
95
|
+
if (!users.includes(filename)) users.push(filename);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return new Map([...out.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 설치 직후 한 번에 돌리는 진입점. targetRoot 기준으로 워크플로우 폴더를 찾는다.
|
|
103
|
+
// 반환: { unresolved: [...], secrets: Map, ok: boolean }
|
|
104
|
+
export function verifyInstall(targetRoot = ".", filenames = []) {
|
|
105
|
+
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
106
|
+
const unresolved = scanUnsubstituted(workflowsDir, filenames);
|
|
107
|
+
const secrets = collectRequiredSecrets(workflowsDir, filenames);
|
|
108
|
+
return { unresolved, secrets, ok: unresolved.length === 0 };
|
|
109
|
+
}
|
package/src/core/version-yml.js
CHANGED
|
@@ -16,9 +16,14 @@ const HEADER = `# ==============================================================
|
|
|
16
16
|
# 4. project_paths: 타입별 프로젝트 폴더 (레포 루트 기준 상대경로, 모노레포용)
|
|
17
17
|
#
|
|
18
18
|
# 자동 버전 업데이트:
|
|
19
|
-
# -
|
|
20
|
-
# -
|
|
21
|
-
#
|
|
19
|
+
# - version_code: 매 빌드마다 자동으로 1씩 증가 (아래 판정과 무관)
|
|
20
|
+
# - 버전 승격 폭은 metadata.template.options.semver_auto 가 정합니다
|
|
21
|
+
# semver_auto: true -> 릴리스 구간 커밋 제목으로 판정
|
|
22
|
+
# "제목 : feat! : 내용" 또는 "feat!:" -> major (x+1.0.0)
|
|
23
|
+
# "제목 : feat : 내용" 또는 "feat:" -> minor (x.y+1.0)
|
|
24
|
+
# 그 외(fix/docs/chore/refactor/test) -> patch (x.y.z+1)
|
|
25
|
+
# 키 없음 / false -> 항상 patch +1
|
|
26
|
+
# - 위 판정과 무관하게 version 값을 직접 수정해도 됩니다
|
|
22
27
|
#
|
|
23
28
|
# 프로젝트 타입별 동기화 파일:
|
|
24
29
|
# - spring: build.gradle (version = "x.y.z")
|
package/src/index.js
CHANGED
|
@@ -221,6 +221,7 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
221
221
|
printSummary({
|
|
222
222
|
mode: opts.mode, types, version, deployBranch: context.deployBranch, migrationGuidePath,
|
|
223
223
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
224
|
+
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
224
225
|
}, cwd);
|
|
225
226
|
return 0;
|
|
226
227
|
}
|
package/src/ui/summary.js
CHANGED
|
@@ -138,15 +138,55 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
138
138
|
}
|
|
139
139
|
err("");
|
|
140
140
|
|
|
141
|
+
// 설치 후 검증 결과 (#549) — 문제가 있을 때만 펼치고, 정상이면 한 줄로 압축한다.
|
|
142
|
+
const verification = ctx?.verification;
|
|
143
|
+
if (verification) {
|
|
144
|
+
const unresolved = verification.unresolved || [];
|
|
145
|
+
err(SEPARATOR);
|
|
146
|
+
err("");
|
|
147
|
+
if (unresolved.length === 0) {
|
|
148
|
+
err("✅ 설치 검증: 치환되지 않은 값 없음");
|
|
149
|
+
} else {
|
|
150
|
+
err(`${YELLOW}⚠️ 설치 검증: 치환되지 않은 값 ${unresolved.length}건${NC}`);
|
|
151
|
+
err("");
|
|
152
|
+
err(" 아래 위치에 템플릿 값이 그대로 남아 있습니다. 그대로 두면 배포 시점에 실패합니다.");
|
|
153
|
+
err(" (프로젝트 구조를 자동으로 찾지 못한 경우입니다 — 직접 값을 채워주세요)");
|
|
154
|
+
err("");
|
|
155
|
+
for (const u of unresolved.slice(0, 10)) {
|
|
156
|
+
err(` ${u.filename}:${u.line} ${u.token}`);
|
|
157
|
+
}
|
|
158
|
+
if (unresolved.length > 10) err(` … 외 ${unresolved.length - 10}건`);
|
|
159
|
+
}
|
|
160
|
+
err("");
|
|
161
|
+
|
|
162
|
+
// 필요한 Secret 목록 — 설치된 워크플로우 기준이라 "이 레포에 실제로 필요한 것"만 나온다.
|
|
163
|
+
const secrets = verification.secrets;
|
|
164
|
+
if (secrets && secrets.size > 0) {
|
|
165
|
+
err(`${CYAN}🔑 등록이 필요한 GitHub Secret (${secrets.size}개)${NC}`);
|
|
166
|
+
err(" → Repository Settings > Secrets and variables > Actions");
|
|
167
|
+
err("");
|
|
168
|
+
for (const [name, users] of secrets) {
|
|
169
|
+
const where = users.length > 2 ? `${users.slice(0, 2).join(", ")} 외 ${users.length - 2}개` : users.join(", ");
|
|
170
|
+
err(` ${name}`);
|
|
171
|
+
err(` └ ${where}`);
|
|
172
|
+
}
|
|
173
|
+
err("");
|
|
174
|
+
err(" 💡 등록 전까지 해당 워크플로우는 실패합니다. 쓰지 않는 워크플로우라면 무시해도 됩니다.");
|
|
175
|
+
err("");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
141
179
|
// 필수 3가지 작업 안내 (.sh L5605~5625 — 원문 유지)
|
|
142
180
|
err(SEPARATOR);
|
|
143
181
|
err("");
|
|
144
|
-
err(`${YELLOW}⚠️ 다음
|
|
182
|
+
err(`${YELLOW}⚠️ 다음 작업을 확인해주세요:${NC}`);
|
|
145
183
|
err("");
|
|
146
|
-
|
|
184
|
+
// #551 — PAT는 없어도 릴리스가 완주한다. 있으면 후속 자동화가 더 매끄러워질 뿐이다.
|
|
185
|
+
err(" 1️⃣ (선택) GitHub Personal Access Token 설정");
|
|
186
|
+
err(" → 없어도 릴리스는 정상 동작합니다. 등록하면 릴리스 후 후속 워크플로우가");
|
|
187
|
+
err(" 더 확실하게 이어지고, 브랜치 보호 규칙이 있어도 자동 머지가 가능합니다.");
|
|
147
188
|
err(" → Repository Settings > Secrets > Actions");
|
|
148
|
-
err(" → Secret Name: _GITHUB_PAT_TOKEN");
|
|
149
|
-
err(" → Scopes: repo, workflow");
|
|
189
|
+
err(" → Secret Name: _GITHUB_PAT_TOKEN / Scopes: repo, workflow");
|
|
150
190
|
err("");
|
|
151
191
|
// #490 — 마법사가 브랜치를 직접 생성(또는 존재 확인)했으면 같은 작업을 재지시하지 않는다
|
|
152
192
|
if (deployBranchReady) {
|