Skip to content

Add setup-python version upgrade recipe - #219

Merged
timtebeek merged 4 commits into
openrewrite:mainfrom
poketopa:feat/setup-python-version-recipe
Aug 23, 2026
Merged

Add setup-python version upgrade recipe#219
timtebeek merged 4 commits into
openrewrite:mainfrom
poketopa:feat/setup-python-version-recipe

Conversation

@poketopa

@poketopa poketopa commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What's changed?

  • Add SetupPythonUpgradePythonVersion, following the existing setup-node/setup-java recipe shape.
  • Upgrade older exact python-version values and safely older npm-semver ranges to a required MAJOR.MINOR target.
  • Skip any actions/setup-python step that uses python-version-file.
  • Preserve compatible/newer versions, dynamic expressions, alternative Python implementations, scalar quoting, and non-workflow YAML.
  • Add positive/negative tests plus recipe and documentation metadata.

What's your motivation?

actions/setup-python users need a reusable recipe that raises outdated Python versions without changing workflows that source their version from a file.

Anything in particular you'd like reviewers to focus on?

Please review the conservative range handling and the step-level python-version-file exclusion.

Anyone you would like to review specifically?

No preference.

Have you considered any alternatives or workarounds?

An exact-version-only implementation would be simpler, but would leave valid older ranges unchanged. This implementation only rewrites range forms it can prove are below the requested target and leaves ambiguous or union ranges untouched.

Any additional context

This follows the maintainer guidance in #85 to use SetupNodeUpgradeNodeVersion as the direct template and skip steps using python-version-file.

Validation completed locally:

  • ./gradlew test --tests org.openrewrite.github.SetupPythonUpgradePythonVersionTest --rerun-tasks — BUILD SUCCESSFUL
  • ./gradlew recipeCsvGenerate recipeCsvValidate — BUILD SUCCESSFUL; content and completeness validation passed
  • ./gradlew check — BUILD SUCCESSFUL
  • ./gradlew build — BUILD SUCCESSFUL
  • git diff --check upstream/main — passed
  • Independent code review — APPROVE
  • Independent architecture review — CLEAR

Checklist

  • I have added positive tests that demonstrate this change.
  • I have added negative tests that demonstrate unchanged cases.
  • The recipe follows existing OpenRewrite recipe conventions.
  • I ran ./gradlew build.
  • I ran ./gradlew recipeCsvGenerate and committed the updated recipes.csv.
  • I verified formatting and whitespace with the repository checks and git diff --check.

@poketopa
poketopa force-pushed the feat/setup-python-version-recipe branch from 7fe133f to 8adffa2 Compare August 23, 2026 13:24
@poketopa
poketopa marked this pull request as ready for review August 23, 2026 13:30
timtebeek and others added 3 commits August 23, 2026 16:10
…erns

The recipe carried seven regexes and a `PythonVersion implements
Comparable` class to decide whether a `python-version` was below the
target. `org.openrewrite.semver.Semver` already answers both halves of
that question, so delegate to it:

- Concrete versions go through `Semver.compare(.., MAVEN)`. Maven
  precedence orders a two-part `major.minor`, which `Ecosystem.NODE`
  rejects as non-strict SemVer.
- Ranges are decided by the comparator alone. Probing the target plus an
  implausibly high version separates a range bounded below the target
  (`<=3.13`, `~3.12`, `2.x`) from one that is open-ended or already
  compatible (`>=3.9`, `^3.9`, `3.x`, `>=4 || <3.14`), without any
  arithmetic on captured groups.

One pattern remains, because that is the branch point the library cannot
give us: `Semver.isVersion` returns true for `3.x` and `3.7 - 3.9`, as
`RELEASE_PATTERN` swallows the trailing text into its qualifier group.

The block scalar skip is now keyed on the scalar style rather than on a
version pattern failing to match. A FOLDED/LITERAL value carries the
block envelope, which `Yaml.Scalar#withValue` would clobber, so it is a
write-safety guard rather than a version question, and it also covers
the folded spelling the regex mismatch happened to miss.

Also inline the visitor as an anonymous class, since it holds no
per-visit state, and put a `python-version` key comparison in front of
`JsonPathMatcher.matches`. That matcher re-visits the enclosing document
on every call, so testing it against every mapping entry in a workflow
made the cost quadratic in file size.

Test indentation now follows `.editorconfig`, which sets a continuation
indent of 2 for `src/test/java`.
`python-version: |` followed by a single version was left untouched,
because the recipe skipped every FOLDED/LITERAL scalar. That skip was
needed for write safety rather than for any version reason: the raw
`Yaml.Scalar#value` of a block scalar carries the envelope, so
`withValue("3.14")` collapsed the whole thing to `python-version: |3.14`.

`BlockScalar` exists for exactly this, and reads and writes the body
without touching the envelope. Use it, so a lone version in a block is
raised like any other.

A block holding several versions is still left alone. That is a
deliberate test matrix, and raising each entry below the target would
just repeat the target.
@timtebeek
timtebeek merged commit 7d791e8 into openrewrite:main Aug 23, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Upgrade Python version used in actions/setup-python

2 participants