99
1010Policies enforced:
1111
12- 1) Deprecation metadata for REST endpoints
12+ 1) REST deprecations must use FastAPI/OpenAPI metadata
13+ - FastAPI route handlers must not use `openhands.sdk.utils.deprecation.deprecated`.
1314 - Endpoints documented as deprecated in their OpenAPI description must also be
1415 marked `deprecated: true` in the generated schema.
1516
2728
2829from __future__ import annotations
2930
31+ import ast
3032import json
3133import subprocess
3234import sys
5153 "head" ,
5254 "trace" ,
5355}
56+ ROUTE_DECORATOR_NAMES = HTTP_METHODS | {"api_route" }
5457OPENAPI_PROGRAM = """
5558import json
5659import sys
@@ -173,6 +176,96 @@ def _generate_openapi_for_git_ref(git_ref: str) -> dict | None:
173176 return _generate_openapi_from_source_tree (source_tree , git_ref )
174177
175178
179+ def _dotted_name (node : ast .AST ) -> str | None :
180+ if isinstance (node , ast .Name ):
181+ return node .id
182+ if isinstance (node , ast .Attribute ):
183+ prefix = _dotted_name (node .value )
184+ if prefix is None :
185+ return None
186+ return f"{ prefix } .{ node .attr } "
187+ return None
188+
189+
190+ def _find_sdk_deprecated_fastapi_routes_in_file (
191+ file_path : Path , repo_root : Path
192+ ) -> list [str ]:
193+ tree = ast .parse (file_path .read_text (), filename = str (file_path ))
194+
195+ deprecated_names : set [str ] = set ()
196+ deprecation_module_names : set [str ] = set ()
197+
198+ for node in tree .body :
199+ if isinstance (node , ast .ImportFrom ):
200+ if node .module == "openhands.sdk.utils.deprecation" :
201+ for alias in node .names :
202+ if alias .name == "deprecated" :
203+ deprecated_names .add (alias .asname or alias .name )
204+ elif node .module == "openhands.sdk.utils" :
205+ for alias in node .names :
206+ if alias .name == "deprecation" :
207+ deprecation_module_names .add (alias .asname or alias .name )
208+ elif isinstance (node , ast .Import ):
209+ for alias in node .names :
210+ if alias .name == "openhands.sdk.utils.deprecation" :
211+ deprecation_module_names .add (alias .asname or alias .name )
212+
213+ errors : list [str ] = []
214+ for node in ast .walk (tree ):
215+ if not isinstance (node , ast .FunctionDef | ast .AsyncFunctionDef ):
216+ continue
217+
218+ has_route_decorator = False
219+ uses_sdk_deprecated = False
220+
221+ for decorator in node .decorator_list :
222+ if not isinstance (decorator , ast .Call ):
223+ continue
224+
225+ dotted_name = _dotted_name (decorator .func )
226+ if (
227+ isinstance (decorator .func , ast .Attribute )
228+ and decorator .func .attr in ROUTE_DECORATOR_NAMES
229+ ):
230+ has_route_decorator = True
231+
232+ if dotted_name in deprecated_names or (
233+ dotted_name == "openhands.sdk.utils.deprecation.deprecated"
234+ ):
235+ uses_sdk_deprecated = True
236+ continue
237+
238+ if (
239+ isinstance (decorator .func , ast .Attribute )
240+ and decorator .func .attr == "deprecated"
241+ ):
242+ base_name = _dotted_name (decorator .func .value )
243+ if base_name in deprecation_module_names or (
244+ base_name == "openhands.sdk.utils.deprecation"
245+ ):
246+ uses_sdk_deprecated = True
247+
248+ if has_route_decorator and uses_sdk_deprecated :
249+ rel_path = file_path .relative_to (repo_root )
250+ errors .append (
251+ f"{ rel_path } :{ node .lineno } FastAPI route `{ node .name } ` uses "
252+ "openhands.sdk.utils.deprecation.deprecated; use the route "
253+ "decorator's deprecated=True flag instead."
254+ )
255+
256+ return errors
257+
258+
259+ def _find_sdk_deprecated_fastapi_routes (repo_root : Path ) -> list [str ]:
260+ app_root = repo_root / "openhands-agent-server" / "openhands" / "agent_server"
261+ errors : list [str ] = []
262+
263+ for file_path in sorted (app_root .rglob ("*.py" )):
264+ errors .extend (_find_sdk_deprecated_fastapi_routes_in_file (file_path , repo_root ))
265+
266+ return errors
267+
268+
176269def _find_deprecation_policy_errors (schema : dict ) -> list [str ]:
177270 errors : list [str ] = []
178271
@@ -301,6 +394,10 @@ def main() -> int:
301394
302395 baseline_git_ref = f"v{ baseline_version } "
303396
397+ static_policy_errors = _find_sdk_deprecated_fastapi_routes (REPO_ROOT )
398+ for error in static_policy_errors :
399+ print (f"::error title={ PYPI_DISTRIBUTION } REST API::{ error } " )
400+
304401 current_schema = _generate_current_openapi ()
305402 if current_schema is None :
306403 return 1
@@ -311,7 +408,7 @@ def main() -> int:
311408
312409 prev_schema = _generate_openapi_for_git_ref (baseline_git_ref )
313410 if prev_schema is None :
314- return 0 if not deprecation_policy_errors else 1
411+ return 0 if not ( static_policy_errors or deprecation_policy_errors ) else 1
315412
316413 prev_schema = _normalize_openapi_for_oasdiff (prev_schema )
317414 current_schema = _normalize_openapi_for_oasdiff (current_schema )
@@ -380,7 +477,7 @@ def main() -> int:
380477 ):
381478 return 1
382479
383- return 1 if deprecation_policy_errors else 0
480+ return 1 if ( static_policy_errors or deprecation_policy_errors ) else 0
384481
385482
386483if __name__ == "__main__" :
0 commit comments