projectops 4.2.8 → 4.2.10

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 CHANGED
@@ -7,7 +7,7 @@
7
7
  > 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
8
8
 
9
9
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
10
- ## 최신 버전 : v4.2.7 (2026-07-10)
10
+ ## 최신 버전 : v4.2.9 (2026-07-12)
11
11
 
12
12
  [전체 버전 기록 보기](CHANGELOG.md)
13
13
 
@@ -124,7 +124,7 @@ npx projectops --mode skills
124
124
 
125
125
 
126
126
 
127
- > Claude Code는 `/projectops:` 자동완성, Gemini는 extension, Codex는 plugin marketplace를 우선 사용합니다. 자세한 설치 방식은 [Skills 가이드](docs/SKILLS.md)를 확인하세요.
127
+ > Claude Code는 `/pro-` 자동완성, Gemini는 extension, Codex는 plugin marketplace를 우선 사용합니다. 자세한 설치 방식은 [Skills 가이드](docs/SKILLS.md)를 확인하세요.
128
128
 
129
129
  ---
130
130
 
@@ -134,7 +134,7 @@ npx projectops --mode skills
134
134
  |------|------|------|
135
135
  | **Agent Skills** | Claude Code, Cursor, Gemini CLI, Codex CLI에서 쓰는 24종 AI DevOps Skills | [상세](docs/SKILLS.md) |
136
136
  | **버전 자동화** | 릴리스(develop→main PR) 시 patch 버전 자동 증가 + Git 태그 | [상세](docs/VERSION-CONTROL.md) |
137
- | **AI 체인지로그** | CodeRabbit 리뷰 기반 CHANGELOG 자동 생성 | [상세](docs/CHANGELOG-AUTOMATION.md) |
137
+ | **AI 체인지로그** | provider 사다리(CodeRabbit/GitHub Models/OpenAI 계열/commit) 기반 CHANGELOG 자동 생성 | [상세](docs/CHANGELOG-AUTOMATION.md) |
138
138
  | **PR Preview** | 댓글 한 줄로 임시 서버 배포, 닫으면 자동 삭제 | [상세](docs/PR-PREVIEW.md) |
139
139
  | **이슈 자동화** | 브랜치명/커밋 메시지 자동 제안, QA 이슈 생성 | [상세](docs/ISSUE-AUTOMATION.md) |
140
140
  | **Flutter CI/CD** | iOS TestFlight + Android Play Store 자동 배포 | [상세](docs/FLUTTER-CICD-OVERVIEW.md) |
@@ -249,9 +249,10 @@ Settings → Actions → General
249
249
  | 문서 | 설명 |
250
250
  |------|------|
251
251
  | [Agent Skills 가이드](docs/SKILLS.md) | 24종 Skills 용도, 사용법, 전체 개발 사이클 흐름 |
252
- | [통합 스크립트 가이드](docs/TEMPLATE-INTEGRATOR.md) | 기존 프로젝트에 템플릿 통합 |
252
+ | [NPX 마법사 가이드](docs/NPX-WIZARD.md) | npx projectops 통합·배포/publish 2축·레거시 자동 마이그레이션 |
253
+ | [통합 스크립트 가이드](docs/TEMPLATE-INTEGRATOR.md) | 구 integrator 지원 종료(EOF) 안내 |
253
254
  | [버전 관리](docs/VERSION-CONTROL.md) | version.yml, 자동 버전 증가 |
254
- | [체인지로그 자동화](docs/CHANGELOG-AUTOMATION.md) | CodeRabbit 연동, AI 문서화 |
255
+ | [체인지로그 자동화](docs/CHANGELOG-AUTOMATION.md) | 릴리스 노트 provider 사다리, CodeRabbit 연동 |
255
256
  | [PR Preview](docs/PR-PREVIEW.md) | 임시 서버 배포 시스템 |
256
257
  | [Flutter CI/CD](docs/FLUTTER-CICD-OVERVIEW.md) | iOS/Android 자동 배포 |
257
258
  | [SSH+Docker 배포](docs/SSH-DOCKER-DEPLOYMENT-GUIDE.md) | SSH 접속 서버에 Docker 배포 (Synology·AWS EC2 등) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectops",
3
- "version": "4.2.8",
3
+ "version": "4.2.10",
4
4
  "description": "ProjectOps — 완전 자동화 GitHub 프로젝트 관리 템플릿 통합 CLI",
5
5
  "keywords": [
6
6
  "devops",
@@ -10,6 +10,7 @@ import { acquireTemplate, readTemplateVersion } from "../core/assets.js";
10
10
  import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers } from "../core/detect-fs.js";
11
11
  import { parseExisting } from "../core/version-yml.js";
12
12
  import { runBreakingCheck } from "../core/breaking-check.js";
13
+ import { runMigrations } from "../core/migrations/index.js";
13
14
  import { resolveProjectPaths } from "../core/paths-resolve.js";
14
15
  import { askAllOptionalWorkflows } from "../core/options-ask.js";
15
16
  import { promptEnvPlan } from "../ui/env-plan.js";
@@ -243,6 +244,14 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
243
244
  }
244
245
  }
245
246
 
247
+ // 레거시 마이그레이션 (#470) — 워크플로우를 만지는 모드에서만. 대화형은 safe 티어 확인 1회.
248
+ if (mode === "full" || mode === "workflows") {
249
+ await runMigrations({
250
+ targetRoot: cwd,
251
+ askYesNo: (msg, def) => io.askYesNo(msg, def),
252
+ });
253
+ }
254
+
246
255
  let result = null;
247
256
  if (mode === "full") result = runFull(ctx, tempDir, cwd, hooks);
248
257
  else if (mode === "version") result = runVersion(ctx, tempDir, cwd);
@@ -7,13 +7,22 @@ import { PATHS } from "../core/paths.js";
7
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
+ import { convertLegacySingularType } from "../core/version-yml.js";
10
11
 
11
12
  export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
12
13
  const { types = [], force = true } = context;
13
14
  const wf = copyWorkflows(context, tempDir, targetRoot, hooks);
14
15
 
15
- // update_version_yml_deploy: 기존 version.yml이 있고 ask 값이 있을 때만 deploy 블록 갱신
16
16
  const vy = join(targetRoot, PATHS.versionFile);
17
+
18
+ // v4.1.0 이전 단수 project_type 최소 변환 (#471) — 여기서 복사되는 신형 version_manager가
19
+ // 단수 키를 거부하므로, 변환 없이 두면 버전 워크플로우가 전부 실패하는 깨진 상태가 된다.
20
+ if (existsSync(vy)) {
21
+ const converted = convertLegacySingularType(readFileSync(vy, "utf8"));
22
+ if (converted !== null) writeFileSync(vy, converted);
23
+ }
24
+
25
+ // update_version_yml_deploy: 기존 version.yml이 있고 ask 값이 있을 때만 deploy 블록 갱신
17
26
  if (existsSync(vy) && wf.deployValues && wf.deployValues.size) {
18
27
  writeFileSync(vy, upsertDeployBlock(readFileSync(vy, "utf8"), wf.deployValues));
19
28
  }
@@ -0,0 +1,78 @@
1
+ // 레거시 마이그레이션 진입점 (#470) — 신호 기반·멱등.
2
+ // 감지(registry × rules) → 계획 표시 → safe 티어 확인 1회 → 적용 → confirm 티어 안내.
3
+ // 버전 번호를 믿지 않는다: 절반의 레거시 레포는 version.yml에 template 메타가 없다(실측).
4
+ import { MIGRATIONS } from "./registry.js";
5
+ import * as workflowRule from "./rules/obsolete-workflows.js";
6
+ import * as rootFileRule from "./rules/root-files.js";
7
+
8
+ const RULES = {
9
+ workflow: workflowRule,
10
+ "root-file": rootFileRule,
11
+ };
12
+
13
+ // 대상 레포에서 레거시 잔재 감지. 반환: { safe: [entry], confirm: [entry] }
14
+ export function detectMigrations(targetRoot = ".") {
15
+ const safe = [];
16
+ const confirm = [];
17
+ for (const entry of MIGRATIONS) {
18
+ const rule = RULES[entry.category];
19
+ if (!rule || !rule.detect(targetRoot, entry)) continue;
20
+ (entry.tier === "safe" ? safe : confirm).push(entry);
21
+ }
22
+ return { safe, confirm };
23
+ }
24
+
25
+ // safe 티어 적용. 실패해도 나머지는 계속(부분 실패 허용 — 멱등이라 재실행으로 복구).
26
+ export function applySafeMigrations(targetRoot, entries) {
27
+ const results = [];
28
+ for (const entry of entries) {
29
+ try {
30
+ results.push({ id: entry.id, ...RULES[entry.category].apply(targetRoot, entry) });
31
+ } catch (e) {
32
+ results.push({ id: entry.id, action: "error", from: entry.file, error: e.message });
33
+ }
34
+ }
35
+ return results;
36
+ }
37
+
38
+ // 마법사 배선 진입점.
39
+ // askYesNo - async(msg, defaultYes)→bool. null이면 비대화형(--force): safe 자동 적용
40
+ // log - 한 줄 출력 함수 (기본 console.log)
41
+ // 반환: { applied: [결과], confirmPending: [entry] }
42
+ export async function runMigrations({ targetRoot = ".", askYesNo = null, log = console.log } = {}) {
43
+ const { safe, confirm } = detectMigrations(targetRoot);
44
+ if (safe.length === 0 && confirm.length === 0) return { applied: [], confirmPending: [] };
45
+
46
+ let applied = [];
47
+ if (safe.length > 0) {
48
+ log(`🧹 레거시 템플릿 파일 ${safe.length}개 감지 — 신형으로 대체되어 공존 시 중복 실행 위험:`);
49
+ for (const e of safe) {
50
+ const arrow = e.replacedBy ? ` → ${e.replacedBy}` : "";
51
+ log(` • ${e.file}${arrow}`);
52
+ log(` (${e.reason}, ${e.since}부터 폐기)`);
53
+ }
54
+ const yes = askYesNo
55
+ ? await askYesNo(`위 ${safe.length}개를 정리할까요? (워크플로우는 .bak 무해화 — 복원 가능)`, true)
56
+ : true;
57
+ if (yes) {
58
+ applied = applySafeMigrations(targetRoot, safe);
59
+ const ok = applied.filter((r) => r.action !== "error");
60
+ const failed = applied.filter((r) => r.action === "error");
61
+ log(`✅ 레거시 정리 완료: ${ok.length}개${failed.length ? ` (실패 ${failed.length}개)` : ""}`);
62
+ for (const f of failed) log(` ⚠️ ${f.from}: ${f.error}`);
63
+ } else {
64
+ log("→ 레거시 정리를 건너뜁니다 (다음 업데이트 때 다시 안내)");
65
+ }
66
+ }
67
+
68
+ if (confirm.length > 0) {
69
+ log(`⚠️ 구세대 배포 워크플로우 ${confirm.length}개 발견 — 현역 배포일 수 있어 자동으로 건드리지 않습니다:`);
70
+ for (const e of confirm) {
71
+ const arrow = e.replacedBy ? ` (신형: ${e.replacedBy})` : "";
72
+ log(` • ${e.file}${arrow} — ${e.reason}`);
73
+ }
74
+ log(" 신형 워크플로우로 전환을 마친 뒤 구 파일을 직접 삭제하세요.");
75
+ }
76
+
77
+ return { applied, confirmPending: confirm };
78
+ }
@@ -0,0 +1,148 @@
1
+ // 레거시 마이그레이션 단일 레지스트리 (#470) — 유일한 관리 지점.
2
+ //
3
+ // ⚠️ 템플릿에서 워크플로우/루트 파일을 리네임·폐기하면 반드시 여기에 구 이름을 한 줄 추가한다.
4
+ // (test/migrations.test.js가 "레지스트리 항목이 현행 배포 세트와 겹치지 않는지" 자동 검증)
5
+ //
6
+ // 항목 스키마:
7
+ // id - 고유 식별자 (kebab-case)
8
+ // category - "workflow" | "root-file" (rules/ 폴더의 구현이 detect/apply 담당)
9
+ // tier - "safe" : 신형이 같은 기능을 대체(순수 리네임). 공존 시 이중 트리거 실해
10
+ // → 확인 1회 후 자동 무해화(.bak) / 삭제
11
+ // "confirm" : 배포 파이프라인일 수 있음(그 레포의 유일한 현역 배포 가능성)
12
+ // → 자동으로 건드리지 않고 안내만. --force에서도 불변
13
+ // file - 정확한 파일명 (글롭 금지 — 사용자 커스텀 보호의 핵심)
14
+ // replacedBy - 대체 신형 파일명 (없으면 null)
15
+ // since - 구 파일이 폐기된 템플릿 버전(참고용)
16
+ // reason - 계획 카드에 표시할 사유
17
+ // contentMarker - (선택) 파일명이 범용적일 때 오탐 방지용 내용 마커 — 이 문자열이
18
+ // 파일 내용에 있을 때만 템플릿 소유로 판정
19
+ //
20
+ // 데이터 출처: git 히스토리 삭제/리네임 전수 + 실제 통합 레포 14개 스캔 (2026-07-11, 설계 문서 참조)
21
+ export const MIGRATIONS = [
22
+ // ── workflow / safe — 순수 리네임·대체 (공존 시 중복 실행 실해) ──────────────
23
+ { id: "wf-version-control", category: "workflow", tier: "safe",
24
+ file: "PROJECT-VERSION-CONTROL.yaml", replacedBy: "PROJECT-COMMON-VERSION-CONTROL.yaml",
25
+ since: "2.x", reason: "1세대 리네임 — 공존 시 버전 이중 증가" },
26
+ { id: "wf-readme-version-update", category: "workflow", tier: "safe",
27
+ file: "PROJECT-README-VERSION-UPDATE.yaml", replacedBy: "PROJECT-COMMON-README-VERSION-UPDATE.yaml",
28
+ since: "2.x", reason: "1세대 리네임 — 공존 시 README 이중 커밋" },
29
+ { id: "wf-sync-issue-labels-v1", category: "workflow", tier: "safe",
30
+ file: "PROJECT-SYNC-ISSUE-LABELS.yaml", replacedBy: "PROJECT-COMMON-SYNC-ISSUE-LABELS.yaml",
31
+ since: "2.x", reason: "1세대 리네임 — 공존 시 라벨 동기화 중복" },
32
+ { id: "wf-sync-issue-labels-v0", category: "workflow", tier: "safe",
33
+ file: "sync-issue-labels.yaml", replacedBy: "PROJECT-COMMON-SYNC-ISSUE-LABELS.yaml",
34
+ since: "2.x", reason: "0세대 리네임 — 공존 시 라벨 동기화 중복" },
35
+ { id: "wf-issue-comment-v1", category: "workflow", tier: "safe",
36
+ file: "PROJECT-ISSUE-COMMENT.yaml", replacedBy: "PROJECT-COMMON-SUH-ISSUE-HELPER-MODULE.yml",
37
+ since: "2.x", reason: "이슈 헬퍼 1세대 — 공존 시 이슈 댓글 중복" },
38
+ { id: "wf-issue-comment-v2", category: "workflow", tier: "safe",
39
+ file: "PROJECT-COMMON-ISSUE-COMMENT.yaml", replacedBy: "PROJECT-COMMON-SUH-ISSUE-HELPER-API.yaml",
40
+ since: "2.x", reason: "이슈 헬퍼 2세대 — 공존 시 이슈 댓글 중복" },
41
+ { id: "wf-auto-changelog-v1", category: "workflow", tier: "safe",
42
+ file: "PROJECT-AUTO-CHANGELOG-CONTROL.yaml", replacedBy: "PROJECT-COMMON-RELEASE-CHANGELOG.yaml",
43
+ since: "2.x", reason: "릴리스 워크플로우 1세대 — 공존 시 릴리스 PR 이중 처리" },
44
+ { id: "wf-auto-changelog-v2", category: "workflow", tier: "safe",
45
+ file: "PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml", replacedBy: "PROJECT-COMMON-RELEASE-CHANGELOG.yaml",
46
+ since: "4.3.0", reason: "RELEASE-CHANGELOG로 리네임 — 공존 시 릴리스 PR 이중 처리" },
47
+ { id: "wf-comqa-bot", category: "workflow", tier: "safe",
48
+ file: "COMQA-ISSUE-CREATION-BOT.yaml", replacedBy: "PROJECT-COMMON-QA-ISSUE-CREATION-BOT.yaml",
49
+ since: "2.x", reason: "QA 봇 구명칭 — 공존 시 QA 이슈 중복 생성" },
50
+ { id: "wf-template-util-sync", category: "workflow", tier: "safe",
51
+ file: "TEMPLATE-UTIL-VERSION-SYNC.yml", replacedBy: "PROJECT-COMMON-TEMPLATE-UTIL-VERSION-SYNC.yml",
52
+ since: "2.x", reason: "구명칭 — 공존 시 util 버전 동기화 중복" },
53
+ { id: "wf-backlog-manager", category: "workflow", tier: "safe",
54
+ file: "PROJECT-COMMON-PROJECT-BACKLOG-MANAGER.yaml", replacedBy: "PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml",
55
+ since: "3.x", reason: "Projects 동기화로 대체 — 공존 시 보드 상태 중복 갱신" },
56
+ { id: "wf-suh-lab-build-trigger", category: "workflow", tier: "safe",
57
+ file: "PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml", replacedBy: "PROJECT-FLUTTER-PROJECTOPS-APP-BUILD-TRIGGER.yaml",
58
+ since: "4.3.0", reason: "리브랜딩 리네임 — 공존 시 빌드 트리거 중복" },
59
+ { id: "wf-next-ci", category: "workflow", tier: "safe",
60
+ file: "PROJECT-NEXT-CI.yaml", replacedBy: "PROJECT-REACT-CI.yaml",
61
+ since: "4.1.0", reason: "next 타입 폐지 (react 흡수)" },
62
+ { id: "wf-next-cicd", category: "workflow", tier: "safe",
63
+ file: "PROJECT-NEXT-CICD.yaml", replacedBy: "PROJECT-REACT-CICD.yaml",
64
+ since: "4.1.0", reason: "next 타입 폐지 (react 흡수)" },
65
+ { id: "wf-flutter-ci-yml-ext", category: "workflow", tier: "safe",
66
+ file: "PROJECT-FLUTTER-CI.yml", replacedBy: "PROJECT-FLUTTER-CI.yaml",
67
+ since: "2.x", reason: "구 확장자(.yml) — 현행 .yaml과 공존 시 CI 중복 실행" },
68
+ { id: "wf-flutter-android-pr-ci", category: "workflow", tier: "safe",
69
+ file: "PROJECT-FLUTTER-ANDROID-PR-CI.yaml", replacedBy: "PROJECT-FLUTTER-CI.yaml",
70
+ since: "3.x", reason: "통합 CI로 흡수 — 공존 시 PR CI 중복 실행" },
71
+
72
+ // ── workflow / confirm — 배포/업로드 계열 (현역 배포일 수 있어 자동 조치 금지) ──
73
+ { id: "wf-syn-spring-simple", category: "workflow", tier: "confirm",
74
+ file: "PROJECT-SPRING-SYNOLOGY-SIMPLE-CICD.yaml", replacedBy: "PROJECT-SPRING-SIMPLE-CICD.yaml",
75
+ since: "3.0.137", reason: "SYNOLOGY 폐기 — SSH+Docker 엔진으로 대체됨" },
76
+ { id: "wf-syn-spring-nonstop", category: "workflow", tier: "confirm",
77
+ file: "PROJECT-SPRING-SYNOLOGY-NONSTOP-CICD.yaml", replacedBy: "PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml",
78
+ since: "3.0.137", reason: "SYNOLOGY 폐기 — 무중단 배포는 NGINX/TRAEFIK 버전으로 대체됨" },
79
+ { id: "wf-syn-spring-preview", category: "workflow", tier: "confirm",
80
+ file: "PROJECT-SPRING-SYNOLOGY-PR-PREVIEW.yaml", replacedBy: "PROJECT-SPRING-PR-PREVIEW.yaml",
81
+ since: "3.0.137", reason: "SYNOLOGY 폐기" },
82
+ { id: "wf-syn-flutter-android", category: "workflow", tier: "confirm",
83
+ file: "PROJECT-FLUTTER-ANDROID-SYNOLOGY-CICD.yaml", replacedBy: "PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml",
84
+ since: "3.0.137", reason: "SYNOLOGY 폐기 — SELFHOSTED(SMB)로 대체됨" },
85
+ { id: "wf-syn-python-cicd", category: "workflow", tier: "confirm",
86
+ file: "PROJECT-PYTHON-SYNOLOGY-CICD.yaml", replacedBy: "PROJECT-PYTHON-SIMPLE-CICD.yaml",
87
+ since: "3.0.137", reason: "SYNOLOGY 폐기" },
88
+ { id: "wf-syn-python-preview", category: "workflow", tier: "confirm",
89
+ file: "PROJECT-PYTHON-SYNOLOGY-PR-PREVIEW.yaml", replacedBy: "PROJECT-PYTHON-PR-PREVIEW.yaml",
90
+ since: "3.0.137", reason: "SYNOLOGY 폐기" },
91
+ { id: "wf-syn-secret-upload", category: "workflow", tier: "confirm",
92
+ file: "PROJECT-COMMON-SYNOLOGY-SECRET-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
93
+ since: "3.0.137", reason: "SYNOLOGY 폐기 — 공통 Secret 업로드로 대체됨" },
94
+ { id: "wf-spring-ci-v1", category: "workflow", tier: "confirm",
95
+ file: "PROJECT-SPRING-CI.yaml", replacedBy: null,
96
+ since: "2.x", reason: "1세대 Spring CI — 현행 대체본 없음(CICD에 통합)" },
97
+ { id: "wf-spring-cicd-v1", category: "workflow", tier: "confirm",
98
+ file: "PROJECT-SPRING-CICD.yaml", replacedBy: "PROJECT-SPRING-SIMPLE-CICD.yaml",
99
+ since: "2.x", reason: "1세대 Spring 배포 — 현역 배포일 수 있음" },
100
+ { id: "wf-python-cicd-v1", category: "workflow", tier: "confirm",
101
+ file: "PROJECT-PYTHON-CICD.yaml", replacedBy: "PROJECT-PYTHON-SIMPLE-CICD.yaml",
102
+ since: "3.x", reason: "구 Python 배포 — 현역 배포일 수 있음" },
103
+ { id: "wf-android-cicd-v1", category: "workflow", tier: "confirm",
104
+ file: "PROJECT-ANDROID-CICD.yaml", replacedBy: "PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml",
105
+ since: "2.x", reason: "1세대 Android 배포 — 현역 배포일 수 있음" },
106
+ { id: "wf-ios-testflight-v1", category: "workflow", tier: "confirm",
107
+ file: "PROJECT-IOS-TESTFLIGHT-CICD.yml", replacedBy: "PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml",
108
+ since: "2.x", reason: "1세대 iOS 배포 — 현역 배포일 수 있음" },
109
+ { id: "wf-flutter-ios-cicd", category: "workflow", tier: "confirm",
110
+ file: "PROJECT-FLUTTER-IOS-CICD.yaml", replacedBy: "PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml",
111
+ since: "3.x", reason: "TESTFLIGHT 워크플로우로 대체 — 현역 배포일 수 있음" },
112
+ { id: "wf-file-upload-spring", category: "workflow", tier: "confirm",
113
+ file: "PROJECT-SPRING-AUTO-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
114
+ since: "3.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
115
+ { id: "wf-file-upload-flutter", category: "workflow", tier: "confirm",
116
+ file: "PROJECT-FLUTTER-AUTO-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
117
+ since: "3.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
118
+ { id: "wf-file-upload-python", category: "workflow", tier: "confirm",
119
+ file: "PROJECT-PYTHON-AUTO-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
120
+ since: "3.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
121
+ { id: "wf-file-upload-v1", category: "workflow", tier: "confirm",
122
+ file: "PROJECT-FILE-AUTO-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
123
+ since: "2.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
124
+ { id: "wf-nexus-publish-v1", category: "workflow", tier: "confirm",
125
+ file: "PROJECT-NEXUS-PUBLISH.yml", replacedBy: "PROJECT-SPRING-NEXUS-PUBLISH.yml",
126
+ since: "2.x", reason: "구 Nexus 배포 — 현역 라이브러리 배포일 수 있음" },
127
+ { id: "wf-nexus-ci-v1", category: "workflow", tier: "confirm",
128
+ file: "PROJECT-NEXUS-MODULE-CI-BUILD-CHECK.yml", replacedBy: "PROJECT-SPRING-NEXUS-CI.yml",
129
+ since: "2.x", reason: "구 Nexus CI" },
130
+ { id: "wf-ci-build-check-v0", category: "workflow", tier: "confirm",
131
+ file: "PROJECT-CI-BUILD-CHECK.yml", replacedBy: "PROJECT-SPRING-NEXUS-CI.yml",
132
+ since: "2.x", reason: "0세대 라이브러리 CI" },
133
+ { id: "wf-publish-v0", category: "workflow", tier: "confirm",
134
+ file: "PROJECT-PUBLISH.yml", replacedBy: "PROJECT-SPRING-NEXUS-PUBLISH.yml",
135
+ since: "2.x", reason: "0세대 라이브러리 배포 — 현역 배포일 수 있음" },
136
+ { id: "wf-deploy-trigger", category: "workflow", tier: "confirm",
137
+ file: "PROJECT-DEPLOY-TRIGGER.yaml", replacedBy: null,
138
+ since: "3.x", reason: "폐기된 배포 트리거" },
139
+
140
+ // ── root-file / safe — 구 설치 가이드 (사용자가 매번 수동 삭제하던 것) ─────────
141
+ { id: "root-setup-guide-v2", category: "root-file", tier: "safe",
142
+ file: "SUH-DEVOPS-TEMPLATE-SETUP-GUIDE.md", replacedBy: "PROJECTOPS-SETUP-GUIDE.md",
143
+ since: "4.3.0", reason: "리브랜딩으로 구 가이드 대체 — 잔재 문서" },
144
+ { id: "root-setup-guide-v1", category: "root-file", tier: "safe",
145
+ file: "SETUP-GUIDE.md", replacedBy: "PROJECTOPS-SETUP-GUIDE.md",
146
+ since: "2.x", reason: "구 가이드 문서 — 잔재",
147
+ contentMarker: "SUH" }, // 범용 파일명이라 내용에 템플릿 마커가 있을 때만 소유 판정
148
+ ];
@@ -0,0 +1,28 @@
1
+ // workflow 카테고리 규칙 (#470) — .github/workflows/의 구 템플릿 워크플로우를 .bak으로 무해화.
2
+ // GitHub Actions는 .yaml/.yml만 실행하므로 .bak 리네임 = 즉시 무해화 + 복원 가능.
3
+ import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const WF_DIR = join(".github", "workflows");
7
+
8
+ function target(targetRoot, entry) {
9
+ return join(targetRoot, WF_DIR, entry.file);
10
+ }
11
+
12
+ export function detect(targetRoot, entry) {
13
+ const p = target(targetRoot, entry);
14
+ if (!existsSync(p)) return false;
15
+ if (entry.contentMarker) {
16
+ try { return readFileSync(p, "utf8").includes(entry.contentMarker); }
17
+ catch { return false; }
18
+ }
19
+ return true;
20
+ }
21
+
22
+ export function apply(targetRoot, entry) {
23
+ const src = target(targetRoot, entry);
24
+ const bak = `${src}.bak`;
25
+ if (existsSync(bak)) rmSync(bak, { force: true }); // Windows rename은 대상 존재 시 실패
26
+ renameSync(src, bak);
27
+ return { action: "bak", from: entry.file, to: `${entry.file}.bak` };
28
+ }
@@ -0,0 +1,24 @@
1
+ // root-file 카테고리 규칙 (#470) — 레포 루트의 구 템플릿 문서를 삭제.
2
+ // 문서라 무해화가 필요 없고, 사용자가 매번 수동 삭제하던 잔재라 delete가 기대 동작.
3
+ import { existsSync, readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ function target(targetRoot, entry) {
7
+ return join(targetRoot, entry.file);
8
+ }
9
+
10
+ export function detect(targetRoot, entry) {
11
+ const p = target(targetRoot, entry);
12
+ if (!existsSync(p)) return false;
13
+ // 범용 파일명 오탐 방지 — contentMarker가 있으면 내용까지 확인해야 템플릿 소유로 판정
14
+ if (entry.contentMarker) {
15
+ try { return readFileSync(p, "utf8").includes(entry.contentMarker); }
16
+ catch { return false; }
17
+ }
18
+ return true;
19
+ }
20
+
21
+ export function apply(targetRoot, entry) {
22
+ rmSync(target(targetRoot, entry), { force: true });
23
+ return { action: "deleted", from: entry.file, to: null };
24
+ }
@@ -133,6 +133,29 @@ export function parseTemplateOptions(content) {
133
133
  return out;
134
134
  }
135
135
 
136
+ // v4.1.0 이전 단수 project_type 키 → project_types 배열 최소 변환 (#471).
137
+ // version.yml을 재생성하지 않는 경로(workflows 모드)에서 신형 version_manager(단수 키 명시 거부)와의
138
+ // 정합을 맞춘다 — 해당 라인만 교체하고 나머지 내용은 손대지 않는다.
139
+ // 배열 키가 이미 있으면 남은 단수 키만 제거한다(4.1.0: 공존 시 단수 무시 — 잔재 정리).
140
+ // 반환: 변환된 전체 내용 문자열, 변환할 것이 없으면 null.
141
+ export function convertLegacySingularType(content) {
142
+ const lines = String(content || "").split(/\r?\n/);
143
+ let hasArray = false, idx = -1, value = "";
144
+ lines.forEach((l, i) => {
145
+ if (/^project_types:/.test(l)) hasArray = true;
146
+ const m = l.match(/^project_type:\s*["']?([a-z-]+)["']?/);
147
+ if (m && idx < 0) { idx = i; value = m[1]; }
148
+ });
149
+ if (idx < 0) return null;
150
+ if (hasArray) {
151
+ lines.splice(idx, 1);
152
+ return lines.join("\n");
153
+ }
154
+ const type = value === "next" ? "react" : value; // next 타입은 4.1.0에서 react로 흡수
155
+ lines[idx] = `project_types: ["${type}"] # 멀티타입 배열 — 첫 항목이 primary, 직접 편집 가능`;
156
+ return lines.join("\n");
157
+ }
158
+
136
159
  // 기존 version.yml에서 값 추출 (.sh grep/sed 등가, 주석 라인 오탐 방지).
137
160
  export function parseExisting(content) {
138
161
  const text = String(content || "");
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import { acquireTemplate, readTemplateVersion } from "./core/assets.js";
12
12
  import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers } from "./core/detect-fs.js";
13
13
  import { parseExisting } from "./core/version-yml.js";
14
14
  import { runBreakingCheck } from "./core/breaking-check.js";
15
+ import { runMigrations } from "./core/migrations/index.js";
15
16
  import { resolveProjectPaths } from "./core/paths-resolve.js";
16
17
  import { printBannerCompact } from "./ui/banner.js";
17
18
  import { printSummary } from "./ui/summary.js";
@@ -133,6 +134,11 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
133
134
  const proceed = await runBreakingCheck({ cwd, tempDir, templateVersion: context.templateVersion });
134
135
  if (!proceed) return 0;
135
136
 
137
+ // 레거시 마이그레이션 (#470) — 워크플로우를 만지는 모드에서만. 비대화형은 safe 티어 자동 적용.
138
+ if (opts.mode === "full" || opts.mode === "workflows") {
139
+ await runMigrations({ targetRoot: cwd });
140
+ }
141
+
136
142
  switch (opts.mode) {
137
143
  case "full": result = runFull(context, tempDir, cwd); break;
138
144
  case "version": result = runVersion(context, tempDir, cwd); break;