TT Lab
블로그

한국어

시작하기
블로그

블로그

열린 PR 45건을 한 번에 점검했다: 병합 상태 다섯 가지와 충돌 셋, 클론 없이 다시 얹기

번외: 병합된 PR 이 아니라 열린 PR 을 돌보는 절차입니다. 이 글에 나오는 PR 은 모두 2026-11-13 기준 「진행 중」입니다. 사실은 GitHub 의 PR·검사 기록과 문서, 각 프로젝트의 기여 규칙 원문, 기여 원장의 조회 원자료와 실측 기록에서 가져왔습니다. 시각은 한국 시간입니다.

목차

한 줄 요약

대상 업스트림 프로젝트에 열려 있는 PR 45건. 10월 중순부터 말 사이에 올린 것이고 모두 초안이 아닙니다. 본인 저장소와 소속 커뮤니티 저장소의 옛 PR 7건은 따로 셉니다
조회 gh pr view --json mergeable,mergeStateStatus,reviewDecision 을 PR 마다 한 번
분포 BLOCKED 24 · UNSTABLE 9 · CLEAN 7 · BEHIND 3 · DIRTY 2. 첫 조회에서는 DIRTY 3, UNKNOWN 4
처리 충돌 세 건 가운데 한 건은 새 기준 위로 다시 얹어 해소. 두 건은 다시 써야 해서 진행 중
열린 PR 45건의 병합 상태를 한 번에 조회했다 2026-11-13 재조회 · gh pr view --json mergeStateStatus ① 상태별 건수: 충돌은 DIRTY 뿐이다 BLOCKED 24 리뷰 대기 UNSTABLE 9 검사 미통과 CLEAN 7 병합 가능 BEHIND 3 뒤처짐 DIRTY 2 충돌 ② 충돌 원인 셋과 처리(첫 조회의 DIRTY 는 3건) 이웃 줄이 고쳐짐 dynamo#16465 · 문서 2개 새 main 위로 재기준 +15/−4 그대로 · 해소 문서를 통째로 다시 씀 dcgm-exporter#760 새 문서에 맞춰 다시 쓰기 새 판 4.9.0 미실측 · 진행 중 주 버전 개편(v3) catwalk#676 새 형식으로 다시 쓰기 새 Go 도구로 빌드 · 진행 중 BEHIND 3건은 충돌이 아니다: 기준 브랜치만 앞서 있다 첫 조회의 UNKNOWN 4건은 다시 조회해야 값이 나온다
그림: 열린 PR 45건의 병합 상태 분포와 충돌 세 건의 원인·처리

1. 왜 점검하나: 열린 PR 은 가만히 있어도 낡는다

PR 을 올린 날에는 충돌도 없고 검사도 통과합니다. 그런데 리뷰를 기다리는 몇 주 동안 기준 브랜치는 계속 움직입니다. 다른 사람이 같은 파일을 고치고, 문서 구조가 바뀌고, 검사를 돌리는 러너의 환경이 바뀝니다. 우리 PR 은 한 글자도 바뀌지 않았는데 어느 날 「충돌」 표시가 붙거나 통과하던 검사가 실패로 바뀝니다.

메인테이너의 눈으로 보면 충돌이 난 PR 은 리뷰할 수 없는 PR 입니다. 리뷰 순서가 돌아왔을 때 충돌이 나 있으면 그 순서를 그냥 흘려보내게 됩니다. 그래서 열린 PR 이 수십 건으로 늘어난 뒤로는, 알림이 올 때만 들여다보지 않고 전체를 한 번에 조회해 상태별로 나누어 보기로 했습니다.

이 글은 2026-11-13 새벽에 한 그 점검의 기록입니다. 1–6편과 달리 병합된 PR 한 건을 따라가지 않고, 열린 PR 여러 건을 돌보는 절차를 다룹니다. 결과가 나오지 않은 PR 이므로 어느 것도 「받아들여졌다」고 적지 않습니다.

2. 조회: 상태 값 다섯 가지와 UNKNOWN

먼저 열린 PR 의 목록을 만들고(gh search prs --author <계정> --state open), PR 마다 다음 명령을 한 번씩 실행했습니다.

gh pr view <번호> --repo <소유자>/<저장소> --json mergeable,mergeStateStatus,reviewDecision

mergeable 은 충돌 여부만 알려 주고(MERGEABLE·CONFLICTING·UNKNOWN), mergeStateStatus 는 「지금 병합 단추를 누를 수 있는가」를 더 잘게 나눕니다. GitHub 의 GraphQL 문서가 적은 뜻과 이번에 나온 건수는 다음과 같습니다.

값 문서의 뜻(요지) 건수 우리가 읽은 방법
BLOCKED 병합이 막혀 있음 24 대부분 리뷰 대기. 리뷰 판정은 「리뷰 필요」 17, 판정 없음 5, 승인 1, 변경 요청 1
UNSTABLE 병합은 되지만 통과 상태가 아닌 검사가 있음 9 검사 기록을 열어 원인이 우리 변경인지부터 확인
CLEAN 병합할 수 있고 검사도 통과 7 우리가 할 일이 없음. 승인까지 받은 것은 그중 1건
BEHIND PR 브랜치가 기준보다 뒤처짐 3 충돌이 아님. 기준 브랜치만 앞서 있음
DIRTY 병합 커밋을 깨끗하게 만들 수 없음 2 충돌. 원인을 PR 단위까지 추적

여기서 두 가지를 배웠습니다.

첫 조회의 UNKNOWN 은 결과가 아닙니다. 첫 조회에서 네 건이 UNKNOWN 으로 나왔습니다. GitHub 의 REST 문서는 이 값이 비어 있으면 병합 가능 여부를 계산하는 백그라운드 작업이 막 시작된 것이니 시간을 두고 다시 요청하라고 안내합니다. 실제로 4분쯤 뒤에 같은 명령으로 다시 조회했더니 세 건은 BLOCKED, 한 건은 UNSTABLE 로 나왔습니다. 오래 조회되지 않은 PR 일수록 첫 조회에서 이 값이 나오므로, 일괄 점검 스크립트는 UNKNOWN 만 모아 한 번 더 조회하게 해야 합니다.

CLEAN 은 「곧 병합된다」는 뜻이 아닙니다. 일곱 건은 기술적으로 막힌 것이 없다는 뜻일 뿐이고, 병합은 메인테이너가 결정합니다. 반대로 BLOCKED 24건도 대부분 고칠 것이 있어서가 아니라 리뷰 순서를 기다리는 상태였습니다. 45건 가운데 우리가 손을 써야 하는 것은 DIRTY 와, UNSTABLE 가운데 원인이 우리 쪽에 있는 것뿐입니다. UNSTABLE 아홉 건의 사유는 건마다 달라 이 글에서는 나누지 않았습니다.

검사 실패의 원인이 우리 변경이 아닌 경우도 있었습니다. 하루 전인 11-12 에 본 containers/kubernetes-mcp-server#1518 은 리뷰어 한 명의 승인을 받은 상태에서 브라우저 시험이 실패로 바뀌어 있었습니다. 같은 시험이 낡은 기준에서만 실패했고, 검사 러너의 이미지가 바뀐 것이 원인으로 보였습니다. 변경 내용은 그대로 두고 새 기준 위로 다시 얹었습니다. 그 뒤 두 번째 승인이 붙었고 이번 조회에서는 CLEAN 이었습니다. 이 PR 도 진행 중입니다.

3. 충돌 셋의 원인은 서로 달랐다

첫 조회의 DIRTY 는 세 건이었습니다. 충돌이 났다는 사실만으로는 무엇을 해야 할지 알 수 없어서, 세 건 모두 어느 PR 이 병합되면서 충돌이 생겼는지까지 찾았습니다. 원인이 달랐고, 그래서 처리도 달랐습니다.

우리 PR 충돌을 만든 병합 그 병합의 크기 원인 처리
ai-dynamo/dynamo#16465 문서 4개, +15/−4 #17229, 11-06 04:34 29개 파일, +97/−3068 우리가 고친 줄의 이웃 줄이 고쳐짐 새 기준 위로 다시 얹음. 해소
NVIDIA/dcgm-exporter#760 문서 1개, +16/−7 #768, 11-07 03:45 961개 파일, +161632/−27808 고친 문단이 들어 있던 문서가 통째로 다시 쓰임 새 문서에 맞춰 다시 써야 함. 진행 중
charmbracelet/catwalk#676 파일 3개, +63/−0 #611, 10-29 23:22 73개 파일, +39578/−18179 주 버전 개편으로 모듈 경로와 설정 형식이 바뀜 새 형식으로 다시 써야 함. 진행 중

이웃 줄이 고쳐진 경우. dynamo 의 PR 은 사용 중단된 기능의 문서 네 곳에 빠져 있던 사용 중단 표기를 더하는 변경입니다. 그 사이 업스트림이 관련 통합 코드를 걷어 내는 PR 을 병합하면서 같은 문서 두 개의 바로 옆 줄을 고쳤습니다. 다시 얹기 전에 먼저 확인한 것은 「우리 변경이 아직 필요한가」였습니다. 새 기준 브랜치의 네 파일을 열어 보니 사용 중단 표기는 여전히 빠져 있었습니다. 그래서 새 기준의 파일에 같은 변경을 다시 적용했고, 변경량은 +15/−4 그대로였습니다. 다시 조회하니 충돌이 풀리고 리뷰 대기(BLOCKED)로 돌아왔습니다.

문서가 통째로 다시 쓰인 경우. dcgm-exporter 의 PR 은 디버그 덤프를 꺼내는 절차의 문단을 고친 것입니다. 그런데 새 판(4.9.0)을 내는 PR 하나가 961개 파일을 바꾸면서 그 문서를 통째로 다시 썼습니다. 이 PR 은 만들어진 지 30분 만에 병합되었으니 미리 알 방법도 없었습니다. 새 문서에서는 우리가 고친 문단이 사라졌고, 덤프를 두는 자리도 바뀌었습니다. 다시 얹는다고 풀리는 충돌이 아닙니다. 새 문서에 남은 복사 명령 한 줄이 새 이미지에서도 실패하는지를 먼저 재야 하는데, 새 판은 아직 재지 못했습니다(미실측). 재고 나서 한 줄짜리 PR 로 다시 쓰거나, 문제가 사라졌다면 닫는 것이 맞습니다.

주 버전이 개편된 경우. catwalk 의 PR 은 공급자 설정을 더하는 변경입니다. 그 사이 v3 가 병합되면서 모듈 경로와 설정 파일의 형식이 바뀌었습니다. 예전 형식의 파일을 그대로 얹을 수 없고, 새 형식으로 다시 써서 새 판의 도구로 빌드해 보아야 합니다. 이것도 진행 중입니다.

BEHIND 는 충돌이 아닙니다. 세 건은 기준 브랜치가 앞서 있을 뿐이고 병합 커밋은 깨끗하게 만들어집니다. 이번에는 손대지 않았습니다. 충돌이 없으니 리뷰가 시작되기 전에 서둘러 다시 얹을 이유가 없다고 보았습니다. 리뷰가 시작된 뒤의 강제 푸시를 꺼리는 프로젝트도 있어서, 필요 없는 다시 얹기는 줄이는 편이 낫습니다.

세 건을 놓고 보면 충돌은 「우리 변경과 남의 변경이 겹친 정도」가 아니라 「우리 변경이 딛고 있던 바닥이 얼마나 바뀌었는가」로 갈렸습니다. 이웃 줄 정도면 다시 얹으면 되고, 문단이나 형식이 사라졌다면 변경 자체를 다시 판단해야 합니다.

4. 클론 없이 다시 얹기: Git Data API 와 SSH 서명

수십 개 저장소를 모두 내려받아 두고 다시 얹기는 부담스럽습니다. 문서 몇 줄을 고치는 PR 을 위해 수 GB 짜리 저장소를 받는 것은 낭비이기도 합니다. 그래서 다시 얹기는 저장소를 내려받지 않고 GitHub 의 Git Data API 로 합니다. 로컬에서는 커밋을 만들지도, 브랜치를 바꾸지도 않습니다. 순서는 다음과 같습니다.

  1. 포크의 기본 브랜치를 업스트림과 맞춥니다. 새 기준 커밋이 포크에 있어야 그 위에 커밋을 만들 수 있습니다.
  2. 새 기준에서 바꿀 파일만 받아 같은 변경을 다시 적용하고, 돌릴 수 있는 시험을 돌립니다.
  3. 파일마다 blob 을 만들고(POST git/blobs), 새 기준의 트리를 base_tree 로 지정해 트리를 만듭니다(POST git/trees).
  4. 커밋 객체의 본문을 직접 조립해 SSH 키로 서명합니다.
  5. 서명을 붙여 커밋을 만들고(POST git/commits), 응답의 verification.verified 가 참인지 확인합니다.
  6. 참일 때만 PR 브랜치의 ref 를 새 커밋으로 옮깁니다. 옮기는 대상은 내 포크의 브랜치뿐입니다.

4번에서 서명하는 대상은 git 이 커밋을 저장할 때 쓰는 글자 그대로입니다.

tree <새 트리 SHA>
parent <새 기준 커밋 SHA>
author <이름> <메일> <epoch 초> +0900
committer <이름> <메일> <epoch 초> +0900

<커밋 메시지>
ssh-keygen -Y sign -n git -f <개인 키 파일> <위 내용을 담은 파일>

여기서 부딪힌 것들을 적어 둡니다.

가장 비싸게 배운 것은 파일 모드입니다. 처음 만든 도구는 올리는 파일의 모드를 100644 로 고정했습니다. 10-31 에 containers/podman-compose#1585 를 다시 얹었더니 실행 파일이던 스크립트가 일반 파일로 바뀌었고, 통합 시험 30개가 PermissionError 로 실패했습니다. 내용은 한 글자도 다르지 않았으므로 diff 의 본문만 보아서는 알 수 없는 변경이었습니다. 다음 날 도구가 기준 트리의 모드를 따르도록 고치고 같은 내용으로 커밋을 다시 만들었습니다. 그 뒤로는 서명 도구를 쓴 다음에 반드시 다음을 확인합니다. 이 PR 도 진행 중입니다.

gh pr diff <번호> --repo <소유자>/<저장소> | grep -n 'mode'

아무것도 나오지 않아야 정상입니다. old mode 100755 와 new mode 100644 가 보이면 의도하지 않은 변경입니다.

5. 올리지 않은 것: 프로젝트마다 다른 문

점검하는 김에, 등록만 해 둔 이슈 가운데 PR 로 이을 수 있는 것이 있는지도 보았습니다. 결론은 「지금 올릴 것은 없다」였습니다. 패치를 못 만들어서가 아니라 프로젝트의 규칙이 순서를 정해 두었기 때문입니다.

올린 것도 하나 있습니다. KubeVirt 의 virt-operator 가 아무 인증서도 버리지 않으면서 「오래된 CA 를 버린다」는 로그를 반복해 찍는 것을 고친 kubevirt/kubevirt#19889 입니다. KubeVirt 의 AI 기여 정책은 AI 가 만든 코드의 모든 줄을 올리기 전에 검토하고 이해하라고 요구하고, 도구 사용을 밝히기를 권합니다. 그래서 커밋에 Assisted-by 트레일러를 붙이고 초안 PR 로 올렸습니다. 사람이 diff 를 끝까지 읽은 뒤에 리뷰 요청 상태로 바꿉니다. 이 PR 도 진행 중이며, 병합되면 따로 한 편을 씁니다.

6. 맥에서 못 돌리는 시험을 재는 법

위 KubeVirt 변경의 단위 시험은 맥에서 돌릴 수 없었습니다. 시험이 딸린 패키지가 리눅스 전용 패키지에 의존하기 때문입니다. 「시험은 CI 가 돌려 주겠지」 하고 올리는 대신 다음 방법으로 직접 쟀습니다.

  1. 맥에서 리눅스용 시험 바이너리만 교차 컴파일합니다. 저장소의 vendor 디렉터리만 쓰므로 새로 받는 것이 없습니다.
  2. 그 바이너리를 TT Lab 클러스터의 임시 파드에 넣어 실행만 합니다. 파드는 노드에 이미 받아져 있는 이미지를 쓰고, capability 를 모두 버리고, root 가 아닌 사용자로, 서비스 계정 토큰 없이, 메모리 상한 1Gi 로 띄웁니다. 끝나면 지워집니다.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off \
  GOFLAGS="-mod=vendor -buildvcs=false" go test -c -o <시험 바이너리> <패키지 경로>
무엇을 결과
새로 쓴 시험 2개, 고치기 전 코드 0 통과, 2 실패
새로 쓴 시험 2개, 고친 코드 2 통과
CA 묶음 관련 시험 27 가운데 27 통과
패키지 전체(순서를 섞어 3회) 160 가운데 160 통과, 약 0.53초
이웃 apply 패키지 전체 219 가운데 219 통과

환경은 리눅스 커널 7.0, x86_64, Go 1.26.6 입니다. 고치기 전의 증상도 쟀습니다. KubeVirt v1.9.0 이 깔린 TT Lab 클러스터에서 리더 operator 의 로그를 24분 동안 세어 보니 같은 줄이 711줄이었습니다. 조정 237회마다 CA 묶음 셋에 한 줄씩 찍힌 수와 맞습니다.

여기에도 함정이 하나 있었습니다. 처음 쓴 시험은 고치기 전 코드에서도 통과했습니다. 시험 환경의 기본 로거가 정보 수준 로그를 걸러 내서, 「로그가 찍히지 않아야 한다」는 시험이 로그가 걸러진 덕에 통과한 것입니다. 시험 안에서 로그 상세 수준을 올리고 끝나면 되돌리게 고친 뒤에야 고치기 전 코드에서 2개가 모두 실패했습니다. 새 시험은 고치기 전 코드에서 실패하는 것을 먼저 확인해야 한다는 원칙을 건너뛰었다면, 아무것도 지키지 못하는 시험을 올릴 뻔했습니다.

7. 배운 것: 열린 PR 을 돌보는 요령

티티 한마디. 도서관에 「이 책 사 주세요」 쪽지를 여러 장 넣어 두었는데, 한참 뒤에 가 보니 어떤 쪽지는 책장 번호가 바뀌어서 다시 써야 했고, 어떤 쪽지는 사서 선생님 도장을 먼저 받아 와야 한대요. 그래서 이제는 쪽지를 넣고 잊어버리지 않고, 가끔 한꺼번에 꺼내서 아직 맞는 쪽지인지 읽어 봐요.

링크

모든 자료는 2026-11-13 에 열람했습니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다