Skip to content

Commit ca2f936

Browse files
stainless-app[bot]RobertCraigie
authored andcommitted
chore(internal/ci): setup breaking change detection
1 parent 4ada66f commit ca2f936

File tree

6 files changed

+150
-1
lines changed

6 files changed

+150
-1
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
on:
3+
pull_request:
4+
branches:
5+
- main
6+
- next
7+
8+
jobs:
9+
detect_breaking_changes:
10+
runs-on: 'ubuntu-latest'
11+
name: detect-breaking-changes
12+
if: github.repository == 'openai/openai-python'
13+
steps:
14+
- name: Calculate fetch-depth
15+
run: |
16+
echo "FETCH_DEPTH=$(expr ${{ github.event.pull_request.commits }} + 1)" >> $GITHUB_ENV
17+
18+
- uses: actions/checkout@v4
19+
with:
20+
# Ensure we can check out the pull request base in the script below.
21+
fetch-depth: ${{ env.FETCH_DEPTH }}
22+
23+
- name: Install Rye
24+
run: |
25+
curl -sSf https://rye.astral.sh/get | bash
26+
echo "$HOME/.rye/shims" >> $GITHUB_PATH
27+
env:
28+
RYE_VERSION: '0.44.0'
29+
RYE_INSTALL_OPTION: '--yes'
30+
- name: Install dependencies
31+
run: |
32+
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 }}"
36+
37+
- name: Detect breaking changes
38+
run: |
39+
# Try to check out previous versions of the breaking change detection script. This ensures that
40+
# we still detect breaking changes when entire files and their tests are removed.
41+
git checkout "${{ github.event.pull_request.base.sha }}" -- ./scripts/detect-breaking-changes 2>/dev/null || true
42+
./scripts/detect-breaking-changes ${{ github.event.pull_request.base.sha }}

.stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 111
22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-7ef7a457c3bf05364e66e48c9ca34f31bfef1f6c9b7c15b1812346105e0abb16.yml
33
openapi_spec_hash: a2b1f5d8fbb62175c93b0ebea9f10063
4-
config_hash: 76afa3236f36854a8705f1281b1990b8
4+
config_hash: 4870312b04f48fd717ea4151053e7fb9

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dev-dependencies = [
7171
"trio >=0.22.2",
7272
"nest_asyncio==1.6.0",
7373
"pytest-xdist>=3.6.1",
74+
"griffe>=1",
7475
]
7576

7677
[tool.rye.scripts]

requirements-dev.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ cffi==1.16.0
4444
# via sounddevice
4545
charset-normalizer==3.3.2
4646
# via requests
47+
colorama==0.4.6
48+
# via griffe
4749
colorlog==6.7.0
4850
# via nox
4951
cryptography==42.0.7
@@ -68,6 +70,7 @@ filelock==3.12.4
6870
frozenlist==1.7.0
6971
# via aiohttp
7072
# via aiosignal
73+
griffe==1.12.1
7174
h11==0.16.0
7275
# via httpcore
7376
httpcore==1.0.9

scripts/detect-breaking-changes

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
5+
cd "$(dirname "$0")/.."
6+
7+
echo "==> Detecting breaking changes"
8+
9+
TEST_PATHS=(
10+
tests/api_resources
11+
tests/test_client.py
12+
tests/test_response.py
13+
tests/test_legacy_response.py
14+
)
15+
16+
for PATHSPEC in "${TEST_PATHS[@]}"; do
17+
# Try to check out previous versions of the test files
18+
# with the current SDK.
19+
git checkout "$1" -- "${PATHSPEC}" 2>/dev/null || true
20+
done
21+
22+
# Instead of running the tests, use the linter to check if an
23+
# older test is no longer compatible with the latest SDK.
24+
./scripts/lint

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+
"openai",
58+
search_paths=[Path(__file__).parent.parent.joinpath("src")],
59+
)
60+
old_package = griffe.load_git(
61+
"openai",
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=["openai"]))
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)