記事一覧に戻る

Lucain Pouget's avatar

Célina Hanouti's avatar

huggingface_hub は、Hugging Faceエコシステムの基盤となるPythonクライアントです。 transformersdatasetsdiffuserssentence-transformers など数十のライブラリが、Hubとの通信に依存しています。新しいリリースを出さない週は、main ブランチに修正や機能が溜まる週となります。

以前は4〜6週間に1回のリリースを行っていました。現在は、単一のGitHub Actionsワークフローから毎週リリースしています。オープンソースツールとオープンウェイトモデルを使って構築し、判断が必要な箇所には人間を関与させています。この記事の内容は、ベンダー契約、クローズドモデル、または自分で運用できないインフラを必要としません。これは、最初から他のメンテナーが採用・カスタマイズできるワークフローを目指した設計目標でした。

この記事を読み終える頃には、ご自身で構築するために必要なすべてが揃っているはずです。

これまでの経緯

従来のプロセスは、一部自動化されていましたが、ほとんどが手作業でした。

すでにCIで自動化されていたこと:

  • タグがプッシュされたらPyPIへ公開。
  • リリース候補版をピン留めした状態で、下流ライブラリにテストブランチを作成。

毎回手作業で行っていたこと:

  • リリースブランチの作成、__init__.py のバージョン更新、コミット、タグ付け、プッシュ。
  • 下流CIの実行状況を監視し、失敗をトリアージ。
  • 前回リリース以降にマージされた全PRを読み、テーマごとにグループ化して、手作業でリリースノートを作成(文脈を加え、git log のダンプのような文章にならないよう配慮)。
  • RC期間終了後に安定版をリリース。
  • 内部Slack向けアナウンスとソーシャル投稿の下書き作成。
  • リリース後に main を次の dev0 に更新するPRを作成。

新バージョンの良いノートを書くことが最も重い作業で、異なるトピックの数十件のPRを集約する必要がありました。技術的に難しいことではありませんが、数時間の集中した作業が必要です。アナウンス作成も加えると、マイナーリリースは数日にわたって半日程度の作業となっていました。

2種類の作業

そこで全体の流れを効率化することにしました。リストを見ると、作業は2つに分けられます。

純粋に機械的な作業は自動化可能です:バージョンの更新、コミット、タグ付け、プッシュ、下流テストブランチの作成、リリース後のPR作成。これらは考える必要がなく、正しい順序で実行されればよいため、CIワークフローが得意とする領域です。

残りは異なります。リリースノートの作成、強調する内容の決定、人間向けのアナウンス文の作成:これらは頭脳労働です。この判断が必要な部分が、長年リリースを手作業に留めていた理由です。ここでAIが登場し、白紙のページを数秒でしっかりした初稿に変換します。ただし、自信たっぷりに見えて微妙に誤った下書きは、ないより悪いため、注意が必要です。

設計原則:誰でも再利用可能なオープンな部品

これを解決するにあたり、最初に1つの制約を設けました:すべての可動部品は、どのメンテナーでも自分で実行できるものでなければならない。交換できないAPIの裏側にあるクローズドモデル、独自のリリースプラットフォーム、秘伝のタレは使わない。

使用したスタックは以下の通りです:

2つ目の原則:モデルが下書きし、人間が決定する。言語モデルは30件の簡潔なPRタイトルを読みやすいリリースノートに変換するのは得意です。しかし、盲目的に信頼されることは得意ではありません。そこでワークフローは人間が監督します:モデルが初回パスを実行し、決定論的なスクリプトがその結果をチェックし、人間がレビュー・編集してから公開します(詳細は後述)。

パイプラインの概要

ワークフローは単一のファイル .github/workflows/release.yml で、Actions UIから手動でトリガーされます。入力は1つだけです:

on:
  workflow_dispatch:
    inputs:
      release_type:
        type: choice
        options:
          - minor-prerelease   # cut an RC from main
          - minor-release      # promote the RC to final
          - patch-release      # bugfix on an existing release branch

そこからジョブは概ね以下の順で実行されます:

  • Prepare. 次のバージョンを計算し、リリースブランチを作成または再利用、__version__ を更新、コミット、タグ付け、プッシュ。
  • Publish to PyPI. huggingface_hub をビルドしてアップロード。同時に hf CLIを独立したPyPIパッケージとしてビルド・アップロード。
  • Release notes. 前回のタグ以降のコミット範囲を差分取得し、GitHub APIからPRメタデータを取得、モデルに構造化されたチェンジログの下書きを作成させる(最近の例)。ドラフトのGitHubリリースとして保存。
  • Downstream test branches. RCの場合、transformersdatasetsdiffuserssentence-transformers にRCをピン留めしたブランチを作成し、破壊的変更を素早く検知。
  • Slack announcement. ノートを読み込み、チームのトーンで内部アナウンスを作成。
  • Archive notes. AIの生ドラフトと人間が編集したバージョンの両方をHugging Face Bucketに並べてアップロード。
  • Post-release bump. 安定版リリース後に、main を次の dev0 に更新するPRを作成。
  • Comment on shipped PRs. リリースに含まれるすべてのPRに「this shipped in vX.Y.Z」というコメントを残す。
  • Sync CLI docs. 生成された hf CLIのskillドキュメントで skills リポジトリにPRを作成。
  • Report to Slack. 各ステップがスレッド返信としてステータスを投稿し、最終ジョブがルートメッセージを✅または❌で更新。

残りの手動ステップは、ドラフトリリースノートのレビューと公開、および内部Slackメッセージのレビューと投稿です。この2ステップに人間を関与させています。

信用せよ、しかし検証せよ:人間参加の核心

AI生成のリリースノートで誰もが懸念するのは、モデルがPRを静かに削除したり、このリリースに含まれないPRを発明したりすることです。ほぼ正しいチェンジログは、誰も再確認しないため、ないチェンジログより悪いです。

生成されたリリースノートを初回で完全とは信頼せず、決定論的に検証します。モデルが実行される前に、Pythonスクリプトがリリースに属するすべてのPRを取得し、根拠として保存します。

# Deterministic: extract PR numbers from squash-merge commits in the range.
PR_NUMBER_PATTERN = re.compile(r"\(#(\d+)\)$")

pr_numbers = [
    int(m.group(1))
    for commit in commits_since_last_tag
    if (m := PR_NUMBER_PATTERN.search(commit.title))
]
save_manifest(pr_numbers)  # the source of truth

その後、モデルがそれらからノートを作成します。完了後、初期のPRリストと出力をチェックします:

expected = set(load_manifest())          # what should be there
found    = extract_pr_refs(notes_md)     # what the model wrote (#1234 -> 1234)

missing = expected - found               # silently dropped
extra   = found - expected               # belongs to a different release

欠落や余分がある場合、失敗させたり誤ったファイルを公開したりせず、矛盾点をエージェントに返して該当PRのみ修正を依頼します:

for _ in range(MAX_ITERATIONS):
    missing, extra = validate(notes)
    if not missing and not extra:
        break  # matches the manifest exactly
    run_agent_fix(missing_prs=missing, extra_prs=extra)

これが全体を信頼できるものにするパターンです:非決定論的なモデルを決定論的なガードレールで包む。モデルは散文を書くのは得意ですが、網羅性に欠けます。そこでモデルに書かせ、コードで一貫性を強制します。

モデルを根拠づけ、でっち上げを防ぐ

完全性は半分です。正確性がもう半分です。タイトルだけでPRを要約するモデルは、実際のAPIと一致しないコード例を堂々とでっち上げます。

それを防ぐため、PRメタデータを取得する際に、各PRが触れた docs/ 配下の .md ファイルの実際のドキュメント差分(unified diff)も取得します。

def fetch_doc_diffs(pr):
    return [
        {"filename": f.filename, "status": f.status, "patch": f.patch}
        for f in pr.get_files()
        if f.filename.startswith("docs/") and f.filename.endswith(".md") and f.patch
    ]

この差分をモデルのコンテキストに入れることで、「新しいCLIコマンドはこちらです」と書く際に、PR作成者が実際にドキュメントに書いた例を引用するようにします。これは前述と同じロジックです:モデルに実際のソース素材と狭いタスクを与える。

プロンプト自体は Skills としてリポジトリにチェックインされた小さなMarkdownファイル(SKILL.md と参照テンプレート)で管理されています。リリースノートskillには、ハイライトの選び方、セクションの構造化方法、docリンクの追加タイミングなどが明記されており、オンボーディング手順のように読めます。これが適切なメンタルモデルです。

人間によるチェックポイント

RCが公開された後、ドラフトのGitHubリリースにAIの初回パスが残ります。ここで人間が関与します:

  1. レビュアーがドラフトを読み、トーンや強調点を編集し、モデルが過大・過小評価した点を修正。
  2. その後で初めて minor-release 実行をトリガーし、RCを正式版に昇格。

レビュアーの時間は洗練に使われ、半日の執筆作業が15分の編集セッションに短縮されます。

また、改善のために記録を残します。AIの生ドラフト(RC時点で誰も触れていない状態)と、人間が編集したバージョン(正式リリース時にアップロード)をHugging Face Bucketに並べてアーカイブします。

# at RC time: straight from the model, untouched
hf cp release_notes_raw.txt    "hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_raw.txt"

# at release time: after the human review
hf cp release_notes_edited.txt "hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_edited.txt"

毎週両方を収集することで、「モデルが書いたもの」と「私たちが望んだもの」のデータセットが蓄積され、エージェントのskill更新に再利用できます。

オープンでセキュアな基盤

リリースプロセスの刷新は、サプライチェーン攻撃対策としてセキュリティを強化する良い機会でした。

PyPIトークンなし。 公開には Trusted Publishing を使用:PyPIはこのワークフロー専用のGitHubが発行した短命のOIDCトークンを検証し、すべての成果物に PEP 740 のattestations / Sigstore provenanceを発行します。漏洩やローテーションが必要な長期シークレットはありません。

permissions:
  id-token: write       # mint the OIDC token for PyPI
  attestations: write   # generate Sigstore provenance
# ...
- uses: pypa/[email protected]
  with:
    attestations: true  # no password, no API token, just OIDC

エージェントランタイムは固定・検証済み。 最新のOpenCodeを curl | bash して実行することはしません。バージョンを固定し、実行前にSHA256を検証します:

curl -fsSL https://opencode.ai/install | bash -s -- --version "${OPENCODE_VERSION}"
echo "${OPENCODE_SHA256}  $(which opencode)" | sha256sum -c -

オープンなツールであっても、注意を怠らないツールであるべきです。

コストはどれくらいか

ほぼゼロです。完全なリリース(ノートとSlackアナウンス、20〜40件のPRと数回のプロンプト)でInference Providersの費用は約 $0.25 です。オープンウェイトを従量課金で利用する場合、毎週の唯一の質問は「リリースする価値があるか?」であり、常に価値があります。

実際に変わったこと

リリース周期は4〜6週間に1回から毎週に変わりました。二次的な効果も興味深いものでした:

  • ノートは悪化せず、改善した。 初稿が常に存在するため、レビューの時間は洗練に使われます。グループ化がより一貫し、見落としも減りました。
  • 破壊的変更が早期に表面化。 毎RCで下流テストブランチを作成することで、候補版ウィンドウ中に統合問題を検知できます。
  • コントリビュータのループが短縮。 自動の「shipped in vX.Y.Z」コメントは予想以上に重要でした。クローズしたPRに誰かが問題を報告した際、どのリリースに修正が入ったかが即座にわかります。以前は手動でタグを探す必要がありました。

ご自身のものに

私たちが最も重視した部分です。ワークフローは huggingface_hub を基にしていますが、構造は汎用です。

ほぼそのまま再利用可能:

  • トリガーとバージョン更新ロジック(minor-prereleaseminor-releasepatch-release)。
  • 信用せよ、しかし検証せよのループ:決定論的なマニフェスト、モデル下書き、検証、再プロンプト。これは生成対象に依存しない、転用可能なアイデアです。
  • OIDC Trusted Publishing、固定・チェックサム検証済みランタイム、Slackスレッディング。
  • skillベースのプロンプト:テンプレートを入れ替え、構造を維持。

当社固有:

  • 下流リポジトリ一覧と依存関係のピン留め形式。
  • skill内の正確なセクション分類とトーン。
  • Slackとbucketの宛先。

適応方法: ワークフローファイルスクリプト をフォークし、パッケージを指定、プロジェクトのトーンに合わせて skill Markdown を書き換え、2つのリポジトリ変数(モデルIDとOpenCodeバージョン)を設定、PyPIでTrusted Publishingを設定、下流がない場合は下流テストジョブを削除します。信用せよ、しかし検証せよのループは、そのまま再利用する価値がある部分です。これが生成物を安全に公開できる理由です。

今後の予定

  • 下流障害の自動トリアージ。 現在はテストブランチを開き、人間がCIを確認しています。次の明らかなステップは、失敗ログを確認して内部Slackメッセージに報告することです。
  • パターンの拡張。 この大部分は汎用です。エコシステム内の他のPythonライブラリでも大部分を再利用する予定です。

要点

以前は人間が半日集中して行っていたリリース作業(ノート作成、アナウンス下書き、下流チェックの調整)は、モデルが下書きを得意とする領域です。それ以外は機械的でYAMLファイルに収まります。コツは単に「AIにやらせる」ことではありません。モデルに下書きさせ、決定論的なコードで検証し、人間に決定させることです。すべてオープンなツールとオープンウェイトで構築されているため、コストはほぼゼロで、誰でも実行可能です。

ワークフローファイルは公開されています。Pythonライブラリをメンテナンスされている方は、フォークして適応し、結果をお知らせください!