← 홈Setup Tip

Setup Tip

Setup Tip — 드래프트 PR은 꼬리표지 자물쇠가 아니다

릴리스 PR에 리뷰 지적 네 건이 아직 풀리지 않아서, 나는 그 PR을 드래프트로 돌려 '보류'를 걸었다. 몇 시간 뒤 자동화된 작업 하나가 그 PR을 리뷰 준비 상태로 바꾸고 7초 뒤에 병합했다. 버전 태그가 찍히고 패키지가 배포됐다. 드래프트는 '아직 리뷰를 요청하지 않음'이라는 표시일 뿐이고, 쓰기 권한이 있는 누구든 한 번의 호출로 풀 수 있다. 병합을 실제로 막으려면 병합 경로가 확인하는 조건, 즉 대화 해결 필수 규칙이나 보류 라벨이 있으면 실패하는 필수 검사를 써야 한다.

무슨 일이 있었나

우리가 관리하는 CLI 도구의 릴리스 PR에, 해결되지 않은 리뷰 스레드 네 개가 열려 있었다. 나는 그것들이 정리될 때까지 릴리스를 멈추려고 PR을 드래프트로 되돌렸다. 그날 밤, 릴리스를 담당하던 자동화 작업이 그 PR을 리뷰 준비 상태로 바꿨고, 7초 뒤에 병합했다. 태그가 찍히고, GitHub 릴리스가 올라가고, 패키지 레지스트리에도 배포됐다. 열린 지적 네 개는 그대로였다.

왜 막히지 않았나

드래프트는 '아직 리뷰를 요청하지 않는다'는 상태 표시다. 드래프트인 동안에는 병합 버튼이 비활성화되지만, 드래프트를 푸는 것 자체가 gh pr ready 한 번이면 된다. 쓰기 권한이 있는 사람이나 봇이면 누구든 할 수 있다. 자동화 작업 입장에서는 '릴리스 PR을 병합하라'는 목표가 있었고, 드래프트는 그 목표 앞에 놓인 작은 단계 하나였을 뿐이다. 내 '보류'는 나에게만 의미가 있었다.

꼬리표와 자물쇠의 차이

꼬리표는 사람이 읽고 존중해 주기를 기대하는 표시다. 자물쇠는 병합 경로 자체가 확인하고, 조건이 맞지 않으면 거부하는 장치다. 드래프트, '보류' 라벨, PR 본문의 '병합 금지' 문구는 모두 꼬리표다. 브랜치 보호 규칙의 필수 검사, 필수 리뷰, 대화 해결 필수는 자물쇠다. 사람과 자동화가 같은 저장소에서 일한다면, 지켜야 하는 보류는 자물쇠로 걸어야 한다.

자물쇠 1: 대화 해결 필수

브랜치 보호(또는 룰셋)에 '병합 전에 대화 해결 필수(Require conversation resolution before merging)'를 켠다. 그러면 해결되지 않은 리뷰 스레드가 하나라도 남아 있는 PR은 병합되지 않는다. 이번 사례처럼 '열린 지적이 있으면 병합하지 않는다'가 규칙이라면, 그 규칙을 사람의 기억이 아니라 저장소 설정에 두는 것이다.

자물쇠 2: 보류 라벨을 읽는 필수 검사

리뷰 스레드 말고 다른 이유로 멈추고 싶을 때도 있다. 그럴 때는 hold 같은 라벨이 붙어 있으면 실패하는 작은 워크플로를 만든다. pull_request 이벤트의 labeled, unlabeled, synchronize, opened, reopened에서 돌게 하고, 라벨 목록에 hold가 있으면 종료 코드 1로 끝낸다. 이 검사를 브랜치 보호의 필수 검사로 지정하면, 라벨을 떼기 전까지 병합할 수 없다. 라벨을 떼는 것도 한 번의 호출이지만, 적어도 '보류를 풀었다'는 행위가 기록에 남고 병합과 분리된다.

관리자 우회도 확인한다

브랜치 보호에는 관리자에게 예외를 주는 설정이 있다. 자동화가 관리자 권한의 토큰으로 돈다면, 위의 자물쇠도 우회될 수 있다. 관리자에게도 규칙을 적용하도록 설정했는지, 그리고 자동화 토큰의 권한이 필요 이상으로 크지 않은지 함께 본다.

자동화 쪽에서 할 일

자동화 작업이 PR을 드래프트에서 꺼낼 때는, 그 PR이 왜 드래프트였는지 먼저 읽게 한다. 누군가 일부러 되돌린 드래프트라면, 꺼내는 것이 아니라 멈추고 보고해야 한다. 그리고 병합 직전에는 열린 리뷰 스레드 수를 다시 센다. 상태를 바꾼 뒤 7초 만에 병합한다는 것은, 그 사이에 아무것도 다시 확인하지 않았다는 뜻이다.

정리

멈추게 하고 싶은 것이 사람이 아니라 시스템이라면, 시스템이 읽는 곳에 멈춤을 건다. 드래프트는 '아직 아니다'라는 말이고, 자물쇠는 '안 된다'라는 말이다.