AI, 오픈 소스 도구, 그리고 Human in the loop를 활용하여 매주 huggingface_hub를 배포하기
요약
Hugging Face는 오픈 소스 도구와 AI를 결합하여 huggingface_hub 라이브러리의 배포 주기를 4~6주에서 매주로 단축했습니다. 반복적인 기계적 작업은 CI로 자동화하고, 릴리스 노트 작성과 같은 판단이 필요한 영역에는 Human-in-the-loop 방식을 적용하여 효율성을 높였습니다.
핵심 포인트
- GitHub Actions를 활용한 배포 프로세스 자동화
- AI를 활용한 릴리스 노트 초안 작성 및 효율화
- 판단이 필요한 핵심 단계에 Human-in-the-loop 적용
- 오픈 소스 및 오픈 웨이트 모델 중심의 지속 가능한 워크플로우 구축
huggingface_hub는 Hugging Face 생태계의 기반이 되는 Python 클라이언트입니다. transformers, datasets, diffusers, sentence-transformers 및 수십 개의 다른 라이브러리들이 Hub와 통신하기 위해 이 라이브러리에 의존합니다. 우리가 새로운 릴리스를 배포하지 않는 매주는 수정 사항과 기능들이 main 브랜치에 갇혀 있는 한 주와 같습니다.
오랫동안 우리는 4~6주마다 한 번씩 릴리스를 진행했습니다. 이제 우리는 단일 GitHub Actions 워크플로우를 통해 매주 릴리스를 합니다. 우리는 오픈 소스 도구와 오픈 웨이트 (open-weights) 모델을 사용하여 이를 구축했으며, 판단이 중요한 단 한 곳에 Human in the loop (인간 참여)를 유지했습니다. 이 포스트의 어떤 내용도 벤더 계약, 폐쇄형 모델, 또는 직접 실행할 수 없는 인프라를 요구하지 않습니다. 이는 다른 유지 관리자들이 채택하고 적응할 수 있는 워크플로우를 원했기에 처음부터 설정한 설계 목표였습니다.
이 포스트를 다 읽을 때쯤이면, 여러분만의 워크플로우를 구축하는 데 필요한 모든 것을 갖추게 될 것입니다.
기존 프로세스는 부분적으로는 자동화되어 있었지만, 대부분은 수동이었습니다.
이미 CI(지속적 통합)에 포함된 사항:
- 태그가 푸시되면 PyPI에 게시.
- 릴리스 후보(release candidate)를 고정하여 다운스트림 라이브러리에 테스트 브랜치 오픈.
여전히 매번 수동으로 진행되는 사항:
- 릴리스 브랜치 생성,
__init__.py에서 버전 업데이트, 커밋, 태깅, 푸시. - 다운스트림 CI 실행을 모니터링하고 실패 사례 분류(triaging).
- 지난 릴리스 이후 병합된 모든 PR(Pull Request)을 읽고 직접 릴리스 노트 작성: 주제별로 그룹화하고, 맥락을 포함하며,
git log덤프처럼 읽히지 않는 어조로 작성. - RC(Release Candidate) 기간 이후 안정적인 릴리스(stable release) 확정.
- 내부 Slack 공지 및 소셜 미디어 게시물 초안 작성.
main브랜치를 다음dev0로 업데이트하기 위한 릴리스 후 PR 오픈.
새 버전에 대한 좋은 노트를 작성하는 것은 서로 다른 주제의 수십 개 PR을 취합해야 하는 가장 힘든 작업이었습니다. 기술적으로 어려운 것은 없었지만 몇 시간의 집중적인 주의가 필요했습니다. 여기에 공지 작업까지 더해지면, 마이너 릴리스 하나를 처리하는 데 며칠에 걸쳐 반나절 이상의 작업 시간이 소요되었습니다.
그래서 우리는 전체 과정을 간소화하기로 결정했습니다. 그 목록을 살펴보면, 작업은 두 가지로 나뉩니다.
일부 단계는 순수하게 기계적이며 자동화할 수 있습니다: 버전 올리기 (bumping the version), 커밋 (committing), 태깅 (tagging), 푸시 (pushing), 다운스트림 테스트 브랜치 열기 (opening downstream test branches), 릴리스 후 PR 열기 (opening the post-release PR). 아무도 이 작업들에 대해 고민할 필요가 없습니다. 이 작업들은 매번 올바른 순서로 실행되기만 하면 되며, 이것이 바로 CI 워크플로우 (CI workflow)가 잘하는 일입니다.
나머지는 다릅니다. 릴리스 노트 (release notes) 작성, 무엇을 강조할지 결정하기, 사람들을 위한 공지 문구 작성하기: 이것은 두뇌 작업입니다. 이는 수년간 릴리스 매뉴얼을 유지해 온 종류의 판단력입니다. 바로 이 지점에서 AI가 등장하여, 빈 페이지를 몇 초 만에 탄탄한 초안으로 바꿔 놓습니다. 또한 이 지점에서는 주의가 필요합니다. 왜냐하면 자신감 있어 보이지만 미묘하게 틀린 초안은 초안이 아예 없는 것보다 더 나쁘기 때문입니다.
우리가 이 문제를 해결하기로 결정했을 때, 한 가지 제약 조건을 미리 설정했습니다: 모든 구성 요소는 어떤 메인테이너 (maintainer)라도 직접 실행할 수 있는 것이어야 한다는 점입니다. 우리가 교체할 수 없는 API 뒤에 숨겨진 폐쇄형 모델 (closed model), 독점적인 릴리스 플랫폼, 혹은 비밀 레시피(secret sauce)는 없어야 했습니다.
전체 스택은 다음과 같습니다:
| 구성 요소 | 역할 |
|---|---|
| GitHub Actions | 전체 릴리스를 오케스트레이션 (orchestrates) 함 |
| OpenCode | 모델을 구동하는 에이전트 런타임 (agent runtime) |
| 오픈 웨이트 모델 (현재 Z.ai의 GLM-5.2) | 릴리스 노트 및 Slack 공지 초안 작성 |
| HF Inference Providers | 모델 서빙 (serves the model) |
| PyPI Trusted Publishing | 패키지 배포 (publishes the package) |
두 번째 원칙: 모델이 초안을 작성하고, 사람이 결정합니다. 언어 모델 (Language models)은 30개의 간결한 PR 제목을 읽기 쉬운 릴리스 노트로 바꾸는 데 능숙합니다. 하지만 맹목적으로 신뢰하기에는 적합하지 않습니다. 따라서 워크플로우는 사람이 감독하는 방식입니다: 모델이 첫 번째 패스 (first pass)를 수행하고, 결정론적 스크립트 (deterministic script)가 그 결과물을 확인하며, 무언가가 배포되기 전에 사람이 검토하고 편집합니다 (이에 대한 자세한 내용은 아래에서 다룹니다).
전체 워크플로우는 단일 파일인 .github/workflows/release.yml로 구성되며, Actions UI에서 수동으로 트리거됩니다. 이 파일은 정확히 하나의 입력값을 받습니다:
on:
workflow_dispatch:
inputs:
...
그다음부터 작업(jobs)은 대략 다음과 같은 순서로 실행됩니다:
준비 (Prepare). 다음 버전을 계산하고, 릴리스 브랜치 (release branch)를 생성하거나 재사용하며, __version__을 올리고(bump), 커밋(commit), 태그(tag), 푸시(push)합니다.PyPI에 게시 (Publish to PyPI). huggingface_hub를 빌드하고 업로드합니다. 이와 병렬로, hf CLI를 별도의 PyPI 패키지로 빌드하고 업로드합니다.릴리스 노트 (Release notes). 마지막 태그 이후의 커밋 범위(commit range)를 차이 분석(diff)하고, GitHub API에서 PR 메타데이터를 가져온 뒤, 모델이 구조화된 변경 로그 (changelog) 초안을 작성하게 합니다 (최근 사례는 여기를 참조하세요). 이는 GitHub 릴리스의 *초안(draft)*으로 저장됩니다.다운스트림 테스트 브랜치 (Downstream test branches). RC(Release Candidate)의 경우, transformers, datasets, diffusers, sentence-transformers에 RC 버전을 고정(pin)한 브랜치를 생성하여, 해당 프로젝트들의 CI(지속적 통합)를 통해 무언가 망가졌는지 빠르게 확인합니다.Slack 공지 (Slack announcement). 노트를 읽고 우리 팀의 어조로 내부 공지 사항을 작성합니다.노트 아카이브 (Archive notes). AI가 작성한 원본 초안과 사람이 편집한 버전을 모두 Hugging Face Bucket에 나란히 업로드합니다.릴리스 후 버전 업 (Post-release bump). 안정적인 릴리스가 완료되면, main 브랜치에 다음 dev0 버전으로 올리는 PR을 생성합니다.배포된 PR에 댓글 달기 (Comment on shipped PRs). 릴리스에 포함된 모든 PR에
# Deterministic: 지정된 범위 내의 squash-merge 커밋에서 PR 번호를 추출합니다.
PR_NUMBER_PATTERN = re.compile(r"\(#(\d+)\)$")
pr_numbers = [
...
그다음 모델이 해당 PR들을 바탕으로 노트를 초안(draft) 작성합니다. 작성이 완료되면, 초기 PR 목록과 모델의 출력물을 대조하여 확인합니다:
expected = set(load_manifest()) # 있어야 할 항목
found = extract_pr_refs(notes_md) # 모델이 작성한 항목 (#1234 -> 1234)
missing = expected - found # 누락된 항목
...
만약 누락되거나 추가된 항목이 있더라도, 프로세스를 실패시키거나 잘못된 파일을 배포하지 않습니다. 대신 불일치 사항을 에이전트(agent)에게 다시 전달하여 정확히 해당 PR들을 수정하도록 요청합니다:
for _ in range(MAX_ITERATIONS):
missing, extra = validate(notes)
if not missing and not extra:
...
이것이 전체 시스템을 신뢰할 수 있게 만드는 패턴입니다. 즉, 비결정론적(non-deterministic)인 모델을 결정론적(deterministic)인 가드레일(guardrails)로 감싸는 것입니다. 모델은 산문을 작성하는 데는 뛰어나지만, 모든 내용을 빠짐없이 작성하는 데는 신뢰도가 낮습니다. 따라서 모델이 글을 쓰게 하되, 코드가 일관성을 강제하도록 합니다.
완전성(Completeness)이 절반이라면, 정확성(Accuracy)은 나머지 절반입니다. PR 제목만 보고 요약하는 모델은 실제 API와 일치하지 않는 코드 예시를 아주 즐겁게 지어낼 수도 있습니다.
이를 방지하기 위해, PR 메타데이터를 가져올 때 각 PR에서 실제 문서 차이점(documentation diffs)도 함께 가져옵니다. 즉, 해당 PR이 수정한 docs/ 디렉토리 하위의 모든 .md 파일에 대한 유니파이드 디프(unified diff)를 가져오는 것입니다.
def fetch_doc_diffs(pr):
return [
{"filename": f.filename, "status": f.status, "patch": f.patch}
...
이 디프(diff) 정보가 모델의 컨텍스트(context)에 포함되므로, 모델이 "여기에 새로운 CLI 명령어가 있습니다"라고 작성할 때 PR 작성자가 문서에 실제로 작성한 예시를 인용하게 됩니다. 이는 앞서 설명한 것과 동일한 논리입니다. 모델에게 실제 소스 자료와 좁은 범위의 작업(narrow job)을 부여하는 것입니다.
프롬프트(prompts) 자체는 스킬(Skills)로서 존재합니다. 즉, 레포지토리(repo)에 체크인된 작은 Markdown 파일(SKILL.md 및 참조 템플릿) 형태입니다. release-notes 스킬은 하이라이트를 선택하는 방법, 섹션을 구성하는 방법, 문서 링크를 추가하는 시점 등을 상세히 설명합니다. 이는 마치 온보딩(onboarding) 지침처럼 읽히는데, 이것이 바로 정확히 적절한 사고 모델(mental model)입니다.
RC(Release Candidate)가 게시된 후, GitHub의 초안(draft) 릴리스에는 AI가 1차적으로 작성한 내용이 담긴 채로 남아 있습니다. 바로 이 지점에서 사람이 개입합니다:
- 리뷰어가 초안을 읽고, 어조와 강조점을 편집하며, 모델이 과하게 또는 부족하게 가중치를 둔 부분을 수정합니다.
- 그제야
minor-release실행을 트리거하여, RC를 최종 버전으로 승격시킵니다.
리뷰어의 시간은 다듬는 작업에 투입되며, 반나절이 걸리던 작문 작업이 15분 내외의 편집 세션으로 바뀝니다.
우리는 또한 시간이 지남에 따라 개선하기 위해 기록(paper trail)을 남깁니다. Hugging Face Bucket에 두 개의 파일을 나란히 아카이브합니다. 하나는 아무도 손대기 전인 RC 시점에 업로드된 가공되지 않은 AI 초안(raw AI draft)이고, 다른 하나는 최종 릴리스가 확정될 때 업로드되는 사람이 편집한 버전(human-edited version)입니다.
# RC 시점: 모델로부터 직접 가져온, 수정되지 않은 상태
hf cp release_notes_raw.txt "hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_raw.txt"
# 릴리스 시점: 사람의 리뷰를 거친 후
...
매주 이 두 가지를 모두 수집하면 "모델이 작성한 것" 대 "우리가 작성하기를 원했던 것"에 대한 데이터셋이 점진적으로 구축됩니다. 이 데이터셋은 이후 에이전트(agent)의 기술을 업데이트하는 데 재사용할 수 있습니다.
릴리스 프로세스를 개편한 것은 특히 공급망 공격(supply-chain attacks)에 대비하여 보안을 강화할 수 있는 좋은 기회였습니다.
PyPI 토큰을 사용하지 않습니다. 게시에는 신뢰할 수 있는 게시(Trusted Publishing) 방식을 사용합니다. PyPI는 이 특정 워크플로우를 위해 GitHub에서 발행한 수명이 짧은 OIDC 토큰을 검증하며, 모든 아티팩트(artifact)에 대해 PEP 740 증명(attestations) 및 Sigstore 출처(provenance)를 발행합니다. 유출되거나 교체(rotate)해야 할 장기 비밀키(long-lived secret)가 존재하지 않습니다.
permissions:
id-token: write # PyPI를 위한 OIDC 토큰 발행
attestations: write # Sigstore 출처 생성
...
에이전트 런타임(agent runtime)은 고정(pinned)되고 검증됩니다. 우리는 최신 OpenCode를 curl | bash로 실행하고 운에 맡기지 않습니다. 버전을 고정하고 실행하기 전에 SHA256을 확인합니다:
curl -fsSL https://opencode.ai/install | bash -s -- --version "${OPENCODE_VERSION}"
echo "${OPENCODE_SHA256} $(which opencode)" | sha256sum -c -
오픈 도구(Open tooling)를 사용한다는 것이 부주의한 도구 사용을 의미하지는 않습니다.
거의 들지 않습니다. 전체 릴리스(노트 및 Slack 공지, 20~40개의 PR과 몇 차례의 프롬프팅 포함)를 수행하는 데 추론 제공업체(Inference Providers) 비용은 약 $0.25 정도입니다. 사용한 만큼 지불하는(pay-as-you-go) 방식의 오픈 웨이트 (open weights) 모델을 사용하면, 매주 유일한 실제 질문은 "배포할 만한 가치가 있는 것이 있는가?"이며, 항상 있습니다.
배포 주기는 4~6주에 한 번에서 매주 한 번으로 바뀌었습니다. 그에 따른 부차적인 효과들이 흥미로웠습니다:
노트(Notes)가 나빠지지 않고 오히려 좋아졌습니다. 초안은 항상 존재하기 때문에, 리뷰 시간은 다듬는 데 사용됩니다. 그룹화가 더 일관되게 이루어지며 누락되는 내용도 줄어듭니다.
장애(Breakages)가 더 일찍 드러납니다. 모든 RC(Release Candidate)에 대한 다운스트림(Downstream) 테스트 브랜치가 후보 기간 동안 통합 이슈를 잡아냅니다.
기여자 루프(Contributor loops)가 단축되었습니다. 자동 "vX.Y.Z에 배포됨" 댓글이 예상보다 더 중요하다는 것이 밝혀졌습니다. 누군가 종료된 PR에 이슈를 보고하면, 모든 사람이 해당 수정 사항이 어떤 릴리스에 포함되었는지 즉시 확인할 수 있습니다. 이전에는 수동으로 태그를 찾아다녀야 했습니다.
이 부분이 우리가 가장 신경 쓴 부분입니다. 워크플로우는 huggingface_hub를 중심으로 형성되어 있지만, 구조는 범용적입니다.
거의 그대로 재사용 가능:
- 트리거 및 버전 업(version-bump) 로직 (
minor-prerelease후minor-release, 그 후patch-release). - 신뢰하되 검증하는(trust-but-verify) 루프: 결정론적 매니페스트(deterministic manifest), 모델 초안, 검증, 재프롬프팅(re-prompt). 이는 무엇을 생성하느냐와 관계없이 전이 가능한 아이디어입니다.
- OIDC 신뢰할 수 있는 게시(Trusted Publishing), 고정 및 체크섬 검증된 런타임(runtime), Slack 스레딩.
- 기술 기반 프롬프트(skill-based prompts): 템플릿을 교체하되 구조는 유지합니다.
우리에게 특화된 부분:
- 다운스트림 리포지토리(repo) 목록 및 해당 의존성 고정(dependency-pin) 형식.
- 기술(skills) 내의 정확한 섹션 분류 체계(taxonomy) 및 톤.
- Slack 및 버킷(bucket) 목적지.
이를 맞춤 설정하려면: 워크플로(workflow) 파일과 스크립트를 포크(fork)하고, 이를 귀하의 패키지로 지정하며, 프로젝트의 어조에 맞춰 기술(skill) 마크다운(Markdown)을 다시 작성하세요. 두 개의 저장소 변수(모델 ID 및 OpenCode 버전)를 설정하고, PyPI에서 신뢰할 수 있는 게시(Trusted Publishing)를 설정한 뒤, 다운스트림(downstream)이 없다면 다운스트림 테스트(downstream-testing) 작업을 삭제하면 됩니다. '신뢰하되 검증하라(trust-but-verify)' 루프는 있는 그대로 재사용할 가치가 있는 부분입니다. 이것이 생성된 아티팩트(artifact)를 안전하게 배포할 수 있게 만드는 핵심입니다.
다운스트림 실패 자동 분류 (Auto-triaging downstream failures). 현재 워크플로는 테스트 브랜치를 열고 사람이 CI(지속적 통합)를 읽습니다. 명백한 다음 단계는 실패한 로그를 확인하여 내부 Slack 메시지에 보고하는 것입니다.
패턴 확장 (Extending the pattern). 이 중 대부분은 일반적입니다. 우리는 생태계 내의 다른 Python 라이브러리 전반에 걸쳐 이 중 큰 부분을 재사용할 수 있을 것으로 기대합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 HuggingFace Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기