From 6096d525211dd8dc33fd099cdaaebc249be3a9b2 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Sat, 27 Jun 2026 15:30:04 -0400 Subject: [PATCH 1/2] feat(checks): renovate support Adds an initial set of checks for Renovate equal to GH200 (REN200 is there a config) and GH210 (REN210 - basic management of GitHub Actions). Made a choice that as long as there was either Renovate or Dependabot configured, repo review should pass out of the box. To do that added DEP200 that will pass if either a Renovate or Dependabot config exists, and made REN200 and GH200 return `None` to skip them and downstream checks. Tries to read all non `package.json` locations (`package.json`), but only supports configs that can be parsed by the built in `json` library. Closes #463 #740 --- README.md | 9 ++ docs/guides/gha_basic.md | 15 +++- pyproject.toml | 3 + src/sp_repo_review/checks/dependencies.py | 49 ++++++++++ src/sp_repo_review/checks/github.py | 4 +- src/sp_repo_review/checks/renovate.py | 105 ++++++++++++++++++++++ src/sp_repo_review/families.py | 6 ++ src/sp_repo_review/files.py | 3 + tests/test_dependencies.py | 31 +++++++ tests/test_renovate.py | 40 +++++++++ 10 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 src/sp_repo_review/checks/dependencies.py create mode 100644 src/sp_repo_review/checks/renovate.py create mode 100644 tests/test_dependencies.py create mode 100644 tests/test_renovate.py diff --git a/README.md b/README.md index 854cf543..ac79c309 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,10 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - [`PP308`](https://learn.scientific-python.org/development/guides/pytest#PP308): Specifies useful pytest summary - [`PP309`](https://learn.scientific-python.org/development/guides/pytest#PP309): Filter warnings specified +### dependencies + +- [`DEP200`](https://learn.scientific-python.org/development/guides/gha-basic#DEP200): Maintained by Dependabot or Renovate. + ### GitHub Actions - [`GH100`](https://learn.scientific-python.org/development/guides/gha-basic#GH100): Has GitHub Actions config @@ -399,6 +403,11 @@ Will not show up if using lefthook instead of pre-commit/prek. - [`PC902`](https://learn.scientific-python.org/development/guides/style#PC902): Custom pre-commit CI autofix message - [`PC903`](https://learn.scientific-python.org/development/guides/style#PC903): Specified pre-commit CI schedule +### Renovate + +- [`REN200`](https://learn.scientific-python.org/development/guides/gha-basic#REN200): Maintained by Renovate +- [`REN210`](https://learn.scientific-python.org/development/guides/gha-basic#REN210): Maintains the GitHub action versions with Renovate + ### ReadTheDocs Will not show up if no `.readthedocs.yml`/`.readthedocs.yaml` file is present. diff --git a/docs/guides/gha_basic.md b/docs/guides/gha_basic.md index f1f0f8af..d2a1f57a 100644 --- a/docs/guides/gha_basic.md +++ b/docs/guides/gha_basic.md @@ -159,7 +159,8 @@ static. And old versioned images are decommissioned. ## Updating -{rr}`GH200` {rr}`GH210` If you use non-default actions in your repository +{rr}`DEP200` {rr}`GH200` {rr}`GH210` {rr}`REN200` {rr}`REN210` +If you use non-default actions in your repository (you will see some in the following pages), then it's a good idea to keep them up to date. GitHub provided a way to do this with dependabot. Just add the following file as `.github/dependabot.yml`: @@ -189,6 +190,18 @@ which is both cleaner and sometimes required for dependent actions, like You can use this for other ecosystems too, including Python. +[Renovate](https://docs.renovatebot.com/) can also be used for keeping GitHub +Actions (and other ecosystems) up to date as well. A good starting point for +`renovate.json` with the +[hosted version](https://docs.renovatebot.com/getting-started/installing-onboarding/) +which will cover GitHub Actions and most other ecosystems is: + +```json +{ + "extends": ["config:recommended"] +} +``` + ## Common needs ### Single OS steps diff --git a/pyproject.toml b/pyproject.toml index 99c9ec74..5f7db1a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ sp-repo-review = "repo_review.__main__:main" sp-ruff-checks = "sp_repo_review.ruff_checks.__main__:main" [project.entry-points."repo_review.checks"] +dependencies = "sp_repo_review.checks.dependencies:repo_review_checks" general = "sp_repo_review.checks.general:repo_review_checks" pyproject = "sp_repo_review.checks.pyproject:repo_review_checks" precommit = "sp_repo_review.checks.precommit:repo_review_checks" @@ -70,6 +71,7 @@ mypy = "sp_repo_review.checks.mypy:repo_review_checks" github = "sp_repo_review.checks.github:repo_review_checks" security = "sp_repo_review.checks.security:repo_review_checks" readthedocs = "sp_repo_review.checks.readthedocs:repo_review_checks" +renovate = "sp_repo_review.checks.renovate:repo_review_checks" setupcfg = "sp_repo_review.checks.setupcfg:repo_review_checks" noxfile = "sp_repo_review.checks.noxfile:repo_review_checks" @@ -79,6 +81,7 @@ noxfile = "sp_repo_review.checks.noxfile:noxfile" precommit = "sp_repo_review.checks.precommit:precommit" pytest = "sp_repo_review.checks.pyproject:pytest" readthedocs = "sp_repo_review.checks.readthedocs:readthedocs" +renovate = "sp_repo_review.checks.renovate:renovate" ruff = "sp_repo_review.checks.ruff:ruff" setupcfg = "sp_repo_review.checks.setupcfg:setupcfg" workflows = "sp_repo_review.checks.github:workflows" diff --git a/src/sp_repo_review/checks/dependencies.py b/src/sp_repo_review/checks/dependencies.py new file mode 100644 index 00000000..55ed33c8 --- /dev/null +++ b/src/sp_repo_review/checks/dependencies.py @@ -0,0 +1,49 @@ +# DU: Dependency Updating + +from __future__ import annotations + +from typing import Any + +from . import mk_url + +class Dependencies: + family = "dependencies" + + +class DEP200(Dependencies): + """Maintained by Dependabot or Renovate.""" + + url = mk_url("gha-basic") + + @staticmethod + def check(dependabot: dict[str, Any], renovate: dict[str, Any]) -> bool: + """ + All projects should have a tool to manage dependencies, either Dependabot or Renovate. + + Something like one of these: + + `.github/dependabot.yml` + ```yaml + version: 2 + updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + ``` + + `renovate.json` + ```json + {{ + "extends": ["config:recommended"] + }} + ``` + Renovate configurations in `package.json` are not supported. + Configurations in `.jsonc` or `.json5` files are not fully supported. + """ + return bool(dependabot or renovate) + + +def repo_review_checks() -> dict[str, Dependencies]: + return {p.__name__: p() for p in Dependencies.__subclasses__()} diff --git a/src/sp_repo_review/checks/github.py b/src/sp_repo_review/checks/github.py index 1547b238..58428e34 100644 --- a/src/sp_repo_review/checks/github.py +++ b/src/sp_repo_review/checks/github.py @@ -199,7 +199,7 @@ class GH200(GitHub): url = mk_url("gha-basic") @staticmethod - def check(dependabot: dict[str, Any]) -> bool: + def check(dependabot: dict[str, Any]) -> bool | None: """ All projects should have a `.github/dependabot.yml` file to support at least GitHub Actions regular updates. Something like this: @@ -214,6 +214,8 @@ def check(dependabot: dict[str, Any]) -> bool: interval: "weekly" ``` """ + if not dependabot: + return None return bool(dependabot) diff --git a/src/sp_repo_review/checks/renovate.py b/src/sp_repo_review/checks/renovate.py new file mode 100644 index 00000000..ec7ea83e --- /dev/null +++ b/src/sp_repo_review/checks/renovate.py @@ -0,0 +1,105 @@ +# REN: Renovate Actions + +from __future__ import annotations + +__lazy_modules__ = ["json"] + +from typing import TYPE_CHECKING, Any + +import json + +from . import mk_url + +if TYPE_CHECKING: + from .._compat.importlib.resources.abc import Traversable + + +SUPPORTED_RENOVATE_FILES = [ + "renovate.json", + "renovate.jsonc", + "renovate.json5", + ".github/renovate.json", + ".github/renovate.jsonc", + ".github/renovate.json5", + ".gitlab/renovate.json", + ".gitlab/renovate.jsonc", + ".gitlab/renovate.json5", + ".renovaterc", + ".renovaterc.json", + ".renovaterc.jsonc", + ".renovaterc.json5", + # "package.json" # Deprecated by Renovate +] + + +def renovate(root: Traversable) -> dict[str, Any]: + renovate_paths = [root.joinpath(f) for f in SUPPORTED_RENOVATE_FILES] + + for renovate_path in renovate_paths: + if renovate_path.is_file(): + with renovate_path.open() as f: + try: + result: dict[str, Any] = json.load(f) + except json.JSONDecodeError: + continue + else: + return result + return {} + +class Renovate: + family = "renovate" + + +class REN200(Renovate): + """Maintained by Renovate""" + + requires = {"DEP200"} + url = mk_url("gha-basic") + + @staticmethod + def check(renovate: dict[str, Any]) -> bool | None: + """ + All projects should have a renovate configuration file (`renovate.json` or other supported locations) + to support dependency updates. Something like this: + + ```json + {{ + "extends": ["config:recommended"] + }} + ``` + + Renovate configurations in `package.json` are not supported. + Configurations in `.jsonc` or `.json5` files are not fully supported. + """ + if not renovate: + return None + return bool(renovate) + + +GHA_EXTENDS = {"config:recommended", "config:best-practices"} + + +class REN210(Renovate): + """Maintains the GitHub action versions with Renovate""" + + requires = {"REN200"} + url = mk_url("gha-basic") + + @staticmethod + def check(renovate: dict[str, Any]) -> bool | None | str: + """ + Ensures that Renovate is configured to maintain GitHub action versions. + + Checks for if the `github-actions` manager is enabled or if the Renovate config extends a known config (`config:recommended` or `config:best-practices`). + """ + if (manager := renovate.get("github-actions", {})) and manager.get("enabled"): + return True + if (extends := renovate.get("extends", [])): + if any(e in GHA_EXTENDS for e in extends): + return True + return f"Renovate config extends {extends}, but none are a known config: {', '.join(GHA_EXTENDS)}." + return False + + +def repo_review_checks() -> dict[str, Renovate]: + return {p.__name__: p() for p in Renovate.__subclasses__()} diff --git a/src/sp_repo_review/families.py b/src/sp_repo_review/families.py index 2dbbf5c1..18dc5e69 100644 --- a/src/sp_repo_review/families.py +++ b/src/sp_repo_review/families.py @@ -102,6 +102,9 @@ def get_families( setupcfg: ConfigParser | None = None, ) -> dict[str, Family]: return { + "dependency-updating": Family( + name="Dependency Updating", + ), "general": Family( name="General", order=-3, @@ -124,6 +127,9 @@ def get_families( "mypy": Family( name="MyPy", ), + "renovate": Family( + name="Renovate", + ), "ruff": Family( name="Ruff", description=ruff_description(ruff), diff --git a/src/sp_repo_review/files.py b/src/sp_repo_review/files.py index fee824a5..82bc6057 100644 --- a/src/sp_repo_review/files.py +++ b/src/sp_repo_review/files.py @@ -1,5 +1,7 @@ from __future__ import annotations +from .checks.renovate import SUPPORTED_RENOVATE_FILES + def prefetch_root() -> set[str]: """ @@ -28,4 +30,5 @@ def prefetch_package() -> set[str]: "noxfile.py", "ruff.toml", ".ruff.toml", + *SUPPORTED_RENOVATE_FILES, } diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py new file mode 100644 index 00000000..32810593 --- /dev/null +++ b/tests/test_dependencies.py @@ -0,0 +1,31 @@ +import yaml +from repo_review.testing import compute_check + + +dependabot = yaml.safe_load( + """ + version: 2 + updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + """ + ) + +renovate = { + "extends": ["config:recommended"] + } + + +def test_du100_both() -> None: + assert compute_check("DEP200", dependabot=dependabot, renovate=renovate).result + +def test_du100_missing_renovate() -> None: + assert compute_check("GH200", dependabot=dependabot, renovate={}).result + +def test_du100_missing_dependabot() -> None: + assert compute_check("DEP200", dependabot={}, renovate=renovate).result + +def test_du100_missing_both() -> None: + assert not compute_check("DEP200", dependabot={}, renovate={}).result diff --git a/tests/test_renovate.py b/tests/test_renovate.py new file mode 100644 index 00000000..d5fe966b --- /dev/null +++ b/tests/test_renovate.py @@ -0,0 +1,40 @@ +from repo_review.testing import compute_check + + +def test_ren200() -> None: + renovate = { + "extends": ["config:recommended"] + } + assert compute_check("REN200", renovate=renovate).result + + +def test_ren200_missing() -> None: + assert not compute_check("REN200", renovate={}).result + +def test_ren210_gha_manager() -> None: + renovate = { + "github-actions": { + "enabled": True, + } + } + assert compute_check("REN210", renovate=renovate).result + +def test_ren210_gha_manager_disabled() -> None: + renovate = { + "github-actions": { + "enabled": False, + } + } + assert not compute_check("REN210", renovate=renovate).result + +def test_ren210_common_extends() -> None: + renovate = { + "extends": ["config:recommended"] + } + assert compute_check("REN210", renovate=renovate).result + +def test_ren210_common_extends_missing() -> None: + renovate = { + "extends": ["some-other-config"] + } + assert not compute_check("REN210", renovate=renovate).result From 468b0c38bb16f399ad82b6a13ca8374130eb9094 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:32:19 +0000 Subject: [PATCH 2/2] style: pre-commit fixes --- src/sp_repo_review/checks/dependencies.py | 1 + src/sp_repo_review/checks/renovate.py | 8 ++++---- tests/test_dependencies.py | 12 ++++++------ tests/test_renovate.py | 16 +++++++--------- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/sp_repo_review/checks/dependencies.py b/src/sp_repo_review/checks/dependencies.py index 55ed33c8..ce80d914 100644 --- a/src/sp_repo_review/checks/dependencies.py +++ b/src/sp_repo_review/checks/dependencies.py @@ -6,6 +6,7 @@ from . import mk_url + class Dependencies: family = "dependencies" diff --git a/src/sp_repo_review/checks/renovate.py b/src/sp_repo_review/checks/renovate.py index ec7ea83e..9b8b49f9 100644 --- a/src/sp_repo_review/checks/renovate.py +++ b/src/sp_repo_review/checks/renovate.py @@ -4,9 +4,8 @@ __lazy_modules__ = ["json"] -from typing import TYPE_CHECKING, Any - import json +from typing import TYPE_CHECKING, Any from . import mk_url @@ -46,6 +45,7 @@ def renovate(root: Traversable) -> dict[str, Any]: return result return {} + class Renovate: family = "renovate" @@ -59,7 +59,7 @@ class REN200(Renovate): @staticmethod def check(renovate: dict[str, Any]) -> bool | None: """ - All projects should have a renovate configuration file (`renovate.json` or other supported locations) + All projects should have a renovate configuration file (`renovate.json` or other supported locations) to support dependency updates. Something like this: ```json @@ -94,7 +94,7 @@ def check(renovate: dict[str, Any]) -> bool | None | str: """ if (manager := renovate.get("github-actions", {})) and manager.get("enabled"): return True - if (extends := renovate.get("extends", [])): + if extends := renovate.get("extends", []): if any(e in GHA_EXTENDS for e in extends): return True return f"Renovate config extends {extends}, but none are a known config: {', '.join(GHA_EXTENDS)}." diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 32810593..fcbbe127 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -1,9 +1,8 @@ import yaml from repo_review.testing import compute_check - dependabot = yaml.safe_load( - """ + """ version: 2 updates: - package-ecosystem: github-actions @@ -11,21 +10,22 @@ schedule: interval: weekly """ - ) +) -renovate = { - "extends": ["config:recommended"] - } +renovate = {"extends": ["config:recommended"]} def test_du100_both() -> None: assert compute_check("DEP200", dependabot=dependabot, renovate=renovate).result + def test_du100_missing_renovate() -> None: assert compute_check("GH200", dependabot=dependabot, renovate={}).result + def test_du100_missing_dependabot() -> None: assert compute_check("DEP200", dependabot={}, renovate=renovate).result + def test_du100_missing_both() -> None: assert not compute_check("DEP200", dependabot={}, renovate={}).result diff --git a/tests/test_renovate.py b/tests/test_renovate.py index d5fe966b..5feeb265 100644 --- a/tests/test_renovate.py +++ b/tests/test_renovate.py @@ -2,15 +2,14 @@ def test_ren200() -> None: - renovate = { - "extends": ["config:recommended"] - } + renovate = {"extends": ["config:recommended"]} assert compute_check("REN200", renovate=renovate).result def test_ren200_missing() -> None: assert not compute_check("REN200", renovate={}).result + def test_ren210_gha_manager() -> None: renovate = { "github-actions": { @@ -19,6 +18,7 @@ def test_ren210_gha_manager() -> None: } assert compute_check("REN210", renovate=renovate).result + def test_ren210_gha_manager_disabled() -> None: renovate = { "github-actions": { @@ -27,14 +27,12 @@ def test_ren210_gha_manager_disabled() -> None: } assert not compute_check("REN210", renovate=renovate).result + def test_ren210_common_extends() -> None: - renovate = { - "extends": ["config:recommended"] - } + renovate = {"extends": ["config:recommended"]} assert compute_check("REN210", renovate=renovate).result + def test_ren210_common_extends_missing() -> None: - renovate = { - "extends": ["some-other-config"] - } + renovate = {"extends": ["some-other-config"]} assert not compute_check("REN210", renovate=renovate).result