Skip to content

External plugin canvas check requires flat extensions/extension.mjs, rejecting valid nested extensions/<name>/extension.mjs layout #2402

Description

@tlmii

Summary

The external-plugin canvas-structure check requires the canvas extension entry point to be a flat file at <pluginRoot>/extensions/extension.mjs. But the documented and widely-used convention nests each extension in its own subfolder — <pluginRoot>/extensions/<extension-name>/extension.mjs. Plugins that follow the nested convention (which install and run correctly) are reported as failing with a false "missing required canvas extension entry point," forcing unnecessary maintainer follow-up.

Observed on PR #2394 (upgrade-agent):

- <sha>: missing required canvas extension entry point "plugins/upgrade-agent/extensions/extension.mjs".

...even though the install smoke test in the same run passed, confirming the extension installs and loads fine.

Where the check lives

The assumption is hardcoded in two places and both need the same change to stay consistent:

  • Quality gate: runCanvasStructureGate in eng/external-plugin-quality-gates.mjs — line ~542:

    const extensionsDir       = toPosixPath(normalizedPluginPath, "extensions");
    const extensionEntryPoint = toPosixPath(extensionsDir, "extension.mjs"); // flat: extensions/extension.mjs

    It confirms extensions/ is a tree, then requires a bare extensions/extension.mjs — it never scans subfolders.

  • Intake: eng/external-plugin-intake.mjs — line ~495:

    const extensionEntryPath = joinRepoPath(pluginRoot, "extensions", "extension.mjs");
    // error: `plugins tagged with "canvas" must include "extensions/extension.mjs"`

Why the flat assumption is wrong

The nested layout is the endorsed convention, not a deviation:

  • AGENTS.md (Canvas Extensions): each extension folder (extensions/<id>/) must include extension.mjs — i.e. extensions/<id>/extension.mjs.
  • This repo's own discovery helpers already accept nested entry points: eng/generate-marketplace.mjs (~L66-69) and eng/generate-website-data.mjs (~L40-42) both probe extensions/<name>/extension.mjs (and extensions/extension.mjs).
  • Real submission microsoft/upgrade-agent-plugins@a70e1ae1ff63dd19ff874e874b4b587a5291f68a lays out:
    plugins/upgrade-agent/
    ├── plugin.json                       # keywords include "canvas"
    └── extensions/
        └── modernize-dashboard/
            ├── extension.mjs             # ← real entry point
            ├── package.json, README.md, canvas/, lib/
    
  • The install smoke test passes for this plugin — the CLI installs and loads the extension from its nested path. Only the structural check disagrees.

Suggested fix

Locate the canvas entry point by scanning <pluginRoot>/extensions/ for at least one immediate subfolder that contains an extension.mjs file (nested convention), while continuing to accept the flat extensions/extension.mjs form for backward compatibility. Apply the equivalent change to both the quality gate (git-object based lookups: git cat-file/git ls-tree over <locator>:<path>) and the intake (GitHub Contents API traversal). Keep the two checks behaviorally aligned.

Acceptance criteria

  • A canvas-tagged plugin whose extension lives at extensions/<name>/extension.mjs passes both the intake canvas check and the runCanvasStructureGate quality gate.
  • The flat extensions/extension.mjs layout continues to pass.
  • A canvas-tagged plugin with an extensions/ directory that contains no extension.mjs anywhere (neither flat nor in a subfolder) still fails.
  • Regression coverage added in eng/external-plugin-quality-gates.test.mjs (and, if practical, for the intake path) covering the nested-subfolder layout.

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions