Skip to content

Commit acf9306

Browse files
fix(outdated): fold monorepo tag review follow-ups
Sync the shipped command guide with the new outdated behavior, add integration coverage for marketplace monorepo tag inference, and align automatic outdated tag patterns with the documented double-dash convention. Addresses shepherd panel follow-ups for PR #1504. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 95c2dec commit acf9306

8 files changed

Lines changed: 80 additions & 14 deletions

File tree

docs/src/content/docs/reference/cli/marketplace.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,20 @@ versions available.
203203

204204
When remote tags use a non-default layout (for example `my-pkg_v1.0.1`), set
205205
`tag_pattern: "{name}_v{version}"` on the package entry or under `build:` in
206-
`apm.yml`. If no tags match the configured pattern, `apm marketplace outdated`
207-
tries common layouts (`v{version}`, `{name}_v{version}`, etc.) automatically.
206+
`apm.yml`:
207+
208+
```yaml
209+
packages:
210+
- name: my-pkg
211+
source: org/monorepo
212+
version: "^1.0.0"
213+
tag_pattern: "{name}_v{version}"
214+
```
215+
216+
If no tags match the configured pattern, `apm marketplace outdated` tries common
217+
layouts (`v{version}`, `{name}_v{version}`, `{name}--v{version}`, etc.)
218+
automatically. Set `tag_pattern` explicitly when your producer uses a different
219+
layout.
208220

209221
### `apm marketplace publish`
210222

docs/src/content/docs/reference/cli/outdated.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ apm outdated [OPTIONS]
1717

1818
`apm outdated` reads `apm.lock.yaml` and queries each remote to detect staleness:
1919

20-
- **Tag-pinned deps** (e.g. `v1.2.3`, `1.2.3`, or patterned tags like `my-pkg_v1.2.3`): semver compare against the latest matching remote tag. Patterned tags (`{name}_v{version}`, `{name}-v{version}`, etc.) are detected automatically from the locked ref.
20+
- **Plain tag-pinned deps** (e.g. `v1.2.3` or `1.2.3`): semver compare against the latest matching remote tag.
21+
- **Patterned tag-pinned deps** (e.g. `my-pkg_v1.2.3`, `my-pkg--v1.2.3`, or `my-pkg-v1.2.3`): semver compare against the latest tag matching the package-specific pattern inferred from the locked ref.
2122
- **Branch-pinned deps** (e.g. `main`): compare the locked commit SHA against the remote branch tip.
2223
- **Default-branch deps** (no ref): compare against `main`/`master` tip.
2324
- **Marketplace deps**: compare the installed ref against the marketplace entry's current `source.ref`.
2425
- **Registry deps** (experimental `registries` feature): compare the lockfile's exact `version` against the highest semver on the registry that satisfies the manifest range (same resolution semantics as `apm install`). Manifest ranges come from the root `apm.yml` and from installed packages' `apm.yml` files (transitive deps). When a registry lockfile entry has no manifest range, `apm outdated` compares against the highest published version and labels the source `(lockfile)`.
2526

27+
Common monorepo layouts are detected automatically for `outdated` reporting. Set an explicit marketplace `tag_pattern` when your producer uses a different layout than the built-in patterns.
28+
2629
Local dependencies and Artifactory-hosted deps are skipped. Legacy `apm.lock` files are migrated to `apm.lock.yaml` automatically on read.
2730

2831
To apply the suggested updates, run `apm install --update` (see [Related](#related)).

packages/apm-guide/.apm/skills/apm-usage/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
| `apm deps why PKG` | Explain why a package is installed (walks lockfile bottom-up to direct deps; analogue of `npm why` / `yarn why`) | `-g` global, `--json` |
2020
| `apm find <PATH>` | Trace a deployed file back to the package(s) that contributed it (inverse of install; reads `apm.lock.yaml` only) | `--source` show OCI/git/local origin, `--path` show full why-chain (same as `apm deps why`) |
2121
| `apm view PKG [FIELD]` | View package details or remote refs | `-g` global, `FIELD=versions` |
22-
| `apm outdated` | Check locked deps via SHA/semver comparison | `-g` global, `-v` verbose, `-j N` parallel checks |
22+
| `apm outdated` | Check locked deps via SHA/semver comparison; patterned per-package tags are auto-detected | `-g` global, `-v` verbose, `-j N` parallel checks |
2323
| `apm deps info PKG` | Alias for `apm view PKG` local metadata | -- |
2424
| `apm deps clean` | Clean dependency cache | `--dry-run`, `-y` skip confirm |
2525
| `apm deps update [PKGS...]` | Deprecated -- use `apm update` instead (now a strict superset). Update specific packages | `--verbose`, `--force`, `--target` (comma-separated), `--parallel-downloads N`, `-g/--global`, `--legacy-skill-paths` |
@@ -136,7 +136,7 @@ Credentials resolve via `APM_REGISTRY_TOKEN_{NAME}` env var (or `apm config set
136136
|---------|---------|-----------|
137137
| `apm marketplace init` | Append a `marketplace:` block to `apm.yml` and create `.claude-plugin/` | `--force`, `--no-gitignore-check`, `--name`, `--owner` |
138138
| `apm marketplace migrate` | Fold a legacy `marketplace.yml` into `apm.yml`'s `marketplace:` block; deletes `marketplace.yml` on success | `--force`/`--yes`/`-y`, `--dry-run`, `-v` |
139-
| `apm marketplace outdated` | Report upgradable plugins, range-aware | `--offline`, `--include-prerelease`, `-v` |
139+
| `apm marketplace outdated` | Report upgradable plugins, range-aware; respects `tag_pattern` and common monorepo tag layouts | `--offline`, `--include-prerelease`, `-v` |
140140
| `apm marketplace check` | Validate the `marketplace:` block and verify refs resolve | `--offline`, `-v` |
141141
| `apm marketplace doctor` | Diagnose git, network, auth, marketplace config readiness, and (when a `marketplace:` block is present) **format coverage** -- which output profiles are configured vs. supported, so producers can spot easy reach wins (e.g. add `codex: {}` to also publish for Codex consumers). All marketplace-specific rows are informational and never affect exit code. | `-v` |
142142
| `apm marketplace publish` | Open PRs on consumer repos from `consumer-targets.yml` | `--targets PATH`, `--dry-run`, `--no-pr`, `--draft`, `--allow-downgrade`, `--allow-ref-change`, `--parallel N`, `-y` |

src/apm_cli/commands/marketplace/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import builtins
1010
import json
11+
import logging
1112
import re
1213
import sys
1314
import traceback
@@ -49,6 +50,8 @@
4950
from ...utils.path_security import PathTraversalError, validate_path_segments
5051
from .._helpers import _get_console, _is_interactive
5152

53+
logger = logging.getLogger(__name__)
54+
5255
# Restore builtins shadowed by subcommand names
5356
list = builtins.list
5457

@@ -1048,6 +1051,12 @@ def _collect(pattern: str) -> list:
10481051
if not results:
10491052
inferred = infer_tag_pattern_from_refs(refs, entry.name)
10501053
if inferred and inferred != pattern:
1054+
logger.debug(
1055+
"Configured tag pattern %r matched no tags for %s; inferred %r",
1056+
pattern,
1057+
entry.name,
1058+
inferred,
1059+
)
10511060
results = _collect(inferred)
10521061
return results
10531062

src/apm_cli/commands/outdated.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
logger = logging.getLogger(__name__)
1818

19+
# Fallback heuristic for _resolve_tag_pattern when inference misses plain tags.
1920
TAG_RE = re.compile(r"^v?\d+\.\d+\.\d+")
2021

2122

@@ -47,6 +48,12 @@ def _resolve_tag_pattern(current_ref: str, package_name: str) -> str | None:
4748

4849
inferred = infer_tag_pattern(current_ref, package_name)
4950
if inferred:
51+
logger.debug(
52+
"Resolved tag pattern %r for %s from ref %s",
53+
inferred,
54+
package_name,
55+
current_ref,
56+
)
5057
return inferred
5158
if TAG_RE.match(current_ref or ""):
5259
return "v{version}" if (current_ref or "").startswith("v") else "{version}"
@@ -264,11 +271,11 @@ def _check_one_dep(dep, downloader, verbose, registry_ctx=None):
264271
)
265272

266273
_, latest_tag = candidates[0]
267-
current_ver = (
268-
parse_tag_version(current_ref, tag_pattern, name=package_basename) or _strip_v(current_ref)
269-
)
270-
latest_ver = (
271-
parse_tag_version(latest_tag, tag_pattern, name=package_basename) or _strip_v(latest_tag)
274+
current_ver = parse_tag_version(
275+
current_ref, tag_pattern, name=package_basename
276+
) or _strip_v(current_ref)
277+
latest_ver = parse_tag_version(latest_tag, tag_pattern, name=package_basename) or _strip_v(
278+
latest_tag
272279
)
273280

274281
if is_newer_version(current_ver, latest_ver):

src/apm_cli/marketplace/tag_pattern.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"v{version}",
3333
"{version}",
3434
"{name}_v{version}",
35+
"{name}--v{version}",
3536
"{name}-v{version}",
3637
)
3738

@@ -120,10 +121,7 @@ def build_tag_regex(pattern: str, *, name: str | None = None) -> re.Pattern[str]
120121
)
121122

122123
escaped = escaped.replace(re.escape(_sentinel_version), _VERSION_RX)
123-
if _PLACEHOLDER_NAME in pattern and name:
124-
name_rx = re.escape(name)
125-
else:
126-
name_rx = r"[^/]+"
124+
name_rx = re.escape(name) if _PLACEHOLDER_NAME in pattern and name else r"[^/]+"
127125
escaped = escaped.replace(re.escape(_sentinel_name), name_rx)
128126

129127
return re.compile(r"^" + escaped + r"$")

tests/integration/marketplace/test_outdated_integration.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ def _refs_alpha():
6161
]
6262

6363

64+
def _refs_alpha_named():
65+
"""alpha: monorepo-style tags with package name prefix."""
66+
return [
67+
RemoteRef(name="refs/tags/alpha_v1.0.0", sha="a" * 40),
68+
RemoteRef(name="refs/tags/alpha_v1.1.0", sha="b" * 40),
69+
RemoteRef(name="refs/tags/beta_v9.9.9", sha="c" * 40),
70+
]
71+
72+
6473
def _refs_beta():
6574
"""beta: v2.0.0 only -- no newer version available."""
6675
return [
@@ -78,6 +87,7 @@ def _refs_pinned():
7887
def _side_effect(owner_repo: str):
7988
return {
8089
"org/alpha": _refs_alpha(),
90+
"org/alpha-monorepo": _refs_alpha_named(),
8191
"org/beta": _refs_beta(),
8292
"org/pinned": _refs_pinned(),
8393
}.get(owner_repo, [])
@@ -206,6 +216,26 @@ def test_major_outside_range_is_noted(self, tmp_path: Path):
206216
# v2.0.0 is the latest overall; it should appear in the overall-latest column
207217
assert "v2.0.0" in combined
208218

219+
def test_name_underscore_tags_are_inferred_end_to_end(self, tmp_path: Path):
220+
"""Monorepo tags like alpha_v1.1.0 are reported through the CLI path."""
221+
yml_content = """\
222+
name: outdated-test
223+
description: Marketplace for outdated tests
224+
version: 1.0.0
225+
owner:
226+
name: Test Org
227+
packages:
228+
- name: alpha
229+
source: org/alpha-monorepo
230+
version: "^1.0.0"
231+
tags:
232+
- test
233+
"""
234+
result = _run_outdated(tmp_path, yml_content=yml_content)
235+
assert result.exit_code == 1
236+
assert "alpha_v1.1.0" in result.output
237+
assert "beta_v9.9.9" not in result.output
238+
209239

210240
class TestOutdatedMissingYml:
211241
"""outdated without marketplace.yml exits 1."""

tests/unit/marketplace/test_tag_pattern.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class TestRoundTrip:
222222
("{name}-v{version}", "my-tool", "2.0.0"),
223223
("release-{version}", "x", "10.20.30"),
224224
("{name}_v{version}", "tool", "1.0.0-beta.1"),
225+
("{name}--v{version}", "tool", "1.0.1"),
225226
],
226227
)
227228
def test_roundtrip(self, pattern: str, name: str, version: str) -> None:
@@ -237,6 +238,9 @@ class TestInferTagPattern:
237238
def test_name_underscore_v_version(self) -> None:
238239
assert infer_tag_pattern("api-governance_v1.0.1") == "{name}_v{version}"
239240

241+
def test_name_double_dash_v_version(self) -> None:
242+
assert infer_tag_pattern("api-governance--v1.0.1") == "{name}--v{version}"
243+
240244
def test_name_underscore_v_scoped_to_package(self) -> None:
241245
assert infer_tag_pattern("apm1_v1.0.1", "apm1") == "{name}_v{version}"
242246
assert infer_tag_pattern("apm2_v1.0.1", "apm1") is None
@@ -258,6 +262,9 @@ class TestIsVersionTagRef:
258262
def test_name_underscore_v_version_is_tag(self) -> None:
259263
assert is_version_tag_ref("api-governance_v1.0.1") is True
260264

265+
def test_name_double_dash_v_version_is_tag(self) -> None:
266+
assert is_version_tag_ref("api-governance--v1.0.1") is True
267+
261268
def test_name_at_version_not_tag(self) -> None:
262269
assert is_version_tag_ref("api-governance@1.0.1") is False
263270

0 commit comments

Comments
 (0)