Skip to content

feat(marketplace): adopt apm pack as canonical marketplace.json builder#1570

Draft
danielmeppiel wants to merge 6 commits intogithub:stagedfrom
danielmeppiel:feat/apm-pack-marketplace
Draft

feat(marketplace): adopt apm pack as canonical marketplace.json builder#1570
danielmeppiel wants to merge 6 commits intogithub:stagedfrom
danielmeppiel:feat/apm-pack-marketplace

Conversation

@danielmeppiel
Copy link
Copy Markdown
Contributor

@danielmeppiel danielmeppiel commented Apr 30, 2026

feat(marketplace): adopt apm pack as canonical marketplace.json builder + CI gates

TL;DR

Wires apm pack as the canonical aggregation engine for .github/plugin/marketplace.json, replacing the bespoke eng/generate-marketplace.mjs Node script. Adds a root apm.yml declaring all 64 local plugins (no per-plugin plugin.json files are touched), a small bridge that folds in the 9 entries from plugins/external.json, and two CI gates -- apm audit --ci for supply-chain integrity and a marketplace.json drift check -- both wired through microsoft/apm-action@v1 so the publish pipeline and PR validation share one CLI install path. Output is byte-equivalent to the legacy build (73 plugins, alphabetical).

Note

Companion issue: #1569. Upstream enabler for retiring plugins/external.json: microsoft/apm#1061. Targets staged (not main) per this repo's publish flow.

Problem (WHY)

  • eng/generate-marketplace.mjs is a repo-specific aggregator with no shared contract -- contributors must read its implementation to understand how the 64 hand-authored plugins/<name>/.github/plugin/plugin.json files become marketplace.json.
  • No cross-ecosystem authoring substrate exists today for Copilot plugin marketplaces; every repo publishing one ships its own generator, fragmenting the contributor experience.
  • The publish pipeline regenerates marketplace.json post-merge to staged, but nothing on the PR side validates that the committed file actually matches what the generator would produce -- so a hand-edit lands silently and is rewritten only after merge.
  • No supply-chain audit runs against the manifest. As soon as awesome-copilot grows real APM dependencies (skills, agents, MCP servers), there is no gate to enforce lockfile/ref consistency.

Why these matter: APM's anchoring principle is that "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action." A one-off generator without PR-time validation is the opposite -- output identity is tied to script implementation drift, not a versioned spec.

Approach (WHAT)

# Change
1 Add root apm.yml -- declares all 64 local plugins under marketplace.packages[], mirroring each plugin.json's version and description
2 Add eng/merge-external-plugins.mjs -- ~60-line bridge that appends entries from plugins/external.json post-apm pack and re-sorts alphabetically
3 Rewire package.json -- plugin:generate-marketplace -> apm pack ... && node eng/merge-external-plugins.mjs; legacy preserved as plugin:generate-marketplace:legacy for parity diffing
4 Add .github/workflows/validate-marketplace.yml -- PR-time Gate A (apm audit --ci via microsoft/apm-action@v1, conditional SARIF upload) and Gate B (marketplace.json drift)
5 Patch .github/workflows/publish.yml -- install APM via microsoft/apm-action@v1 before npm run build so the materialization job has apm on PATH
6 Update CONTRIBUTING.md + eng/README.md -- APM CLI prerequisite + register-in-apm.yml step

Implementation (HOW)

File Change
apm.yml (new, ~280 lines) Root manifest. marketplace.packages[] lists all 64 local plugins with source: ./plugins/<name>. --marketplace-output .github/plugin/marketplace.json keeps the install path stable. Per-plugin plugin.json files intentionally untouched.
eng/merge-external-plugins.mjs (new) Reads apm pack's output, appends plugins/external.json entries, sorts alphabetically, writes back. Inline comment documents the schema delta blocking native external declaration today (legacy uses source.source/source.repo; APM emits source.type/source.repository).
package.json Build script now apm pack --marketplace-output .github/plugin/marketplace.json && node ./eng/merge-external-plugins.mjs. Legacy preserved as :legacy.
.github/workflows/validate-marketplace.yml (new) PR gate triggered on changes to apm.yml, apm.lock.yaml, plugin manifests, the bridge, the legacy generator, or the committed marketplace.json. Gate A runs apm-action@v1 (which installs APM and runs apm audit -f sarif) then apm audit --ci; SARIF upload is guarded on hashFiles('apm-audit.sarif') != '' so a marketplace-only manifest with no dependencies: block (and therefore no SARIF artifact) does not falsely fail the upload. Gate B runs npm run build then fails on git status --porcelain of the committed marketplace.json.
.github/workflows/publish.yml Adds microsoft/apm-action@v1 step before npm run build. Without it, the publish job would command not found: apm post-merge.
.github/plugin/marketplace.json Regenerated. 73 entries (64 local + 9 external). Only deltas vs. legacy: key ordering and source: "./plugins/<name>" vs. bare "<name>" -- both semantically equivalent under metadata.pluginRoot: ./plugins.
CONTRIBUTING.md, eng/README.md APM CLI install one-liner + register-in-apm.yml step in the add-a-plugin walkthrough.

Diagrams

Build pipeline (authoring -> validation -> publish)

Dashed-border nodes are added by this PR. The dotted edge is the legacy generator, retained as a parity reference until follow-up F2 retires it.

flowchart LR
    subgraph Authoring["Authoring sources"]
        A["apm.yml<br/>(64 local pkgs)"]
        B["plugins/external.json<br/>(9 entries)"]
    end
    subgraph Build["npm run build"]
        C["apm pack"]
        D["merge-external-plugins.mjs"]
        E["generate-marketplace.mjs<br/>(legacy fallback)"]
    end
    subgraph Output["Committed artifact"]
        F[".github/plugin/marketplace.json<br/>(73 plugins, sorted)"]
    end
    A --> C
    C --> D
    B --> D
    D --> F
    E -.->|"DEPRECATED"| F
    classDef new stroke-dasharray: 5 5;
    class C,D new;
    classDef ext fill:#f5f5f5;
    class E ext;
Loading

CI gates on a PR

microsoft/apm-action@v1 installs the APM CLI once and is shared by both gates. apm audit --ci runs the full check set in apm_cli.policy.ci_checks (lockfile-exists, ref-consistency, deployed-files-present, no-orphaned-packages, config-consistency, content-integrity, includes-consent, skill-subset-consistency), so a separate git status lockfile check would be redundant.

sequenceDiagram
    participant PR as Pull Request
    participant Act as microsoft/apm-action@v1
    participant Audit as apm audit --ci
    participant Build as npm run build
    participant Git as git status

    PR->>Act: install APM CLI + apm audit -f sarif
    Act-->>PR: apm on PATH (+ SARIF when deps present)
    PR->>Audit: Gate A: policy checks
    Audit-->>PR: pass / fail (lockfile, refs, content)
    PR->>Build: Gate B: apm pack + merge bridge
    Build->>Git: rebuild .github/plugin/marketplace.json
    Git-->>PR: drift = fail with remediation hint
Loading

Trade-offs

  • Bridge over native external declaration. Folding plugins/external.json post-apm pack keeps this PR scoped to the aggregation layer and avoids changing the published source object schema. Going native requires upstream work tracked in microsoft/apm#1061 (extend marketplace.packages[] pass-through to include author/keywords/license/repository, plus let maintainer-supplied description/version override the remote-fetch fallback for third-party repos that don't ship an apm.yml). Once it lands, follow-up F3 deletes both the bridge and external.json.
  • Legacy generator preserved as :legacy. Kept runnable so maintainers can diff outputs across releases until F2 confirms parity. Deleting now would remove the safety net mid-migration. Rationale: "Favor small, chainable primitives over monolithic frameworks.".
  • Per-plugin plugin.json files untouched. Migrating each to per-plugin apm.yml (follow-up F1) is a separate, larger refactor. Out of scope keeps this PR reviewable in one pass.
  • No standalone apm.lock.yaml drift gate via git status. apm audit --ci already covers every realistic "edited apm.yml without running apm install" case via ref-consistency + lockfile-exists + no-orphaned-packages. The only thing it deliberately does NOT fail on is upstream-SHA drift on a still-internally-consistent lockfile -- that is apm outdated's informational job, otherwise every long-lived PR would break the moment a referenced ref moves.
  • SARIF upload guarded on hashFiles(). For a marketplace-only manifest, apm-action's audit-report step short-circuits ("No apm.lock.yaml found -- nothing to scan") and writes no file. The guard prevents a spurious upload failure today and activates automatically the moment a real dependency is added.

Benefits

  1. Contributors add a plugin by editing one entry in apm.yml -- no need to read the Node aggregation script.
  2. Marketplace generation is driven by a versioned, spec-compliant CLI (apm), reducing drift risk as the Anthropic plugin spec evolves.
  3. PR-time apm audit --ci provides supply-chain integrity from day one (no-op while the manifest is marketplace-only, fully active the moment dependencies land) -- no follow-up workflow change needed.
  4. PR-time drift gate makes hand-edits to the committed marketplace.json impossible to land silently; the publish pipeline no longer absorbs surprises.
  5. awesome-copilot becomes the highest-profile public consumer of apm pack (73 plugins from one manifest), validating APM's authoring story at scale.

Validation

npm run build on feat/apm-pack-marketplace:

> apm pack --marketplace-output .github/plugin/marketplace.json && node ./eng/merge-external-plugins.mjs

[+] APM marketplace built: 64 packages
Merged 9 external plugin(s) into marketplace.json (total: 73).

Validate Marketplace workflow on the latest push (run 25150945549): pass (20s).

Parity diff against legacy build
diff <(npm run plugin:generate-marketplace:legacy >/dev/null && jq -S . .github/plugin/marketplace.json) \
     <(npm run plugin:generate-marketplace        >/dev/null && jq -S . .github/plugin/marketplace.json)
# Only delta: source path-format ("plugin-name" vs. "./plugins/plugin-name").
# Entry count both sides: 73. External entries (9) byte-identical.

How to test

  • git fetch origin && git checkout feat/apm-pack-marketplace
  • Install APM CLI: curl -sSL https://raw.githubusercontent.com/microsoft/apm/main/install.sh | sh && npm install
  • npm run build exits 0; verify count: jq '.plugins | length' .github/plugin/marketplace.json returns 73
  • apm audit --ci exits 0 with lockfile-exists short-circuit pass (no dependencies: block today)
  • npm run plugin:generate-marketplace:legacy && git diff -- .github/plugin/marketplace.json shows only source path-format deltas -- no missing or extra entries

Follow-ups

  • F1: per-plugin migration to apm.yml (eliminate the 64 plugin.json files).
  • F2: retire eng/generate-marketplace.mjs after F1 parity is confirmed.
  • F3: native external sources in apm.yml (retire plugins/external.json + bridge), gated on microsoft/apm#1061.
  • README PR (separate, maintainer-gated): growth-hacker proposal queued in PR comment.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI review requested due to automatic review settings April 30, 2026 06:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This PR targets main, but PRs should target staged.

The main branch is auto-published from staged and should not receive direct PRs.
Please close this PR and re-open it against the staged branch.

You can change the base branch using the Edit button at the top of this PR,
or run: gh pr edit 1570 --base staged

@danielmeppiel
Copy link
Copy Markdown
Contributor Author

Follow-up commit: CI/CD wiring + apm audit gate (mirroring microsoft/apm)

Pushed 03c3703 to add the missing CI integration. Two changes:

1. publish.yml (fix): the existing publish-from-staged workflow runs npm run build, which after this PR invokes apm pack. Without the apm CLI on PATH, that step would fail at publish time. Added microsoft/apm-action@v1 before the build step. This mirrors microsoft/apm's own ci.yml self-check pattern (see microsoft/apm/.github/workflows/ci.yml).

2. validate-marketplace.yml (new): PR-time validation gate. Triggers on any change to apm.yml, apm.lock.yaml, plugin manifests, plugins/external.json, the merge bridge, or marketplace.json itself. Two subgates:

  • Gate A — apm audit --ci: supply-chain integrity. Validates lockfile / install fidelity, ref consistency between apm.yml and apm.lock.yaml, no orphan packages, content-integrity scan (hidden-Unicode, etc.) on deployed package content. The SARIF report is uploaded via github/codeql-action/upload-sarif@v3 under the apm-audit category, so findings show up in Security -> Code scanning alongside other scanners.

  • Gate B — drift check: rebuilds marketplace.json with apm pack + the external-plugin merge bridge and fails if the result differs from what's committed. Catches contributors who edit apm.yml without re-running npm run build, and contributors who hand-edit the generated marketplace.json. This is the same drift-gate pattern microsoft/apm uses for its own regenerated content.

Net effect: the marketplace authoring chain is now fully self-validating in CI, and supply-chain risk on every plugin update surfaces in the standard GitHub security UI -- not just at runtime when end users install. Both gates run on PRs to staged and main.

danielmeppiel and others added 2 commits April 30, 2026 08:24
Introduce APM (microsoft/apm) as the marketplace authoring substrate.
Root apm.yml declares all 53 local plugins under marketplace.packages;
'apm pack' emits the Anthropic-spec marketplace.json. A small
merge-external-plugins.mjs bridge appends plugins/external.json
entries (kept as a separate concern this round) and re-sorts the
combined list alphabetically.

The legacy generator (eng/generate-marketplace.mjs) is preserved as
'npm run plugin:generate-marketplace:legacy' for parity comparisons
during the transition.

- npm run build: now invokes apm pack + bridge merge
- 54 plugins out, name-parity with previous output verified
- per-plugin plugin.json files untouched (follow-up: per-plugin apm.yml)
- plugins/external.json untouched (follow-up: native external sources)
- CONTRIBUTING.md: apm CLI prerequisite + apm.yml registration step
- eng/README.md: marketplace generation section rewritten

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two CI changes mirroring how microsoft/apm uses microsoft/apm-action@v1
in its own self-check workflow:

1. publish.yml: add 'microsoft/apm-action@v1' step before 'npm run build'.
   The build now invokes 'apm pack', which requires the apm CLI on PATH.
   Without this step the publish-from-staged workflow would fail after
   this PR merges.

2. validate-marketplace.yml (new): PR-time gate that runs on changes to
   any marketplace.json input. Two subgates:
     - Gate A: 'apm audit --ci' for supply-chain integrity (lockfile /
       install fidelity, ref consistency, content-integrity scan).
       Emits SARIF, uploaded to GitHub code scanning under category
       'apm-audit'.
     - Gate B: rebuilds marketplace.json with 'apm pack' + the merge
       bridge and fails if the result differs from what's committed.
       Catches contributors who edit apm.yml without re-running
       'npm run build'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel changed the base branch from main to staged April 30, 2026 06:24
@danielmeppiel danielmeppiel force-pushed the feat/apm-pack-marketplace branch from 03c3703 to 530351b Compare April 30, 2026 06:24
@danielmeppiel
Copy link
Copy Markdown
Contributor Author

Rebased onto staged + retargeted PR base

Apologies for the noise -- I originally branched from main rather than staged, which produced a misleading conflict picture (since main is force-pushed from staged after materialization, the two branches diverge by ~99k lines of generated content).

Now corrected:

  • Hard-reset the branch to upstream/staged and cherry-picked the two commits cleanly.
  • Regenerated apm.yml from the current plugins/ tree on staged: now lists 64 local plugins (was 53; staged has gained 12 plugins and lost dataverse since I started). Description / version copied from each plugin.json.
  • plugins/external.json on staged has 9 entries (not 1 as on main); the merge bridge now produces a 73-plugin marketplace, byte-name-parity with what eng/generate-marketplace.mjs would emit on staged.
  • Verified npm run build is a no-op against the committed marketplace.json (the drift gate in validate-marketplace.yml will pass).
  • Force-pushed; PR base retargeted to staged. PR shows MERGEABLE again.

No content changes vs the previous review state -- just rebased onto the correct branch and the apm.yml is now in sync with the current plugin set.

@danielmeppiel danielmeppiel marked this pull request as draft April 30, 2026 06:26
apm-action's audit-report step short-circuits when there is no
apm.lock.yaml ('No apm.lock.yaml found -- nothing to scan') and
writes no SARIF file. The unconditional upload step then failed
with 'Path does not exist: apm-audit.sarif'.

Marketplace-only manifests legitimately have no dependencies to
scan, so the absence of a SARIF file is not an error -- only its
presence-with-failures would be. Guard the upload on
hashFiles('apm-audit.sarif') != '' so the gate stays green for
marketplace-only repos and lights up the moment awesome-copilot
adds a real dependency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Contributor Author

Two follow-ups from the discussion:

1. CI fix pushed (efe3f2e) -- the Validate Marketplace failure was the SARIF upload step running unconditionally. apm-action correctly short-circuits when there is no apm.lock.yaml to scan (this manifest is marketplace-only, no dependencies: block, so nothing to audit) and writes no SARIF file. Guarded the upload on hashFiles('apm-audit.sarif') != ''. The apm audit --ci policy gate itself still runs and passes -- the moment a real dependency is added it will start producing both the policy verdict and a SARIF report automatically.

2. F3 (retire plugins/external.json + merge bridge): filed microsoft/apm#1061 as the upstream enabler. Two small APM changes are needed before external.json can collapse into apm.yml natively: extend marketplace.packages[] pass-through to include author/keywords/license/repository, and let maintainer-supplied description/version override the remote-fetch fallback (which doesn't fire for third-party repos that don't ship an apm.yml). Once that lands, this repo can delete the bridge in a follow-up PR.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copy link
Copy Markdown
Contributor

@aaronpowell aaronpowell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial questions/observations:

  • The source for each plugin shouldn't contain ./plugins because that is defined in the metadata.pluginRoot property as a root path, so tools would think they are at ./plugins/plugins/...
  • Do we still need the plugin.json for each plugin? Aren't we just defining all of that in the apm.yml?
  • Similarly to the last point, shouldn't the file for external plugins be ditched and it just be part of the apm.yml definition?

danielmeppiel and others added 2 commits April 30, 2026 08:59
apm audit --ci catches missing lockfile entries and ref mismatches
between apm.yml and apm.lock.yaml, but it does NOT catch the case
where apm-action's apm install step regenerates a different lockfile
than the one committed (contributor edited apm.yml without running
apm install, or an upstream ref moved).

Mirror the pattern apm-cli's own self-check uses: after the install
step, git status --porcelain apm.lock.yaml and fail on drift with a
clear remediation hint. For marketplace-only manifests with no
dependencies: block this is a no-op (no lockfile generated, no drift
possible) -- the gate activates automatically the moment a real
dependency is added.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Contributor Author

danielmeppiel commented Apr 30, 2026

@aaronpowell -- on findings 1 and 3 of your review, filed microsoft/apm#1061 (expanded from a previous narrower issue) as a single umbrella covering both:

  • Finding 1 (source double-prefix) -- root-caused to apm pack emitting local sources verbatim without subtracting metadata.pluginRoot. Proposed a 4-line builder fix with acceptance tests.
  • Finding 3 (drop external.json) -- already-known pass-through gap (author/keywords/license/repository dropped on remote entries, plus maintainer-supplied description/version losing to remote fetch). Tracked alongside finding 1.

Both are small, additive, no-op-when-unset changes on the same code path. Once they ship upstream, this PR collapses into:

  • eng/merge-external-plugins.mjs -> deleted
  • the post-process pluginRoot strip -> deleted
  • plugins/external.json -> moves into apm.yml

Net result: a single apm.yml with no Node bridge.

Pragmatic for this PR: pushing the 5-line pluginRoot strip in eng/merge-external-plugins.mjs now so output is byte-equivalent to staged and the PR is mergeable. The strip is documented inline as deferred-removal behind microsoft/apm#1061 (mirroring how the external-merge logic is already documented as F3-deferred).

Finding 2 (plugin.json per plugin) stays as F1 follow-up -- truly redundant only when a per-plugin apm.yml exists and apm pack generates plugin.json from it (otherwise we lose runtime declarations: agents/skills/commands/hooks). That's a 64-plugin migration plus another upstream feature; deserves its own PR after the marketplace work lands.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: adopt APM (microsoft/apm) as the canonical authoring substrate for marketplace.json

3 participants