Automation/Github

[Automation] Gitlab에서 Github으로 일괄 마이그레이션 하기

[앙금빵] 2024. 6. 30. 20:41

개요

운영중인 모든 Repository를 Github Cloud로 이관해야하는 일이 발생하였다.
Github 측에서 별도의 유료 서비스 형태로 mirgration tool을 제공하나 기술지원도 같이 이용해야 하기에 공수시간과 비용이 많이 발생하는 점을 확인하였다.
 
어차피 CI/CD툴은 젠킨스로 이용중에 있었고 레파지토리 정보(Branch, Tag, Commit) 정보만 옮기는 것이 우선이였기에
git clone --bare 옵션과 git push -mirror 명령어를 통해 이관을 진행하기로 하였다.
 
수백개 프로젝트 대상으로 일괄적으로 마이그레이션을 진행하기 위해서는 코드 레벨에서 해결이 필요하였다.


Workflow

 
1. 초기 설정
- 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)