필요한 정보 모아보기
테크

깃허브(GitHub) 비동기 머지 API 정식 출시 — 기존 /merge와 차이, 머지 큐·스택 PR 자동화에 붙이는 법

#깃허브#GitHub API#머지 큐#풀 리퀘스트#자동화

깃허브(GitHub)가 10월 1일 **비동기 머지 API(async merge API)**를 정식 출시했다. 깃허브 공지에는 GA(General Availability)라고 적혀 있는데, 미리보기(프리뷰) 단계를 끝내고 모든 사용자에게 정식 기능으로 제공한다는 뜻이다. 비동기 머지 API는 풀 리퀘스트(PR) 머지를 요청과 동시에 끝내지 않고, 요청을 먼저 접수한 뒤 백그라운드에서 처리하는 REST 엔드포인트다. PR 하나를 바로 머지할 수도 있고, 머지 큐에 넣거나 스택 PR을 아래층까지 한꺼번에 머지할 수도 있다. 깃허브는 체인지로그에서 이 API를 기존 동기 REST 엔드포인트나 GraphQL 뮤테이션 대신 쓸 "권장 경로"로 못 박았다. 스택 PR을 머지할 수 있는 API도 이것 하나뿐이다.

머지 봇이나 릴리스 스크립트, 코딩 에이전트에게 PR 머지를 맡기고 있다면 손볼 곳이 생긴다는 얘기다. 공식 문서를 바탕으로 기존 API와 무엇이 다른지, 요청과 상태 조회를 어떻게 이어 붙이는지 정리했다. 실제 저장소에서 읽기 전용으로 호출해 본 결과와, 정식 출시 직후 먼저 써 본 개발자들의 보고도 함께 담았다.

기존 동기 머지 API와 뭐가 다른가

지금까지 스크립트에서 PR을 머지할 때는 PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge를 불렀다. 요청 한 번에 머지가 끝나고, 성공하면 200과 함께 머지 커밋 sha가 돌아온다. 머지할 수 없으면 405, 넘긴 sha와 PR 헤드가 다르면 409다. 단순한 대신 한계가 분명하다. 공식 문서도 이제 이 엔드포인트 위에 "비동기 머지 API를 권장한다. 이 엔드포인트는 스택 PR과 머지 큐를 지원하지 않는다"는 안내를 달아 두었다.

새 엔드포인트는 경로 끝만 다르다. PUT .../pulls/{pull_number}/merge-async. 요청 본문과 응답을 나란히 놓으면 차이가 잘 보인다.

항목동기 /merge비동기 /merge-async
처리 방식요청 안에서 즉시 머지접수 후 백그라운드 처리, 일부 오류는 재시도
정상 접수 응답200 + 머지 커밋 sha202 + 요청 UUID
머지 큐지원 안 함merge_action: merge_queue
스택 PR지원 안 함지원(열린 아래층 PR 전부 포함)
규칙 우회없음bypass_rules: true(우회 권한이 있을 때만)
이미 머지·대기 중인 PR해당 없음200을 바로 반환
같은 PR에 중복 요청해당 없음409 + 진행 중인 요청의 UUID

merge_action이 핵심이다. direct_merge는 큐를 거치지 않고 바로 머지하고, merge_queue는 큐에 넣는다. 기본값인 default는 대상 브랜치에 머지 큐가 설정돼 있으면 큐로, 없으면 직접 머지로 간다. merge_method(merge·squash·rebase)와 커밋 제목·메시지는 직접 머지일 때만 의미가 있다. 큐로 가면 큐 설정이 머지 방식을 정하기 때문이다.

sha도 눈여겨볼 만하다. 비워 두면 요청 시점의 PR 헤드가 기준이 되고, 접수와 실행 사이에 누가 새 커밋을 푸시하면 머지가 취소된다. "검토한 그 커밋만 들어간다"는 보장을 서버가 대신 걸어 주는 셈이다. 그래도 봇이라면 검토한 커밋의 sha를 명시적으로 넘기는 편이 낫다.

요청하고 결과를 받는 흐름

흐름은 두 단계다. PUT으로 접수하고, 돌려받은 UUID로 GET .../merge-async/{uuid}를 불러 결과를 확인한다. 응답에는 공통으로 status 필드가 있고 값은 pending, merged, enqueued, failed 넷 중 하나다.

  • pending — 아직 처리 중. UUID, 머지 방식, 머지 액션, 기대하는 헤드 sha가 같이 온다.
  • merged — 베이스 브랜치에 머지됐다. 머지 커밋 OID가 붙는다.
  • enqueued — 머지 큐에 들어갔다. 머지됐다는 뜻이 아니다.
  • failed — 실패. 이유를 담은 메시지가 온다.

여기서 실수하기 쉬운 지점이 있다. 문서에 따르면 enqueued는 머지 큐 요청의 최종 상태이고, 나중에 큐가 실제로 PR을 머지해도 이 값은 바뀌지 않는다. 그러니 merged가 나올 때까지 폴링하는 코드는 큐 환경에서 영원히 기다린다. 최종 머지 여부는 기존 GET .../pulls/{pull_number}/merge(머지됐으면 204, 아니면 404)나 웹훅으로 따로 봐야 한다.

결과는 마지막 갱신 후 24시간만 보관된다. 그 뒤에는 같은 UUID로 조회해도 404다. 하루 넘게 쌓아 두고 나중에 한꺼번에 확인하는 배치 구조라면 이 점을 감안해야 한다.

요청 단계에서 걸러지는 것은 기본 상태뿐이다. 닫혔거나 드래프트인 PR은 즉시 400이 돌아오지만, 브랜치 보호 규칙과 저장소 규칙(ruleset)은 접수 시점에 검사하지 않는다. 202를 받았다고 머지가 보장되는 건 아니라는 뜻이다.

아래는 문서의 파라미터대로 짠 봇용 셸 스크립트 예시다. 접수 후 2초 간격으로 최대 1분간 폴링하고, enqueued를 만나면 거기서 멈춘다. 실제 머지가 일어나는 요청이라 우리 저장소에서는 실행하지 않았다.

#!/usr/bin/env bash
# 사용법: ./merge-async.sh OWNER/REPO PR번호 검토한_헤드_SHA
set -euo pipefail
REPO="$1"; PR="$2"; SHA="$3"
API_VER="X-GitHub-Api-Version: 2026-03-10"

# 1) 접수 — 브랜치에 머지 큐가 있으면 큐로, 없으면 squash 직접 머지
RESP=$(gh api -X PUT -H "$API_VER" "repos/$REPO/pulls/$PR/merge-async" \
  -f sha="$SHA" -f merge_action=default -f merge_method=squash)
STATUS=$(jq -r '.status' <<<"$RESP")
UUID=$(jq -r '.details.uuid // empty' <<<"$RESP")

# 2) pending 이면 결과 폴링
for _ in $(seq 1 30); do
  [ "$STATUS" != "pending" ] && break
  sleep 2
  RESP=$(gh api -H "$API_VER" "repos/$REPO/pulls/$PR/merge-async/$UUID")
  STATUS=$(jq -r '.status' <<<"$RESP")
done

case "$STATUS" in
  merged)   echo "머지 완료: $(jq -r '.details.sha' <<<"$RESP")" ;;
  enqueued) echo "머지 큐 진입 — 최종 머지는 GET .../pulls/$PR/merge 로 확인" ;;
  failed)   echo "실패: $(jq -r '.details.message' <<<"$RESP")"; exit 1 ;;
  *)        echo "아직 처리 중(UUID $UUID)"; exit 2 ;;
esac

gh api가 2xx가 아닌 응답에서 오류로 끝나기 때문에, 409(이미 진행 중인 요청 있음)를 받아 그 UUID로 이어서 폴링하려면 set -e 대신 응답 코드를 직접 다루도록 고쳐야 한다. 머지된 결과의 커밋 필드 이름은 문서 스키마의 sha 기준으로 적었다.

직접 호출해 보니

실제 머지를 일으키는 PUT은 빼고, 결과 조회 쪽만 읽기 전용으로 불러 봤다(2026-10-05, macOS, gh 2.101.0). 우리 저장소의 이미 머지된 PR 번호에 존재하지 않는 UUID를 넣었다.

gh api -i -H "X-GitHub-Api-Version: 2026-03-10" \
  repos/OWNER/REPO/pulls/92/merge-async/00000000-0000-0000-0000-000000000000
# HTTP/2.0 404 Not Found
# X-Github-Api-Version-Selected: 2026-03-10
# {"message":"Not Found","documentation_url":"…/rest/pulls/pulls#get-the-result-of-an-asynchronous-merge","status":"404"}

404 자체는 예상대로다. 볼 만한 건 documentation_url이다. 라우트가 없어서 나는 404가 아니라 "비동기 머지 결과 조회" 엔드포인트로 정상 라우팅된 뒤 UUID를 못 찾은 404라는 게 드러난다. 버전 헤더를 빼고 다른 공개 저장소에 같은 요청을 보내도 결과는 같았다. 응답 헤더엔 기본 버전 2022-11-28이 찍혔다. 2026-03-10 버전 전용 기능이 아니라는 얘기고, 실제로 공식 문서도 두 버전 모두에 이 엔드포인트를 싣고 있다.

권한 표에서도 하나 건졌다. 세분화된 개인 액세스 토큰(fine-grained PAT)과 GitHub 앱 모두 PUT과 GET에 Contents 쓰기 권한이 필요하다. 결과 조회도 마찬가지다. 기존 "머지됐는지 확인"(GET .../merge)이 Pull requests 읽기 권한이면 되는 것과 대조적이다. 상태만 보는 대시보드용 토큰을 따로 두려던 팀이라면 이 차이에서 막힌다.

머지 큐·자동화 봇에 붙인다면

정식 출시 바로 다음 날부터, PR을 자동으로 만들고 머지하는 오픈소스 도구들 쪽에서 반응이 나왔다. 의존성 업데이트 봇 Renovate의 저장소에는 "우리 머지 경로도 비동기 API로 옮기자"는 이슈가 열렸다. 다만 회사가 자체 서버에 설치해 쓰는 GitHub Enterprise Server(GHES)나 기업용 클라우드(GHEC)에는 이 API가 아직 없을 수 있으니, 호출 전에 API가 있는지부터 확인하는 코드(기능 감지)를 붙여야 한다는 단서도 달렸다. 반대로 머지 봇 Shipyard 개발자는 10월 2일 GitHub 앱으로 시험해 본 결과를 공개했는데, 내용이 꽤 구체적이다.

  • 필수 체크가 대기 중이거나 실패한 PR에 요청하면 202를 받은 뒤 몇 초 만에 failed가 됐다. 체크 통과를 기다려 주는 기존 자동 머지(auto-merge)와는 다르게 움직인다.
  • 체크가 다 통과한 PR도 앱 토큰으로는 큐에 넣지 못했다("Enqueuer is not authorized to merge"). 같은 토큰으로 GraphQL enqueuePullRequest는 됐다.
  • 같은 요청을 두 번 보냈더니 둘 다 202였다고 한다. 문서가 말하는 409는 보지 못했다고.

한 개발자의 한 저장소 결과이고 본인도 정식 출시 초기의 빈틈일 수 있다며 한 달 뒤 재시험을 예고했으니, 이걸로 API 전체를 판단할 수는 없다. 그래도 이전 작업에 앞서 검증할 항목은 이 보고와 문서에서 그대로 뽑아낼 수 있다.

  1. 대상 브랜치에 머지 큐가 있는지부터 본다. 있다면 CI 워크플로가 merge_group 이벤트에서도 도는지 확인한다. 이 이벤트를 빠뜨리면 큐 안에서 필수 체크가 보고되지 않아 머지가 실패한다고 문서에 적혀 있다.
  2. 체크가 아직 도는 PR은 비동기 API로 보내지 말고 기존 자동 머지에 맡기거나, 체크 완료 웹훅을 받은 뒤 요청한다.
  3. enqueued를 성공으로 다루되, 최종 머지는 GET .../merge나 pull_request closed 웹훅으로 따로 추적한다.
  4. 중복 요청 방지를 서버의 409에만 기대지 말고, 봇 쪽에서 PR별 진행 중 UUID를 기억한다.
  5. 토큰 권한을 Contents 쓰기로 맞추고, bypass_rules는 기본값(false)으로 둔다. 우회 권한이 있는 계정이라도 봇에서 켜 두면 사람이 보는 보호 규칙을 조용히 건너뛰게 된다.

개인적으로는 큐를 쓰지 않는 작은 저장소일수록 바꿀 이유가 더 적다고 본다. 동기 /merge가 당장 사라지는 것도 아니고, 이득이 큰 쪽은 머지 큐나 스택 PR을 쓰는 팀이다. 스택 PR은 7월 30일 공개 프리뷰로 나왔고, 맨 위 PR을 머지하면 아래층까지 한 번에 들어간다. API로 이걸 하려면 이제 비동기 엔드포인트밖에 없다.

머지 권한 관련 보안 이력

비동기 머지 API 자체의 취약점 보고는 찾지 못했다. 다만 머지 권한을 우회한 사례는 최근에도 있었다. 2월 공개된 CVE-2026-1999는 GitHub Enterprise Server의 GraphQL enable_auto_merge 뮤테이션 권한 검사 오류다. 푸시 권한이 없는 공격자가 포크에서 연 자기 PR을 머지할 수 있었다(CVSS 4.0 기준 7.1). 포크를 허용하고 브랜치 보호 규칙이 없는 브랜치에서만 성립했고, 3.17.11·3.18.5·3.19.2에서 고쳐졌다. 이보다 낮은 GHES를 쓰고 있다면 업그레이드가 먼저다.

7월 말 깃허브 커뮤니티에는 클래식 브랜치 보호의 "머지 큐 필수" 설정을 켜 둔 브랜치에서, 우회 권한 없는 쓰기 권한 계정이 동기 /merge로 큐를 건너뛰고 머지했다는 보고가 올라왔다. 깃허브 직원의 답변은 아직 없고, 작성자는 저장소 규칙(ruleset)으로 옮겨 해결하겠다고 했다. 머지 큐를 필수로 운영한다면 보호 설정을 ruleset 쪽에 두는 편이 안전해 보인다. 비동기 API의 bypass_rules가 "우회가 허용된 규칙만" 건너뛰도록 명시돼 있는 것도 같은 맥락에서 읽힌다.

이 글의 API 설명 일부는 GitHub Docs(docs.github.com)의 내용을 바탕으로 번역·재구성했으며, 원문은 CC BY 4.0 라이선스를 따릅니다.

출처 github.blog/…/2026-10-01-github-async-merge-api-generally-availabledocs.github.com/…/pullsdocs.github.com/…/permissions-required-for-fine-grained-personal-access-tokensdocs.github.com/…/managing-a-merge-queuegithub.blog/…/2026-07-30-stacked-pull-requests-are-now-in-public-previewgithub.com/…/46667github.com/…/697nvd.nist.govgithub.com/…/203246

내용이 틀렸나요? 정정 요청

관련 글