Skip to content

Commit 4bdc67a

Browse files
chore(internal): improve breaking change detection
1 parent 0962510 commit 4bdc67a

4 files changed

Lines changed: 86 additions & 0 deletions

File tree

.github/workflows/detect-breaking-changes.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ jobs:
3030
- name: Install dependencies
3131
run: |
3232
rye sync --all-features
33+
- name: Detect removed symbols
34+
run: |
35+
rye run python scripts/detect-breaking-changes.py "${{ github.event.pull_request.base.sha }}"
3336
3437
- name: Detect breaking changes
3538
run: |

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dev-dependencies = [
5858
"rich>=13.7.1",
5959
"nest_asyncio==1.6.0",
6060
"pytest-xdist>=3.6.1",
61+
"griffe>=1",
6162
]
6263

6364
[tool.rye.scripts]

requirements-dev.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ attrs==25.3.0
3131
certifi==2023.7.22
3232
# via httpcore
3333
# via httpx
34+
colorama==0.4.6
35+
# via griffe
3436
colorlog==6.7.0
3537
# via nox
3638
dirty-equals==0.6.0
@@ -48,6 +50,7 @@ filelock==3.12.4
4850
frozenlist==1.6.2
4951
# via aiohttp
5052
# via aiosignal
53+
griffe==1.12.1
5154
h11==0.16.0
5255
# via httpcore
5356
httpcore==1.0.9

scripts/detect-breaking-changes.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from typing import Iterator
5+
from pathlib import Path
6+
7+
import rich
8+
import griffe
9+
from rich.text import Text
10+
from rich.style import Style
11+
12+
13+
def public_members(obj: griffe.Object | griffe.Alias) -> dict[str, griffe.Object | griffe.Alias]:
14+
if isinstance(obj, griffe.Alias):
15+
# ignore imports for now, they're technically part of the public API
16+
# but we don't have good preventative measures in place to prevent
17+
# changing them
18+
return {}
19+
20+
return {name: value for name, value in obj.all_members.items() if not name.startswith("_")}
21+
22+
23+
def find_breaking_changes(
24+
new_obj: griffe.Object | griffe.Alias,
25+
old_obj: griffe.Object | griffe.Alias,
26+
*,
27+
path: list[str],
28+
) -> Iterator[Text | str]:
29+
new_members = public_members(new_obj)
30+
old_members = public_members(old_obj)
31+
32+
for name, old_member in old_members.items():
33+
if isinstance(old_member, griffe.Alias) and len(path) > 2:
34+
# ignore imports in `/types/` for now, they're technically part of the public API
35+
# but we don't have good preventative measures in place to prevent changing them
36+
continue
37+
38+
new_member = new_members.get(name)
39+
if new_member is None:
40+
cls_name = old_member.__class__.__name__
41+
yield Text(f"({cls_name})", style=Style(color="rgb(119, 119, 119)"))
42+
yield from [" " for _ in range(10 - len(cls_name))]
43+
yield f" {'.'.join(path)}.{name}"
44+
yield "\n"
45+
continue
46+
47+
yield from find_breaking_changes(new_member, old_member, path=[*path, name])
48+
49+
50+
def main() -> None:
51+
try:
52+
against_ref = sys.argv[1]
53+
except IndexError as err:
54+
raise RuntimeError("You must specify a base ref to run breaking change detection against") from err
55+
56+
package = griffe.load(
57+
"cloudflare",
58+
search_paths=[Path(__file__).parent.parent.joinpath("src")],
59+
)
60+
old_package = griffe.load_git(
61+
"cloudflare",
62+
ref=against_ref,
63+
search_paths=["src"],
64+
)
65+
assert isinstance(package, griffe.Module)
66+
assert isinstance(old_package, griffe.Module)
67+
68+
output = list(find_breaking_changes(package, old_package, path=["cloudflare"]))
69+
if output:
70+
rich.print(Text("Breaking changes detected!", style=Style(color="rgb(165, 79, 87)")))
71+
rich.print()
72+
73+
for text in output:
74+
rich.print(text, end="")
75+
76+
sys.exit(1)
77+
78+
79+
main()

0 commit comments

Comments
 (0)