셀프 호스팅 방식의 PR 리뷰어: GitHub App이 아닌 사용자가 트리거를 제어합니다
요약
GitHub App 방식과 달리 사용자가 직접 트리거를 제어하는 셀프 호스팅 방식의 PR 리뷰 도구인 commitbrief를 소개합니다. gh CLI를 활용하여 별도의 서버 없이 사용자의 인증 정보를 기반으로 안전하고 수동적인 PR 리뷰 프로세스를 구축할 수 있습니다.
핵심 포인트
- 사용자가 원하는 시점에 직접 PR 리뷰를 실행할 수 있는 제어권 제공
- gh CLI를 활용하여 별도의 중간 서버 없이 사용자 계정으로 직접 댓글 게시
- HTTPS 직접 호출 대신 gh 바이너리를 셸로 호출하여 보안 및 인증 처리
- Head-OID 체크를 통해 리뷰 중 PR 변경 시 데이터 정합성 유지
GitHub App은 당신의 의사와 상관없이 풀 리퀘스트(Pull Request, PR)가 열리는 즉시 모든 PR을 리뷰합니다. 반면 commitbrief remote pr <id>는 당신이 지정한 PR을 당신이 실행할 때 리뷰하며, 당신의 gh 인증(auth)을 기반으로 작동하고, 중간 서버 없이 당신의 계정으로 직접 게시합니다. 이것은 이 시리즈의 마지막 통합 단계이며, 아키텍처의 핵심은 바로 포지셔닝, 즉 '누가 트리거를 소유하는가'에 있습니다.
호스팅된 리뷰어는 웹훅(Webhook)에 의해 실행됩니다. 이는 타인의 인프라에서 실행되며, 당신의 저장소(Repo)에 대한 설치 토큰(Installation Token)을 보유하고, 자체적인 일정에 따라 작동합니다. CLI는 이 세 가지를 모두 뒤집습니다. 당신의 머신에서 실행되고, 이미 가지고 있는 GitHub 인증을 빌려 쓰며, 당신이 PR을 다시 검토할 가치가 있다고 결정하는 바로 그 순간에 실행됩니다. 터미널 및 에이전트 경로와 동일한 리뷰 엔진을 사용하지만, 트리거가 다르며, 그 트리거가 바로 핵심입니다.
요약 (TL;DR)
commitbrief remote pr 42는gh를 통해 PR의 디프(Diff)를 가져와 리뷰하고, 당신의 명령에 따라 당신의 계정으로 인라인 댓글(Inline Comments)을 게시합니다.- 자체적인 HTTPS 호출을 수행하지 않습니다. 모든 GitHub 왕복(Round-trip)은 이미 인증 정보를 보유하고 있는 당신의
ghCLI를 통해 이루어집니다. - 변경 요청(Request-changes) 판정은 선택 사항(
--request-changes-on)입니다. 기본적으로는 댓글을 달거나 승인(Approve)만 수행합니다. - 헤드-OID(Head-OID) 경합 체크를 통해, 리뷰 도중 PR이 변경되면 리뷰를 한 번 재실행하며, 오래된 정보를 게시하는 대신 중단합니다.
- 한계점. 이것은 호스팅된 GitHub App이나 정책 게이트(Policy Gate)가 아닙니다. 당신의
gh인증과 유료 API 제공자가 필요하며, 여전히 '최초의 리뷰어'일 뿐 최종 결정권자는 아닙니다.
서비스가 아니라 당신의 gh를 구동합니다
리모트 패키지는 네트워크 호출을 하지 않습니다. gh 바이너리를 셸(Shell)로 호출하여 인증, 호스트 해석(Host resolution), 그리고 REST 왕복(Round-trips)을 처리하도록 합니다.
// 주어진 인자와 함께 `gh`를 셸로 호출하며,
// 호출자가 의미 있는 메시지를 로그로 남길 수 있도록
// stderr를 에러에 노출합니다.
func (execRunner) Run(ctx context.Context, args ...string) ([]byte, error) {
...
네 번의 gh 호출이 리뷰 게시를 구성하며, 이는 정확히 사용자가 수동으로 실행할 법한 명령들입니다:
// FetchPRMeta는 `gh pr view <id> --json number,author,url,headRepository,commits`를 실행합니다.
err := runJSON(ctx, r, &m, repoArgs(repo, "pr", "view", id, "--json", prViewFields)...)
...
네 번째는 아래의 판결(verdict) 제출입니다. PR ID는 gh 네이티브 형식인 42, owner/repo#42 또는 전체 URL을 허용하며, --repo owner/repo를 사용하면 작업 디렉토리에서 추론된 저장소를 덮어쓸 수 있으므로, 현재 위치하지 않은 저장소의 PR도 리뷰할 수 있습니다.
누구의 PR인가, 그리고 여전히 동일한 PR인가?
두 가지 가드(guard)가 리뷰를 감쌉니다. 첫 번째는 위의 gh api user 호출입니다. 이는 _사용자_의 로그인을 확인하고 본인의 PR을 리뷰하는 것을 거부합니다. 계정에서 직접 게시되는 셀프 리뷰(self-review)는 노이즈이므로, 프로바이더(provider)가 호출되기 전에 차단됩니다.
두 번째는 리뷰하는 동안 변경되는 PR을 처리합니다. 디프(diff)는 하나의 헤드 커밋(head commit) 시점에 가져옵니다. 모델이 응답하고 댓글을 게시할 준비가 될 때쯤이면 팀원이 새로운 코드를 푸시했을 수도 있습니다. 따라서 리뷰는 더 이상 존재하지 않는 라인에 댓글을 고정하는 대신, 헤드 OID를 다시 읽고 변경되었다면 한 번 더 실행합니다:
for attempt := 0; ; attempt++ {
res, err := reviewOnePRDiff(ctx, runner, prID, f, app, prov, model, loaded, prog)
// ...
...
FetchLastOID는 커밋 정보만 다시 읽기 때문에(gh pr view <id> --json commits), 경합 상태(race) 확인 비용이 저렴합니다. 한 번의 재시도 후에도 변경된다면 too_volatile 상태가 됩니다. 즉, 이미 사라진 코드에 대한 분석 결과를 게시하느니 차라리 아무것도 게시하지 않는 쪽을 택합니다.
봇 모드: 터미널 앞에 사람이 없는 경우
터미널 리뷰는 중단하고 사용자에게 무언가를 물어볼 수 있습니다. 하지만 PR 리뷰는 그럴 수 없습니다. 프로세스를 지켜보는 사람이 없기 때문입니다. 따라서 세 가지 변경 사항이 있는 동일한 파이프라인이 실행됩니다 (ADR-0016 §3). 대화형 .commitbrief/** 가드(guard)와 비용 사전 점검(cost preflight)은 건너뜁니다. 비밀 정보 스캐너(secret scanner)는 여전히 실행되지만, 중단하는 대신 경고를 보냅니다. 자신의 리뷰를 중단함으로써 타인의 PR에 포함된 자격 증명(credential)을 수정할 수는 없으므로, 올바른 조치는 이를 크게 알리고 계속 진행하는 것입니다:
if app.Config.Guard.SecretScan && !global.allowSecrets {
if hits := guard.ScanForSecrets(diffText); len(hits) > 0 {
prog.Info(app.Catalog.T("remote.secret_warn", len(hits)))
...
그리고 게시된 리뷰는 구조화된 결과(structured findings)를 포함해야 하므로, 게시 경로에는 API 제공자(API provider)가 필요합니다. 일반 텍스트 CLI 제공자는 사전에 거부되며(if _, plain := prov.(provider.PlainTextEmitter); plain { ... }), 줄 기반의 결과(line-anchored findings)가 위치해야 할 곳에 산문(prose)을 게시하는 대신 Markdown으로 저하되는 리뷰는 게시되지 않고 중단됩니다.
결과(finding)를 올바른 줄에 고정하기 (Anchoring)
인라인 댓글(Inline comments)은 REST API를 통해 게시되며, GitHub은 댓글이 디프(diff)의 어느 _측면(side)_에 속하는지 알아야 합니다. 새 파일의 경우 RIGHT, 이전 파일의 경우 LEFT입니다:
func PostComment(ctx context.Context, r Runner, c CommentRequest) error {
side := c.Side
if side == "" {
...
새 코드에 대한 결과는 기본값인 RIGHT로 갑니다. 삭제된 코드에 대한 결과는 LEFT가 필요하며, 측면은 결과 자체의 스니펫(snippet)으로부터 추론됩니다:
// 휴리스틱(Heuristic): 스니펫이 최소 하나 이상의 삭제된("-") 줄을 포함하고
// 추가된("+") 줄을 포함하지 않는 경우. 스니펫이 없는 경우 RIGHT 우선 기본값을 유지합니다.
func preferLeftSide(f render.Finding) bool {
...
결과의 줄이 디프 범위를 벗어나는 경우 — 모델이 참조하지 말아야 할 줄을 참조했거나 POST가 거부된 경우 — 해당 결과는 버려지지 않습니다. 대신 "특정 줄에 첨부할 수 없음"이라는 제목 아래 리뷰 요약에 추가되므로, 앵커(anchor)가 유지되지 않더라도 신호(signal)는 살아남습니다.
판결은 선택 사항입니다 (The verdict is opt-in)
기본적으로 이 리뷰어는 절대 차단(block)하지 않습니다. 리뷰 레벨의 판결(verdict)은 gh pr review의 세 가지 플래그 중 하나에 매핑됩니다:
func (v Verdict) ghFlag() string {
switch v {
case VerdictApprove:
...
하지만 request-changes는 --request-changes-on <severity>를 통해 제어됩니다. 이 설정을 해제해 두면, 발견된 문제의 심각도(severity)가 아무리 높더라도 판결은 approve(결과 없음 또는 정보 제공용) 또는 comment로만 내려질 수 있으며, 절대로 request-changes로 내려지지 않습니다:
enabled := threshold != ""
// ...
if enabled && severityRank[fnd.Severity] <= tr {
...
인라인 코멘트(inline comments)는 해당 판결과 독립적입니다. request-changes를 비활성화하는 것은 리뷰가 _차단(blocks)_할지 여부를 변경하는 것이지, 어떤 발견 사항이 _게시(posted)_될지를 변경하는 것이 아닙니다. 당신은 이 도구가 당신을 대신하여 변경을 요구할 수 있게 할지 결정하며, 기본값은 '아니오'입니다. 즉, 도구는 조언하고, 당신이 판결합니다.
만약 아예 게시하고 싶지 않다면, --no-post를 사용하면 PR 디프(diff)에 대해 정확히 동일한 '가져오기 및 리뷰(fetch-and-review)' 과정을 수행하지만 결과는 로컬에 출력합니다. 이 경우 일반적인 터미널 리뷰처럼 동작하며, --json, --markdown, --output, --copy, --cli(게시 경로에서는 금지된 로컬 CLI 프로바이더 포함)를 다시 사용할 수 있습니다.
commitbrief remote pr 42 # 코멘트 전용 리뷰, 게시됨
commitbrief remote pr 42 --request-changes-on high # high 등급 이상의 심각도에 대해 차단 옵트인(opt in)
commitbrief remote pr owner/repo#42 --no-post --json # 로컬에서 리뷰, 아무것도 게시하지 않음
이것이 아닌 것 (What it is not)
이것이 아닌 것 (What it is not)
이것은 호스팅되는 GitHub App이 아니며, 이는 기능의 공백이 아니라 의도된 설계입니다. 설치 토큰(installation token), 웹훅(webhook), 상시 가동되는 리스너(always-on listener)가 없습니다. 즉, 당신이 잊어버린 PR을 자동으로 리뷰하지 않으며, 모든 PR이 머지(merge)되기 전에 반드시 리뷰되어야 한다는 팀 정책을 강제할 수도 없습니다. CommitBrief는 의무적 리뷰 게이팅(mandatory-review gating)을 수행하지 않습니다(이는 명시적인 비목표(non-goal)입니다). 대신 개발자별 트리거링(per-developer triggering)과 옵트인 방식의 판정(opt-in verdict)이 이 시스템이 지원하는 통합 형태입니다. 게시를 위해서는 당신의 gh 인증과 유료 API 제공업체가 필요하며, 이 시리즈의 다른 모든 방식과 마찬가지로 이것은 '제0의 리뷰어(zeroth reviewer)'입니다. 즉, 사람이 확인하기 전에 명백한 오류를 잡아내는 패스트 패스(fast pass)이지, 사람을 대체하는 것이 아닙니다.
상시 가동되는 웹훅을 포기하는 대신 얻는 것은, 웹훅을 사용할 때 지불해야 하는 비용인 '트리거에 대한 제어권'입니다. 리뷰는 당신이 명령어를 실행했을 때, 당신이 선택한 PR에 대해, 당신의 계정으로부터 실행됩니다. 또한 당신의 코드나 저장소(repo)에 관한 그 어떤 것도 GitHub의 서버 외에 다른 누구의 서버에도 저장되지 않습니다.
Repo: github.com/CommitBrief/commitbrief.
Building CommitBrief의 9부 — 피날레. 6개의 내부 구성 요소, 3개의 통합: 터미널, 에이전트, 그리고 풀 리퀘스트(pull request), 하나의 엔진.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기