랜딩 스택 PR을 기본 브랜치로 병합하기
여러 PR이 체인으로 연결되어 있고(PR N이 PR N-1의 브랜치를 기반) 실제 기본 브랜치에 도달해야 할 때 사용합니다. 핵심 원칙: 중간 브랜치 CI는 게이트가 아닙니다. 기본 브랜치의 보호 규칙이 게이트입니다. 따라서 각 계층의 기본 브랜치를 적시에 재지정하고 네이티브 CI가 검증하도록 합니다. 중간 브랜치 이름을 CI 트리거에 하드코딩하는 방식에 절대 의존하지 마십시오.
저장소: gh에 -R owner/repo를 전달하거나 클론에서 실행합니다(gh repo view --json nameWithOwner -q .nameWithOwner로 해석). 아래에서 R은 owner/repo입니다.
원칙
- 병합 순서는 실제 종속성(콘텐츠 종속성 + 둘 이상의 계층이 편집한 파일)에 의해 결정됩니다. 먼저 그 순서를 정하십시오. 편의를 위해 재정렬하지 마십시오.
- 각 계층은 기본 브랜치로만 한 번에 하나씩 병합합니다. 자식을 이전의 필수 브랜치로 병합하지 마십시오.
- 계층의 CI가 작동하기 전에 현재 기본 브랜치를 해당 계층에 병합하여 diff가 자체 변경 사항만 포함하고 이미 병합된 모든 내용을 반영하도록 합니다.
- 팁 브랜치에는 모든 계층이 누적되어 있습니다. 팁(기본 브랜치로 재지정됨)에서 한 번의 CI 실행으로 전체 스택이 검증됩니다. 선택적 사전 점검으로 사용할 수 있습니다.
0단계: 순서 설정 및 충돌 감지
저장소의 실제 기본 브랜치를 확인합니다(git 리모트의 기본 브랜치, 예: develop 또는 main; 사용자의 에이전트 명령 파일(AGENTS.md / CLAUDE.md)에 알려진 기본 브랜치가 명시되어 있을 수 있음). 각 PR에 대해 기본 브랜치와의 충돌을 검사합니다. (오래된 스택 기본 브랜치와 검사하면 CLEAN으로 나와서 오해할 수 있습니다):
git fetch origin <base> <head> --quiet
git merge-tree --write-tree origin/<base> origin/<head> | grep -iE 'CONFLICT|changed in both' || echo "clean vs base"
순서 경험 법칙: 게이트가 필요로 하는 필수 콘텐츠를 먼저 병합합니다(예: lint-cleanup을 lint-enable보다 먼저). 두 계층이 같은 파일을 편집한 경우, 먼저 편집한 계층이 먼저 병합됩니다.
1단계..N단계: 순서대로 각 계층 처리
PR=<number>
gh pr edit $PR -R $R --base <base> # 스택 브랜치에서 재지정
# 별도 작업 트리를 사용하여 메인 체크아웃을 깨끗하게 유지:
WT=../<repo>-<head> # 메인 체크아웃 외부의 아무 경로
git worktree add "$WT" <head> 2>/dev/null || true
git -C "$WT" fetch origin --quiet
git -C "$WT" merge origin/<base> # 기본 브랜치 동기화; 충돌 해결(아래 참조)
git -C "$WT" push
gh pr ready $PR -R $R # 활성 계층일 때
gh pr checks $PR -R $R --watch # 네이티브 CI, base=<base>
# 리뷰 승인 후에만:
gh pr merge $PR -R $R --squash --delete-branch
# 그런 다음 다음 계층으로 이동(재지정+기본 동기화가 이 계층을 흡수함)
GitHub 네이티브 스택(merge-async)
GitHub의 네이티브 스택 기능으로 만든 PR은 위의 일반적인 단계를 거부합니다:
gh pr edit --base:GraphQL: Cannot change the base branch because the pull request is part of a stack. (updatePullRequest)gh pr merge --auto:GraphQL: Auto-merge is not supported for stacked pull requests.gh pr merge:This pull request is part of a stack and must be merged using the asynchronous merge REST API.
비동기 API를 사용하며, 하위 PR부터 먼저 처리합니다(GitHub가 자동으로 다음 PR의 기본 브랜치를 재지정하며 약 30초 소요):
sha=$(gh pr view $PR -R $R --json headRefOid -q .headRefOid)
u=$(gh api -X PUT repos/$R/pulls/$PR/merge-async -f merge_method=squash -f sha=$sha \
-f merge_action=direct_merge --jq '.details.uuid // .status')
gh api repos/$R/pulls/$PR/merge-async/$u --jq '.status+" "+(.details.message//"")' # "pending"이 사라질 때까지 폴링
- 이 방식으로 스택 PR을 병합하면 그 아래의 모든 열려 있는 PR도 병합되므로 실제로 랜딩하려는 계층만 병합하십시오.
- 각 스쿼시 후, 다음 계층은 종종 양쪽이 수정한 파일(ADR, 버전 목록)에서 DIRTY 상태가 됩니다. 기본 브랜치를 병합하고, 해결하고, 푸시하고, CI를 기다린 후 merge-async를 수행합니다.
- 승인은 오래된 리뷰를 dismiss하지 않는 저장소에서 기본 동기화를 유지합니다(브랜치 보호에서
required_pull_request_reviews.dismiss_stale_reviews확인). 실제 게이트는 종종 각 수정 경로에 대한 CODEOWNERS 승인이며, 승인 수가 아닙니다(pr-approval-gate에서 격차를 나열). - 모든 열려 있는 PR을 랜딩 사이에 기본 브랜치와 최신 상태로 유지하려면 기본 브랜치를 각 열려 있는 PR 브랜치에 병합하고 푸시하는 기본 동기화 패스를 실행하십시오(일반 병합, 강제 푸시 금지).
충돌 해결
- 여러 계층이 수정한 워크플로 파일(재사용 가능한 빌드 워크플로, CI 오케스트레이터 워크플로): 한쪽을 선택하지 말고 양쪽 편집을 모두 유지하십시오.
- 포맷 스윕 충돌(Spotless/Prettier 적용 스윕 계층과 기본 브랜치의 콘텐츠 변경):
충돌 파일의 기본 브랜치 콘텐츠를 가져온 후 해당 파일에 대해 포맷터를 다시 실행하십시오(
mvn -pl <module> -am spotless:apply또는pnpm fmt). 스윕 쪽을 선택하면 실제 변경 사항이 손실됩니다. 재포맷 없이 기본 브랜치를 가져오면 포맷 게이트를 통과하지 못합니다. 포맷터 실행은 빌드를 해결해야 합니다(일부 저장소는 개인 레지스트리에서 종속성을 가져오며 클라우드 로그인이 필요합니다. 빌드가 종속성을 가져오지 못하면 새로 고치십시오).
안전
- 강제 푸시, 기록 변경, 보호된 브랜치에 푸시하지 마십시오.
- 저장소의 커밋 작성자 규칙을 따르십시오(일부는 AI 공동 작성자 트레일러를 금지함).
- 기본 브랜치에 병합하기 전에 인간 리뷰 승인을 위해 일시 중지하십시오.
- 중간 브랜치 CI 허용 목록 PR이 있는 경우(이 스킬이 대체하는 안티패턴), 닫는 것을 권장합니다. 이미 병합된 경우 추가된 브랜치 이름과 이벤트 유형을 되돌리십시오.
관련 스킬
opening-a-stacked-pr-sequence: 스택 열기(기본 브랜치, 병합 순서) 및 병합 유형별 리스택 수정(stack.sh restack).pr-approval-gate: 각 계층이 아직 필요한 코드 소유자.resolve-pr-threads-and-conflicts-and-merge: 스레드와 승인이 완료된 후 한 계층을 랜딩.