백포트 시스템
이 문서에서는 ClickHouse의 백포트 정책과 이를 구현하는 자동화 시스템을 설명합니다.
릴리스 모델
ClickHouse 버전은 YY.M.patch.build-type 형식을 따릅니다. 여기서 YY는 두 자리 연도, M은 릴리스 월(앞에 0 없음), patch는 해당 브랜치의 패치 번호, build는 단조롭게 증가하는 빌드 번호이며, type은 stable 또는 lts입니다.
예시: 25.3.8.23-lts — 2025년 3월 LTS, 패치 8, 빌드 23.
릴리스 트랙은 두 가지입니다:
- Stable 릴리스는 대략 매월 제공됩니다. 가장 최근의 stable 릴리스 3개에 패치가 제공되므로, 각 릴리스는 약 3개월간 활성 지원을 받습니다.
- LTS (Long-Term Support) 릴리스는 매년 3월과 8월에 제공됩니다. 두 개의 LTS 버전이 동시에 지원되며, 각각 최소 12개월 동안 지원됩니다.
프로덕션 워크로드를 실행하는 사용자는 최신 stable 릴리스 또는 LTS 릴리스를 사용하는 것이 권장되며, 패치 릴리스에는 호환성을 깨뜨리는 변경이 포함되지 않으므로 새 패치 버전으로 신속히 업그레이드하는 것이 좋습니다.
백포트 정책
모든 변경이 백포트되는 것은 아닙니다. 목표는 릴리스 브랜치의 안정성을 유지하는 것이므로, 백포트 범위는 의도적으로 제한적으로 운영됩니다.
- 보안 수정 사항 — 항상 백포트됩니다.
- 치명적인 버그 수정 (예외(논리 오류), 데이터 손실, 잘못된 결과, RBAC 문제) — 일반 백포트 규칙에 따라 자동으로 백포트 대상으로 선정됩니다.
pr-critical-bugfix레이블로 식별되며, 이 레이블이 있으면pr-must-backport가 자동으로 추가됩니다. - 안정성 및 회귀 수정 — 변경의 위험이 버그를 그대로 두는 위험보다 낮은 경우 백포트됩니다. 유지 관리자가 수동으로 추가한
pr-must-backport레이블로 식별됩니다. - 우회 방법이 있는 경미한 버그 수정 — 일반적으로 릴리스 브랜치의 안정성을 해치지 않기 위해 백포트되지 않습니다.
- 새 기능, 개선 사항, 성능 관련 작업 — 백포트되지 않습니다.
pr-must-backport 레이블은 유지 관리자가 PR을 백포트 대상으로 지정할 때 사용하는 수동 재정의 레이블입니다. pr-critical-bugfix 레이블이 있으면 CI hook이 pr-must-backport를 자동으로 추가합니다(pr_labels_and_category.py 참조).
충돌 에스컬레이션. 자동 백포트로 병합 충돌을 해결할 수 없는 경우에도 체리픽 PR은 반드시 생성해야 하며, 사람이 충돌을 해결하고 백포트를 완료할 수 있도록 원래 PR의 작성자, 병합자, 기존 담당자에게 할당해야 합니다.
Backport 도구
위에서 설명한 백포트 정책은 tests/ci/cherry_pick.py의 자동화 도구로 구현되어 있습니다. 이 도구는 ClickHouse 인프라에서 GitHub Actions 워크플로로 실행되며, 활성 릴리스 브랜치 탐지, 백포트 대상 PR 선택, 2단계 체리픽 및 백포트 절차 수행, 충돌 관리, 지연 정책 적용, 레이블을 동기화된 상태로 유지하는 작업까지 모든 요구 사항을 충족합니다.
장기적인 목표는 이 구현을 다른 프로젝트에서도 채택할 수 있는 독립 실행형 오픈소스 Python 도구로 분리하는 것입니다. 목표 설계는 다음과 같습니다.
- Configurable — 모든 정책 매개변수(대상 레이블, 지연 기간, 오래된 PR 임곗값, 롤아웃 동작 등)를 설정 파일로 표현하여, 코드 변경 없이도 어떤 프로젝트의 백포트 요구 사항에도 맞게 도구를 조정할 수 있도록 합니다.
- Distributable — ClickHouse의 CI 인프라에 의존하지 않으며, PyPI에서 설치할 수 있는 자체 완결형 Python wheel 패키지로 제공합니다.
- Programmable — 사용자가 코어 엔진 위에서 사용자 지정 워크플로를 스크립트로 작성할 수 있도록 pull request, 레이블, 릴리스 브랜치에 대한 명확한 객체 모델을 제공합니다.
테스트
독립 실행형 도구의 계획된 구성 요소 중 하나는 전용 테스트 스위트와 경량 테스트 인프라입니다. 이 인프라는 다음 항목이 미리 준비된 임시 GitHub 저장소(또는 이에 상응하는 로컬 환경)를 생성할 수 있습니다.
- 릴리스 줄을 나타내는 구성 가능한 브랜치 집합
- 다양한 조합의 backport 레이블이 지정된 pull request
- 릴리스 브랜치를 가리키는
release레이블이 있는 릴리스 PR
이를 통해 프로덕션 상태에 영향을 주지 않으면서도 실제와 같지만 폐기 가능한 저장소를 대상으로 전체 자동화 루프(레이블 감지, 체리픽 브랜치 생성, 충돌 처리, backport PR 생성, 담당자 로직, 롤아웃 건너뛰기, 지연 정책)를 테스트할 수 있습니다. 또한 동일한 인프라를 사용해 정책 변경을 배포하기 전에 회귀 테스트를 수행할 수도 있습니다.
활성 릴리스 브랜치
활성 릴리스 브랜치는 해당 릴리스 PR(release 레이블이 지정됨)이 GitHub에서 아직 열려 있는 브랜치를 의미합니다. 백포트 자동화는 실행할 때마다 이를 동적으로 감지하므로, 새 릴리스가 생성되거나 기존 릴리스의 지원이 종료되더라도 설정 변경이 필요하지 않습니다.
릴리스 브랜치는 새 릴리스가 배포되는 기간 동안 롤아웃 중 상태(해당 릴리스 PR에 rolling-out 레이블이 지정됨)일 수 있습니다. 롤아웃을 복잡하게 만들지 않기 위해 롤아웃 중인 브랜치에 대해서는 일반 백포트를 일시 중지합니다. 버전별 레이블(예: v25.3-must-backport)은 이 동작을 재정의하여 롤아웃 중에도 백포트를 강제합니다.
구현
개요
백포트 자동화는 .github/workflows/cherry_pick.yml의 CherryPick GitHub Actions 워크플로로 매시간 실행되며, tests/ci/cherry_pick.py에 구현되어 있습니다. 이 자동화는 GitHub API와 셀프 호스팅 style-checker-aarch64 러너에서 수행되는 로컬 git 작업을 통해 동작합니다.
이 프로세스는 각 (원본 PR, 릴리스 브랜치) 쌍에 대해 2단계로 진행됩니다:
- 실제 병합 대상과 충돌 해결을 분리하기 위해 체리픽 PR이 생성됩니다. 충돌이 없으면 자동으로 병합됩니다.
- 실제 릴리스 브랜치를 대상으로 backport PR이 생성되며, 체리픽된 변경 사항은 단일 커밋으로 스쿼시됩니다.
레이블
원본 PR에 붙은 레이블은 백포트 수행 여부와 수행 대상 브랜치를 제어합니다.
| 레이블 | 효과 |
|---|---|
pr-must-backport | 모든 활성 릴리스 브랜치에 백포트합니다(rolling-out으로 표시된 브랜치는 제외) |
pr-must-backport-force | rolling-out 제한을 무시하고 모든 활성 릴리스 브랜치에 백포트합니다 |
pr-critical-bugfix | pr_labels_and_category.py의 AUTO_BACKPORT를 통해 pr-must-backport를 자동으로 트리거합니다 |
v{VER}-must-backport (예: v25.3-must-backport) | 해당 특정 릴리스 브랜치에만 백포트합니다. 해당 브랜치에서는 rolling-out 제외 규칙보다 우선합니다 |
pr-backports-created | 필요한 모든 백포트 PR이 생성되면 봇이 설정하며, 체리픽 PR이 다시 열리면 해제됩니다 |
pr-cherrypick | 봇이 생성한 체리픽 PR에 적용됩니다 |
pr-backport | 봇이 생성한 백포트 PR에 적용됩니다 |
do not test | CI가 실행되지 않도록 체리픽 PR에 적용됩니다 |
rolling-out | 해당 브랜치가 현재 롤아웃 중임을 나타내기 위해 릴리스 PR에 설정됩니다. 일반 백포트에서는 이 브랜치를 건너뜁니다 |
브랜치 및 PR 이름 지정
각 원본 PR 번호 N과 릴리스 브랜치 release/X.Y에 대해:
- 체리픽 브랜치:
cherrypick/release/X.Y/N - 백포트 브랜치:
backport/release/X.Y/N - 체리픽 PR 제목:
Cherry pick #N to release/X.Y: <original title> - 백포트 PR 제목:
Backport #N to release/X.Y: <original title>
단계별 프로세스
1. 활성 릴리스 확인
BackportPRs.receive_release_prs는 GitHub에서 release 레이블이 붙은 열린 PR을 모두 조회합니다. 이러한 PR의 head ref는 릴리스 브랜치 이름입니다(예: release/25.3). 여기서 호환성 레이블 세트가 파생됩니다. 예를 들면 v25.3-must-backport 등이 있습니다.
2. 백포트할 PR 찾기
BackportPRs.receive_prs_for_backport는 GitHub search API를 사용하여 다음 조건을 충족하는 병합된 PR을 찾습니다.
- 하나 이상의 백포트 레이블(
pr-must-backport,pr-must-backport-force,pr-critical-bugfix또는 버전별 레이블)이 지정되어 있고, - 아직
pr-backports-created레이블이 없고, - 모든 릴리스 브랜치에서 확인된 가장 오래된 커밋 날짜 이후에 병합되었고,
- 최근 90일 이내에 업데이트되었습니다(검색 쿼리의 효율을 유지하기 위해).
3. 롤링아웃 브랜치 처리
릴리스 PR에 rolling-out 레이블이 있으면 일반 백포트 레이블(pr-must-backport, pr-critical-bugfix)은 해당 브랜치를 건너뜁니다. 봇은 해당 브랜치에 대해 이전에 생성된 체리픽 또는 백포트 PR을 설명 댓글과 함께 닫습니다. 버전별 레이블(예: v25.3-must-backport)은 항상 이 동작을 재정의합니다. pr-must-backport-force는 모든 브랜치에서 rolling-out 검사를 우회합니다.
4. 체리픽 단계 (ReleaseBranch.create_cherrypick)
아직 체리픽 PR이 없는 각 (원본 PR, 릴리스 브랜치) 쌍에 대해 다음을 수행합니다.
- 릴리스 브랜치를 체크아웃한 다음, 해당 브랜치에서 백포트 브랜치(
backport/release/X.Y/N)를 생성합니다. - merge commit의 첫 번째 부모를 대상으로
git merge -s ours를 수행하여, 내용 변경이 없는 합성 merge base를 생성합니다. - 원본 PR의 merge commit을 직접 가리키도록 체리픽 브랜치(
cherrypick/release/X.Y/N)를 강제로 생성합니다. - 백포트 브랜치에 체리픽 브랜치를 병합하기 위해
git merge --no-commit --no-ff를 시도합니다.- 이미 최신 상태라면 해당 변경이 이미 릴리스 브랜치에 포함되어 있으므로 완료로 표시하고 건너뜁니다.
- 그렇지 않다면(충돌 여부와 무관하게) reset한 뒤 두 브랜치를 모두 push합니다.
cherrypick/release/X.Y/N에서backport/release/X.Y/N을 대상으로 하는 체리픽 PR을 생성하고,pr-cherrypick및do not test라벨을 지정합니다.- 해당하는 경우 원본 PR의
pr-bugfix또는pr-critical-bugfix라벨도 전달합니다. - 이 시점에서는 담당자(assignee)를 설정하지 않으며, 충돌이 감지된 경우에만 추가합니다.
5. 충돌 없는 체리픽 PR의 자동 병합
체리픽 PR이 병합 가능한 상태라면(충돌이 없다면), 봇이 GitHub API를 통해 자동으로 병합한 뒤 바로 백포트 단계로 진행합니다.
6. 백포트 단계 (ReleaseBranch.create_backport)
체리픽 PR이 병합된 후:
- 백포트 브랜치를 체크아웃한 다음 pull합니다.
- 릴리스 브랜치와 백포트 브랜치 간의 merge-base를 찾습니다.
- merge-base로
git reset --soft를 수행하여 체리픽된 모든 커밋을 하나로 합칩니다. - 백포트 PR 제목을 메시지로 사용해 커밋합니다.
- 백포트 브랜치를 force-push한 뒤 실제 릴리스 브랜치를 대상으로 백포트 PR을 엽니다.
- PR에
pr-backport레이블을 지정합니다(해당하는 경우pr-bugfix/pr-critical-bugfix도 지정). - PR을 원본 PR의 저자, 병합한 사용자, 기존 담당자에게 할당합니다(로봇 계정 제외).
7. 완료
특정 원본 PR의 모든 릴리스 브랜치에 대한 백포트가 완료되면 봇이 원본 PR에 pr-backports-created를 추가합니다.
8. 사전 확인
PR 작업을 시작하기 전에 ReleaseBranch.pre_check는 git merge-base --is-ancestor를 실행하여 merge 커밋이 release 브랜치에 이미 포함되어 있는지 확인합니다. 이미 포함되어 있으면 해당 PR은 이미 백포트된 것으로 간주하고 건너뜁니다.
오래된 Cherry-pick PR 처리
CherryPickPRs class는 매시간 실행이 시작될 때마다 다음 두 가지 시나리오를 처리합니다:
- 고아 체리픽 PR: 체리픽 PR의 릴리스 브랜치에 더 이상 열려 있는 릴리스 PR이 없으면(즉, 릴리스가 종료된 경우), 해당 체리픽 PR은 자동으로 닫힙니다.
- 다시 열린 체리픽 PR: 원본 PR에 이미
pr-backports-created레이블이 있지만 해당 체리픽 PR이 여전히 열려 있으면, 원본 PR에서pr-backports-created레이블을 제거하여 다시 처리할 수 있게 합니다.
수동 충돌 해결을 기다리는 체리픽 PR의 경우:
- 3일 동안 업데이트가 없으면, 봇이 담당자를 멘션하는 리마인드 댓글을 게시합니다.
- 7일 동안 업데이트가 없으면, 봇이 종료 안내 댓글을 게시하고 PR을 닫습니다.
충돌 해결
체리픽 중 충돌이 발생하면 수동으로 해결할 수 있도록 체리픽 PR은 열린 상태로 남겨 둡니다. 봇은 이 PR을 원본 PR의 저자, 병합한 사람, 그리고 담당자에게 할당합니다. 충돌을 해결한 뒤 체리픽 PR이 병합되면 봇은 다음 매시 실행 시 백포트 PR을 생성합니다.
백포트를 완전히 폐기하려면 체리픽 PR을 닫으십시오. 봇은 이를 의도적으로 건너뛴 것으로 간주합니다.
손상된 체리픽 PR을 처음부터 다시 생성하려면:
- 체리픽 PR에서
pr-cherrypick레이블을 제거합니다. cherrypick/...브랜치를 삭제합니다.- 원본 PR에
pr-backports-created가 있으면 제거합니다.
Backport PR용 CI
Backport PR는 릴리스 브랜치를 대상으로 하므로, 표준 pull request 워크플로 대신 전용 CI 워크플로(BackportPR, ci/workflows/backport_branches.py에 정의)를 사용합니다. 이 워크플로는 CI의 대표적인 일부만 실행합니다. 여기에는 ASan/UBSan 및 TSan 빌드, 릴리스 빌드, macOS 빌드, ASan 환경에서의 기능 테스트, TSan 환경에서의 스트레스 테스트, 그리고 통합 테스트가 포함됩니다. 또한 백포트 브랜치에 커밋이 1개 이상 50개 이하인지, 그리고 변경된 파일이 최소 1개 이상 있는지 검증합니다(check_backport_branch.py에서 강제 적용).
인증
이 워크플로에서는 Git push 작업에 SSH 키(ROBOT_CLICKHOUSE_SSH_KEY)를 사용합니다. GitHub API 호출은 get_best_robot_token을 통해 인증되며, 이 함수는 SSM(/github-tokens)에 저장된 풀에서 남은 할당량이 가장 많은 토큰을 선택합니다. ROBOT_CLICKHOUSE_COMMIT_TOKEN은 API 호출용이 아니라 Actions 워크플로의 checkout 단계에서 사용됩니다. 담당자를 지정할 때는 로봇 계정(robot-clickhouse, clickhouse-gh)을 제외합니다.
GitHub API 캐시
GitHubCache(cache_utils.py에 있음)는 PyGithub 객체 캐시를 S3에 저장하여 시간별 실행 전반의 API 호출 수를 줄입니다. 캐시는 각 실행이 시작될 때 다운로드되고, 종료될 때 업로드됩니다.
오류 처리
개별 PR를 처리하는 동안 발생한 오류는 포착되어 로그에 기록되지만, 전체 실행은 중단되지 않습니다. 모든 PR 처리가 끝난 뒤 오류가 하나라도 발생했다면 BackportException이 발생합니다. CI 환경에서는 이로 인해 CIBuddy를 통해 팀 채팅으로 알림이 전송됩니다.