commit d3b8270f41aeb71fd7bd9a27c2cb1116d1638a3e Author: Lectom C Han Date: Mon Jul 21 17:58:09 2025 +0900 깃헙 이슈 테스트 완료 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcf527e --- /dev/null +++ b/.gitignore @@ -0,0 +1,206 @@ +.env +.venv + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c3193a --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# GitHub 이슈 관리 스크립트 + +이 프로젝트는 CSV 파일을 사용하여 GitHub 이슈를 일괄 생성하고, 모든 이슈를 삭제하는 등의 관리 작업을 자동화하는 Python 스크립트 모음입니다. + +## 주요 기능 + +- **CSV로부터 이슈 일괄 생성**: `github_issues_from_csv.py` + - CSV 파일에 정의된 내용(제목, 본문, 라벨, 담당자, 마일스톤 등)을 바탕으로 GitHub 이슈를 자동으로 생성합니다. + - 필요한 라벨과 마일스톤이 없으면 자동으로 생성합니다. +- **모든 이슈 일괄 삭제**: `delete_all_issues.py` + - 리포지토리에 있는 모든 이슈(Open, Closed 상태 모두)를 영구적으로 삭제합니다. + - 삭제 전 사용자 확인 절차를 거칩니다. +- **샘플 CSV 파일 생성**: `generate_sample_csv.py` + - 이슈 생성 스크립트에서 사용할 수 있는 형식의 샘플 `github_issues.csv` 파일을 생성합니다. + +## 사전 준비 + +1. **Python 설치**: Python 3.6 이상이 필요합니다. +2. **필요 라이브러리 설치**: + ```bash + pip install requests + ``` +3. **GitHub 개인용 액세스 토큰(PAT) 발급**: + - GitHub에서 `repo` 스코프 권한을 가진 개인용 액세스 토큰을 발급받아야 합니다. [GitHub 문서 참고](https://docs.github.com/ko/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) + - 발급받은 토큰을 환경 변수에 설정합니다. + + ```bash + # Linux/macOS + export GITHUB_TOKEN='your_personal_access_token' + + # Windows (Command Prompt) + set GITHUB_TOKEN='your_personal_access_token' + ``` + +4. **스크립트 내 리포지토리 정보 수정**: + - `delete_all_issues.py`와 `github_issues_from_csv.py` 파일 상단에 있는 `REPO_OWNER`와 `REPO_NAME` 변수를 자신의 GitHub 리포지토리 정보에 맞게 수정해야 합니다. + +## 스크립트 사용법 + +### 1. 샘플 CSV 파일 생성하기 + +이슈 생성을 테스트하기 위해 샘플 CSV 파일을 생성할 수 있습니다. + +```bash +python generate_sample_csv.py +``` + +- `github_issues.csv` 파일이 생성됩니다. 이 파일을 수정하여 원하�� 이슈 내용을 추가할 수 있습니다. + +### 2. CSV 파일로 GitHub 이슈 생성하기 + +`github_issues.csv` 파일을 기반으로 이슈를 생성합니다. + +```bash +python github_issues_from_csv.py github_issues.csv +``` + +- **Dry Run**: 실제 생성 없이 실행 결과만 미리 보려면 `--dry-run` 플래그를 사용하세요. + ```bash + python github_issues_from_csv.py github_issues.csv --dry-run + ``` +- **라벨/마일스톤 생성 건너뛰기**: 라벨이나 마일스톤 생성을 원치 않을 경우 아래 플래그를 사용할 수 있습니다. + ```bash + python github_issues_from_csv.py github_issues.csv --skip-labels --skip-milestones + ``` + +### 3. 리포지토리의 모든 이슈 삭제하기 + +**주의: 이 작업은 되돌릴 수 없습니다!** + +리포지토리의 모든 이슈를 삭제하려면 아래 명령어를 실행하세요. + +```bash +python delete_all_issues.py +``` + +- 스크립트가 실행되면 삭제할 이슈 목록을 보여주고, 최종 확인을 위해 `yes`를 입력해야만 삭제가 진행됩니다. diff --git a/delete_all_issues.py b/delete_all_issues.py new file mode 100644 index 0000000..d7cbb07 --- /dev/null +++ b/delete_all_issues.py @@ -0,0 +1,130 @@ +import requests +import os + +# --- 설정 --- +# GitHub 개인용 액세스 토큰(PAT)이 필요합니다. 'repo' 권한이 필요합니다. +# 보안을 위해 환경 변수로 설정하는 것을 권장합니다. +# Linux/macOS: export GITHUB_TOKEN='your_token_here' +# Windows: set GITHUB_TOKEN='your_token_here' +GITHUB_TOKEN = os.getenv('GITHUB_TOKEN') +REPO_OWNER = 'baron-consultant' # 리포지토리 소유자 (예: 'octocat') +REPO_NAME = 'llm-gateway-plan' # 리포지토리 이름 (예: 'Hello-World') +# --- --- + +GRAPHQL_URL = 'https://api.github.com/graphql' +HEADERS = { + 'Authorization': f'bearer {GITHUB_TOKEN}', + 'Content-Type': 'application/json', +} + +def run_graphql_query(query, variables): + """GraphQL 쿼리를 실행하고 결과를 반환하는 헬퍼 함수""" + response = requests.post( + GRAPHQL_URL, + headers=HEADERS, + json={'query': query, 'variables': variables} + ) + response.raise_for_status() + json_response = response.json() + if 'errors' in json_response: + raise Exception(f"GraphQL query failed: {json_response['errors']}") + return json_response['data'] + +def get_all_issue_ids(): + """리포지토리의 모든 이슈 ID (GraphQL Node ID)를 가져옵니다.""" + issue_details = [] + has_next_page = True + cursor = None + + get_issues_query = """ + query($owner: String!, $name: String!, $cursor: String) { + repository(owner: $owner, name: $name) { + issues(first: 100, after: $cursor, states: [OPEN, CLOSED]) { + nodes { + id + number + title + } + pageInfo { + endCursor + hasNextPage + } + } + } + } + """ + + print(f"{REPO_OWNER}/{REPO_NAME} 리포지토리에서 모든 이슈를 가져오는 중...") + while has_next_page: + variables = {"owner": REPO_OWNER, "name": REPO_NAME, "cursor": cursor} + data = run_graphql_query(get_issues_query, variables) + + issues_data = data['repository']['issues'] + for issue in issues_data['nodes']: + issue_details.append({ + "id": issue['id'], + "number": issue['number'], + "title": issue['title'] + }) + + has_next_page = issues_data['pageInfo']['hasNextPage'] + cursor = issues_data['pageInfo']['endCursor'] + print(f" 지금까지 찾은 이슈: {len(issue_details)}개") + + return issue_details + +def delete_issue(issue_id, issue_number, issue_title): + """GraphQL `deleteIssue` 뮤테이션을 사용하여 이슈를 삭제합니다.""" + delete_mutation = """ + mutation($issueId: ID!) { + deleteIssue(input: {issueId: $issueId}) { + clientMutationId + } + } + """ + variables = {"issueId": issue_id} + try: + run_graphql_query(delete_mutation, variables) + print(f"성공적으로 이슈 #{issue_number}를 삭제했습니다: '{issue_title}'") + except Exception as e: + print(f"이슈 #{issue_number} 삭제 실패: {e}") + + +def main(): + """메인 실행 함수""" + if not GITHUB_TOKEN: + print("오류: GITHUB_TOKEN 환경 변수가 설정되지 않았습니다.") + print("'repo' 권한을 가진 GitHub 개인용 액세스 토큰을 설정해주세요.") + return + + if REPO_OWNER == 'your_repo_owner' or REPO_NAME == 'your_repo_name': + print("오류: 스크립트 상단의 REPO_OWNER와 REPO_NAME을 당신의 리포지토리 정보로 수정해주세요.") + return + + try: + issues_to_delete = get_all_issue_ids() + + if not issues_to_delete: + print(f"{REPO_OWNER}/{REPO_NAME} 리포지토리에서 이슈를 찾지 못했습니다.") + return + + print(f"\n총 {len(issues_to_delete)}개의 이슈를 {REPO_OWNER}/{REPO_NAME}에서 찾았습니다.") + + # 되돌릴 수 없는 작업이므로 사용자 확인을 받습니다. + confirm = input("이 모든 이슈를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. (yes/no): ").lower() + + if confirm != 'yes': + print("삭제 작업이 취소되었습니다.") + return + + print("\n삭제를 시작합니다...") + for issue in issues_to_delete: + delete_issue(issue['id'], issue['number'], issue['title']) + + print("\n모든 이슈 처리가 완료되었습니다.") + + except Exception as e: + print(f"\n오류가 발생했습니다: {e}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/generate_sample_csv.py b/generate_sample_csv.py new file mode 100644 index 0000000..01be2a8 --- /dev/null +++ b/generate_sample_csv.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +샘플 CSV 파일 생성 스크립트 +""" + +import csv + +def create_sample_csv(): + """샘플 CSV 파일 생성""" + + sample_issues = [ + { + 'title': '[1-1] Docker Compose 인프라 설정', + 'body': '''## 목표 +PostgreSQL, Redis, Celery 컨테이너를 포함한 기본 인프라를 Docker Compose로 설정합니다. + +## 작업 상세 +- [ ] Docker Compose 파일 작성 (PostgreSQL, Redis, Celery) +- [ ] 환경 변수 설정 및 관리 +- [ ] 네트워크 및 볼륨 설정 +- [ ] 헬스체크 설정 + +## 완료 조건 +- Docker Compose로 모든 서비스가 정상 실행되는 것 +- 각 서비스 간 통신이 원활한 것 + +## 예상 작업 시간 +1주''', + 'labels': 'priority:high,milestone-1,infrastructure', + 'assignees': 'developer-a', + 'milestone': 1, + 'priority': 'High', + 'estimated_weeks': 1 + }, + { + 'title': '[1-2] CI/CD 파이프라인 구축', + 'body': '''## 목표 +GitHub Actions 또는 GitLab CI를 사용하여 자동 빌드/배포 파이프라인을 구축합니다. + +## 작업 상세 +- [ ] GitHub Actions workflow 파일 작성 +- [ ] 자동 테스트 실행 설정 +- [ ] Docker 이미지 빌드 및 푸시 +- [ ] 배포 자동화 설정 + +## 완료 조건 +- Push 시 자동 빌드/테스트가 실행되는 것 +- 성공적인 배포가 자동으로 이루어지는 것''', + 'labels': 'priority:high,milestone-1,infrastructure', + 'assignees': 'developer-b', + 'milestone': 1, + 'priority': 'High', + 'estimated_weeks': 2 + }, + { + 'title': '[2-1] 기본 CRUD API 구현', + 'body': '''## 목표 +팀과 사용자 관리를 위한 기본 CRUD API 엔드포인트를 구현합니다. + +## 작업 상세 +- [ ] /teams CRUD 엔드포인트 구현 +- [ ] /users CRUD 엔드포인트 구현 +- [ ] 입력 검증 및 에러 처리 + +## 완료 조건 +- 모든 CRUD 작업이 정상 동작하는 것''', + 'labels': 'priority:high,milestone-2,api,backend', + 'assignees': 'developer-a', + 'milestone': 2, + 'priority': 'High', + 'estimated_weeks': 2 + } + ] + + with open('github_issues.csv', 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['title', 'body', 'labels', 'assignees', 'milestone', 'priority', 'estimated_weeks'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for issue in sample_issues: + writer.writerow(issue) + + print("✅ 샘플 CSV 파일이 'github_issues.csv'로 생성되었습니다.") + print("\nCSV 파일 구조:") + print("- title: 이슈 제목") + print("- body: 이슈 본문 (마크다운 지원)") + print("- labels: 라벨들 (쉼표로 구분)") + print("- assignees: 담당자들 (쉼표로 구분, GitHub 사용자명)") + print("- milestone: 마일스톤 번호 (1, 2, 3)") + print("- priority: 우선순위") + print("- estimated_weeks: 예상 작업 시간 (주)") + +if __name__ == "__main__": + create_sample_csv() \ No newline at end of file diff --git a/github_issues.csv b/github_issues.csv new file mode 100644 index 0000000..0cb045d --- /dev/null +++ b/github_issues.csv @@ -0,0 +1,38 @@ +title,body,labels,assignees,milestone,priority,estimated_weeks +[1-1] Docker Compose 인프라 설정,"## 목표 +PostgreSQL, Redis, Celery 컨테이너를 포함한 기본 인프라를 Docker Compose로 설정합니다. + +## 작업 상세 +- [ ] Docker Compose 파일 작성 (PostgreSQL, Redis, Celery) +- [ ] 환경 변수 설정 및 관리 +- [ ] 네트워크 및 볼륨 설정 +- [ ] 헬스체크 설정 + +## 완료 조건 +- Docker Compose로 모든 서비스가 정상 실행되는 것 +- 각 서비스 간 통신이 원활한 것 + +## 예상 작업 시간 +1주","priority:high,milestone-1,infrastructure",developer-a,1,High,1 +[1-2] CI/CD 파이프라인 구축,"## 목표 +GitHub Actions 또는 GitLab CI를 사용하여 자동 빌드/배포 파이프라인을 구축합니다. + +## 작업 상세 +- [ ] GitHub Actions workflow 파일 작성 +- [ ] 자동 테스트 실행 설정 +- [ ] Docker 이미지 빌드 및 푸시 +- [ ] 배포 자동화 설정 + +## 완료 조건 +- Push 시 자동 빌드/테스트가 실행되는 것 +- 성공적인 배포가 자동으로 이루어지는 것","priority:high,milestone-1,infrastructure",developer-b,1,High,2 +[2-1] 기본 CRUD API 구현,"## 목표 +팀과 사용자 관리를 위한 기본 CRUD API 엔드포인트를 구현합니다. + +## 작업 상세 +- [ ] /teams CRUD 엔드포인트 구현 +- [ ] /users CRUD 엔드포인트 구현 +- [ ] 입력 검증 및 에러 처리 + +## 완료 조건 +- 모든 CRUD 작업이 정상 동작하는 것","priority:high,milestone-2,api,backend",developer-a,2,High,2 diff --git a/github_issues_from_csv.py b/github_issues_from_csv.py new file mode 100644 index 0000000..869a1b8 --- /dev/null +++ b/github_issues_from_csv.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +GitHub Issues Creator from CSV +CSV 파일을 읽어서 GitHub 이슈를 자동으로 생성하는 스크립트 +""" + +import os +import csv +import time +from datetime import datetime, timedelta +from typing import List, Dict, Optional +import requests +from dataclasses import dataclass +import argparse + + +# 설정 상수 +GITHUB_OWNER = "baron-consultant" # 실제 GitHub 사용자명으로 변경 필요 +GITHUB_REPO = "llm-gateway-plan" # 실제 리포지토리명으로 변경 필요 + +# 마일스톤 설정 (순서대로 1, 2, 3) +MILESTONES = [ + { + "title": "마일스톤 1: 통합 AI 게이트웨이 및 기본 로깅 백엔드 구축", + "description": "모든 LLM 및 내부 분석 모델을 위한 안정적인 단일 엔드포인트 제공 및 로깅 시스템 구축", + "weeks": 12 + }, + { + "title": "마일스톤 2: API 기반 거버넌스 및 분석 기능 강화", + "description": "플랫폼 관리자를 위한 API 거버넌스 및 사용량 분석 기능 구현", + "weeks": 24 + }, + { + "title": "마일스톤 3: 성능 및 지능 최적화 (LLM 중심)", + "description": "시맨틱 캐싱, A/B 테스트, 성능 최적화 기능 구현", + "weeks": 40 + } +] + + +@dataclass +class IssueData: + title: str + body: str + labels: List[str] + assignees: List[str] + milestone: int + priority: str + estimated_weeks: int + + +class GitHubIssueCreator: + def __init__(self, token: str, owner: str = GITHUB_OWNER, repo: str = GITHUB_REPO): + self.token = token + self.owner = owner + self.repo = repo + self.base_url = "https://api.github.com" + self.headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3+json", + "X-GitHub-Api-Version": "2022-11-28" + } + self.milestones = {} + self.rate_limit_remaining = 5000 # 초기값 + + def check_rate_limit(self): + """API 사용량 확인""" + url = f"{self.base_url}/rate_limit" + response = requests.get(url, headers=self.headers) + if response.status_code == 200: + data = response.json() + self.rate_limit_remaining = data['rate']['remaining'] + print(f"남은 API 호출 수: {self.rate_limit_remaining}") + + def wait_if_needed(self): + """API 제한을 고려한 대기""" + if self.rate_limit_remaining < 10: + print("API 사용량이 부족합니다. 1분 대기...") + time.sleep(60) + self.check_rate_limit() + else: + time.sleep(0.5) # 기본 대기 시간 + + def get_existing_milestones(self) -> Dict[str, int]: + """기존 마일스톤 조회""" + url = f"{self.base_url}/repos/{self.owner}/{self.repo}/milestones" + response = requests.get(url, headers=self.headers) + + if response.status_code == 200: + milestones = response.json() + return {m['title']: m['number'] for m in milestones} + else: + print(f"마일스톤 조회 실패: {response.status_code}") + return {} + + def create_milestone(self, title: str, description: str, due_date: str) -> Optional[int]: + """마일스톤 생성""" + url = f"{self.base_url}/repos/{self.owner}/{self.repo}/milestones" + data = { + "title": title, + "description": description, + "due_on": due_date + } + + response = requests.post(url, headers=self.headers, json=data) + + if response.status_code == 201: + milestone = response.json() + print(f"✅ 마일스톤 생성 완료: {title}") + return milestone['number'] + else: + print(f"❌ 마일스톤 생성 실패: {title}, Status: {response.status_code}") + if response.status_code == 422: + print(f" 오류 내용: {response.text}") + return None + + def setup_milestones(self): + """마일스톤들 설정""" + print("🔄 마일스톤 설정 중...") + self.milestones = self.get_existing_milestones() + + for i, milestone_data in enumerate(MILESTONES): + title = milestone_data["title"] + if title not in self.milestones: + due_date = (datetime.now() + timedelta(weeks=milestone_data["weeks"])).strftime("%Y-%m-%dT%H:%M:%SZ") + milestone_number = self.create_milestone( + title, + milestone_data["description"], + due_date + ) + if milestone_number: + self.milestones[title] = milestone_number + self.wait_if_needed() + else: + print(f"✅ 기존 마일스톤 사용: {title}") + + def get_milestone_number(self, milestone_index: int) -> Optional[int]: + """마일스톤 번호 조회 (1, 2, 3 형태의 인덱스를 받음)""" + if 1 <= milestone_index <= len(MILESTONES): + milestone_title = MILESTONES[milestone_index - 1]["title"] + return self.milestones.get(milestone_title) + return None + + def create_labels(self): + """필요한 라벨들 생성""" + print("🔄 라벨 설정 중...") + + labels_to_create = [ + {"name": "priority:high", "color": "d73a4a", "description": "높은 우선순위"}, + {"name": "priority:medium", "color": "fbca04", "description": "중간 우선순위"}, + {"name": "priority:low", "color": "0e8a16", "description": "낮은 우선순위"}, + {"name": "milestone-1", "color": "1f77b4", "description": "마일스톤 1 관련"}, + {"name": "milestone-2", "color": "ff7f0e", "description": "마일스톤 2 관련"}, + {"name": "milestone-3", "color": "2ca02c", "description": "마일스톤 3 관련"}, + {"name": "infrastructure", "color": "8c564b", "description": "인프라 관련"}, + {"name": "backend", "color": "e377c2", "description": "백엔드 개발"}, + {"name": "database", "color": "7f7f7f", "description": "데이터베이스 관련"}, + {"name": "api", "color": "bcbd22", "description": "API 개발"}, + {"name": "integration", "color": "17becf", "description": "통합 작업"}, + {"name": "documentation", "color": "aec7e8", "description": "문서화"}, + {"name": "testing", "color": "ffbb78", "description": "테스트 관련"}, + {"name": "optimization", "color": "ff9896", "description": "성능 최적화"}, + {"name": "monitoring", "color": "c5b0d5", "description": "모니터링"}, + ] + + url = f"{self.base_url}/repos/{self.owner}/{self.repo}/labels" + + # 기존 라벨 조회 + existing_labels = requests.get(url, headers=self.headers) + existing_label_names = [] + if existing_labels.status_code == 200: + existing_label_names = [label['name'] for label in existing_labels.json()] + + created_count = 0 + for label in labels_to_create: + if label['name'] not in existing_label_names: + response = requests.post(url, headers=self.headers, json=label) + if response.status_code == 201: + print(f"✅ 라벨 생성: {label['name']}") + created_count += 1 + else: + print(f"❌ 라벨 생성 실패: {label['name']}") + self.wait_if_needed() + + if created_count == 0: + print("✅ 모든 라벨이 이미 존재합니다.") + + def create_issue(self, issue_data: IssueData) -> bool: + """이슈 생성""" + url = f"{self.base_url}/repos/{self.owner}/{self.repo}/issues" + + milestone_number = self.get_milestone_number(issue_data.milestone) + + data = { + "title": issue_data.title, + "body": issue_data.body, + "labels": issue_data.labels, + } + + # assignees가 비어있지 않은 경우에만 추가 + if issue_data.assignees: + data["assignees"] = issue_data.assignees + + if milestone_number: + data["milestone"] = milestone_number + + response = requests.post(url, headers=self.headers, json=data) + + if response.status_code == 201: + issue = response.json() + print(f"✅ 이슈 생성 완료: #{issue['number']} - {issue_data.title}") + return True + else: + print(f"❌ 이슈 생성 실패: {issue_data.title}") + print(f" Status: {response.status_code}") + if response.status_code == 422: + error_data = response.json() + print(f" 오류: {error_data}") + return False + + +def parse_csv(csv_file: str) -> List[IssueData]: + """CSV 파일을 파싱하여 IssueData 리스트 반환""" + issues = [] + + with open(csv_file, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + + for row in reader: + # 라벨 파싱 (쉼표 구분) - None 값 처리 개선 + labels_str = row.get('labels', '') or '' + labels = [label.strip() for label in labels_str.split(',') if label.strip()] + + # 담당자 파싱 (쉼표 구분) - None 값 처리 개선 + assignees_str = row.get('assignees', '') or '' + assignees = [assignee.strip() for assignee in assignees_str.split(',') if assignee.strip()] + + # 마일스톤 파싱 (숫자) + try: + milestone_str = row.get('milestone', '1') or '1' + milestone = int(milestone_str) + except (ValueError, TypeError): + milestone = 1 + + # 예상 작업 시간 파싱 + try: + weeks_str = row.get('estimated_weeks', '1') or '1' + estimated_weeks = int(weeks_str) + except (ValueError, TypeError): + estimated_weeks = 1 + + # 제목과 본문도 None 값 처리 + title = row.get('title', '') or '' + body = row.get('body', '') or '' + priority = row.get('priority', 'Medium') or 'Medium' + + issue = IssueData( + title=title.strip(), + body=body.strip(), + labels=labels, + assignees=assignees, + milestone=milestone, + priority=priority.strip(), + estimated_weeks=estimated_weeks + ) + + if issue.title: # 제목이 있는 경우에만 추가 + issues.append(issue) + + return issues + + +def main(): + parser = argparse.ArgumentParser(description='CSV에서 GitHub 이슈 생성') + parser.add_argument('csv_file', help='이슈 데이터가 포함된 CSV 파일 경로') + parser.add_argument('--token', help='GitHub 개인 액세스 토큰 (환경변수 GITHUB_TOKEN 사용 가능)') + parser.add_argument('--dry-run', action='store_true', help='실제 생성 없이 미리보기만 실행') + parser.add_argument('--skip-labels', action='store_true', help='라벨 생성 건너뛰기') + parser.add_argument('--skip-milestones', action='store_true', help='마일스톤 생성 건너뛰기') + parser.add_argument('--debug', action='store_true', help='CSV 파일 디버그 정보 표시') + args = parser.parse_args() + + # GitHub 토큰 확인 + token = args.token or os.environ.get('GITHUB_TOKEN') + if not token: + print("❌ GitHub 토큰이 필요합니다. --token 또는 GITHUB_TOKEN 환경변수를 설정하세요.") + return + + # CSV 파일 확인 + if not os.path.exists(args.csv_file): + print(f"❌ CSV 파일을 찾을 수 없습니다: {args.csv_file}") + return + + # 디버그 모드로 CSV 구조 확인 + if args.debug: + print(f"🔍 CSV 파일 디버그 정보: {args.csv_file}") + with open(args.csv_file, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + print(f"📋 컬럼: {reader.fieldnames}") + + for i, row in enumerate(reader): + if i >= 3: # 처음 3개 행만 표시 + break + print(f"🔍 행 {i+1}: {dict(row)}") + return + + print(f"📁 CSV 파일 읽는 중: {args.csv_file}") + + try: + issues = parse_csv(args.csv_file) + except Exception as e: + print(f"❌ CSV 파일 파싱 중 오류 발생: {e}") + print("💡 --debug 옵션을 사용하여 CSV 파일 구조를 확인해보세요.") + return + + if not issues: + print("❌ 생성할 이슈가 없습니다.") + return + + print(f"📊 총 {len(issues)}개의 이슈를 발견했습니다.") + + if args.dry_run: + print("\n🔍 [DRY RUN] 생성될 이슈들:") + for i, issue in enumerate(issues, 1): + print(f" {i:2d}. [{issue.milestone}] {issue.title}") + print(f" 라벨: {', '.join(issue.labels)}") + print(f" 담당자: {', '.join(issue.assignees)}") + print(f" 우선순위: {issue.priority}") + print() + return + + # GitHub API 클라이언트 생성 + creator = GitHubIssueCreator(token) + + print(f"🎯 GitHub 리포지토리: {GITHUB_OWNER}/{GITHUB_REPO}") + + # API 사용량 확인 + creator.check_rate_limit() + + # 마일스톤 설정 + if not args.skip_milestones: + creator.setup_milestones() + + # 라벨 설정 + if not args.skip_labels: + creator.create_labels() + + # 이슈 생성 + print(f"\n🚀 {len(issues)}개 이슈 생성 시작...") + success_count = 0 + fail_count = 0 + + for i, issue in enumerate(issues, 1): + print(f"\n[{i}/{len(issues)}] 이슈 생성 중...") + + if creator.create_issue(issue): + success_count += 1 + else: + fail_count += 1 + + creator.wait_if_needed() + + print(f"\n📈 완료: 성공 {success_count}개, 실패 {fail_count}개") + + if fail_count > 0: + print("\n⚠️ 일부 이슈 생성에 실패했습니다. 다음 사항을 확인하세요:") + print(" - GitHub 토큰 권한") + print(" - 리포지토리 접근 권한") + print(" - 담당자 사용자명") + print(" - API 사용량 제한") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7756dc7 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from llm-gateway-plan!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..42efee2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "llm-gateway-plan" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f7c52e4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +argparse==1.4.0 +certifi==2025.7.14 +charset-normalizer==3.4.2 +idna==3.10 +requests==2.32.4 +urllib3==2.5.0