{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-23-shipping-huggingface-hub-every-week-with-ai-open-tools-and/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "`huggingface_hub` is the Python client at the base of the Hugging Face ecosystem. `transformers`, `datasets`, `diffusers`, `sentence-transformers` and dozens of other libraries depend on it to talk to the Hub. Every week we don't ship a new release is a week of fixes and features stuck on `main`.\n\nFor a long time we released every 4 to 6 weeks. We now release every week from a single GitHub Actions workflow. We built it using open-source tools and open-weights models and kept a human in the loop at the one place where judgment matters. Nothing in this post requires a vendor contract, a closed model, or infrastructure you can't run yourself. That was a design goal from the start since we wanted a workflow other maintainers could pick up and adapt.\n\nBy the end of this post, you'll have everything you need to build your own.\n\n## Where we started\n\nThe old process was partly automated, mostly manual.\n\nAlready in CI:\n\n- Publishing to PyPI once a tag was pushed.\n- Opening test branches in downstream libraries with the release candidate pinned.\n\nStill manual, every single time:\n\n- Creating the release branch, bumping the version in `__init__.py`, committing, tagging, pushing.\n- Watching the downstream CI runs and triaging failures.\n- Reading through every PR merged since the last release and writing release notes by hand: grouped by theme, with context, in a voice that didn't read like a `git log` dump.\n- Cutting the stable release after the RC period.\n- Drafting an internal Slack announcement and social posts.\n- Opening the post-release PR to bump `main` to the next `dev0`.\n\nWriting good notes for a new version was the heavy part, aggregating tens of PRs on different topics. Nothing technically hard but a few hours of focused attention. Add the announcements on top and a minor release was easily a half-day of work spread over several days.\n\n## Two kinds of work\n\nSo we decided to streamline the whole thing. Looking at that list, the work splits in two.\n\nSome steps are purely mechanical and can be automated: bumping the version, committing, tagging, pushing, opening downstream test branches, opening the post-release PR. Nobody needs to think about those. They just have to happen in the right order, every time, which is what a CI workflow is good at.\n\nThe rest is different. Writing release notes, deciding what to highlight, phrasing an announcement for a human audience: that's brain work. It's the kind of judgment that kept the release manual for years. This is where AI comes in, turning a blank page into a solid first draft in seconds. It's also where we have to be careful because a draft that looks confident and is subtly wrong is worse than no draft at all.\n\n## The design principle: open parts, reusable by anyone\n\nWhen we decided to fix this, we set one constraint up front: every moving part had to be something any maintainer could run themselves. No closed model behind an API we couldn't swap, no proprietary release platform, no secret sauce.\n\nHere's the entire stack:\n\n| Part                                                                                               | What it does                                    |\n| -------------------------------------------------------------------------------------------------- | ----------------------------------------------- |\n| **GitHub Actions**                                                                                 | Orchestrates the whole release                  |\n| **[OpenCode](https://opencode.ai/)**                                                              | Agent runtime that drives the model             |\n| **An open-weights model** (currently [GLM-5.2](https://huggingface.co/zai-org/GLM-5.2) from Z.ai) | Drafts the release notes and Slack announcement |\n| **[HF Inference Providers](https://huggingface.co/docs/inference-providers/index)**               | Serves the model                                |\n| **[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)**                          | Publishes the package                           |\n\nThe second principle: the model drafts, a human decides. Language models are good at turning thirty terse PR titles into readable release notes. They are not good at being trusted blindly. So the workflow is human-supervised: the model does the first pass, a deterministic script checks its work, and a human reviews and edits before anything ships (more on that below).\n\n## A tour of the pipeline\n\nThe full workflow is a single file, [`.github/workflows/release.yml`](https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml), triggered by hand from the Actions UI. It takes exactly one input:\n\n```yaml\non:\n  workflow_dispatch:\n    inputs:\n      release_type:\n        type: choice\n        options:\n          - minor-prerelease   # cut an RC from main\n          - minor-release      # promote the RC to final\n          - patch-release      # bugfix on an existing release branch\n```\n\nFrom there, the jobs run roughly in this order:\n\n- **Prepare.** Compute the next version, create or reuse the release branch, bump `__version__`, commit, tag, push.\n- **Publish to PyPI.** Build and upload `huggingface_hub`. In parallel, build and upload the `hf` CLI as its own PyPI package.\n- **Release notes.** Diff the commit range since the last tag, pull PR metadata from the GitHub API, and have the model draft a structured changelog ([here's a recent one](https://github.com/huggingface/huggingface_hub/releases/tag/v1.20.0)). Saved as a *draft* GitHub release.\n- **Downstream test branches.** For RCs, open a branch in `transformers`, `datasets`, `diffusers`, `sentence-transformers` with the RC pinned, so their CI tells us fast if we broke something.\n- **Slack announcement.** Read the notes and produce an internal announcement in our team voice.\n- **Archive notes.** Upload both the raw AI draft and the human-edited version to a Hugging Face Bucket, side by side.\n- **Post-release bump.** After a stable release, open a PR on `main` bumping to the next `dev0`.\n- **Comment on shipped PRs.** Leave a \"this shipped in vX.Y.Z\" comment on every PR in the release.\n- **Sync CLI docs.** Open a PR to our [skills](https://github.com/huggingface/skills) repo with the regenerated `hf` CLI skill docs.\n- **Report to Slack.** Every step posts its status as a thread reply; a final job updates the root message with ✅ or ❌.\n\nThe remaining manual steps are reviewing and publishing the draft release notes, and reviewing and posting an internal Slack message. Those two steps are where we want a human in the loop.\n\n## Trust but verify: the human-in-the-loop core\n\nHere's the failure mode everyone worries about with AI-generated release notes: the model quietly drops a PR or invents one that isn't in this release. A changelog that's almost right is worse than no changelog because nobody re-checks it.\n\nWe don't trust the generated release notes to be complete on the first try, we verify it deterministically. Before the model runs, a Python script retrieves all PRs that belong to the release and stores them as ground truth.\n\n```python\n# Deterministic: extract PR numbers from squash-merge commits in the range.\nPR_NUMBER_PATTERN = re.compile(r\"\\(#(\\d+)\\)$\")\n\npr_numbers = [\n    int(m.group(1))\n    for commit in commits_since_last_tag\n    if (m := PR_NUMBER_PATTERN.search(commit.title))\n]\nsave_manifest(pr_numbers)  # the source of truth\n```\n\nThen model drafts the notes from them. Once done, we check its output against the initial list of PRs:\n\n```python\nexpected = set(load_manifest())          # what should be there\nfound    = extract_pr_refs(notes_md)     # what the model wrote (#1234 -> 1234)\n\nmissing = expected - found               # silently dropped\nextra   = found - expected               # belongs to a different release\n```\n\nIf anything is missing or extra, we don't fail and we don't ship a wrong file. We hand the discrepancy back to the agent and ask it to fix exactly those PRs:\n\n```python\nfor _ in range(MAX_ITERATIONS):\n    missing, extra = validate(notes)\n    if not missing and not extra:\n        break  # matches the manifest exactly\n    run_agent_fix(missing_prs=missing, extra_prs=extra)\n```\n\nThis is the pattern that makes the whole thing trustworthy: a non-deterministic model wrapped in deterministic guardrails. The model is great at writing prose and unreliable at being exhaustive. So we let it write and let code enforce the consistency.\n\n## Grounding the model so it doesn't make things up\n\nCompleteness is one half. Accuracy is the other. A model summarizing a PR from its title alone will cheerfully invent a code example that doesn't match the real API.\n\nTo prevent that, when we fetch PR metadata we also pull the actual documentation diffs from each PR: the unified diff of any `.md` file under `docs/` that the PR touched.\n\n```python\ndef fetch_doc_diffs(pr):\n    return [\n        {\"filename\": f.filename, \"status\": f.status, \"patch\": f.patch}\n        for f in pr.get_files()\n        if f.filename.startswith(\"docs/\") and f.filename.endswith(\".md\") and f.patch\n    ]\n```\n\nThat diff goes into the model's context so when it writes \"here's the new CLI command,\" it's quoting the example the PR author actually wrote in the docs. That's the same logic as before: give the model real source material and a narrow job.\n\nThe prompts themselves live as [Skills](https://github.com/huggingface/huggingface_hub/tree/main/.opencode/skills/hf-release-notes): small Markdown files (`SKILL.md` plus reference templates) checked into the repo. The release-notes skill spells out how to pick highlights, how to structure sections, when to add a doc link, etc. It reads like onboarding instructions, which is exactly the right mental model.\n\n## The human checkpoint\n\nAfter the RC is published, the draft GitHub release sits there with the AI's first pass in it. This is where the human comes in:\n\n1. A reviewer reads the draft, edits for tone and emphasis, fixes anything the model over- or under-weighted.\n2. Only then do they trigger the `minor-release` run, which promotes the RC to final.\n\nThe reviewer's time goes into polishing, turning a half-day of writing into a fifteen-minute editing session.\n\nWe also keep a paper trail to improve over time. We archive two files side by side to a Hugging Face Bucket: the raw AI draft, uploaded at RC time before anyone touches it, and the human-edited version, uploaded when the final release is cut.\n\n```bash\n# at RC time: straight from the model, untouched\nhf cp release_notes_raw.txt    \"hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_raw.txt\"\n\n# at release time: after the human review\nhf cp release_notes_edited.txt \"hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_edited.txt\"\n```\n\nCollecting both every week gives us a growing dataset of \"what the model wrote\" versus \"what we wished it wrote\". Dataset that we can then reuse to update the agent's skill.\n\n## Open and secure plumbing\n\nRevamping the release process was a good opportunity to tighten security, especially against supply-chain attacks.\n\n**No PyPI token.** Publishing uses [Trusted Publishing](https://docs.pypi.org/trusted-publishers/): PyPI verifies a short-lived OIDC token minted by GitHub for this exact workflow, and issues [PEP 740](https://peps.python.org/pep-0740/) attestations / Sigstore provenance for every artifact. There's no long-lived secret to leak or rotate.\n\n```yaml\npermissions:\n  id-token: write       # mint the OIDC token for PyPI\n  attestations: write   # generate Sigstore provenance\n# ...\n- uses: pypa/gh-action-pypi-publish@v1.14.0\n  with:\n    attestations: true  # no password, no API token, just OIDC\n```\n\n**The agent runtime is pinned and verified.** We don't `curl | bash` the latest OpenCode and hope. We pin a version and check its SHA256 before running it:\n\n```bash\ncurl -fsSL https://opencode.ai/install | bash -s -- --version \"${OPENCODE_VERSION}\"\necho \"${OPENCODE_SHA256}  $(which opencode)\" | sha256sum -c -\n```\n\nOpen tooling doesn't mean careless tooling.\n\n## So, what did it cost?\n\nAlmost nothing. A full release (notes plus the Slack announcement, across 20-40 PRs and a few rounds of prompting) costs about **$0.25** on Inference Providers. With open weights billed pay-as-you-go, the only real question each week is \"is there something worth shipping?\", and there always is.\n\n## What changed in practice\n\nThe cadence went from one release every 4 to 6 weeks to once a week. The secondary effects were the interesting ones:\n\n- **Notes got better, not worse.** A first draft always exists, so review time goes to polishing. Grouping is more consistent and we omit fewer things.\n- **Breakages surface earlier.** Downstream test branches on every RC catch integration issues during the candidate window.\n- **Contributor loops shortened.** The automatic \"shipped in vX.Y.Z\" comment turned out to matter more than we expected. When someone reports an issue on a closed PR, everyone can immediately see which release the fix is in. That used to be a manual tag hunt.\n\n## Make it yours\n\nThis is the part we cared about most. The workflow is shaped around `huggingface_hub` but the structure is generic.\n\n**Reusable almost as-is:**\n\n- The trigger and version-bump logic (`minor-prerelease` then `minor-release` then `patch-release`).\n- The trust-but-verify loop: deterministic manifest, model draft, validate, re-prompt. This is the transferable idea, independent of what you're generating.\n- OIDC Trusted Publishing, pinned and checksum-verified runtime, Slack threading.\n- The skill-based prompts: swap the templates, keep the structure.\n\n**Specific to us:**\n\n- The downstream repo list and their dependency-pin formats.\n- The exact section taxonomy and tone in the skills.\n- The Slack and bucket destinations.\n\nTo adapt it: fork the [workflow file](https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml) and [scripts](https://github.com/huggingface/huggingface_hub/tree/main/utils/release_notes), point it at your package, rewrite the [skill Markdown](https://github.com/huggingface/huggingface_hub/blob/main/.opencode/skills/hf-release-notes/SKILL.md) for your project's voice, set two repo variables (the model ID and your OpenCode version), set up Trusted Publishing on PyPI, and delete the downstream-testing job if you don't have downstreams. The trust-but-verify loop is the part worth reusing as-is. It's what makes a generated artifact safe to ship.\n\n## What's next\n\n- **Auto-triaging downstream failures.** Today the workflow opens test branches and a human reads the CI. An obvious next step is to check the failing logs to report them in the internal slack message.\n- **Extending the pattern.** Most of this is generic. We expect to reuse large parts across other Python libraries in our ecosystem.\n\n## Takeaway\n\nThe parts of a release that used to need a half-day of focused human work (writing notes, drafting announcements, coordinating downstream checks) are the parts a model is good at drafting. Everything else is mechanical and fits in a YAML file. The trick was never just \"let the AI do it\". It's to let the model draft, let deterministic code verify, and let a human decide. It's built entirely from open tools and open weights so the cost rounds to zero and anyone can run it.\n\nThe full workflow file is public. If you maintain a Python library, [fork it](https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml), adapt it, and let us know how it goes!\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<p><code>huggingface_hub</code> is the Python client at the base of the Hugging Face ecosystem. <code>transformers</code>, <code>datasets</code>, <code>diffusers</code>, <code>sentence-transformers</code> and dozens of other libraries depend on it to talk to the Hub. Every week we don't ship a new release is a week of fixes and features stuck on <code>main</code>.</p>\n<p>For a long time we released every 4 to 6 weeks. We now release every week from a single GitHub Actions workflow. We built it using open-source tools and open-weights models and kept a human in the loop at the one place where judgment matters. Nothing in this post requires a vendor contract, a closed model, or infrastructure you can't run yourself. That was a design goal from the start since we wanted a workflow other maintainers could pick up and adapt.</p>\n<p>By the end of this post, you'll have everything you need to build your own.</p>\n<h2>Where we started</h2>\n<p>The old process was partly automated, mostly manual.</p>\n<p>Already in CI:</p>\n<ul>\n<li>Publishing to PyPI once a tag was pushed.</li>\n<li>Opening test branches in downstream libraries with the release candidate pinned.</li>\n</ul>\n<p>Still manual, every single time:</p>\n<ul>\n<li>Creating the release branch, bumping the version in <code>__init__.py</code>, committing, tagging, pushing.</li>\n<li>Watching the downstream CI runs and triaging failures.</li>\n<li>Reading through every PR merged since the last release and writing release notes by hand: grouped by theme, with context, in a voice that didn't read like a <code>git log</code> dump.</li>\n<li>Cutting the stable release after the RC period.</li>\n<li>Drafting an internal Slack announcement and social posts.</li>\n<li>Opening the post-release PR to bump <code>main</code> to the next <code>dev0</code>.</li>\n</ul>\n<p>Writing good notes for a new version was the heavy part, aggregating tens of PRs on different topics. Nothing technically hard but a few hours of focused attention. Add the announcements on top and a minor release was easily a half-day of work spread over several days.</p>\n<h2>Two kinds of work</h2>\n<p>So we decided to streamline the whole thing. Looking at that list, the work splits in two.</p>\n<p>Some steps are purely mechanical and can be automated: bumping the version, committing, tagging, pushing, opening downstream test branches, opening the post-release PR. Nobody needs to think about those. They just have to happen in the right order, every time, which is what a CI workflow is good at.</p>\n<p>The rest is different. Writing release notes, deciding what to highlight, phrasing an announcement for a human audience: that's brain work. It's the kind of judgment that kept the release manual for years. This is where AI comes in, turning a blank page into a solid first draft in seconds. It's also where we have to be careful because a draft that looks confident and is subtly wrong is worse than no draft at all.</p>\n<h2>The design principle: open parts, reusable by anyone</h2>\n<p>When we decided to fix this, we set one constraint up front: every moving part had to be something any maintainer could run themselves. No closed model behind an API we couldn't swap, no proprietary release platform, no secret sauce.</p>\n<p>Here's the entire stack:</p>\n<table>\n<thead>\n<tr>\n<th>Part</th>\n<th>What it does</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>GitHub Actions</strong></td>\n<td>Orchestrates the whole release</td>\n</tr>\n<tr>\n<td><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://opencode.ai/\">OpenCode</a></strong></td>\n<td>Agent runtime that drives the model</td>\n</tr>\n<tr>\n<td><strong>An open-weights model</strong> (currently <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/zai-org/GLM-5.2\">GLM-5.2</a> from Z.ai)</td>\n<td>Drafts the release notes and Slack announcement</td>\n</tr>\n<tr>\n<td><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/inference-providers/index\">HF Inference Providers</a></strong></td>\n<td>Serves the model</td>\n</tr>\n<tr>\n<td><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.pypi.org/trusted-publishers/\">PyPI Trusted Publishing</a></strong></td>\n<td>Publishes the package</td>\n</tr>\n</tbody>\n</table>\n<p>The second principle: the model drafts, a human decides. Language models are good at turning thirty terse PR titles into readable release notes. They are not good at being trusted blindly. So the workflow is human-supervised: the model does the first pass, a deterministic script checks its work, and a human reviews and edits before anything ships (more on that below).</p>\n<h2>A tour of the pipeline</h2>\n<p>The full workflow is a single file, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml\"><code>.github/workflows/release.yml</code></a>, triggered by hand from the Actions UI. It takes exactly one input:</p>\n<pre><code class=\"language-yaml\"><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-constant syntax-language syntax-boolean syntax-yaml\">on</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">workflow_dispatch</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n    <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">inputs</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n      <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">release_type</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n        <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">type</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">choice</span>\n        <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">options</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n          <span class=\"syntax-punctuation syntax-definition syntax-block syntax-sequence syntax-item syntax-yaml\">-</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">minor-prerelease</span>   <span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> cut an RC from main\n</span>          <span class=\"syntax-punctuation syntax-definition syntax-block syntax-sequence syntax-item syntax-yaml\">-</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">minor-release</span>      <span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> promote the RC to final\n</span>          <span class=\"syntax-punctuation syntax-definition syntax-block syntax-sequence syntax-item syntax-yaml\">-</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">patch-release</span>      <span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> bugfix on an existing release branch\n</span></span></span></code></pre>\n<p>From there, the jobs run roughly in this order:</p>\n<ul>\n<li><strong>Prepare.</strong> Compute the next version, create or reuse the release branch, bump <code>__version__</code>, commit, tag, push.</li>\n<li><strong>Publish to PyPI.</strong> Build and upload <code>huggingface_hub</code>. In parallel, build and upload the <code>hf</code> CLI as its own PyPI package.</li>\n<li><strong>Release notes.</strong> Diff the commit range since the last tag, pull PR metadata from the GitHub API, and have the model draft a structured changelog (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/releases/tag/v1.20.0\">here's a recent one</a>). Saved as a <em>draft</em> GitHub release.</li>\n<li><strong>Downstream test branches.</strong> For RCs, open a branch in <code>transformers</code>, <code>datasets</code>, <code>diffusers</code>, <code>sentence-transformers</code> with the RC pinned, so their CI tells us fast if we broke something.</li>\n<li><strong>Slack announcement.</strong> Read the notes and produce an internal announcement in our team voice.</li>\n<li><strong>Archive notes.</strong> Upload both the raw AI draft and the human-edited version to a Hugging Face Bucket, side by side.</li>\n<li><strong>Post-release bump.</strong> After a stable release, open a PR on <code>main</code> bumping to the next <code>dev0</code>.</li>\n<li><strong>Comment on shipped PRs.</strong> Leave a &quot;this shipped in vX.Y.Z&quot; comment on every PR in the release.</li>\n<li><strong>Sync CLI docs.</strong> Open a PR to our <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/skills\">skills</a> repo with the regenerated <code>hf</code> CLI skill docs.</li>\n<li><strong>Report to Slack.</strong> Every step posts its status as a thread reply; a final job updates the root message with ✅ or ❌.</li>\n</ul>\n<p>The remaining manual steps are reviewing and publishing the draft release notes, and reviewing and posting an internal Slack message. Those two steps are where we want a human in the loop.</p>\n<h2>Trust but verify: the human-in-the-loop core</h2>\n<p>Here's the failure mode everyone worries about with AI-generated release notes: the model quietly drops a PR or invents one that isn't in this release. A changelog that's almost right is worse than no changelog because nobody re-checks it.</p>\n<p>We don't trust the generated release notes to be complete on the first try, we verify it deterministically. Before the model runs, a Python script retrieves all PRs that belong to the release and stores them as ground truth.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Deterministic: extract PR numbers from squash-merge commits in the range.\n</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">PR_NUMBER_PATTERN</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">re</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">compile</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-storage syntax-type syntax-string syntax-python\">r</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-source syntax-regexp syntax-python\"><span class=\"syntax-constant syntax-character syntax-escape syntax-backslash syntax-regexp\">\\(</span>#<span class=\"syntax-meta syntax-group syntax-regexp\"><span class=\"syntax-punctuation syntax-definition syntax-group syntax-begin syntax-regexp\">(</span><span class=\"syntax-constant syntax-character syntax-character-class syntax-regexp\">\\d</span><span class=\"syntax-keyword syntax-operator syntax-quantifier syntax-regexp\">+</span><span class=\"syntax-punctuation syntax-definition syntax-group syntax-end syntax-regexp\">)</span></span><span class=\"syntax-constant syntax-character syntax-escape syntax-backslash syntax-regexp\">\\)</span><span class=\"syntax-keyword syntax-control syntax-anchor syntax-regexp\">$</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">pr_numbers</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span>\n    <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">int</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">m</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">group</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n    <span class=\"syntax-meta syntax-expression syntax-generator syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-generator syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">commit</span> <span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">commits_since_last_tag</span></span>\n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-if syntax-inline syntax-python\">if</span> <span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">m</span></span> := <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">PR_NUMBER_PATTERN</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">search</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">commit</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">title</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">save_manifest</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">pr_numbers</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> the source of truth\n</span></span></span></code></pre>\n<p>Then model drafts the notes from them. Once done, we check its output against the initial list of PRs:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">expected</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">set</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">load_manifest</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>          <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> what should be there\n</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">found</span></span>    <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">extract_pr_refs</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">notes_md</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>     <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> what the model wrote (#1234 -&gt; 1234)\n</span>\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">missing</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">expected</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">found</span></span>               <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> silently dropped\n</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">extra</span></span>   <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">found</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">expected</span></span>               <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> belongs to a different release\n</span></span></span></code></pre>\n<p>If anything is missing or extra, we don't fail and we don't ship a wrong file. We hand the discrepancy back to the agent and ask it to fix exactly those PRs:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-variable syntax-language syntax-python\">_</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-function syntax-builtin syntax-python\">range</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-other syntax-constant syntax-python\">MAX_ITERATIONS</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">missing</span></span>, <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">extra</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">validate</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">notes</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n    <span class=\"syntax-meta syntax-statement syntax-if syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">if</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">not</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">missing</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">and</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">not</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">extra</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-conditional syntax-python\">:</span></span>\n        <span class=\"syntax-keyword syntax-control syntax-flow syntax-python\">break</span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> matches the manifest exactly\n</span>    <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">run_agent_fix</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">missing_prs</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">missing</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">extra_prs</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">extra</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>This is the pattern that makes the whole thing trustworthy: a non-deterministic model wrapped in deterministic guardrails. The model is great at writing prose and unreliable at being exhaustive. So we let it write and let code enforce the consistency.</p>\n<h2>Grounding the model so it doesn't make things up</h2>\n<p>Completeness is one half. Accuracy is the other. A model summarizing a PR from its title alone will cheerfully invent a code example that doesn't match the real API.</p>\n<p>To prevent that, when we fetch PR metadata we also pull the actual documentation diffs from each PR: the unified diff of any <code>.md</code> file under <code>docs/</code> that the PR touched.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">fetch_doc_diffs</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">pr</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-return syntax-python\">return</span> <span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span>\n        <span class=\"syntax-meta syntax-structure syntax-dictionary-or-set syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary-or-set syntax-begin syntax-python\">{</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">filename<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">filename</span></span><span class=\"syntax-punctuation syntax-separator syntax-dictionary-or-set syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">status<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">status</span></span><span class=\"syntax-punctuation syntax-separator syntax-dictionary-or-set syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">patch<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">patch</span></span><span class=\"syntax-punctuation syntax-section syntax-dictionary-or-set syntax-end syntax-python\">}</span></span>\n        <span class=\"syntax-meta syntax-expression syntax-generator syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-generator syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">f</span> <span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">pr</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">get_files</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n        <span class=\"syntax-keyword syntax-control syntax-flow syntax-if syntax-inline syntax-python\">if</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">filename</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">startswith</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">docs/<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">and</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">filename</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">endswith</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">.md<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">and</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">f</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">patch</span></span>\n    <span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span>\n</span></span></code></pre>\n<p>That diff goes into the model's context so when it writes &quot;here's the new CLI command,&quot; it's quoting the example the PR author actually wrote in the docs. That's the same logic as before: give the model real source material and a narrow job.</p>\n<p>The prompts themselves live as <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/tree/main/.opencode/skills/hf-release-notes\">Skills</a>: small Markdown files (<code>SKILL.md</code> plus reference templates) checked into the repo. The release-notes skill spells out how to pick highlights, how to structure sections, when to add a doc link, etc. It reads like onboarding instructions, which is exactly the right mental model.</p>\n<h2>The human checkpoint</h2>\n<p>After the RC is published, the draft GitHub release sits there with the AI's first pass in it. This is where the human comes in:</p>\n<ol>\n<li>A reviewer reads the draft, edits for tone and emphasis, fixes anything the model over- or under-weighted.</li>\n<li>Only then do they trigger the <code>minor-release</code> run, which promotes the RC to final.</li>\n</ol>\n<p>The reviewer's time goes into polishing, turning a half-day of writing into a fifteen-minute editing session.</p>\n<p>We also keep a paper trail to improve over time. We archive two files side by side to a Hugging Face Bucket: the raw AI draft, uploaded at RC time before anyone touches it, and the human-edited version, uploaded when the final release is cut.</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> at RC time: straight from the model, untouched</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> cp release_notes_raw.txt    <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>hf://buckets/huggingface/releases/huggingface_hub/<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell\">{</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">V</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell\">}</span></span>/release_notes_raw.txt<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> at release time: after the human review</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> cp release_notes_edited.txt <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>hf://buckets/huggingface/releases/huggingface_hub/<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell\">{</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">V</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell\">}</span></span>/release_notes_edited.txt<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n</span></span></code></pre>\n<p>Collecting both every week gives us a growing dataset of &quot;what the model wrote&quot; versus &quot;what we wished it wrote&quot;. Dataset that we can then reuse to update the agent's skill.</p>\n<h2>Open and secure plumbing</h2>\n<p>Revamping the release process was a good opportunity to tighten security, especially against supply-chain attacks.</p>\n<p><strong>No PyPI token.</strong> Publishing uses <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publishing</a>: PyPI verifies a short-lived OIDC token minted by GitHub for this exact workflow, and issues <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://peps.python.org/pep-0740/\">PEP 740</a> attestations / Sigstore provenance for every artifact. There's no long-lived secret to leak or rotate.</p>\n<pre><code class=\"language-yaml\"><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">permissions</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">id-token</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">write</span>       <span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> mint the OIDC token for PyPI\n</span>  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">attestations</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">write</span>   <span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> generate Sigstore provenance\n</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> ...\n</span><span class=\"syntax-punctuation syntax-definition syntax-block syntax-sequence syntax-item syntax-yaml\">-</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">uses</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">pypa/gh-action-pypi-publish@v1.14.0</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">with</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n    <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">attestations</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-constant syntax-language syntax-boolean syntax-yaml\">true</span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-yaml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-line syntax-number-sign syntax-yaml\">#</span> no password, no API token, just OIDC\n</span></span></span></code></pre>\n<p><strong>The agent runtime is pinned and verified.</strong> We don't <code>curl | bash</code> the latest OpenCode and hope. We pin a version and check its SHA256 before running it:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">curl</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>fsSL</span> https://opencode.ai/install</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">bash</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span><span class=\"syntax-keyword syntax-operator syntax-end-of-options syntax-shell\"> --</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> --version <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell\">{</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">OPENCODE_VERSION</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell\">}</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-echo syntax-shell\">echo</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell\">{</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">OPENCODE_SHA256</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell\">}</span></span>  <span class=\"syntax-meta syntax-group syntax-expansion syntax-command syntax-parens syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell\">(</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">which</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> opencode</span><span class=\"syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell\">)</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">sha256sum</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>c</span> -</span>\n</span></span></code></pre>\n<p>Open tooling doesn't mean careless tooling.</p>\n<h2>So, what did it cost?</h2>\n<p>Almost nothing. A full release (notes plus the Slack announcement, across 20-40 PRs and a few rounds of prompting) costs about <strong>$0.25</strong> on Inference Providers. With open weights billed pay-as-you-go, the only real question each week is &quot;is there something worth shipping?&quot;, and there always is.</p>\n<h2>What changed in practice</h2>\n<p>The cadence went from one release every 4 to 6 weeks to once a week. The secondary effects were the interesting ones:</p>\n<ul>\n<li><strong>Notes got better, not worse.</strong> A first draft always exists, so review time goes to polishing. Grouping is more consistent and we omit fewer things.</li>\n<li><strong>Breakages surface earlier.</strong> Downstream test branches on every RC catch integration issues during the candidate window.</li>\n<li><strong>Contributor loops shortened.</strong> The automatic &quot;shipped in vX.Y.Z&quot; comment turned out to matter more than we expected. When someone reports an issue on a closed PR, everyone can immediately see which release the fix is in. That used to be a manual tag hunt.</li>\n</ul>\n<h2>Make it yours</h2>\n<p>This is the part we cared about most. The workflow is shaped around <code>huggingface_hub</code> but the structure is generic.</p>\n<p><strong>Reusable almost as-is:</strong></p>\n<ul>\n<li>The trigger and version-bump logic (<code>minor-prerelease</code> then <code>minor-release</code> then <code>patch-release</code>).</li>\n<li>The trust-but-verify loop: deterministic manifest, model draft, validate, re-prompt. This is the transferable idea, independent of what you're generating.</li>\n<li>OIDC Trusted Publishing, pinned and checksum-verified runtime, Slack threading.</li>\n<li>The skill-based prompts: swap the templates, keep the structure.</li>\n</ul>\n<p><strong>Specific to us:</strong></p>\n<ul>\n<li>The downstream repo list and their dependency-pin formats.</li>\n<li>The exact section taxonomy and tone in the skills.</li>\n<li>The Slack and bucket destinations.</li>\n</ul>\n<p>To adapt it: fork the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml\">workflow file</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/tree/main/utils/release_notes\">scripts</a>, point it at your package, rewrite the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/blob/main/.opencode/skills/hf-release-notes/SKILL.md\">skill Markdown</a> for your project's voice, set two repo variables (the model ID and your OpenCode version), set up Trusted Publishing on PyPI, and delete the downstream-testing job if you don't have downstreams. The trust-but-verify loop is the part worth reusing as-is. It's what makes a generated artifact safe to ship.</p>\n<h2>What's next</h2>\n<ul>\n<li><strong>Auto-triaging downstream failures.</strong> Today the workflow opens test branches and a human reads the CI. An obvious next step is to check the failing logs to report them in the internal slack message.</li>\n<li><strong>Extending the pattern.</strong> Most of this is generic. We expect to reuse large parts across other Python libraries in our ecosystem.</li>\n</ul>\n<h2>Takeaway</h2>\n<p>The parts of a release that used to need a half-day of focused human work (writing notes, drafting announcements, coordinating downstream checks) are the parts a model is good at drafting. Everything else is mechanical and fits in a YAML file. The trick was never just &quot;let the AI do it&quot;. It's to let the model draft, let deterministic code verify, and let a human decide. It's built entirely from open tools and open weights so the cost rounds to zero and anyone can run it.</p>\n<p>The full workflow file is public. If you maintain a Python library, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml\">fork it</a>, adapt it, and let us know how it goes!</p>\n",
  "content_markdown": "`huggingface_hub` is the Python client at the base of the Hugging Face ecosystem. `transformers`, `datasets`, `diffusers`, `sentence-transformers` and dozens of other libraries depend on it to talk to the Hub. Every week we don't ship a new release is a week of fixes and features stuck on `main`.\n\nFor a long time we released every 4 to 6 weeks. We now release every week from a single GitHub Actions workflow. We built it using open-source tools and open-weights models and kept a human in the loop at the one place where judgment matters. Nothing in this post requires a vendor contract, a closed model, or infrastructure you can't run yourself. That was a design goal from the start since we wanted a workflow other maintainers could pick up and adapt.\n\nBy the end of this post, you'll have everything you need to build your own.\n\n## Where we started\n\nThe old process was partly automated, mostly manual.\n\nAlready in CI:\n\n- Publishing to PyPI once a tag was pushed.\n- Opening test branches in downstream libraries with the release candidate pinned.\n\nStill manual, every single time:\n\n- Creating the release branch, bumping the version in `__init__.py`, committing, tagging, pushing.\n- Watching the downstream CI runs and triaging failures.\n- Reading through every PR merged since the last release and writing release notes by hand: grouped by theme, with context, in a voice that didn't read like a `git log` dump.\n- Cutting the stable release after the RC period.\n- Drafting an internal Slack announcement and social posts.\n- Opening the post-release PR to bump `main` to the next `dev0`.\n\nWriting good notes for a new version was the heavy part, aggregating tens of PRs on different topics. Nothing technically hard but a few hours of focused attention. Add the announcements on top and a minor release was easily a half-day of work spread over several days.\n\n## Two kinds of work\n\nSo we decided to streamline the whole thing. Looking at that list, the work splits in two.\n\nSome steps are purely mechanical and can be automated: bumping the version, committing, tagging, pushing, opening downstream test branches, opening the post-release PR. Nobody needs to think about those. They just have to happen in the right order, every time, which is what a CI workflow is good at.\n\nThe rest is different. Writing release notes, deciding what to highlight, phrasing an announcement for a human audience: that's brain work. It's the kind of judgment that kept the release manual for years. This is where AI comes in, turning a blank page into a solid first draft in seconds. It's also where we have to be careful because a draft that looks confident and is subtly wrong is worse than no draft at all.\n\n## The design principle: open parts, reusable by anyone\n\nWhen we decided to fix this, we set one constraint up front: every moving part had to be something any maintainer could run themselves. No closed model behind an API we couldn't swap, no proprietary release platform, no secret sauce.\n\nHere's the entire stack:\n\n| Part                                                                                               | What it does                                    |\n| -------------------------------------------------------------------------------------------------- | ----------------------------------------------- |\n| **GitHub Actions**                                                                                 | Orchestrates the whole release                  |\n| **[OpenCode](https://opencode.ai/)**                                                              | Agent runtime that drives the model             |\n| **An open-weights model** (currently [GLM-5.2](https://huggingface.co/zai-org/GLM-5.2) from Z.ai) | Drafts the release notes and Slack announcement |\n| **[HF Inference Providers](https://huggingface.co/docs/inference-providers/index)**               | Serves the model                                |\n| **[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)**                          | Publishes the package                           |\n\nThe second principle: the model drafts, a human decides. Language models are good at turning thirty terse PR titles into readable release notes. They are not good at being trusted blindly. So the workflow is human-supervised: the model does the first pass, a deterministic script checks its work, and a human reviews and edits before anything ships (more on that below).\n\n## A tour of the pipeline\n\nThe full workflow is a single file, [`.github/workflows/release.yml`](https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml), triggered by hand from the Actions UI. It takes exactly one input:\n\n```yaml\non:\n  workflow_dispatch:\n    inputs:\n      release_type:\n        type: choice\n        options:\n          - minor-prerelease   # cut an RC from main\n          - minor-release      # promote the RC to final\n          - patch-release      # bugfix on an existing release branch\n```\n\nFrom there, the jobs run roughly in this order:\n\n- **Prepare.** Compute the next version, create or reuse the release branch, bump `__version__`, commit, tag, push.\n- **Publish to PyPI.** Build and upload `huggingface_hub`. In parallel, build and upload the `hf` CLI as its own PyPI package.\n- **Release notes.** Diff the commit range since the last tag, pull PR metadata from the GitHub API, and have the model draft a structured changelog ([here's a recent one](https://github.com/huggingface/huggingface_hub/releases/tag/v1.20.0)). Saved as a *draft* GitHub release.\n- **Downstream test branches.** For RCs, open a branch in `transformers`, `datasets`, `diffusers`, `sentence-transformers` with the RC pinned, so their CI tells us fast if we broke something.\n- **Slack announcement.** Read the notes and produce an internal announcement in our team voice.\n- **Archive notes.** Upload both the raw AI draft and the human-edited version to a Hugging Face Bucket, side by side.\n- **Post-release bump.** After a stable release, open a PR on `main` bumping to the next `dev0`.\n- **Comment on shipped PRs.** Leave a \"this shipped in vX.Y.Z\" comment on every PR in the release.\n- **Sync CLI docs.** Open a PR to our [skills](https://github.com/huggingface/skills) repo with the regenerated `hf` CLI skill docs.\n- **Report to Slack.** Every step posts its status as a thread reply; a final job updates the root message with ✅ or ❌.\n\nThe remaining manual steps are reviewing and publishing the draft release notes, and reviewing and posting an internal Slack message. Those two steps are where we want a human in the loop.\n\n## Trust but verify: the human-in-the-loop core\n\nHere's the failure mode everyone worries about with AI-generated release notes: the model quietly drops a PR or invents one that isn't in this release. A changelog that's almost right is worse than no changelog because nobody re-checks it.\n\nWe don't trust the generated release notes to be complete on the first try, we verify it deterministically. Before the model runs, a Python script retrieves all PRs that belong to the release and stores them as ground truth.\n\n```python\n# Deterministic: extract PR numbers from squash-merge commits in the range.\nPR_NUMBER_PATTERN = re.compile(r\"\\(#(\\d+)\\)$\")\n\npr_numbers = [\n    int(m.group(1))\n    for commit in commits_since_last_tag\n    if (m := PR_NUMBER_PATTERN.search(commit.title))\n]\nsave_manifest(pr_numbers)  # the source of truth\n```\n\nThen model drafts the notes from them. Once done, we check its output against the initial list of PRs:\n\n```python\nexpected = set(load_manifest())          # what should be there\nfound    = extract_pr_refs(notes_md)     # what the model wrote (#1234 -> 1234)\n\nmissing = expected - found               # silently dropped\nextra   = found - expected               # belongs to a different release\n```\n\nIf anything is missing or extra, we don't fail and we don't ship a wrong file. We hand the discrepancy back to the agent and ask it to fix exactly those PRs:\n\n```python\nfor _ in range(MAX_ITERATIONS):\n    missing, extra = validate(notes)\n    if not missing and not extra:\n        break  # matches the manifest exactly\n    run_agent_fix(missing_prs=missing, extra_prs=extra)\n```\n\nThis is the pattern that makes the whole thing trustworthy: a non-deterministic model wrapped in deterministic guardrails. The model is great at writing prose and unreliable at being exhaustive. So we let it write and let code enforce the consistency.\n\n## Grounding the model so it doesn't make things up\n\nCompleteness is one half. Accuracy is the other. A model summarizing a PR from its title alone will cheerfully invent a code example that doesn't match the real API.\n\nTo prevent that, when we fetch PR metadata we also pull the actual documentation diffs from each PR: the unified diff of any `.md` file under `docs/` that the PR touched.\n\n```python\ndef fetch_doc_diffs(pr):\n    return [\n        {\"filename\": f.filename, \"status\": f.status, \"patch\": f.patch}\n        for f in pr.get_files()\n        if f.filename.startswith(\"docs/\") and f.filename.endswith(\".md\") and f.patch\n    ]\n```\n\nThat diff goes into the model's context so when it writes \"here's the new CLI command,\" it's quoting the example the PR author actually wrote in the docs. That's the same logic as before: give the model real source material and a narrow job.\n\nThe prompts themselves live as [Skills](https://github.com/huggingface/huggingface_hub/tree/main/.opencode/skills/hf-release-notes): small Markdown files (`SKILL.md` plus reference templates) checked into the repo. The release-notes skill spells out how to pick highlights, how to structure sections, when to add a doc link, etc. It reads like onboarding instructions, which is exactly the right mental model.\n\n## The human checkpoint\n\nAfter the RC is published, the draft GitHub release sits there with the AI's first pass in it. This is where the human comes in:\n\n1. A reviewer reads the draft, edits for tone and emphasis, fixes anything the model over- or under-weighted.\n2. Only then do they trigger the `minor-release` run, which promotes the RC to final.\n\nThe reviewer's time goes into polishing, turning a half-day of writing into a fifteen-minute editing session.\n\nWe also keep a paper trail to improve over time. We archive two files side by side to a Hugging Face Bucket: the raw AI draft, uploaded at RC time before anyone touches it, and the human-edited version, uploaded when the final release is cut.\n\n```bash\n# at RC time: straight from the model, untouched\nhf cp release_notes_raw.txt    \"hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_raw.txt\"\n\n# at release time: after the human review\nhf cp release_notes_edited.txt \"hf://buckets/huggingface/releases/huggingface_hub/${V}/release_notes_edited.txt\"\n```\n\nCollecting both every week gives us a growing dataset of \"what the model wrote\" versus \"what we wished it wrote\". Dataset that we can then reuse to update the agent's skill.\n\n## Open and secure plumbing\n\nRevamping the release process was a good opportunity to tighten security, especially against supply-chain attacks.\n\n**No PyPI token.** Publishing uses [Trusted Publishing](https://docs.pypi.org/trusted-publishers/): PyPI verifies a short-lived OIDC token minted by GitHub for this exact workflow, and issues [PEP 740](https://peps.python.org/pep-0740/) attestations / Sigstore provenance for every artifact. There's no long-lived secret to leak or rotate.\n\n```yaml\npermissions:\n  id-token: write       # mint the OIDC token for PyPI\n  attestations: write   # generate Sigstore provenance\n# ...\n- uses: pypa/gh-action-pypi-publish@v1.14.0\n  with:\n    attestations: true  # no password, no API token, just OIDC\n```\n\n**The agent runtime is pinned and verified.** We don't `curl | bash` the latest OpenCode and hope. We pin a version and check its SHA256 before running it:\n\n```bash\ncurl -fsSL https://opencode.ai/install | bash -s -- --version \"${OPENCODE_VERSION}\"\necho \"${OPENCODE_SHA256}  $(which opencode)\" | sha256sum -c -\n```\n\nOpen tooling doesn't mean careless tooling.\n\n## So, what did it cost?\n\nAlmost nothing. A full release (notes plus the Slack announcement, across 20-40 PRs and a few rounds of prompting) costs about **$0.25** on Inference Providers. With open weights billed pay-as-you-go, the only real question each week is \"is there something worth shipping?\", and there always is.\n\n## What changed in practice\n\nThe cadence went from one release every 4 to 6 weeks to once a week. The secondary effects were the interesting ones:\n\n- **Notes got better, not worse.** A first draft always exists, so review time goes to polishing. Grouping is more consistent and we omit fewer things.\n- **Breakages surface earlier.** Downstream test branches on every RC catch integration issues during the candidate window.\n- **Contributor loops shortened.** The automatic \"shipped in vX.Y.Z\" comment turned out to matter more than we expected. When someone reports an issue on a closed PR, everyone can immediately see which release the fix is in. That used to be a manual tag hunt.\n\n## Make it yours\n\nThis is the part we cared about most. The workflow is shaped around `huggingface_hub` but the structure is generic.\n\n**Reusable almost as-is:**\n\n- The trigger and version-bump logic (`minor-prerelease` then `minor-release` then `patch-release`).\n- The trust-but-verify loop: deterministic manifest, model draft, validate, re-prompt. This is the transferable idea, independent of what you're generating.\n- OIDC Trusted Publishing, pinned and checksum-verified runtime, Slack threading.\n- The skill-based prompts: swap the templates, keep the structure.\n\n**Specific to us:**\n\n- The downstream repo list and their dependency-pin formats.\n- The exact section taxonomy and tone in the skills.\n- The Slack and bucket destinations.\n\nTo adapt it: fork the [workflow file](https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml) and [scripts](https://github.com/huggingface/huggingface_hub/tree/main/utils/release_notes), point it at your package, rewrite the [skill Markdown](https://github.com/huggingface/huggingface_hub/blob/main/.opencode/skills/hf-release-notes/SKILL.md) for your project's voice, set two repo variables (the model ID and your OpenCode version), set up Trusted Publishing on PyPI, and delete the downstream-testing job if you don't have downstreams. The trust-but-verify loop is the part worth reusing as-is. It's what makes a generated artifact safe to ship.\n\n## What's next\n\n- **Auto-triaging downstream failures.** Today the workflow opens test branches and a human reads the CI. An obvious next step is to check the failing logs to report them in the internal slack message.\n- **Extending the pattern.** Most of this is generic. We expect to reuse large parts across other Python libraries in our ecosystem.\n\n## Takeaway\n\nThe parts of a release that used to need a half-day of focused human work (writing notes, drafting announcements, coordinating downstream checks) are the parts a model is good at drafting. Everything else is mechanical and fits in a YAML file. The trick was never just \"let the AI do it\". It's to let the model draft, let deterministic code verify, and let a human decide. It's built entirely from open tools and open weights so the cost rounds to zero and anyone can run it.\n\nThe full workflow file is public. If you maintain a Python library, [fork it](https://github.com/huggingface/huggingface_hub/blob/main/.github/workflows/release.yml), adapt it, and let us know how it goes!\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-06-23T00:00:00+00:00",
  "date_modified": "2026-06-23T00:00:00+00:00",
  "date_published": "2026-06-23T00:00:00+00:00",
  "description": "huggingface_hub is the Python client at the base of the Hugging Face ecosystem. transformers, datasets, diffusers, sentence-transformers and dozens of other libraries depend on it to talk to the Hub. Every week we don't ship a new release…",
  "external_url": "https://huggingface.co/blog/huggingface-hub-release-ci",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2026/06/2026-06-23-shipping-huggingface-hub-every-week-with-ai-open-tools-and.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2026/06/2026-06-23-shipping-huggingface-hub-every-week-with-ai-open-tools-and.md"
  },
  "id": "urn:aggr:item:757263ad88cb66349e835262b480b8c641c38146",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/huggingface-hub-release-ci"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/huggingface-hub-release-ci",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-23-shipping-huggingface-hub-every-week-with-ai-open-tools-and/#webpage"
    },
    "dateModified": "2026-06-23T00:00:00+00:00",
    "datePublished": "2026-06-23T00:00:00+00:00",
    "headline": "Shipping huggingface_hub every week with AI, open tools, and a human in the loop",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://huggingface.co/blog/assets/huggingface-hub-release-ci/thumbnail.png",
      "width": 160
    },
    "timeRequired": "PT10M",
    "url": "https://huggingface.co/blog/huggingface-hub-release-ci",
    "wordCount": 2218
  },
  "name": "Archived snapshot: Shipping huggingface_hub every week with AI, open tools, and a human in the loop",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://huggingface.co/blog/assets/huggingface-hub-release-ci/thumbnail.png",
    "width": 160
  },
  "reading_minutes": 10,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "Shipping huggingface_hub every week with AI, open tools, and a human in the loop",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-23-shipping-huggingface-hub-every-week-with-ai-open-tools-and/",
  "word_count": 2218
}