Skip to content

Commit f862bcc

Browse files
kevinbeier-enbwsergio-sisternes-epamdanielmeppielCopilot
authored
fix(outdated): use virtual-path basename for monorepo tag pattern matching (#1893)
* fix(outdated): use virtual-path basename for monorepo tag pattern matching PR #1504 added monorepo tag support ({name}_v{version}), but _package_basename() only considered repo_url. For virtual subdirectory packages (e.g. path: packages/agent-dev-workflow), the repo basename (smo-architektur.ai) did not match the tag prefix (agent-dev-workflow), causing apm outdated to report unknown. This fix derives the basename from virtual_path for subdirectory packages, aligning with the same logic already used in revision_pins.py. Resolves the unknown status seen for monorepo virtual deps. * fix(outdated): avoid MagicMock truthiness in _package_basename Existing unit tests build deps with bare MagicMock() and never set is_virtual. MagicMock returns a new (truthy) MagicMock for unknown attributes, so the new virtual-path branch fires and returns a MagicMock as package_name. That flows into re.compile and raises TypeError. Change `if dep.is_virtual` to `if dep.is_virtual is True` so only real LockedDependency instances with is_virtual=True enter the branch. * fix(outdated): address review-panel feedback for PR #1893 - Revert 'is True' identity guard; fix test helpers to set is_virtual=False. - Delegate virtual-subdir basename extraction to revision_pins._package_name to avoid silent divergence between outdated and update. - Pass already-built dep_ref into _package_basename to avoid double-allocation. - Add parametrized unit tests for trailing-slash / nested virtual_path variants. - Add CHANGELOG entry under [Unreleased] > Fixed. * fix(outdated): fold review feedback for PR 1893 Restore the release changelog entries that the branch had dropped, move the new outdated fix note back under Unreleased, and promote the revision-pin package-name helper before reusing it from outdated.py. Addresses apm-review-panel follow-ups for PR #1893. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Sergio Sisternes <sergio_sisternes@epam.com> Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com>
1 parent 53c4c79 commit f862bcc

6 files changed

Lines changed: 95 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- `apm outdated` tag-pattern matching for monorepo virtual subdirectory
13+
dependencies now derives the `{name}` segment from the `virtual_path`
14+
basename (e.g., `packages/my-pkg` -> `my-pkg`), aligning with `apm update`
15+
behavior. (by @kevinbeier-enbw; closes #1893) (#1893)
1216
- Skipped startup update checks for unknown `apm` commands and bare help
1317
invocations so invalid CLI input fails without update-check latency. (by
1418
@maro114510) (#1541)

src/apm_cli/commands/outdated.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import click
1515

16+
from ..deps.lockfile import LockedDependency
1617
from ..deps.outdated_row import OutdatedRow
1718
from ..deps.revision_pins import (
1819
RevisionPinResolutionError,
@@ -21,6 +22,10 @@
2122
find_latest_annotated_tag,
2223
is_full_revision_pin,
2324
)
25+
from ..deps.revision_pins import (
26+
package_name as revision_pin_package_name,
27+
)
28+
from ..models.dependency.reference import DependencyReference
2429
from ..models.dependency.types import RemoteRef
2530

2631
logger = logging.getLogger(__name__)
@@ -50,10 +55,17 @@ def _strip_v(ref: str) -> str:
5055
return ref[1:] if ref and ref.startswith("v") else (ref or "")
5156

5257

53-
def _package_basename(dep) -> str:
58+
def _package_basename(dep: LockedDependency, dep_ref: DependencyReference | None = None) -> str:
5459
"""Return the display name used in ``{name}`` tag patterns."""
5560
if dep.marketplace_plugin_name:
5661
return dep.marketplace_plugin_name
62+
# For virtual subdirectory packages, derive the name from the virtual path
63+
# (e.g. "packages/agent-dev-workflow" -> "agent-dev-workflow").
64+
if dep.is_virtual and dep.virtual_path:
65+
if dep_ref is None:
66+
dep_ref = dep.to_dependency_ref()
67+
if dep_ref.is_virtual_subdirectory():
68+
return revision_pin_package_name(dep_ref)
5769
repo = dep.repo_url or ""
5870
if not repo:
5971
return ""
@@ -294,7 +306,7 @@ def _check_one_dep(dep, downloader, verbose, registry_ctx=None):
294306
package=package_name, current=current_ref or "(none)", latest="-", status="unknown"
295307
)
296308

297-
package_basename = _package_basename(dep)
309+
package_basename = _package_basename(dep, dep_ref=dep_ref)
298310
revision_pin_row = _check_revision_pin_ref(
299311
current_ref=current_ref,
300312
package_name=package_name,

src/apm_cli/deps/revision_pins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def abbreviate_sha(sha: str | None) -> str:
6161
return (sha or "")[:_SHA_DISPLAY_LEN]
6262

6363

64-
def _package_name(dep_ref: DependencyReference) -> str:
64+
def package_name(dep_ref: DependencyReference) -> str:
6565
"""Return the tag-pattern package name for *dep_ref*."""
6666
if dep_ref.is_virtual_subdirectory() and dep_ref.virtual_path:
6767
return dep_ref.virtual_path.rstrip("/").rsplit("/", 1)[-1]
@@ -141,7 +141,7 @@ def _resolve_one(dep_ref: DependencyReference) -> RevisionPinUpdate | None:
141141
dep_key = dep_ref.get_unique_key()
142142
old_sha = (dep_ref.reference or "").strip().lower()
143143
remote_refs = downloader.list_remote_tag_refs(dep_ref)
144-
latest = find_latest_annotated_tag(remote_refs, package_name=_package_name(dep_ref))
144+
latest = find_latest_annotated_tag(remote_refs, package_name=package_name(dep_ref))
145145
latest_sha = latest.commit_sha.strip().lower()
146146
if not is_full_revision_pin(latest_sha):
147147
raise RevisionPinResolutionError(

tests/unit/test_outdated_command.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pathlib import Path
77
from unittest.mock import MagicMock, patch
88

9-
import pytest # noqa: F401
9+
import pytest
1010
from click.testing import CliRunner
1111

1212
from apm_cli.cli import cli
@@ -908,6 +908,51 @@ def test_virtual_dep_processed_normally(
908908
# Remote refs were fetched -- virtual deps are NOT silently skipped
909909
mock_downloader.list_remote_refs.assert_called_once()
910910

911+
@patch(_PATCH_AUTH)
912+
@patch(_PATCH_DOWNLOADER)
913+
@patch(_PATCH_MIGRATE)
914+
@patch(_PATCH_GET_LOCKFILE_PATH)
915+
@patch(_PATCH_GET_APM_DIR)
916+
@patch(_PATCH_LOCKFILE)
917+
def test_monorepo_virtual_subdir_tag_pattern_recognized(
918+
self,
919+
mock_lf_cls,
920+
mock_get_apm_dir,
921+
mock_get_path,
922+
mock_migrate,
923+
mock_dl_cls,
924+
mock_auth,
925+
):
926+
"""Monorepo tags using virtual-path basename are parsed, not marked unknown."""
927+
with self._chdir_tmp() as tmp:
928+
mock_get_apm_dir.return_value = tmp
929+
mock_get_path.return_value = tmp / "apm.lock.yaml"
930+
931+
virtual_dep = LockedDependency(
932+
repo_url="org/monorepo",
933+
resolved_ref="agent-dev-workflow_v0.0.20260624",
934+
resolved_commit="d2b0ad19fd792cbb12e0f653b65607b8f9c95d4e",
935+
is_virtual=True,
936+
virtual_path="packages/agent-dev-workflow",
937+
)
938+
deps = {"org/monorepo/packages/agent-dev-workflow": virtual_dep}
939+
mock_lf_cls.read.return_value = _make_lockfile(deps)
940+
941+
mock_downloader = MagicMock()
942+
mock_downloader.list_remote_refs.return_value = [
943+
_remote_tag("agent-dev-workflow_v0.0.20260624"),
944+
_remote_tag("agent-dev-workflow_v0.0.20260625"),
945+
]
946+
mock_dl_cls.return_value = mock_downloader
947+
948+
result = self.runner.invoke(cli, ["outdated"])
949+
950+
assert result.exit_code == 0
951+
assert "unknown" not in result.output.lower()
952+
assert "outdated" in result.output.lower()
953+
# Latest tag is shown (may be truncated in narrow tables)
954+
assert "agent-dev-" in result.output
955+
911956
# --- Dev dependency visibility ---
912957

913958
@patch(_PATCH_AUTH)
@@ -1221,3 +1266,28 @@ def test_ado_dep_builds_correct_reference(
12211266
assert result.exit_code == 0
12221267
# Verify list_remote_refs was called (dep was not silently skipped)
12231268
mock_downloader.list_remote_refs.assert_called_once()
1269+
1270+
1271+
class TestPackageBasename:
1272+
"""Direct unit tests for _package_basename edge cases."""
1273+
1274+
@pytest.mark.parametrize(
1275+
"virtual_path,expected",
1276+
[
1277+
("packages/agent-dev-workflow", "agent-dev-workflow"),
1278+
("packages/agent-dev-workflow/", "agent-dev-workflow"),
1279+
("packages/deep/nested/pkg", "pkg"),
1280+
("pkg", "pkg"),
1281+
],
1282+
)
1283+
def test_virtual_subdir_basename_variants(self, virtual_path, expected):
1284+
"""Trailing slashes, deep nesting, and simple names all resolve correctly."""
1285+
dep = LockedDependency(
1286+
repo_url="org/monorepo",
1287+
resolved_ref="v1.0.0",
1288+
is_virtual=True,
1289+
virtual_path=virtual_path,
1290+
)
1291+
from apm_cli.commands.outdated import _package_basename
1292+
1293+
assert _package_basename(dep) == expected

tests/unit/test_outdated_marketplace_detection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def _make_dep(
4747
dep.registry_prefix = registry_prefix
4848
dep.discovered_via = discovered_via
4949
dep.marketplace_plugin_name = marketplace_plugin_name
50+
dep.is_virtual = False
51+
dep.virtual_path = None
5052
return dep
5153

5254

tests/unit/test_outdated_phase3w5.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def _make_dep(
4747
dep.registry_prefix = registry_prefix
4848
dep.discovered_via = discovered_via
4949
dep.marketplace_plugin_name = marketplace_plugin_name
50+
dep.is_virtual = False
51+
dep.virtual_path = None
5052
return dep
5153

5254

0 commit comments

Comments
 (0)