Skip to content

Commit 1651331

Browse files
msyycCopilotCopilot
authored
feat(breaking): generate changelog from two code reports (#45790)
* feat(breaking): generate changelog from two code reports Add a fast-path to \�zpysdk breaking\ that compares two pre-built code reports directly, without requiring a package directory, wheel build, or PyPI install. Changes: - breaking.py: short-circuit run() when --source-report and --target-report are both provided, delegating to new _run_from_reports() which calls detect_breaking_changes.py directly - detect_breaking_changes.py: fix test_compare_reports() to handle absolute report paths (only joins with pkg_dir when path is relative) - test_changelog.py: add test_compare_reports_with_absolute_paths to verify absolute path handling end-to-end Usage: azpysdk breaking --source-report stable.json --target-report current.json --changelog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * format * docs(breaking): add generate-changelog-from-reports section to README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 08e2b52 commit 1651331

4 files changed

Lines changed: 127 additions & 4 deletions

File tree

eng/tools/azure-sdk-tools/azpysdk/breaking.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import os
33
import sys
4+
import tempfile
45

56
from typing import Optional, List
67
from subprocess import CalledProcessError, check_call
@@ -89,6 +90,12 @@ def run(self, args: argparse.Namespace) -> int:
8990
logger.info("Running breaking check...")
9091

9192
set_envvar_defaults()
93+
94+
# Fast path: if two pre-built code reports are provided, compare them directly
95+
# without needing a package directory, build, or install step.
96+
if getattr(args, "source_report", None) and getattr(args, "target_report", None):
97+
return self._run_from_reports(args)
98+
9299
targeted = self.get_targeted_directories(args)
93100

94101
results: List[int] = []
@@ -159,3 +166,37 @@ def run(self, args: argparse.Namespace) -> int:
159166
continue
160167

161168
return max(results) if results else 0
169+
170+
def _run_from_reports(self, args: argparse.Namespace) -> int:
171+
"""Compare two pre-built code reports directly, skipping package build and install."""
172+
source = os.path.abspath(args.source_report)
173+
target = os.path.abspath(args.target_report)
174+
175+
# Use a temporary root directory, but create a subdirectory whose basename is derived
176+
# from the source report path so detect_breaking_changes.py can infer the package name
177+
# correctly from os.path.basename(pkg_dir).
178+
with tempfile.TemporaryDirectory() as tmp_root:
179+
# Heuristic: use the parent directory name of the source report as the package name.
180+
pkg_name = os.path.basename(os.path.dirname(source))
181+
tmp_pkg_dir = os.path.join(tmp_root, pkg_name)
182+
os.makedirs(tmp_pkg_dir, exist_ok=True)
183+
184+
cmd = [
185+
sys.executable,
186+
os.path.join(BREAKING_CHECKER_PATH, "detect_breaking_changes.py"),
187+
"--target",
188+
tmp_pkg_dir,
189+
"--source-report",
190+
source,
191+
"--target-report",
192+
target,
193+
]
194+
if getattr(args, "changelog", False):
195+
cmd.append("--changelog")
196+
197+
try:
198+
check_call(cmd)
199+
except CalledProcessError as e:
200+
logger.error(f"Breaking change report generation failed: {e}")
201+
return 1
202+
return 0

scripts/breaking_changes_checker/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,45 @@ Example:
138138
```
139139
C:\azure-sdk-for-python\sdk\storage\azure-storage-blob> azpysdk breaking . --source-report ./source_code_report.json --target-report ./target_code_report.json
140140
```
141+
142+
### Generate changelog from two code reports
143+
144+
When both `--source-report` and `--target-report` are provided together with the `--changelog` flag, the tool skips the package build and PyPI install steps entirely and generates the changelog by comparing the two reports directly. This is the fastest way to produce a changelog when you already have code reports for both versions.
145+
146+
**Step 1:** Generate a code report for the stable (old) version:
147+
148+
```
149+
C:\azure-sdk-for-python\sdk\storage\azure-storage-blob> azpysdk breaking . --code-report
150+
```
151+
152+
Rename the output file: `mv code_report.json stable_report.json`
153+
154+
**Step 2:** After updating your code, generate a report for the new version:
155+
156+
```
157+
C:\azure-sdk-for-python\sdk\storage\azure-storage-blob> azpysdk breaking . --code-report
158+
```
159+
160+
Rename the output file: `mv code_report.json current_report.json`
161+
162+
**Step 3:** Generate the changelog by comparing the two reports:
163+
164+
```
165+
C:\azure-sdk-for-python\sdk\storage\azure-storage-blob> azpysdk breaking --source-report ./stable_report.json --target-report ./current_report.json --changelog
166+
```
167+
168+
Example output:
169+
170+
```
171+
===== changelog start =====
172+
### Features Added
173+
174+
- Client `BlobServiceClient` added method `list_blobs_flat`
175+
176+
### Breaking Changes
177+
178+
- Deleted or renamed client method `BlobServiceClient.list_blobs`
179+
===== changelog end =====
180+
```
181+
182+
> **Note:** When `--source-report` and `--target-report` are both specified, you do not need to pass a positional package directory argument, but you should still run the command from the package's root directory so that the package name is inferred correctly for opt-in and ignore-rule behavior. The `--changelog` flag is optional — omitting it will report only breaking changes and exit with code 1 if any are found.

scripts/breaking_changes_checker/detect_breaking_changes.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,18 @@ def build_library_report(target_module: str) -> Dict:
443443
def test_compare_reports(pkg_dir: str, changelog: bool, source_report: str = "stable.json", target_report: str = "current.json") -> None:
444444
package_name = os.path.basename(pkg_dir)
445445

446-
with open(os.path.join(pkg_dir, source_report), "r") as fd:
446+
# Preserve the original argument values so we can decide later whether cleanup is safe.
447+
original_source_report = source_report
448+
original_target_report = target_report
449+
450+
if not os.path.isabs(source_report):
451+
source_report = os.path.join(pkg_dir, source_report)
452+
if not os.path.isabs(target_report):
453+
target_report = os.path.join(pkg_dir, target_report)
454+
455+
with open(source_report, "r") as fd:
447456
stable = json.load(fd)
448-
with open(os.path.join(pkg_dir, target_report), "r") as fd:
457+
with open(target_report, "r") as fd:
449458
current = json.load(fd)
450459

451460
if "azure-mgmt-" in package_name:
@@ -461,10 +470,25 @@ def test_compare_reports(pkg_dir: str, changelog: bool, source_report: str = "st
461470
post_processing_checkers = POST_PROCESSING_CHECKERS
462471
)
463472
if changelog:
464-
checker = ChangelogTracker(stable, current, package_name, checkers = CHECKERS, ignore = IGNORE_BREAKING_CHANGES, post_processing_checkers = POST_PROCESSING_CHECKERS)
473+
checker = ChangelogTracker(
474+
stable,
475+
current,
476+
package_name,
477+
checkers = CHECKERS,
478+
ignore = IGNORE_BREAKING_CHANGES,
479+
post_processing_checkers = POST_PROCESSING_CHECKERS,
480+
)
465481
checker.run_checks()
466482

467-
remove_json_files(pkg_dir)
483+
# Only clean up reports that were generated into pkg_dir with default, non-absolute names.
484+
cleanup_default_reports = (
485+
original_source_report == "stable.json"
486+
and original_target_report == "current.json"
487+
and not os.path.isabs(original_source_report)
488+
and not os.path.isabs(original_target_report)
489+
)
490+
if cleanup_default_reports:
491+
remove_json_files(pkg_dir)
468492

469493
print(checker.report_changes())
470494

scripts/breaking_changes_checker/tests/test_changelog.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from checkers.added_method_overloads_checker import AddedMethodOverloadChecker
1212
from breaking_changes_checker.changelog_tracker import ChangelogTracker, BreakingChangesTracker
1313
from breaking_changes_checker.detect_breaking_changes import main
14+
from breaking_changes_checker.detect_breaking_changes import test_compare_reports as compare_reports
1415

1516

1617
def test_changelog_flag():
@@ -681,3 +682,18 @@ def test_added_overload():
681682
msg, _, *args = bc.features_added[1]
682683
assert msg == AddedMethodOverloadChecker.message["default"]
683684
assert args == ['azure.contoso', 'class_name', 'two', 'def two(foo: JSON)']
685+
686+
687+
def test_compare_reports_with_absolute_paths(capsys):
688+
"""Verify test_compare_reports accepts absolute paths for source/target reports."""
689+
tests_dir = os.path.dirname(__file__)
690+
source_report = os.path.abspath(os.path.join(tests_dir, "examples", "code-reports", "content-safety", "stable.json"))
691+
target_report = os.path.abspath(os.path.join(tests_dir, "examples", "code-reports", "content-safety", "current.json"))
692+
pkg_dir = tests_dir # pkg_dir must exist and is still used (e.g., for package_name and cleanup); its value is independent of using absolute report paths
693+
694+
# Should not raise; changelog=True means no SystemExit(1) even if breaking changes exist
695+
compare_reports(pkg_dir, changelog=True, source_report=source_report, target_report=target_report)
696+
697+
captured = capsys.readouterr()
698+
assert "===== changelog start =====" in captured.out
699+
assert "===== changelog end =====" in captured.out

0 commit comments

Comments
 (0)