merge-train
한 저장소에 열려 있는 본인 PR 들을 정해진 순서로
한 번에 하나씩 정리 · 머지하고, PR 당 한 줄짜리
[MERGED] / [SKIPPED] / [FAILED] 보고서를 남긴다.
/gh-pr:merge-trainmerge 단독이라면 거부했을 것을 머지하지 않는다.
/gh-pr:merge-emergency 는 절대 호출하지 않는다(NF-2).Skill() 로 체이닝한다 — 다른 하네스에서는 PR 하나씩 직접 실행한다.언제 쓰고 언제 안 쓰는가
쓴다
gh-flow:issue 가 병렬로 돌아 여러 개의 열린 PR 을 남겼을 때.
첫 PR 을 머지하면 나머지가 전부 BEHIND 가 되고, conflict 나 빨간 CI 가 겹치면 PR 마다 다른
정리 스킬을 골라 반복해야 한다 — 그 반복이 이 스킬의 존재 이유다. 큐를 무인으로 비울 때도(cron) 쓴다.
쓰지 않는다
- PR 이 하나뿐이면
/gh-pr:merge를 쓴다(정리가 필요하면/gh-resolve:outdated/:conflict/:ci-fail을 직접). PR 하나에 train 은 순수 오버헤드다. - 동료의 PR —
--author @me전용(D-7)이라 애초에 보이지 않는다. - admin 바이패스가 필요하거나 사람의 판단이 남아 있을 때 —
train 은
/gh-pr:merge-emergency를 호출하지 않고, 자체 리뷰 의견도 만들지 않는다.
호출 형식
/gh-pr:merge-train [owner/repo] [remote]
| # | 이름 | 기본값 | 설명 |
|---|---|---|---|
| 1 | [owner/repo] 또는 -h / --help / help |
[remote] 의 repo | 대상 저장소. cron 디스패처는 항상 명시적으로 넘긴다 |
| 2 | [remote] | origin |
호스트를 고정하는 git remote (slug 에는 호스트 정보가 없다) |
/gh-pr:merge-train # 이 체크아웃의 origin repo
/gh-pr:merge-train acme/dotfiles upstream # repo 명시, host 는 upstream 에서
/gh-pr:merge-train -h
동작 단계
-
Step 1 — 타깃 바인딩
TARGET_REPO/TARGET_HOST/GH_HOST를 같은 remote URL 하나에서 해석한 뒤에야gh를 호출한다(dEitY719/dotfiles#1403 / dEitY719/dotfiles#1407).owner/repo를 명시해도 호스트는 여전히 remote URL 에서 온다. -
Step 2 — 큐 수집과 정렬
gh pr list --author @me --state open(작성자 한정은 선택이 아니다, D-7) 결과를 공용 필터_gh_pr_merge_train_filter_targets에 통과시킨다 — draft,reply-pending라벨, 11분 quiet period(D-6) 안의 갱신을 떨어뜨리며, cron 디스패처와 문자 그대로 같은 함수다. 남은 PR 은CLEAN→BEHIND→UNSTABLE→DIRTY, 동순위는 번호 오름차순(D-2).gh pr list실패는 빈 보고서와 함께 런 종료다. -
Step 3 — base 별 승인 정책 읽기
rulesets 와 classic branch protection 양쪽에서
required_approving_review_count를, PR 마다가 아니라 서로 다른baseRefName마다 한 번씩 읽어 캐시한다(base 당 2콜). 한쪽이라도>= 1이면 게이트 ON, 양쪽 다 "정책 없음"이면 OFF(D-5). 판정 기준은 exit code 가 아니라 HTTP status —403/404는 "정책 없음"이고, 진짜 판정 불가(5xx / 401 / 무응답)만 fail-closed 로 게이트를 켠다(dEitY719/dotfiles#1519). -
Step 3.5 — 리뷰 verdict 게이트
Step 2 가 이미 들고 있는
labels만으로 판정한다(추가 API 콜 없음). 부재는 "통과"가 아니라 "미검증"이라는 것이 이 게이트의 전부다. 라벨의 유일한 작성자는gh-verify:review-all이며, 리뷰 코멘트 본문 파싱은 금지다. -
Step 4 — train 실행, PR 한 번에 하나씩
큐 순서대로 (1) 처리 직전 상태 재조회(F-3 — 앞의 머지가 뒤의 모든 상태를 무효화했다), (2) D-1 라우팅 테이블로 분기, (3) 필요하면 원자 스킬에 위임, (4) 재조회 후 재라우팅, (5)
Skill(gh-pr:merge, "<N>")(전략 인자 없음), (6) 머지 성공 시 그 PR 의 구현 탭이idle이면 닫는다(dEitY719/dotfiles#1565).BEHIND/DIRTY의 리베이스는 시도마다 만들고 반드시 제거하는 detached scratch worktree 안에서 돈다(dEitY719/dotfiles#1493). 시도는 PR 당 최대 3회(F-5), 실패하면 그 PR 만 건너뛰고 train 은 계속한다(F-6). -
Step 5 — 보고
PR 당 한 줄, 반드시 사유를 붙인 보고서(F-9). 헤더에 큐 크기와
approval gate:판정 문자열이 들어가며, 항상 일반 assistant 텍스트로 출력한다(Bashheredoc /Write금지).
Step 3.5 verdict 게이트 판정
| 라벨 상태 | 판정 |
|---|---|
review-blocked (낡은 review-passed 가 함께 있어도) |
SKIPPED review-blocked — reviewer verdict is blocking |
| 두 라벨 모두 부재 | SKIPPED review not verified — no review-passed label |
review-passed 단독 | 큐에 잔류 |
라우팅 테이블 (D-1)
references/routing-table.md 의 결정론적 표 — 판단이 필요한 두 행만 원자 스킬에 위임한다.
mergeStateStatus | mergeable | 동작 |
|---|---|---|
CLEAN | MERGEABLE | 바로 Skill(gh-pr:merge, "<N>") |
BEHIND | MERGEABLE | scratch worktree 에서 gh-resolve:outdated → 재조회 → 머지 |
DIRTY | CONFLICTING | scratch worktree 에서 gh-resolve:conflict → 재조회 → 머지 |
UNSTABLE | MERGEABLE | statusCheckRollup 을 보고 분기 |
BLOCKED | — | 사유를 기록하고 SKIPPED |
UNKNOWN | UNKNOWN | 폴링 후 재평가, 3회 폴링 후 SKIPPED |
DRAFT | — | SKIPPED |
표를 읽기 전에 먼저 확인하는 단락(short-circuit)과 UNSTABLE 분기
short-circuit — isDraft, reply-pending 라벨,
review-blocked 라벨, 두 verdict 라벨의 부재, review-passed 의 sha 신선도
(dEitY719/dotfiles#1601 — 마커 불일치 / 부재 / 조회 실패가 각각 다른 [SKIPPED] 사유).
UNSTABLE 은 rollup 으로 갈라진다 —
FAILURE / TIMED_OUT / CANCELLED / ACTION_REQUIRED
가 있으면 Skill(gh-resolve:ci-fail, "<N>"),
IN_PROGRESS / QUEUED / PENDING 뿐이면 기다린다
(아직 안 끝난 테스트를 "고치러" 가지 않는다), 전부 성공이면 재조회한다.
폴링은 최대 3회(약 30초 간격)이고 F-5 시도 횟수를 소모하지 않는다.
주의사항과 제약
DIRTY 가 마지막인 이유도 같다 — 가장 비싼 작업을 최종 base 를 상대로
정확히 한 번만 하려는 것이다.merge 단독이라면 거부했을 것을 머지하지 않는다.
gh-pr:merge 의 게이트(비어 있지 않고 APPROVED 도 아닌 reviewDecision)는
위임하기 전에 감지해 사유와 함께 [SKIPPED] 로 기록한다./gh-pr:merge-emergency 를 절대 호출하지 않는다(NF-2).
BLOCKED 에도, 승인 누락에도, "이번 한 번만"도 없다.gh pr list 실패)이다.
머지 전략 인자도 넘기지 않는다(D-4).reviewDecision 이 비었을 때만
Skill(gh-pr:approve, "<N> <remote> --self-record") 를 head 당 한 번 위임하고
보드를 읽어 판정으로 삼는다 — 승인이 없으면 머지도 없다. ai-metrics 코멘트도 쓰지 않는다.Skill() 로 체이닝하는데 Skill() 은 Claude Code 밖에 등가물이 없다.
다른 하네스에서는 train 대신 PR 하나씩 per-PR 스킬을 직접 실행한다:
상태에 맞는 /gh-resolve:outdated / :conflict / :ci-fail 후
/gh-pr:merge.merge-train/SKILL.md 는 148줄로 100줄 progressive-disclosure 한도를 넘긴다
(마이그레이션 중 동작 변경 금지라 그대로 넘어왔다). 해결책은 한도를 올리는 것이 아니라
references/ 로 상세를 옮기는 Phase 4 작업이다. 또한 dotfiles 의
shell-common/lib/gh_pr_merge_train.sh 등을 source 하므로, 체크아웃이 없는
머신에서는 해당 단계가 저하된다.관련 스킬
호출하는 원자 스킬
gh-resolve:outdated · gh-resolve:conflict ·
gh-resolve:ci-fail · gh-pr:approve(게이트 OFF 경로
--self-record 전용) · gh-pr:merge
의도적으로 호출하지 않는 것
gh-pr:merge-emergency — NF-2. 바이패스는 사람이 한 번씩 의도적으로 하는 행위다.
주변 스킬
PR 들의 생산자는 gh-flow:issue, verdict 라벨의 유일한 작성자는
gh-verify:review-all, 프로비저닝은 gh-setup:label-bootstrap.