요약
.gitattributes의 packages/*/CHANGELOG.md merge=union은 로컬 git에서만 동작하고 GitHub의 병합 판정에는 적용되지 않는다. 그 결과 두 가지가 동시에 일어난다.
- 실제로는 깨끗이 붙는 PR 15개가 GitHub에서
dirty로 표시되어 머지 버튼이 막힌다.
- 로컬에서 union이 조용히 성공할 때, 이미 릴리스된 섹션에 항목을 잘못 집어넣는다. 충돌 마커도, 경고도, CI 신호도 없다.
근거 1 — GitHub는 union을 무시한다 (A/B, 통제됨)
같은 base(1f735b7b), 같은 head(#3892 f18ae6ff), .gitattributes의 union 한 줄만 차이:
| 조건 |
충돌 파일 |
| union 적용 (저장소 설정 그대로) |
없음 |
| union 줄 제거 |
packages/ai/CHANGELOG.md, packages/coding-agent/CHANGELOG.md |
GitHub REST는 같은 head에 대해 mergeable: false, mergeable_state: "dirty"를 반환한다. 즉 GitHub는 union 없는 쪽 규칙으로 계산한다.
열린 PR 24개 전수 대조 — 예외 0건, 상관 100%:
base.sha가 낡아서 생긴 캐시 문제가 아니다. #3892의 head를 base.sha(4bc67a8d)에 붙여도, 현재 dev에 붙여도 union 하에서는 둘 다 깨끗하다.
근거 2 — union이 릴리스된 섹션을 오염시킨다 (재현됨)
릴리스 커밋은 ## [Unreleased] 바로 아래에 ## [0.12.12] - 2026-08-05을 삽입한다(0e9e9447d). [Unreleased] 헤딩 자체는 그대로 남는다. 그래서 릴리스 이전 base를 가진 PR의 CHANGELOG 추가분은 union 병합 시 두 삽입 지점이 겹친다.
#3697(base가 0.12.12 릴리스 이전)을 현재 dev에 union 병합한 결과:
28: ## [0.12.12] - 2026-08-05
...
61: - Deferred composer shell results now commit to the session transcript before ...
아직 머지되지도 않은 #3697의 항목이 이미 배포된 0.12.12 릴리스 노트 안에 들어갔다. 위쪽 ## [Unreleased]가 아니다. 충돌 없음, CI 통과, 리뷰 diff에도 안 보인다 — 리베이스 결과물에만 나타난다.
부수 손상도 있다. 같은 병합 결과에서 ### Fixed 다음 빈 줄이 사라지고 목록 중간에 빈 줄이 끼어든다. union이 양쪽 청크를 그대로 이어붙이기 때문이다.
지금 치르고 있는 비용
저장소에 exact-head 게이트(git merge-base --is-ancestor "$GITHUB_BASE_SHA" HEAD)가 있어서 모든 PR은 머지 전 리베이스가 강제된다. 여기에 "GitHub에서만 충돌로 보이는 CHANGELOG"가 겹치면:
dev가 한 번 움직일 때마다 열린 PR 전부가 dirty로 뒤집힌다.
- 기여자는 유령 충돌을 보고 리베이스·force-push한다 → verdict 무효화, CI 전면 재실행.
- 그 리베이스 중 일부가 위의 오염을 조용히 만들어낸다.
#3928(docs-index.generated.ts)과 합치면 열린 PR 40개가 안 줄어드는 메커니즘이 설명된다. 저쪽은 병합 불가능한 생성 파일, 이쪽은 GitHub가 안 보는 병합 규칙이다.
제안
A. CHANGELOG 조각 파일로 전환 (권장). PR은 changelog.d/<pr-번호>.md 같은 조각만 추가하고, 릴리스 스크립트가 릴리스 시점에 CHANGELOG.md로 합친다. 두 PR이 같은 파일을 건드리지 않으니 충돌 자체가 사라지고, git과 GitHub의 판정이 일치하며, 잘못된 섹션에 끼어들 자리도 없어진다. union 줄은 그때 삭제한다. 비용은 스크립트 + CI 검사 + PR 템플릿 한 줄.
B. 최소 조치 — union은 두되 오염을 막는다. CI에 "## [Unreleased] 위쪽이 아닌 이미 릴리스된 ## [X.Y.Z] 섹션이 이 PR에서 수정되면 실패" 검사를 추가한다. 유령 충돌과 리베이스 churn은 그대로 남지만 조용한 오염은 막는다.
C. union 줄 삭제. 로컬과 GitHub 판정이 일치해 정직해지지만 충돌은 더 늘어난다. A와 묶을 때만 의미 있다.
A를 권한다. B는 A를 하기 전까지의 임시 방벽으로도 유효하다.
관련: #3928
요약
.gitattributes의packages/*/CHANGELOG.md merge=union은 로컬 git에서만 동작하고 GitHub의 병합 판정에는 적용되지 않는다. 그 결과 두 가지가 동시에 일어난다.dirty로 표시되어 머지 버튼이 막힌다.근거 1 — GitHub는 union을 무시한다 (A/B, 통제됨)
같은 base(
1f735b7b), 같은 head(#3892f18ae6ff),.gitattributes의 union 한 줄만 차이:packages/ai/CHANGELOG.md,packages/coding-agent/CHANGELOG.mdGitHub REST는 같은 head에 대해
mergeable: false,mergeable_state: "dirty"를 반환한다. 즉 GitHub는 union 없는 쪽 규칙으로 계산한다.열린 PR 24개 전수 대조 — 예외 0건, 상관 100%:
dirty라고 한 15개(fix: harden memory recovery and Windows resource cleanup #3919 fix(session): retry message-only first-event timeouts under bare defaults #3908 fix(models): surface DeepSeek V4 proxy max effort and wire mapping #3892 fix(telegram): yield orphan pollers on sustained getUpdates 409 #3891 fix(session): complete mid-session credential rotation observability #3889 fix(ai): support JSONL responses in Google-compatible streams #3887 fix(session): harden pre-admission artifact spill #3873 fix(prompt): remove contradictory soul overrides #3870 perf(ai): shard bundled model catalog by provider #3869 fix(coding-agent): make keybinding migration failure-clean #3866 fix(config): make legacy migration durable #3864 fix(utils): preserve message of thrown non-Error objects in crash diagnostics #3823 fix(notifications): exclude retained cleanup leak artifacts from endpoint health #3729 fix(coding-agent): retain deferred shell output #3697 외) → union을 끄면 전부 CHANGELOG에서만 충돌.clean/unstable이라고 한 9개 → union을 꺼도 충돌 없음.base.sha가 낡아서 생긴 캐시 문제가 아니다. #3892의 head를base.sha(4bc67a8d)에 붙여도, 현재dev에 붙여도 union 하에서는 둘 다 깨끗하다.근거 2 — union이 릴리스된 섹션을 오염시킨다 (재현됨)
릴리스 커밋은
## [Unreleased]바로 아래에## [0.12.12] - 2026-08-05을 삽입한다(0e9e9447d).[Unreleased]헤딩 자체는 그대로 남는다. 그래서 릴리스 이전 base를 가진 PR의 CHANGELOG 추가분은 union 병합 시 두 삽입 지점이 겹친다.#3697(base가 0.12.12 릴리스 이전)을 현재
dev에 union 병합한 결과:아직 머지되지도 않은 #3697의 항목이 이미 배포된 0.12.12 릴리스 노트 안에 들어갔다. 위쪽
## [Unreleased]가 아니다. 충돌 없음, CI 통과, 리뷰 diff에도 안 보인다 — 리베이스 결과물에만 나타난다.부수 손상도 있다. 같은 병합 결과에서
### Fixed다음 빈 줄이 사라지고 목록 중간에 빈 줄이 끼어든다. union이 양쪽 청크를 그대로 이어붙이기 때문이다.지금 치르고 있는 비용
저장소에 exact-head 게이트(
git merge-base --is-ancestor "$GITHUB_BASE_SHA" HEAD)가 있어서 모든 PR은 머지 전 리베이스가 강제된다. 여기에 "GitHub에서만 충돌로 보이는 CHANGELOG"가 겹치면:dev가 한 번 움직일 때마다 열린 PR 전부가dirty로 뒤집힌다.#3928(
docs-index.generated.ts)과 합치면 열린 PR 40개가 안 줄어드는 메커니즘이 설명된다. 저쪽은 병합 불가능한 생성 파일, 이쪽은 GitHub가 안 보는 병합 규칙이다.제안
A. CHANGELOG 조각 파일로 전환 (권장). PR은
changelog.d/<pr-번호>.md같은 조각만 추가하고, 릴리스 스크립트가 릴리스 시점에CHANGELOG.md로 합친다. 두 PR이 같은 파일을 건드리지 않으니 충돌 자체가 사라지고, git과 GitHub의 판정이 일치하며, 잘못된 섹션에 끼어들 자리도 없어진다. union 줄은 그때 삭제한다. 비용은 스크립트 + CI 검사 + PR 템플릿 한 줄.B. 최소 조치 — union은 두되 오염을 막는다. CI에 "
## [Unreleased]위쪽이 아닌 이미 릴리스된## [X.Y.Z]섹션이 이 PR에서 수정되면 실패" 검사를 추가한다. 유령 충돌과 리베이스 churn은 그대로 남지만 조용한 오염은 막는다.C. union 줄 삭제. 로컬과 GitHub 판정이 일치해 정직해지지만 충돌은 더 늘어난다. A와 묶을 때만 의미 있다.
A를 권한다. B는 A를 하기 전까지의 임시 방벽으로도 유효하다.
관련: #3928