diff --git a/.github/workflows/dependabot-pr-cleaner.yaml b/.github/workflows/dependabot-pr-cleaner.yaml
new file mode 100644
index 000000000..d95f0b2da
--- /dev/null
+++ b/.github/workflows/dependabot-pr-cleaner.yaml
@@ -0,0 +1,75 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Dependabot PR cleaner
+run-name: >-
+ Clean up description of PR ${{github.event.pull_request.number}} on
+ ${{github.ref_name}} by @${{github.actor}}
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - synchronize
+ - reopened
+
+ workflow_dispatch:
+ inputs:
+ pr-number:
+ description: 'The PR number of the PR to clean:'
+ required: true
+ debug:
+ description: 'Run with debugging options'
+ type: boolean
+ default: true
+
+# Declare default workflow permissions as read only.
+permissions: read-all
+
+jobs:
+ clean-pr-description:
+ if: >-
+ ${{github.actor == 'dependabot[bot]' &&
+ github.repository_owner == 'quantumlib'}}
+ name: Clean PR description
+ runs-on: ubuntu-slim
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Check out a copy of the git repository
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+ with:
+ sparse-checkout: |
+ ./dev_tools/ci/dependabot_pr_description_cleaner.py
+
+ - name: Set up Python
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
+ with:
+ cache: pip
+
+ - name: Install dependencies
+ run: pip install html2text
+
+ - name: Clean the PR description
+ env:
+ GH_REPO: ${{github.repository}}
+ GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+ PR_NUMBER: ${{inputs.pr-number || github.event.pull_request.number}}
+ SHELLOPTS: ${{inputs.debug && 'xtrace' || '' }}
+ run: |
+ gh pr view ${{env.PR_NUMBER}} --json body --jq .body > body.txt
+ python3 dev_tools/ci/dependabot_pr_description_cleaner.py body.txt
+ gh pr edit ${{env.PR_NUMBER}} --body-file body.txt
diff --git a/dev_tools/ci/dependabot_pr_description_cleaner.py b/dev_tools/ci/dependabot_pr_description_cleaner.py
new file mode 100644
index 000000000..d366e9e18
--- /dev/null
+++ b/dev_tools/ci/dependabot_pr_description_cleaner.py
@@ -0,0 +1,79 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Convert and streamline description bodies of Dependabot PRs."""
+
+import os
+import re
+import sys
+
+import html2text
+
+
+def process_text(content):
+ """Take PR description body and clean it.
+
+ Cleaning steps applied:
+ * Remove the section "Commits"
+ * Convert HTML to Markdown
+ * Remove the section "Dependabot commands and options"
+ """
+
+ # Pattern: match the start of the details block for Commits, lazy match
+ # until the closing details tag, and match the following br tag if present.
+ content = re.sub(
+ r" Sourced from raven-actions/actionlint's releases. See details of all code changes: https://github.com/raven-actions/actionlint/compare/v2.0.2...v2.1.0 since previous release. See details of all code changes: https://github.com/raven-actions/actionlint/compare/v2.0.1...v2.0.2 since previous release. Sourced from actions/checkout's releases. Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1 Sourced from cmake's releases. Full Changelog: https://github.com/scikit-build/cmake-python-distributions/compare/4.2.0...4.2.1Commits
.*?
)?",
+ "",
+ content,
+ flags=re.DOTALL | re.IGNORECASE,
+ )
+
+ h = html2text.HTML2Text()
+ h.body_width = 0
+ markdown_content = h.handle(content)
+
+ target_phrase = "Dependabot commands and options"
+ if target_phrase in markdown_content:
+ # Split at the last occurrence of the phrase.
+ parts = markdown_content.rsplit(target_phrase, 1)
+ pre_text = parts[0]
+ pre_text = pre_text.rstrip()
+ # Remove the marker if present at the end.
+ if pre_text.endswith(r"\---"):
+ pre_text = pre_text[:-4]
+ elif pre_text.endswith("---"):
+ pre_text = pre_text[:-3]
+ markdown_content = pre_text.strip()
+
+ return markdown_content
+
+
+def clean_body(file_path):
+ """Reads the file, cleans the text, and writes it back to the same file."""
+ with open(file_path, "r") as f:
+ content = f.read()
+
+ markdown_content = process_text(content)
+
+ # Write back to file.
+ with open(file_path, "w") as f:
+ f.write(markdown_content)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ script_name = os.path.basename(sys.argv[0])
+ print(f"Usage: python3 {script_name} FILE")
+ sys.exit(1)
+ clean_body(sys.argv[1])
diff --git a/dev_tools/ci/dependabot_pr_description_cleaner_test.py b/dev_tools/ci/dependabot_pr_description_cleaner_test.py
new file mode 100644
index 000000000..edc2fdcfd
--- /dev/null
+++ b/dev_tools/ci/dependabot_pr_description_cleaner_test.py
@@ -0,0 +1,350 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# pylint: disable=line-too-long
+
+import dependabot_pr_description_cleaner as cleaner
+import pytest
+
+# Sample 1
+SAMPLE_1 = r"""Bumps [raven-actions/actionlint](https://github.com/raven-actions/actionlint) from 2.0.1 to 2.1.0.
+Release notes
+
+
+v2.1.0
+🔄️ What's Changed
+
+
+@​DariuszPorowski (#46)👥 Contributors
+
+v2.0.2
+🔄️ What's Changed
+
+
+@​allejo (#43)@​DariuszPorowski (#33)👥 Contributors
+@​DariuszPorowski, @​allejo, @​dependabot[bot], @​raven-actions-sync[bot], dependabot[bot] and raven-actions-sync[bot]Commits
+
+
+963d477 ci: update action versions in workflows and action metadata (#46)591b1df ci(deps): bump actions/checkout from 5 to 6 in the all group (#45)fd1b743 fix: don't interfere with repo package.json (#43)14ed449 ci(deps): bump actions/cache from 4.2.4 to 4.3.0 in the all group (#42)0ecc789 ci(deps): bump actions/github-script from 7.0.1 to 8.0.0 in the all group (#41)193928d ci(deps): bump the all group across 1 directory with 2 updates (#40)a9f1bde chore: synced file(s) with raven-actions/.workflows (#38)e35ca82 chore: synced file(s) with raven-actions/.workflows (#37)8155254 chore: synced file(s) with raven-actions/.workflows (#36)0d9faa2 chore: synced file(s) with raven-actions/.workflows (#35)
+
+
+[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
+
+Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
+
+[//]: # (dependabot-automerge-start)
+[//]: # (dependabot-automerge-end)
+
+---
+
+Dependabot commands and options
+
+
+You can trigger Dependabot actions by commenting on this PR:
+- `@dependabot rebase` will rebase this PR
+- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
+- `@dependabot merge` will merge this PR after your CI passes on it
+- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
+- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
+- `@dependabot reopen` will reopen this PR if it is closed
+- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
+- `@dependabot show Release notes
+
+
+v6.0.1
+What's Changed
+
+
+@​ericsciple in actions/checkout#2314@​ericsciple in actions/checkout#2327@​ericsciple in actions/checkout#2328Commits
+
+
+
+
+[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
+
+Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
+
+[//]: # (dependabot-automerge-start)
+[//]: # (dependabot-automerge-end)
+
+---
+
+Dependabot commands and options
+
+
+You can trigger Dependabot actions by commenting on this PR:
+- `@dependabot rebase` will rebase this PR
+- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
+- `@dependabot merge` will merge this PR after your CI passes on it
+- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
+- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
+- `@dependabot reopen` will reopen this PR if it is closed
+- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
+- `@dependabot show Release notes
+
+
+4.2.1
+
+What's Changed
+
+
+@​shanemcd in scikit-build/cmake-python-distributions#665@​mayeut in scikit-build/cmake-python-distributions#669@​mayeut in scikit-build/cmake-python-distributions#670@​scikit-build-app-bot[bot] in scikit-build/cmake-python-distributions#666New Contributors
+
+
+@​shanemcd made their first contribution in scikit-build/cmake-python-distributions#665Commits
+
+
+535af4c [Bot] Update to CMake 4.2.1 (#666)b9233cd chore(deps): update clang to 21.1.8.0 (#670)5a1434b fix: workaround issue in lastversion with OpenSSL (#669)dd81945 chore(deps): bump the actions group with 3 updates (#668)40acf75 chore(deps): update pre-commit hooks (#664)a28030b fix: add missing f-string prefix for --parallel bootstrap arg (#665)aa7b6bf chore(deps): bump the actions group with 3 updates (#663)a611e25 chore(deps): update clang to 21.1.6.0 (#661)c89cf39 chore: monthly updates (#660)597f158 chore: add changelog exclusion for bots (#658)
+
+
+Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
+
+[//]: # (dependabot-automerge-start)
+[//]: # (dependabot-automerge-end)
+
+---
+
+Dependabot commands and options
+
+
+You can trigger Dependabot actions by commenting on this PR:
+- `@dependabot rebase` will rebase this PR
+- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
+- `@dependabot merge` will merge this PR after your CI passes on it
+- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
+- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
+- `@dependabot reopen` will reopen this PR if it is closed
+- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
+- `@dependabot show
Hello World
" + expected_output = "Hello **World**" + result = cleaner.process_text(html_input) + assert result.strip() == expected_output + + +def test_remove_commits_section(): + html_input = """ +Some text
+More text
+ """ + # "Commits" section should be removed. + # html2text introduces newlines. + result = cleaner.process_text(html_input) + + assert "Commits" not in result + assert "Commit 1" not in result + assert "Some text" in result + assert "More text" in result + + +def test_remove_dependabot_commands(): + html_input = """ +Content
+commands
+Notes
+help
+