From cda86437f0e104265a1856d03f1dfdb6d8d5ddfc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=AE=B8=E6=9C=AC=E5=BC=BA?= <820396428@qq.com>
Date: Tue, 3 Nov 2026 00:18:19 +0800
Subject: [PATCH] fix(bundle): keep a directory source package out of the
bundle path
`apm install
` sends every directory with a plugin.json at its root down
the local-bundle path, even when the directory is a normal APM source package
with an apm.yml and no pack manifest. That path warns about an older APM
version, copies the source tree verbatim and records no dependency.
Only treat a directory with an apm.yml as a bundle when its lockfile carries the
pack manifest written by `apm pack`; otherwise it stays a local dependency.
Fixes #3443
---
src/apm_cli/bundle/local_bundle.py | 12 ++++++++++++
tests/unit/bundle/test_local_bundle.py | 21 +++++++++++++++++++++
2 files changed, 33 insertions(+)
diff --git a/src/apm_cli/bundle/local_bundle.py b/src/apm_cli/bundle/local_bundle.py
index 264cf61c87..9c442e57b5 100644
--- a/src/apm_cli/bundle/local_bundle.py
+++ b/src/apm_cli/bundle/local_bundle.py
@@ -179,6 +179,13 @@ def _read_bundle_lockfile(bundle_dir: Path) -> dict[str, Any] | None:
return data
+def _has_pack_manifest(bundle_dir: Path) -> bool:
+ """Whether ``apm.lock.yaml`` carries the pack manifest written by ``apm pack``."""
+ lockfile = _read_bundle_lockfile(bundle_dir)
+ pack = (lockfile or {}).get("pack")
+ return isinstance(pack, dict) and bool(pack)
+
+
def _extract_pack_targets(lockfile: dict[str, Any] | None) -> list[str]:
"""Return list of pack targets from a parsed bundle lockfile."""
if not lockfile:
@@ -345,6 +352,11 @@ def detect_local_bundle(path: Path) -> LocalBundleInfo | None:
if path.is_dir():
if not (path / "plugin.json").is_file():
return None
+ if (path / "apm.yml").is_file() and not _has_pack_manifest(path):
+ # An APM source package that happens to ship a plugin.json is a normal
+ # local dependency, not a bundle: only packed bundles carry the pack
+ # manifest in apm.lock.yaml.
+ return None
return _build_info(
path,
is_archive=False,
diff --git a/tests/unit/bundle/test_local_bundle.py b/tests/unit/bundle/test_local_bundle.py
index f00c5c189f..c6639439ac 100644
--- a/tests/unit/bundle/test_local_bundle.py
+++ b/tests/unit/bundle/test_local_bundle.py
@@ -117,6 +117,27 @@ def _make_plugin_bundle(
return bundle
+def test_detect_local_bundle_skips_source_package_with_apm_yml(tmp_path: Path) -> None:
+ """A source package with apm.yml and plugin.json is a dependency, not a bundle."""
+ package = tmp_path / "demo-pkg"
+ package.mkdir()
+ (package / "apm.yml").write_text("name: demo-pkg\n", encoding="utf-8")
+ (package / "plugin.json").write_text(json.dumps({"id": "demo-pkg"}), encoding="utf-8")
+
+ assert detect_local_bundle(package) is None
+
+
+def test_detect_local_bundle_keeps_packed_bundle_with_apm_yml(tmp_path: Path) -> None:
+ """A packed bundle stays a bundle even when it also ships an apm.yml."""
+ bundle = _make_plugin_bundle(tmp_path)
+ (bundle / "apm.yml").write_text("name: demo-pkg\n", encoding="utf-8")
+
+ info = detect_local_bundle(bundle)
+
+ assert info is not None
+ assert info.source_dir == bundle
+
+
def _make_plugin_tarball(tmp_path: Path, bundle_dir: Path) -> Path:
"""Archive a bundle directory into .tar.gz."""
tmp_path.mkdir(parents=True, exist_ok=True)