번외: 병합된 PR 이 아니라 열린 PR 을 돌보는 절차입니다. 이 글에 나오는 PR 은 모두 2026-11-13 기준 「진행 중」입니다. 사실은 GitHub 의 PR·검사 기록과 문서, 각 프로젝트의 기여 규칙 원문, 기여 원장의 조회 원자료와 실측 기록에서 가져왔습니다. 시각은 한국 시간입니다.
목차
- 한 줄 요약
- 1. 왜 점검하나: 열린 PR 은 가만히 있어도 낡는다
- 2. 조회: 상태 값 다섯 가지와 UNKNOWN
- 3. 충돌 셋의 원인은 서로 달랐다
- 4. 클론 없이 다시 얹기: Git Data API 와 SSH 서명
- 5. 올리지 않은 것: 프로젝트마다 다른 문
- 6. 맥에서 못 돌리는 시험을 재는 법
- 7. 배운 것: 열린 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 |
| 처리 | 충돌 세 건 가운데 한 건은 새 기준 위로 다시 얹어 해소. 두 건은 다시 써야 해서 진행 중 |
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 로 합니다. 로컬에서는 커밋을 만들지도, 브랜치를 바꾸지도 않습니다. 순서는 다음과 같습니다.
- 포크의 기본 브랜치를 업스트림과 맞춥니다. 새 기준 커밋이 포크에 있어야 그 위에 커밋을 만들 수 있습니다.
- 새 기준에서 바꿀 파일만 받아 같은 변경을 다시 적용하고, 돌릴 수 있는 시험을 돌립니다.
- 파일마다 blob 을 만들고(
POST git/blobs), 새 기준의 트리를base_tree로 지정해 트리를 만듭니다(POST git/trees). - 커밋 객체의 본문을 직접 조립해 SSH 키로 서명합니다.
- 서명을 붙여 커밋을 만들고(
POST git/commits), 응답의verification.verified가 참인지 확인합니다. - 참일 때만 PR 브랜치의 ref 를 새 커밋으로 옮깁니다. 옮기는 대상은 내 포크의 브랜치뿐입니다.
4번에서 서명하는 대상은 git 이 커밋을 저장할 때 쓰는 글자 그대로입니다.
tree <새 트리 SHA>
parent <새 기준 커밋 SHA>
author <이름> <메일> <epoch 초> +0900
committer <이름> <메일> <epoch 초> +0900
<커밋 메시지>
ssh-keygen -Y sign -n git -f <개인 키 파일> <위 내용을 담은 파일>
여기서 부딪힌 것들을 적어 둡니다.
- 글자가 하나라도 다르면 서명이 맞지 않습니다. API 에 넘기는 작성 시각은 ISO 형식이고 커밋 객체 안의 시각은 epoch 초와 시간대입니다.
두 값이 같은 순간을 가리켜야 하고, 메시지 끝의 줄바꿈도 같아야 합니다. 그래서 브랜치를 옮기기 전에 반드시
verification.verified를 확인하고, 거짓이면 멈춥니다. - 문서는 PGP 서명만 말하지만 SSH 서명도 검증되었습니다. REST 문서는
signature를 커밋의 PGP 서명이라고 설명합니다. 우리는 계정에 서명 키로 등록한 SSH 키로 서명했고, 만든 커밋들은 「Verified」 로 표시되었습니다. 문서에 없는 동작이므로 실측으로만 적습니다. - 브랜치 머리를 확인하고 옮깁니다. 도구는 「지금 브랜치 머리가 이 SHA 일 것」이라는 기대값을 받아, 다르면 아무것도 하지 않습니다. 그 사이 메인테이너가 브랜치에 커밋을 더했을 수 있기 때문입니다.
- 큰 저장소는 트리 전체를 한 번에 읽을 수 없습니다. 트리를 재귀로 읽는 호출은 항목이 너무 많으면 응답이 잘리고
truncated가 참으로 옵니다. vendor 디렉터리가 큰 저장소에서 이 일을 겪은 뒤로는 경로를 따라 한 단계씩 비재귀 트리를 읽게 했습니다.
가장 비싸게 배운 것은 파일 모드입니다. 처음 만든 도구는 올리는 파일의 모드를 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 로 이을 수 있는 것이 있는지도 보았습니다. 결론은 「지금 올릴 것은 없다」였습니다. 패치를 못 만들어서가 아니라 프로젝트의 규칙이 순서를 정해 두었기 때문입니다.
- 승인 라벨이 붙은 이슈에만 PR 을 받는 프로젝트. fake-gpu-operator 의 기여 규칙은 모든 PR 이 메인테이너가
approved라벨을 붙인 열린 이슈 하나에 연결되어야 한다고 적고, 검사가 이를 강제합니다. 우리가 등록한 이슈 두 건에는 아직 라벨이 없습니다. - 라벨이 없으면 PR 을 자동으로 닫는 프로젝트. kubeflow/pipelines 는 조직 구성원이 아닌 사람의 PR 이
ready라벨이 붙은 이슈에 연결되어 있기를 요구합니다. 버그 수정과 문서 변경에도 적용되고, 조건을 채우지 못하면 워크플로가 댓글을 달고 PR 을 닫습니다. - 진행 승인을 먼저 받으라는 프로젝트. Moby 와 Docker 오픈소스 프로젝트가 함께 쓰는 AI 정책은, 이슈를 먼저 열어 메인테이너가 필요를 확인하고 진행을 승인하기 전에 AI 의 도움을 받은 PR 을 올리는 일을 「기여 부풀리기」의 예로 듭니다. 우리가 문서 이슈를 등록해 둔 지 27일이 지났지만 라벨도 댓글도 없으므로 PR 은 올리지 않았습니다.
- 다른 기여자가 먼저 PR 을 연 이슈. 우리가 등록한 이슈 다섯 건에는 이미 다른 사람이 PR 을 열어 두었습니다. 같은 수정을 겹쳐 올리면 메인테이너의 일만 늘어납니다.
올린 것도 하나 있습니다. KubeVirt 의 virt-operator 가 아무 인증서도 버리지 않으면서 「오래된 CA 를 버린다」는 로그를 반복해 찍는 것을
고친 kubevirt/kubevirt#19889 입니다. KubeVirt 의 AI 기여 정책은 AI 가 만든 코드의
모든 줄을 올리기 전에 검토하고 이해하라고 요구하고, 도구 사용을 밝히기를 권합니다. 그래서 커밋에 Assisted-by 트레일러를 붙이고
초안 PR 로 올렸습니다. 사람이 diff 를 끝까지 읽은 뒤에 리뷰 요청 상태로 바꿉니다. 이 PR 도 진행 중이며, 병합되면 따로 한 편을 씁니다.
6. 맥에서 못 돌리는 시험을 재는 법
위 KubeVirt 변경의 단위 시험은 맥에서 돌릴 수 없었습니다. 시험이 딸린 패키지가 리눅스 전용 패키지에 의존하기 때문입니다. 「시험은 CI 가 돌려 주겠지」 하고 올리는 대신 다음 방법으로 직접 쟀습니다.
- 맥에서 리눅스용 시험 바이너리만 교차 컴파일합니다. 저장소의 vendor 디렉터리만 쓰므로 새로 받는 것이 없습니다.
- 그 바이너리를 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 을 돌보는 요령
- 열린 PR 은 주기적으로 한 번에 조회합니다. 알림은 충돌을 알려 주지 않는 경우가 많습니다. 명령 하나를 PR 수만큼 돌리면 됩니다.
UNKNOWN은 다시 조회합니다. 첫 조회의 값은 「아직 계산하지 않았다」는 뜻이고, 몇 분 뒤에 값이 나옵니다.DIRTY만 충돌입니다.BEHIND는 뒤처진 것이고BLOCKED는 대부분 리뷰 대기입니다. 상태마다 할 일이 다릅니다.- 충돌은 원인이 된 병합을 찾고 나서 처리합니다. 이웃 줄이 바뀐 것이면 다시 얹고, 문단이나 형식이 사라졌으면 변경이 아직 필요한지부터 다시 판단합니다. 다시 얹기 전에 새 기준에서도 문제가 남아 있는지 확인합니다.
- API 로 커밋을 만들 때는 내용 말고도 잃는 것이 있는지 봅니다. 파일 모드가 그랬습니다. 서명 검증과 모드 변경 여부를 매번 확인합니다.
- PR 을 올리기 전에 그 프로젝트의 문이 어디인지 읽습니다. 이슈 라벨, 진행 승인, AI 도구 표기처럼 프로젝트마다 순서가 다릅니다. 규칙을 건너뛴 PR 은 자동으로 닫히거나 메인테이너의 시간을 씁니다.
- 새 시험은 고치기 전 코드에서 실패하는지부터 확인합니다. 통과만 확인한 시험은 무엇을 지키는지 알 수 없습니다.
티티 한마디. 도서관에 「이 책 사 주세요」 쪽지를 여러 장 넣어 두었는데, 한참 뒤에 가 보니 어떤 쪽지는 책장 번호가 바뀌어서 다시 써야 했고, 어떤 쪽지는 사서 선생님 도장을 먼저 받아 와야 한대요. 그래서 이제는 쪽지를 넣고 잊어버리지 않고, 가끔 한꺼번에 꺼내서 아직 맞는 쪽지인지 읽어 봐요.
링크
모든 자료는 2026-11-13 에 열람했습니다.
- GitHub 문서: REST, PR 한 건 조회와
mergeable, GraphQLMergeStateStatus, REST, 커밋 만들기와signature, REST, 트리와truncated - 다시 얹은 PR: ai-dynamo/dynamo#16465(충돌을 만든 병합 #17229), containers/kubernetes-mcp-server#1518
- 다시 써야 하는 PR: NVIDIA/dcgm-exporter#760(충돌을 만든 병합 #768), charmbracelet/catwalk#676(충돌을 만든 병합 #611)
- 파일 모드를 잃었던 PR: containers/podman-compose#1585
- 초안으로 올린 PR: kubevirt/kubevirt#19889
- 기여 규칙: KubeVirt AI 기여 정책, Moby·Docker AI 정책, fake-gpu-operator CONTRIBUTING, kubeflow/pipelines CONTRIBUTING