[Automation] Gitlab에서 Github으로 일괄 마이그레이션 하기
개요
운영중인 모든 Repository를 Github Cloud로 이관해야하는 일이 발생하였다.
Github 측에서 별도의 유료 서비스 형태로 mirgration tool을 제공하나 기술지원도 같이 이용해야 하기에 공수시간과 비용이 많이 발생하는 점을 확인하였다.
어차피 CI/CD툴은 젠킨스로 이용중에 있었고 레파지토리 정보(Branch, Tag, Commit) 정보만 옮기는 것이 우선이였기에
git clone --bare 옵션과 git push -mirror 명령어를 통해 이관을 진행하기로 하였다.
수백개 프로젝트 대상으로 일괄적으로 마이그레이션을 진행하기 위해서는 코드 레벨에서 해결이 필요하였다.
Workflow
- GitLab 및 GitHub의 URL, 토큰 설정.
- 작업 디렉토리 설정 (실행파일 디렉토리)
2. 저장소 정의
마이그레이션할 저장소를 그룹화
3. GitHub 저장소 생성
create_github_repo 함수를 통해 GitHub 저장소 생성.
4. GitLab 저장소 클론
clone_and_push 함수에서 "git clone --bare" 를 통한 GitLab 저장소 클론.
5. GitHub에 저장소 푸시
클론된 저장소 디렉토리로 이동 후, "git remote add"와 "git push --mirror" 명령어를 사용하여 GitHub에 푸시.
6. 마이그레이션 시간 기록 및 실패한 저장소 기록
각 저장소의 마이그레이션 시간을 기록하고 실패한 저장소 기록.
7. 결과 출력
모든 저장소에 대한 마이그레이션 시간과 실패한 저장소 목록을 출력.
유의사항
1. Gitlab의 'Group'은 GitHub의 Organization에 매치된다.
Gitlab은 'Sub-group'의 형태로 Group 밑에 하위 그룹을 두고, 여기에 프로젝트들을 별도로 관리할 수 있지만, GitHub은 sub-organization의 개념은 없다.
GitHub은 아래와 같이 크게 3단계로 '계위'가 있다고 보면 된다. 제일 상위가 Enterprise Account, 그 밑에 여러개의 Organization을 생성할 수 있고, 실제 프로젝트는 각 Org 밑에 위치하게 된다.
2. Role에 대한 권한 명칭 차이점
[+] https://developer.mozilla.org/en-US/blog/migrating-from-github-to-gitlab-seamlessly-a-step-by-step-guide/
사전준비: Gitlab Project 경로 리스트업
Step 1. gitlab-rails console 접속
gitlab-rails console
Step 2. 리스트 출력
Project.all.each { |project| puts "ID: #{project.id}, Name: #{project.name}, Path: #{project.path_with_namespace}" }
출력 예시
#<Project id:396 groupA/example_projectA>>,
#<Project id:466 groupA/example_projectB>>,
#<Project id:411 groupB/example_projectC>>,
#<Project id:473 groupB/example_projectD>>]
Code
import os
import subprocess
import requests
import time
# 초기 설정
GITLAB_URL = 'your_gitlab_url'
GITLAB_TOKEN = 'your_gitlab_token'
GITHUB_ENTERPRISE_TOKEN = 'your_github_enterprise_token'
WORKING_DIR = '<<코드 실행 위치>>' #예시 C:\\Users\\User\\Desktop\\Python
# 저장소 정의
groupA_repositories = {
'org': 'group-a',
'repos': [
'groupA/repo1',
'groupA/repo2',
'groupA/repo3',
]
}
groupB_repositories = {
'org': 'group-b',
'repos': [
'groupB/repo1',
'groupB/repo2',
'groupB/repo3',
]
}
# 저장소 통합
ALL_REPOSITORIES = [
groupA_repositories,
groupB_repositories
]
# 윈도우 환경에서 명령어 실행
def run_command(command, cwd=None):
try:
result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True, cwd=cwd)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr)
return result
except subprocess.CalledProcessError as e:
print(f"명령어 '{e.cmd}' 실행 실패 (코드: {e.returncode})")
print(e.stderr)
raise
# GitHub 저장소 생성
def create_github_repo(org_name, repo_name):
url = f'https://api.github.com/orgs/{org_name}/repos'
headers = {
'Authorization': f'token {GITHUB_ENTERPRISE_TOKEN}',
'Accept': 'application/vnd.github.v3+json'
}
data = {
'name': repo_name,
'private': True
}
response = requests.post(url, headers=headers, json=data)
if response.status_code == 201:
print(f"GitHub 저장소 {repo_name} 생성 성공 (조직: {org_name}).")
elif response.status_code == 422:
print(f"GitHub 저장소 {repo_name} 이미 존재 (조직: {org_name}).")
else:
print(f"GitHub 저장소 {repo_name} 생성 실패 (조직: {org_name}): {response.content}")
# GitLab 저장소를 클론 & GitHub에 푸시
def clone_and_push(repository, org_name):
gitlab_repo_url = f'https://oauth2:{GITLAB_TOKEN}@{GITLAB_URL}/{repository}.git'
project_name = repository.split('/')[-1]
github_repo_name = project_name
github_repo_url = f'https://{GITHUB_ENTERPRISE_TOKEN}@github.com/{org_name}/{github_repo_name}.git'
repo_path = os.path.join(WORKING_DIR, f'{project_name}.git')
if os.path.exists(repo_path):
print(f"기존 디렉토리 제거: {repo_path}")
run_command(f'rmdir /S /Q {repo_path}', cwd=WORKING_DIR)
try:
create_github_repo(org_name, github_repo_name)
print(f"GitLab 저장소 클론 중: {gitlab_repo_url}")
run_command(f'git clone --bare {gitlab_repo_url}', cwd=WORKING_DIR)
if not os.path.exists(repo_path):
raise FileNotFoundError(f"클론 후 디렉토리 없음: {repo_path}")
os.chdir(repo_path)
print(f"GitHub 원격 URL 설정: {github_repo_url}")
run_command(f'git remote add github {github_repo_url}')
print(f"GitHub에 푸시 중: {github_repo_url}")
run_command(f'git push --mirror github')
os.chdir(WORKING_DIR)
run_command(f'rmdir /S /Q {repo_path}', cwd=WORKING_DIR)
print(f"{repository}의 GitHub 마이그레이션 성공 (조직: {org_name}).")
return True
except subprocess.CalledProcessError as e:
print(f"{repository}의 GitHub 마이그레이션 실패 (조직: {org_name}): {e}")
return False
except FileNotFoundError as e:
print(e)
return False
# 레포 마이그레이션
def migrate_repositories():
migration_times = {}
unsuccessful_migrations = []
start_time_total = time.time()
for repo_group in ALL_REPOSITORIES:
org_name = repo_group['org']
repos = repo_group['repos']
for repository in repos:
start_time = time.time()
success = clone_and_push(repository, org_name)
end_time = time.time()
total_time = end_time - start_time
migration_times[repository] = total_time
print(f"{repository} 마이그레이션 소요 시간: {total_time} 초")
if not success:
unsuccessful_migrations.append(repository)
end_time_total = time.time()
total_migration_time = end_time_total - start_time_total
print(f"Group Repositroy 마이그레이션 총 소요 시간: {total_migration_time} 초")
return migration_times, unsuccessful_migrations
if __name__ == '__main__':
migration_times, unsuccessful_migrations = migrate_repositories()
print("프로젝트별 소요된 마이그레이션 시간:")
for repo, duration in migration_times.items():
print(f"{repo}: {duration} 초")
if unsuccessful_migrations:
print("마이그레이션에 실패한 저장소:")
for repo in unsuccessful_migrations:
print(repo)