66from typing import Optional , List
77
88from .Check import Check , REPO_ROOT
9+ from ci_tools .functions import discover_targeted_packages
910from ci_tools .logging import logger
11+ from ci_tools .parsing import ParsedSetup
1012
1113# The expected Chronus package name and on-disk install location.
1214# Chronus is pinned as a dev dependency in .github/chronus/package.json with
@@ -211,13 +213,17 @@ def _ensure_chronus_installed(self) -> None:
211213 )
212214 raise SystemExit (1 )
213215
214- def _detect_package_from_cwd (self ) -> Optional [str ]:
215- """If CWD is inside a package directory (``sdk/<service>/<package>``),
216- return the Chronus package **name** (the directory basename, e.g.
217- ``azure-core``). Otherwise return ``None``.
216+ def _find_package_root_from_cwd (self ) -> Optional [str ]:
217+ """Find the package root directory when CWD is at or below ``sdk/<service>/<package>``.
218218
219- The chronus config uses the pattern ``sdk/*/*`` for packages, so we
220- look for CWD being at or below ``<REPO_ROOT>/sdk/<service>/<pkg>``.
219+ Walks up from the current directory to locate the package root,
220+ unlike ``get_targeted_directories(target=".")`` which only works
221+ when CWD is exactly the package root. This lets developers run
222+ changelog commands from subdirectories such as
223+ ``sdk/core/azure-core/tests/``.
224+
225+ Returns the absolute path to the package root, or ``None`` if CWD
226+ is not inside an ``sdk/<service>/<package>`` tree.
221227 """
222228 try :
223229 cwd = os .path .abspath (os .getcwd ())
@@ -227,40 +233,38 @@ def _detect_package_from_cwd(self) -> Optional[str]:
227233 # On Windows, relpath raises ValueError when paths are on different drives
228234 return None
229235
230- # rel should start with "sdk/<service>/<package>" (at least 3 components)
231236 parts = rel .replace ("\\ " , "/" ).split ("/" )
232237 if len (parts ) >= 3 and parts [0 ] == "sdk" :
233- # Return the package name (third component, e.g. "azure-core")
234- return parts [2 ]
238+ return os .path .join (repo , parts [0 ], parts [1 ], parts [2 ])
235239 return None
236240
237- def _resolve_package_name (self , package : str ) -> str :
238- """Resolve a user-supplied package argument to a Chronus package name.
239-
240- Accepts either:
241- - A package name directly (e.g. ``azure-core``) — returned as-is.
242- - A relative or absolute path (e.g. ``sdk/core/azure-core``,
243- ``.\\ sdk\\ core\\ azure-core\\ ``) — resolved to the package name.
241+ def _resolve_package (self , package_arg : Optional [str ]) -> Optional [str ]:
242+ """Resolve a package argument or CWD to a Chronus package name.
244243
245- Chronus identifies packages by name (the directory basename under
246- ``sdk/<service>/``), not by path.
244+ Uses ``discover_targeted_packages`` — the same discovery function
245+ that powers ``get_targeted_directories`` — to locate packages by
246+ path or bare name. When *package_arg* is ``None``, the method
247+ detects the package from CWD by walking up to the nearest
248+ ``sdk/<service>/<package>`` directory.
247249 """
248- # If the path is absolute or starts with '.' resolve it relative to repo root
249- if os .path .isabs (package ) or package .startswith ("." ):
250- try :
251- abs_path = os .path .abspath (os .path .join (os .getcwd (), package ))
252- repo = os .path .abspath (REPO_ROOT )
253- package = os .path .relpath (abs_path , repo )
254- except ValueError :
255- pass
256- # Normalize separators and strip trailing slashes
257- package = package .replace ("\\ " , "/" ).strip ("/" )
258- # If it looks like a path (sdk/<service>/<package>...), extract the name
259- parts = package .split ("/" )
260- if len (parts ) >= 3 and parts [0 ] == "sdk" :
261- return parts [2 ]
262- # Otherwise assume it's already a package name
263- return package
250+ if package_arg :
251+ found = discover_targeted_packages (package_arg , REPO_ROOT )
252+ if found :
253+ try :
254+ return ParsedSetup .from_path (found [0 ]).name
255+ except Exception :
256+ return os .path .basename (found [0 ])
257+ # Not found by discovery — pass through as-is
258+ return package_arg
259+
260+ # No explicit package — detect from CWD
261+ pkg_root = self ._find_package_root_from_cwd ()
262+ if pkg_root is None :
263+ return None
264+ try :
265+ return ParsedSetup .from_path (pkg_root ).name
266+ except Exception :
267+ return os .path .basename (os .path .normpath (pkg_root ))
264268
265269 def _run_chronus (self , chronus_args : List [str ]) -> int :
266270 """Run a chronus command from the repository root.
@@ -293,13 +297,10 @@ def _run_add(self, args: argparse.Namespace) -> int:
293297 ``azpysdk changelog add --kind breaking -m "Removed foo API"``).
294298 """
295299 chronus_args = ["add" ]
296- package = args .package
297- if package :
298- package = self ._resolve_package_name (package )
299- else :
300- package = self ._detect_package_from_cwd ()
301- if package :
302- logger .info (f"Detected package from current directory: { package } " )
300+ detected_from_cwd = not args .package
301+ package = self ._resolve_package (args .package )
302+ if package and detected_from_cwd :
303+ logger .info (f"Detected package from current directory: { package } " )
303304 if package :
304305 chronus_args .append (package )
305306
@@ -323,13 +324,10 @@ def _run_create(self, args: argparse.Namespace) -> int:
323324 via ``--package`` so only that package's changelog is generated.
324325 """
325326 chronus_args = ["changelog" ]
326- package = args .package
327- if package :
328- package = self ._resolve_package_name (package )
329- else :
330- package = self ._detect_package_from_cwd ()
331- if package :
332- logger .info (f"Detected package from current directory: { package } " )
327+ detected_from_cwd = not args .package
328+ package = self ._resolve_package (args .package )
329+ if package and detected_from_cwd :
330+ logger .info (f"Detected package from current directory: { package } " )
333331 if not package :
334332 logger .error (
335333 "No package specified and could not detect one from the current directory.\n "
@@ -356,13 +354,10 @@ def _run_status(self, args: argparse.Namespace) -> int:
356354 via ``--only`` so only that package's status is shown.
357355 """
358356 chronus_args = ["status" ]
359- package = args .package
360- if package :
361- package = self ._resolve_package_name (package )
362- else :
363- package = self ._detect_package_from_cwd ()
364- if package :
365- logger .info (f"Detected package from current directory: { package } " )
357+ detected_from_cwd = not args .package
358+ package = self ._resolve_package (args .package )
359+ if package and detected_from_cwd :
360+ logger .info (f"Detected package from current directory: { package } " )
366361 if package :
367362 chronus_args .extend (["--only" , package ])
368363 return self ._run_chronus (chronus_args )
0 commit comments