Skip to content

Normalize Python >= lower bounds to major.0.0 in Renovate PRs #345

@JacobPEvans-personal

Description

@JacobPEvans-personal

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:mediumMedium - Normal workflow (semver: standard)size:mM - Moderate effort, 1-2 daystype:choreChore - Maintenance tasks, dependencies, tooling

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions