Skip to content

Commit fa2b145

Browse files
sergio-sisternes-epamSergio SisternesCopilotdanielmeppiel
authored
fix(marketplace): honour producer tag_pattern in consumer semver range resolution (#2366)
* Fix consumer-side semver range ignoring marketplace tag_pattern (#2319) When a consumer resolves a marketplace dependency with a semver range (e.g. version: "~2.1.0"), APM now reads the tag naming pattern declared by the marketplace producer instead of always falling back to the hardcoded "{name}--v{version}" convention. Changes: - builder.py: add effective_tag_pattern to ResolvedPackage; thread it from _resolve_entry through _resolve_explicit_ref and _resolve_version_range. - output_mappers.py: emit tag_pattern in the remote source dict so marketplace.json carries the producer's chosen pattern. - models.py: parse tag_pattern from the source dict into MarketplacePlugin. - resolver.py: pass plugin.tag_pattern (or DEFAULT_TAG_PATTERN fallback) to resolve_version_constraint. - Update golden fixture and existing unit tests to reflect the new tag_pattern field in source dicts. - Add unit and integration tests covering the full producer-to-consumer closure. - Update docs and CHANGELOG. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: cover marketplace tag pattern lifecycle Add installed-binary install, update, outdated, fallback, and failure coverage while centralizing consumer validation and preserving marketplace provenance. Addresses the review-panel lifecycle and canonical-owner follow-ups for #2319. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: cover bare marketplace no-match Exercise bare-version fail-closed behavior through the installed binary and clarify marketplace refresh semantics. Addresses the terminal test-coverage and documentation follow-ups for #2319. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: finish tag pattern contract Make bare-version failure, producer republishing, error recovery, and resolver provenance explicit. Addresses the terminal review-panel follow-ups for #2319. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: clarify catalog validation failure Document that one malformed source.tag_pattern rejects marketplace registration or refresh. Addresses the final doc-writer follow-up for #2319. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: sync architecture instruction mirror Update the deployed Copilot instruction and lockfile hashes after adding the marketplace tag-pattern owner. Addresses the APM Self-Check drift failure. apm-spec-waiver: internal provenance threading implements existing resolver semantics without extending OpenAPM v0.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: deduplicate rebased changelog Keep one marketplace tag-pattern entry alongside the merged anonymous-first authentication fix after rebasing onto #2406. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: reconcile marketplace changelog Preserve latest main release notes and retain one marketplace tag-pattern entry after the final rebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: deduplicate rebased marketplace changelog Preserve the single certified marketplace tag-pattern entry alongside merged-main release notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: reconcile final rebased changelog Retain all merged-main entries and one marketplace tag-pattern entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: align final conflict resolution Preserve the inspected concurrent resolution while retaining rebased history. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: preserve hermetic marketplace rewrites Carry both package and marketplace URL rewrites in process-scoped Git config so auth hardening cannot bypass the lifecycle fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: reconcile strict-write rebase changelog Retain all merged-main entries and one marketplace tag-pattern entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Sergio Sisternes <sergio.sisternes@epam.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
1 parent bbb8b8d commit fa2b145

32 files changed

Lines changed: 1244 additions & 100 deletions

.apm/instructions/architecture.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ semicolon-delimited, and specific to the file(s) that own the fact.
6464
| MCP container launcher selection and Docker argv shape | adapters/client/base.py (MCPClientAdapter) | `src/apm_cli/adapters/client/base.py` |
6565
| Dependency CLI identifier parsing + uninstall selection | models/dependency/selection.py (via DependencyReference) | `src/apm_cli/models/dependency/selection.py` |
6666
| JetBrains Copilot MCP config path | adapters/client/intellij.py | `src/apm_cli/adapters/client/intellij.py` |
67+
| Marketplace tag-pattern validation and expansion | marketplace/tag_pattern.py | `src/apm_cli/marketplace/tag_pattern.py` |
6768
<!-- /canonical-owner-table -->
6869

6970
Host + credential resolution includes public github.com anonymous-first ordering.

.github/instructions/architecture.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ semicolon-delimited, and specific to the file(s) that own the fact.
6464
| MCP container launcher selection and Docker argv shape | adapters/client/base.py (MCPClientAdapter) | `src/apm_cli/adapters/client/base.py` |
6565
| Dependency CLI identifier parsing + uninstall selection | models/dependency/selection.py (via DependencyReference) | `src/apm_cli/models/dependency/selection.py` |
6666
| JetBrains Copilot MCP config path | adapters/client/intellij.py | `src/apm_cli/adapters/client/intellij.py` |
67+
| Marketplace tag-pattern validation and expansion | marketplace/tag_pattern.py | `src/apm_cli/marketplace/tag_pattern.py` |
6768
<!-- /canonical-owner-table -->
6869

6970
Host + credential resolution includes public github.com anonymous-first ordering.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9292
explicit target mappings now preserve `generated_at`, deployment ownership,
9393
and `mcp_target_servers`, leaving `apm.lock.yaml` byte-identical instead of
9494
rewriting it. (#2306)
95+
- Marketplace semver range resolution now honours the `tagPattern` declared by the producer; `version: "~2.1.0"` entries no longer silently fall back to the hardcoded `{name}--v{version}` tag pattern. Existing marketplace files without `tag_pattern` keep the legacy convention. Bare versions also fail closed when no tag matches; use an explicit tag ref instead. (#2366)
9596
- On-prem Azure DevOps Server hosts configured with `ADO_HOST` or
9697
`APM_ADO_HOSTS` are no longer misclassified as GitHub Enterprise Server when
9798
`GITHUB_HOST` overlaps, keeping ADO credentials isolated end to end.

apm.lock.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2783,7 +2783,7 @@ deployments:
27832783
owners:
27842784
- .
27852785
active_owner: .
2786-
content_hash: sha256:c0acf69ef4de5c9de8373337f2164d3206d8d9c6534c6cdf4294f156d99e2762
2786+
content_hash: sha256:6baeb73b4acf037915c23a813947c21e01b99c36bc68d032568863dd842f5b92
27872787
- kind: project-relative
27882788
target: copilot
27892789
value: .github/instructions/changelog.instructions.md
@@ -3290,7 +3290,7 @@ local_deployed_file_hashes:
32903290
.github/agents/spec-tag-architect.agent.md: sha256:82907265c5e7cf1ac61ad96866fa7c5683b69c8f09b7a4c5f3cc241acc9568ca
32913291
.github/agents/supply-chain-security-expert.agent.md: sha256:8fb8cc426d6af17ba084a28b3f026c2b475b62e3ca63ed2f88b83bd823f877af
32923292
.github/agents/test-coverage-expert.agent.md: sha256:48c2172d1f18a394fa83ef9dc2be0b9b921a4e51e976498165250fed66369711
3293-
.github/instructions/architecture.instructions.md: sha256:c0acf69ef4de5c9de8373337f2164d3206d8d9c6534c6cdf4294f156d99e2762
3293+
.github/instructions/architecture.instructions.md: sha256:6baeb73b4acf037915c23a813947c21e01b99c36bc68d032568863dd842f5b92
32943294
.github/instructions/changelog.instructions.md: sha256:1e51ec4c74e847967962bd279dc4c6e582c5d3578490b3c28d5f3acd3e05f73e
32953295
.github/instructions/cicd.instructions.md: sha256:08d87b7d635761cb41deb8fc71d5d83f54678de463db484afb16d2d4f8713ecb
32963296
.github/instructions/cli.instructions.md: sha256:8e39e8d5047ce88575cb02f87c2bcede584dfef258bd86f7466c7badf136541a

docs/src/content/docs/consumer/manage-dependencies.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ parser. The supported forms:
6363
| SSH with non-default user | `myuser@host:acme/repo.git` or `ssh://myuser@host/acme/repo.git` | Honors a non-`git` SSH user from the URL — useful for Enterprise Managed User (EMU) accounts or any server where the SSH login is not `git`. Username is validated against `^[a-zA-Z0-9_][a-zA-Z0-9_.+-]*$` (64-char cap); percent-encoded userinfo is rejected. The username is presentation-only and not part of dependency identity. |
6464
| Local path | `./packages/shared` or `/abs/path` | Sibling package on disk. |
6565
| Object form (git) | `{ git: <url>, path: <subpath>, ref: <ref>, alias: <name>, type: gitlab }` | Aliases, nested groups, monorepo subpaths, bespoke GitLab hosts, or anything string forms cannot express. |
66-
| Marketplace dict | `{ name: <plugin>, marketplace: <mkt>, version: <range> }` | Install a plugin from a registered marketplace. Optional `version` accepts a semver range (e.g. `~2.1.0`). Resolved to a concrete git ref at install time. |
66+
| Marketplace dict | `{ name: <plugin>, marketplace: <mkt>, version: <range> }` | Install a plugin from a registered marketplace. Optional `version` accepts a semver range (e.g. `~2.1.0`). The publisher controls the tag naming convention. |
6767
| Registry shorthand | `owner/repo#^2.0.0` with a default registry configured | Routes dep through the default registry instead of git. Default may come from `apm.yml` or `~/.apm/config.json`. Requires `registries` experimental flag. |
6868
| Registry object form | `{ id: owner/repo, version: ^2.0.0 }` | Explicit registry dep. `registry:` optional when a default registry is configured. Requires `registries` experimental flag. |
6969

@@ -254,10 +254,18 @@ override the marketplace entry's default `source.ref`:
254254
apm install plugin@marketplace#v2.0.0
255255
```
256256

257-
In `apm.yml`, use the `version` field in the marketplace object form.
258-
Semver ranges and bare versions (e.g. `~2.1.0`, `^2.0`, `2.1.0`) are
259-
resolved against git tags matching `{name}--v{version}` on the
260-
marketplace repository. The highest matching tag is used.
257+
In `apm.yml`, use the `version` field in the marketplace object form. Semver
258+
ranges and bare versions (for example `~2.1.0`, `^2.0`, or `2.1.0`) resolve
259+
against the publisher's effective tag pattern: the package override first,
260+
then the marketplace build default. Old marketplace metadata without a
261+
`tag_pattern` field keeps the legacy `{name}--v{version}` convention.
262+
Malformed patterns and ranges with no match fail instead of becoming raw refs.
263+
264+
`apm install` resolves and locks the highest matching tag. Re-running it
265+
replays the locked version without changes. After the producer adds tags and
266+
republishes the marketplace metadata, run `apm marketplace update <name>`;
267+
`apm outdated` then reports the new resolved ref and `apm update --yes`
268+
applies it.
261269

262270
### Pin a semver range
263271

docs/src/content/docs/producer/publish-to-a-marketplace.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ for the full validation and override rules.
140140
The generated source object is also a producer-to-consumer contract.
141141
`apm pack` emits `source: url` for a remote repository and
142142
`source: git-subdir` when `subdir` is set.
143+
It also emits the effective `source.tag_pattern`: a package override takes
144+
precedence over `marketplace.build.tagPattern`. Repack and publish the generated
145+
metadata after changing either value so consumers receive the new convention.
143146
`apm install <package>@<marketplace>` accepts both forms, derives the package host from
144147
the generated entry rather than from the marketplace host, and preserves
145148
the generated path and ref.

docs/src/content/docs/reference/manifest-schema.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,22 @@ Marketplace dependency (resolved at install time):
497497

498498
The `marketplace` key is mutually exclusive with `git`, `path`, `registry`, and `id`; combining them raises a parse error. Unknown keys in a marketplace entry are rejected. During dependency resolution the resolver calls `resolve_marketplace_plugin()`. A plugin entry that declares `registry` plus a semver `version` becomes a registry-sourced dependency using its declared owner/repo repository identity. Other entries become concrete Git coordinates (owner/repo, ref, and optional virtual path).
499499

500-
When `version` is specified and is a semver range or bare version number (e.g. `~2.1.0`, `^2.0`, `2.1.0`), the resolver lists git tags on the marketplace repository matching the `{name}--v{version}` convention, filters to those satisfying the constraint, and resolves to the highest matching tag. If no tag satisfies an explicit semver range, resolution fails with a `NoMatchingVersionError`. A bare version with no matching tag falls back to using the value as a raw git ref. Pre-release versions (e.g. `2.0.0-beta.1`) are excluded from semver-range resolution; target them explicitly as raw git refs. When `version` is a raw git ref (e.g. `v2.0.0`, `main`, or a commit SHA), it is used as a direct ref override without tag resolution.
500+
When `version` is a semver range or bare version number (for example
501+
`~2.1.0`, `^2.0`, or `2.1.0`), the resolver uses the plugin source's
502+
`tag_pattern`. `apm pack` emits that effective pattern from the package's
503+
`tag_pattern` override, then `marketplace.build.tagPattern`. Marketplace files
504+
created before this field existed fall back to the legacy
505+
`{name}--v{version}` consumer convention. A pattern must contain exactly one
506+
`{version}` placeholder; `{name}` is optional. Unsupported or malformed
507+
patterns, and ranges or bare versions with no matching tags, fail without
508+
falling back to a raw ref. Pre-release versions are excluded from range resolution; target them
509+
explicitly as raw git refs. Raw refs such as `v2.0.0`, `main`, or a commit SHA
510+
bypass tag resolution.
511+
512+
`source.tag_pattern` is validated while the catalog is loaded. One malformed
513+
pattern makes `apm marketplace add` or `apm marketplace update` reject the
514+
entire marketplace, so consumers never resolve against a partially accepted
515+
catalog.
501516

502517
Resolution failures stop the install instead of silently skipping the dependency. The lockfile records the **resolved** coordinates and pinned commit, not the marketplace placeholder. Unresolved marketplace dependencies cannot compute install paths or serialize back to `apm.yml`.
503518

@@ -883,7 +898,7 @@ marketplace:
883898

884899
| Field | Type | Default | Description |
885900
|---|---|---|---|
886-
| `tagPattern` | `string` | `v{version}` | Pattern used to construct git tags for packages. MUST contain at least one of `{version}` or `{name}`. Per-package overrides live on `packages[].tag_pattern`. |
901+
| `tagPattern` | `string` | `v{version}` | Pattern used to construct git tags for packages. MUST contain exactly one `{version}`; `{name}` is optional. Per-package overrides live on `packages[].tag_pattern`. |
887902

888903
### 7.5. `marketplace.packages`
889904

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,19 @@ that at most one declaration remains.
229229
|-------|----------|-------------|
230230
| `name` | REQUIRED | Plugin identifier within the marketplace (`^[a-zA-Z0-9._-]+$`). |
231231
| `marketplace` | REQUIRED | Registered marketplace name (`^[a-zA-Z0-9._-]+$`). |
232-
| `version` | OPTIONAL | Semver range or exact version (e.g. `~2.1.0`, `^2.0`, `>=1.4`, `2.1.0`). Resolved against `{name}--v{version}` git tags on the marketplace repo. |
232+
| `version` | OPTIONAL | Semver range or exact version (e.g. `~2.1.0`, `^2.0`, `>=1.4`, `2.1.0`). Resolved against git tags using the publisher's effective pattern: `packages[].tag_pattern`, then `marketplace.build.tagPattern`. Older metadata without `source.tag_pattern` uses the legacy `{name}--v{version}` fallback. |
233233

234234
During resolution, marketplace entries are looked up in the marketplace's
235235
`marketplace.json` and replaced with concrete git coordinates. When `version`
236236
is a semver range or bare version number, the resolver lists git tags
237-
matching `{name}--v{version}`, filters by the constraint, and picks the
238-
highest matching tag. Raw git refs (e.g. `v2.0.0`, `main`) bypass tag
239-
resolution and override the source ref directly. The lockfile records the
240-
resolved ref, not the marketplace placeholder. Unknown keys in a marketplace
241-
entry are rejected.
237+
using the `source.tag_pattern` emitted by `apm pack`. The package-level
238+
`tag_pattern` overrides `marketplace.build.tagPattern`. APM filters by the
239+
constraint and picks the highest matching tag. Old `marketplace.json` files
240+
that omit `source.tag_pattern` fall back to `{name}--v{version}`. Patterns
241+
must contain exactly one `{version}` placeholder, and a no-match does not
242+
silently become a raw ref. Raw git refs (e.g. `v2.0.0`, `main`) bypass tag
243+
resolution. The lockfile records the resolved ref, not the marketplace
244+
placeholder. Unknown keys in a marketplace entry are rejected.
242245

243246
Producer-emitted `source: url` and `source: git-subdir` objects resolve
244247
through the same Git dependency parser as direct object-form dependencies.

packages/apm-guide/.apm/skills/apm-usage/package-authoring.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,9 @@ The compiler:
771771

772772
1. Emits `plugins:` verbatim (Anthropic's key name).
773773
2. Copies `metadata:` byte-for-byte.
774-
3. Strips `build:`, per-plugin `version`, `tag_pattern`, `include_prerelease`.
774+
3. Strips authoring-only `build:`, per-plugin `version`, and
775+
`include_prerelease`; emits the effective package-over-build tag convention
776+
as remote `source.tag_pattern`.
775777
4. Omits empty `tags:` and inherited top-level `description`/`version`
776778
from the output (matches Anthropic's canonical hand-authored shape,
777779
e.g. microsoft/azure-skills).

scripts/lint-architecture-boundaries.sh

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,6 @@ if ! grep -q '_clear_platform_token_env(env)' src/apm_cli/core/auth.py \
12711271
[ -n "$ado_transport_direct_hits" ] && echo "$ado_transport_direct_hits"
12721272
violations=$((violations + 1))
12731273
fi
1274-
12751274
echo "[*] AC28: JetBrains Copilot MCP config-path authority"
12761275
intellij_path_owner="src/apm_cli/adapters/client/intellij.py"
12771276
intellij_path_owner_count=$(grep -Ec '^def _intellij_config_dir\(' "$intellij_path_owner" || true)
@@ -1294,6 +1293,30 @@ if [ "$intellij_path_owner_count" -ne 1 ] \
12941293
violations=$((violations + 1))
12951294
fi
12961295

1296+
echo "[*] AC27: marketplace tag-pattern authority"
1297+
tag_pattern_owner="src/apm_cli/marketplace/tag_pattern.py"
1298+
tag_pattern_parallel_hits=$(
1299+
grep -rEn --include='*.py' \
1300+
'["'\'']\{version\}["'\''][[:space:]]+(not[[:space:]]+)?in[[:space:]]+(pattern|tag_pattern)|\.(count)\(["'\'']\{version\}["'\'']\)' \
1301+
src/apm_cli/marketplace \
1302+
| grep -v "^${tag_pattern_owner}:" \
1303+
| grep -v 'architecture-authority-exempt:' \
1304+
|| true
1305+
)
1306+
if ! grep -q '^def validate_tag_pattern(' "$tag_pattern_owner" \
1307+
|| ! grep -A8 '^def _validate_tag_pattern(' \
1308+
src/apm_cli/marketplace/yml_schema.py \
1309+
| grep -q 'validate_tag_pattern(pattern, context=context)' \
1310+
|| ! grep -A12 'raw_tp = source.get("tag_pattern")' \
1311+
src/apm_cli/marketplace/models.py \
1312+
| grep -q 'tag_pattern = validate_tag_pattern(' \
1313+
|| ! grep -q 'tag_pattern = validate_tag_pattern(tag_pattern)' \
1314+
src/apm_cli/marketplace/version_resolver.py \
1315+
|| [ -n "$tag_pattern_parallel_hits" ]; then
1316+
echo "[x] Marketplace tag patterns must route through marketplace/tag_pattern.py"
1317+
[ -n "$tag_pattern_parallel_hits" ] && echo "$tag_pattern_parallel_hits"
1318+
violations=$((violations + 1))
1319+
fi
12971320
if [ "$violations" -gt 0 ]; then
12981321
echo "[x] $violations architecture boundary rule(s) failed"
12991322
exit 1

0 commit comments

Comments
 (0)