Files
llm-wiki/raw/branch-notes/feature-ci-quality-gates-contract.md
T

423 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-ci-quality-gates-contract
source_type: branch-note
status: raw
branch: feature-ci-quality-gates-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/devops-ci-supply-chain-dx]
tags: [branch, ca-skeleton, ci, quality-gate, contract-test]
created: 2026-05-22
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-028
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-028
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: e13ec9fd666d546ce8cb089f48f43da5ed3d77a72d0c6edc691bad11e65956e8
---
# branch: feature-ci-quality-gates-contract
> Layer: `raw/branch-notes/` — skeleton 계약이 문서에만 남지 않도록 CI에서 강제할 quality gate 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: architecture·contract·OpenAPI blocking gate가 분리 실행된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1` | verification suite가 OpenAPI drift의 release-blocking 판정권을 소유한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1` | test framework는 JUnit 5다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
운영 계약은 깨지기 쉽습니다. response envelope, log schema, env fail-fast, OpenAPI drift, repository capability, security/log leakage 같은 항목은 CI에서 실패 조건으로 고정해야 합니다.
본 branch 의 책임은 **gate wiring(어떤 gate 가 CI 에서 어떻게 실행/차단되는가)** 이다. 개별 scanner/tool/severity *정책 결정* 은 전용 owner branch 가 소유하고 본 branch 는 그 결과를 release-blocking gate 로 *배선* 한다 (§구현 가이드 §6, §엣지·실패·의존 의존 목록).
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- format/lint/test/contract test gate.
- OpenAPI drift check.
- dependency vulnerability scan gate.
- optional adapter test matrix.
- warning-only와 release-blocking gate 구분.
### 제외 범위
- 실제 CI provider workflow 구현 세부.
- 배포 승인 프로세스.
- load/performance test.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/ci-github-actions-vs-gitlab-comparison]] | GitHub Actions `needs:` + `if: success( |
| [[raw/official-docs/ci-openapi-snapshot-diff-tooling]] | springdoc + openapi-diff (Tufin/oasdiff |
| [[raw/company-tech-blogs/ci-flaky-test-quarantine-spotify-google]] | Spotify/Google/MS quarantine 인정 vs Fowler 반대 |
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-E: CI Quality Gates)
본 branch의 Gate ownership matrix 20행 + flaky quarantine 14d + OpenAPI snapshot diff 결정에 대한 외부 source.
- **채택 결정 (GitHub Actions + matrix gate + flaky 14d sunset)**:
- [[raw/official-docs/ci-github-actions-vs-gitlab-comparison]] — GitHub Actions `needs:` + `if: success()`가 ca-tmpl contract gate 모델과 정확히 맞물림
- [[raw/official-docs/ci-openapi-snapshot-diff-tooling]] — springdoc + openapi-diff (Tufin/oasdiff)
- **검토한 대안**:
- **대안 1: GitLab CI vs GitHub Actions** — 동일 source에서 비교. **선택 조건**: ca-tmpl repo 가 GitHub 호스팅 → GitHub Actions 채택; GitLab 호스팅으로 이전 시 `needs` ↔ `stages` 매핑(`CIGG-C2`)으로 이식 가능 (provider 선정 자체는 별도 ADR — `CIGG-C2` 는 매핑 *가능성* 만 보장)
- **대안 2: Jenkins / Tekton (k8s-native)** — k8s 인프라/plugin 의존도로 skeleton 단계에 과함
- **대안 3: CircleCI / Buildkite / Drone CI** — vendor 다양성, ca-tmpl scope 외
- **사례 (flaky quarantine)**: [[raw/company-tech-blogs/ci-flaky-test-quarantine-spotify-google]] — Spotify/Google/MS quarantine 인정 vs Fowler 반대. ca-tmpl 14일 sunset은 절충안
- **비교 핵심**: GitHub Actions의 `needs:`/`if:` gate 의존성 모델이 ca-tmpl 11 release-blocking gate에 정확히 맞물림. Tekton/Jenkins는 k8s 인프라/plugin 부담으로 skeleton에 과함. Flaky quarantine은 Spotify/Google/MS 인정 vs Fowler 반대 양립 — ca-tmpl 14일 sunset이 절충.
## TODO
> TODO drained 2026-05-22 — required CI gate 목록 / release-blocking vs warning-only 기준 / contract test 차단 / optional adapter matrix / OpenAPI drift / vulnerability 차단 정책 모두 "결정 사항"과 "Gate ↔ Branch Contract Test 소유권 매트릭스"에 반영됨. 잔존 TODO 없음.
## 진행 중 메모
- 2026-06-15 (`/branch-spec`): pre-template 노트를 현 템플릿 구조로 보강 — 누락 섹션(구현 가이드 / 엣지·실패·의존 / 진행 중 메모 / 관련 일일 노트) 추가, `parent_branch` + `governing_docs` frontmatter 추가, §Coverage seed, §Audit & Findings(ground-truth drift) 추가. 기존 결정·매트릭스·테스트 계약 본문은 verbatim 보존. ca-tmpl ground truth 대조 결과 모든 gate 는 여전히 `documented-only`(`.github/workflows/` 부재 확인) — actually-implemented 주장 없음.
- 2026-06-20 (구현): gate wiring 을 `actually-implemented`(로컬 `locally-verified`)로 승급. 산출물 — `.github/workflows/ci-quality-gates.yml`(9 잡: quality-gates/security-snapshot-gates/openapi-drift/sample-removal-smoke/optional-adapter-matrix/gate-matrix-lint/breaking-change-approval/quarantine/**release-gate** fan-in), `.github/ci-gate-matrix.yml`(20행 in-repo SSOT), `.github/scripts/verify-gate-matrix.sh`(C7 cross-check), `flaky-quarantine.yaml`(repo-루트 레지스트리, 빈 버킷), `src/build.gradle`(`test` excludeTags 'quarantine' + `quarantineTest` 버킷 + `verifyQuarantineSunset` 14일 sunset, check 연결), `.github/CODEOWNERS`/`.github/pull_request_template.md`(D8 escape-hatch 거버넌스), `src/README.md` 문서.
- **핵심 구현 결정 (UNSUPPORTED_IMPL_DECISION 해소):**
- **§4 quarantine 메커니즘 = `@Tag("quarantine")`(JUnit 기본, 전 모듈 즉시 사용) + repo-루트 `flaky-quarantine.yaml` 레지스트리** — note 의 `@QuarantinedSince` custom annotation 후보 대신 채택. 이유: custom annotation 의 cross-module 사용은 test-fixtures/신규 모듈 plumbing 필요(과함)이고, `shared-contract`(stdlib-only)에 JUnit 타입을 둘 수 없음. 레지스트리 방식은 기존 4개 거버넌스 게이트(verifyTrivyignore/verifyEnvKeys/verifyOneTypePerFile/verifyPublicPathSnapshot)와 동형이며 gitignored-docs 제약(아래)도 회피.
- **release-gate fan-in = `if: always()` + `needs.*.result` 스캔** — `if: success()` 단독은 상위 실패 시 aggregator 가 skipped(차단 아님). Claim C1 의 실증적 해소.
- **gitignored 설정 제약 발견:** `.gitignore` 가 `/docs` 전체 제외(0 tracked) → CI-read 신규 파일은 `docs/` 금지, tracked 경로(repo 루트/`.github/`)에 배치(`.trivyignore.yaml` 선례). `check` 의 registry 의존은 워크플로 "RUNTIME-CONFIG PREREQUISITE" 로 문서화(범위 밖 — env-driven 소유).
- 검증(로컬): verifyQuarantineSunset 3종 control(empty→OK / over-age 170d→fail / drift unregistered→fail), `:shared-contract:quarantineTest` BUILD SUCCESSFUL(빈 버킷), gate-matrix-lint PASS(20=16 verified+4 delegated), openapiCheckSnapshot/SampleRemovalSmoke/TestTaxonomyArchitectureTest/verifyCleanArchitectureDependencies 통과, 워크플로 YAML PyYAML 파싱 OK. **CI 실제 실행은 `needs-confirmation`.**
- 파생 노트: [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]], [[raw/blog-topics/ci-gate-wiring-vs-policy-ownership-2026-06-20]], [[raw/interviews/ci-release-gate-fan-in-blocking-2026-06-20]].
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --- | --- | --- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## 결정 사항
- 2026-05-22: contract violation은 warning-only로 두지 않음.
- 2026-05-22: optional adapter test는 adapter enabled matrix에서만 실행.
- 2026-05-22: OpenAPI drift, registry drift, security/privacy leakage, architecture boundary, sample removal smoke는 release-blocking.
- 2026-05-22: warning-only는 dependency freshness advisory처럼 release artifact correctness를 직접 깨지 않는 항목에만 허용.
- 2026-05-22: vulnerability scanner = Trivy (image + dependency). suppression은 `trivy-ignore` 파일 + PR review approval 필수.
- 2026-05-22: OpenAPI drift ground truth = code-generated snapshot (springdoc 등). hand-maintained는 forbidden.
- 2026-05-22: flaky test quarantine bucket 허용. quarantine된 test는 별도 gradle task로 분리, sunset deadline 14일.
- 2026-05-22: contract test snapshot 의도적 갱신 = breaking change catalog row 인용 + PR label `intent:breaking-change-approved`로 escape hatch.
- 2026-05-22: 본 branch가 **flaky test quarantine SSOT** (sunset 14일). test-taxonomy-fixture-contract는 consumer (flaky 발생 시 quarantine bucket 참조).
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. `Decision ID` 는 이 branch-note 안에서 안정적으로 유지.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | contract violation은 warning-only로 두지 않음 (release-blocking) | UNSUPPORTED_DECISION (외부 source 직접 증명 없음 — 조직 policy 결정) | `team-policy` | release-blocking 강도 자체의 외부 표준 부재 |
| D2 | optional adapter test는 adapter enabled matrix에서만 실행 | `raw/official-docs/ci-github-actions-vs-gitlab-comparison.md#CIGG-C3` (GitHub Actions `needs` key 로 job 의존성 명시) | `official-vendor-doc` (matrix job 표현은 vendor docs 에서 직접 지원) | `strategy.matrix` 의 정확한 표현은 본 인용 범위 밖 — 별도 GitHub Actions matrix 문서 raw 등록 권고 |
| D3 | OpenAPI drift, registry drift, security/privacy leakage, architecture boundary, sample removal smoke 는 release-blocking | `raw/official-docs/ci-openapi-snapshot-diff-tooling.md#CIOS-C4`, `raw/official-docs/ci-openapi-snapshot-diff-tooling.md#CIOS-C5`, `raw/official-docs/ci-github-actions-vs-gitlab-comparison.md#CIGG-C2` | `official-vendor-doc` | breaking change 판정 규칙 차이 (openapi-diff vs oasdiff) 별도 검증 필요 |
| D4 | warning-only는 dependency freshness advisory 처럼 release artifact correctness 를 직접 깨지 않는 항목에만 허용 | UNSUPPORTED_DECISION (외부 source 직접 증명 없음 — 조직 분류 정책) | `team-policy` | freshness advisory 와 vulnerability advisory 의 경계 정의 필요 |
| D5 | vulnerability scanner = Trivy (image + dependency), suppression 은 `trivy-ignore` + PR review approval 필수 | UNSUPPORTED_DECISION (Trivy 공식 docs raw source 없음) — **+ OWNER_AMBIGUITY**: scanner *tool 선택* 은 본 branch(gate wiring) 범위 밖 후보. [[raw/branch-notes/feature-dependency-vulnerability-management-contract]](현재 scanner 미결) 또는 severity 정책 owner [[raw/branch-notes/feature-build-release-supply-chain-contract]] 로 위임 권고 (§Audit) | `team-convention` | Trivy 공식 페이지 raw source 보강 필요 + tool 선택 owner 미확정 |
| D6 | OpenAPI drift ground truth = code-generated snapshot (springdoc 등), hand-maintained 는 forbidden | `raw/official-docs/ci-openapi-snapshot-diff-tooling.md#CIOS-C1`, `raw/official-docs/ci-openapi-snapshot-diff-tooling.md#CIOS-C2`, `raw/official-docs/ci-openapi-snapshot-diff-tooling.md#CIOS-C3` | `official-vendor-doc` (springdoc runtime introspection 의 공식 동작) | springdoc 은 dynamic routing (WebFlux functional routes) 일부 누락 위험 — `CIOS-C1` 의 inferred semantics 한계 |
| D7 | flaky test quarantine bucket 허용, 별도 gradle task 로 분리, sunset deadline 14일 | UNSUPPORTED_DECISION (`raw/company-tech-blogs/ci-flaky-test-quarantine-spotify-google.md` 는 company-case-study — 공식 best practice 로 단정 불가) | `company-case-study` (Spotify/Google/MS 인정 vs Fowler 반대 양립) | 14일 sunset 정량값은 ca-tmpl 절충안 — 외부 표준 부재 |
| D8 | contract test snapshot 의도적 갱신 = breaking change catalog row 인용 + PR label `intent:breaking-change-approved` escape hatch | UNSUPPORTED_DECISION (조직 governance 결정 — 외부 source 직접 증명 없음) | `team-policy` | label 권한 정책 (`feature-contract-verification-test-suite` D-Verify Claim 과 cross-link) |
| D9 | 본 branch 가 flaky test quarantine SSOT (sunset 14일), test-taxonomy-fixture-contract 는 consumer | UNSUPPORTED_DECISION (cross-branch ownership 결정) | `team-policy` | 본 branch ↔ test-taxonomy branch 간 ownership 경계 명문화 |
> Note: `ci-flaky-test-quarantine-spotify-google` 는 `company-tech-blog` 카테고리이므로 본 branch 의 quarantine 정책은 `company-case-study` 강도만 가지며 official best practice 로 표현 금지.
## 구현 가이드
> *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세. 본 branch 의 핵심 산출물 카탈로그(gate 목록 + owner 매핑)는 `## Gate ↔ Branch Contract Test 소유권 매트릭스`(SSOT, 20행). 본 §는 그 매트릭스가 담지 못하는 **wiring 메커니즘**(needs/if 위상, OpenAPI gate step, flaky 강제, escape hatch, 위임 경계)을 결정·근거 reference 와 함께 명세한다.
### 1. Gate 위계 — release-blocking vs warning-only 분류 규칙
> **Trace**: D1 (contract violation = release-blocking) + D3 (release-blocking 목록) + D4 (warning-only 한정). Supporting: `CIGG-C2` (stages↔needs gate 의존성 모델), team-policy.
>
> - **UNSUPPORTED_IMPL_DECISION**: "release-blocking" 강도 자체(D1/D4)는 조직 policy — 외부 표준 부재. trade-off: 엄격 차단(merge 속도 ↓, 계약 안전 ↑) vs warning-only 완화(반대). freshness advisory 와 vulnerability advisory 의 경계(D4 Open Risk)도 조직 분류.
- 분류 규칙: **release artifact correctness 를 직접 깨는 gate = release-blocking**(D3 목록 + 매트릭스 release-blocking 열), **freshness advisory 류만 warning-only**(D4).
- 전체 gate 목록·owner·release-blocking 여부 = `## Gate ↔ Branch Contract Test 소유권 매트릭스`(SSOT). 본 sub-section 은 *판정 규칙*만, 카탈로그는 매트릭스가 소유(중복 금지).
### 2. GitHub Actions gate 위상 (needs
> **Trace**: D2 (optional adapter = enabled matrix only) + D3. Supporting: `CIGG-C3` (job 의존성 = `needs` key), `CIGG-C2` (GitLab `stages` ↔ GitHub `needs` 매핑).
>
> - **UNSUPPORTED_IMPL_DECISION**: ① `strategy.matrix` 의 정확한 yaml shape(adapter-enabled 조합 표현) — `CIGG-C3` 는 `needs` key 만 보장, matrix 표현은 인용 범위 밖. trade-off: 별도 GitHub Actions matrix vendor 문서 raw 등록 필요(D2 Open Risk). ② fan-in 시 status 전파(`if: success()` vs `if: always()`)의 정확한 규칙 — `CIGG-C3` 미보장(§엣지 Claim 1 로 검증 위임).
- 각 contract-test job 은 release job 의 `needs:` 의존성으로 선언, `if: success()` 로 release gate.
- optional adapter test(D2)는 `strategy.matrix` 의 adapter-enabled 조합에서만 실행 → 매트릭스 행 "integration test (optional adapter matrix) | true if matrix enabled".
### 3. OpenAPI drift gate
> **Trace**: D6 (ground truth = code-generated snapshot, hand-maintained forbidden) + D3 (release-blocking). Supporting: `CIOS-C1`/`C2`/`C3` (springdoc runtime introspection), `CIOS-C4` (openapi-diff 3.x 비교), `CIOS-C5`/`C6` (oasdiff breaking 서브명령).
>
> - **UNSUPPORTED_IMPL_DECISION**: ① gradle task 명 `openapiCheckSnapshot` — note 자체 명명, 인용 외. trade-off: 명명 임의(되묻기 방지용 고정). ② exit-code 기반 차단 — `CIOS-C5` 가 breaking 시 non-zero exit 을 직접 보장하지 않음(§엣지 Claim 2 검증 위임).
- baseline `openapi-snapshot.yaml`(checked-in) vs build 시 springdoc-generated OpenAPI 를 `oasdiff breaking`(또는 openapi-diff)으로 비교, breaking 1건+ 이면 release-block.
- 알려진 한계: springdoc 은 WebFlux functional route 등 dynamic routing 일부 누락 가능(`CIOS-C1`).
### 4. Flaky test quarantine bucket (본 branch SSOT, 14d sunset)
> **Trace**: D7 (quarantine bucket + 별도 gradle task + 14일 sunset) + D9 (본 branch = SSOT, test-taxonomy = consumer). Supporting: `company-case-study`(Spotify/Google/MS) + team-policy.
>
> - **UNSUPPORTED_IMPL_DECISION**: `@QuarantinedSince` annotation 명 + CI step 의 14일 초과 build-fail 자동 강제 메커니즘 — 외부 source 없음(company-case-study 는 quarantine *인정* 만, 14d 정량·강제 메커니즘 무). trade-off: 14d 는 ca-tmpl 절충값; 자동 강제 미구현 시 수동 리뷰로 대체(§엣지 Claim 5).
- quarantine bucket = 별도 gradle task 로 분리(메인 gate 에서 격리). [[raw/branch-notes/feature-test-taxonomy-fixture-contract]] 는 flaky 발생 시 본 bucket 을 참조하는 consumer.
### 5. Snapshot 의도적 갱신 escape hatch
> **Trace**: D8 (breaking change catalog row 인용 + PR label `intent:breaking-change-approved`). Supporting: team-policy (governance).
>
> - **UNSUPPORTED_IMPL_DECISION**: label 부여 권한 정책(누가 label 을 달 수 있나) — 외부 source 없음. trade-off: branch protection + CODEOWNERS 로 label 권한 제한 필요(§엣지 Claim 6); 미설정 시 누구나 우회.
- contract test snapshot 의 의도적 갱신은 breaking change catalog row 를 인용하고 PR 에 `intent:breaking-change-approved` label 부여로만 통과.
### 6. 위임된 tool
> **Trace**: D5 (vulnerability scan). 본 branch 는 **gate wiring owner** — 아래 gate 의 *실행/release-blocking 배선* 은 in-scope 이나, *tool 선택·severity·정책 결정* 은 전용 owner branch 로 위임. 매트릭스의 owner 열이 위임 대상을 가리킨다(단 슬러그 drift 는 §Audit `OWNER_BRANCH_DRIFT` 참조).
>
> - **UNSUPPORTED_IMPL_DECISION**: D5 의 Trivy *tool 선택* 은 본 branch 결정 범위 밖 후보 — 전용 vuln branch 미결. trade-off: 본 branch 는 vuln gate 의 release-blocking 배선만 소유, scanner 선택은 위임/확정 필요(§Audit OWNER_AMBIGUITY).
| Gate (wiring in-scope here) | tool/policy 결정 owner (위임) |
|---|---|
| vulnerability scan (Trivy) | tool 선택 = [[raw/branch-notes/feature-dependency-vulnerability-management-contract]](미결) / severity 정책 = [[raw/branch-notes/feature-build-release-supply-chain-contract]] |
| secret scan (gitleaks) | [[raw/branch-notes/feature-secrets-config-source-contract]] |
| SBOM / Cosign / SLSA / license | [[raw/branch-notes/feature-build-release-supply-chain-contract]] |
| format / lint (tool + ruleset) | [[raw/branch-notes/feature-static-analysis-quality-contract]] (매트릭스 "(toolchain)" 셀의 실제 owner — parent §2051) |
| container image scan | [[raw/branch-notes/feature-container-runtime-contract]] |
| .env.example drift | [[raw/branch-notes/feature-env-driven-runtime-configuration]] |
## Gate ↔ Branch Contract Test 소유권 매트릭스
모든 gate는 단일 owner branch contract test를 실행. release-blocking 여부 명시.
| CI gate | release-blocking | owning branch contract test |
|---------|------------------|------------------------------|
| format / lint | true | (toolchain) |
| unit test | true | test-taxonomy-fixture-contract |
| architecture test (ArchUnit) | true | architecture-enforcement-rules |
| envelope/error contract test | true | contract-verification-test-suite (envelope) |
| log/MDC contract test | true | contract-verification-test-suite (log) |
| env contract test | true | contract-verification-test-suite (env) |
| registry contract test | true | contract-verification-test-suite (registry) |
| OpenAPI drift | true | contract-verification-test-suite (OpenAPI) |
| schema drift (JSON serialization) | true | contract-verification-test-suite (schema) |
| integration test (default profile) | true | test-taxonomy-fixture-contract |
| integration test (optional adapter matrix) | true if matrix enabled | integration-adapter-templates |
| sample removal smoke | true | sample-removal-adoption-contract |
| SBOM generation | true | build-release-supply-chain |
| signed artifact (Cosign) verification | true | build-release-supply-chain |
| SLSA provenance attestation | true | build-release-supply-chain |
| vulnerability scan (Trivy) high/critical | true | build-release-supply-chain |
| license scan (NOTICE compliance) | true | build-release-supply-chain |
| secret scan (gitleaks) | true | secrets-config-source |
| .env.example drift | true | env-driven-runtime-configuration |
| container image scan (Trivy image) | true | container-runtime-contract |
> ⚠️ owner 열 슬러그 drift — `build-release-supply-chain` → `feature-build-release-supply-chain-contract`, `secrets-config-source` → `feature-secrets-config-source-contract`, `(toolchain)`(format/lint) → `feature-static-analysis-quality-contract`. 상세·근거는 §Audit & Findings `OWNER_BRANCH_DRIFT`. (사용자 결정 영역이므로 자동 rewrite 하지 않고 정합 권고만 — §구현 가이드 §6 및 §엣지·실패·의존 의존 목록은 정합된 슬러그 사용.)
## Gate Matrix (deprecated)
> CI Gate 전체 목록과 owner branch 매핑은 위 "Gate ↔ Branch Contract Test 소유권 매트릭스"가 SSOT. 별도 Gate Matrix 양식은 deprecated.
## 테스트 계약
- contract test result gate: GitHub Actions matrix에서 `contract-test` job의 status가 `failure`이면 workflow status도 `failure`여야 함. 측정 방법: workflow yaml의 `needs: [contract-test]` 의존성 + `if: success()` gate 명시 verify. 누락 시 fail.
- OpenAPI drift gate: `openapi-diff` 또는 동등 도구를 `openapi-snapshot.yaml` (checked-in baseline) vs build 시 generated OpenAPI과 비교. diff 결과에 breaking change가 1건이라도 있으면 release-block. 측정 방법: CI step `./gradlew openapiCheckSnapshot` exit code 0 verify.
- high/critical vulnerability 차단 정책이 없으면 실패.
- sample removal smoke gate: workflow yaml에 `sample-removal-smoke` job이 정의되고 release-blocking matrix에 포함되어 있어야 함. 측정 방법: workflow yaml grep on `sample-removal-smoke` + matrix.profile에 `sample-off` 포함 verify.
## 엣지·실패·의존
> R4 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. 각 실패 경로는 §Claims To Verify 항목과 1:1 대응(검증 위임).
### 실패·엣지 경로
- **needs/if fan-in status 전파**: 실패한 matrix job 이 release job 으로 failure 를 전파하는가 — `CIGG-C3` 가 `if: always()` 등 정확한 fan-in 규칙 미보장. 기대: gate 1건 실패 → release block (§Claims C1).
- **oasdiff exit code semantic**: breaking change 발생 시 `oasdiff breaking` 이 non-zero exit 인가 — `CIOS-C5` 미보장. 기대: breaking 1건 → exit != 0 → CI fail (§Claims C2).
- **Trivy false negative / suppression bypass**: CVE DB 갱신 지연, 또는 무단 `trivy-ignore` 추가로 우회. 기대: known CVE → fail, 무단 suppression PR review 없이 차단 (§Claims C3).
- **flaky 14d sunset 자동 강제 부재**: `@QuarantinedSince` 류 annotation 없으면 14일 초과를 감지할 수 없음. 기대: 14일 초과 → build fail (§Claims C5).
- **label escape-hatch 무단 사용**: label 부여 권한 정책 부재 시 누구나 `intent:breaking-change-approved` 로 우회. 기대: branch protection + CODEOWNERS 로 권한 제한 (§Claims C6).
- **gate matrix ↔ 실제 contract test 불일치**: 20행 표의 owning branch 가 실제 contract test 와 어긋남(슬러그 drift 포함, §Audit). 기대: lint script 로 표 ↔ 코드 cross-check (§Claims C7).
### 다른 계약 의존 (delegated owner = consume 대상)
> 본 branch 는 아래 owner branch 의 contract test 를 release-blocking gate 로 consume 한다. 해당 계약이 바뀌면 본 branch 의 gate 실패 조건이 변동된다 (R4 IMPLICIT_DEPENDENCY 명시).
- [[raw/branch-notes/feature-contract-verification-test-suite]] — envelope/log/env/registry/OpenAPI/schema contract test 를 gate 로 consume.
- [[raw/branch-notes/feature-test-taxonomy-fixture-contract]] — unit/integration test gate + flaky quarantine **consumer**(D9: 본 branch 가 quarantine SSOT).
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — ArchUnit architecture test gate.
- [[raw/branch-notes/feature-build-release-supply-chain-contract]] — SBOM/Cosign/SLSA/license/vulnerability-severity gate (severity 정책 owner).
- [[raw/branch-notes/feature-secrets-config-source-contract]] — secret scan (gitleaks) gate.
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] — .env.example drift gate.
- [[raw/branch-notes/feature-container-runtime-contract]] — container image scan gate.
- [[raw/branch-notes/feature-integration-adapter-templates]] — optional adapter test matrix.
- [[raw/branch-notes/feature-sample-removal-adoption-contract]] — sample removal smoke gate.
- [[raw/branch-notes/feature-static-analysis-quality-contract]] — format/lint tool + ruleset (매트릭스 "(toolchain)" 셀의 실제 owner; 본 branch 는 threshold/gate wiring 만).
- [[raw/branch-notes/feature-dependency-vulnerability-management-contract]] — vulnerability scanner *tool 선택*(D5 위임 후보, 현재 미결).
## 검증해야 할 주장
> 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| GitHub Actions `needs:` + `if: success()` 조합이 ca-tmpl 11 release-blocking gate 모두를 강제 | `CIGG-C2`/`C3` 는 매핑 가능성만 보장, `if:` 의 fan-in 시 status 전파 규칙은 인용 범위 밖 | 의도적 fail job 을 matrix 에 추가 → 후속 release job 이 실제로 block 되는지 verify | `partially-implemented` — release-gate 를 `if: always()` + `needs.*.result` 스캔으로 구현(`success()` 단독은 skipped→차단 실패임을 확인). **CI 실제 fail 전파 = `needs-confirmation`** |
| `openapi-diff` 또는 `oasdiff` 의 exit code 가 breaking change 발생 시 non-zero | `CIOS-C5` 는 breaking 검출 기능만 보장, exit code semantic 명시 없음 | 의도적 breaking change PR 생성 → `oasdiff breaking` exit code != 0 verify | `locally-verified` — ca-tmpl 은 oasdiff 대신 `openapiCheckSnapshot`(Gradle Test, 스냅샷 byte-compare) 채택; 로컬 exit 0 확인. drift 시 fail 은 OpenApiDriftContractTest 가 보장 |
| Trivy high/critical 차단 정책이 false negative 없이 동작 | Trivy CVE DB 갱신 주기 / suppression 우회 가능성 | 의도적 CVE-known dependency (예: log4j 2.14) 추가 → CI fail verify; `trivy-ignore` 무단 추가가 차단되는지 verify | `delegated` — `dependency-vulnerability.yml`(feature-dependency-vulnerability-management-contract) 소유. 본 branch 는 gate-matrix 에서 release-blocking 으로 배선만 |
| sample-removal-smoke job 이 release-blocking matrix 에 실제 포함됨 | workflow yaml 의 matrix 구성 검증 부재 | workflow yaml grep on `sample-removal-smoke` + `matrix.profile` 에 `sample-off` 포함 verify | `implemented` — `sample-removal-smoke` 잡 + `strategy.matrix.profile: [sample-off]` 존재, release-gate `needs` 포함. SampleRemovalSmokeContractTest 로컬 통과 |
| flaky quarantine bucket 의 14일 sunset 이 자동 강제 | sunset deadline 의 자동 감지 메커니즘 부재 가능 | quarantine bucket 의 test 별 `@QuarantinedSince` annotation + CI step 으로 14일 초과 시 build fail verify | `implemented` (`locally-verified`) — `@Tag("quarantine")` + repo-루트 `flaky-quarantine.yaml` + `verifyQuarantineSunset`(check 연결). over-age 170일 positive control fail 확인. (annotation 대신 레지스트리 채택 — §진행 중 메모) |
| `intent:breaking-change-approved` label escape hatch 가 무단 사용 차단 | label 추가 권한 정책 부재 시 누구나 우회 | branch protection + CODEOWNERS 로 label 권한 제한 + audit log 점검 | `partially-implemented` — `breaking-change-approval` 잡(governed snapshot 변경 시 label 강제) + CODEOWNERS(snapshot/approved 경로). **branch protection "Require Code Owners" 활성화는 운영 설정(미적용) = `needs-confirmation`** |
| 20개 gate 표의 owning branch 매핑이 실제 contract test 와 일치 | 표만 있고 cross-check 부재 + 슬러그 drift(§Audit) | 각 owning branch 의 contract test 코드 grep + 본 표와 일치 verify (수동 또는 lint script) | `implemented` (`locally-verified`) — `.github/ci-gate-matrix.yml`(20행) + `verify-gate-matrix.sh`. 로컬 PASS(16 verified + 4 delegated-pending). 슬러그는 §Audit 정합본 사용 |
## 관심사 커버리지 (coverage-auditor 자동 생성 — seed)
> `/coverage` 가 채우는 **생성물**. 아래는 `/branch-spec`(2026-06-15)이 governing doc [[wiki/projects/ca-tmpl/devops-ci-supply-chain-dx]] + parent §538 "CI Quality Gates" 관심사로 seed 한 것 — coverage-auditor 가 검증/정정한다. 상태: `covered-here` / `delegated` / `missing`.
| 관심사 (governing §538 CI Quality Gates) | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| format/lint/test/contract/OpenAPI drift/security scan 이 분리된 gate 로 실행 | covered-here | — | — | 매트릭스 20행 + D2/D3 |
| merge 전 실패 가능 gate vs warning-only gate 구분 | covered-here | — | — | D1, D3, D4 / §구현 §1 |
| contract violation = release-blocking (warning-only 불가) | covered-here | — | — | D1 |
| optional adapter test = adapter enabled matrix only | covered-here | — | — | D2 / §구현 §2 |
| OpenAPI drift ground truth = code-generated snapshot | covered-here | — | — | D6 / §구현 §3 |
| flaky test quarantine + sunset 정책 | covered-here | — | — | D7, D9 / §구현 §4 |
| vulnerability scan *tool 선택* | delegated | [[raw/branch-notes/feature-dependency-vulnerability-management-contract]] (미결) | Should-fix | OWNER_AMBIGUITY: 위임 링크 존재, 단 owner 의 scanner 미결 (§Audit) |
| vulnerability severity → release-block 정책 | delegated | [[raw/branch-notes/feature-build-release-supply-chain-contract]] | OK | §구현 §6 |
| secret scan (gitleaks) tool | delegated | [[raw/branch-notes/feature-secrets-config-source-contract]] | OK | §구현 §6 / §엣지 의존 |
| SBOM / Cosign / SLSA / license | delegated | [[raw/branch-notes/feature-build-release-supply-chain-contract]] | OK | §구현 §6 |
| format/lint *tool + ruleset* | delegated | [[raw/branch-notes/feature-static-analysis-quality-contract]] | OK | parent §2051 / §구현 §6 |
| container image scan | delegated | [[raw/branch-notes/feature-container-runtime-contract]] | OK | §구현 §6 |
## Audit & Findings (2026-06-15 — `/branch-spec` ground-truth 대조)
> ca-tmpl `docs/registries/*.yaml` + `src/` + sibling branch-notes 대조 결과. **사용자 작성 결정 영역(매트릭스 owner 열, §완료 후 wiki 추출 대상)은 자동 rewrite 하지 않고 정합 권고만** (CLAUDE.md §15.5 R3, `/branch-spec` §2 drift 규칙). 신규 작성 섹션(§구현 가이드 §6, §엣지·실패·의존, §Coverage)은 정합된 슬러그 사용.
- **OWNER_BRANCH_DRIFT** (Gate matrix owner 열):
- `build-release-supply-chain` → 실제 branch-note 슬러그 `feature-build-release-supply-chain-contract` (존재 확인). 권고: 매트릭스 5개 행(SBOM/Cosign/SLSA/vuln/license) owner 정합.
- `secrets-config-source` → 실제 `feature-secrets-config-source-contract` (`docs/registries/secrets-classification.yaml` `owner_branch` SSOT 와 일치). 권고: secret scan 행 정합.
- `(toolchain)` (format/lint 행) → 실제 owner `feature-static-analysis-quality-contract` (parent §2051: "tool 선택 + 룰셋" owner; 본 branch 는 *threshold/gate wiring* 만). 권고: owner 명시.
- **OWNER_AMBIGUITY** (D5 — vulnerability scanner tool 선택): scanner *tool 선택* 의 owner 미확정. 전용 `feature-dependency-vulnerability-management-contract` 는 현재 scanner 미결, `feature-build-release-supply-chain-contract` 는 severity 정책만 소유. 권고: 본 branch 는 vuln gate 의 release-blocking 배선만 유지하고, Trivy *tool 선택* 결정은 dependency-vulnerability 또는 supply-chain owner 로 위임/확정.
- **EXTRACTION_TARGET_DRIFT** (§완료 후 wiki 추출 대상): 지정 경로 `wiki/projects/ca-skeleton-operational-contract.md` 는 wiki 파일로 존재하지 않음(그 슬러그는 `raw/project-notes/`). 실제 CI canonical = [[wiki/projects/ca-tmpl/devops-ci-supply-chain-dx]] (§CI `documented-only`, line 41~47). 권고: 추출 대상 정합.
- **NO_CI_WORKFLOW** (ground truth, non-blocking): ca-tmpl 에 `.github/workflows/` 부재 → 본 branch 의 모든 gate 는 `documented-only`/`planned` 단계. 노트 self-report(§Cluster, parent §CI documented-only)와 일치 — `actually-implemented` 과장 없음. drift 아님, 현황 기록.
## 완료 후 wiki 추출 대상
- `wiki/projects/ca-skeleton-operational-contract.md`의 CI quality gate canonical section. (⚠️ 경로 drift — 실제 canonical 은 [[wiki/projects/ca-tmpl/devops-ci-supply-chain-dx]], §Audit `EXTRACTION_TARGET_DRIFT` 참조.)
## 마주친 문제
- 2026-06-20: 구현 중 회피한 함정 3종 — (1) `if: success()` fan-in aggregator 는 상위 실패 시 *skipped*(차단 아님) → `always()`+result 스캔으로 전환; (2) `/docs` 전체 gitignore → CI-read 신규 파일을 tracked 경로로(레지스트리 = repo 루트, gate-matrix = `.github/`); (3) Gradle 빈 tag 버킷 Test 실패 → `failOnNoDiscoveredTests=false`. 상세: [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]].
- 2026-06-20 (CI 실관측 + 사용자 결정): 사용자가 워크플로를 실제 CI 러너에서 돌려 `verifyEnvKeys: missing docs/registries/env-keys.yaml` → `BUILD FAILED` 확인. 핵심 트레이드오프 부상 — registry 게이트를 "부재 시 skip" 으로 완화하면 CI green 이지만 **CI 에서 vacuous**(계약 미강제) → 본 branch 목표("계약을 CI 에서 강제")와 정면 충돌. docs 읽는 contract 테스트 18/21 이 이미 skip-tolerant, verifyEnvKeys 만 throw outlier 임을 확인. 사용자에게 옵션 제시 → **Option 1: registries 커밋** 채택. `.gitignore` 를 `/docs/*` + `!/docs/registries/` 로 좁혀 운영 레지스트리 7개만 추적(나머지 `/docs` 는 private 유지). `verifyEnvKeys: OK — 99 env keys / 74 required placeholders / 84 APP_ keys` 재확인. 게이트가 fresh checkout 에서 실제 강제됨 = `locally-verified`(CI 재실행 `needs-confirmation`).
- 2026-06-20 (CI 3차 — **quarantine 메커니즘 첫 실사용**): full `check` 에서 `PrivacySettingsTest.blankSalt_warnsAndFallsBackToDevSentinel(CapturedOutput)` 1건만 실패(487 tests, 1 failed) → release-gate 가 다시 정확히 차단(`quality-gates: failure` → `::error::release-gate`), **Claim C1 재실증**. 원인(증거): `CapturedOutput` 이 JVM-전역 async logback appender(`logback-spring.xml:23` `ASYNC_ENABLED` defaultValue=**true** → `:126` `MetricsAsyncAppender` on root)와 race — sibling `@SpringBootTest`(ActuatorSecurityHttpTest 등 4종)가 그 async appender 를 설치하면, 경량 `ApplicationContextRunner` 테스트의 `log.warn` 이 worker 스레드에서 `output.getOut()` 읽은 *뒤* flush → 단언 실패. 순서/타이밍 의존(로컬 단독·full 모두 통과 = 이기는 순서, CI = 지는 순서). 처리: 사용자 결정 **격리(quarantine)** — flaky 한 `blankSalt` 메서드에만 `@Tag("quarantine")`(realSalt 는 경고 미발생이라 async 무관, 제외) + `flaky-quarantine.yaml` 등록(reason + tracking_issue(TODO, 머지 전 실 Gitea 이슈로 교체) + `quarantined_since: 2026-06-20`, sunset 2026-07-04). 검증: `verifyQuarantineSunset: OK — 1 registered, 1 tagged`, drift guard simple-name suffix 매칭(`build.gradle:670`) 정합, `:app-bootstrap:test` 전체 BUILD SUCCESSFUL(flaky 제외), `quarantineTest` 가 1건 비차단 실행(`tests=1 failures=0`), `check verifyPublicPathSnapshot` BUILD SUCCESSFUL. **잠복 위험**: 같은 모듈 `LoggingSettingsTest`(badTimezone/badAsyncQueueSize `warnsAndFallsBack`)도 동일 CapturedOutput+async race 패턴 — 이번엔 미발생이나 다른 순서에서 재현 가능. 근본수정(sunset 내 owner 몫): `logback-test.xml` 로 test 시 async 비활성, 또는 `ListAppender` 직접 단언으로 stdout race 제거 — 한 번에 이 클래스 전체 flake 해소.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/ci-flaky-test-quarantine-spotify-google]]
- [[raw/official-docs/ci-github-actions-vs-gitlab-comparison]]
- [[raw/official-docs/ci-openapi-snapshot-diff-tooling]]
- [[raw/official-docs/dx-mise-asdf-tool-versioning]]
- [[raw/official-docs/dx-testcontainers-java-best-practices]]
- [[raw/official-docs/supply-chain-cosign-keyless-sigstore]]
- [[raw/official-docs/supply-chain-slsa-provenance-framework]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: interviews:start -->
- [[raw/interviews/ci-release-gate-fan-in-blocking-2026-06-20]]
<!-- GENERATED: interviews:end -->
<!-- GENERATED: errors:start -->
- [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]]
<!-- GENERATED: errors:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/ci-gate-wiring-vs-policy-ownership-2026-06-20]]
<!-- GENERATED: blog-topics:end -->
> 2026-06-20 구현 단계에서 errors / blog-topics / interview-prep 파생 자료 누적.
### 오류 기록 (본 feature 작업 중 발생)
- [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]] — fan-in skip 함정 + gitignored 설정 CI 의존 + 빈 tag 버킷.
### Blog topics (이 작업에서 나온 글감)
- [[raw/blog-topics/ci-gate-wiring-vs-policy-ownership-2026-06-20]] — gate wiring vs policy 소유권 분리 + quarantine sunset 강제.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- [[raw/interviews/ci-release-gate-fan-in-blocking-2026-06-20]] — fan-in 으로 release 차단을 *보장* 하는 법.
## 관련 일일 노트
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- 2026-06-20 — gate wiring 구현(워크플로 + gate-matrix + 크로스체크 + flaky quarantine sunset + escape-hatch 거버넌스). documented-only → actually-implemented(`locally-verified`; CI 실행 `needs-confirmation`).
- 2026-06-20 (후속) — CI 실관측으로 verifyEnvKeys 실패 → docs/registries 커밋(gitignore 좁힘, 사용자 Option 1)으로 registry 게이트 CI 강제 회복. PR 템플릿 한국어화.
- 2026-06-20 (CI 속도 최적화 — 사용자 결정 "gradle 잡 통합"): CI wall-clock ~10분+ 원인 = 게이트별 잡 분리로 단일 self-hosted 러너가 잡마다 checkout+setup-java+Gradle캐시+재컴파일 반복(특히 `quality-gates`의 `check` 5m14s 외에 openapi-drift/sample-removal/security-snapshot이 check가 *이미 실행하는* 테스트를 재실행, optional-adapter-matrix는 테스트 1개에 app-bootstrap 테스트를 4× 재컴파일). 해결: gradle 잡 4개 제거하고 `./gradlew check verifyPublicPathSnapshot` 단일 invocation으로 통합(9잡→5잡, gradle 잡 6→2). 게이트 강도 불변(check가 전 테스트 실행, gate-matrix-lint가 매트릭스↔코드 정합 유지). 매트릭스의 optional-adapter 행 mechanism을 workflow-job→contract-test(OptionalAdapterConditionalExecutionContractTest, check 내 실행)로 정합. gate-matrix-lint PASS(20=16+4) 유지.
- 2026-06-20 (CI 2차 — 게이트 배선 검증 성공 + 2차 수정): release-gate fan-in 이 `quality-gates: failure` + `breaking-change-approval: failure` 를 정확히 감지·차단(`::error::release-gate: ... failed`) → **Claim C1 실증 완료**. 두 실패 모두 원인 규명·수정: (1) registries 만 커밋해 `docs/runbooks/` 부재 → Runbook/BackgroundJobErrorCode 계약 5건이 skip→fail(runbook 파일 dangling). `mv docs/runbooks` 로 로컬 재현 후 gitignore 에 `!/docs/runbooks/` 추가(45개 runbook 추적). (2) breaking-change governed 정규식에 `ci-gate-matrix.yml`(config)을 과포함 → 매트릭스 생성 PR 이 라벨 강요당함. governed 를 OpenAPI 스냅샷·`*.approved.*` 로 한정(config 는 CODEOWNERS+lint 로 보호). checkstyleTest ERROR 대량은 비차단 노이즈(static-analysis branch 소유, `ignoreFailures=true`) — 본 branch 실패 원인 아님.
- 2026-06-20 (CI 3차 — quarantine 첫 실사용): full `check` 에서 `PrivacySettingsTest.blankSalt`(CapturedOutput) 1건 flaky 실패 → release-gate 재차단(Claim C1 재실증). 원인 = async logback(`logback-spring.xml` ASYNC_ENABLED 기본 true) + sibling `@SpringBootTest` 가 설치한 JVM-전역 appender 와의 stdout race. 사용자 결정 **격리**: `blankSalt` 메서드만 `@Tag("quarantine")` + `flaky-quarantine.yaml` 등록(14d sunset). `verifyQuarantineSunset OK(1/1)`, `quarantineTest` 1건 비차단 실행, `check verifyPublicPathSnapshot` BUILD SUCCESSFUL. 잠복: `LoggingSettingsTest` 동일 패턴. 근본수정(owner): `logback-test.xml` async-off 또는 ListAppender 단언. 상세 → [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]] (Trap 4 추가).
## 완료 후 정리
> 머지/종료 시점에 채움.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):