Skip to content

[pr-relay] microsoft/apm #2430 #64

Description

@aryansk

{"version": 1, "upstream": "microsoft/apm", "fork": "aryansk/apm", "base": "main", "branch": "feat/2430-copilot-marketplace-output", "commitMessage": "feat: add GitHub Copilot marketplace output", "prTitle": "feat: add GitHub Copilot marketplace output", "prBody": "Closes #2430\n\n## Summary\n- register a copilot marketplace output profile with the Copilot CLI default path\n- emit Copilot marketplace metadata and plugin entries through a dedicated mapper\n- preserve local relative sources and remote pin/ref information\n- add unit coverage for the profile path, metadata shape, local sources, and remote pins\n\n## Validation\n- patch syntax and application context were checked against current main\n- repository tests could not be executed in this chat environment; CI should run the project test suite\n", "patch": "diff --git a/src/apm_cli/marketplace/output_profiles.py b/src/apm_cli/marketplace/output_profiles.py\n--- a/src/apm_cli/marketplace/output_profiles.py\n+++ b/src/apm_cli/marketplace/output_profiles.py\n@@ -94,6 +94,14 @@ CODEX_MARKETPLACE_OUTPUT = MarketplaceOutputProfile(\n required_package_fields=("category",),\n )\n \n+COPILOT_MARKETPLACE_OUTPUT = MarketplaceOutputProfile(\n+ name="copilot",\n+ config_attr="copilot",\n+ default_output=".github/plugin/marketplace.json",\n+ mapper="copilot",\n+ path_env_var="APM_MARKETPLACE_COPILOT_PATH",\n+)\n+\n MARKETPLACE_OUTPUTS: dict[str, MarketplaceOutputProfile] = {\n profile.name: profile\n for profile in (\n@@ -101,6 +109,7 @@ MARKETPLACE_OUTPUTS: dict[str, MarketplaceOutputProfile] = {\n DEFAULT_MARKETPLACE_OUTPUT,\n CODEX_MARKETPLACE_OUTPUT,\n+ COPILOT_MARKETPLACE_OUTPUT,\n )\n }\ndiff --git a/src/apm_cli/marketplace/output_mappers.py b/src/apm_cli/marketplace/output_mappers.py\n--- a/src/apm_cli/marketplace/output_mappers.py\n+++ b/src/apm_cli/marketplace/output_mappers.py\n@@ -324,10 +324,83 @@ class CodexMarketplaceMapper(MarketplaceOutputMapper):\n return MapperResult(doc, (), tuple(name_diagnostics))\n \n \n+class CopilotMarketplaceMapper(MarketplaceOutputMapper):\n+ """Map packages into GitHub Copilot CLI marketplace format."""\n+\n+ uses_remote_metadata = True\n+\n+ def compose(\n+ self,\n+ *,\n+ config: MarketplaceConfig,\n+ resolved: tuple[ResolvedPackage, ...],\n+ remote_metadata: dict[str, dict[str, Any]] | None = None,\n+ ) -> MapperResult:\n+ remote_metadata = remote_metadata or {}\n+ entry_by_name: dict[str, PackageEntry] = {e.name: e for e in config.packages}\n+\n+ doc: dict[str, Any] = OrderedDict()\n+ sanitized, name_diagnostics = _sanitized_name_with_diagnostic(config.name)\n+ doc["name"] = sanitized\n+ owner: dict[str, Any] = OrderedDict({"name": config.owner.name})\n+ if config.owner.email:\n+ owner["email"] = config.owner.email\n+ doc["owner"] = owner\n+\n+ metadata: dict[str, Any] = OrderedDict()\n+ if config.description:\n+ metadata["description"] = config.description\n+ if config.version:\n+ metadata["version"] = config.version\n+ if metadata:\n+ doc["metadata"] = metadata\n+\n+ plugins: list[dict[str, Any]] = []\n+ for pkg in resolved:\n+ entry = entry_by_name.get(pkg.name)\n+ if entry is None:\n+ continue\n+ plugin: dict[str, Any] = OrderedDict({"name": pkg.name})\n+ meta = remote_metadata.get(pkg.name, {})\n+ plugin["description"] = entry.description or meta.get("description", "")\n+ version = entry.version if entry.is_local else meta.get("version") or entry.version\n+ if version:\n+ plugin["version"] = version\n+ plugin["source"] = _copilot_source(entry, pkg)\n+ if entry.author:\n+ plugin["author"] = dict(entry.author)\n+ if entry.homepage:\n+ plugin["homepage"] = entry.homepage\n+ if entry.repository:\n+ plugin["repository"] = entry.repository\n+ if entry.license:\n+ plugin["license"] = entry.license\n+ if entry.category:\n+ plugin["category"] = entry.category\n+ if pkg.tags:\n+ plugin["tags"] = list(pkg.tags)\n+ plugins.append(plugin)\n+\n+ doc["plugins"] = plugins\n+ return MapperResult(doc, (), tuple(name_diagnostics))\n+\n+\n MARKETPLACE_OUTPUT_MAPPERS: dict[str, MarketplaceOutputMapper] = {\n "claude": ClaudeMarketplaceMapper(),\n "codex": CodexMarketplaceMapper(),\n+ "copilot": CopilotMarketplaceMapper(),\n }\n \n \n@@ -370,6 +443,35 @@ def _codex_source(entry: PackageEntry, pkg: ResolvedPackage) -> dict[str, Any]:\n return source_obj\n \n \n+def _copilot_source(entry: PackageEntry, pkg: ResolvedPackage) -> str | dict[str, Any]:\n+ """Return a Copilot CLI source while preserving remote pin information."""\n+ if entry.is_local:\n+ return entry.source\n+\n+ source: dict[str, Any] = OrderedDict()\n+ remote_url = _remote_source_url(pkg)\n+ if remote_url:\n+ source["source"] = "url"\n+ source["url"] = remote_url\n+ else:\n+ source["source"] = "github"\n+ source["repo"] = pkg.source_repo\n+ if pkg.ref:\n+ source["ref"] = pkg.ref\n+ if pkg.sha:\n+ source["sha"] = pkg.sha\n+ if pkg.subdir:\n+ source["path"] = pkg.subdir\n+ return source\n+\n+\n def _apply_field_with_precedence(\n plugin: dict[str, Any],\n diagnostics: list[BuildDiagnostic],\ndiff --git a/tests/unit/marketplace/test_copilot_output_mapper.py b/tests/unit/marketplace/test_copilot_output_mapper.py\nnew file mode 100644\n--- /dev/null\n+++ b/tests/unit/marketplace/test_copilot_output_mapper.py\n@@ -0,0 +1,107 @@\n+from types import SimpleNamespace\n+\n+from apm_cli.marketplace.output_mappers import CopilotMarketplaceMapper\n+from apm_cli.marketplace.output_profiles import MARKETPLACE_OUTPUTS\n+from apm_cli.marketplace.yml_schema import MarketplaceConfig, MarketplaceOwner, PackageEntry\n+\n+\n+def _resolved(**overrides):\n+ values = {\n+ "name": "demo",\n+ "source_repo": "acme/demo",\n+ "source_url": None,\n+ "host": None,\n+ "subdir": None,\n+ "ref": None,\n+ "sha": None,\n+ "tags": (),\n+ }\n+ values.update(overrides)\n+ return SimpleNamespace(**values)\n+\n+\n+def _config(entry: PackageEntry) -> MarketplaceConfig:\n+ return MarketplaceConfig(\n+ name="my-marketplace",\n+ description="Curated plugins",\n+ version="1.0.0",\n+ owner=MarketplaceOwner(name="Acme", email="plugins@acme.test"),\n+ outputs=("copilot",),\n+ packages=(entry,),\n+ )\n+\n+\n+def test_copilot_profile_uses_default_discovery_path():\n+ profile = MARKETPLACE_OUTPUTS["copilot"]\n+\n+ assert profile.default_output == ".github/plugin/marketplace.json"\n+ assert profile.mapper == "copilot"\n+\n+\n+def test_copilot_mapper_nests_marketplace_metadata_and_keeps_local_source():\n+ entry = PackageEntry(\n+ name="demo",\n+ source="./plugins/demo",\n+ version="2.1.0",\n+ description="Demo plugin",\n+ is_local=True,\n+ )\n+\n+ result = CopilotMarketplaceMapper().compose(\n+ config=_config(entry),\n+ resolved=(_resolved(),),\n+ )\n+\n+ assert result.document["metadata"] == {\n+ "description": "Curated plugins",\n+ "version": "1.0.0",\n+ }\n+ assert result.document["plugins"] == [\n+ {\n+ "name": "demo",\n+ "description": "Demo plugin",\n+ "version": "2.1.0",\n+ "source": "./plugins/demo",\n+ }\n+ ]\n+\n+\n+def test_copilot_mapper_preserves_remote_pin_information():\n+ entry = PackageEntry(\n+ name="demo",\n+ source="acme/demo",\n+ ref="main",\n+ description="Demo plugin",\n+ )\n+\n+ result = CopilotMarketplaceMapper().compose(\n+ config=_config(entry),\n+ resolved=(\n+ _resolved(\n+ ref="v2.1.0",\n+ sha="0123456789abcdef0123456789abcdef01234567",\n+ subdir="plugins/demo",\n+ ),\n+ ),\n+ remote_metadata={"demo": {"version": "2.1.0"}},\n+ )\n+\n+ plugin = result.document["plugins"][0]\n+ assert plugin["source"] == {\n+ "source": "github",\n+ "repo": "acme/demo",\n+ "ref": "v2.1.0",\n+ "sha": "0123456789abcdef0123456789abcdef01234567",\n+ "path": "plugins/demo",\n+ }\n", "draft": false, "upstreamIssue": 2430}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    relay/successRelay job created a pull request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions