깃헙 이슈 테스트 완료
This commit is contained in:
206
.gitignore
vendored
Normal file
206
.gitignore
vendored
Normal file
@@ -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
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
77
README.md
Normal file
77
README.md
Normal file
@@ -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` 파일이 생성됩니다. 이 파일을 수정하여 원하<EC9B90><ED9598> 이슈 내용을 추가할 수 있습니다.
|
||||
|
||||
### 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`를 입력해야만 삭제가 진행됩니다.
|
||||
130
delete_all_issues.py
Normal file
130
delete_all_issues.py
Normal file
@@ -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()
|
||||
94
generate_sample_csv.py
Normal file
94
generate_sample_csv.py
Normal file
@@ -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()
|
||||
38
github_issues.csv
Normal file
38
github_issues.csv
Normal file
@@ -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
|
||||
|
374
github_issues_from_csv.py
Normal file
374
github_issues_from_csv.py
Normal file
@@ -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()
|
||||
6
main.py
Normal file
6
main.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from llm-gateway-plan!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@@ -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 = []
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user