Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 14 additions & 1 deletion docs/guides/gha_basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand All @@ -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"
Expand Down
50 changes: 50 additions & 0 deletions src/sp_repo_review/checks/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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__()}
4 changes: 3 additions & 1 deletion src/sp_repo_review/checks/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -214,6 +214,8 @@ def check(dependabot: dict[str, Any]) -> bool:
interval: "weekly"
```
"""
if not dependabot:
return None
return bool(dependabot)


Expand Down
105 changes: 105 additions & 0 deletions src/sp_repo_review/checks/renovate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# REN: Renovate Actions

from __future__ import annotations

__lazy_modules__ = ["json"]

import json
from typing import TYPE_CHECKING, Any

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__()}
6 changes: 6 additions & 0 deletions src/sp_repo_review/families.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -124,6 +127,9 @@ def get_families(
"mypy": Family(
name="MyPy",
),
"renovate": Family(
name="Renovate",
),
"ruff": Family(
name="Ruff",
description=ruff_description(ruff),
Expand Down
3 changes: 3 additions & 0 deletions src/sp_repo_review/files.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from .checks.renovate import SUPPORTED_RENOVATE_FILES


def prefetch_root() -> set[str]:
"""
Expand Down Expand Up @@ -28,4 +30,5 @@ def prefetch_package() -> set[str]:
"noxfile.py",
"ruff.toml",
".ruff.toml",
*SUPPORTED_RENOVATE_FILES,
}
31 changes: 31 additions & 0 deletions tests/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions tests/test_renovate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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