feat: add apm info, apm outdated, and list_remote_refs#613
feat: add apm info, apm outdated, and list_remote_refs#613sergio-sisternes-epam wants to merge 7 commits intomicrosoft:mainfrom
Conversation
Promote `apm deps info` to top-level `apm info <package> [field]`. Add `versions` field selector to list remote tags/refs via git ls-remote. Add `apm outdated` to compare locked deps against latest available tags. - list_remote_refs() on GitHubPackageDownloader enumerates refs without cloning - RemoteRef dataclass in models/dependency/types.py - apm deps info kept as backward-compatible alias - apm outdated supports --global and --verbose flags - 83 new tests (3608 total, 0 failures) - Updated CLI reference docs, dependency guide, commands skill, changelog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove _list_refs_via_ado_api() and the ADO-specific branch in list_remote_refs(). All hosts (GitHub, ADO, GitLab, generic) now use the single git ls-remote code path. git ls-remote works against any git remote including Azure DevOps when given an authenticated URL. -69 lines removed, simpler to maintain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of marking branch-pinned dependencies as 'unknown', compare the locked commit SHA against the remote branch tip SHA via git ls-remote. This turns every branch-pinned dep into a meaningful 'up-to-date' or 'outdated' status. - Add _find_remote_tip() helper to resolve branch/default-branch SHA - Branch-pinned deps: compare locked SHA vs named branch tip - No-ref deps: compare against main/master (default branch fallback) - Tag-pinned deps: unchanged (semver comparison still used) - Commit-pinned deps with unrecognized ref: still 'unknown' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Better description: 'Show information about a package' (not just installed) - Add --global/-g flag to inspect packages from user scope (~/.apm/) - Add examples showing both local metadata and remote versions usage - Document available fields (versions) in help text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add --global flag to apm info documentation - Update outdated behavior: SHA comparison for branch-pinned deps - Update CHANGELOG with all new features under [Unreleased] - Update skills commands.md summary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show Ref and Commit fields in apm info output when lockfile data is available. Uses substring matching to find the lockfile entry for the queried package (handles virtual packages and org/repo keys). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract per-dep check logic into _check_one_dep() for thread safety - Add --parallel-checks / -j option (default: 4, 0 = sequential) - Rich progress bar with spinner during remote ref checks - ThreadPoolExecutor for concurrent git ls-remote calls - Plain text fallback when Rich is unavailable - 4 new tests covering parallel, sequential, custom workers, and error handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
9167e97 to
5f776df
Compare
There was a problem hiding this comment.
Pull request overview
Adds new CLI capabilities for inspecting installed package metadata and checking whether locked dependencies are stale, backed by a unified git ls-remote implementation for enumerating remote tags/branches.
Changes:
- Introduces top-level
apm info PACKAGE [FIELD](includingFIELD=versionsfor remote refs) andapm outdated(remote staleness checks with optional parallelism). - Adds
GitHubPackageDownloader.list_remote_refs()usinggit ls-remote --tags --heads, plus aRemoteRefmodel for parsed refs. - Updates docs/changelog and adds unit tests for ref enumeration, info, and outdated behavior.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_outdated_command.py | New unit tests for apm outdated scenarios (tags/branches/errors/parallelism). |
| tests/unit/test_list_remote_refs.py | New unit tests for list_remote_refs() parsing/sorting/auth/error handling. |
| tests/unit/test_info_command.py | New unit tests for apm info local metadata + versions field behavior. |
| tests/unit/test_deps_list_tree_info.py | Adds back-compat assertion that apm deps info matches apm info. |
| src/apm_cli/models/dependency/types.py | Adds RemoteRef dataclass for remote tags/branches. |
| src/apm_cli/models/dependency/init.py | Re-exports RemoteRef. |
| src/apm_cli/models/apm_package.py | Re-exports RemoteRef for backward-compatible import paths. |
| src/apm_cli/deps/github_downloader.py | Implements list_remote_refs() + parsing/sorting helpers using git ls-remote. |
| src/apm_cli/commands/outdated.py | New apm outdated command with Rich progress + parallel checks. |
| src/apm_cli/commands/info.py | New top-level apm info command (local metadata + remote versions). |
| src/apm_cli/commands/deps/cli.py | Refactors apm deps info to delegate to shared apm info helpers. |
| src/apm_cli/cli.py | Registers new top-level commands (info, outdated). |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Updates command matrix for new commands/alias behavior. |
| docs/src/content/docs/reference/cli-commands.md | Documents apm info and apm outdated; updates deps info as alias. |
| docs/src/content/docs/guides/dependencies.md | Updates guide examples to prefer apm info over apm deps info. |
| CHANGELOG.md | Adds changelog entries for new commands/ref infrastructure. |
Comments suppressed due to low confidence (1)
CHANGELOG.md:46
- The new changelog bullets under
[Unreleased]don't follow the existing entry style in this file (most entries end with a PR reference like(#562)). Also, the same features are duplicated under the released[0.8.10]section, which risks misrepresenting what actually shipped in that release. Please format the new bullets with PR numbers and keep them only under[Unreleased]until they are released.
### Added
- Artifactory archive entry download for virtual file packages (#525)
### Added
- `apm info <package> [field]` command for inspecting package metadata and remote refs
- `apm info <package> versions` field selector lists remote tags and branches via `git ls-remote`
- `apm outdated` command compares locked dependencies against remote refs
- `--parallel-checks` (`-j`) option on `apm outdated` for concurrent remote checks (default: 4)
- Rich progress feedback during `apm outdated` dependency checking
- `--global` flag on `apm info` for inspecting user-scope packages
### Changed
- Scope resolution now happens once via `TargetProfile.for_scope()` and `resolve_targets()` -- integrators no longer need scope-aware parameters (#562)
- Unified integration dispatch table in `dispatch.py` -- both install and uninstall import from one source of truth (#562)
- Hook merge logic deduplicated: three copy-pasted JSON-merge methods replaced with `_integrate_merged_hooks()` + config dict (#562)
- `apm outdated` uses SHA comparison for branch-pinned deps instead of reporting them as `unknown`
### Fixed
- Reject symlinked primitive files in all discovery and resolution paths to prevent symlink-based traversal attacks (#596)
- `apm install -g` now deploys hooks to the scope-resolved target directory instead of hardcoding `.github/hooks/` (#565, #566)
- Hook sync/cleanup derives prefixes dynamically from `KNOWN_TARGETS` instead of hardcoded paths (#565)
- `auto_create=False` targets no longer get directories unconditionally created during install (#576)
- `apm deps update -g` now correctly passes scope, preventing user-scope updates from silently using project-scope paths (#562)
- Subprocess encoding failures on Windows non-UTF-8 consoles (CP950/CP936) -- all subprocess calls now use explicit UTF-8 encoding (#591)
- PowerShell 5.1 compatibility: replace multi-argument `Join-Path` calls with nested two-argument calls (#593)
- `apm marketplace add` now respects `GITHUB_HOST` environment variable for GitHub Enterprise users (#589)
- `compilation.exclude` patterns now filter primitive discovery, preventing excluded files from leaking into compiled output (#477)
- Runtime detection in script runner now uses anchored patterns to prevent false positives when runtime keywords appear in flag values (#563)
- `apm compile` now warns when instructions are missing `applyTo` across all compilation modes (#449)
- Detect remote default branch instead of hardcoding `main` (#574)
- Warn when two packages deploy a native skill with the same name (#545)
| direct_match = apm_modules_path / package | ||
| if direct_match.is_dir() and ( | ||
| (direct_match / APM_YML_FILENAME).exists() | ||
| or (direct_match / SKILL_MD_FILENAME).exists() | ||
| ): | ||
| return direct_match | ||
|
|
There was a problem hiding this comment.
resolve_package_path() joins the user-provided package argument directly onto apm_modules_path. This allows path traversal inputs (e.g. ../...) to escape apm_modules_path and can lead to reading arbitrary local files when display_package_info() loads metadata. Please validate the package path segments (and ensure_path_within) before constructing/using the path.
| content = "\n".join(content_lines) | ||
| panel = Panel( | ||
| content, | ||
| title=f"[i] Package Info: {package}", |
There was a problem hiding this comment.
The Rich panel title uses "[i] Package Info: ...", but [i] is a Rich markup tag (italic), not a literal status symbol. This will render incorrectly (and may leak unclosed markup) on Rich-enabled terminals. Prefer emitting the literal [i] (escaped) or using STATUS_SYMBOLS['info']/CommandLogger output, and consider using the shared themed console (commands._helpers._get_console()) instead of creating a new Console() instance.
| title=f"[i] Package Info: {package}", | |
| title=f"[[i]] Package Info: {package}", |
| try: | ||
| dep_ref = DependencyReference.parse(package) | ||
| except ValueError as exc: | ||
| _rich_error(f"Invalid package reference '{package}': {exc}") | ||
| sys.exit(1) | ||
|
|
||
| try: | ||
| downloader = GitHubPackageDownloader(auth_resolver=AuthResolver()) | ||
| refs: List[RemoteRef] = downloader.list_remote_refs(dep_ref) | ||
| except RuntimeError as exc: | ||
| _rich_error(f"Failed to list versions for '{package}': {exc}") | ||
| sys.exit(1) | ||
|
|
||
| if not refs: | ||
| _rich_info(f"No versions found for '{package}'") |
There was a problem hiding this comment.
display_versions() takes a logger: CommandLogger argument but never uses it (it calls _rich_error/_rich_info directly). This makes the parameter misleading and can also trigger unused-arg linting. Either use the passed logger for reporting (consistent with other commands) or remove the parameter.
| try: | |
| dep_ref = DependencyReference.parse(package) | |
| except ValueError as exc: | |
| _rich_error(f"Invalid package reference '{package}': {exc}") | |
| sys.exit(1) | |
| try: | |
| downloader = GitHubPackageDownloader(auth_resolver=AuthResolver()) | |
| refs: List[RemoteRef] = downloader.list_remote_refs(dep_ref) | |
| except RuntimeError as exc: | |
| _rich_error(f"Failed to list versions for '{package}': {exc}") | |
| sys.exit(1) | |
| if not refs: | |
| _rich_info(f"No versions found for '{package}'") | |
| def _report_error(message: str) -> None: | |
| logger_error = getattr(logger, "error", None) | |
| if callable(logger_error): | |
| logger_error(message) | |
| else: | |
| _rich_error(message) | |
| def _report_info(message: str) -> None: | |
| logger_info = getattr(logger, "info", None) | |
| if callable(logger_info): | |
| logger_info(message) | |
| else: | |
| _rich_info(message) | |
| try: | |
| dep_ref = DependencyReference.parse(package) | |
| except ValueError as exc: | |
| _report_error(f"Invalid package reference '{package}': {exc}") | |
| sys.exit(1) | |
| try: | |
| downloader = GitHubPackageDownloader(auth_resolver=AuthResolver()) | |
| refs: List[RemoteRef] = downloader.list_remote_refs(dep_ref) | |
| except RuntimeError as exc: | |
| _report_error(f"Failed to list versions for '{package}': {exc}") | |
| sys.exit(1) | |
| if not refs: | |
| _report_info(f"No versions found for '{package}'") |
| # Build a DependencyReference to query remote refs | ||
| try: | ||
| dep_ref = DependencyReference( | ||
| repo_url=dep.repo_url, | ||
| host=dep.host, | ||
| ) | ||
| except Exception: | ||
| return (package_name, current_ref or "(none)", "-", "unknown", []) | ||
|
|
There was a problem hiding this comment.
_check_one_dep() reconstructs a DependencyReference from only repo_url and host. This drops host-specific fields (notably Azure DevOps ado_organization/project/repo), which GitHubPackageDownloader._build_repo_url() requires to form valid ADO clone URLs. As a result, apm outdated can mis-handle ADO dependencies (likely returning unknown or erroring). Consider constructing the reference via DependencyReference.parse(...) (including host prefix) or persisting the needed ADO fields in the lockfile and rehydrating them here.
| is_tag = _is_tag_ref(current_ref) | ||
|
|
||
| if is_tag: | ||
| tag_refs = [r for r in remote_refs if r.ref_type == GitReferenceType.TAG] | ||
| if not tag_refs: | ||
| return (package_name, current_ref, "-", "unknown", []) | ||
|
|
||
| latest_tag = tag_refs[0].name | ||
| current_ver = _strip_v(current_ref) | ||
| latest_ver = _strip_v(latest_tag) | ||
|
|
||
| if is_newer_version(current_ver, latest_ver): | ||
| extra = [r.name for r in tag_refs[:10]] if verbose else [] | ||
| return (package_name, current_ref, latest_tag, "outdated", extra) | ||
| else: | ||
| return (package_name, current_ref, latest_tag, "up-to-date", []) | ||
| else: |
There was a problem hiding this comment.
For tag-pinned deps, the code relies on is_newer_version() from utils/version_checker.py, which only parses versions like 1.2.3, 1.2.3a1, 1.2.3b1, 1.2.3rc1. Tags that are still semver-valid but common in Git (e.g. v1.2.3-beta, v1.2.3-rc.1) will be treated as invalid and therefore never reported as outdated (since is_newer_version() returns False on invalid input). If apm outdated is meant to do semver comparisons, consider using a semver-capable parser here or treating unparseable tags as unknown rather than up-to-date.
| def _find_remote_tip(ref_name, remote_refs): | ||
| """Find the tip SHA for a branch ref from remote refs. | ||
|
|
||
| If *ref_name* is empty/None, looks for HEAD or falls back to | ||
| common default branch names (main, master). | ||
| Returns the commit SHA string or None if not found. | ||
| """ | ||
| from ..models.dependency.types import GitReferenceType | ||
|
|
||
| if not remote_refs: | ||
| return None | ||
|
|
||
| branch_refs = {r.name: r.commit_sha for r in remote_refs | ||
| if r.ref_type == GitReferenceType.BRANCH} | ||
|
|
||
| if ref_name: | ||
| return branch_refs.get(ref_name) | ||
|
|
||
| # No ref specified -- find the default branch | ||
| # HEAD is included by git ls-remote; fall back to main/master | ||
| head_refs = [r for r in remote_refs if r.name == "HEAD"] | ||
| if head_refs: | ||
| return head_refs[0].commit_sha |
There was a problem hiding this comment.
_find_remote_tip() claims it can look for HEAD in remote_refs, but list_remote_refs() calls git ls-remote --tags --heads and the parser only includes refs/heads/* and refs/tags/*, so a HEAD entry will never be present. This makes the default-branch logic/docstring misleading; either adjust list_remote_refs()/parser to include HEAD, or remove the HEAD branch and document the actual fallback behavior (main/master/first branch).
Description
Adds two new top-level commands and the underlying
git ls-remoteinfrastructure to support package version inspection and staleness detection.apm info PACKAGE [FIELD]— Show installed package metadata (name, version, description, ref, commit from lockfile). Passversionsas the field selector to list remote tags and branches without cloning.apm outdated— Compare locked dependencies against remote refs. Tag-pinned deps use semver comparison; branch-pinned deps compare the locked commit SHA against the remote branch tip. Includes Rich progress feedback and configurable parallel checks (-j N, default 4).list_remote_refs()— New function ingithub_downloader.pythat wrapsgit ls-remote --tags --headsfor all git hosts (GitHub, ADO, GHE) using a single unified code path with no host-specific APIs.Type of change
Testing
test_list_remote_refs.py— 29 tests (parsing, sorting, auth, error handling)test_info_command.py— 15 tests (metadata display, versions, --global, lockfile ref)test_outdated_command.py— 24 tests (tag/branch/SHA comparison, parallel, progress, edge cases)test_deps_list_tree_info.py— 1 backward-compat test forapm deps infoalias