refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소
외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.
삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard
이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
ca.java-conventions / ca.quality-conventions / ca.java-library /
ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로
완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로
task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리
CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정
순증감 +3,274 / -7,483.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d00c76241c
commit
ef947e5bb0
@@ -0,0 +1,73 @@
|
||||
# CI/CD 경계 — GitHub Actions는 CI, Argo CD는 CD
|
||||
|
||||
## 결론
|
||||
|
||||
GitHub Actions는 **검증하고 아티팩트를 만든다**. Argo CD는 **배포한다**. 두 역할은 겹치지 않는다.
|
||||
|
||||
GitHub Actions 워크플로는 `kubectl apply`, `helm upgrade`, `argocd app sync` 중 어느 것도 하지
|
||||
않는다. 그러므로 CI에는 클러스터 자격증명(kubeconfig, 서비스 계정 토큰)이 들어가지 않는다.
|
||||
|
||||
## 흐름
|
||||
|
||||
```text
|
||||
git push / tag
|
||||
│
|
||||
▼
|
||||
GitHub Actions ─────────────── CI ───────────────┐
|
||||
• 테스트 · 정적분석 · 아키텍처 검증 │
|
||||
• 컨테이너 이미지 빌드 │
|
||||
• 취약점 스캔 (Trivy) │
|
||||
• SBOM 생성 │
|
||||
• 레지스트리에 이미지 push │
|
||||
│ │
|
||||
│ 이미지 태그(다이제스트)를 manifest에 기록 │
|
||||
▼ │
|
||||
GitOps 저장소 (배포 희망 상태) ──────────────────┘
|
||||
│
|
||||
│ Argo CD가 watch
|
||||
▼
|
||||
Argo CD ──────────────────── CD ───────────────
|
||||
│ auto-sync
|
||||
▼
|
||||
Kubernetes
|
||||
```
|
||||
|
||||
용어 한 줄 풀이:
|
||||
|
||||
- **GitOps 저장소** — 클러스터에 무엇이 떠 있어야 하는지를 적어 둔 Git 저장소. 애플리케이션 소스와
|
||||
분리한다.
|
||||
- **manifest** — Kubernetes에 넣을 YAML(Deployment, Service 등).
|
||||
- **auto-sync** — Argo CD가 GitOps 저장소의 변경을 스스로 감지해 클러스터에 반영하는 모드. 이걸 쓰면
|
||||
CI가 Argo CD API 서버에 접근할 필요가 없다.
|
||||
|
||||
## 왜 이렇게 나누나
|
||||
|
||||
1. **자격증명 반경.** CI가 배포하면 CI 러너가 프로덕션 클러스터에 대한 쓰기 권한을 갖는다. 포크된
|
||||
PR, 서드파티 액션, 캐시 오염이 모두 그 권한에 닿는다. auto-sync를 쓰면 그 권한은 클러스터 안의
|
||||
Argo CD에만 있고, CI는 Git에 커밋만 한다.
|
||||
2. **현재 상태의 소유자가 하나.** 클러스터에 무엇이 떠 있는지는 GitOps 저장소가 답한다. CI가 직접
|
||||
apply 하면 답이 두 개가 된다 — Git에 적힌 것과 실제로 떠 있는 것.
|
||||
3. **롤백이 revert.** 배포를 되돌리는 것이 `git revert`가 된다.
|
||||
|
||||
## 이 저장소의 현재 위치
|
||||
|
||||
| 항목 | 상태 |
|
||||
| --- | --- |
|
||||
| 이미지 빌드/스캔/push | `release.yml`이 수행 |
|
||||
| SBOM | `release.yml`이 생성 |
|
||||
| 이미지 서명 · provenance attestation | **없음.** 추가 대상 |
|
||||
| GitOps 저장소 | **없음.** 별도 저장소로 만들 예정 |
|
||||
| Argo CD Application 정의 | **없음.** GitOps 저장소에 둘 예정 |
|
||||
| CI에서의 클러스터 접근 | 없음 — 유일했던 `kubectl apply`는 제거됨 |
|
||||
|
||||
`fileserver-certification.yml`은 예외처럼 보이지만 아니다. PVC 매니페스트가 여전히 ReadWriteOnce를
|
||||
선언하는지 **파일만** 확인하고, 클러스터에는 아무것도 적용하지 않는다. 실제 클러스터에서의 인증은
|
||||
운영자가 `infra/fileserver/kubernetes/pvc-certification-job.yaml`을 직접 실행하고
|
||||
`docs/fileserver/storage-certification.md`에 기록한다. 이름을 `fileserver-release.yml`에서 바꾼 이유가
|
||||
이것이다 — 이 워크플로는 릴리스하지 않는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
- 워크플로에 클러스터 자격증명 secret을 추가하지 않는다.
|
||||
- 배포 대상이 바뀌면 GitOps 저장소의 manifest를 바꾼다. 워크플로를 바꾸지 않는다.
|
||||
- CI가 만드는 것은 **불변 다이제스트로 지정된 이미지**다. `latest` 태그로 배포하지 않는다.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Template maintainer와 Template consumer의 검증은 다르다
|
||||
|
||||
## 결론
|
||||
|
||||
이 저장소에는 성격이 다른 두 종류의 검증이 섞여 있다.
|
||||
|
||||
1. **스켈레톤을 만드는 사람**에게 필요한 검증 — sample 모듈이 정말 제거 가능한가, optional 모듈
|
||||
조합이 모두 빌드되는가, 레지스트리가 확장 가능한가.
|
||||
2. **스켈레톤을 가져다 서비스를 만드는 사람**에게 필요한 검증 — 내 애플리케이션의 테스트,
|
||||
아키텍처 방향, 보안, 릴리스.
|
||||
|
||||
파생 프로젝트가 1번을 그대로 물려받으면, 자기 서비스와 아무 상관 없는 게이트를 평생 유지하게 된다.
|
||||
이 문서는 어느 쪽이 어느 쪽인지 적어 둔다.
|
||||
|
||||
## Template 전용 (파생 프로젝트는 삭제해도 된다)
|
||||
|
||||
| 대상 | 무엇을 지키는가 |
|
||||
| --- | --- |
|
||||
| `:app-bootstrap:sampleOffTest`, `ci-quality-gates.yml`의 `sample-off` job | sample 픽스처를 지워도 애플리케이션이 빌드·부팅되는가 |
|
||||
| `sample-portfolio` leaf 전체 | 참조 구현 |
|
||||
| `Dockerfile.sample`, `docker-compose.*` 중 sample 관련 | 위와 동일 |
|
||||
| `docs/superpowers/**` | 이 템플릿을 만든 과정의 설계/계획 기록 |
|
||||
| `gradle/qualification/**` | 이 템플릿이 벤더링한 플랫폼(JPA, messaging)의 인증 체계 |
|
||||
| `*-certification.yml`, `*-qualification.yml`, `jpa-next-*.yml` | 템플릿이 광고하는 지원 매트릭스의 근거 |
|
||||
|
||||
## Consumer 필수 (파생 프로젝트가 유지해야 한다)
|
||||
|
||||
| 대상 | 무엇을 지키는가 |
|
||||
| --- | --- |
|
||||
| `architectureCheck` | Clean Architecture 의존 방향. 이 템플릿의 존재 이유 |
|
||||
| 각 leaf의 `check` | 컴파일 · 단위 테스트 · 포맷 · 스타일 · Error Prone |
|
||||
| `qualityCheck` | SpotBugs / FindSecBugs |
|
||||
| `configContractCheck` | 환경변수 계약 |
|
||||
| `verifyDependencyLocks` | 재현 가능한 의존성 해석 |
|
||||
| `dependency-vulnerability.yml` | dependency-review + Trivy |
|
||||
| `ci-quality-gates.yml` | PR 게이트 |
|
||||
| `release.yml` | 이미지 · SBOM 생산 |
|
||||
| action의 full SHA 핀 | 공급망 |
|
||||
|
||||
## 파생 프로젝트가 할 일
|
||||
|
||||
1. Template 전용 표의 항목을 삭제한다. 삭제는 대부분 파일 삭제 + `config/architecture/modules.json`
|
||||
에서 leaf 항목 제거로 끝난다 — 레지스트리가 leaf 목록의 SSOT이고, 개수를 따로 적어 둔 곳은 없다.
|
||||
2. `docs/ci-cd/boundary.md`의 경계를 그대로 유지한 채 자기 GitOps 저장소를 연결한다.
|
||||
3. `.trivyignore.yaml`과 CODEOWNERS는 그대로 쓴다.
|
||||
|
||||
## 아직 하지 않은 것
|
||||
|
||||
Template CI와 Generated Application CI를 **물리적으로** 분리하지는 않았다(생성기 없음). 지금은 이
|
||||
문서가 그 경계다. 생성기를 만든다면, 위 표의 "Template 전용" 열이 생성기가 벗겨 내야 할 목록이다.
|
||||
@@ -12,7 +12,7 @@
|
||||
#
|
||||
# Only APP_HTTPCLIENT_ENABLED is registered in docs/registries/env-keys.yaml and shipped in
|
||||
# src/.env: it is the only key with a deployment-independent value, and it is the only one the
|
||||
# three-way verifyEnvKeys gate can express. Everything below is per deployment and is set directly
|
||||
# three-way :app-bootstrap:verifyEnvKeys gate can express. Everything below is per deployment and is set directly
|
||||
# in the environment — templating an indexed client in application.yml would materialise a nameless
|
||||
# client in every deployment, which the settings' aggregate validation refuses.
|
||||
#
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
```bash
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
./gradlew verifyRuntimeModuleMembership --console=plain
|
||||
./gradlew verifyOneTypePerFile --console=plain
|
||||
./gradlew checkstyleMain --console=plain
|
||||
```
|
||||
|
||||
destination profile은 startup에서 검증된다. 아래는 **부팅 실패**다.
|
||||
|
||||
@@ -1897,7 +1897,7 @@ env_keys:
|
||||
# Bound only by RedisSdkAutoConfiguration, which exists only while APP_REDIS_ENABLED
|
||||
# is true. They are deliberately absent from application.yml and src/.env: putting
|
||||
# them there would make a Redis-free deployment carry Redis configuration, which is
|
||||
# the defect the conditional composition root removes. verifyEnvKeys checks them
|
||||
# the defect the conditional composition root removes. :app-bootstrap:verifyEnvKeys checks them
|
||||
# against spring-configuration-metadata.json instead.
|
||||
|
||||
- name: APP_REDIS_ACKNOWLEDGED_WRITE_LOSS_ACCEPTED
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Messaging R2 자격(qualification) — 미구현
|
||||
|
||||
추적: MSG-015
|
||||
|
||||
## 상태
|
||||
|
||||
**구현되지 않았다.** R2 자격을 주장할 수 있는 근거가 없다.
|
||||
|
||||
- qualification producer 없음
|
||||
- 대응하는 Test 태스크 없음
|
||||
- 공통 스키마 validator 없음
|
||||
|
||||
따라서 `config/messaging/readiness-cards.yaml`의 카드는 `verifyMessagingContracts`와
|
||||
`verifyMessagingJsonSchemaV1` 두 개를 제외하면 모두 `maturity: not-implemented`다.
|
||||
|
||||
## 왜 Gradle 태스크를 미리 만들어 두지 않는가
|
||||
|
||||
2026-09 이전에는 루트 빌드가 아래 아홉 개 태스크 이름을 미리 등록해 두고, 그 본문이 **입력과 무관하게
|
||||
무조건 예외를 던졌다**.
|
||||
|
||||
```text
|
||||
verifyMessagingPollingOutboxR2 verifyMessagingTargetBinding
|
||||
verifyMessagingKafkaProducerR2 verifyMessagingDeploymentCutover
|
||||
verifyMessagingSecurityR2 verifyMessagingCleanupTargetBinding
|
||||
verifyMessagingReleaseProfile verifyMessagingFinalR2Profile
|
||||
verifyMessagingTargetBindingPreflight
|
||||
```
|
||||
|
||||
의도는 "fail-closed"였지만 결과는 다음과 같았다.
|
||||
|
||||
- `./gradlew tasks`에 게이트처럼 보이는 이름 아홉 개가 나타난다.
|
||||
- `dependsOn`으로 걸 수 있다. 거는 순간 그 레인은 영원히 빨간불이다.
|
||||
- 정상적인 입력으로도 성공할 수 없으므로 "검증"이 아니다.
|
||||
|
||||
즉 TODO를 Gradle 태스크 API로 표현한 것이었다. 미구현 사실을 기록하는 자리는 이 문서이고, 태스크는
|
||||
**실제로 통과할 수 있게 된 시점에** 그 producer와 함께 추가한다.
|
||||
|
||||
## 구현 시 추가할 것
|
||||
|
||||
1. 각 시나리오를 실제로 실행하는 Test 태스크.
|
||||
2. 그 실행 결과(JUnit XML)에서 payload-free manifest를 만드는 producer.
|
||||
3. `config/messaging/evidence/build-evidence-manifest-v1.schema.json`으로 그 manifest 바이트를
|
||||
검증하는 finalizer.
|
||||
4. 위 셋이 모두 생긴 다음에 `verifyMessaging<Scenario>R2` 태스크 등록.
|
||||
|
||||
`gradle/qualification/messaging-qualification.gradle`의 `verifyMessagingJsonSchemaV1`이 그 네 단계를
|
||||
모두 갖춘 예시다.
|
||||
@@ -0,0 +1,167 @@
|
||||
# 검증 표면 축소 설계 — 스켈레톤을 qualification framework에서 되돌리기
|
||||
|
||||
날짜: 2026-09-16
|
||||
근거: 외부 리뷰 "현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를 다시 검증하는 구조까지 생겼다"
|
||||
|
||||
## 0. 리뷰 기준점과 현재 체크아웃의 차이
|
||||
|
||||
리뷰는 이 저장소의 **이전 스냅샷**을 보고 작성됐다. 실제 작업 전에 항목별로 재측정했고,
|
||||
이미 해결된 항목은 "완료"로 확정하고 남은 항목만 작업 대상으로 삼는다.
|
||||
|
||||
| 리뷰 주장 | 리뷰가 본 값 | 현재 실측 | 판정 |
|
||||
| --- | --- | --- | --- |
|
||||
| `settings.gradle` 183줄 validator | 183줄 | 16줄 (`ca.architecture-registry` 설정 플러그인으로 이전) | 완료 |
|
||||
| 모듈 수 정확히 18개 강제 | 있음 | 없음 | 완료 |
|
||||
| runtime composition이 정확히 `app-bootstrap` | 있음 | `runtime_compositions`를 JSON에서 읽음 | 완료 |
|
||||
| JSON 필드 집합 정확히 일치 | 있음 | `ModuleRegistry.groovy:88,124`에 그대로 있음 | **작업 대상** |
|
||||
| `sample-portfolio` negative re-entry guard | 있음 | `ModuleRegistry.groovy:215`에 그대로 있음 | **작업 대상** |
|
||||
| `build-logic` 없음 | 없음 | 존재 (9개 convention plugin) | 부분 완료 |
|
||||
| version catalog 없음 | 없음 | `gradle/libs.versions.toml` 140줄 | 완료 |
|
||||
| `adapter/inbound/web/build.gradle` 799줄 OpenAPI | 799줄 | 256줄, codegen 없음 | 완료 |
|
||||
| leaf `check`가 저장소 전체 검사 | 그랬음 | 루트 `check`로 이미 이전 | 부분 완료 |
|
||||
| `fileserver-release.yml`의 `kubectl apply` | 있음 | 이미 제거됨 | 완료 |
|
||||
| `httpclient-release.yml` | 있음 | 파일 자체가 없음 | 해당 없음 |
|
||||
| `ci-gate-matrix.yml` 282줄 / 37 gate | 282줄 | **1,025줄 / 107 gate** | **작업 대상(악화)** |
|
||||
| `verify-gate-matrix.sh` | 있음 | 568줄 | **작업 대상** |
|
||||
| `verify-gradle-wrapper.sh` 740줄 | 740줄 | **799줄** | **작업 대상** |
|
||||
| `DeveloperExperienceContractTest` 1,100줄 | 1,100줄 | 1,141줄 (CI YAML mutation test 25개) | **작업 대상** |
|
||||
| `src/build.gradle` 2,469줄 | 2,469줄 | **3,211줄** | **작업 대상(악화)** |
|
||||
| always-fail Messaging task | 있음 | 9개 그대로 | **작업 대상** |
|
||||
| 모든 빌드에 Git SHA 강제 | 있음 | 그대로 (`build.gradle:47`) | **작업 대상** |
|
||||
|
||||
## 1. 채택하는 판단 기준
|
||||
|
||||
리뷰의 핵심 원칙을 이 저장소의 결정 규칙으로 승격한다.
|
||||
|
||||
1. **현재 상태(Current State)가 아니라 불변조건(Invariant)을 검증한다.**
|
||||
"모듈이 N개다", "필드가 정확히 이 집합이다", "문서에 적힌 수가 레지스트리와 같다"는 현재 상태다.
|
||||
"ID가 중복되지 않는다", "domain이 framework를 참조하지 않는다"는 불변조건이다.
|
||||
2. **검증기를 검증하지 않는다.** validator를 mutation해서 validator가 실패하는지 보는 task는
|
||||
스켈레톤의 기본 빌드 정책이 아니다.
|
||||
3. **자동으로 구성할 수 있는 것은 검증으로 강제하지 않는다.** convention plugin으로 주입한다.
|
||||
4. **로컬 `check`는 로컬이어야 한다.** leaf의 `check`는 그 leaf만 검사한다.
|
||||
5. **릴리스 불변조건을 일반 개발 빌드에 강제하지 않는다.**
|
||||
6. **문서 drift는 빌드 실패 사유가 아니다.** 커스텀 Markdown/Java 파서를 유지하지 않는다.
|
||||
7. **GitHub Actions = CI + artifact 생산, Argo CD = CD.** CI에 클러스터 배포 자격증명을 넣지 않는다.
|
||||
8. **Template maintainer용 검증과 Template consumer용 검증을 분리한다.**
|
||||
|
||||
이 기준은 기존의 D8 결정("quality 블록을 convention plugin으로 빼지 않는다")을 **대체한다**.
|
||||
D8의 3번 근거(build-logic이 플러그인 버전을 두 번 선언하게 된다)는 이미 무효다 —
|
||||
`build-logic/settings.gradle`이 메인 빌드의 `libs.versions.toml`을 읽고 있으므로 버전은 한 곳에 있다.
|
||||
|
||||
## 2. 목표 task 계층
|
||||
|
||||
```text
|
||||
:<leaf>:check 컴파일 + 단위 테스트 + spotless + checkstyle + errorprone (그 leaf만)
|
||||
check (root) 모든 leaf의 check
|
||||
architectureCheck 의존 방향 · 런타임 멤버십 · application-core 순수성 · Git 미추적 패키지
|
||||
qualityCheck SpotBugs + FindSecBugs (전 leaf)
|
||||
configContractCheck :app-bootstrap:verifyEnvKeys
|
||||
integrationCheck 통합/슬라이스 레인
|
||||
ci check + architectureCheck + qualityCheck + configContractCheck
|
||||
releaseCheck ci + 아카이브 위생 + public path snapshot + 릴리스 provenance
|
||||
```
|
||||
|
||||
qualification(JPA readiness, Messaging evidence, notification evidence, transport 등)은
|
||||
어느 것도 `check` / `ci`에 걸지 않는다. 명시적으로 이름을 불러야 실행된다.
|
||||
|
||||
## 3. 변경 목록
|
||||
|
||||
### 3.1 삭제
|
||||
|
||||
| 대상 | 줄 수 | 이유 |
|
||||
| --- | ---: | --- |
|
||||
| `.github/ci-gate-matrix.yml` | 1,025 | Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제 |
|
||||
| `.github/scripts/verify-gate-matrix.sh` | 568 | 위 복제본의 정합성 검사기 |
|
||||
| `.github/scripts/verify-gradle-wrapper.sh` | 799 | workflow 바이트 해시 잠금. 공격자는 해시도 같이 고치면 되고, 개발자는 주석 하나에 해시를 갱신해야 한다 |
|
||||
| `DeveloperExperienceContractTest`의 wrapper/gate mutation test 25개 | ~700 | 애플리케이션 test suite가 GitHub Actions YAML 파서를 검증 |
|
||||
| always-fail Messaging skeleton task 9개 | ~85 | 정상 입력으로도 성공할 수 없는 task. TODO를 Gradle API로 만든 것 |
|
||||
| `verifyReadmeCommands` | 105 | 커스텀 Markdown 명령 파서 |
|
||||
| `verifyRunbookReferences` | 75 | 커스텀 runbook 식별자 파서 |
|
||||
| `verifyDocumentedLeafCount` | 78 | 문서에 적힌 leaf 수 = 전형적인 현재 상태 검증 |
|
||||
| `verifyTestSourceSetRegistry` | 92 | 문서 표 ↔ source set 대조 파서 |
|
||||
| `verifySpotBugsAnalysisFailureContract` | 58 | 검증기의 검증 |
|
||||
| `verifyConfigurationPropertiesProcessor` | 90 | build.gradle을 regex로 읽는 검증 → convention으로 대체 |
|
||||
| `verifyOneTypePerFile` | 8 | 이미 `checkstyleMain` 별칭. 호출자를 `checkstyleMain`으로 바꾸고 이름 폐기 |
|
||||
| `verifyTrivyignore` | 105 | 빈 registry를 지키는 커스텀 YAML 파서 |
|
||||
| `verifyQuarantineSunset` | 250 | 빈 registry를 지키는 커스텀 YAML + Java 파서 |
|
||||
| `blankJavaCommentsAndLiterals` | 95 | 위 두 개만 쓰던 Java 렉서 흉내 |
|
||||
| `ModuleRegistry`의 필드 집합 정확 일치 · sample-portfolio negative guard | ~25 | 확장 차단 · 삭제된 모듈의 역사가 영구 invariant |
|
||||
|
||||
합계 약 4,150줄.
|
||||
|
||||
### 3.2 이동
|
||||
|
||||
| 대상 | 현 위치 | 새 위치 | 이유 |
|
||||
| --- | --- | --- | --- |
|
||||
| java/quality/spring 공통 설정 | `build.gradle`의 `configure(subprojects…)` | `ca.java-conventions` · `ca.quality-conventions` · `ca.java-library` · `ca.spring-library` | 모듈이 자신의 성격을 스스로 선언 |
|
||||
| `verifyCleanArchitectureDependencies` 외 3개 | `build.gradle` | `ca.architecture` | 아키텍처 규칙을 한 곳에 |
|
||||
| JPA readiness registry + release gate | `build.gradle` ~610줄 | `gradle/qualification/jpa-qualification.gradle` | 빌드 정책과 certification 분리 |
|
||||
| Messaging evidence manifest | `build.gradle` ~600줄 | `gradle/qualification/messaging-qualification.gradle` | 동일 |
|
||||
| `verifyEnvKeys` | 루트 task, 루트 `check` | `:app-bootstrap` 소유, `configContractCheck` | 환경 계약은 composition root의 책임 |
|
||||
| 모듈 의존 edge 존재/자기참조 검사 | settings 단계(`ModuleRegistry`) | `verifyCleanArchitectureDependencies` | settings에서 죽으면 복구 수단이 없다 |
|
||||
|
||||
### 3.3 완화
|
||||
|
||||
| 대상 | 현재 | 변경 후 |
|
||||
| --- | --- | --- |
|
||||
| Git revision | 없으면 **모든** 빌드가 configuration 단계에서 실패 | 일반 빌드는 `0.0.1-SNAPSHOT`/`unknown`. `releaseCheck`·아카이브 생성에서만 요구 |
|
||||
| SpotBugs / FindSecBugs | 전 leaf `check` 블로킹 | `qualityCheck` (CI lane). 로컬 `check`에서 제외 |
|
||||
| `.trivyignore.yaml` | 커스텀 파서가 expiry/reason 강제 | 파일은 유지, 규칙은 문서화 + CODEOWNERS 승인 |
|
||||
| `flaky-quarantine.yaml` | 커스텀 파서 + 14일 sunset 강제 | 레지스트리 삭제. `@Tag("quarantine")` 제외와 `quarantineTest`는 유지(각 3줄) |
|
||||
|
||||
### 3.4 CI
|
||||
|
||||
```text
|
||||
.github/workflows/
|
||||
├── _reusable-gradle.yml 신규 — checkout + wrapper validation + JDK/캐시 + Gradle 호출
|
||||
├── ci-quality-gates.yml → 재사용 workflow 호출로 축약
|
||||
├── dependency-vulnerability.yml 유지 (dependency-review + submission + Trivy)
|
||||
├── link-check.yml 유지
|
||||
├── release.yml image/SBOM 생산까지. 클러스터 배포 없음
|
||||
├── fileserver-certification.yml ← fileserver-release.yml 개명 (CD가 아니라 certification)
|
||||
└── 나머지 feature qualification 유지, 전부 재사용 workflow 사용
|
||||
```
|
||||
|
||||
wrapper 검증은 `gradle/actions/wrapper-validation`(full SHA 핀)에 맡기고, 재사용 workflow
|
||||
한 곳에서만 선언한다. full SHA 핀은 리뷰 판단대로 **유지**한다.
|
||||
|
||||
### 3.5 CI/CD 경계
|
||||
|
||||
```text
|
||||
GitHub Actions ──► test / scan / image build / SBOM / push ──► GitOps repo manifest ──► Argo CD ──► K8s
|
||||
```
|
||||
|
||||
`docs/ci-cd/boundary.md`로 고정한다. GitHub Actions는 `kubectl apply` / `helm upgrade` /
|
||||
`argocd app sync`를 하지 않는다. Argo CD auto-sync를 쓰면 CI에 클러스터 자격증명이 필요 없다.
|
||||
|
||||
### 3.6 Template maintainer vs consumer
|
||||
|
||||
`docs/ci-cd/template-vs-consumer.md`로 구분을 명시한다.
|
||||
|
||||
- Template CI: sample 모듈 제거 가능성, optional 모듈 조합 빌드, 레지스트리 확장 가능성
|
||||
- Consumer CI: 자기 애플리케이션의 test / architecture / security / release
|
||||
|
||||
파생 프로젝트가 가져가면 안 되는 workflow와 task를 목록으로 적는다.
|
||||
|
||||
## 4. 유지하는 것 (리뷰가 "잘한 것"으로 분류)
|
||||
|
||||
`verifyCleanArchitectureDependencies`, dependency locking(STRICT), full SHA action 핀,
|
||||
dependency-review, Trivy 스캔, path filter 기반 feature CI, nightly 분리, Spotless,
|
||||
`-Werror`/`-Xlint`, ErrorProne, 재현 가능한 아카이브.
|
||||
|
||||
## 5. 검증 방법
|
||||
|
||||
- `./gradlew help --offline`로 configuration 성공
|
||||
- 변경한 leaf마다 `./gradlew <path>:check --offline`
|
||||
- `./gradlew architectureCheck --offline`
|
||||
- 워크플로 YAML은 `python3 -c "import yaml…"`로 파싱 확인
|
||||
- 삭제한 task 이름이 저장소 어디에도 남지 않았는지 `grep`
|
||||
|
||||
## 6. 명시적 위험
|
||||
|
||||
1. leaf 62개에 `plugins {}` 블록을 추가한다. 적용 순서가 바뀌므로 leaf별 `check`로 확인한다.
|
||||
2. dependency locking이 STRICT라, 어떤 leaf의 configuration에 의존성이 추가되면 락 파일이 깨진다.
|
||||
따라서 convention 이동은 **해석되는 의존성 집합을 바꾸지 않는 범위**로 제한한다.
|
||||
`ca.spring-config`는 이미 processor를 선언한 leaf만 opt-in한다.
|
||||
3. 삭제하는 task 이름을 참조하는 workflow/문서/테스트를 같은 변경에서 고친다.
|
||||
@@ -1,7 +1,7 @@
|
||||
# 테스트 전략 — 레벨 정의와 소스셋 매핑 (SSOT)
|
||||
|
||||
- 기준 일자: 2026-09-07
|
||||
- 상태: **활성 계약.** `verifyTestSourceSetRegistry` 가 이 문서의 §3 표와 실제 Gradle 소스셋 선언의
|
||||
- 상태: **활성 문서.** 아래 §3 표는 사람이 유지한다. `verifyTestSourceSetRegistry` 가 이 문서의 §3 표와 실제 Gradle 소스셋 선언의
|
||||
불일치를 빌드 실패로 만든다.
|
||||
- 근거 리뷰: `docs/reviews/2026-09-07-app-bootstrap-module-code-review.md` (BOOT-014, BOOT-015,
|
||||
BOOT-016)
|
||||
@@ -60,7 +60,10 @@ smoke 이고 regression 일 수 있다.
|
||||
|
||||
## 3. 소스셋 레지스트리 (기계 검증 대상)
|
||||
|
||||
`verifyTestSourceSetRegistry` 가 이 표를 읽어 실제 `sourceSets` 선언과 대조한다. 표에 없는 소스셋을
|
||||
이 표를 읽어 실제 `sourceSets` 선언과 대조하던 `verifyTestSourceSetRegistry` 는 2026-09에 삭제했다
|
||||
(Markdown 표 파서였고, `<!-- registry:begin -->` 마커가 사라지면 계약이 산문으로 되돌아가는 것을
|
||||
막으려고 마커 존재 자체까지 검사했다). 레인을 추가하면 이 표도 같이 고친다. 아래 옛 설명은 표를
|
||||
어떻게 읽어야 하는지에 대한 기준으로 남긴다: 표에 없는 소스셋을
|
||||
추가하거나 표에 있는 소스셋을 지우면 빌드가 실패한다.
|
||||
|
||||
<!-- registry:begin -->
|
||||
|
||||
Reference in New Issue
Block a user