Skip to content

Commit f5997b5

Browse files
committed
Capture deprecated API warnings from tutorial builds
1 parent f79c3d9 commit f5997b5

File tree

6 files changed

+404
-1
lines changed

6 files changed

+404
-1
lines changed

.github/workflows/_build-tutorials-base.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ jobs:
158158
JOB_TYPE: manager
159159
COMMIT_SOURCE: ${{ github.ref }}
160160
GITHUB_PYTORCHBOT_TOKEN: ${{ secrets.PYTORCHBOT_TOKEN }}
161+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
161162
USE_NIGHTLY: ${{ inputs.USE_NIGHTLY }}
162163
run: |
163164
set -ex
@@ -172,6 +173,7 @@ jobs:
172173
-e JOB_TYPE \
173174
-e COMMIT_SOURCE \
174175
-e GITHUB_PYTORCHBOT_TOKEN \
176+
-e GITHUB_TOKEN \
175177
-e USE_NIGHTLY \
176178
--env-file="/tmp/github_env_${GITHUB_RUN_ID}" \
177179
--tty \
@@ -184,6 +186,14 @@ jobs:
184186
185187
docker exec -u ci-user -t "${container_name}" sh -c ".jenkins/build.sh"
186188
189+
- name: Upload API deprecation report
190+
if: always()
191+
uses: actions/upload-artifact@v4
192+
with:
193+
name: api-deprecation-report
194+
path: tutorials/_build/api_report.md
195+
if-no-files-found: ignore
196+
187197
- name: Upload docs preview
188198
uses: seemethere/upload-artifact-s3@v5
189199
if: ${{ github.event_name == 'pull_request' }}

.jenkins/build.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ if [[ "${JOB_TYPE}" == "worker" ]]; then
6969
export FILES_TO_RUN
7070

7171
# Step 3: Run `make docs` to generate HTML files and static files for these tutorialis
72-
make docs
72+
export PYTHONWARNINGS="all::DeprecationWarning,all::FutureWarning"
73+
make docs 2>&1 | tee _build/build.log
7374

7475
# Step 3.1: Run the post-processing script:
7576
python .jenkins/post_process_notebooks.py
@@ -125,6 +126,9 @@ if [[ "${JOB_TYPE}" == "worker" ]]; then
125126
bash $DIR/remove_invisible_code_block_batch.sh docs
126127
python .jenkins/validate_tutorials_built.py
127128

129+
# Step 5.1: Generate API deprecation report from build warnings
130+
python -m tools.deprecation_checker.api_report --build-log _build/build.log -o _build/api_report.md || true
131+
128132
# Step 6: Copy generated files to S3, tag with commit ID
129133
if [ "${UPLOAD:-0}" -eq 1 ]; then
130134
7z a worker_${WORKER_ID}.7z docs
@@ -156,6 +160,9 @@ elif [[ "${JOB_TYPE}" == "manager" ]]; then
156160
# Step 5.1: Run post-processing script on .ipynb files:
157161
python .jenkins/post_process_notebooks.py
158162

163+
# Step 5.2: Create/update GitHub Issue with deprecation findings
164+
python -m tools.deprecation_checker.api_report --build-log _build/build.log --create-issue || true
165+
159166
# Step 6: Copy generated HTML files and static files to S3
160167
7z a manager.7z docs
161168
awsv2 s3 cp manager.7z s3://${BUCKET_NAME}/${BUILD_PREFIX}/${COMMIT_ID}/manager.7z

tools/__init__.py

Whitespace-only changes.

tools/deprecation_checker/__init__.py

Whitespace-only changes.
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
"""Generate Markdown deprecation reports and optionally file GitHub Issues.
2+
3+
Usage::
4+
5+
# Local report to stdout
6+
python -m tools.deprecation_checker.api_report --build-log _build/build.log
7+
8+
# Local report written to a file
9+
python -m tools.deprecation_checker.api_report --build-log _build/build.log -o _build/api_report.md
10+
11+
# Create / update a GitHub Issue (requires GITHUB_TOKEN)
12+
python -m tools.deprecation_checker.api_report --build-log _build/build.log --create-issue
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
import json
19+
import os
20+
import sys
21+
import urllib.request
22+
import urllib.error
23+
from collections import defaultdict
24+
from pathlib import Path
25+
from typing import List
26+
27+
from .build_warning_parser import BuildWarning, is_tutorial_source, parse_log
28+
29+
# --------------------------------------------------------------------------- #
30+
# Constants
31+
# --------------------------------------------------------------------------- #
32+
33+
REPO_OWNER = "pytorch"
34+
REPO_NAME = "tutorials"
35+
ISSUE_LABEL = "docs-agent-deprecations"
36+
ISSUE_TITLE = "[CI] Deprecated API usage in tutorials"
37+
ISSUE_CC = "svekars"
38+
39+
# --------------------------------------------------------------------------- #
40+
# Markdown report generation
41+
# --------------------------------------------------------------------------- #
42+
43+
44+
def _summary_table(warnings: List[BuildWarning]) -> str:
45+
"""Build a Markdown table summarising warning counts per file."""
46+
counts: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
47+
for w in warnings:
48+
counts[w.file][w.category] += 1
49+
50+
lines = [
51+
"| Tutorial file | DeprecationWarning | FutureWarning | Total |",
52+
"|---|---:|---:|---:|",
53+
]
54+
for file in sorted(counts):
55+
dep = counts[file].get("DeprecationWarning", 0)
56+
fut = counts[file].get("FutureWarning", 0)
57+
lines.append(f"| `{file}` | {dep} | {fut} | {dep + fut} |")
58+
59+
total_dep = sum(c.get("DeprecationWarning", 0) for c in counts.values())
60+
total_fut = sum(c.get("FutureWarning", 0) for c in counts.values())
61+
lines.append(f"| **Total** | **{total_dep}** | **{total_fut}** | **{total_dep + total_fut}** |")
62+
return "\n".join(lines)
63+
64+
65+
def _findings_section(warnings: List[BuildWarning]) -> str:
66+
"""Detailed findings grouped by file, sorted by line number."""
67+
by_file: dict[str, list[BuildWarning]] = defaultdict(list)
68+
for w in warnings:
69+
by_file[w.file].append(w)
70+
71+
sections: list[str] = []
72+
for file in sorted(by_file):
73+
items = sorted(by_file[file], key=lambda w: w.lineno)
74+
parts = [f"### `{file}`\n"]
75+
for w in items:
76+
parts.append(
77+
f"- **Line {w.lineno}** ({w.category}): {w.message}"
78+
)
79+
sections.append("\n".join(parts))
80+
81+
return "\n\n".join(sections)
82+
83+
84+
def generate_report(warnings: List[BuildWarning]) -> str:
85+
"""Return a full Markdown report string."""
86+
if not warnings:
87+
return (
88+
"# API Deprecation Report\n\n"
89+
"No `DeprecationWarning` or `FutureWarning` detected in this build. :tada:"
90+
)
91+
92+
tutorial_warnings = [w for w in warnings if is_tutorial_source(w.file)]
93+
other_warnings = [w for w in warnings if not is_tutorial_source(w.file)]
94+
95+
parts = [
96+
"# API Deprecation Report",
97+
"",
98+
f"**{len(warnings)}** unique deprecation/future warnings found in this build.",
99+
"",
100+
]
101+
102+
if tutorial_warnings:
103+
parts += [
104+
"## Summary (tutorial sources)",
105+
"",
106+
_summary_table(tutorial_warnings),
107+
"",
108+
"## Findings",
109+
"",
110+
_findings_section(tutorial_warnings),
111+
"",
112+
]
113+
114+
if other_warnings:
115+
parts += [
116+
"## Warnings from dependencies / non-tutorial code",
117+
"",
118+
_findings_section(other_warnings),
119+
"",
120+
]
121+
122+
return "\n".join(parts)
123+
124+
125+
# --------------------------------------------------------------------------- #
126+
# GitHub Issue creation / update
127+
# --------------------------------------------------------------------------- #
128+
129+
130+
def _gh_api(
131+
method: str,
132+
endpoint: str,
133+
token: str,
134+
body: dict | None = None,
135+
) -> dict:
136+
"""Minimal GitHub REST API helper using only stdlib."""
137+
url = f"https://api.github.com{endpoint}"
138+
data = json.dumps(body).encode() if body else None
139+
req = urllib.request.Request(
140+
url,
141+
data=data,
142+
method=method,
143+
headers={
144+
"Accept": "application/vnd.github+json",
145+
"Authorization": f"Bearer {token}",
146+
"X-GitHub-Api-Version": "2022-11-28",
147+
},
148+
)
149+
with urllib.request.urlopen(req) as resp:
150+
return json.loads(resp.read())
151+
152+
153+
def _ensure_label(token: str) -> None:
154+
"""Create the issue label if it doesn't exist yet."""
155+
try:
156+
_gh_api(
157+
"POST",
158+
f"/repos/{REPO_OWNER}/{REPO_NAME}/labels",
159+
token,
160+
{"name": ISSUE_LABEL, "color": "d93f0b", "description": "Auto-generated deprecation report from CI"},
161+
)
162+
except urllib.error.HTTPError as exc:
163+
if exc.code == 422:
164+
pass # label already exists
165+
else:
166+
raise
167+
168+
169+
def _find_existing_issue(token: str) -> int | None:
170+
"""Return the issue number of the existing open deprecation issue, or None."""
171+
results = _gh_api(
172+
"GET",
173+
f"/repos/{REPO_OWNER}/{REPO_NAME}/issues?labels={ISSUE_LABEL}&state=open&per_page=1",
174+
token,
175+
)
176+
if results:
177+
return results[0]["number"]
178+
return None
179+
180+
181+
def create_or_update_issue(report_body: str, token: str) -> str:
182+
"""Create or update the deprecation GitHub Issue. Returns the issue URL."""
183+
_ensure_label(token)
184+
existing = _find_existing_issue(token)
185+
186+
body = f"cc: @{ISSUE_CC}\n\n{report_body}"
187+
188+
if existing:
189+
result = _gh_api(
190+
"PATCH",
191+
f"/repos/{REPO_OWNER}/{REPO_NAME}/issues/{existing}",
192+
token,
193+
{"body": body},
194+
)
195+
return result["html_url"]
196+
else:
197+
result = _gh_api(
198+
"POST",
199+
f"/repos/{REPO_OWNER}/{REPO_NAME}/issues",
200+
token,
201+
{
202+
"title": ISSUE_TITLE,
203+
"body": body,
204+
"labels": [ISSUE_LABEL],
205+
},
206+
)
207+
return result["html_url"]
208+
209+
210+
def close_issue_if_open(token: str) -> str | None:
211+
"""Close the deprecation issue if one is open. Returns the URL or None."""
212+
existing = _find_existing_issue(token)
213+
if not existing:
214+
return None
215+
result = _gh_api(
216+
"PATCH",
217+
f"/repos/{REPO_OWNER}/{REPO_NAME}/issues/{existing}",
218+
token,
219+
{
220+
"state": "closed",
221+
"body": f"cc: @{ISSUE_CC}\n\n"
222+
"All `DeprecationWarning` and `FutureWarning` issues have been resolved. "
223+
"This issue will reopen automatically if new deprecations are detected.",
224+
},
225+
)
226+
return result["html_url"]
227+
228+
229+
# --------------------------------------------------------------------------- #
230+
# CLI
231+
# --------------------------------------------------------------------------- #
232+
233+
234+
def main(argv: list[str] | None = None) -> None:
235+
parser = argparse.ArgumentParser(
236+
description="Generate an API deprecation report from a Sphinx build log.",
237+
)
238+
parser.add_argument(
239+
"--build-log",
240+
required=True,
241+
help="Path to the build log file (e.g. _build/build.log).",
242+
)
243+
parser.add_argument(
244+
"-o",
245+
"--output",
246+
default=None,
247+
help="Write the Markdown report to this file instead of stdout.",
248+
)
249+
parser.add_argument(
250+
"--create-issue",
251+
action="store_true",
252+
help="Create or update a GitHub Issue with the report (requires GITHUB_TOKEN).",
253+
)
254+
args = parser.parse_args(argv)
255+
256+
warnings = parse_log(args.build_log)
257+
report = generate_report(warnings)
258+
259+
if args.output:
260+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
261+
Path(args.output).write_text(report)
262+
print(f"Report written to {args.output}")
263+
else:
264+
print(report)
265+
266+
if args.create_issue:
267+
token = os.environ.get("GITHUB_TOKEN", "")
268+
if not token:
269+
print("WARNING: GITHUB_TOKEN not set — skipping issue creation.", file=sys.stderr)
270+
return
271+
if not warnings:
272+
url = close_issue_if_open(token)
273+
if url:
274+
print(f"All warnings resolved — closed issue: {url}")
275+
else:
276+
print("No warnings found and no open issue to close.")
277+
return
278+
url = create_or_update_issue(report, token)
279+
print(f"GitHub Issue: {url}")
280+
281+
282+
if __name__ == "__main__":
283+
main()

0 commit comments

Comments
 (0)