의존성 업데이트 봇
오래된 패키지를 스캔합니다. 보안 감사를 실행합니다. 변경 로그를 가져옵니다. 주요 변경 사항을 요약합니다. 위험 그룹별로 하나의 PR을 엽니다.
중요 규칙: 패키지 매니저의 outdated 명령어가 실제로 보고하는 패키지만 업데이트하세요. 버전 번호를 추측하거나 임의로 만들지 마세요. 변경 로그를 가져올 수 없으면 내용을 지어내지 말고 그 사실을 기록하세요.
1단계: 설정 확인
echo "GEMINI_API_KEY: ${GEMINI_API_KEY:+set}"
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set, changelog fetching rate-limited to 60/hour}"
gh auth status 2>/dev/null | head -1 || echo "gh: not authenticated"
GEMINI_API_KEY가 없으면: 중단하고 사용자에게 알리세요: "GEMINI_API_KEY가 필요합니다. aistudio.google.com에서 키를 받아 .env 파일에 추가하세요."
gh가 인증되지 않았으면: 중단하고 사용자에게 알리세요: "GitHub CLI가 인증되어야 합니다. 실행: gh auth login"
패키지 매니저 감지:
ls package.json 2>/dev/null && echo "npm"
ls requirements.txt pyproject.toml 2>/dev/null && echo "pip"
ls Cargo.toml 2>/dev/null && echo "cargo"
ls go.mod 2>/dev/null && echo "go"
ls Gemfile 2>/dev/null && echo "ruby"
여러 개가 발견되면 질문: "[목록]을 찾았습니다. 무엇을 스캔할까요? (all / npm / pip / cargo / go / ruby)"
2단계: 오래된 패키지 감지
npm:
npm outdated --json --long 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
for name, info in data.items():
print(json.dumps({'name': name, 'current': info.get('current','?'), 'latest': info.get('latest','?'), 'dep_type': info.get('type','dependencies')}))
"
pip:
pip list --outdated --format=json 2>/dev/null | python3 -c "
import sys, json
for p in json.load(sys.stdin):
print(json.dumps({'name': p['name'], 'current': p['version'], 'latest': p['latest_version']}))
"
Cargo (Rust):
cargo outdated --format json 2>/dev/null || \
cargo outdated 2>/dev/null | grep -v "^---" | tail -n +3 | head -30
# cargo-outdated가 설치되지 않은 경우: cargo install cargo-outdated
Go 모듈:
go list -u -m -json all 2>/dev/null | python3 -c "
import sys, json
decoder = json.JSONDecoder()
buf = sys.stdin.read()
pos = 0
while pos < len(buf):
try:
obj, idx = decoder.raw_decode(buf, pos)
if obj.get('Update'):
print(json.dumps({'name': obj['Path'], 'current': obj['Version'], 'latest': obj['Update']['Version']}))
pos += idx
except: break
"
Ruby (Bundler):
bundle outdated --parseable 2>/dev/null | python3 -c "
import sys
for line in sys.stdin:
parts = line.strip().split()
if len(parts) >= 4:
print('{\"name\":\"' + parts[0] + '\",\"current\":\"' + parts[3].strip('()') + '\",\"latest\":\"' + parts[1] + '\"}')
"
모두 빈 결과를 반환하면: "모든 패키지가 최신 상태입니다." 중단합니다.
진행하기 전에 개수를 알립니다: "오래된 패키지 X개를 찾았습니다."
3단계: 위험 수준별 분류
버전 범위 파싱 (현재 → 최신):
- MAJOR: 첫 번째 숫자가 변경됨 (1.x.x → 2.x.x)
- MINOR: 두 번째 숫자가 변경됨 (1.2.x → 1.3.x)
- PATCH: 세 번째 숫자가 변경됨 (1.2.3 → 1.2.4)
python3 -c "
def classify(current, latest):
try:
c = [int(x) for x in current.lstrip('v').split('.')[:3]]
l = [int(x) for x in latest.lstrip('v').split('.')[:3]]
if l[0] > c[0]: return 'major'
if len(l) > 1 and len(c) > 1 and l[1] > c[1]: return 'minor'
return 'patch'
except: return 'unknown'
"
분류 결과를 알립니다: "패치: X개 패키지. 마이너: Y개 패키지. 메이저: Z개 패키지."
4단계: 보안 감사
PR을 생성하기 전에 CVE 스캔을 실행합니다. 이는 긴급도를 결정합니다.
npm:
npm audit --json 2>/dev/null | python3 -c "
import sys, json
d = json.load(sys.stdin)
vulns = d.get('vulnerabilities', {})
for pkg, info in vulns.items():
sev = info.get('severity', 'unknown')
via = [v.get('title','') for v in info.get('via',[]) if isinstance(v, dict)]
print(f' [{sev.upper()}] {pkg}: {via[0] if via else \"see npm audit\"}')
" 2>/dev/null || echo "취약점이 발견되지 않았거나 npm audit을 사용할 수 없습니다"
pip:
pip-audit --format=json 2>/dev/null | python3 -c "
import sys, json
for vuln in json.load(sys.stdin):
print(f' [{vuln.get(\"aliases\",[\"\"])[0]}] {vuln[\"name\"]} {vuln[\"version\"]}: {vuln[\"description\"][:80]}')
" 2>/dev/null || echo "pip-audit이 설치되지 않았습니다. 실행: pip install pip-audit"
Cargo:
cargo audit 2>/dev/null | grep -E "^(ID|Package|Severity|URL)" | head -30 \
|| echo "cargo-audit이 설치되지 않았습니다. 실행: cargo install cargo-audit"
에스컬레이션 규칙: PATCH 또는 MINOR 업데이트에 Critical 또는 High CVE가 있으면 MAJOR 우선순위로 승격합니다. 해당 패키지는 자체 PR을 받고 CVE 세부 정보는 PR 본문에 포함됩니다.
보안 결과를 진행하기 전에 보고합니다:
보안 감사: N개 취약점 발견
[CRITICAL] lodash 4.17.19: 프로토타입 오염 (CVE-2021-23337)
[HIGH] axios 0.21.1: 서버사이드 요청 위조 (CVE-2021-3749)
취약점이 없으면: "보안 감사: 이상 없음."
5단계: 변경 로그 가져오기
각 패키지에 대해 순서대로 소스를 시도합니다. 콘텐츠를 반환하는 첫 번째 소스에서 중단합니다.
소스 1: GitHub 릴리스 API
레지스트리에서 리포지토리 URL 가져오기:
# npm
curl -s "https://registry.npmjs.org/{PACKAGE}/latest" \
| python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('repository',{}); print(r.get('url','') if isinstance(r,dict) else str(r))"
# pip
curl -s "https://pypi.org/pypi/{PACKAGE}/json" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('info',{}).get('home_page','') or d.get('info',{}).get('project_urls',{}).get('Source',''))"
최근 5개 릴리스 가져오기:
AUTH_HEADER=""
[ -n "$GITHUB_TOKEN" ] && AUTH_HEADER="-H \"Authorization: Bearer $GITHUB_TOKEN\""
curl -s $AUTH_HEADER \
"https://api.github.com/repos/{OWNER}/{REPO}/releases?per_page=5" \
| python3 -c "import sys,json; [print(json.dumps({'tag':r.get('tag_name',''),'body':r.get('body','')[:1500]})) for r in json.load(sys.stdin)]"
현재 버전과 최신 버전 사이의 릴리스만 유지합니다.
소스 2: npm 레지스트리 README (대체)
curl -s "https://registry.npmjs.org/{PACKAGE}" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('readme','')[:3000])"
소스 3: PyPI 설명 (pip의 마지막 수단)
curl -s "https://pypi.org/pypi/{PACKAGE}/json" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('info',{}).get('description','')[:2000])"
어떤 소스도 콘텐츠를 반환하지 않으면: "변경 로그를 찾을 수 없음"을 기록하고 계속 진행합니다.
6단계: Gemini로 요약
위험 그룹당 하나의 요청. CVE 영향을 받는 패키지에 대한 보안 결과 포함:
cat > /tmp/deps-summary-request.json << 'ENDJSON'
{
"system_instruction": {
"parts": [{
"text": "You are a developer writing a GitHub PR description for a dependency update. Given a list of packages being updated and their raw changelog content, write a concise PR body in Markdown. Rules: For each package, list only what changed between the OLD version and the NEW version. Use bullet points. Flag breaking changes with a BREAKING prefix. Flag CVE fixes with a SECURITY prefix and include the CVE ID. Keep each package section to 3-5 bullets maximum. If no changelog was found for a package, write 'No changelog available.' Do not use em dashes. Do not use these words: seamless, robust, leverage, transform, innovative. Output only the Markdown PR body, no commentary."
}]
},
"contents": [{
"parts": [{
"text": "PACKAGES_AND_CHANGELOGS_HERE"
}]
}],
"generationConfig": {
"temperature": 0.2,
"maxOutputTokens": 2048
}
}
ENDJSON
curl -s -X POST \
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=$GEMINI_API_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/deps-summary-request.json \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['candidates'][0]['content']['parts'][0]['text'])"
7단계: PR 생성
비어 있지 않은 위험 그룹당 하나의 PR. 메이저 업데이트의 경우 패키지당 하나의 PR (개별 검토 필요).
1. 브랜치 생성:
BRANCH="deps/{RISK}-updates-$(date +%Y%m%d)"
git checkout -b "$BRANCH"
2. 패키지 파일 업데이트:
npm:
npm install {package}@{latest_version} --save-exact
# devDependencies:
npm install {package}@{latest_version} --save-dev --save-exact
pip:
python3 -c "
import re, sys
pkg, version, filename = sys.argv[1], sys.argv[2], sys.argv[3]
with open(filename) as f: content = f.read()
pattern = rf'^{re.escape(pkg)}[>=<!\s].*$'
new_content = re.sub(pattern, f'{pkg}=={version}', content, flags=re.MULTILINE|re.IGNORECASE)
if new_content == content: new_content = content + f'\n{pkg}=={version}'
open(filename, 'w').write(new_content)
" "{PACKAGE}" "{LATEST}" "requirements.txt"
Cargo:
# Cargo.toml의 패키지 버전 필드를 편집한 후:
cargo update {package}
Go:
go get {module}@{latest_version}
go mod tidy
Ruby:
bundle update {gem_name}
3. 커밋:
git add -A
git commit -m "chore(deps): update {RISK} dependencies $(date +%Y-%m-%d)"
4. PR 생성:
cat > /tmp/dep-pr-body-{RISK}.md << 'ENDMD'
PR_BODY_FROM_GEMINI
ENDMD
gh pr create \
--title "chore(deps): update {RISK} dependencies" \
--body-file /tmp/dep-pr-body-{RISK}.md \
--label "dependencies" \
--base main
메이저 업데이트에는 dependencies,breaking-change 라벨이 붙습니다. CVE 수정 업데이트에는 dependencies,security 라벨이 붙습니다.
각 PR 후, 메인 브랜치로 돌아갑니다: git checkout main
8단계: 진단 모드
트리거: 실행 중에 패키지 설치 명령이 실패하면 중단하지 않고 진단 모드로 들어갑니다.
실패 유형 감지:
| 오류 패턴 | 가능한 원인 | 제안된 수정 |
|---|---|---|
peer dep conflict | 피어 의존성 비호환 | 충돌하는 쌍을 표시하고 --legacy-peer-deps 플래그 또는 다운그레이드 제안 |
ERESOLVE | npm 해결 충돌 | 해당 패키지에 대해서만 npm install --legacy-peer-deps 실행 |
version not found | 레지스트리에 버전이 없음 | npm view {pkg} versions로 레지스트리 확인 |
python requires | Python 버전 비호환 | 필요한 Python 버전을 기록하고 패키지 건너뛰기 |
cargo E0463 | Rust 에디션 비호환 | 수동 검토 플래그 지정 |
진단 요약 제시:
{package} 설치 실패: {error type}
가능한 원인: {explanation}
제안된 수정: {specific command or action}
나머지 패키지: 성공한 {N}개로 계속 진행합니다.
하나의 패키지가 실패해도 전체 실행을 중단하지 마세요. 성공한 패키지로 계속 진행합니다.
9단계: 결과 요약
## 의존성 업데이트 요약: [YYYY-MM-DD]
### 보안
[CRITICAL] lodash: CVE-2021-23337 4.17.21에서 수정됨: PR #42
[HIGH] axios: CVE-2021-3749 0.21.4에서 수정됨: PR #42
| 위험 수준 | 패키지 | PR |
|------------|----------|-----|
| 패치 | lodash 4.17.19→4.17.21, axios 0.21.1→0.21.4 | #42 |
| 마이너 | express 4.17.1→4.18.2 | #43 |
| 메이저 | react 17.0.2→18.2.0 | #44 |
열린 PR: 3개
변경 로그가 없는 패키지: some-obscure-pkg (레지스트리에 GitHub 리포지토리 없음)
설치 실패: 없음
다음 작업: 병합 전에 메이저 업데이트 PR을 개별적으로 검토하세요.