egovframe-scaffold-mcp 0.44.0 → 0.45.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.en.md +11 -1
- package/README.md +34 -5
- package/catalog/deploy-rules.json +114 -0
- package/dist/agents-md.js +2 -2
- package/dist/deploy.js +476 -0
- package/dist/i18n.js +1 -0
- package/dist/index.js +1 -0
- package/dist/output-schemas.js +17 -0
- package/dist/server.js +20 -0
- package/dist/tool-meta.js +2 -0
- package/docs/design-deploy.md +96 -0
- package/package.json +4 -2
package/README.en.md
CHANGED
|
@@ -44,7 +44,7 @@ Requires Node.js 18+. Downloads go to `codeload.github.com`, `raw.githubusercont
|
|
|
44
44
|
| `EGOVFRAME_ALLOWED_ROOTS` | Optional sandbox: every directory argument (`projectDir`, `outputDir`) must be inside one of these roots (path-separator-delimited, realpath-checked). |
|
|
45
45
|
| `EGOVFRAME_LANG` | `en` → English tool descriptions in `tools/list`. Responses stay Korean. |
|
|
46
46
|
|
|
47
|
-
## Tools (
|
|
47
|
+
## Tools (34)
|
|
48
48
|
|
|
49
49
|
| Tool | What it does |
|
|
50
50
|
|---|---|
|
|
@@ -73,6 +73,7 @@ Requires Node.js 18+. Downloads go to `codeload.github.com`, `raw.githubusercont
|
|
|
73
73
|
| `generate_egovframe_sbom` | CycloneDX 1.6 JSON (default) or SPDX 2.3 JSON (`bomFormat="spdx-json"`) SBOM without touching build files — Maven via cyclonedx-maven-plugin (hashes, licenses), Gradle from the resolved tree; baseline verdict per component (`egovframe:*` properties), optional OSV `vulnerabilities[]`; writes inside the project only, `dryRun` by default |
|
|
74
74
|
| `check_egovframe_sbom` | Check an existing CycloneDX or SPDX 2.x SBOM (format detected) without changing it: the 7 minimum elements (supplier, name, version, unique identifier, dependency relationship, author, timestamp) per component with a ready / needs-work verdict, a purl-only re-check (changed baseline verdicts, OSV findings published since generation), a diff against `baselinePath`, and with `vex=true` a CycloneDX VEX draft (`in_triage`, BOM-Link) that keeps human decisions |
|
|
75
75
|
| `check_egovframe_secure_coding` | Scan sources for security weaknesses from the implementation-stage list of the Korean secure coding guide (MOIS/KISA 2021) that static patterns can find: SQL injection, XSS, path traversal, open redirect, OS commands, XXE, weak crypto, insecure random, hard-coded secrets, cookies, error exposure, empty catch, deserialization, debug code — file, line, CWE, severity, fix and origin (common component original/modified/unmanaged, project, test); exceptions need a reason and reviewer; baseline diff; SARIF 2.1.0. A pattern-based aid, not a certified diagnosis |
|
|
76
|
+
| `generate_egovframe_deploy` | Container and deployment files from the project diagnosis: multi-stage Dockerfile (Classic war unpacked as ROOT on Tomcat 10.1 or 11.0, Boot jar on the Temurin JRE; non-root, HEALTHCHECK; or a prebuilt artifact), `.dockerignore`, `deploy/docker-compose.yml` with a mysql/mariadb/postgres container initialised from the template SQL, and Kubernetes manifests (Deployment with probes and a hardened securityContext, Service, ConfigMap, Secret, optional Ingress). Database settings via Spring environment variables (Boot) or an overridden `globals.properties` (Classic); `imageRegistry` for air-gapped registries. New files only, `dryRun` by default |
|
|
76
77
|
| `diagnose_egovframe_network` | Probe the hosts the server downloads from, classify failures (DNS, timeout, TLS, proxy auth) and prescribe environment settings as bash/cmd/PowerShell commands |
|
|
77
78
|
| `generate_agents_md` | `AGENTS.md` for AI coding tools: build/test commands, RTE and migration status, components, rules, available MCP tools (ko/en) |
|
|
78
79
|
|
|
@@ -126,6 +127,15 @@ check_egovframe_secure_coding(projectDir, baselinePath="reports/secure-prev.json
|
|
|
126
127
|
|
|
127
128
|
Projects subject to information-system audits in Korea follow the MOIS/KISA software development security guide (2021: 20 design-stage requirements, 49 implementation-stage weaknesses). The tool checks the 14 implementation-stage items that static patterns can find with controlled false positives (20 rules as data in `catalog/secure-coding-rules.json`). Comments are stripped first — all 96 MyBatis `${…}` occurrences in the official common components v4.3.2 turned out to be comments saying they were changed to `#{…}` — and EL inside custom-tag attributes, `<c:out>`, placeholders and `ENC(…)` values are not reported. With the same rules the official common components drop from 516 findings (v4.3.2) to 38 (v5.0.7), and the official web and boot-web templates have none. Exceptions go into `.egovframe-secure-ignore.json` with a reason and reviewer. `generate_egovframe_ci(supplyChain=true, secureCoding=true)` adds a `secure --fail-on high` gate step and uploads the SARIF to GitHub code scanning. Design notes: [docs/design-secure-coding.md](docs/design-secure-coding.md).
|
|
128
129
|
|
|
130
|
+
## Container and deployment files
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
generate_egovframe_deploy(projectDir, dryRun=false)
|
|
134
|
+
generate_egovframe_deploy(projectDir, dryRun=false, imageRegistry="harbor.example.go.kr/dockerhub", buildInImage=false)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The tool writes a Dockerfile, `.dockerignore`, `deploy/docker-compose.yml` and `deploy/k8s/<app>.yaml` from the project diagnosis. Classic projects read `globals.properties` directly, so their WAR is unpacked into the image and a copy of that file (database host and password replaced) is mounted as a volume or Secret; Boot projects read settings through Spring's `Environment`, so `GLOBALS_MYSQL_URL`-style environment variables are enough. In CI, the official web (Classic) and boot-web (Boot) templates pass hadolint and kubeconform, build, start, answer HTTP 200 and report healthy as uid 10001; the official simple-backend starts with MySQL through `docker compose up` and serves the initialised data. Design notes: [docs/design-deploy.md](docs/design-deploy.md).
|
|
138
|
+
|
|
129
139
|
## Running in CI (CLI mode)
|
|
130
140
|
|
|
131
141
|
```bash
|
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대화 중 즉시** 표준프레임워크
|
|
14
14
|
프로젝트를 만들고 공통컴포넌트·AI 계층을 조립할 수 있습니다. 기존 프로젝트 진단, 리포트, 안전한 upstream 재동기화도 지원합니다.
|
|
15
15
|
|
|
16
|
-
현재 v0.
|
|
16
|
+
현재 v0.45.0은 **도구 34종(title·annotations·구조화 출력 13종) + CLI 명령 11종, 공식 템플릿 22종, 설정 템플릿 21종, 5.x 전환 규칙(RTE 18모듈·클래스 331종 + 공통컴포넌트 1,089종 근거)과 적용·검증·재조립·사본 리허설, 의존성 기준(공식 parent 관리 좌표 139종+계열 6종+Spring Boot BOM 1,473종+RTE 전이 58종)·해석된 의존성 트리·CycloneDX·SPDX SBOM 생성·점검(최소 요소·비교·VEX), 개발보안 가이드 보안약점 점검(SARIF), 컨테이너·배포 산출물(Dockerfile·docker-compose·Kubernetes), GitHub·GitLab·Jenkins CI 공급망 게이트, 규칙·기준 drift 감시, 폐쇄망 오프라인 번들, 네트워크 진단·AGENTS.md 생성, 공통컴포넌트 카탈로그 190항목(리프 176종+그룹 14종, v5.0.7)**을 제공합니다.
|
|
17
17
|
|
|
18
18
|
## 진행 현황 (2026-10-13)
|
|
19
19
|
|
|
20
|
-
- **소스 기준**: v0.
|
|
20
|
+
- **소스 기준**: v0.45.0(`package.json`), 도구 34종 + CLI 명령 11종·공식 템플릿 22종·설정 템플릿 21종·카탈로그 190항목(공통컴포넌트 v5.0.7).
|
|
21
21
|
- **안전성 기준선 완료**: [PR #7](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/7)~[#16](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/16)을 반영해 provenance·strict assertion·safe remove와 프로젝트 생성/레시피/직접 조립/AI 조립/업그레이드 transaction을 갖췄습니다.
|
|
22
22
|
- **v0.22.0 추가**: `EGOVFRAME_ALLOWED_ROOTS`를 19개 도구 진입점에 적용해 `..`·symlink/junction 이탈을 차단하고, 실패 시 rollback 결과를 구조화해 반환합니다. [PR #16](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/16)은 7파일(+340/−31), 로컬 16종 스위트 전체 통과 후 병합됐습니다.
|
|
23
23
|
- **v0.23.0 추가**: `build_egovframe_project` — 생성한 프로젝트를 실제로 빌드(maven/gradle·mvnw/gradlew 자동 감지)하고 컴파일·테스트 오류를 파일/라인 단위로 구조화해 생성→검증 루프를 완성합니다. 타임아웃·로그 상한·허용 root·dryRun 포함. [PR #18](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/18)은 4파일(+458/−3), 오프라인 40단언과 실제 spawn 경로를 검증했습니다.
|
|
@@ -32,6 +32,7 @@ Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대
|
|
|
32
32
|
- **v0.30.0 추가**: `migrate_egovframe_project` 2단계(`apply=true`) — 진단의 auto 항목을 하나의 transaction 으로 실제 치환합니다(dryRun 기본, 원본 `migration-backup/` 보관, `migration-plan.json`, 실패 시 복구). 3.10 좌표·javax 픽스처를 적용한 뒤 JDK 17 `mvn compile` 통과를 CI 에서 확인합니다. `check_egovframe_dependencies` — 공식 5.x parent 2종에서 추출한 기준(관리 좌표 139종 + BOM 계열 7종, `catalog/dependency-baseline.json`)과 의존성을 대조해 기준 충족/미만/parent 관리/전환 대상/교체 필요/기준 없음으로 분류하고, 보안 설정(sec.security·CSRF·XSS 필터·보안 헤더·HTTPS 저장소) 존재 여부를 근거와 함께 보고합니다. 기본 오프라인, `offline=false` 면 OSV 취약점 조회(설계: [docs/design-dependency-check.md](docs/design-dependency-check.md)).
|
|
33
33
|
- **v0.31.0 추가**: 운영 편의 — `diagnose_egovframe_network`(도구가 쓰는 호스트 7종에 DNS·HEAD 프로브, 실패 종류 분류, HTTPS_PROXY+NODE_USE_ENV_PROXY·IPv4 우선·사내 CA 처방을 bash/cmd/PowerShell 명령으로), 모든 다운로드 실패 메시지에 같은 처방 한 줄 부착, `generate_agents_md`(진단 결과로 AI 코딩 도구용 AGENTS.md, ko/en), 영문 README(`README.en.md`)와 `EGOVFRAME_LANG=en` 영문 도구 설명, MCP Registry 메타데이터(`server.json`·`mcpName`). 기획 3개 버전(v0.29–v0.31)이 모두 완료됐습니다.
|
|
34
34
|
- **v0.32.0 추가**: MCP 프로토콜 현대화 — 도구 27종을 `registerTool` 로 전환해 `title`(ko/en)과 annotations(`readOnlyHint` 14종·`destructiveHint` 4종·`idempotentHint`·`openWorldHint`)를 `tools/list` 에 노출하고, `format=json` 을 제공하던 5종(`diagnose_egovframe_project`·`validate_egovframe_project`·`migrate_egovframe_project`·`check_egovframe_dependencies`·`diagnose_egovframe_network`)은 `outputSchema` 와 `structuredContent` 를 함께 돌려줍니다(기존 `text` 유지). 테스트 이식성 가드(`scripts/check-test-portability.mjs`, 게이트 포함)와 플랫폼 주입 단언으로 Windows 전용 실패 재발을 막습니다.
|
|
35
|
+
- **v0.45.0 추가**: 컨테이너·배포 산출물 — 새 도구 **`generate_egovframe_deploy`**(34번째)가 진단(빌드 도구·war/jar·Boot·웹·actuator·DbType·설정 파일)으로 Dockerfile(멀티 스테이지 Maven/Gradle 빌드 → Classic war 는 Tomcat 10.1 에 ROOT 로 풀어서, Boot jar 는 Temurin JRE — 비루트·HEALTHCHECK·Asia/Seoul·UTF-8, `buildInImage=false` 면 미리 빌드한 산출물), `deploy/docker-compose.yml`(앱 + DbType 별 mysql·mariadb·postgres 컨테이너, 템플릿 DDL·DATA 초기화), Kubernetes 매니페스트(Deployment 프로브·비루트 securityContext·리소스, Service, ConfigMap, Secret, 선택 Ingress)를 만듭니다. DB 설정은 Boot 는 Spring 환경 변수(`Globals.mysql.Url` → `GLOBALS_MYSQL_URL`), Classic 은 `globals.properties` 사본을 볼륨·Secret 으로 덮어쓰고, 기반 이미지는 `imageRegistry`(Dockerfile `BASE_REGISTRY`)로 사내 레지스트리에서 받습니다. 공식 web(Classic)·boot-web(Boot) 템플릿은 hadolint 경고 0·kubeconform 통과 후 실제 `docker build` → 기동 → HTTP 200·healthy·uid 10001, 공식 simple-backend 는 `docker compose up` 으로 MySQL 초기화 데이터가 API 로 나오는 것까지 CI 에서 확인합니다.
|
|
35
36
|
- **v0.44.0 추가**: 납품 전 보안약점 점검 — 새 도구 **`check_egovframe_secure_coding`**(33번째)이 행정안전부·KISA 「소프트웨어 개발보안 가이드」(2021, 구현단계 보안약점 49개) 중 정적 패턴으로 오탐을 통제할 수 있는 14개 항목(규칙 20종 — SQL 삽입·크로스사이트 스크립트·경로 조작·URL 자동접속·운영체제 명령어·XML 외부개체·취약한 암호·부적절한 난수·하드코드된 중요정보·쿠키·오류 정보노출·오류상황 대응 부재·역직렬화·디버그 코드)을 소스에서 찾습니다. 규칙은 데이터(`catalog/secure-coding-rules.json`)이고, 주석(공식 공통컴포넌트 매퍼의 `${…}` 96건은 전부 "#{} 로 변경" 주석)·`<c:out>`·커스텀 태그 속성·자리표시자 같은 오탐을 실측으로 걸러 냅니다. 항목마다 공통컴포넌트 원본·수정본·매니페스트 없음·프로젝트·테스트 출처를 붙이고, 예외 파일(사유·검토자 필수)·이전 결과와 비교(새 항목만)·**SARIF 2.1.0**(GitHub code scanning, OASIS 공식 스키마 통과)을 지원합니다. 같은 규칙으로 공식 공통컴포넌트는 v4.3.2 516건 → **v5.0.7 38건**(`escapeXml="false"` 304 → 6)이라 업그레이드 효과가 숫자로 보이고, 공식 web·boot-web 템플릿은 0건입니다. 평가서 4절, CLI `secure`, `generate_egovframe_ci(secureCoding=true)` 게이트 단계(GitHub 은 SARIF 업로드).
|
|
36
37
|
- **v0.43.0 추가**: CI·제출 형식 다변화 — `generate_egovframe_ci(platform="gitlab"|"jenkins")` 가 GitHub Actions 와 같은 빌드·공급망 게이트를 `.gitlab-ci.yml`(JUnit·CycloneDX 리포트)·`Jenkinsfile`(Declarative, Linux `sh`·Windows `bat` 공용)로 만들고, 게이트에 **SBOM 최소 요소 점검**(`sbom-check --fail-on minimum`)을 세 플랫폼 모두 추가했습니다. 사내 npm 레지스트리(`npmRegistry`)·오프라인 번들 경로(`offlineBundle`)는 환경 변수로 들어가고, 공급자 단서가 없으면 게이트가 실패할 것을 미리 알립니다. SBOM 은 **SPDX 2.3 JSON** 으로도 냅니다 — `generate_egovframe_sbom(bomFormat="spdx-json")` 이 패키지·`DEPENDS_ON` 관계(의존 없음은 `NONE`)·purl·공급자·creators·created·체크섬·선언 라이선스·OSV 권고·기준 판정(annotations)을 담고, `check_egovframe_sbom` 은 형식을 자동 판별해 SPDX 도 같은 최소 요소 7종·재판정·비교로 점검합니다(VEX 는 CycloneDX 만). 공식 web 템플릿을 실제 Maven 으로 만든 SBOM 은 SPDX 로 바꿔도 component 67·최소 요소 판정(요소별 충족 수까지)이 같고 SPDX ↔ CycloneDX 비교 차이 0 이며, 생성 `.gitlab-ci.yml` 은 GitLab 공식 CI 스키마를, SPDX 출력은 공식 spdx-spec v2.3 스키마를 통과합니다. 도구 수는 32종 그대로입니다.
|
|
37
38
|
- **v0.42.0 추가**: 폐쇄망(망분리) 운영 — 새 도구 **`prepare_egovframe_offline_bundle`**(32번째)이 인터넷 PC 에서 OSV Maven 취약점 전체 덤프(≈10MB, 7,190건)·고정 공통컴포넌트 아카이브(sha256 검증)·Initializr zip 템플릿(고정 지문)·재조립용 공식 공통컴포넌트 태그 22개 미러(≈37MB)를 한 디렉터리에 담고 `bundle-manifest.json`(파일별 sha256·크기·출처 URL·받은 시각)을 씁니다. 폐쇄망 PC 에서 `EGOVFRAME_OFFLINE_BUNDLE` 만 지정하면 다운로드는 번들 파일을 지문 검증 후 쓰고(변조 거부), `offline=false` 의 OSV 조회는 로컬 덤프로 판정하며(스냅숏 기준일 표시, 30일 넘으면 경고), 재조립은 번들 미러를 씁니다. 같은 날 받은 덤프의 로컬 판정이 온라인 OSV 와 **질의 73/73·ID 134/134 일치**했고, 네트워크를 막은 상태에서 프로젝트 생성·공통컴포넌트 조립·재조립 미리보기·SBOM 재점검이 번들만으로 끝나는 것(네트워크 시도 0건)을 CI 에서 확인합니다. CLI `bundle`, 네트워크 진단의 오프라인 번들 처방.
|
|
@@ -44,7 +45,7 @@ Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대
|
|
|
44
45
|
- **v0.35.0 추가**: 릴리스 자동화 — `main` 에서 CI 가 성공하면 `release.yml` 이 네 조건(`server.json` 버전 일치·변경 이력 항목·태그 없음·npm 미배포)을 검사해 npm(OIDC trusted publishing, provenance 자동) → 그 커밋에 태그 → GitHub Release(변경 이력에서 추출) → MCP Registry(GitHub OIDC) 순으로 게시합니다. 사람은 PR 병합만 합니다. 게이트에 `test:release`(판정 함수·릴리스 노트·`npm pack` 내용·크기 상한)와 `server.json` 설명 100자 제한(레지스트리 검증 조건 — 실제 `mcp-publisher validate` 로 발견한 결함 수정)을 추가했습니다.
|
|
45
46
|
- **v0.34.0 추가**: 의존성 기준 완성 + 규칙·기준 drift 감시 — `check_egovframe_dependencies` 의 기준에 **Spring Boot BOM 전체**(`spring-boot-dependencies` 3.5.6 직접 항목 + BOM import 44종을 한 단계 풀어 1,473 좌표)와 **RTE 모듈 18종의 전이 의존성**(58 좌표, 어느 모듈이 끌어오는지)을 더해 항목마다 기준 출처(parent 직접·계열·Boot BOM·RTE 전이·전환 규칙)를 표시하고, 국내 벤더·기관 배포 좌표는 `vendor` 로, EOL·이전 좌표(옛 MySQL/Oracle 드라이버·Jackson 1·xmlbeans·Ehcache 2·HttpClient 4·ANTLR 3·Spring Social)는 교체 규칙으로 분류해 공식 공통컴포넌트 3.10 pom 의 '기준 없음'을 17 → 1 로 줄였습니다. `sync_egovframe_templates` 가 동봉 규칙·기준의 upstream drift(`egovframe-runtime`·공통컴포넌트 새 태그, 공식 parent 새 버전, 고정 pom sha256 변화)를 갱신 절차와 함께 보고합니다. 기준 parent 를 5.0.2 로 올렸습니다.
|
|
46
47
|
- **v0.33.0 추가**: `migrate_egovframe_project` 3단계 — `verify=true` 가 compile 을 실행해 컴파일 오류를 수동 항목과 연결하고 "처리하면 해결될 오류 수" 순 작업 목록을 냅니다(javac `symbol:`/`location:` 파싱). 규칙 카탈로그(schemaVersion 2)에 공통컴포넌트 3.x→5.x 대응표(`egovframe-common-components` v3.10.0↔v5.0.6, 제거 54·이동 4)를 추가해 `egovframework.com.*` 제거·이동 클래스를 진단하고, 3.x 공통컴포넌트 소스가 섞인 프로젝트에는 컴포넌트 단위 재조립 권고(`skipComponents` 로 치환 제외)를 냅니다.
|
|
47
|
-
- **다음 계획**: v0.
|
|
48
|
+
- **다음 계획**: v0.46 5.x 최신 유지 + 생성 CI 실행 검증. [다음 버전 기획](#다음-버전-기획-v044v046) 참조.
|
|
48
49
|
- **배포 상태**: npm 최신 배포 버전은 상단 npm 배지와 [npm 패키지 페이지](https://www.npmjs.com/package/egovframe-scaffold-mcp)를 단일 출처로 확인합니다. 이 문서의 버전 표기는 저장소 소스(`package.json`) 기준이며, git 태그 `vX.Y.Z`가 해당 배포본의 커밋을 가리킵니다.
|
|
49
50
|
|
|
50
51
|
## 제공 도구
|
|
@@ -83,6 +84,7 @@ Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대
|
|
|
83
84
|
| `generate_egovframe_sbom` | **SBOM 생성** — Maven/Gradle 프로젝트의 CycloneDX 1.6 JSON(기본) 또는 SPDX 2.3 JSON(`bomFormat="spdx-json"`)을 빌드 파일 변경 없이 생성(Maven: cyclonedx-maven-plugin makeAggregateBom, 해시·라이선스 포함 / Gradle: 해석된 의존성 트리로 구성). `enrich` 로 component 마다 기준 판정(`egovframe:status·basis·baseline`) 속성, `offline=false` 로 OSV 취약점을 `vulnerabilities[]` 에 포함. 출력은 프로젝트 안 경로(기본 `sbom/bom.cdx.json`, SPDX 는 `sbom/bom.spdx.json`), 기존 파일은 `overwrite` 없이는 거부, `dryRun`(기본)은 계획만 |
|
|
84
85
|
| `check_egovframe_sbom` | **SBOM 점검** — 기존 CycloneDX·SPDX 2.x SBOM(형식 자동 판별)의 최소 요소 7종(공급자·구성요소명·버전·고유식별자·의존관계·작성자·생성 시각) 충족 여부와 빠진 component, 빌드 도구 없이 purl 로 기준 재판정·OSV 재조회(생성 이후 새 취약점), `baselinePath` 와 비교(추가·제거·버전·판정·취약점), `vex=true` 면 CycloneDX VEX 초안(`in_triage`, BOM-Link, 사람의 판단 보존, CycloneDX 입력만). SBOM 파일은 바꾸지 않음 |
|
|
85
86
|
| `check_egovframe_secure_coding` | **보안약점 점검** — 행안부·KISA 개발보안 가이드(2021) 구현단계 보안약점 중 정적 패턴 14개 항목(규칙 20종: SQL 삽입·XSS·경로 조작·URL 자동접속·OS 명령·XXE·약한 암호·난수·하드코드 정보·쿠키·오류 노출·빈 catch·역직렬화·디버그 코드)을 파일·라인·CWE·등급·조치·출처(공통컴포넌트 원본/수정본/매니페스트 없음·프로젝트·테스트)로. 예외 파일(사유·검토자), `baselinePath` 비교(새 항목), SARIF 2.1.0(`format=sarif`·`sarifPath`). 진단 보조이며 공인 진단을 대체하지 않음 |
|
|
87
|
+
| `generate_egovframe_deploy` | **컨테이너·배포 산출물** — Dockerfile(멀티 스테이지·Classic war → Tomcat 10.1(Servlet 6.1+ 는 11.0) ROOT, Boot jar → Temurin JRE, 비루트·HEALTHCHECK, `buildInImage=false` 면 미리 빌드한 산출물)·`.dockerignore`·`deploy/docker-compose.yml`(DbType 별 DB 컨테이너·초기화 SQL)·`deploy/k8s/<app>.yaml`(Deployment·Service·ConfigMap·Secret·선택 Ingress). DB 설정은 Boot 환경 변수·Classic 설정 파일 덮어쓰기, `imageRegistry`(폐쇄망), `dockerfilePath`(템플릿 자체 Dockerfile 과 공존). 새 파일만, `dryRun` 기본 |
|
|
86
88
|
| `generate_agents_md` | 프로젝트 진단 결과로 AI 코딩 도구용 `AGENTS.md` 생성 — 빌드·테스트 명령(래퍼 감지), RTE·5.x 전환 상태, DbType, 기본 패키지·설정 디렉터리, 설치 컴포넌트(매니페스트 여부), 규칙(좌표·백업 디렉터리·비밀 정보·의존성 기준), MCP 도구 목록. 기존 파일은 `overwrite` 없이는 거부, `dryRun`, ko/en, 파일명 변경(`CLAUDE.md` 등) |
|
|
87
89
|
|
|
88
90
|
### create_egovframe_project 파라미터
|
|
@@ -235,6 +237,27 @@ check_egovframe_sbom(projectDir, baselinePath="sbom/bom-2026Q3.cdx.json", offlin
|
|
|
235
237
|
|
|
236
238
|
제출처가 SPDX 를 요구하면(가이드라인 1.0 은 SPDX·CycloneDX 를 모두 언급) `bomFormat="spdx-json"` 으로 같은 내용을 SPDX 2.3 JSON 으로 냅니다. 체크섬·선언 라이선스(이름만 있는 라이선스는 `LicenseRef-` + `hasExtractedLicensingInfos`)·OSV 권고(`SECURITY`/`advisory` externalRefs)·기준 판정(`egovframe:*` annotations)도 옮기므로, SPDX 로 낸 뒤 `check_egovframe_sbom(sbomPath="sbom/bom.spdx.json")` 의 재판정·비교가 CycloneDX 와 같은 결과를 냅니다. SPDX 3.0, 취약점 판단 자동화(VEX 상태는 사람의 결정), 중앙 저장소 제출 API 는 범위 밖입니다.
|
|
237
239
|
|
|
240
|
+
### 컨테이너·배포 산출물 (`generate_egovframe_deploy`)
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
generate_egovframe_deploy(projectDir) # 미리보기(dryRun) — 파일 내용과 사용법
|
|
244
|
+
generate_egovframe_deploy(projectDir, dryRun=false) # Dockerfile·.dockerignore·deploy/docker-compose.yml·deploy/k8s/<app>.yaml
|
|
245
|
+
generate_egovframe_deploy(projectDir, dryRun=false, imageRegistry="harbor.example.go.kr/dockerhub", buildInImage=false) # 폐쇄망
|
|
246
|
+
generate_egovframe_deploy(projectDir, dryRun=false, dockerfilePath="deploy/Dockerfile", ingressHost="egov.example.go.kr") # 템플릿 자체 Dockerfile 과 공존
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
| 프로젝트 | 실행 이미지 | DB 설정 주입 | 헬스체크 |
|
|
250
|
+
|---|---|---|---|
|
|
251
|
+
| Classic(war) — web·template·mobile 등 | `tomcat:10.1-jdk17-temurin`(Servlet 6.1 이상 직접 선언 시 11.0), WAR 를 `webapps/ROOT` 로 풀어 둠 | `globals.properties` 사본(DB 호스트·비밀번호만 바꿈)을 compose 볼륨·Kubernetes Secret 으로 `WEB-INF/classes/…` 에 덮어씀 | `/` |
|
|
252
|
+
| Boot(jar) — boot-web·simple-backend·batch-web 등 | `eclipse-temurin:17-jre`, `exec java $JAVA_OPTS -jar` | Spring 환경 변수(`Globals.mysql.Url` → `GLOBALS_MYSQL_URL`, `spring.datasource.url` → `SPRING_DATASOURCE_URL`) | actuator 프로브가 있으면 `/actuator/health/liveness`·`readiness`, 아니면 `/` |
|
|
253
|
+
| Boot 배치(웹 없음) | 위와 같음 | 위와 같음 | 없음(포트·Service 없이, Job·CronJob 안내) |
|
|
254
|
+
| 멀티 모듈·라이브러리 jar·빌드 파일 없음 | — | — | 만들지 않고 이유를 돌려줌 |
|
|
255
|
+
|
|
256
|
+
- **DB**: mysql 8.4·mariadb 11.4·postgres 16 은 compose 에 DB 컨테이너를 붙이고 템플릿의 DDL·DATA 를 초기화 스크립트로 마운트합니다(`--lower-case-table-names=1`, utf8mb4). 오라클·티베로·알티베이스·큐브리드는 연결 정보만 쓰고, Kubernetes 는 클러스터 밖 DB 를 가정해 `CHANGE_ME_DB_HOST`·Secret 자리표시자를 둡니다.
|
|
257
|
+
- **보안 기본값**: 비루트(uid 10001)·`runAsNonRoot`·`seccompProfile RuntimeDefault`·`allowPrivilegeEscalation false`·`capabilities drop ALL`·리소스 요청/제한·startup/readiness/liveness 프로브.
|
|
258
|
+
- **폐쇄망**: 기반 이미지는 `ARG BASE_REGISTRY`(기본 `imageRegistry`)로 사내 레지스트리에서 받고, `# syntax` 지시어를 쓰지 않아 BuildKit 프론트엔드 이미지를 받지 않습니다. 컨테이너 안에서 Maven 저장소에 닿지 않으면 `buildInImage=false` 로 호스트에서 빌드한 산출물만 담습니다. 반입할 이미지: `tomcat:10.1-jdk17-temurin`·`eclipse-temurin:17-jre`·`eclipse-temurin:17-jdk`(미리 빌드할 때)·`maven:3.9-eclipse-temurin-17`(이미지 안에서 빌드할 때)·DB 이미지(`docker save`/`docker load` 또는 사내 Harbor 프록시 캐시).
|
|
259
|
+
- **실측**: 공식 web(Classic)·boot-web(Boot) 템플릿은 hadolint 경고 0·kubeconform(쿠버네티스 1.31) 통과, 실제 빌드·기동에서 HTTP 200·healthy·uid 10001. 공식 simple-backend(Boot + MySQL)는 compose 로 readiness UP·`/mainPage` 가 MySQL 초기화 데이터를 돌려주고, 공식 portal-site(Classic + MySQL)는 설정 파일 덮어쓰기로 MySQL 사용자 `pst` 에 연결해 메인 페이지가 열립니다. 설계: [docs/design-deploy.md](docs/design-deploy.md).
|
|
260
|
+
|
|
238
261
|
### 납품 전 보안약점 점검 (`check_egovframe_secure_coding`)
|
|
239
262
|
|
|
240
263
|
감리 대상 사업은 「소프트웨어 개발보안 가이드」(행정안전부·KISA, 2021)의 구현단계 보안약점 49개를 지켜야 하고, 납품 전 진단에서 지적된 항목은 재작업이 됩니다. 이 도구는 그중 정적 패턴으로 근거 있게 찾을 수 있는 항목을 먼저 찾아 진단 전에 고칠 목록을 만듭니다(데이터 흐름 분석이 아닌 패턴 기반 진단 보조 — 공인 진단을 대체하지 않습니다).
|
|
@@ -467,6 +490,8 @@ export EGOVFRAME_OFFLINE_BUNDLE=/opt/egov-bundle # cmd: setx EGOVFRAME_OF
|
|
|
467
490
|
| `templates/<id>.zip` (Initializr zip 템플릿) | `create_egovframe_project` 의 zip 템플릿 | 템플릿 고정 sha256·크기 |
|
|
468
491
|
| `origin/…git` (공식 태그 22개, 내용 포함) | 재조립·리허설의 원본 태그 식별·패치 | 원격 조회 없이 로컬 미러만 |
|
|
469
492
|
|
|
493
|
+
컨테이너 이미지는 번들에 담지 않습니다 — `generate_egovframe_deploy` 가 쓰는 기반 이미지는 `docker save`/`docker load` 나 사내 레지스트리로 반입하고 `imageRegistry` 로 지정하세요(「컨테이너·배포 산출물」 참조).
|
|
494
|
+
|
|
470
495
|
번들 파일은 쓸 때마다 manifest 의 sha256·크기와 대조하고 다르면 거부합니다. 번들에 없는 URL 은 평소처럼 네트워크로 가므로, 저장소 템플릿(`simple-backend` 등)이 필요하면 `--templates` 로 지정해 함께 담으세요. Maven 의존성 jar 는 번들에 넣지 않습니다 — 기관 Nexus·Artifactory 미러를 `settings.xml` 에 지정하세요. upstream 대조 도구(`sync_egovframe_*`)와 가이드 문서 조회는 인터넷이 필요합니다. 설계: [docs/design-offline-bundle.md](docs/design-offline-bundle.md).
|
|
471
496
|
|
|
472
497
|
## 리소스·프롬프트 (MCP Resources/Prompts)
|
|
@@ -534,10 +559,11 @@ curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.Eri
|
|
|
534
559
|
- 네트워크 진단 레벨: 오류 분류 8종, 프로브·DNS 주입으로 7가지 시나리오(전부 도달, 405/404/407/503, 프록시+타임아웃, 프록시 없음+IPv6 혼재, TLS, DNS, 필터)의 처방·안내·자격 증명 가림·셸별 명령을 단언하고, `fetchWithTimeout` 실패 메시지에 처방이 붙는지 확인 (`npm run test:network`, 33단언, 네트워크 불필요). 실제 호스트 7종 프로브는 CI 통합 job (`npm run test:network-live`)
|
|
535
560
|
- AGENTS.md 레벨: 5.x(매니페스트·래퍼·컴포넌트 2종·백업 디렉터리)와 3.x gradle 픽스처, 빈 디렉터리에서 사실 수집·ko/en 렌더링·dryRun 무기록·기존 파일 거부·overwrite·파일명 검증·staging 정리 (`npm run test:agents-md`, 22단언, 네트워크 불필요)
|
|
536
561
|
- 레지스트리 메타데이터 레벨: `server.json` ↔ `package.json` 의 이름(`mcpName`)·버전·npm 좌표·스키마 URL·환경변수 문서화 정합 (`npm run test:registry`, 네트워크 불필요)
|
|
537
|
-
- 프로토콜 레벨(추가): `EGOVFRAME_LANG=en` 으로 띄운 서버의 도구
|
|
562
|
+
- 프로토콜 레벨(추가): `EGOVFRAME_LANG=en` 으로 띄운 서버의 도구 34종 설명이 모두 영문이고 표에 빠진 도구가 없는지, `tools/list` 에 `title`·annotations(readOnly 13종·destructive 5종)·`outputSchema` 13종이 노출되는지 확인 (`npm run test:handshake`)
|
|
538
563
|
- SBOM·해석 레벨: Maven·Gradle 트리 파서(실제 출력 픽스처), `resolve=true` 의 전이 항목·경로·선언/해석 차이·실패 경로, CycloneDX 문서 구성·보강·취약점 병합·출력 경로 거부·overwrite·dryRun (`npm run test:dependencies`, `npm run test:sbom`, 네트워크 불필요); CI 통합이 공식 `egovframe-web` 템플릿을 실제 Maven 으로 해석하고 SBOM 을 생성 (`npm run test:sbom-live`)
|
|
539
564
|
- 오프라인 번들 레벨: Maven 버전 비교(숫자·한정자 순서·같은 표기), OSV 범위(여러 구간·fixed·last_affected), 덤프 색인(Maven 만·철회 제외·GIT 범위 무시)과 querybatch 같은 모양의 결과, 번들 만들기(dryRun·manifest 지문·고정 지문 불일치 거부·실패 시 무흔적·덮어쓰기 거부·로컬 git 원본 미러), 번들 읽기(fetch 훅·POST 제외·변조 거부·깨진 manifest), 원본 미러(태그·커밋·트리·blob, 없는 태그 안내), OSV 출처 노트와 30일 경고, 의존성 점검 연동, 네트워크 진단 처방 (`npm run test:offline-bundle`, 30단언, 네트워크 불필요); CI 통합이 실제 번들을 만들고 로컬 OSV 와 온라인 OSV 를 대조(97% 이상 일치 요구, 실측 100%)한 뒤 fetch 를 막고 생성·조립·재조립 미리보기·SBOM 재점검을 번들만으로 확인 (`npm run test:bundle-live`)
|
|
540
565
|
- 전환 리허설 레벨: pom 맞춤(parent 추가·교체, 누락 좌표, 버전·scope, parent 관리 속성, 주석·dependencyManagement·plugin 제외, 멱등), 누락 패키지·연쇄·수동 항목 분류와 후보 좌표, Maven fork 형식·경고 제외 파서, 사본 제외 규칙·원본 지문 불변, 단계 순서(재조립 → 적용 → 컴파일 → pom 맞춤 → 컴파일)·javac 상한 해제 인자, 단계 실패 계속, 기록과 평가서 6절 (`npm run test:rehearse`, 38단언, 네트워크 불필요); CI 통합이 공식 공통컴포넌트 v4.3.2 전체 트리를 실제로 리허설해 재조립·적용 수와 오류 6,003(±10%, 누락 패키지·연쇄 95% 이상)→≤5 를 단언 (`npm run test:rehearse-live`)
|
|
566
|
+
- 배포 산출물 레벨: 분석(Classic war·Boot jar·Gradle 배치·Boot `server.port`·context-path·actuator 프로브·Servlet 6.1 → Tomcat 11·`Globals.<type>.Url`/`Globals.Url`/`spring.datasource.url`·DB 별칭·초기화 SQL 선택·지원 안 함 3종), Dockerfile(멀티 스테이지·캐시 마운트·5xx 재시도·WAR 풀기·비루트·HEALTHCHECK·레지스트리 ARG·미리 빌드), 생성 `docker-compose.yml` 의 공식 Compose 스키마(compose-spec 고정물) 통과·DB 컨테이너·초기화 순서·설정 볼륨, Kubernetes(보안 설정·프로브·ConfigMap/Secret 분리·Classic Secret subPath·Ingress), 입력 주입 거부·새 파일만·부분 충돌 시 무기록 (`npm run test:deploy`, 49단언, 네트워크 불필요). 실제 빌드·기동·compose 는 CI 통합 `test:deploy-live`(hadolint·kubeconform 포함)
|
|
541
567
|
- 보안약점 점검 레벨: 규칙 20종 데이터 검증(가이드 7개 유형·항목·CWE·컴파일), Java·XML·JSP 주석 제거(문자열·텍스트 블록 보존, HTML 주석 안 EL 유지), 규칙마다 양성·음성 픽스처(오탐 방지 사례 — `<c:out>`·커스텀 태그 속성·`fn:escapeXml`·상수 c:out·`ENC()`·자리표시자·파라미터 이름·보호 설정 있는 XML 파서 등), ORDER BY 강등·요청 값 동반 승격·보안 문맥 난수, 출처(매니페스트 원본·수정본·매니페스트 없음), 예외 파일(사유·검토자 없으면 미적용), baseline(JSON·SARIF, 줄 이동에도 같은 항목), SARIF 2.1.0 OASIS 공식 스키마(테스트 고정물) (`npm run test:secure-coding`, 52단언, 네트워크 불필요). 공식 공통컴포넌트 v3.10.0·v4.3.2 의 규칙별 건수는 회귀 코퍼스 기대값(±1%), 공식 템플릿은 CI 통합 `test:secure-live`(Initializr 고정 템플릿은 단언, 저장소 브랜치 템플릿은 drift 로그)
|
|
542
568
|
- SPDX 레벨: 실제 플러그인 출력 고정물을 SPDX 2.3 으로 바꿔 공식 spdx-spec v2.3 스키마 통과, 패키지 68·`DEPENDS_ON` 간선 수 = CycloneDX 의존 간선·의존 없음 `NONE`·체크섬·라이선스(LicenseRef)·DESCRIBES, 되읽었을 때 최소 요소 요소별 충족 수·purl·의존 간선·기준 판정(annotations)·취약점(advisory) 보존, 다른 도구 모양 SPDX(`DEPENDENCY_OF`·`NOASSERTION`·여러 DESCRIBES·Person/Organization/Tool)의 필드 대응, `bomFormat="spdx-json"` 생성·SPDX 입력 점검(형식 판별·CycloneDX 와 같은 판정·SPDX↔CycloneDX 비교 차이 0·VEX 거부)·outputSchema (`npm run test:spdx`, 43단언, 네트워크 불필요). 실제 Maven SBOM 의 SPDX 판정 일치는 CI 통합 `test:sbom-live` 가 확인
|
|
543
569
|
- SBOM 운영 레벨: 실제 플러그인 출력 고정물(공식 web 템플릿 67 component)로 최소 요소 7종(빠진 항목별)·공급자 표 보완·purl 재판정·가짜 OSV(새/사라진 ID)·비교(추가·제거·버전·판정·취약점)·VEX 생성/판단 보존/같은 ID 의 새 영향 component·깨진 VEX 중단을 단언하고, 생성 SBOM·VEX 를 CycloneDX 1.6 공식 JSON 스키마(specification 1.6.1)로 검증 (`npm run test:sbom-check`, 54단언, 네트워크 불필요); CI 통합이 실제 SBOM 으로 보완 필요(주 component 공급자·작성자) → supplier·author 로 보완(환경에 따른 다른 공백은 `info:` 로그), 두 SBOM 비교 차이 0, 실제 OSV VEX 초안·판단 보존을 확인 (`npm run test:sbom-live`)
|
|
@@ -598,7 +624,7 @@ v0.32–v0.34 로 MCP 프로토콜 현대화, 5.x 전환 3단계(검증)와 공
|
|
|
598
624
|
| **v0.42 완료** | 폐쇄망 운영: 오프라인 번들 | `prepare_egovframe_offline_bundle`: OSV Maven 덤프·고정 컴포넌트 아카이브·Initializr zip·원본 태그 미러 + sha256 manifest, `EGOVFRAME_OFFLINE_BUNDLE` 로 모든 다운로드·OSV 조회를 번들로 대체(스냅숏 기준일 표시·오래됨 경고) |
|
|
599
625
|
| **v0.43 완료** | CI·제출 형식 다변화 | `generate_egovframe_ci(platform=gitlab\|jenkins)`(GitLab 공식 스키마 통과·Declarative Jenkinsfile)와 세 플랫폼 게이트에 `sbom-check --fail-on minimum`, 사내 npm 레지스트리·오프라인 번들 변수, SBOM SPDX 2.3 JSON 출력·점검(공식 spdx-spec v2.3 스키마, 실제 Maven SBOM 에서 CycloneDX 와 판정 일치) |
|
|
600
626
|
| **v0.44 완료** | 보안약점 점검 | `check_egovframe_secure_coding`: 행안부·KISA 개발보안 가이드 구현단계 보안약점 중 정적 패턴 항목(SQL 삽입·XSS·경로 조작·XXE·약한 암호·하드코드 정보·오류 노출 등)을 데이터 규칙으로, 공통컴포넌트 출처 구분, SARIF 2.1.0·CI 게이트·평가서 4절 |
|
|
601
|
-
| **v0.45
|
|
627
|
+
| **v0.45 완료** | 컨테이너·배포 산출물 | `generate_egovframe_deploy`: Classic war(Tomcat 10.1)·Boot jar 멀티 스테이지 Dockerfile, DbType 별 docker-compose, Kubernetes 매니페스트, 폐쇄망 레지스트리 — hadolint·kubeconform·실제 빌드·기동 검증 |
|
|
602
628
|
| **v0.46 계획** | 5.x 최신 유지 + CI 실행 검증 | `migrate_egovframe_project(target="5.0.2")` 로 5.x 프로젝트 parent·RTE 갱신(적용·컴파일 검증), 생성 `Jenkinsfile`·`.gitlab-ci.yml` 을 실제 Jenkins·GitLab 실행기로 검증 |
|
|
603
629
|
|
|
604
630
|
## 다음 버전 기획 (v0.44–v0.46)
|
|
@@ -651,6 +677,8 @@ v0.43.0 까지 끝난 상태(2026-10-12, 도구 32종 + CLI 명령 10종, npm 0.
|
|
|
651
677
|
|
|
652
678
|
**완료 정의**: 도구 34종, `docs/design-deploy.md`, README 「컨테이너·배포 산출물」 절(폐쇄망 이미지 반입 포함)
|
|
653
679
|
|
|
680
|
+
**결과(2026-10-13) — 완료**: `src/deploy.ts`(분석·렌더러)와 규칙 데이터 `catalog/deploy-rules.json`(기반 이미지·DB 서비스·외부 DB·지원 안 함). 실측: 공식 web(Classic)·boot-web(Boot) 템플릿의 Dockerfile 은 hadolint 2.14 경고 0, 매니페스트는 kubeconform 0.7(쿠버네티스 1.31) 통과, 빌드·기동 후 HTTP 200·HEALTHCHECK healthy·실행 uid 10001. 공식 simple-backend(Boot + MySQL 8.4)는 `docker compose up` 후 readiness UP·`/mainPage` 가 초기화 데이터(공지 게시물)를 돌려줘 환경 변수 주입이 동작함을 확인했고, 공식 portal-site(Classic + MySQL)는 설정 파일 덮어쓰기로 사용자 `pst`·비밀번호 `CHANGE_ME` 로 연결돼 메인 페이지가 열렸습니다. 계획과 달라진 점: (a) **Classic 의 DB 설정은 환경 변수로 덮을 수 없었습니다** — Classic 템플릿은 `EgovProperties` 가 `globals.properties` 를 직접 읽어서, WAR 를 이미지 안에서 풀어 두고 설정 파일 사본을 볼륨·Secret 으로 덮어쓰는 방식으로 바꿨습니다(Boot 템플릿은 Spring `Environment` 로 읽어 환경 변수가 그대로 동작). (b) Classic 템플릿의 키는 `Globals.Url`·`Globals.UserName`(Boot 템플릿은 `Globals.<DbType>.Url`)이라 두 형식을 모두 읽습니다. (c) 공식 simple-backend·portal-site 등은 **자체 Dockerfile·compose·k8s 를 갖고 있어** 덮어쓰지 않고 알리며, `dockerfilePath`(예: `deploy/Dockerfile`, `.dockerignore` 는 BuildKit 규칙대로 `<경로>.dockerignore`)로 공존시킵니다. (d) 폐쇄망을 위해 `# syntax` 지시어를 쓰지 않고(BuildKit 내장 프론트엔드), 컨테이너 안에서 Maven 저장소에 닿지 않는 환경을 위해 `buildInImage=false`(미리 빌드한 산출물) 경로를 더했습니다 — 개발 환경 실측도 이 경로로 했고, CI 는 이미지 안에서 빌드합니다. (e) Servlet 6.1 이상을 직접 선언한 프로젝트는 Tomcat 11.0 을 고릅니다(5.x 공식 parent 는 Servlet 5.0·fileupload2 servlet6 이라 기본 10.1).
|
|
681
|
+
|
|
654
682
|
### v0.46.0 — 5.x 최신 유지 + 생성 CI 실행 검증
|
|
655
683
|
|
|
656
684
|
**목표**: 공식 Initializr 템플릿은 여전히 parent 5.0.0 으로 만들어져(공식 parent 최신 5.0.2) 새 프로젝트가 만들어지는 순간부터 기준 미만입니다. 3.x/4.x → 5.x 전환(`migrate`)은 있지만 **5.x 안의 갱신**(parent·RTE 5.0.x·Spring Boot 패치, 공통컴포넌트 5.0.x → 5.0.7)은 사람이 합니다. 또 v0.43 의 GitLab·Jenkins 설정은 스키마·구조만 확인했고 실제 빌드 서버에서 돌려 보지 않았습니다.
|
|
@@ -1054,6 +1082,7 @@ v0.28.1 까지의 상태에서 다음 세 릴리스를 아래 순서로 진행
|
|
|
1054
1082
|
|
|
1055
1083
|
## 변경 이력
|
|
1056
1084
|
|
|
1085
|
+
- **0.45.0** — 컨테이너·배포 산출물(도구 33 → 34종). (1) 새 도구 `generate_egovframe_deploy(projectDir, targets=["docker","compose","k8s"], jdk="17"|"21", imageRegistry="docker.io/library", image?, appName?, buildInImage=true, dockerfilePath="Dockerfile", ingressHost?, dryRun=true, format)`: `src/deploy.ts` — `analyzeDeploy`(빌드 도구·래퍼·war/jar·Boot·웹·actuator 프로브·Tomcat 10.1/11·포트·컨텍스트 경로·DB: `Globals.<type>.Url`/`Globals.Url`/`spring.datasource.url`, 별칭, 초기화 SQL, 지원 안 함 3종), `renderDockerfile`(멀티 스테이지 Maven·Gradle 캐시 마운트·Maven 5xx 재시도·plain 산출물 제외·WAR 풀기·비루트 uid 10001·TZ·UTF-8·MaxRAMPercentage·HEALTHCHECK·`ARG BASE_REGISTRY`·미리 빌드 경로), `.dockerignore`(미리 빌드 시 산출물 화이트리스트), `renderCompose`(DB 컨테이너 mysql 8.4·mariadb 11.4·postgres 16, 초기화 마운트, 헬스체크 후 기동, `.env.example`), `renderClassicConfig`(설정 사본), `renderKubernetes`(ConfigMap·Secret·Deployment 프로브·보안 설정·리소스·Service·Ingress). 규칙 데이터 `catalog/deploy-rules.json`. 새 파일만(하나라도 있으면 무기록), 템플릿 자체 배포 구성 안내. 도구 메타 비읽기·비파괴, outputSchema(13종), 영문 설명, AGENTS.md. (2) 테스트: 신규 `test:deploy` 49단언(게이트, 공식 Compose 스키마 고정물), `test:deploy-live`(CI 통합: hadolint 2.14·kubeconform 0.7 sha256 고정 다운로드, 공식 web·boot-web 빌드·기동·헬스·비루트, simple-backend compose + MySQL 데이터 확인, `DEPLOY_LIVE_PREBUILT=1` 로 미리 빌드 경로), `output-schemas`·`handshake` 34종·13종. 설계: [docs/design-deploy.md](docs/design-deploy.md). `server.json` 0.45.0.
|
|
1057
1086
|
- **0.44.0** — 보안약점 점검(도구 32 → 33종). (1) 새 도구 `check_egovframe_secure_coding(projectDir, rules?, paths?, includeTests=true, baselinePath?, sarifPath?, overwrite=false, maxFindings=2000, top=40, format="markdown"|"json"|"sarif")`: `src/secure-coding.ts` — 규칙 데이터 `catalog/secure-coding-rules.json`(schemaVersion 1, 20종·가이드 항목 14개, 유형·CWE·등급·대상·패턴·`unless`·`fileHas`/`fileLacks`·`downgrade`/`escalate`/`requireNear`·`stripCustomTags`·`scope`), Java 주석 렉서·XML/JSP 주석 제거(줄 수 유지), 대상 판별(java·jsp·mapper·sqlmap·config, 메시지 번들 제외), 테스트 코드 등급 낮춤, 출처(매니페스트 해시로 원본·수정본·매니페스트 없음·프로젝트·테스트), 예외 파일 `.egovframe-secure-ignore.json`(사유 5자 이상·검토자 필수, glob·라인), 지문(규칙·파일·정규화한 줄·순번)과 baseline(JSON·SARIF), SARIF 2.1.0(규칙·CWE taxonomy·security-severity·partialFingerprints) 기록(transaction, overwrite). 도구 메타 비읽기·비파괴·멱등, outputSchema(12종), 영문 설명, AGENTS.md. (2) 평가서 4절에 보안약점 요약(등급 산식 미반영), 회귀 코퍼스 기대값에 규칙별 건수(`scripts/corpus-lib.mjs` `secure`), `catalog/migration-corpus.json` `secureTemplates`. (3) CLI `secure`(명령 11종, 지표 high·medium·low·total·new), `generate_egovframe_ci(secureCoding=true)` — 세 플랫폼 게이트에 `secure --fail-on high` 단계, GitHub 은 `security-events: write`·`upload-sarif`(continue-on-error), GitLab·Jenkins 는 SARIF 보관. (4) 테스트: 신규 `test:secure-coding` 52단언(게이트, OASIS SARIF 2.1.0 스키마 고정물)·`test:secure-live`(CI 통합: 공식 템플릿 건수·SARIF 스키마), `test:ci` 에 secureCoding(GitHub 권한·업로드, GitLab 스키마, Jenkins 단계), `output-schemas`·`handshake` 33종·12종, `cli` 11종, CI 통합의 actionlint 대상 워크플로에 SARIF 업로드 포함. 설계: [docs/design-secure-coding.md](docs/design-secure-coding.md). `server.json` 0.44.0.
|
|
1058
1087
|
- **0.43.0** — CI·제출 형식 다변화: GitLab·Jenkins, SPDX(도구 32종 유지). (1) `generate_egovframe_ci` 에 `platform="github"|"gitlab"|"jenkins"`: `src/ci-config.ts` — `generateGitlabCi`(`.gitlab-ci.yml`, `maven:3.9-eclipse-temurin-<JDK>`·`eclipse-temurin:<JDK>` 이미지, 로컬 저장소 캐시, JUnit 리포트, `supply-chain` job 은 `needs: []`·Node 준비·`artifacts:reports:cyclonedx`)·`generateJenkinsfile`(Declarative — options·environment·`Build`/`SBOM`/`SBOM check`/`Assessment` 단계·`post { always { archiveArtifacts } }`, `isUnix()` 로 `sh`/`bat`), 세 플랫폼 공통 게이트 명령 `supplyChainCommands`(sbom → **`sbom-check --fail-on minimum`**(GitHub 게이트에도 추가) → assess), 새 파라미터 `supplier`(SBOM 공급자)·`npmRegistry`(`NPM_CONFIG_REGISTRY`)·`offlineBundle`(`EGOVFRAME_OFFLINE_BUNDLE`) — 모두 형식 제한으로 설정 파일 주입 차단, 공급자 단서가 없으면 게이트 실패 예고(`notes`). (2) SPDX 2.3: `src/spdx.ts` — `cdxToSpdx`(패키지·`DESCRIBES`·`DEPENDS_ON`(의존 없음 `NONE`)·purl/cpe/swid externalRefs·supplier/originator·creators·created·체크섬·선언 라이선스(`LicenseRef-` + `hasExtractedLicensingInfos`)·OSV 권고 `SECURITY`/`advisory`·`egovframe:*` annotations, serialNumber UUID 로 결정적 namespace)·`spdxToCdx`(점검용, `*_DEPENDENCY_OF` 뒤집기·`NOASSERTION` 은 모름). `generate_egovframe_sbom(bomFormat="spdx-json")`(기본 경로 `sbom/bom.spdx.json`, 최소 요소는 SPDX 를 되읽어 판정), `check_egovframe_sbom` 이 CycloneDX·SPDX 2.x 자동 판별(`document.format`, 형식이 다른 baseline 은 purl 로 비교, SPDX 의 `vex=true` 는 거부). CLI `sbom --bom-format spdx-json`. (3) 테스트: 신규 `test:spdx` 43단언(게이트, 공식 spdx-spec v2.3 스키마 고정물), `test:ci` 에 GitLab 공식 CI 스키마(gitlab v19.4.1-ee 고정물, devDependency `yaml`)·Jenkinsfile 구조·주입 거부, `test:sbom-live`(CI 통합)에 실제 Maven SBOM 의 SPDX 스키마·판정 일치, `test:sbom-check` 의 비 CycloneDX 거부 단언을 SPDX 3.0 등으로 갱신. (4) CI: integration job 의 Maven 이 원격 저장소 5xx(502 등)를 3초 간격 최대 5회 재시도(#53). (5) `generate_egovframe_sbom`: cyclonedx-maven-plugin 출력에 `metadata.timestamp` 가 없으면(CI 러너에서 실측) 생성 시각으로 채우고 알림 — v0.40 이후 간헐적이던 sbom-live 최소 요소 실패의 원인. `server.json` 0.43.0.
|
|
1059
1088
|
- **0.42.0** — 폐쇄망 오프라인 번들(도구 31 → 32종). (1) 새 도구 `prepare_egovframe_offline_bundle(outputDir, include=["osv","components","templates","origin"], templates?, dryRun=true, format)`: `src/offline-bundle.ts`·`src/offline-bundle-plan.ts` — OSV Maven 전체 덤프(`osv-vulnerabilities/Maven/all.zip`, 레코드 수·원본 갱신 시각 기록), 카탈로그 고정 커밋의 공통컴포넌트 아카이브(sha256·크기 검증), 템플릿(기본: 고정 지문이 있는 Initializr zip 12종, 저장소 템플릿은 지정 시 codeload zip), 공식 공통컴포넌트 태그 전부의 내용 포함 bare 미러(`git fetch --depth 1 +refs/tags/*`)를 임시 디렉터리에 받아 `bundle-manifest.json`(schemaVersion 1, 파일별 path·kind·url·sha256·bytes·fetchedAt)과 함께 새(또는 빈) 디렉터리로 옮김, 고정 지문 불일치·실패 시 무흔적. 도구 메타 비읽기·비파괴·openWorld, outputSchema(11종), 영문 설명, AGENTS.md 도구 목록. (2) `EGOVFRAME_OFFLINE_BUNDLE`: `fetchWithTimeout`(GET)과 카탈로그 아카이브 다운로드가 manifest 의 같은 URL 을 지문 검증 후 번들에서 돌려줌(변조 거부, 프로세스당 1회 검증), `defaultOsvQuery` 가 로컬 덤프로 판정(`buildOsvIndex`·`queryOsvIndex`·`inOsvRange`·`compareMavenVersions` — Maven ComparableVersion 요지, `versions` 목록 또는 ECOSYSTEM 범위, 철회 제외), `GitOriginSource` 가 번들 미러를 씀(`for-each-ref` 태그 목록, 원격 fetch 없음, 없는 태그 안내), OSV 를 쓴 결과(의존성 점검·SBOM 생성·SBOM 점검)에 스냅숏 기준일 노트와 30일 경고(`EGOVFRAME_BUNDLE_MAX_AGE_DAYS`). (3) `diagnose_egovframe_network` 가 실패 시 오프라인 번들 처방(bash·cmd·PowerShell)과 사용 중 표시, CLI `bundle`(`--write` 로만 받음, `--output-dir` 는 현재 디렉터리 기준), `server.json` 환경변수 `EGOVFRAME_OFFLINE_BUNDLE`(4종). (4) 테스트: 신규 `test:offline-bundle` 30단언(게이트)·`test:bundle-live`(CI 통합: 실제 번들, 로컬 OSV = 온라인, fetch 차단 상태의 생성·조립·재조립·SBOM 재점검), `registry` 환경변수 4종, `output-schemas`·`handshake` 32종·11종, `cli` 명령 10종. 설계: [docs/design-offline-bundle.md](docs/design-offline-bundle.md). (5) 런타임 의존성 `handlebars` 4.7.9 → 4.7.10(GHSA-xw65-4hp5-5hc7·GHSA-8r5x-fm3f-whwj·GHSA-p8wg-vrv2-v86f, `npm audit --omit=dev` 게이트). `server.json` 0.42.0.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"surveyedAt": "2026-10-13",
|
|
4
|
+
"note": "generate_egovframe_deploy(v0.45) 의 기반 이미지·DB 서비스·설정 주입 규칙. 이미지 태그는 Docker Hub 공식 이미지 기준이며 BASE_REGISTRY 로 사내 레지스트리(프록시 캐시)로 바꿀 수 있다.",
|
|
5
|
+
"jdks": [
|
|
6
|
+
"17",
|
|
7
|
+
"21"
|
|
8
|
+
],
|
|
9
|
+
"images": {
|
|
10
|
+
"maven": "maven:3.9-eclipse-temurin-{jdk}",
|
|
11
|
+
"jdk": "eclipse-temurin:{jdk}-jdk",
|
|
12
|
+
"tomcat": "tomcat:10.1-jdk{jdk}-temurin",
|
|
13
|
+
"jre": "eclipse-temurin:{jdk}-jre",
|
|
14
|
+
"tomcat11": "tomcat:11.0-jdk{jdk}-temurin"
|
|
15
|
+
},
|
|
16
|
+
"runtime": {
|
|
17
|
+
"tomcat": {
|
|
18
|
+
"port": 8080,
|
|
19
|
+
"webapps": "/usr/local/tomcat/webapps",
|
|
20
|
+
"note": "Classic war — 기본 Tomcat 10.1(Servlet 6.0, 5.x 템플릿의 jakarta.servlet-api 5.0·6.0 과 commons-fileupload2-jakarta-servlet6 호환), pom 이 jakarta.servlet-api 6.1 이상을 직접 선언하면 Tomcat 11.0. ROOT 로 풀어서 배포"
|
|
21
|
+
},
|
|
22
|
+
"boot": {
|
|
23
|
+
"port": 8080,
|
|
24
|
+
"note": "Boot 실행 jar — java -jar"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"user": {
|
|
28
|
+
"uid": 10001,
|
|
29
|
+
"name": "app"
|
|
30
|
+
},
|
|
31
|
+
"timezone": "Asia/Seoul",
|
|
32
|
+
"databases": {
|
|
33
|
+
"mysql": {
|
|
34
|
+
"image": "mysql:8.4",
|
|
35
|
+
"port": 3306,
|
|
36
|
+
"rootUser": "root",
|
|
37
|
+
"env": {
|
|
38
|
+
"rootPassword": "MYSQL_ROOT_PASSWORD",
|
|
39
|
+
"database": "MYSQL_DATABASE",
|
|
40
|
+
"user": "MYSQL_USER",
|
|
41
|
+
"password": "MYSQL_PASSWORD"
|
|
42
|
+
},
|
|
43
|
+
"command": [
|
|
44
|
+
"--character-set-server=utf8mb4",
|
|
45
|
+
"--collation-server=utf8mb4_unicode_ci",
|
|
46
|
+
"--lower-case-table-names=1"
|
|
47
|
+
],
|
|
48
|
+
"healthcheck": "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" --silent",
|
|
49
|
+
"data": "/var/lib/mysql",
|
|
50
|
+
"initdb": "/docker-entrypoint-initdb.d"
|
|
51
|
+
},
|
|
52
|
+
"maria": {
|
|
53
|
+
"image": "mariadb:11.4",
|
|
54
|
+
"port": 3306,
|
|
55
|
+
"rootUser": "root",
|
|
56
|
+
"env": {
|
|
57
|
+
"rootPassword": "MARIADB_ROOT_PASSWORD",
|
|
58
|
+
"database": "MARIADB_DATABASE",
|
|
59
|
+
"user": "MARIADB_USER",
|
|
60
|
+
"password": "MARIADB_PASSWORD"
|
|
61
|
+
},
|
|
62
|
+
"command": [
|
|
63
|
+
"--character-set-server=utf8mb4",
|
|
64
|
+
"--collation-server=utf8mb4_unicode_ci",
|
|
65
|
+
"--lower-case-table-names=1"
|
|
66
|
+
],
|
|
67
|
+
"healthcheck": "healthcheck.sh --connect --innodb_initialized",
|
|
68
|
+
"data": "/var/lib/mysql",
|
|
69
|
+
"initdb": "/docker-entrypoint-initdb.d"
|
|
70
|
+
},
|
|
71
|
+
"postgres": {
|
|
72
|
+
"image": "postgres:16",
|
|
73
|
+
"port": 5432,
|
|
74
|
+
"rootUser": "postgres",
|
|
75
|
+
"env": {
|
|
76
|
+
"rootPassword": "POSTGRES_PASSWORD",
|
|
77
|
+
"database": "POSTGRES_DB",
|
|
78
|
+
"user": "POSTGRES_USER",
|
|
79
|
+
"password": "POSTGRES_PASSWORD"
|
|
80
|
+
},
|
|
81
|
+
"command": [],
|
|
82
|
+
"healthcheck": "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\"",
|
|
83
|
+
"data": "/var/lib/postgresql/data",
|
|
84
|
+
"initdb": "/docker-entrypoint-initdb.d"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"dbAliases": {
|
|
88
|
+
"mariadb": "maria",
|
|
89
|
+
"postgresql": "postgres",
|
|
90
|
+
"pgsql": "postgres"
|
|
91
|
+
},
|
|
92
|
+
"externalDatabases": {
|
|
93
|
+
"oracle": "라이선스로 공개 이미지를 쓰지 않습니다 — 기관 DB 또는 별도 컨테이너를 연결 정보로만 연결",
|
|
94
|
+
"tibero": "상용 DB — 연결 정보로만 연결",
|
|
95
|
+
"altibase": "상용 DB — 연결 정보로만 연결",
|
|
96
|
+
"cubrid": "공식 이미지 태그·초기화 방식이 템플릿 스크립트와 맞지 않아 연결 정보로만 연결",
|
|
97
|
+
"goldilocks": "상용 DB — 연결 정보로만 연결",
|
|
98
|
+
"hsql": "내장(메모리) DB — DB 컨테이너 없이 앱만 띄움"
|
|
99
|
+
},
|
|
100
|
+
"unsupported": [
|
|
101
|
+
{
|
|
102
|
+
"id": "no-build-file",
|
|
103
|
+
"reason": "pom.xml·build.gradle 이 없습니다(프론트엔드·정적 자산 템플릿은 각자의 배포 방식을 쓰세요)"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "multi-module",
|
|
107
|
+
"reason": "멀티 모듈(pom <modules>·settings.gradle include) 프로젝트 — 모듈별로 이미지를 만들어야 해 1차 범위 밖입니다(MSA 템플릿은 저장소의 자체 배포 구성을 쓰세요)"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "not-runnable",
|
|
111
|
+
"reason": "war 도 실행 jar(Spring Boot)도 아닌 라이브러리 jar — 실행할 수 있는 배포 단위가 없습니다"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
package/dist/agents-md.js
CHANGED
|
@@ -95,7 +95,7 @@ const T = {
|
|
|
95
95
|
"의존성 버전을 올릴 때는 `check_egovframe_dependencies` 로 공식 5.x parent 기준과 대조하고, parent 가 관리하는 좌표에는 `<version>` 을 적지 않습니다.",
|
|
96
96
|
],
|
|
97
97
|
tools: "## 사용할 수 있는 MCP 도구", toolsNote: `이 프로젝트는 \`egovframe-scaffold-mcp\`(v${SERVER_VERSION}) 로 관리할 수 있습니다. 진단·검증은 읽기 전용이고, 쓰기 도구는 dryRun 과 transaction(실패 시 복구)을 제공합니다.`,
|
|
98
|
-
toolRows: ["`diagnose_egovframe_project` · `validate_egovframe_project` · `generate_egovframe_report` — 진단·검증·리포트", "`add_egovframe_components` · `remove_egovframe_components` · `upgrade_egovframe_project` — 공통컴포넌트 수명주기", "`generate_egovframe_crud` · `generate_egovframe_config` · `generate_egovframe_ci` — 코드·설정·CI 생성(GitHub·GitLab·Jenkins)", "`build_egovframe_project` · `test_egovframe_project` — 빌드·테스트 실행과 오류 구조화", "`rehearse_egovframe_migration` · `reassemble_egovframe_components` · `migrate_egovframe_project` — 5.x 전환(사본 리허설 → 재조립 → 적용)", "`check_egovframe_dependencies` — 의존성 점검", "`generate_egovframe_sbom` · `check_egovframe_sbom` — SBOM 생성·점검(최소 요소·이전 SBOM 비교·VEX)", "`check_egovframe_secure_coding` — 보안약점 점검(개발보안 가이드 구현단계, SARIF)", "`diagnose_egovframe_network` · `prepare_egovframe_offline_bundle` — 다운로드 실패 시 네트워크 진단, 폐쇄망용 오프라인 번들"],
|
|
98
|
+
toolRows: ["`diagnose_egovframe_project` · `validate_egovframe_project` · `generate_egovframe_report` — 진단·검증·리포트", "`add_egovframe_components` · `remove_egovframe_components` · `upgrade_egovframe_project` — 공통컴포넌트 수명주기", "`generate_egovframe_crud` · `generate_egovframe_config` · `generate_egovframe_ci` — 코드·설정·CI 생성(GitHub·GitLab·Jenkins)", "`build_egovframe_project` · `test_egovframe_project` — 빌드·테스트 실행과 오류 구조화", "`rehearse_egovframe_migration` · `reassemble_egovframe_components` · `migrate_egovframe_project` — 5.x 전환(사본 리허설 → 재조립 → 적용)", "`check_egovframe_dependencies` — 의존성 점검", "`generate_egovframe_sbom` · `check_egovframe_sbom` — SBOM 생성·점검(최소 요소·이전 SBOM 비교·VEX)", "`check_egovframe_secure_coding` — 보안약점 점검(개발보안 가이드 구현단계, SARIF)", "`generate_egovframe_deploy` — 컨테이너·배포 산출물(Dockerfile·docker-compose·Kubernetes)", "`diagnose_egovframe_network` · `prepare_egovframe_offline_bundle` — 다운로드 실패 시 네트워크 진단, 폐쇄망용 오프라인 번들"],
|
|
99
99
|
generated: (d) => `_생성: ${d} · egovframe-scaffold-mcp generate_agents_md. 프로젝트가 바뀌면 다시 생성하세요._`,
|
|
100
100
|
backups: (dirs) => `현재 백업 디렉터리가 있습니다: ${dirs.join(", ")}`,
|
|
101
101
|
},
|
|
@@ -121,7 +121,7 @@ const T = {
|
|
|
121
121
|
"When bumping dependency versions, compare against the official 5.x parent baseline with `check_egovframe_dependencies`; do not write `<version>` for coordinates the parent manages.",
|
|
122
122
|
],
|
|
123
123
|
tools: "## Available MCP tools", toolsNote: `This project can be managed with \`egovframe-scaffold-mcp\` (v${SERVER_VERSION}). Diagnosis and validation are read-only; write tools offer dryRun and transactions (rollback on failure).`,
|
|
124
|
-
toolRows: ["`diagnose_egovframe_project` · `validate_egovframe_project` · `generate_egovframe_report` — diagnosis, validation, report", "`add_egovframe_components` · `remove_egovframe_components` · `upgrade_egovframe_project` — common-component lifecycle", "`generate_egovframe_crud` · `generate_egovframe_config` · `generate_egovframe_ci` — code, configuration and CI generation (GitHub, GitLab, Jenkins)", "`build_egovframe_project` · `test_egovframe_project` — build/test execution with structured errors", "`rehearse_egovframe_migration` · `reassemble_egovframe_components` · `migrate_egovframe_project` — 5.x migration (rehearse on a copy, reassemble, apply)", "`check_egovframe_dependencies` — dependency check", "`generate_egovframe_sbom` · `check_egovframe_sbom` — SBOM generation and checks (minimum elements, diff, VEX)", "`check_egovframe_secure_coding` — security weakness scan (Korean secure coding guide, SARIF)", "`diagnose_egovframe_network` · `prepare_egovframe_offline_bundle` — network diagnosis when downloads fail, offline bundle for air-gapped networks"],
|
|
124
|
+
toolRows: ["`diagnose_egovframe_project` · `validate_egovframe_project` · `generate_egovframe_report` — diagnosis, validation, report", "`add_egovframe_components` · `remove_egovframe_components` · `upgrade_egovframe_project` — common-component lifecycle", "`generate_egovframe_crud` · `generate_egovframe_config` · `generate_egovframe_ci` — code, configuration and CI generation (GitHub, GitLab, Jenkins)", "`build_egovframe_project` · `test_egovframe_project` — build/test execution with structured errors", "`rehearse_egovframe_migration` · `reassemble_egovframe_components` · `migrate_egovframe_project` — 5.x migration (rehearse on a copy, reassemble, apply)", "`check_egovframe_dependencies` — dependency check", "`generate_egovframe_sbom` · `check_egovframe_sbom` — SBOM generation and checks (minimum elements, diff, VEX)", "`check_egovframe_secure_coding` — security weakness scan (Korean secure coding guide, SARIF)", "`generate_egovframe_deploy` — container and deployment files (Dockerfile, docker-compose, Kubernetes)", "`diagnose_egovframe_network` · `prepare_egovframe_offline_bundle` — network diagnosis when downloads fail, offline bundle for air-gapped networks"],
|
|
125
125
|
generated: (d) => `_Generated ${d} by egovframe-scaffold-mcp generate_agents_md. Regenerate when the project changes._`,
|
|
126
126
|
backups: (dirs) => `Backup directories currently present: ${dirs.join(", ")}`,
|
|
127
127
|
},
|
package/dist/deploy.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// 컨테이너·배포 산출물 (generate_egovframe_deploy, v0.45) — 진단 결과(빌드 도구·패키징·DbType·설정 파일)로
|
|
2
|
+
// Dockerfile(멀티 스테이지 또는 미리 빌드한 산출물)·.dockerignore·docker-compose(DbType 별 DB)·Kubernetes 매니페스트를 만든다.
|
|
3
|
+
// 규칙(기반 이미지·DB 서비스·지원하지 않는 구조)은 catalog/deploy-rules.json 데이터. 새 파일만 쓰고(기존 파일 거부) dryRun 이 기본.
|
|
4
|
+
// 설정 주입: Boot 는 Spring Environment 의 환경 변수 매핑(Globals.mysql.Url → GLOBALS_MYSQL_URL), Classic 은 WAR 를 풀어 배포하고
|
|
5
|
+
// DB 설정 파일(globals.properties)을 볼륨(compose)·Secret(Kubernetes)으로 덮어쓴다.
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { withFileTransaction } from "./file-transaction.js";
|
|
9
|
+
import { SERVER_VERSION } from "./version.js";
|
|
10
|
+
const RULES_URL = new URL("../catalog/deploy-rules.json", import.meta.url);
|
|
11
|
+
let cached = null;
|
|
12
|
+
export function loadDeployRules() {
|
|
13
|
+
if (!cached)
|
|
14
|
+
cached = JSON.parse(fs.readFileSync(RULES_URL, "utf8"));
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
const read = (p) => { try {
|
|
18
|
+
return fs.readFileSync(p, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "";
|
|
22
|
+
} };
|
|
23
|
+
const stripXmlComments = (s) => s.replace(/<!--[\s\S]*?-->/g, "");
|
|
24
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50) || "egov-app";
|
|
25
|
+
/** .properties 를 키→{값, 줄} 로(주석·빈 줄 무시, 마지막 값 우선) */
|
|
26
|
+
export function parseProperties(text) {
|
|
27
|
+
const out = new Map();
|
|
28
|
+
text.split(/\r?\n/).forEach((raw, i) => {
|
|
29
|
+
const l = raw.trim();
|
|
30
|
+
if (!l || l.startsWith("#") || l.startsWith("!"))
|
|
31
|
+
return;
|
|
32
|
+
const m = l.match(/^([^=:\s]+)\s*[=:]\s*(.*)$/) ?? l.match(/^(\S+)\s+(.*)$/);
|
|
33
|
+
if (m)
|
|
34
|
+
out.set(m[1], { value: m[2].trim(), line: i + 1 });
|
|
35
|
+
});
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** jdbc URL 에서 호스트·포트·DB 이름(log4jdbc 접두어 포함). */
|
|
39
|
+
export function parseJdbcUrl(url) {
|
|
40
|
+
const m = url.match(/^jdbc:(?:log4jdbc:)?(\w+):\/\/([^/:?;]+)(?::(\d+))?(?:\/([^?;]*))?/);
|
|
41
|
+
if (!m)
|
|
42
|
+
return null;
|
|
43
|
+
return { host: m[2], port: m[3] ? Number(m[3]) : null, database: m[4] ? m[4] : null };
|
|
44
|
+
}
|
|
45
|
+
const replaceJdbcHost = (url, host) => url.replace(/^(jdbc:(?:log4jdbc:)?\w+:\/\/)[^/:?;]+/, `$1${host}`);
|
|
46
|
+
/** Spring Environment 가 시스템 환경 변수에서 찾는 이름(Globals.mysql.Url → GLOBALS_MYSQL_URL) */
|
|
47
|
+
export const envNameOf = (key) => key.replace(/[.-]/g, "_").toUpperCase();
|
|
48
|
+
function walkFiles(root, rel, out, depth) {
|
|
49
|
+
if (depth > 8 || out.length > 5000)
|
|
50
|
+
return;
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const e of entries) {
|
|
59
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
60
|
+
if (e.isDirectory()) {
|
|
61
|
+
if (!/^(\.|node_modules$|target$|build$|migration-backup$)/.test(e.name))
|
|
62
|
+
walkFiles(root, r, out, depth + 1);
|
|
63
|
+
}
|
|
64
|
+
else if (e.isFile())
|
|
65
|
+
out.push(r);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 프로젝트를 읽어 배포 프로필을 만든다(읽기 전용). */
|
|
69
|
+
export function analyzeDeploy(projectDir) {
|
|
70
|
+
const rules = loadDeployRules();
|
|
71
|
+
const notes = [];
|
|
72
|
+
const un = (id) => ({ profile: null, unsupported: rules.unsupported.find((u) => u.id === id), notes });
|
|
73
|
+
const pomPath = path.join(projectDir, "pom.xml");
|
|
74
|
+
const gradlePath = ["build.gradle", "build.gradle.kts"].map((f) => path.join(projectDir, f)).find((p) => fs.existsSync(p));
|
|
75
|
+
const isMaven = fs.existsSync(pomPath);
|
|
76
|
+
if (!isMaven && !gradlePath)
|
|
77
|
+
return un("no-build-file");
|
|
78
|
+
let servlet = null;
|
|
79
|
+
let packaging;
|
|
80
|
+
let boot;
|
|
81
|
+
let web;
|
|
82
|
+
let actuator;
|
|
83
|
+
let artifactId;
|
|
84
|
+
let version;
|
|
85
|
+
let wrapper;
|
|
86
|
+
if (isMaven) {
|
|
87
|
+
const pom = stripXmlComments(read(pomPath));
|
|
88
|
+
if (/<modules>\s*<module>/.test(pom))
|
|
89
|
+
return un("multi-module");
|
|
90
|
+
const self = pom.replace(/<parent>[\s\S]*?<\/parent>/, "").replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/, "").replace(/<dependencies>[\s\S]*?<\/dependencies>/, "").replace(/<build>[\s\S]*?<\/build>/, "").replace(/<profiles>[\s\S]*?<\/profiles>/, "");
|
|
91
|
+
packaging = /<packaging>\s*war\s*<\/packaging>/.test(self) ? "war" : "jar";
|
|
92
|
+
const parentArt = pom.match(/<parent>[\s\S]*?<artifactId>\s*([^<\s]+)\s*<\/artifactId>[\s\S]*?<\/parent>/)?.[1] ?? "";
|
|
93
|
+
boot = /spring-boot-starter-parent|egovframe-boot-starter-parent/.test(parentArt) || /spring-boot-maven-plugin/.test(pom);
|
|
94
|
+
web = packaging === "war" || /<artifactId>\s*spring-boot-starter-(web|webflux|thymeleaf)\s*<\/artifactId>/.test(pom);
|
|
95
|
+
actuator = /<artifactId>\s*spring-boot-starter-actuator\s*<\/artifactId>/.test(pom);
|
|
96
|
+
artifactId = self.match(/<artifactId>\s*([^<\s]+)\s*<\/artifactId>/)?.[1] ?? path.basename(projectDir);
|
|
97
|
+
version = self.match(/<version>\s*([^<\s$]+)\s*<\/version>/)?.[1] ?? "1.0.0";
|
|
98
|
+
wrapper = fs.existsSync(path.join(projectDir, "mvnw"));
|
|
99
|
+
servlet = pom.match(/<artifactId>\s*jakarta\.servlet-api\s*<\/artifactId>\s*<version>\s*([\d.]+)/)?.[1] ?? null;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const g = read(gradlePath);
|
|
103
|
+
const settings = read(path.join(projectDir, "settings.gradle")) + read(path.join(projectDir, "settings.gradle.kts"));
|
|
104
|
+
if (/^\s*include\s*[\s(]/m.test(settings))
|
|
105
|
+
return un("multi-module");
|
|
106
|
+
boot = /org\.springframework\.boot/.test(g);
|
|
107
|
+
packaging = /id\s*\(?\s*['"]war['"]|apply\s+plugin\s*:\s*['"]war['"]|\bwar\s*\{/.test(g) ? "war" : "jar";
|
|
108
|
+
web = packaging === "war" || /spring-boot-starter-(web|webflux|thymeleaf)/.test(g);
|
|
109
|
+
actuator = /spring-boot-starter-actuator/.test(g);
|
|
110
|
+
artifactId = settings.match(/rootProject\.name\s*=\s*["']([^"']+)["']/)?.[1] ?? path.basename(projectDir);
|
|
111
|
+
version = g.match(/^\s*version\s*=?\s*["']([^"']+)["']/m)?.[1] ?? "1.0.0";
|
|
112
|
+
wrapper = fs.existsSync(path.join(projectDir, "gradlew"));
|
|
113
|
+
servlet = g.match(/jakarta\.servlet:jakarta\.servlet-api:([\d.]+)/)?.[1] ?? null;
|
|
114
|
+
}
|
|
115
|
+
const tomcat = servlet && /^(6\.[1-9]|[7-9]\.)/.test(servlet) ? "11.0" : "10.1";
|
|
116
|
+
if (packaging === "jar" && !boot)
|
|
117
|
+
return un("not-runnable");
|
|
118
|
+
const runtime = packaging === "war" ? "tomcat" : "boot";
|
|
119
|
+
if (packaging === "war" && boot)
|
|
120
|
+
notes.push("Spring Boot 의 war 패키징 — 외부 Tomcat 에 배포하는 구성으로 만듭니다(SpringBootServletInitializer 가 있어야 함).");
|
|
121
|
+
// 설정: Boot application.properties·yml / Classic globals.properties
|
|
122
|
+
const files = [];
|
|
123
|
+
walkFiles(projectDir, "src/main/resources", files, 0);
|
|
124
|
+
const appProps = files.find((f) => /(^|\/)application\.properties$/.test(f) && f.split("/").length === 4);
|
|
125
|
+
const props = appProps ? parseProperties(read(path.join(projectDir, appProps))) : new Map();
|
|
126
|
+
let port = rules.runtime[runtime].port;
|
|
127
|
+
let contextPath = "";
|
|
128
|
+
if (runtime === "boot") {
|
|
129
|
+
const p = props.get("server.port")?.value;
|
|
130
|
+
if (p && /^\d+$/.test(p))
|
|
131
|
+
port = Number(p);
|
|
132
|
+
const cp = props.get("server.servlet.context-path")?.value;
|
|
133
|
+
if (cp && /^\/[\w\-/]*$/.test(cp))
|
|
134
|
+
contextPath = cp.replace(/\/+$/, "");
|
|
135
|
+
if (fs.existsSync(path.join(projectDir, "src/main/resources/application.yml")) || fs.existsSync(path.join(projectDir, "src/main/resources/application.yaml")))
|
|
136
|
+
notes.push("application.yml 은 읽지 않았습니다 — 포트·컨텍스트 경로·DB 설정이 yml 에 있으면 생성 파일을 확인하세요.");
|
|
137
|
+
}
|
|
138
|
+
const actuatorProbes = runtime === "boot" && actuator && /probes\.enabled\s*=\s*true/.test(read(path.join(projectDir, appProps ?? "")));
|
|
139
|
+
const liveness = `${contextPath}${actuatorProbes ? "/actuator/health/liveness" : actuator ? "/actuator/health" : "/"}`;
|
|
140
|
+
const readiness = `${contextPath}${actuatorProbes ? "/actuator/health/readiness" : actuator ? "/actuator/health" : "/"}`;
|
|
141
|
+
// DB
|
|
142
|
+
let db = null;
|
|
143
|
+
const dbSource = runtime === "boot" && props.has("Globals.DbType") ? { file: appProps, props }
|
|
144
|
+
: (() => {
|
|
145
|
+
const f = files.filter((x) => /globals\.properties$/i.test(x)).sort((a, b) => a.length - b.length)[0];
|
|
146
|
+
return f ? { file: f, props: parseProperties(read(path.join(projectDir, f))) } : null;
|
|
147
|
+
})();
|
|
148
|
+
if (dbSource && dbSource.props.has("Globals.DbType")) {
|
|
149
|
+
const rawType = dbSource.props.get("Globals.DbType").value.trim();
|
|
150
|
+
// Boot 템플릿은 Globals.<DbType>.Url, Classic 템플릿은 Globals.Url 형식 — 앞의 것을 먼저 찾는다
|
|
151
|
+
const pfx = (k) => [...dbSource.props.keys()].find((x) => x.toLowerCase() === `globals.${rawType.toLowerCase()}.${k}`) ?? [...dbSource.props.keys()].find((x) => x.toLowerCase() === `globals.${k}`);
|
|
152
|
+
const keys = { dbType: "Globals.DbType", driver: pfx("driverclassname"), url: pfx("url"), user: pfx("username"), password: pfx("password") };
|
|
153
|
+
const url = keys.url ? dbSource.props.get(keys.url).value : null;
|
|
154
|
+
const parsed = url ? parseJdbcUrl(url) : null;
|
|
155
|
+
const type = rules.dbAliases[rawType.toLowerCase()] ?? rawType.toLowerCase();
|
|
156
|
+
const svc = rules.databases[type];
|
|
157
|
+
db = {
|
|
158
|
+
type, inject: type === "hsql" ? "none" : runtime === "boot" ? "env" : "file", file: dbSource.file, keys,
|
|
159
|
+
url, user: keys.user ? dbSource.props.get(keys.user).value || null : null,
|
|
160
|
+
host: parsed?.host ?? null, port: parsed?.port ?? null, database: parsed?.database ?? null,
|
|
161
|
+
service: svc ? "container" : "external", ...(svc ? {} : { serviceNote: rules.externalDatabases[type] ?? "공개 이미지 규칙이 없는 DB — 연결 정보로만 연결" }),
|
|
162
|
+
initScripts: [],
|
|
163
|
+
};
|
|
164
|
+
if (svc) {
|
|
165
|
+
const all = [];
|
|
166
|
+
walkFiles(projectDir, "", all, 0);
|
|
167
|
+
const sql = all.filter((f) => f.toLowerCase().endsWith(".sql") && new RegExp(`(^|[_\\-./])${type === "maria" ? "(maria|mariadb|mysql)" : type}([_\\-./]|$)`, "i").test(f.toLowerCase()));
|
|
168
|
+
const pick = (kind) => sql.filter((f) => kind.test(path.basename(f).toLowerCase())).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))[0];
|
|
169
|
+
const ddl = pick(/ddl/), data = pick(/data|dml/);
|
|
170
|
+
db.initScripts = [ddl, data].filter((x) => !!x);
|
|
171
|
+
if (!db.initScripts.length)
|
|
172
|
+
notes.push(`${type} 초기화 SQL(ddl·data)을 찾지 못해 빈 DB 로 띄웁니다.`);
|
|
173
|
+
}
|
|
174
|
+
if (!url)
|
|
175
|
+
notes.push(`${dbSource.file} 에 Globals.${rawType}.Url·Globals.Url 이 없어 DB 연결 정보를 덮어쓰지 않습니다.`);
|
|
176
|
+
}
|
|
177
|
+
else if (props.has("spring.datasource.url")) {
|
|
178
|
+
const url = props.get("spring.datasource.url").value;
|
|
179
|
+
const parsed = parseJdbcUrl(url);
|
|
180
|
+
const type = parsed ? (rules.dbAliases[url.match(/^jdbc:(?:log4jdbc:)?(\w+):/)?.[1] ?? ""] ?? url.match(/^jdbc:(?:log4jdbc:)?(\w+):/)?.[1] ?? "unknown") : "unknown";
|
|
181
|
+
db = { type, inject: "env", file: appProps, keys: { url: "spring.datasource.url", user: props.has("spring.datasource.username") ? "spring.datasource.username" : undefined, password: "spring.datasource.password", driver: props.has("spring.datasource.driver-class-name") ? "spring.datasource.driver-class-name" : undefined }, url, user: props.get("spring.datasource.username")?.value ?? null, host: parsed?.host ?? null, port: parsed?.port ?? null, database: parsed?.database ?? null, service: rules.databases[type] ? "container" : "external", initScripts: [] };
|
|
182
|
+
}
|
|
183
|
+
const appName = slug(artifactId);
|
|
184
|
+
return { profile: { appName, version, buildTool: isMaven ? "maven" : "gradle", wrapper, packaging, runtime, web, actuator, tomcat, port, contextPath, liveness, readiness, db }, unsupported: null, notes };
|
|
185
|
+
}
|
|
186
|
+
const img = (tpl, jdk) => tpl.replace("{jdk}", jdk);
|
|
187
|
+
const q = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
188
|
+
export function renderDockerfile(p, o) {
|
|
189
|
+
const r = loadDeployRules();
|
|
190
|
+
const u = r.user;
|
|
191
|
+
const javaOpts = `-Dfile.encoding=UTF-8 -Duser.timezone=${r.timezone} -XX:MaxRAMPercentage=75`;
|
|
192
|
+
const L = [
|
|
193
|
+
`# eGovFrame ${p.runtime === "tomcat" ? `Classic(war → Tomcat ${p.tomcat})` : "Boot(실행 jar)"} — egovframe-scaffold-mcp ${SERVER_VERSION} generate_egovframe_deploy`,
|
|
194
|
+
`# 폐쇄망: docker build --build-arg BASE_REGISTRY=<사내 레지스트리>/library . (기반 이미지를 사내 레지스트리에서)`,
|
|
195
|
+
`# BuildKit(Docker 23 이상 기본)이 필요합니다(RUN --mount=type=cache). # syntax 지시어는 쓰지 않습니다 — 폐쇄망에서 docker/dockerfile 이미지를 받지 않도록`,
|
|
196
|
+
`ARG BASE_REGISTRY=${o.imageRegistry}`,
|
|
197
|
+
];
|
|
198
|
+
const art = p.packaging;
|
|
199
|
+
const outDir = p.buildTool === "maven" ? "target" : "build/libs";
|
|
200
|
+
if (o.buildInImage) {
|
|
201
|
+
const buildImage = p.buildTool === "maven" || !p.wrapper ? (p.buildTool === "maven" ? img(r.images.maven, o.jdk) : "gradle:8-jdk" + o.jdk) : img(r.images.jdk, o.jdk);
|
|
202
|
+
const cache = p.buildTool === "maven" ? "/root/.m2" : "/root/.gradle";
|
|
203
|
+
// 원격 저장소(maven.egovframe.go.kr 등)의 일시 5xx 를 재시도(Maven 3.9 기본은 429·503 만)
|
|
204
|
+
const cmd = p.buildTool === "maven" ? "mvn -B -q -DskipTests -Daether.connector.http.retryHandler.serviceUnavailable=429,500,502,503,504 -Daether.connector.http.retryHandler.count=5 package" : p.wrapper ? "chmod +x ./gradlew && ./gradlew --no-daemon -q -x test build" : "gradle --no-daemon -q -x test build";
|
|
205
|
+
L.push(``, `FROM \${BASE_REGISTRY}/${buildImage} AS build`, "WORKDIR /workspace", "COPY . .", `RUN --mount=type=cache,target=${cache} ${cmd} \\`, ` && find ${outDir} -maxdepth 1 -name '*.${art}' ! -name '*-plain.${art}' -exec cp {} /workspace/app.${art} \\;`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
L.push(``, `# 미리 빌드한 산출물(${outDir}/*.${art})을 씁니다 — 먼저 ${p.buildTool === "maven" ? "mvn -B -DskipTests package" : "./gradlew build -x test"}`, `FROM \${BASE_REGISTRY}/${img(r.images.jdk, o.jdk)} AS build`, "WORKDIR /workspace", `COPY ${outDir}/ /workspace/${outDir}/`, `RUN find ${outDir} -maxdepth 1 -name '*.${art}' ! -name '*-plain.${art}' -exec cp {} /workspace/app.${art} \\;`);
|
|
209
|
+
}
|
|
210
|
+
if (p.runtime === "tomcat") {
|
|
211
|
+
// WAR 를 이미지 안에서 풀어 둔다 — 설정 파일(globals.properties)을 볼륨·Secret 으로 덮어쓸 수 있게
|
|
212
|
+
L.push("WORKDIR /workspace/ROOT", "RUN jar xf /workspace/app.war && test -d WEB-INF");
|
|
213
|
+
L.push(``, `FROM \${BASE_REGISTRY}/${img(p.tomcat === "11.0" ? r.images.tomcat11 : r.images.tomcat, o.jdk)}`, `ENV TZ=${r.timezone} LANG=C.UTF-8 JAVA_OPTS="${javaOpts}"`, `RUN groupadd -r -g ${u.uid} ${u.name} && useradd -r -u ${u.uid} -g ${u.name} -d /usr/local/tomcat ${u.name} \\`, ` && rm -rf /usr/local/tomcat/webapps/* \\`, ` && chown -R ${u.name}:${u.name} /usr/local/tomcat/logs /usr/local/tomcat/temp /usr/local/tomcat/work /usr/local/tomcat/webapps`, `COPY --from=build --chown=${u.name}:${u.name} /workspace/ROOT /usr/local/tomcat/webapps/ROOT`, `USER ${u.uid}`, `EXPOSE ${p.port}`, `HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 CMD curl -fsS -o /dev/null http://127.0.0.1:${p.port}${p.liveness} || exit 1`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
L.push(``, `FROM \${BASE_REGISTRY}/${img(r.images.jre, o.jdk)}`, `ENV TZ=${r.timezone} LANG=C.UTF-8 JAVA_OPTS="${javaOpts}"`, `RUN groupadd -r -g ${u.uid} ${u.name} && useradd -r -u ${u.uid} -g ${u.name} -d /app ${u.name} \\`, ` && mkdir -p /app && chown ${u.name}:${u.name} /app`, "WORKDIR /app", `COPY --from=build --chown=${u.name}:${u.name} /workspace/app.jar /app/app.jar`, `USER ${u.uid}`);
|
|
217
|
+
if (p.web)
|
|
218
|
+
L.push(`EXPOSE ${p.port}`, `HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 CMD curl -fsS -o /dev/null http://127.0.0.1:${p.port}${p.liveness} || exit 1`);
|
|
219
|
+
L.push(`ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]`);
|
|
220
|
+
}
|
|
221
|
+
return L.join("\n") + "\n";
|
|
222
|
+
}
|
|
223
|
+
export function renderDockerignore(p, o) {
|
|
224
|
+
if (!o.buildInImage) {
|
|
225
|
+
const outDir = p.buildTool === "maven" ? "target" : "build/libs";
|
|
226
|
+
return ["# 미리 빌드한 산출물만 보냄", "*", `!${outDir}/`, `!${outDir}/*.${p.packaging}`, ""].join("\n");
|
|
227
|
+
}
|
|
228
|
+
return [".git", ".gitignore", ".idea", ".vscode", ".settings", ".project", ".classpath", "*.iml", "target", "build", ".gradle", "node_modules", "deploy", "migration-backup", ".egov*", "*.log", "logs", "Dockerfile", ".dockerignore", ""].join("\n");
|
|
229
|
+
}
|
|
230
|
+
/** 앱 쪽 DB 설정 덮어쓰기 목록(Boot env). host 는 compose 서비스 이름 또는 Kubernetes 자리표시자. */
|
|
231
|
+
function dbEnvFor(p, host) {
|
|
232
|
+
const db = p.db;
|
|
233
|
+
if (!db || db.inject !== "env")
|
|
234
|
+
return [];
|
|
235
|
+
const out = [];
|
|
236
|
+
if (db.keys.dbType)
|
|
237
|
+
out.push({ name: envNameOf(db.keys.dbType), key: db.keys.dbType, value: db.type === "maria" ? "maria" : db.type, secret: false });
|
|
238
|
+
if (db.keys.url && db.url)
|
|
239
|
+
out.push({ name: envNameOf(db.keys.url), key: db.keys.url, value: replaceJdbcHost(db.url, host), secret: false });
|
|
240
|
+
if (db.keys.user)
|
|
241
|
+
out.push({ name: envNameOf(db.keys.user), key: db.keys.user, value: db.user ?? loadDeployRules().databases[db.type]?.rootUser ?? "app", secret: false });
|
|
242
|
+
if (db.keys.password)
|
|
243
|
+
out.push({ name: envNameOf(db.keys.password), key: db.keys.password, value: "CHANGE_ME", secret: true });
|
|
244
|
+
return out.filter((e) => e.value !== undefined);
|
|
245
|
+
}
|
|
246
|
+
/** Classic: DB 키만 바꾼 설정 파일 사본. */
|
|
247
|
+
export function renderClassicConfig(projectDir, p, host) {
|
|
248
|
+
const db = p.db;
|
|
249
|
+
if (!db || db.inject !== "file")
|
|
250
|
+
return null;
|
|
251
|
+
const text = read(path.join(projectDir, db.file));
|
|
252
|
+
const lines = text.split(/\r?\n/);
|
|
253
|
+
const props = parseProperties(text);
|
|
254
|
+
const set = (key, value) => { if (!key)
|
|
255
|
+
return; const e = props.get(key); if (e)
|
|
256
|
+
lines[e.line - 1] = `${key} = ${value}`; };
|
|
257
|
+
if (db.url)
|
|
258
|
+
set(db.keys.url, replaceJdbcHost(db.url, host));
|
|
259
|
+
set(db.keys.password, "CHANGE_ME");
|
|
260
|
+
return [`# egovframe-scaffold-mcp ${SERVER_VERSION} generate_egovframe_deploy — ${db.file} 사본(DB 호스트·비밀번호만 바꿈). 운영 값은 저장소에 두지 마세요.`, ...lines].join("\n");
|
|
261
|
+
}
|
|
262
|
+
/** WAR 를 풀어 둔 위치에서 설정 파일 경로 */
|
|
263
|
+
const classicTarget = (p) => `${loadDeployRules().runtime.tomcat.webapps}/ROOT/WEB-INF/classes/${p.db.file.replace(/^src\/main\/resources\//, "")}`;
|
|
264
|
+
export function renderCompose(p, o) {
|
|
265
|
+
const r = loadDeployRules();
|
|
266
|
+
const db = p.db;
|
|
267
|
+
const svc = db && db.service === "container" && db.inject !== "none" ? r.databases[db.type] : null;
|
|
268
|
+
const L = [
|
|
269
|
+
`# eGovFrame 로컬 실행 — egovframe-scaffold-mcp ${SERVER_VERSION} generate_egovframe_deploy`,
|
|
270
|
+
`# 사용: cd deploy && cp .env.example .env && docker compose up --build`,
|
|
271
|
+
`name: ${p.appName}`,
|
|
272
|
+
"services:",
|
|
273
|
+
" app:",
|
|
274
|
+
" build:",
|
|
275
|
+
" context: ..",
|
|
276
|
+
` dockerfile: ${o.dockerfile ?? "Dockerfile"}`,
|
|
277
|
+
" args:",
|
|
278
|
+
` BASE_REGISTRY: \${BASE_REGISTRY:-${o.imageRegistry}}`,
|
|
279
|
+
` image: ${o.image}`,
|
|
280
|
+
];
|
|
281
|
+
if (p.web)
|
|
282
|
+
L.push(" ports:", ` - "\${APP_PORT:-${p.port}}:${p.port}"`);
|
|
283
|
+
const env = dbEnvFor(p, svc ? "db" : (db?.host ?? "db"));
|
|
284
|
+
if (env.length) {
|
|
285
|
+
L.push(" environment:");
|
|
286
|
+
for (const e of env)
|
|
287
|
+
L.push(` ${e.name}: ${e.secret ? '"${DB_PASSWORD:-CHANGE_ME}"' : q(e.value)}`);
|
|
288
|
+
}
|
|
289
|
+
if (db?.inject === "file")
|
|
290
|
+
L.push(" volumes:", ` - ./config/${path.posix.basename(db.file)}:${classicTarget(p)}:ro`);
|
|
291
|
+
if (svc)
|
|
292
|
+
L.push(" depends_on:", " db:", " condition: service_healthy");
|
|
293
|
+
L.push(" restart: unless-stopped");
|
|
294
|
+
if (svc && db) {
|
|
295
|
+
const rootUser = svc.rootUser;
|
|
296
|
+
const user = db.user ?? rootUser;
|
|
297
|
+
L.push(" db:", ` image: \${BASE_REGISTRY:-${o.imageRegistry}}/${svc.image}`, " environment:");
|
|
298
|
+
if (db.type === "postgres") {
|
|
299
|
+
L.push(` ${svc.env.user}: ${q(user)}`, ` ${svc.env.password}: "\${DB_PASSWORD:-CHANGE_ME}"`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
L.push(` ${svc.env.rootPassword}: "\${DB_PASSWORD:-CHANGE_ME}"`);
|
|
303
|
+
if (user !== rootUser)
|
|
304
|
+
L.push(` ${svc.env.user}: ${q(user)}`, ` ${svc.env.password}: "\${DB_PASSWORD:-CHANGE_ME}"`);
|
|
305
|
+
}
|
|
306
|
+
if (db.database)
|
|
307
|
+
L.push(` ${svc.env.database}: ${q(db.database)}`);
|
|
308
|
+
L.push(" TZ: Asia/Seoul");
|
|
309
|
+
if (svc.command.length)
|
|
310
|
+
L.push(" command:", ...svc.command.map((c) => ` - ${q(c)}`));
|
|
311
|
+
L.push(" volumes:", ` - db-data:${svc.data}`);
|
|
312
|
+
db.initScripts.forEach((s, i) => L.push(` - ../${s}:${svc.initdb}/${String(i + 1).padStart(2, "0")}-${path.posix.basename(s)}:ro`));
|
|
313
|
+
L.push(" healthcheck:", ` test: ["CMD-SHELL", ${JSON.stringify(svc.healthcheck)}]`, " interval: 10s", " timeout: 5s", " retries: 30", " start_period: 30s");
|
|
314
|
+
L.push(" restart: unless-stopped", "volumes:", " db-data: {}");
|
|
315
|
+
}
|
|
316
|
+
return L.join("\n") + "\n";
|
|
317
|
+
}
|
|
318
|
+
export function renderEnvExample(p, o) {
|
|
319
|
+
return [
|
|
320
|
+
"# docker compose 변수 — .env 로 복사해 값을 바꾸세요(.env 는 저장소에 넣지 않음)",
|
|
321
|
+
`BASE_REGISTRY=${o.imageRegistry}`,
|
|
322
|
+
...(p.web ? [`APP_PORT=${p.port}`] : []),
|
|
323
|
+
...(p.db && p.db.inject !== "none" ? ["DB_PASSWORD=CHANGE_ME"] : []),
|
|
324
|
+
"",
|
|
325
|
+
].join("\n");
|
|
326
|
+
}
|
|
327
|
+
export function renderKubernetes(projectDir, p, o) {
|
|
328
|
+
const r = loadDeployRules();
|
|
329
|
+
const u = r.user;
|
|
330
|
+
const name = p.appName;
|
|
331
|
+
const labels = [`app.kubernetes.io/name: ${name}`, `app.kubernetes.io/version: ${q(p.version)}`, "app.kubernetes.io/managed-by: egovframe-scaffold-mcp"];
|
|
332
|
+
const docs = [];
|
|
333
|
+
const env = dbEnvFor(p, "CHANGE_ME_DB_HOST");
|
|
334
|
+
const plain = env.filter((e) => !e.secret);
|
|
335
|
+
const secret = env.filter((e) => e.secret);
|
|
336
|
+
docs.push(["apiVersion: v1", "kind: ConfigMap", "metadata:", ` name: ${name}-config`, " labels:", ...labels.map((l) => ` ${l}`), "data:", ` TZ: ${q(r.timezone)}`, ` JAVA_OPTS: ${q(`-Dfile.encoding=UTF-8 -Duser.timezone=${r.timezone} -XX:MaxRAMPercentage=75`)}`, ...plain.map((e) => ` ${e.name}: ${q(e.value)}`)]);
|
|
337
|
+
const classic = p.db?.inject === "file" ? renderClassicConfig(projectDir, p, "CHANGE_ME_DB_HOST") : null;
|
|
338
|
+
if (secret.length || classic) {
|
|
339
|
+
const S = ["apiVersion: v1", "kind: Secret", "metadata:", ` name: ${name}-secret`, " labels:", ...labels.map((l) => ` ${l}`), "type: Opaque", "stringData:"];
|
|
340
|
+
for (const e of secret)
|
|
341
|
+
S.push(` ${e.name}: ${q(e.value)}`);
|
|
342
|
+
if (classic) {
|
|
343
|
+
S.push(` ${path.posix.basename(p.db.file)}: |`);
|
|
344
|
+
for (const l of classic.split("\n"))
|
|
345
|
+
S.push(l ? ` ${l}` : "");
|
|
346
|
+
}
|
|
347
|
+
docs.push(S);
|
|
348
|
+
}
|
|
349
|
+
const probe = (kind, pathStr, delay) => [` ${kind}:`, " httpGet:", ` path: ${pathStr}`, " port: http", ` initialDelaySeconds: ${delay}`, " periodSeconds: 10", " timeoutSeconds: 5", " failureThreshold: 6"];
|
|
350
|
+
const D = ["apiVersion: apps/v1", "kind: Deployment", "metadata:", ` name: ${name}`, " labels:", ...labels.map((l) => ` ${l}`), "spec:", " replicas: 1", " selector:", " matchLabels:", ` app.kubernetes.io/name: ${name}`, " template:", " metadata:", " labels:", ...labels.map((l) => ` ${l}`), " spec:",
|
|
351
|
+
" securityContext:", " runAsNonRoot: true", ` runAsUser: ${u.uid}`, ` runAsGroup: ${u.uid}`, ` fsGroup: ${u.uid}`, " seccompProfile:", " type: RuntimeDefault",
|
|
352
|
+
" containers:", ` - name: ${name}`, ` image: ${o.image}`, " imagePullPolicy: IfNotPresent"];
|
|
353
|
+
if (p.web)
|
|
354
|
+
D.push(" ports:", " - name: http", ` containerPort: ${p.port}`);
|
|
355
|
+
D.push(" envFrom:", " - configMapRef:", ` name: ${name}-config`);
|
|
356
|
+
if (secret.length)
|
|
357
|
+
D.push(" - secretRef:", ` name: ${name}-secret`);
|
|
358
|
+
if (p.web)
|
|
359
|
+
D.push(...probe("startupProbe", p.liveness, 10).map((l) => l.replace("failureThreshold: 6", "failureThreshold: 30")), ...probe("readinessProbe", p.readiness, 0), ...probe("livenessProbe", p.liveness, 0));
|
|
360
|
+
D.push(" resources:", " requests:", " cpu: 250m", " memory: 768Mi", " limits:", " cpu: '1'", " memory: 1536Mi", " securityContext:", " allowPrivilegeEscalation: false", " capabilities:", " drop:", " - ALL");
|
|
361
|
+
if (classic) {
|
|
362
|
+
D.push(" volumeMounts:", " - name: db-config", ` mountPath: ${classicTarget(p)}`, ` subPath: ${path.posix.basename(p.db.file)}`, " readOnly: true", " volumes:", " - name: db-config", " secret:", ` secretName: ${name}-secret`, " items:", ` - key: ${path.posix.basename(p.db.file)}`, ` path: ${path.posix.basename(p.db.file)}`);
|
|
363
|
+
}
|
|
364
|
+
docs.push(D);
|
|
365
|
+
if (p.web) {
|
|
366
|
+
docs.push(["apiVersion: v1", "kind: Service", "metadata:", ` name: ${name}`, " labels:", ...labels.map((l) => ` ${l}`), "spec:", " type: ClusterIP", " selector:", ` app.kubernetes.io/name: ${name}`, " ports:", " - name: http", " port: 80", " targetPort: http"]);
|
|
367
|
+
if (o.ingressHost)
|
|
368
|
+
docs.push(["apiVersion: networking.k8s.io/v1", "kind: Ingress", "metadata:", ` name: ${name}`, " labels:", ...labels.map((l) => ` ${l}`), "spec:", " rules:", ` - host: ${o.ingressHost}`, " http:", " paths:", ` - path: ${p.contextPath || "/"}`, " pathType: Prefix", " backend:", " service:", ` name: ${name}`, " port:", " name: http"]);
|
|
369
|
+
}
|
|
370
|
+
return [`# eGovFrame Kubernetes 매니페스트 — egovframe-scaffold-mcp ${SERVER_VERSION} generate_egovframe_deploy`, `# 적용 전: 이미지(${o.image})를 레지스트리에 올리고 CHANGE_ME 값(DB 호스트·비밀번호)을 바꾸세요. kubectl apply -f deploy/k8s/`, ...docs.map((d) => `---\n${d.join("\n")}`)].join("\n") + "\n";
|
|
371
|
+
}
|
|
372
|
+
export const DEPLOY_IMAGE_REGISTRY_RE = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:\d{1,5})?(\/[a-z0-9._-]+)*$/;
|
|
373
|
+
export const DEPLOY_IMAGE_RE = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:\d{1,5})?(\/[a-z0-9._-]+)*(:[\w.-]{1,128})?$/;
|
|
374
|
+
export const DEPLOY_HOST_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
375
|
+
export async function generateDeploy(opts) {
|
|
376
|
+
const projectDir = path.resolve(opts.projectDir);
|
|
377
|
+
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory())
|
|
378
|
+
throw new Error(`프로젝트 디렉터리가 없습니다: ${projectDir}`);
|
|
379
|
+
const rules = loadDeployRules();
|
|
380
|
+
const jdk = opts.jdk ?? "17";
|
|
381
|
+
if (!rules.jdks.includes(jdk))
|
|
382
|
+
throw new Error(`jdk 는 ${rules.jdks.join("·")} 중 하나여야 합니다(5.x 는 Java 17 이상): ${jdk}`);
|
|
383
|
+
const imageRegistry = (opts.imageRegistry ?? "docker.io/library").replace(/\/+$/, "");
|
|
384
|
+
if (!DEPLOY_IMAGE_REGISTRY_RE.test(imageRegistry))
|
|
385
|
+
throw new Error(`imageRegistry 형식 오류(예: docker.io/library, harbor.example.go.kr/dockerhub): ${JSON.stringify(opts.imageRegistry)}`);
|
|
386
|
+
if (opts.ingressHost !== undefined && !DEPLOY_HOST_RE.test(opts.ingressHost))
|
|
387
|
+
throw new Error(`ingressHost 는 도메인 이름이어야 합니다: ${JSON.stringify(opts.ingressHost)}`);
|
|
388
|
+
const targets = [...new Set(opts.targets?.length ? opts.targets : ["docker", "compose", "k8s"])];
|
|
389
|
+
const dryRun = opts.dryRun !== false;
|
|
390
|
+
const a = analyzeDeploy(projectDir);
|
|
391
|
+
if (!a.profile)
|
|
392
|
+
return { projectDir, supported: false, unsupported: a.unsupported, dryRun, written: false, files: [], usage: [], notes: a.notes };
|
|
393
|
+
const p = a.profile;
|
|
394
|
+
if (opts.appName) {
|
|
395
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,48}[a-z0-9])?$/.test(opts.appName))
|
|
396
|
+
throw new Error(`appName 은 소문자·숫자·하이픈 50자 이하여야 합니다: ${opts.appName}`);
|
|
397
|
+
p.appName = opts.appName;
|
|
398
|
+
}
|
|
399
|
+
const image = opts.image ?? `${p.appName}:${p.version.toLowerCase().replace(/[^\w.-]/g, "-")}`;
|
|
400
|
+
if (!DEPLOY_IMAGE_RE.test(image))
|
|
401
|
+
throw new Error(`image 형식 오류: ${image}`);
|
|
402
|
+
const dockerfile = (opts.dockerfilePath ?? "Dockerfile").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
403
|
+
if (!/^(?!\/)(?!.*(^|\/)\.\.(\/|$))[\w.\-/]+$/.test(dockerfile))
|
|
404
|
+
throw new Error(`dockerfilePath 는 프로젝트 안의 상대 경로여야 합니다: ${opts.dockerfilePath}`);
|
|
405
|
+
const dockerignore = dockerfile === "Dockerfile" ? ".dockerignore" : `${dockerfile}.dockerignore`;
|
|
406
|
+
const o = { jdk, imageRegistry, buildInImage: opts.buildInImage !== false, image, ingressHost: opts.ingressHost, dockerfile };
|
|
407
|
+
const notes = [...a.notes];
|
|
408
|
+
const files = [];
|
|
409
|
+
if (targets.includes("docker") || targets.includes("compose")) {
|
|
410
|
+
files.push({ path: dockerfile, target: "docker", content: renderDockerfile(p, o) }, { path: dockerignore, target: "docker", content: renderDockerignore(p, o) });
|
|
411
|
+
}
|
|
412
|
+
if (targets.includes("compose")) {
|
|
413
|
+
files.push({ path: "deploy/docker-compose.yml", target: "compose", content: renderCompose(p, o) }, { path: "deploy/.env.example", target: "compose", content: renderEnvExample(p, o) });
|
|
414
|
+
const svcHost = p.db?.service === "container" ? "db" : (p.db?.host ?? "db");
|
|
415
|
+
const cfg = renderClassicConfig(projectDir, p, svcHost);
|
|
416
|
+
if (cfg)
|
|
417
|
+
files.push({ path: `deploy/config/${path.posix.basename(p.db.file)}`, target: "compose", content: cfg + "\n" });
|
|
418
|
+
}
|
|
419
|
+
if (targets.includes("k8s"))
|
|
420
|
+
files.push({ path: `deploy/k8s/${p.appName}.yaml`, target: "k8s", content: renderKubernetes(projectDir, p, o) });
|
|
421
|
+
if (p.db?.service === "external" && p.db.inject !== "none")
|
|
422
|
+
notes.push(`DB(${p.db.type})는 ${p.db.serviceNote} — compose 는 앱만 띄우고 연결 정보(${p.db.url ? replaceJdbcHost(p.db.url, p.db.host ?? "") : "Url 없음"})를 그대로 씁니다.`);
|
|
423
|
+
if (p.db?.inject === "env")
|
|
424
|
+
notes.push(`DB 설정은 Spring 환경 변수로 덮어씁니다(${dbEnvFor(p, "db").map((e) => `${e.key} → ${e.name}`).join(", ")}).`);
|
|
425
|
+
if (p.db?.inject === "file")
|
|
426
|
+
notes.push(`Classic 은 WAR 를 풀어 배포하고 ${p.db.file} 을 compose 는 볼륨(deploy/config/), Kubernetes 는 Secret 으로 덮어씁니다(${classicTarget(p)}).`);
|
|
427
|
+
if (!p.web)
|
|
428
|
+
notes.push("웹 의존성이 없는 Boot 앱(배치 등) — 포트·헬스체크·Service 없이 만듭니다. 한 번 실행하고 끝나는 커맨드라인 배치는 Kubernetes Job·CronJob 으로 바꾸세요.");
|
|
429
|
+
if (p.web && p.liveness === `${p.contextPath}/`)
|
|
430
|
+
notes.push(`헬스체크는 ${p.contextPath || "/"} 의 HTTP 응답으로 봅니다${p.runtime === "boot" ? " — spring-boot-starter-actuator 와 management.endpoint.health.probes.enabled=true 가 있으면 /actuator/health/liveness·readiness 를 씁니다" : ""}.`);
|
|
431
|
+
if (!o.buildInImage)
|
|
432
|
+
notes.push(`buildInImage=false — 이미지 빌드 전에 ${p.buildTool === "maven" ? "mvn -B -DskipTests package" : "./gradlew build -x test"} 로 산출물을 만드세요(폐쇄망에서 컨테이너 안 Maven 접근이 어려울 때).`);
|
|
433
|
+
// 템플릿이 이미 배포 구성을 갖고 있으면 알린다(공식 simple-backend 등)
|
|
434
|
+
const upstream = ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml", "k8s", "kubernetes", "helm", "charts"].filter((f) => fs.existsSync(path.join(projectDir, f)));
|
|
435
|
+
if (upstream.length)
|
|
436
|
+
notes.push(`프로젝트에 이미 배포 구성(${upstream.join(", ")})이 있습니다 — 템플릿이 제공한 구성을 먼저 검토하세요. 함께 두려면 dockerfilePath(예: deploy/Dockerfile)로 겹치지 않게 만드세요.`);
|
|
437
|
+
const exists = files.filter((f) => fs.existsSync(path.join(projectDir, f.path))).map((f) => f.path);
|
|
438
|
+
if (exists.length && !dryRun)
|
|
439
|
+
throw new Error(`이미 있는 파일은 덮어쓰지 않습니다: ${exists.join(", ")}`);
|
|
440
|
+
if (exists.length)
|
|
441
|
+
notes.push(`이미 있는 파일: ${exists.join(", ")} — dryRun=false 면 거부됩니다.`);
|
|
442
|
+
if (!dryRun)
|
|
443
|
+
await withFileTransaction(projectDir, "배포 산출물 생성", (tx) => { for (const f of files)
|
|
444
|
+
tx.writeFile(f.path, f.content, { mustNotExist: true }); });
|
|
445
|
+
const usage = [
|
|
446
|
+
...(files.some((f) => f.path === dockerfile) ? [`docker build -t ${image}${dockerfile === "Dockerfile" ? "" : ` -f ${dockerfile}`} .${imageRegistry !== "docker.io/library" ? "" : " # 폐쇄망: --build-arg BASE_REGISTRY=<사내 레지스트리>"}`] : []),
|
|
447
|
+
...(p.web && files.some((f) => f.path === dockerfile) ? [`docker run --rm -p ${p.port}:${p.port} ${image} # http://localhost:${p.port}${p.contextPath || "/"}`] : []),
|
|
448
|
+
...(targets.includes("compose") ? ["cd deploy && cp .env.example .env && docker compose up --build"] : []),
|
|
449
|
+
...(targets.includes("k8s") ? [`kubectl apply -f deploy/k8s/${p.appName}.yaml # CHANGE_ME 를 먼저 바꾸고 이미지를 레지스트리에 올린 뒤`] : []),
|
|
450
|
+
];
|
|
451
|
+
return { projectDir, supported: true, profile: p, dryRun, written: !dryRun, files: files.map((f) => ({ path: f.path, target: f.target, bytes: Buffer.byteLength(f.content), ...(dryRun ? { content: f.content } : {}) })), usage, notes };
|
|
452
|
+
}
|
|
453
|
+
export function renderDeployMarkdown(r) {
|
|
454
|
+
const L = [`# 배포 산출물 ${r.dryRun ? "(dryRun — 쓰지 않음)" : "생성"}`, ""];
|
|
455
|
+
if (!r.supported) {
|
|
456
|
+
L.push(`- 지원하지 않는 구조: ${r.unsupported.reason}`);
|
|
457
|
+
for (const n of r.notes)
|
|
458
|
+
L.push(`- ${n}`);
|
|
459
|
+
return L.join("\n");
|
|
460
|
+
}
|
|
461
|
+
const p = r.profile;
|
|
462
|
+
L.push(`- ${p.appName} ${p.version} · ${p.buildTool}${p.wrapper ? "(래퍼)" : ""} · ${p.packaging} → ${p.runtime === "tomcat" ? `Tomcat ${p.tomcat}(ROOT)` : "Boot java -jar"}${p.web ? ` · 포트 ${p.port}${p.contextPath ? ` · 컨텍스트 ${p.contextPath}` : ""} · 헬스 ${p.liveness}` : " · 웹 없음"}`);
|
|
463
|
+
if (p.db)
|
|
464
|
+
L.push(`- DB: ${p.db.type} (${p.db.file}) — ${p.db.inject === "env" ? "환경 변수로 덮어씀" : p.db.inject === "file" ? "설정 파일 덮어씀" : "내장 DB"}${p.db.service === "container" && p.db.inject !== "none" ? ` · compose DB 컨테이너${p.db.initScripts.length ? `(초기화 ${p.db.initScripts.join(", ")})` : ""}` : p.db.inject !== "none" ? " · 외부 DB" : ""}`);
|
|
465
|
+
L.push("", "| 파일 | 대상 | 크기 |", "|---|---|---|");
|
|
466
|
+
for (const f of r.files)
|
|
467
|
+
L.push(`| ${f.path} | ${f.target} | ${f.bytes} B |`);
|
|
468
|
+
if (r.usage.length)
|
|
469
|
+
L.push("", "```bash", ...r.usage, "```");
|
|
470
|
+
for (const n of r.notes)
|
|
471
|
+
L.push(`- ${n}`);
|
|
472
|
+
if (r.dryRun)
|
|
473
|
+
for (const f of r.files.filter((x) => x.content))
|
|
474
|
+
L.push("", `### ${f.path}`, "", "```" + (f.path.endsWith(".yml") || f.path.endsWith(".yaml") ? "yaml" : f.path === "Dockerfile" ? "dockerfile" : ""), f.content.trimEnd(), "```");
|
|
475
|
+
return L.join("\n");
|
|
476
|
+
}
|
package/dist/i18n.js
CHANGED
|
@@ -26,6 +26,7 @@ export const TOOL_DESCRIPTIONS_EN = {
|
|
|
26
26
|
generate_egovframe_sbom: "Generate a CycloneDX 1.6 JSON (default) or SPDX 2.3 JSON (bomFormat=spdx-json: packages, DEPENDS_ON relationships, purls, supplier, creators, created) SBOM for a Maven or Gradle project without changing its build files: Maven runs cyclonedx-maven-plugin (hashes, licenses), Gradle is built from the resolved dependency tree. enrich=true adds the baseline verdict (egovframe:status/basis/baseline) to every component; offline=false adds known vulnerabilities from OSV as vulnerabilities[]. Writes inside the project only (default sbom/bom.cdx.json), refuses existing files unless overwrite=true, dryRun (default) returns the plan only.",
|
|
27
27
|
generate_agents_md: "Generate AGENTS.md for AI coding tools from project diagnosis: build/test commands (wrapper-aware), RTE version and 5.x migration status, DbType, base packages and config directories, installed components, rules (coordinates, backup dirs, secrets, dependency baseline) and available MCP tools. Refuses existing files unless overwrite=true; dryRun returns content only; ko/en.",
|
|
28
28
|
search_egovframe_docs: "Search the official guide index by keyword (offline; fetchTop>0 downloads snippets).",
|
|
29
|
+
generate_egovframe_deploy: "Generate container and deployment files from the project diagnosis (build tool, packaging, Boot or Classic, web, actuator, DbType, configuration file): a Dockerfile (multi-stage Maven/Gradle build, Classic war unpacked as ROOT on Tomcat 10.1 or 11.0 for Servlet 6.1+, Boot jar on the Eclipse Temurin JRE; non-root, HEALTHCHECK, Asia/Seoul, UTF-8; buildInImage=false packs a prebuilt artifact), .dockerignore, deploy/docker-compose.yml (app plus a mysql/mariadb/postgres container per DbType initialised with the template DDL and data; commercial databases as connection settings only) and Kubernetes manifests (Deployment with startup/readiness/liveness probes, non-root securityContext and resources, Service, ConfigMap, Secret, optional Ingress). Database settings are overridden through Spring environment variables for Boot (Globals.mysql.Url -> GLOBALS_MYSQL_URL) and through a copy of globals.properties mounted as a volume or Secret for Classic. imageRegistry pulls base images from an internal registry for air-gapped networks. New files only; dryRun (default) shows the content. Multi-module projects, library jars and projects without a build file return the reason.",
|
|
29
30
|
check_egovframe_secure_coding: "Scan project sources for security weaknesses from the implementation-stage list of the Korean Ministry of the Interior and Safety / KISA software development security guide (2021) that static patterns can find with controlled false positives: SQL injection (MyBatis ${…}, iBATIS $…$, JDBC string concatenation), XSS (request values or EL param/header/cookie printed without escaping, <c:out escapeXml=\"false\">), path traversal, open redirect, OS command execution, XXE, weak hashes and ciphers, insecure random in security contexts, hard-coded secrets in code and configuration, cookies without HttpOnly/Secure, printStackTrace, empty catch blocks, unsafe deserialization and leftover debug output. Rules are data (catalog/secure-coding-rules.json); comments are ignored; each finding has file, line, CWE, severity, fix and origin (common component original / modified / unmanaged, project code, test). Exceptions in .egovframe-secure-ignore.json need a reason and reviewer; baselinePath (previous JSON result or SARIF) marks new findings; format=sarif or sarifPath writes SARIF 2.1.0 for code scanning. Pattern-based aid, not a certified diagnosis.",
|
|
30
31
|
check_egovframe_sbom: "Check an existing CycloneDX or SPDX 2.x JSON SBOM (format detected automatically) as a submission artifact without changing it: (1) the 7 minimum elements (supplier, component name, version, unique identifier, dependency relationship, author, timestamp; NTIA minimum elements, same as the Korean SW supply-chain security guideline 1.0) per component with a ready / needs-work verdict, (2) re-check without any build tool from purls only: baseline verdicts that changed since generation and, with offline=false, OSV vulnerabilities newly published since generation, (3) with baselinePath, a diff against a previous SBOM (added, removed, version changes, verdict changes, new vulnerabilities), (4) with vex=true (CycloneDX input only), a CycloneDX VEX draft (default sbom/vex.cdx.json) where new findings are analysis.state=in_triage referencing the SBOM via BOM-Link; human decisions in an existing VEX are preserved and only new findings are added (dryRun previews).",
|
|
31
32
|
rehearse_egovframe_migration: "Rehearse the 5.x migration on a copy of the project without touching it: copy (build output, VCS and backups excluded) into EGOVFRAME_CACHE_DIR/rehearsal, then (1) reassemble the common components (before apply, so the origin tag is still recognisable), (2) apply the automatic migration items, (3) compile and count every javac error (Maven forks javac with -Xmaxerrs, no 100-error cap), (4) align the copy's pom to the official common-components pom of the pinned tag (parent, missing coordinates, versions, scopes, parent-managed properties) and compile again. Reports both error counts, missing packages with candidate coordinates, the cascade errors in the same files, manual items and files that remain, a worklist ordered by errors removed, the pom patch, and proves the original is unchanged with a before/after fingerprint. The latest result is shown in the assessment report.",
|
package/dist/index.js
CHANGED
|
@@ -102,3 +102,4 @@ export { COMPONENTS_REPOSITORY, gitBlobId, blobIdsOf, tagVersionKey, compareTags
|
|
|
102
102
|
export { EXIT as CLI_EXIT, COMMANDS as CLI_COMMANDS, parseArgs, kebabToCamel, coerceArgs, parseFailOn, evaluateFailOn, usage as cliUsage, runCli } from "./cli.js";
|
|
103
103
|
export { REASSEMBLE_STATES, REASSEMBLE_STATE_LABEL, unifiedPatch, reassembleComponents, renderReassembleMarkdown } from "./reassemble.js";
|
|
104
104
|
export { loadSecureRules, compileRulePattern, stripJavaComments, stripMarkupComments, targetOf, checkSecureCoding, toSarif, renderSecureCodingMarkdown, IGNORE_FILE } from "./secure-coding.js";
|
|
105
|
+
export { loadDeployRules, analyzeDeploy, parseProperties, parseJdbcUrl, envNameOf, renderDockerfile, renderDockerignore, renderCompose, renderEnvExample, renderKubernetes, renderClassicConfig, generateDeploy, renderDeployMarkdown, DEPLOY_IMAGE_REGISTRY_RE, DEPLOY_IMAGE_RE, DEPLOY_HOST_RE } from "./deploy.js";
|
package/dist/output-schemas.js
CHANGED
|
@@ -278,6 +278,22 @@ export const SecureCodingOutput = loose({
|
|
|
278
278
|
notes: z.array(z.string()),
|
|
279
279
|
disclaimer: z.string(),
|
|
280
280
|
});
|
|
281
|
+
// v0.45: generate_egovframe_deploy
|
|
282
|
+
export const DeployOutput = loose({
|
|
283
|
+
projectDir: z.string(),
|
|
284
|
+
supported: z.boolean(),
|
|
285
|
+
unsupported: loose({ id: z.string(), reason: z.string() }).optional(),
|
|
286
|
+
profile: loose({
|
|
287
|
+
appName: z.string(), version: z.string(), buildTool: z.enum(["maven", "gradle"]), wrapper: z.boolean(), packaging: z.enum(["war", "jar"]), runtime: z.enum(["tomcat", "boot"]),
|
|
288
|
+
web: z.boolean(), actuator: z.boolean(), tomcat: z.enum(["10.1", "11.0"]), port: z.number().int(), contextPath: z.string(), liveness: z.string(), readiness: z.string(),
|
|
289
|
+
db: loose({ type: z.string(), inject: z.enum(["env", "file", "none"]), file: z.string(), url: z.string().nullable(), user: z.string().nullable(), host: z.string().nullable(), port: z.number().int().nullable(), database: z.string().nullable(), service: z.enum(["container", "external"]), initScripts: z.array(z.string()) }).nullable(),
|
|
290
|
+
}).optional(),
|
|
291
|
+
dryRun: z.boolean(),
|
|
292
|
+
written: z.boolean(),
|
|
293
|
+
files: z.array(loose({ path: z.string(), target: z.enum(["docker", "compose", "k8s"]), bytes: z.number().int(), content: z.string().optional() })),
|
|
294
|
+
usage: z.array(z.string()),
|
|
295
|
+
notes: z.array(z.string()),
|
|
296
|
+
});
|
|
281
297
|
export const OUTPUT_SCHEMAS = {
|
|
282
298
|
diagnose_egovframe_project: DiagnoseOutput,
|
|
283
299
|
validate_egovframe_project: ValidateOutput,
|
|
@@ -287,6 +303,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
287
303
|
generate_egovframe_sbom: SbomOutput,
|
|
288
304
|
check_egovframe_sbom: SbomCheckOutput,
|
|
289
305
|
check_egovframe_secure_coding: SecureCodingOutput,
|
|
306
|
+
generate_egovframe_deploy: DeployOutput,
|
|
290
307
|
rehearse_egovframe_migration: RehearseOutput,
|
|
291
308
|
prepare_egovframe_offline_bundle: OfflineBundleOutput,
|
|
292
309
|
generate_egovframe_report: ReportOutput,
|
package/dist/server.js
CHANGED
|
@@ -29,6 +29,7 @@ import { generateAgentsMd } from "./agents-md.js";
|
|
|
29
29
|
import { generateSbom, renderSbomMarkdown } from "./sbom.js";
|
|
30
30
|
import { checkSbom, renderSbomCheckMarkdown } from "./sbom-check.js";
|
|
31
31
|
import { checkSecureCoding, renderSecureCodingMarkdown, toSarif } from "./secure-coding.js";
|
|
32
|
+
import { DEPLOY_HOST_RE, DEPLOY_IMAGE_RE, DEPLOY_IMAGE_REGISTRY_RE, generateDeploy, renderDeployMarkdown } from "./deploy.js";
|
|
32
33
|
import { rehearseMigration, renderRehearsalMarkdown } from "./rehearse.js";
|
|
33
34
|
import { prepareEgovframeOfflineBundle } from "./offline-bundle-plan.js";
|
|
34
35
|
import { renderBundleMarkdown } from "./offline-bundle.js";
|
|
@@ -564,6 +565,25 @@ export function buildServer(opts = {}) {
|
|
|
564
565
|
const text = args.format === "json" ? JSON.stringify(r, null, 2) : renderRehearsalMarkdown(r);
|
|
565
566
|
return { content: [{ type: "text", text }], structuredContent: r };
|
|
566
567
|
});
|
|
568
|
+
// ── 컨테이너·배포 산출물 (v0.45) ──────────────────────
|
|
569
|
+
server.registerTool("generate_egovframe_deploy", { title: t("generate_egovframe_deploy"), description: d("generate_egovframe_deploy", "프로젝트 진단(빌드 도구·패키징·Boot 여부·웹·actuator·DbType·설정 파일)으로 컨테이너·배포 산출물을 만듭니다 — Dockerfile(멀티 스테이지: Maven/Gradle 빌드 → Classic war 는 Tomcat 10.1(Servlet 6.1 이상이면 11.0)에 ROOT 로 풀어서, Boot jar 는 eclipse-temurin JRE 로; 비루트·HEALTHCHECK·Asia/Seoul·UTF-8, buildInImage=false 면 미리 빌드한 산출물), .dockerignore, deploy/docker-compose.yml(앱 + DbType 별 DB 컨테이너 mysql·mariadb·postgres, 템플릿 DDL·DATA 초기화, 상용 DB 는 연결 정보만), Kubernetes 매니페스트(deploy/k8s/<app>.yaml — Deployment(startup·readiness·liveness 프로브, 비루트 securityContext, 리소스)·Service·ConfigMap·Secret·선택 Ingress). DB 설정은 Boot 는 Spring 환경 변수(Globals.mysql.Url → GLOBALS_MYSQL_URL), Classic 은 globals.properties 사본을 볼륨·Secret 으로 덮어씁니다. imageRegistry 로 기반 이미지를 사내 레지스트리(폐쇄망)에서 받습니다. 새 파일만 쓰고(기존 파일 거부) dryRun(기본)은 내용만 보여 줍니다. 멀티 모듈·라이브러리 jar·빌드 파일 없는 프로젝트는 이유를 돌려줍니다."), inputSchema: {
|
|
570
|
+
projectDir: z.string().describe("프로젝트 디렉터리(절대경로 권장)"),
|
|
571
|
+
targets: z.array(z.enum(["docker", "compose", "k8s"])).min(1).default(["docker", "compose", "k8s"]).describe("만들 산출물(compose 는 Dockerfile 도 함께)"),
|
|
572
|
+
jdk: z.enum(["17", "21"]).default("17").describe("JDK(빌드·실행 이미지)"),
|
|
573
|
+
imageRegistry: z.string().regex(DEPLOY_IMAGE_REGISTRY_RE, "예: docker.io/library, harbor.example.go.kr/dockerhub").default("docker.io/library").describe("기반 이미지 레지스트리(폐쇄망 사내 레지스트리·프록시 캐시) — Dockerfile ARG BASE_REGISTRY 기본값"),
|
|
574
|
+
image: z.string().regex(DEPLOY_IMAGE_RE).optional().describe("앱 이미지 이름(기본 <appName>:<version>)"),
|
|
575
|
+
appName: z.string().regex(/^[a-z0-9]([a-z0-9-]{0,48}[a-z0-9])?$/).optional().describe("앱 이름(기본 artifactId)"),
|
|
576
|
+
buildInImage: z.boolean().default(true).describe("true: 이미지 안에서 빌드(멀티 스테이지) · false: 미리 빌드한 target/*.war·jar 를 담음(폐쇄망에서 컨테이너 안 Maven 접근이 어려울 때)"),
|
|
577
|
+
dockerfilePath: z.string().default("Dockerfile").describe("Dockerfile 경로(프로젝트 상대) — 템플릿에 이미 Dockerfile 이 있으면 deploy/Dockerfile 등"),
|
|
578
|
+
ingressHost: z.string().regex(DEPLOY_HOST_RE).optional().describe("Kubernetes Ingress 호스트(주면 Ingress 추가)"),
|
|
579
|
+
dryRun: z.boolean().default(true).describe("true(기본)면 쓰지 않고 내용만"),
|
|
580
|
+
format: z.enum(["markdown", "json"]).default("markdown").describe("응답 형식"),
|
|
581
|
+
}, outputSchema: OUTPUT_SCHEMAS.generate_egovframe_deploy.shape, annotations: toolAnnotations("generate_egovframe_deploy") }, async (args) => {
|
|
582
|
+
enforceAllowedRoots(args);
|
|
583
|
+
const r = await generateDeploy({ projectDir: args.projectDir, targets: args.targets, jdk: args.jdk, imageRegistry: args.imageRegistry, image: args.image, appName: args.appName, buildInImage: args.buildInImage, dockerfilePath: args.dockerfilePath, ingressHost: args.ingressHost, dryRun: args.dryRun });
|
|
584
|
+
const text = args.format === "json" ? JSON.stringify(r, null, 2) : renderDeployMarkdown(r);
|
|
585
|
+
return { content: [{ type: "text", text }], structuredContent: r };
|
|
586
|
+
});
|
|
567
587
|
// ── 보안약점 점검 (v0.44) ─────────────────────────────
|
|
568
588
|
server.registerTool("check_egovframe_secure_coding", { title: t("check_egovframe_secure_coding"), description: d("check_egovframe_secure_coding", "행정안전부·KISA 「소프트웨어 개발보안 가이드」(2021) 구현단계 보안약점 중 정적 패턴으로 오탐을 통제할 수 있는 항목을 소스에서 찾습니다 — SQL 삽입(MyBatis ${…}·iBATIS $…$·JDBC 문자열 연결), 크로스사이트 스크립트(요청 값·EL param/header/cookie 무이스케이프 출력, <c:out escapeXml=\"false\">), 경로 조작, 신뢰되지 않는 URL 자동접속, 운영체제 명령어, XML 외부개체, 취약한 해시·암호, 보안 문맥의 부적절한 난수, 코드·설정의 하드코드된 중요정보, HttpOnly·Secure 없는 쿠키, printStackTrace, 빈 catch, 역직렬화, 남은 디버그 출력. 규칙은 catalog/secure-coding-rules.json 데이터이고 주석은 무시합니다. 항목마다 파일·라인·CWE·등급·조치와 출처(공통컴포넌트 원본·수정본·매니페스트 없음·프로젝트 코드·테스트)를 냅니다. 예외는 .egovframe-secure-ignore.json(사유·검토자 필수), baselinePath(이전 JSON 결과·SARIF)와 비교해 새 항목 표시, format=sarif 또는 sarifPath 로 SARIF 2.1.0(GitHub code scanning 등). 패턴 기반 진단 보조이며 공인 진단을 대체하지 않습니다."), inputSchema: {
|
|
569
589
|
projectDir: z.string().describe("프로젝트 디렉터리(절대경로 권장) — 읽기만 함"),
|
package/dist/tool-meta.js
CHANGED
|
@@ -30,6 +30,8 @@ export const TOOL_META = {
|
|
|
30
30
|
generate_egovframe_sbom: { title: { ko: "SBOM 생성 (CycloneDX)", en: "Generate SBOM (CycloneDX)" }, annotations: create(true) },
|
|
31
31
|
// v0.40: SBOM 은 읽기만 하고 vex=true 일 때만 VEX 파일을 새로 만들거나 판단을 보존하며 항목을 덧붙인다(비파괴), offline=false 는 OSV
|
|
32
32
|
// v0.44: 소스를 읽기만 하지만 sarifPath 를 주면 그 파일을 쓴다(overwrite 로 덮어쓸 수 있음)
|
|
33
|
+
// v0.45: 새 파일만 만든다(기존 파일 거부) — 로컬 파일만 읽음
|
|
34
|
+
generate_egovframe_deploy: { title: { ko: "컨테이너·배포 산출물 생성", en: "Generate container and deployment files" }, annotations: create() },
|
|
33
35
|
check_egovframe_secure_coding: { title: { ko: "보안약점 점검 (개발보안 가이드)", en: "Check secure coding weaknesses" }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } },
|
|
34
36
|
check_egovframe_sbom: { title: { ko: "SBOM 점검 (최소 요소·비교·VEX)", en: "Check SBOM (minimum elements, diff, VEX)" }, annotations: create(true) },
|
|
35
37
|
search_egovframe_docs: { title: { ko: "가이드 문서 검색", en: "Search guide documents" }, annotations: ro(true) },
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 컨테이너·배포 산출물 설계 (`generate_egovframe_deploy`, v0.45)
|
|
2
|
+
|
|
3
|
+
## 1. 문제
|
|
4
|
+
|
|
5
|
+
공공 정보시스템의 클라우드 네이티브 전환으로, 표준프레임워크 프로젝트도 컨테이너 이미지와 쿠버네티스 매니페스트를 함께 납품하는 일이 늘었습니다. 그런데 사업마다 Dockerfile 을 손으로 쓰고, 다음이 매번 달라집니다.
|
|
6
|
+
|
|
7
|
+
- 실행 단위: Classic(war)·Boot(jar)
|
|
8
|
+
- DB 종류와 초기화 스크립트
|
|
9
|
+
- DB 설정을 넣는 방법: `globals.properties` 냐 `application.properties` 냐
|
|
10
|
+
|
|
11
|
+
이 도구는 진단 결과로 같은 기준의 산출물을 만듭니다.
|
|
12
|
+
|
|
13
|
+
## 2. 분석 (`analyzeDeploy`)
|
|
14
|
+
|
|
15
|
+
| 항목 | 근거 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 빌드 도구·래퍼 | `pom.xml` / `build.gradle(.kts)`, `mvnw`·`gradlew` |
|
|
18
|
+
| 패키징 | Maven `<packaging>war</packaging>`, Gradle `war` 플러그인 |
|
|
19
|
+
| Boot 여부 | parent `spring-boot-starter-parent`·`egovframe-boot-starter-parent`, `spring-boot-maven-plugin`, Gradle `org.springframework.boot` |
|
|
20
|
+
| 웹 여부 | war 이거나 `spring-boot-starter-web`·`webflux`·`thymeleaf`(아니면 배치) |
|
|
21
|
+
| Tomcat 버전 | 기본 10.1(Servlet 6.0). pom 이 `jakarta.servlet-api` 6.1 이상을 직접 선언하면 11.0 |
|
|
22
|
+
| 포트·컨텍스트 경로 | Boot `server.port`·`server.servlet.context-path` |
|
|
23
|
+
| 헬스 경로 | actuator 와 `management.endpoint.health.probes.enabled=true` 가 있으면 `/actuator/health/liveness`·`readiness`, actuator 만 있으면 `/actuator/health`, 그 밖에는 `/` |
|
|
24
|
+
| DB | `Globals.DbType` 과 `Globals.<type>.Url`(Boot 템플릿) 또는 `Globals.Url`(Classic 템플릿), 그 밖에는 `spring.datasource.url` |
|
|
25
|
+
| 초기화 SQL | 파일 이름에 DB 종류와 `ddl`·`data` 가 들어간 `.sql`(얕은 경로 우선) |
|
|
26
|
+
|
|
27
|
+
다음 구조는 지원하지 않으며, 파일을 만들지 않고 이유를 돌려줍니다.
|
|
28
|
+
|
|
29
|
+
- 멀티 모듈: 모듈별 이미지가 필요하고, MSA 템플릿은 저장소에 자체 구성이 있습니다.
|
|
30
|
+
- 라이브러리 jar
|
|
31
|
+
- 빌드 파일이 없는 프론트엔드
|
|
32
|
+
|
|
33
|
+
## 3. Dockerfile
|
|
34
|
+
|
|
35
|
+
- **빌드 단계**(기본, 이미지 안에서 빌드)
|
|
36
|
+
- `maven:3.9-eclipse-temurin-<JDK>` 에서 `mvn -B -q -DskipTests package` 를 실행합니다.
|
|
37
|
+
- `RUN --mount=type=cache,target=/root/.m2` 로 로컬 저장소를 캐시하고, Maven 5xx 재시도 옵션을 붙입니다. `maven.egovframe.go.kr` 의 일시 502 대응입니다(v0.42 CI 실측).
|
|
38
|
+
- Gradle 은 래퍼(`./gradlew`)가 있으면 JDK 이미지, 없으면 `gradle:8-jdk<JDK>` 를 씁니다.
|
|
39
|
+
- 산출물은 `find … ! -name '*-plain.*'` 로 고릅니다(Gradle Boot 의 plain jar 제외).
|
|
40
|
+
- **미리 빌드**(`buildInImage=false`)
|
|
41
|
+
- 컨테이너 안에서 Maven 저장소에 닿지 않는 폐쇄망용입니다.
|
|
42
|
+
- 호스트에서 만든 `target/*.war`·`jar` 만 보내도록 `.dockerignore` 를 산출물 화이트리스트로 만듭니다.
|
|
43
|
+
- **Classic 실행**
|
|
44
|
+
- `tomcat:10.1-jdk<JDK>-temurin` 을 씁니다.
|
|
45
|
+
- WAR 를 **빌드 단계에서 풀어** `webapps/ROOT` 로 복사합니다. 실행 중 설정 파일(`WEB-INF/classes/…/globals.properties`)을 볼륨·Secret 으로 덮어쓸 수 있게 하려는 것입니다. 풀린 디렉터리가 있으면 Tomcat 은 WAR 를 다시 풀지 않습니다.
|
|
46
|
+
- **Boot 실행**: `eclipse-temurin:<JDK>-jre` 에서 `ENTRYPOINT ["sh","-c","exec java $JAVA_OPTS -jar /app/app.jar"]` 로 실행합니다(시그널 전달).
|
|
47
|
+
- **공통**
|
|
48
|
+
- 비루트 사용자 uid 10001.
|
|
49
|
+
- `TZ=Asia/Seoul`, `LANG=C.UTF-8`, `-Dfile.encoding=UTF-8 -XX:MaxRAMPercentage=75`.
|
|
50
|
+
- `HEALTHCHECK curl -fsS`(웹만). 실측으로 두 기반 이미지 모두에 curl 이 있습니다.
|
|
51
|
+
- **기반 이미지 레지스트리**: `ARG BASE_REGISTRY=<imageRegistry>` 로 바꿉니다(폐쇄망 사내 레지스트리·프록시 캐시).
|
|
52
|
+
- **`# syntax` 지시어 없음**: 폐쇄망에서 `docker/dockerfile` 이미지를 받지 않도록 쓰지 않습니다. BuildKit(Docker 23 이상 기본)의 내장 프론트엔드로 `RUN --mount` 를 씁니다.
|
|
53
|
+
- **경로**: 템플릿에 Dockerfile 이 이미 있으면(공식 simple-backend 등) `dockerfilePath`(예: `deploy/Dockerfile`)로 겹치지 않게 만듭니다. 이때 `.dockerignore` 는 BuildKit 규칙대로 `<경로>.dockerignore` 입니다.
|
|
54
|
+
|
|
55
|
+
## 4. DB 설정 주입
|
|
56
|
+
|
|
57
|
+
| 실행 | 방법 | 근거 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Boot | Spring Environment 의 시스템 환경 변수 매핑: `Globals.mysql.Url` → `GLOBALS_MYSQL_URL`, `spring.datasource.url` → `SPRING_DATASOURCE_URL` | 공식 simple-backend 의 데이터소스는 `env.getProperty("Globals.%s.Url")` 로 읽음 |
|
|
60
|
+
| Classic | `globals.properties` 사본(DB 호스트·비밀번호만 바꿈)을 compose 는 읽기 전용 볼륨, Kubernetes 는 Secret 의 `subPath` 로 `WEB-INF/classes/…` 위에 마운트 | Classic 템플릿은 `EgovProperties` 가 파일을 직접 읽어 환경 변수로 덮을 수 없음 |
|
|
61
|
+
|
|
62
|
+
- 비밀번호 기본값은 `CHANGE_ME` 입니다. compose 는 `.env` 의 `DB_PASSWORD`, Kubernetes 는 Secret 으로 넣습니다. 보안약점 점검의 자리표시자 예외와 맞물려 지적되지 않습니다.
|
|
63
|
+
- Kubernetes 의 DB 호스트는 `CHANGE_ME_DB_HOST` 입니다. 운영 DB 는 클러스터 밖(기관 DB)이 일반적이라 DB 를 클러스터에 띄우지 않습니다.
|
|
64
|
+
|
|
65
|
+
## 5. docker-compose
|
|
66
|
+
|
|
67
|
+
- **DB 컨테이너**: mysql 8.4·mariadb 11.4·postgres 16 을 같은 레지스트리 변수로 받습니다.
|
|
68
|
+
- **초기화**: 템플릿의 DDL·DATA 를 `docker-entrypoint-initdb.d/01-…`·`02-…` 로 마운트합니다.
|
|
69
|
+
- **MySQL·MariaDB 옵션**: `--lower-case-table-names=1`(표준프레임워크 SQL 은 대문자 테이블명)과 utf8mb4 를 줍니다.
|
|
70
|
+
- **기동 순서**: DB 헬스체크가 통과한 뒤 앱을 띄웁니다(`depends_on: condition: service_healthy`).
|
|
71
|
+
- **외부 DB**: 오라클·티베로·알티베이스·큐브리드 등은 DB 컨테이너 없이 앱만 띄우고 연결 정보를 그대로 씁니다.
|
|
72
|
+
|
|
73
|
+
## 6. Kubernetes
|
|
74
|
+
|
|
75
|
+
- **리소스**: ConfigMap(TZ·JAVA_OPTS·비밀이 아닌 DB 설정), Secret(비밀번호 또는 Classic 설정 파일), Deployment, Service(ClusterIP 80 → http), 선택 Ingress.
|
|
76
|
+
- **프로브**: startup(실패 30회까지 기다림)·readiness·liveness 가 같은 헬스 경로를 봅니다.
|
|
77
|
+
- **보안 설정**: `runAsNonRoot`·`runAsUser 10001`·`fsGroup`·`seccompProfile RuntimeDefault`·`allowPrivilegeEscalation false`·`capabilities drop ALL`.
|
|
78
|
+
- **리소스 요청·제한**: 250m/768Mi, 1/1536Mi.
|
|
79
|
+
- **배치**: 웹이 아닌 Boot 앱은 포트·프로브·Service 없이 만들고, 커맨드라인 배치는 Job·CronJob 으로 바꾸라고 안내합니다.
|
|
80
|
+
|
|
81
|
+
## 7. 검증
|
|
82
|
+
|
|
83
|
+
- **게이트** (`test:deploy`)
|
|
84
|
+
- 분석: Classic·Boot(maria·context-path·actuator)·Gradle 배치·spring.datasource(PostgreSQL)·Oracle(Tomcat 11)·HSQL·지원 안 함 3종
|
|
85
|
+
- 생성 파일 구조와 생성 `docker-compose.yml` 의 공식 Compose 스키마(compose-spec, draft 2020-12 고정물) 검증
|
|
86
|
+
- Kubernetes 보안 설정·프로브·Secret 마운트
|
|
87
|
+
- 입력 검증, 새 파일만(일부라도 있으면 아무것도 쓰지 않음)
|
|
88
|
+
- **CI 통합** (`test:deploy-live`)
|
|
89
|
+
- 공식 web(Classic)·boot-web(Boot) 템플릿: hadolint 2.14 경고 0, kubeconform 0.7(쿠버네티스 1.31) 통과
|
|
90
|
+
- 실제 `docker build`(이미지 안에서 Maven 빌드) → 기동 → HTTP 200 → HEALTHCHECK healthy → 실행 uid 10001
|
|
91
|
+
- 공식 simple-backend: `docker compose up --build` → actuator readiness UP → `/mainPage` 가 MySQL 초기화 데이터를 돌려줌(환경 변수로 DB 연결)
|
|
92
|
+
- 컨테이너 안에서 Maven 저장소에 닿지 않는 환경은 `DEPLOY_LIVE_PREBUILT=1` 로 미리 빌드 경로를 검증합니다.
|
|
93
|
+
|
|
94
|
+
## 8. 범위 밖
|
|
95
|
+
|
|
96
|
+
Helm 차트·Kustomize 오버레이, 레지스트리 push·클러스터 배포 실행, 운영 DB 이관, 멀티 모듈.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "egovframe-scaffold-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"mcpName": "io.github.EricSeokgon/egovframe-scaffold-mcp",
|
|
5
5
|
"description": "전자정부 표준프레임워크(eGovFrame) 프로젝트·CRUD 스캐폴딩과 검증된 공통컴포넌트 완전 조립 MCP 서버",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"generate:dependency-baseline": "node scripts/generate-dependency-baseline.mjs",
|
|
23
23
|
"release:check": "node scripts/release-check.mjs",
|
|
24
24
|
"release:notes": "node scripts/release-notes.mjs",
|
|
25
|
-
"prepublishOnly": "npm run build && npm run check:portability && npm run test:output-schemas && npm run test:catalog && npm run test:catalog-sync && npm run test:template-catalog && npm run test:catalog-drift && npm run test:config && npm run test:migration-rules && npm run test:migrate && npm run test:dependencies && npm run test:sbom && npm run test:sbom-check && npm run test:spdx && npm run test:secure-coding && npm run test:network && npm run test:agents-md && npm run test:registry && npm run test:release && npm run test:ai && npm run test:recipes && npm run test:diagnose && npm run test:docs && npm run test:report && npm run test:assessment && npm run test:reassemble && npm run test:rehearse && npm run test:offline-bundle && npm run test:upgrade && npm run test:remove && npm run test:transaction && npm run test:allowed-roots && npm run test:explain && npm run test:ci && npm run test:cli && npm run test:build && npm run test:test && npm run test:pom && npm run test:crud && npm run test:handshake",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run check:portability && npm run test:output-schemas && npm run test:catalog && npm run test:catalog-sync && npm run test:template-catalog && npm run test:catalog-drift && npm run test:config && npm run test:migration-rules && npm run test:migrate && npm run test:dependencies && npm run test:sbom && npm run test:sbom-check && npm run test:spdx && npm run test:secure-coding && npm run test:deploy && npm run test:network && npm run test:agents-md && npm run test:registry && npm run test:release && npm run test:ai && npm run test:recipes && npm run test:diagnose && npm run test:docs && npm run test:report && npm run test:assessment && npm run test:reassemble && npm run test:rehearse && npm run test:offline-bundle && npm run test:upgrade && npm run test:remove && npm run test:transaction && npm run test:allowed-roots && npm run test:explain && npm run test:ci && npm run test:cli && npm run test:build && npm run test:test && npm run test:pom && npm run test:crud && npm run test:handshake",
|
|
26
26
|
"test:ai": "node test/ai.mjs",
|
|
27
27
|
"generate:ai-catalog": "node scripts/generate-ai-catalog.mjs",
|
|
28
28
|
"test:ai-assembly": "node test/ai-assembly.mjs",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"test:spdx": "node test/spdx.mjs",
|
|
40
40
|
"test:secure-coding": "node test/secure-coding.mjs",
|
|
41
41
|
"test:secure-live": "node test/secure-live.mjs",
|
|
42
|
+
"test:deploy": "node test/deploy.mjs",
|
|
43
|
+
"test:deploy-live": "node test/deploy-live.mjs",
|
|
42
44
|
"test:reassemble-live": "node test/reassemble-live.mjs",
|
|
43
45
|
"test:rehearse": "node test/rehearse.mjs",
|
|
44
46
|
"test:rehearse-live": "node test/rehearse-live.mjs",
|