Repository navigation
Description
Describe the bug
apm install <plugin>@<marketplace> (with or without --update) silently installs nothing when the marketplace's
plugins live on a GitHub Enterprise Server host whose first label is github (e.g. github.example.com) and
GITHUB_HOST is unset. Validation succeeds and rewrites the plugin's apm.yml entry, then the --only filter drops
the package, so the download and integrate phases see nothing. The run reports No changes and exits 0.
Cause: only_packages_from_validation hands the pipeline the canonical string
(github.example.com/org/repo/plugins/p#<sha>). _apply_only_filter (install/phases/resolve.py:936-941) re-parses
it with DependencyReference.parse. For this host shape, parse_host_qualified_reference raises
UnsupportedHostQualifiedVirtualPackageError (models/dependency/host_virtual.py:195-196, via
_looks_like_unconfigured_platform_host at :335-342). The except Exception branch then adds the raw string, #<sha>
included, to only_identities. That string never equals the manifest dependency's identity
(github.example.com/org/repo/plugins/p), so ctx.deps_to_install becomes empty.
With --update this leaves the manifest and lock disagreeing: validation already wrote the new ref into apm.yml
("updated ref in apm.yml"), while the lock keeps the old resolved_commit. apm install --frozen then refuses
(lockfile_satisfies_manifest: manifest commit != lockfile resolved_commit).
To Reproduce
Pure functions, no network. With apm 0.33.0's interpreter:
from types import SimpleNamespace
from apm_cli.install.phases.resolve import _apply_only_filter
from apm_cli.models.apm_package import DependencyReference
SHA = "0123456789abcdef0123456789abcdef01234567"
def case(host):
# What validation hands the pipeline for `plugin@marketplace` (only_packages_from_validation: the canonical).
canonical = f"{host}/org/plugins-repo/plugins/p#{SHA}"
# What the manifest holds after validation wrote the structured entry.
dep = DependencyReference.parse_from_dict(
{"git": f"git@{host}:org/plugins-repo.git", "path": "plugins/p", "ref": SHA})
try:
parsed = DependencyReference.parse(canonical).get_identity()
except Exception as e: # noqa: BLE001
parsed = f"raises {type(e).__name__}"
ctx = SimpleNamespace(only_packages=[canonical], deps_to_install=[dep],
dependency_graph=SimpleNamespace(dependency_tree=SimpleNamespace(nodes={})))
_apply_only_filter(ctx)
print(f"{host:18} manifest identity={dep.get_identity()!r}\n{'':18} parse(canonical)={parsed!r}\n"
f"{'':18} kept after --only filter: {len(ctx.deps_to_install)} of 1")
case("github.com") # control
case("git.example.com") # control: a generic host
case("github.example.com") # a GitHub Enterprise Server host, GITHUB_HOST unsetOutput:
github.com manifest identity='org/plugins-repo/plugins/p'
parse(canonical)='org/plugins-repo/plugins/p'
kept after --only filter: 1 of 1
git.example.com manifest identity='git.example.com/org/plugins-repo/plugins/p'
parse(canonical)='git.example.com/org/plugins-repo/plugins/p'
kept after --only filter: 1 of 1
github.example.com manifest identity='github.example.com/org/plugins-repo/plugins/p'
parse(canonical)='raises UnsupportedHostQualifiedVirtualPackageError'
kept after --only filter: 0 of 1
The host is the only variable between the three cases.
End to end, I saw the same thing against a real GHES-hosted marketplace (an SSH remote; relative-path plugins in one
repo; the marketplace re-registered at a newer commit), running apm install -g --update -t claude,codex <plugin>@<marketplace> --verbose. Excerpt, with host, org and plugin names replaced:
Resolved to: <ghes-host>/<org>/<repo>/plugins/<plugin>#<new-sha>
git ls-remote (ssh) rc=0 for <ghes-host>/<org>/<repo>/plugins/<plugin>#<new-sha>
Updated existing dependency entry to structured form: <ghes-host>/<org>/<repo>/plugins/<plugin>#<new-sha>
[+] <ghes-host>/<org>/<repo>/plugins/<plugin>#<new-sha> (updated ref in apm.yml)
[*] Updated apm.yml dependency entries
[>] Installing 1 new package...
<ghes-host>/<org>/<repo>/plugins/<plugin>: locked at <old-sha>
...
Phase: download -> 0.000s
[+] <project root> (local)
...
[i] No changes -- install state already up to date in 6.2s.
Exit code 0. Afterwards apm.yml holds ref: <new-sha> for the plugin, the lock still says resolved_commit: <old-sha>, and apm_modules/ still holds the old bytes. A first install of such a plugin installs nothing in the
same way.
Expected behavior
The positional package is installed (or updated) at the ref validation resolved, as for a github.com or a generic
host. If apm cannot install it, the command fails and leaves apm.yml as it was.
Environment (please complete the following information):
- OS: macOS 27.0.1
- Python Version: 3.14.8 (Homebrew apm keg)
- APM Version: 0.33.0.
_apply_only_filterand_looks_like_unconfigured_platform_hostare unchanged at v0.34.0
(read at the tag, not run).
Logs
Above.
Additional context
GITHUB_HOST=<ghes-host>makes the parse succeed, but it is not a workaround when the same manifest also holds
github.com dependencies:default_host()(utils/github_host.py:65-67) then sends every bareowner/repothere.- Validation already holds the parsed
DependencyReferencefor each positional package. Passing its identity to the
filter, instead of re-parsing the canonical string, would avoid the lossy round trip. At minimum, a spec the filter
cannot parse should fail the install rather than match nothing. - A plain
apm install -g(no positional) after the failed update does see the drift and fetches the new ref. It is
not a workaround either: a hooks-only package from the same marketplace then fails "Downloaded package has no
apm.yml or SKILL.md", which the positional (marketplace) path accepts. That may be a separate issue; I have not
isolated it.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
danielmeppiel commentedon Oct 20, 2026
Closing this as fixed on current main by #3156, with an important release distinction: the fix is not in the latest published release, v0.34.0. That release was published on October 19 before #3156 merged later that day, so the reported released-version failure remains credible.
The reported journey was checked at
53ccf06d11dd2d675e8ea8c3e2523a6d321c1b79: register a marketplace at commit A, install its relative-path native plugin, advance and re-register the marketplace at commit B, then run the positional install with--update. The manifest, lock and deployed skill all moved to B, and subsequent frozen replay succeeded.Six real Python-CLI lifecycle scenarios passed:
github.example.com,github.comand a generic-host control, each in project and user scope withGITHUB_HOSTunset. The 42 commands included dry-run, first install, repeat install, update and frozen replay. These used the existing APMLifecycle helpers, isolated configuration and real local Git fixtures behind SSH-shaped URLs, with IP networking disabled. They were not live GHES, SSH-authentication, packaged-binary or cross-platform tests.The relevant change in #3156 keeps already-interpreted dependency references instead of reparsing their display strings. All 18 observed selection calls had matching requested and resolved refs and retained the requested package. The empty dependency graph in the issue's small reproducer does not represent those normal CLI calls.
Thank you for the detailed report, and Aayush D C Dangi (@dcaayushd) for #3296. That PR handles a manually constructed old-graph/new-selector mismatch, but the current CLI journeys did not reach it. The PR remains open and is not approved or closed by this decision. If a supported install still fails on current main, please provide that complete command sequence and resulting manifest/lock state so the remaining case can be evaluated without adding an unnecessary second fix.
No release date is promised by this closure.
AI-assisted investigation and write-up; this completed disposition was explicitly confirmed by Daniel Meppiel (@danielmeppiel).