Context
PR #344 restricts Python >= lower-bound bumps to major-version Renovate updates only. Renovate still writes >=N.M.P (e.g. >=1.16.1) on those major bumps because the pep440 versioning strategy has no built-in way to emit >=N.0.0.
Goal: a reusable workflow that pushes a single signed fixup commit on the Renovate PR branch, rewriting every >=N.M.P floor to >=N.0.0 in pyproject.toml, **/requirements*.txt, and uv.lock specifier lines — while preserving CVE-mitigation pins.
Policy the normalizer must enforce
(canonical in the bump-rule description in renovate-presets.json, restated here)
- Default:
>=N.0.0 (major-only floor).
- CVE exception: a floor may exceed
>=N.0.0 only when it pins to the oldest secure release that fixes one or more CVEs. Every such floor MUST be preceded by an inline comment block that cites the CVE identifier(s) it mitigates.
- The normalizer must preserve any floor preceded by a CVE comment block. The marker: any comment line within N lines above the floor (or in the same array/block in TOML) mentioning a CVE identifier —
CVE-, GHSA-, PYSEC-, or the literal token cve-pin.
- The normalizer rewrites every other
>=N.M.P floor to >=N.0.0.
Canonical pattern lives in mlx-benchmarks/space/requirements.txt:
# Minimum-version pins for direct + transitive deps that have CVEs in
# older versions. Lower-bound only — HF Spaces resolves the actual
# installed version.
# pyarrow 17.0.0 → PYSEC-2026-113 (CVSS 7.0 High) fixed in 23.0.1
# pillow 10.4.0 → GHSA-cfh3-3jmp-rvhc + GHSA-whj4-6x5x-4v2j fixed in 12.2.0
pyarrow>=23.0.1
pillow>=12.2.0
Why a separate PR
The signed-commit path needs all of:
actions/create-github-app-token@v3 for an installation token
- A bash helper invoking the Git Data API (blobs → tree → commit → ref update) to produce a single signed commit
- A Python rewrite helper handling pep440 edge cases (extras, markers, pre-releases, comma-separated upper bounds), skipping
requires-python / comment lines, AND skipping CVE-pinned floors per the marker rule above
- Sparse-checkout of these scripts from
.github into consumer repos (same pattern as _ci-gate.yml's watchdog)
- Per-repo consumer workflow opt-in:
.github/workflows/normalize-python-bounds.yml calling the reusable workflow on pull_request from renovate[bot]
Worth its own focused review.
Proposed shape
scripts/normalize_python_lower_bounds.py:
- Walks
pyproject.toml + **/requirements*.txt + uv.lock
- Rewrites
>=N.M.P[suffix] → >=N.0.0
- Skips lines beginning with
#, requires-python, python_requires
- Skips floors preceded by a CVE comment block (regex:
(?:CVE|GHSA|PYSEC)- or token cve-pin in any of the N preceding comment lines, where the comment block is contiguous #-prefixed lines)
- Excludes
.venv, node_modules, .tox, .direnv, etc.
- Prints changed paths on stdout, one per line
- Idempotent
scripts/git-data-commit.sh:
- Reads paths from a stdin/argv list
- POSTs blobs, tree (with
base_tree), commit (parent = current HEAD), PATCHes ref
- Single signed commit (web-flow attribution to whichever bot owns the installation token)
.github/workflows/_normalize-python-lower-bounds.yml:
workflow_call reusable
- Inputs: optional
runner_label (default ubuntu-latest)
- Secrets:
GH_APP_PRIVATE_KEY
- Steps: mint app token, checkout PR head with that token, sparse-checkout
.github, setup Python, run the rewrite, run the commit helper
Consumer opt-in (per-repo):
on:
pull_request:
paths: [pyproject.toml, '**/requirements*.txt', uv.lock]
jobs:
normalize:
if: github.actor == 'renovate[bot]'
permissions: { contents: write, pull-requests: write }
uses: JacobPEvans/.github/.github/workflows/_normalize-python-lower-bounds.yml@main
secrets:
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
Open questions
uv.lock specifier lines: rewrite in place, or regenerate via uv lock? Regenerating is safer (uv recomputes hashes too) but adds an astral-sh/setup-uv step. Recommendation: regenerate.
- One-shot housekeeping PR per consumer to normalize existing in-tree
>=N.M.P floors (excluding CVE-pinned)? Or let them age out as Renovate touches them? Recommendation: opt-in housekeeping run per repo.
- Lint companion: should there be a pre-commit hook / CI check that fails when a
>=N.M.P floor exists without a CVE comment block? That makes the policy enforceable rather than aspirational. Recommendation: yes, but as a follow-up to this one.
Acceptance
- A Renovate major-bump PR for a non-CVE-pinned dep lands with
>=N.0.0.
- A Renovate-touched file containing CVE-pinned floors (like
mlx-benchmarks/space/requirements.txt) has the CVE pins preserved exactly — pyarrow>=23.0.1, pillow>=12.2.0, orjson>=3.11.6, idna>=3.15 all unchanged.
- No regression on
requires-python = ">=3.10" lines.
required_signatures branch protection accepts every commit produced by the workflow.
Labels
- type:enhancement, area:renovate, size:m, priority:medium
Context
PR #344 restricts Python
>=lower-bound bumps to major-version Renovate updates only. Renovate still writes>=N.M.P(e.g.>=1.16.1) on those major bumps because thepep440versioning strategy has no built-in way to emit>=N.0.0.Goal: a reusable workflow that pushes a single signed fixup commit on the Renovate PR branch, rewriting every
>=N.M.Pfloor to>=N.0.0inpyproject.toml,**/requirements*.txt, anduv.lockspecifier lines — while preserving CVE-mitigation pins.Policy the normalizer must enforce
(canonical in the bump-rule description in
renovate-presets.json, restated here)>=N.0.0(major-only floor).>=N.0.0only when it pins to the oldest secure release that fixes one or more CVEs. Every such floor MUST be preceded by an inline comment block that cites the CVE identifier(s) it mitigates.CVE-,GHSA-,PYSEC-, or the literal tokencve-pin.>=N.M.Pfloor to>=N.0.0.Canonical pattern lives in
mlx-benchmarks/space/requirements.txt:Why a separate PR
The signed-commit path needs all of:
actions/create-github-app-token@v3for an installation tokenrequires-python/ comment lines, AND skipping CVE-pinned floors per the marker rule above.githubinto consumer repos (same pattern as_ci-gate.yml's watchdog).github/workflows/normalize-python-bounds.ymlcalling the reusable workflow onpull_requestfromrenovate[bot]Worth its own focused review.
Proposed shape
scripts/normalize_python_lower_bounds.py:pyproject.toml+**/requirements*.txt+uv.lock>=N.M.P[suffix]→>=N.0.0#,requires-python,python_requires(?:CVE|GHSA|PYSEC)-or tokencve-pinin any of the N preceding comment lines, where the comment block is contiguous#-prefixed lines).venv,node_modules,.tox,.direnv, etc.scripts/git-data-commit.sh:base_tree), commit (parent = current HEAD), PATCHes ref.github/workflows/_normalize-python-lower-bounds.yml:workflow_callreusablerunner_label(defaultubuntu-latest)GH_APP_PRIVATE_KEY.github, setup Python, run the rewrite, run the commit helperConsumer opt-in (per-repo):
Open questions
uv.lockspecifier lines: rewrite in place, or regenerate viauv lock? Regenerating is safer (uv recomputes hashes too) but adds anastral-sh/setup-uvstep. Recommendation: regenerate.>=N.M.Pfloors (excluding CVE-pinned)? Or let them age out as Renovate touches them? Recommendation: opt-in housekeeping run per repo.>=N.M.Pfloor exists without a CVE comment block? That makes the policy enforceable rather than aspirational. Recommendation: yes, but as a follow-up to this one.Acceptance
>=N.0.0.mlx-benchmarks/space/requirements.txt) has the CVE pins preserved exactly —pyarrow>=23.0.1,pillow>=12.2.0,orjson>=3.11.6,idna>=3.15all unchanged.requires-python = ">=3.10"lines.required_signaturesbranch protection accepts every commit produced by the workflow.Labels