From cf04ddde790008b3cf01dcdbb1f7213cd6e55a71 Mon Sep 17 00:00:00 2001 From: qinezh Date: Fri, 17 Jul 2026 12:22:35 +0800 Subject: [PATCH 1/5] Update foundry-agent-canvas version to 1.0.1 (#2335) --- .github/plugin/marketplace.json | 4 ++-- plugins/external.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 398505e0..ff43fa63 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -553,7 +553,7 @@ { "name": "foundry-agent-canvas", "description": "Interactive Copilot canvas for designing, configuring, testing, and deploying Microsoft Foundry hosted agents.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Microsoft", "url": "https://www.microsoft.com" @@ -574,7 +574,7 @@ "source": "github", "repo": "microsoft/foundry-toolkit", "path": "foundry-agent-canvas", - "sha": "0d311e56508ac200ebb86586adbee9c7b8b0601f" + "sha": "e16be2b8533ca82c22806388e07581e7497785d7" } }, { diff --git a/plugins/external.json b/plugins/external.json index f69a91b1..19ec538a 100644 --- a/plugins/external.json +++ b/plugins/external.json @@ -339,7 +339,7 @@ { "name": "foundry-agent-canvas", "description": "Interactive Copilot canvas for designing, configuring, testing, and deploying Microsoft Foundry hosted agents.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Microsoft", "url": "https://www.microsoft.com" @@ -360,7 +360,7 @@ "source": "github", "repo": "microsoft/foundry-toolkit", "path": "foundry-agent-canvas", - "sha": "0d311e56508ac200ebb86586adbee9c7b8b0601f" + "sha": "e16be2b8533ca82c22806388e07581e7497785d7" } }, { From 38ab1360a496ec5bc1eae972d8f9d061a0640cfc Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Fri, 17 Jul 2026 00:39:55 -0700 Subject: [PATCH 2/5] Update modernize-dotnet plugin to 1.0.1161-preview1 (#2317) --- .github/plugin/marketplace.json | 5 +++-- plugins/external.json | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index ff43fa63..03689693 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -770,7 +770,7 @@ { "name": "modernize-dotnet", "description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.", - "version": "1.0.1157-preview1", + "version": "1.0.1161-preview1", "author": { "name": "Microsoft", "url": "https://www.microsoft.com" @@ -787,7 +787,8 @@ "source": { "source": "github", "repo": "dotnet/modernize-dotnet", - "path": "plugins/modernize-dotnet" + "path": "plugins/modernize-dotnet", + "sha": "ce4ec09678498da38099ca9169c1624c59c2a89a" } }, { diff --git a/plugins/external.json b/plugins/external.json index 19ec538a..90b147ba 100644 --- a/plugins/external.json +++ b/plugins/external.json @@ -502,7 +502,7 @@ { "name": "modernize-dotnet", "description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.", - "version": "1.0.1157-preview1", + "version": "1.0.1161-preview1", "author": { "name": "Microsoft", "url": "https://www.microsoft.com" @@ -519,7 +519,8 @@ "source": { "source": "github", "repo": "dotnet/modernize-dotnet", - "path": "plugins/modernize-dotnet" + "path": "plugins/modernize-dotnet", + "sha": "ce4ec09678498da38099ca9169c1624c59c2a89a" } }, { From 40665c23b79b8c141f9bd46ac5241b8fecd132f3 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 17 Jul 2026 18:04:03 +1000 Subject: [PATCH 3/5] Migrate extension plugin materialization to extensions container (#2334) * Migrate extension plugin materialization layout Materialize extension plugins into a dedicated extensions/ container, validate the new manifest convention, and bump extension plugin versions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Keep extension manifests source-compatible Restore source extension manifests to "extensions": "." while preserving materialization-time rewrite to "extensions" in distribution output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Validate canvas extension layout for external submissions Add intake and quality-gate checks for canvas-tagged external plugins so they must include extensions/extension.mjs and optional manifest extensions is validated when present. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Fix plugin clean extension pass and typo guard text Declare EXTENSIONS_DIR in clean-materialized-plugins and run extension cleanup once after plugin cleanup. Also normalize misspelled-key detection strings to satisfy spelling checks without changing validation behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Proper codespell fix * Separate canvas structure quality gate status Track canvas structure as its own gate status and output, include it in aggregate summaries, and enforce Git object types so extensions/ is a tree and extensions/extension.mjs is a blob. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .codespellrc | 6 +- .github/plugin/marketplace.json | 36 ++-- .../external-plugin-command-router.yml | 2 + .github/workflows/external-plugin-intake.yml | 2 + .../external-plugin-pr-quality-gates.yml | 15 +- AGENTS.md | 2 +- eng/clean-materialized-plugins.mjs | 44 +++++ eng/external-plugin-intake.mjs | 68 +++++++ eng/external-plugin-pr-quality-gates.mjs | 2 +- eng/external-plugin-quality-gates.mjs | 167 +++++++++++++++++- eng/external-plugin-quality-gates.test.mjs | 101 +++++++++++ eng/materialize-plugins.mjs | 94 +++++++++- eng/materialize-plugins.test.mjs | 48 +++++ eng/validate-plugins.mjs | 9 +- .../.github/plugin/plugin.json | 2 +- .../apng-studio/.github/plugin/plugin.json | 2 +- .../arcade-canvas/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../color-orb/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../diagram-viewer/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../gesture-review/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../site-studio/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../token-pacman/.github/plugin/plugin.json | 2 +- .../where-was-i/.github/plugin/plugin.json | 2 +- .../work-hub/.github/plugin/plugin.json | 2 +- 32 files changed, 583 insertions(+), 49 deletions(-) create mode 100644 eng/external-plugin-quality-gates.test.mjs create mode 100644 eng/materialize-plugins.test.mjs diff --git a/.codespellrc b/.codespellrc index 1a42b532..d38ea35d 100644 --- a/.codespellrc +++ b/.codespellrc @@ -45,7 +45,9 @@ # Wee, Sherif - proper name (Wee, Sherif, contributor names should not be flagged as typos) # queston - intentional misspelling example in skills/arize-dataset/SKILL.md demonstrating typo detection in field names - + +# extenions - intentional misspelled key name used in plugin validators to detect invalid manifests + # nin - MongoDB $nin operator in security instructions NoSQL injection detection regex # Vertexes - FreeCAD shape sub-elements used as property of obj.Shape @@ -60,7 +62,7 @@ # Vally/vally - Name of product -ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,Vertexes,nin,FO,CAF,Parth,ans,gud,Vally,vally +ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,extenions,Vertexes,nin,FO,CAF,Parth,ans,gud,Vally,vally # Skip certain files and directories # *.tm7 - MTM DataContract exports; embedded base64 icon blobs trigger false positives diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 03689693..991790aa 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "accessibility-kanban", "source": "extensions/accessibility-kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "acreadiness-cockpit", @@ -85,13 +85,13 @@ "name": "apng-studio", "source": "extensions/apng-studio", "description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "arcade-canvas", "source": "extensions/arcade-canvas", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "arch", @@ -158,7 +158,7 @@ "name": "backlog-swipe-triage", "source": "extensions/backlog-swipe-triage", "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "cast-imaging", @@ -195,7 +195,7 @@ "name": "chromium-control-canvas", "source": "extensions/chromium-control-canvas", "description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "clojure-interactive-programming", @@ -239,13 +239,13 @@ "name": "color-orb", "source": "extensions/color-orb", "description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "connector-namespaces", "source": "extensions/connector-namespaces", "description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.", - "version": "1.1.0" + "version": "1.1.1" }, { "name": "context-engineering", @@ -373,7 +373,7 @@ "name": "diagram-viewer", "source": "extensions/diagram-viewer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "dotnet", @@ -520,7 +520,7 @@ "name": "feedback-themes", "source": "extensions/feedback-themes", "description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "figma", @@ -593,7 +593,7 @@ "name": "gesture-review", "source": "extensions/gesture-review", "description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "gh-skills-builder", @@ -701,7 +701,7 @@ "name": "java-modernization-studio", "source": "extensions/java-modernization-studio", "description": "Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "kotlin-mcp-development", @@ -980,13 +980,13 @@ "name": "release-notes-showcase", "source": "extensions/release-notes-showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "repo-actions-hub", "source": "extensions/repo-actions-hub", "description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "roundup", @@ -1028,7 +1028,7 @@ "name": "site-studio", "source": "extensions/site-studio", "description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "skill-image-gen", @@ -1118,13 +1118,13 @@ "name": "tiny-tool-town-submitter", "source": "extensions/tiny-tool-town-submitter", "description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "token-pacman", "source": "extensions/token-pacman", "description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "typescript-mcp-development", @@ -1311,7 +1311,7 @@ "name": "where-was-i", "source": "extensions/where-was-i", "description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "winappcli", @@ -1375,7 +1375,7 @@ "name": "work-hub", "source": "extensions/work-hub", "description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.", - "version": "1.0.0" + "version": "1.0.1" } ] } diff --git a/.github/workflows/external-plugin-command-router.yml b/.github/workflows/external-plugin-command-router.yml index 1b8c317a..5769f3b5 100644 --- a/.github/workflows/external-plugin-command-router.yml +++ b/.github/workflows/external-plugin-command-router.yml @@ -753,6 +753,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.', }; @@ -764,6 +765,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow did not return results. Re-run intake to retry.', }; diff --git a/.github/workflows/external-plugin-intake.yml b/.github/workflows/external-plugin-intake.yml index c3859a31..ca7852dd 100644 --- a/.github/workflows/external-plugin-intake.yml +++ b/.github/workflows/external-plugin-intake.yml @@ -116,6 +116,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.', }; @@ -127,6 +128,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow did not return results. Re-run intake to retry.', }; diff --git a/.github/workflows/external-plugin-pr-quality-gates.yml b/.github/workflows/external-plugin-pr-quality-gates.yml index 807cb27d..3010788b 100644 --- a/.github/workflows/external-plugin-pr-quality-gates.yml +++ b/.github/workflows/external-plugin-pr-quality-gates.yml @@ -158,6 +158,7 @@ jobs: failure_class: 'infra', checked_plugins: [], version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.', }; } else if (shouldRun) { @@ -167,6 +168,7 @@ jobs: failure_class: 'infra', checked_plugins: [], version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.', }; } else if (process.env.QUALITY_RESULT_JSON) { @@ -177,6 +179,7 @@ jobs: failure_class: 'infra', checked_plugins: [], version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', summary: 'External plugin PR quality checks did not return a result payload.', }; } @@ -245,15 +248,16 @@ jobs: const sourceUrl = String(entry?.source_tree_url || ''); const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository'); const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator; - return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`; + return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.canvas_structure_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`; }) - : ['| _none_ | not_run | not_run | not_run | not_run | _n/a_ |']; + : ['| _none_ | not_run | not_run | not_run | not_run | not_run | _n/a_ |']; const failureDetails = checkedPlugins.flatMap((entry) => { const name = String(entry?.name || 'unknown'); const quality = entry?.quality || {}; const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0; const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0; const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0; + const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0; const details = []; if (shouldShowVally) { @@ -265,6 +269,9 @@ jobs: if (shouldShowVersionMatch) { details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output)); } + if (shouldShowCanvasStructure) { + details.push(formatGateOutput(name, 'canvas structure', quality.canvas_structure_status, quality.canvas_structure_output)); + } return details; }); @@ -277,8 +284,8 @@ jobs: '', '### Per-plugin quality summary', '', - '| Plugin | vally lint | install smoke test | version match | overall | source tree |', - '|---|---|---|---|---|---|', + '| Plugin | vally lint | install smoke test | version match | canvas structure | overall | source tree |', + '|---|---|---|---|---|---|---|', ...rows, '', ...(failureDetails.length > 0 diff --git a/AGENTS.md b/AGENTS.md index 7eedca71..3a17693c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,7 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must - Extension `plugin.json` **must** follow the convention: - `name`, `description`, `version` are required - `logo` **must** be exactly `"assets/preview.png"` (enforced convention) - - `extensions` **must** be exactly `"."` (per [copilot-agent-runtime#9929](https://github.com/github/copilot-agent-runtime/pull/9929)) + - `extensions` **must** be exactly `"."` in source manifests (materialization rewrites this to `"extensions"` for distribution output) - Optional: `author`, `keywords` fields - **Must not** include `x-awesome-copilot` field (use convention-based `assets/preview.png` only) - Each extension must have `assets/preview.png` as the primary visual asset diff --git a/eng/clean-materialized-plugins.mjs b/eng/clean-materialized-plugins.mjs index 9379d78a..2fcbacc2 100644 --- a/eng/clean-materialized-plugins.mjs +++ b/eng/clean-materialized-plugins.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from "url"; import { ROOT_FOLDER } from "./constants.mjs"; const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins"); +const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions"); const MATERIALIZED_SPECS = { agents: { path: "agents", @@ -89,6 +90,30 @@ function cleanPlugin(pluginPath) { return { removed, manifestUpdated }; } +function cleanMaterializedExtensionPlugin(extensionPath) { + const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); + let manifestUpdated = false; + if (fs.existsSync(pluginJsonPath)) { + const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + if (plugin.extensions === "extensions") { + plugin.extensions = "."; + fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8"); + manifestUpdated = true; + console.log(` Updated ${path.basename(extensionPath)}/.github/plugin/plugin.json`); + } + } + + const target = path.join(extensionPath, "extensions"); + if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) { + return { removed: 0, manifestUpdated }; + } + + const count = countFiles(target); + fs.rmSync(target, { recursive: true, force: true }); + console.log(` Removed ${path.basename(extensionPath)}/extensions/ (${count} files)`); + return { removed: count, manifestUpdated }; +} + function countFiles(dir) { let count = 0; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { @@ -171,6 +196,25 @@ function main() { } } + if (fs.existsSync(EXTENSIONS_DIR)) { + const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + for (const dirName of extensionDirs) { + const extensionPath = path.join(EXTENSIONS_DIR, dirName); + if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) { + continue; + } + const { removed, manifestUpdated } = cleanMaterializedExtensionPlugin(extensionPath); + total += removed; + if (manifestUpdated) { + manifestsUpdated++; + } + } + } + console.log(); if (total === 0 && manifestsUpdated === 0) { console.log("✅ No materialized files found. Plugins are already clean."); diff --git a/eng/external-plugin-intake.mjs b/eng/external-plugin-intake.mjs index 794e36f5..fa9696e1 100644 --- a/eng/external-plugin-intake.mjs +++ b/eng/external-plugin-intake.mjs @@ -459,6 +459,55 @@ async function validateCanvasPluginMetadata(plugin, errors, warnings, token) { ); } + if (manifest.extenions !== undefined) { + errors.push( + `submission: plugins tagged with "canvas" must use "extensions" (found misspelled key "extenions") in "${manifestPath}"`, + ); + } + + if (manifest.extensions !== undefined && manifest.extensions !== "extensions") { + errors.push( + `submission: plugins tagged with "canvas" may omit "extensions", but if provided it must be "extensions" in "${manifestPath}"`, + ); + } + + const extensionContainerPath = joinRepoPath(pluginRoot, "extensions"); + const extensionContainerResponse = await fetchGitHubFile(repo, extensionContainerPath, releaseLocator, token); + if (extensionContainerResponse.kind === "notFound") { + errors.push( + `submission: plugins tagged with "canvas" must include an "extensions" directory at ${releaseLocatorDescription}`, + ); + } else if (extensionContainerResponse.kind === "apiError") { + warnings.push( + `submission: could not verify "extensions" directory in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`, + ); + } else if ( + !( + extensionContainerResponse.data?.type === "dir" + || Array.isArray(extensionContainerResponse.data) + ) + ) { + errors.push( + `submission: "extensions" must be a directory in ${releaseLocatorDescription}`, + ); + } + + const extensionEntryPath = joinRepoPath(pluginRoot, "extensions", "extension.mjs"); + const extensionEntryResponse = await fetchGitHubFile(repo, extensionEntryPath, releaseLocator, token); + if (extensionEntryResponse.kind === "notFound") { + errors.push( + `submission: plugins tagged with "canvas" must include "extensions/extension.mjs" at ${releaseLocatorDescription}`, + ); + } else if (extensionEntryResponse.kind === "apiError") { + warnings.push( + `submission: could not verify "extensions/extension.mjs" in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`, + ); + } else if (extensionEntryResponse.data?.type !== "file") { + errors.push( + `submission: "extensions/extension.mjs" must be a file in ${releaseLocatorDescription}`, + ); + } + const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH); const previewResponse = await fetchGitHubFile(repo, previewPath, releaseLocator, token); if (previewResponse.kind === "notFound") { @@ -580,11 +629,13 @@ function normalizeQualityGateResult(rawResult) { vally_lint_status: "not_run", smoke_status: "not_run", version_match_status: "not_run", + canvas_structure_status: "not_run", failure_class: "none", summary: "", vally_lint_output: "", smoke_output: "", version_match_output: "", + canvas_structure_output: "", }; if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) { @@ -601,6 +652,7 @@ function buildQualityGatesCommentSection(qualityResult) { const vallyState = qualityResult.vally_lint_status || "not_run"; const smokeState = qualityResult.smoke_status || "not_run"; const versionMatchState = qualityResult.version_match_status || "not_run"; + const canvasStructureState = qualityResult.canvas_structure_status || "not_run"; const summaryText = String(qualityResult.summary || "").trim() || "_No quality gate details were provided._"; const sections = [ @@ -611,6 +663,7 @@ function buildQualityGatesCommentSection(qualityResult) { `| vally lint | ${vallyState} |`, `| install smoke test | ${smokeState} |`, `| version match | ${versionMatchState} |`, + `| canvas structure | ${canvasStructureState} |`, "", summaryText, ]; @@ -660,6 +713,21 @@ function buildQualityGatesCommentSection(qualityResult) { ); } + const canvasStructureOutput = String(qualityResult.canvas_structure_output || "").trim(); + if (canvasStructureOutput) { + sections.push( + "", + "
", + "Canvas structure output", + "", + "```text", + canvasStructureOutput, + "```", + "", + "
", + ); + } + return sections.join("\n"); } diff --git a/eng/external-plugin-pr-quality-gates.mjs b/eng/external-plugin-pr-quality-gates.mjs index 5cbf4c9b..85cd6d9c 100644 --- a/eng/external-plugin-pr-quality-gates.mjs +++ b/eng/external-plugin-pr-quality-gates.mjs @@ -86,7 +86,7 @@ export async function runExternalPluginPrQualityGates(plugins) { ? "No changed external plugin entries were detected in plugins/external.json." : checkedPlugins .map((entry) => - `- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, overall=${entry.quality.overall_status}` + `- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, canvas-structure=${entry.quality.canvas_structure_status}, overall=${entry.quality.overall_status}` ) .join("\n"); diff --git a/eng/external-plugin-quality-gates.mjs b/eng/external-plugin-quality-gates.mjs index 358ecdda..507cc28f 100644 --- a/eng/external-plugin-quality-gates.mjs +++ b/eng/external-plugin-quality-gates.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "child_process"; import { runLint, LintConsoleReporter } from "@microsoft/vally"; const MAX_OUTPUT_LENGTH = 12000; +const EXTERNAL_CANVAS_KEYWORD = "canvas"; const INFRA_ERROR_PATTERNS = [ /\b401\b/, @@ -69,6 +70,12 @@ function normalizePluginPath(pluginPath) { return normalized; } +function hasCanvasKeyword(plugin) { + return (plugin?.keywords ?? []).some( + (keyword) => String(keyword).trim().toLowerCase() === EXTERNAL_CANVAS_KEYWORD, + ); +} + function resolveFetchSpec(pluginSource) { if (pluginSource.sha) { return pluginSource.sha; @@ -467,6 +474,148 @@ function runVersionMatchGate(repoDir, plugin, primaryFetchSpec) { }; } +function checkPathExistsAtLocator(repoDir, locator, repoPath, expectedType) { + const result = runCommand("git", ["cat-file", "-e", `${locator}:${repoPath}`], { cwd: repoDir }); + if (result.exitCode === 0) { + if (!expectedType) { + return { exists: true, output: "" }; + } + + const typeResult = runCommand("git", ["cat-file", "-t", `${locator}:${repoPath}`], { cwd: repoDir }); + if (typeResult.exitCode !== 0) { + return { + exists: false, + output: `Unable to verify path "${repoPath}" type at "${locator}": ${typeResult.output}`, + }; + } + + const actualType = String(typeResult.stdout ?? "").trim(); + if (actualType !== expectedType) { + return { + exists: false, + output: "", + kindMismatch: true, + actualType, + }; + } + + return { exists: true, output: "" }; + } + + const normalizedOutput = String(result.output ?? "").toLowerCase(); + if ( + normalizedOutput.includes("not a valid object name") + || normalizedOutput.includes("path '") + || normalizedOutput.includes("does not exist") + ) { + return { exists: false, output: "" }; + } + + return { + exists: false, + output: `Unable to verify path "${repoPath}" at "${locator}": ${result.output}`, + }; +} + +export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) { + if (!hasCanvasKeyword(plugin)) { + return { + status: "not_run", + output: "Canvas structure gate skipped because plugin is not tagged with \"canvas\".", + }; + } + + const normalizedPluginPath = normalizePluginPath(plugin?.source?.path || "/"); + const locators = [plugin?.source?.ref, plugin?.source?.sha] + .filter((value) => typeof value === "string" && value.trim().length > 0) + .map((value) => value.trim()) + .filter((value, index, values) => values.indexOf(value) === index); + + if (locators.length === 0) { + return { + status: "not_run", + output: "Canvas structure gate skipped because neither source.ref nor source.sha was provided.", + }; + } + + const extensionsDir = toPosixPath(normalizedPluginPath, "extensions"); + const extensionEntryPoint = toPosixPath(extensionsDir, "extension.mjs"); + + let hasFailure = false; + let hasInfraError = false; + const messages = []; + + for (const locator of locators) { + if (locator !== primaryFetchSpec) { + const fetchResult = fetchLocatorIntoRepo(repoDir, locator); + if (fetchResult.status === "fail") { + hasFailure = true; + messages.push(`- ${locator}: ${fetchResult.output}`); + continue; + } + + if (fetchResult.status === "infra_error") { + hasInfraError = true; + messages.push(`- ${locator}: ${fetchResult.output}`); + continue; + } + } + + const extensionDirCheck = checkPathExistsAtLocator(repoDir, locator, extensionsDir, "tree"); + if (extensionDirCheck.output) { + hasInfraError = true; + messages.push(`- ${locator}: ${extensionDirCheck.output}`); + continue; + } + if (!extensionDirCheck.exists) { + hasFailure = true; + if (extensionDirCheck.kindMismatch) { + messages.push(`- ${locator}: "${extensionsDir}" must be a directory.`); + } else { + messages.push(`- ${locator}: missing required canvas extension directory "${extensionsDir}".`); + } + continue; + } + + const extensionEntryCheck = checkPathExistsAtLocator(repoDir, locator, extensionEntryPoint, "blob"); + if (extensionEntryCheck.output) { + hasInfraError = true; + messages.push(`- ${locator}: ${extensionEntryCheck.output}`); + continue; + } + if (!extensionEntryCheck.exists) { + hasFailure = true; + if (extensionEntryCheck.kindMismatch) { + messages.push(`- ${locator}: "${extensionEntryPoint}" must be a file.`); + } else { + messages.push(`- ${locator}: missing required canvas extension entry point "${extensionEntryPoint}".`); + } + continue; + } + + messages.push(`- ${locator}: found "${extensionsDir}" with entry point "${extensionEntryPoint}".`); + } + + if (hasInfraError) { + return { + status: "infra_error", + output: messages.join("\n"), + }; + } + + if (hasFailure) { + return { + status: "fail", + output: messages.join("\n"), + }; + } + + return { + status: "pass", + output: messages.join("\n"), + }; +} + function toOverallStatus(states) { if (states.includes("infra_error")) { return "infra_error"; @@ -497,11 +646,13 @@ export async function runExternalPluginQualityGates(plugin) { vally_lint_status: "not_run", smoke_status: "not_run", version_match_status: "not_run", + canvas_structure_status: "not_run", failure_class: "none", summary: "", vally_lint_output: "", smoke_output: "", version_match_output: "", + canvas_structure_output: "", }; try { @@ -513,10 +664,14 @@ export async function runExternalPluginQualityGates(plugin) { result.vally_lint_status = "fail"; result.smoke_status = "fail"; result.version_match_status = "fail"; + result.canvas_structure_status = hasCanvasKeyword(plugin) ? "fail" : "not_run"; result.overall_status = "fail"; result.failure_class = "submitter_fixes"; result.summary = `Plugin path "${plugin.source?.path || "/"}" was not found in the submitted repository snapshot.`; result.version_match_output = result.summary; + if (hasCanvasKeyword(plugin)) { + result.canvas_structure_output = result.summary; + } return result; } @@ -524,6 +679,10 @@ export async function runExternalPluginQualityGates(plugin) { result.version_match_status = versionMatchResult.status; result.version_match_output = versionMatchResult.output; + const canvasStructureResult = runCanvasStructureGate(repoDir, plugin, fetchSpec); + result.canvas_structure_status = canvasStructureResult.status; + result.canvas_structure_output = canvasStructureResult.output; + const vallyResult = await runVallyLintGate(pluginRoot); result.vally_lint_status = vallyResult.status; result.vally_lint_output = vallyResult.output; @@ -532,12 +691,18 @@ export async function runExternalPluginQualityGates(plugin) { result.smoke_status = smokeResult.status; result.smoke_output = smokeResult.output; - result.overall_status = toOverallStatus([result.vally_lint_status, result.smoke_status, result.version_match_status]); + result.overall_status = toOverallStatus([ + result.vally_lint_status, + result.smoke_status, + result.version_match_status, + result.canvas_structure_status, + ]); result.failure_class = toFailureClass(result.overall_status); result.summary = [ `- vally lint: ${result.vally_lint_status}`, `- install smoke test: ${result.smoke_status}`, `- version match: ${result.version_match_status}`, + `- canvas structure: ${result.canvas_structure_status}`, `- overall: ${result.overall_status}`, ].join("\n"); diff --git a/eng/external-plugin-quality-gates.test.mjs b/eng/external-plugin-quality-gates.test.mjs new file mode 100644 index 00000000..5f619fae --- /dev/null +++ b/eng/external-plugin-quality-gates.test.mjs @@ -0,0 +1,101 @@ +import assert from "node:assert/strict"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { spawnSync } from "child_process"; +import { after, test } from "node:test"; +import { runCanvasStructureGate } from "./external-plugin-quality-gates.mjs"; + +const tempDirs = []; + +after(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function runGit(repoDir, ...args) { + const result = spawnSync("git", args, { cwd: repoDir, encoding: "utf8" }); + if (result.status !== 0) { + throw new Error(`git ${args.join(" ")} failed: ${result.stdout}\n${result.stderr}`); + } + return String(result.stdout ?? "").trim(); +} + +function createTempRepo() { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-")); + tempDirs.push(repoDir); + + runGit(repoDir, "init", "-q"); + runGit(repoDir, "config", "user.name", "Copilot Test"); + runGit(repoDir, "config", "user.email", "copilot@example.com"); + return repoDir; +} + +function commitAll(repoDir, message) { + runGit(repoDir, "add", "-A"); + runGit(repoDir, "commit", "-m", message, "--quiet"); + return runGit(repoDir, "rev-parse", "HEAD"); +} + +test("runCanvasStructureGate passes when extensions/extension.mjs exists", () => { + const repoDir = createTempRepo(); + fs.mkdirSync(path.join(repoDir, "extensions"), { recursive: true }); + fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs"), "export default {};\n"); + const sha = commitAll(repoDir, "Add canvas extension container"); + + const plugin = { + name: "canvas-plugin", + keywords: ["canvas"], + source: { + source: "github", + repo: "owner/repo", + sha, + }, + }; + + const result = runCanvasStructureGate(repoDir, plugin, sha); + assert.equal(result.status, "pass"); + assert.match(result.output, /found "extensions"/); +}); + +test("runCanvasStructureGate fails when extension entrypoint is only at repo root", () => { + const repoDir = createTempRepo(); + fs.writeFileSync(path.join(repoDir, "extension.mjs"), "export default {};\n"); + const sha = commitAll(repoDir, "Add root extension entrypoint"); + + const plugin = { + name: "canvas-plugin", + keywords: ["canvas"], + source: { + source: "github", + repo: "owner/repo", + sha, + }, + }; + + const result = runCanvasStructureGate(repoDir, plugin, sha); + assert.equal(result.status, "fail"); + assert.match(result.output, /missing required canvas extension directory "extensions"/); +}); + +test("runCanvasStructureGate fails when extension entrypoint path is a directory", () => { + const repoDir = createTempRepo(); + fs.mkdirSync(path.join(repoDir, "extensions", "extension.mjs"), { recursive: true }); + fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs", "placeholder.txt"), "not-a-module\n"); + const sha = commitAll(repoDir, "Add invalid extension entrypoint directory"); + + const plugin = { + name: "canvas-plugin", + keywords: ["canvas"], + source: { + source: "github", + repo: "owner/repo", + sha, + }, + }; + + const result = runCanvasStructureGate(repoDir, plugin, sha); + assert.equal(result.status, "fail"); + assert.match(result.output, /"extensions\/extension\.mjs" must be a file/); +}); diff --git a/eng/materialize-plugins.mjs b/eng/materialize-plugins.mjs index 978c250f..2d91cb61 100644 --- a/eng/materialize-plugins.mjs +++ b/eng/materialize-plugins.mjs @@ -2,9 +2,11 @@ import fs from "fs"; import path from "path"; +import { fileURLToPath } from "url"; import { ROOT_FOLDER } from "./constants.mjs"; const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins"); +const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions"); /** * Recursively copy a directory. @@ -22,6 +24,17 @@ function copyDirRecursive(src, dest) { } } +function copyEntryRecursive(srcPath, destPath) { + const stats = fs.statSync(srcPath); + if (stats.isDirectory()) { + copyDirRecursive(srcPath, destPath); + return; + } + + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + fs.copyFileSync(srcPath, destPath); +} + /** * Resolve a plugin-relative path to the repo-root source file. * @@ -45,6 +58,48 @@ function resolveSource(relPath) { return null; } +export function materializeExtensionPlugin(extensionPath) { + const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); + if (!fs.existsSync(pluginJsonPath)) { + return { copiedEntries: 0, manifestUpdated: false, skipped: true }; + } + + let metadata; + try { + metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + } catch (err) { + throw new Error(`Failed to parse ${pluginJsonPath}: ${err.message}`); + } + + const extensionContainerPath = path.join(extensionPath, "extensions"); + fs.rmSync(extensionContainerPath, { recursive: true, force: true }); + fs.mkdirSync(extensionContainerPath, { recursive: true }); + + let copiedEntries = 0; + for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) { + if (entry.name === ".github" || entry.name === "extensions") { + continue; + } + + copyEntryRecursive( + path.join(extensionPath, entry.name), + path.join(extensionContainerPath, entry.name) + ); + copiedEntries++; + } + + let manifestUpdated = false; + if (metadata.extensions !== "extensions") { + metadata.extensions = "extensions"; + manifestUpdated = true; + } + if (manifestUpdated) { + fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8"); + } + + return { copiedEntries, manifestUpdated, skipped: false }; +} + function materializePlugins() { console.log("Materializing plugin files...\n"); @@ -61,6 +116,8 @@ function materializePlugins() { let totalAgents = 0; let totalSkills = 0; let totalExtensions = 0; + let totalExtensionPlugins = 0; + let totalExtensionPluginEntries = 0; let warnings = 0; let errors = 0; @@ -185,7 +242,36 @@ function materializePlugins() { } } - console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`); + if (fs.existsSync(EXTENSIONS_DIR)) { + const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + for (const dirName of extensionDirs) { + const extensionPath = path.join(EXTENSIONS_DIR, dirName); + if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) { + continue; + } + + try { + const result = materializeExtensionPlugin(extensionPath); + if (result.skipped) { + continue; + } + + totalExtensionPlugins++; + totalExtensionPluginEntries += result.copiedEntries; + console.log(`✓ ${dirName}: materialized extension bundle into ./extensions (${result.copiedEntries} entries)`); + } catch (err) { + console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`); + errors++; + } + } + } + + console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} plugin extension refs.`); + console.log(`Materialized ${totalExtensionPlugins} extension plugins (${totalExtensionPluginEntries} top-level entries).`); if (warnings > 0) { console.log(`${warnings} warning(s).`); } @@ -195,4 +281,8 @@ function materializePlugins() { } } -materializePlugins(); +export { materializePlugins }; + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + materializePlugins(); +} diff --git a/eng/materialize-plugins.test.mjs b/eng/materialize-plugins.test.mjs new file mode 100644 index 00000000..7f42981c --- /dev/null +++ b/eng/materialize-plugins.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { after, test } from "node:test"; +import { materializeExtensionPlugin } from "./materialize-plugins.mjs"; + +const tempDirs = []; + +after(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test("materializeExtensionPlugin writes extension bundles to ./extensions and rewrites manifest", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "materialize-extension-plugin-")); + tempDirs.push(tempDir); + + const pluginDir = path.join(tempDir, "extension-plugin"); + fs.mkdirSync(path.join(pluginDir, ".github", "plugin"), { recursive: true }); + fs.mkdirSync(path.join(pluginDir, "assets"), { recursive: true }); + fs.writeFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), JSON.stringify({ + name: "test-extension-plugin", + description: "test plugin", + version: "1.0.0", + logo: "assets/preview.png", + extensions: ".", + }, null, 2)); + fs.writeFileSync(path.join(pluginDir, "extension.mjs"), "export default {};\n"); + fs.writeFileSync(path.join(pluginDir, "README.md"), "# test\n"); + fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes"); + + const result = materializeExtensionPlugin(pluginDir); + + assert.equal(result.skipped, false); + assert.equal(result.manifestUpdated, true); + assert.equal(result.copiedEntries, 3); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "extension.mjs")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "assets", "preview.png")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "README.md")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", ".github")), false); + + const pluginManifest = JSON.parse( + fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8") + ); + assert.equal(pluginManifest.extensions, "extensions"); +}); diff --git a/eng/validate-plugins.mjs b/eng/validate-plugins.mjs index fc8003b3..7161df06 100755 --- a/eng/validate-plugins.mjs +++ b/eng/validate-plugins.mjs @@ -305,9 +305,14 @@ function validateExtensionManifest(folderName) { errors.push("x-awesome-copilot field must not be present (use convention-based logo instead)"); } - // Extension convention: extensions field must be "." + if (parsed.extenions !== undefined) { + errors.push('use "extensions" field (found misspelled key "extenions")'); + } + + // Extension convention: source manifests keep extensions at repository root. + // Materialization rewrites this to "extensions" on distribution branches. if (parsed.extensions !== ".") { - errors.push('extensions field must be exactly "." (extension convention)'); + errors.push('extensions field must be exactly "." in source manifests (extension convention)'); } return { errors, plugin: parsedPlugin }; diff --git a/extensions/accessibility-kanban/.github/plugin/plugin.json b/extensions/accessibility-kanban/.github/plugin/plugin.json index 824a71c5..03903a27 100644 --- a/extensions/accessibility-kanban/.github/plugin/plugin.json +++ b/extensions/accessibility-kanban/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "accessibility-kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/apng-studio/.github/plugin/plugin.json b/extensions/apng-studio/.github/plugin/plugin.json index 6c46943c..fcd5bfc8 100644 --- a/extensions/apng-studio/.github/plugin/plugin.json +++ b/extensions/apng-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "apng-studio", "description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Andrea Griffiths", "url": "https://github.com/AndreaGriffiths11" diff --git a/extensions/arcade-canvas/.github/plugin/plugin.json b/extensions/arcade-canvas/.github/plugin/plugin.json index 8bf1d4cb..975971d8 100644 --- a/extensions/arcade-canvas/.github/plugin/plugin.json +++ b/extensions/arcade-canvas/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "arcade-canvas", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Dan Wahlin", "url": "https://github.com/DanWahlin" diff --git a/extensions/backlog-swipe-triage/.github/plugin/plugin.json b/extensions/backlog-swipe-triage/.github/plugin/plugin.json index d45e7e39..6dda88bd 100644 --- a/extensions/backlog-swipe-triage/.github/plugin/plugin.json +++ b/extensions/backlog-swipe-triage/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "backlog-swipe-triage", "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/chromium-control-canvas/.github/plugin/plugin.json b/extensions/chromium-control-canvas/.github/plugin/plugin.json index c8a2fa72..8c4d631d 100644 --- a/extensions/chromium-control-canvas/.github/plugin/plugin.json +++ b/extensions/chromium-control-canvas/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "chromium-control-canvas", "description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Andrea Griffiths", "url": "https://github.com/AndreaGriffiths11" diff --git a/extensions/color-orb/.github/plugin/plugin.json b/extensions/color-orb/.github/plugin/plugin.json index c162c294..3b564245 100644 --- a/extensions/color-orb/.github/plugin/plugin.json +++ b/extensions/color-orb/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "color-orb", "description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/connector-namespaces/.github/plugin/plugin.json b/extensions/connector-namespaces/.github/plugin/plugin.json index 18b8c116..397859c5 100644 --- a/extensions/connector-namespaces/.github/plugin/plugin.json +++ b/extensions/connector-namespaces/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "connector-namespaces", "description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.", - "version": "1.1.0", + "version": "1.1.1", "author": { "name": "Alex Yang", "url": "https://github.com/alexyaang" diff --git a/extensions/diagram-viewer/.github/plugin/plugin.json b/extensions/diagram-viewer/.github/plugin/plugin.json index 0ad6af5d..9e2b6ef2 100644 --- a/extensions/diagram-viewer/.github/plugin/plugin.json +++ b/extensions/diagram-viewer/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-viewer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/feedback-themes/.github/plugin/plugin.json b/extensions/feedback-themes/.github/plugin/plugin.json index 3bbdbc96..09435104 100644 --- a/extensions/feedback-themes/.github/plugin/plugin.json +++ b/extensions/feedback-themes/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "feedback-themes", "description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/gesture-review/.github/plugin/plugin.json b/extensions/gesture-review/.github/plugin/plugin.json index 648d069b..1ba5cf3b 100644 --- a/extensions/gesture-review/.github/plugin/plugin.json +++ b/extensions/gesture-review/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gesture-review", "description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/java-modernization-studio/.github/plugin/plugin.json b/extensions/java-modernization-studio/.github/plugin/plugin.json index 1f71b764..c3f22658 100644 --- a/extensions/java-modernization-studio/.github/plugin/plugin.json +++ b/extensions/java-modernization-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "java-modernization-studio", "description": "Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Ayan Gupta", "url": "https://github.com/ayangupt" diff --git a/extensions/release-notes-showcase/.github/plugin/plugin.json b/extensions/release-notes-showcase/.github/plugin/plugin.json index f87e6d7b..dcc4ba18 100644 --- a/extensions/release-notes-showcase/.github/plugin/plugin.json +++ b/extensions/release-notes-showcase/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "release-notes-showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Kayla Cinnamon", "url": "https://github.com/cinnamon-msft" diff --git a/extensions/repo-actions-hub/.github/plugin/plugin.json b/extensions/repo-actions-hub/.github/plugin/plugin.json index 24221451..bd317192 100644 --- a/extensions/repo-actions-hub/.github/plugin/plugin.json +++ b/extensions/repo-actions-hub/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "repo-actions-hub", "description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/site-studio/.github/plugin/plugin.json b/extensions/site-studio/.github/plugin/plugin.json index f2c294af..2c1605b6 100644 --- a/extensions/site-studio/.github/plugin/plugin.json +++ b/extensions/site-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "site-studio", "description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Ayan Gupta", "url": "https://github.com/ayangupt" diff --git a/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json b/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json index bdda3a08..70b8c98a 100644 --- a/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json +++ b/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "tiny-tool-town-submitter", "description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/token-pacman/.github/plugin/plugin.json b/extensions/token-pacman/.github/plugin/plugin.json index 1bdc62c8..b7868bb1 100644 --- a/extensions/token-pacman/.github/plugin/plugin.json +++ b/extensions/token-pacman/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "token-pacman", "description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/where-was-i/.github/plugin/plugin.json b/extensions/where-was-i/.github/plugin/plugin.json index ca60cd3e..e43c6f26 100644 --- a/extensions/where-was-i/.github/plugin/plugin.json +++ b/extensions/where-was-i/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "where-was-i", "description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/work-hub/.github/plugin/plugin.json b/extensions/work-hub/.github/plugin/plugin.json index 3e85d376..c3999519 100644 --- a/extensions/work-hub/.github/plugin/plugin.json +++ b/extensions/work-hub/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "work-hub", "description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" From 71df97432a4d077e2df17e163199fc27e8b8e1e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:28:42 +0000 Subject: [PATCH 4/5] Add external plugin upgrade-agent (#2338) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/plugin/marketplace.json | 24 ++++++++++++++++++++++++ plugins/external.json | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 991790aa..37acb1e9 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -1247,6 +1247,30 @@ "sha": "80f2d93287054f9d30dd990e842e15bcfca581c9" } }, + { + "name": "upgrade-agent", + "description": "GitHub Copilot upgrade is an AI-powered agent that helps you upgrade applications to newer versions of languages, frameworks, and runtimes. It assesses your application, creates an upgrade plan, applies code changes, and validates the results through an interactive upgrade workflow.", + "version": "1.1.222", + "author": { + "name": "Microsoft", + "url": "https://www.microsoft.com" + }, + "repository": "https://github.com/microsoft/upgrade-agent-plugins", + "license": "MIT", + "keywords": [ + "modernization", + "upgrade", + "migration", + "dotnet", + "canvas" + ], + "source": { + "source": "github", + "repo": "microsoft/upgrade-agent-plugins", + "path": "plugins/upgrade-agent", + "sha": "379d344e42823b25223f878c002f38fb3a2c1d2b" + } + }, { "name": "vercel-plugin", "description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.", diff --git a/plugins/external.json b/plugins/external.json index 90b147ba..053c9779 100644 --- a/plugins/external.json +++ b/plugins/external.json @@ -733,6 +733,30 @@ "sha": "80f2d93287054f9d30dd990e842e15bcfca581c9" } }, + { + "name": "upgrade-agent", + "description": "GitHub Copilot upgrade is an AI-powered agent that helps you upgrade applications to newer versions of languages, frameworks, and runtimes. It assesses your application, creates an upgrade plan, applies code changes, and validates the results through an interactive upgrade workflow.", + "version": "1.1.222", + "author": { + "name": "Microsoft", + "url": "https://www.microsoft.com" + }, + "repository": "https://github.com/microsoft/upgrade-agent-plugins", + "license": "MIT", + "keywords": [ + "modernization", + "upgrade", + "migration", + "dotnet", + "canvas" + ], + "source": { + "source": "github", + "repo": "microsoft/upgrade-agent-plugins", + "path": "plugins/upgrade-agent", + "sha": "379d344e42823b25223f878c002f38fb3a2c1d2b" + } + }, { "name": "vercel-plugin", "description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.", From 26fe2d126bf79aafb38f43344d450b69632200f8 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Sat, 18 Jul 2026 10:49:27 +1000 Subject: [PATCH 5/5] Fix extension materialization to move bundles into container (#2339) * Migrate extension plugin materialization layout Materialize extension plugins into a dedicated extensions/ container, validate the new manifest convention, and bump extension plugin versions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Keep extension manifests source-compatible Restore source extension manifests to "extensions": "." while preserving materialization-time rewrite to "extensions" in distribution output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Validate canvas extension layout for external submissions Add intake and quality-gate checks for canvas-tagged external plugins so they must include extensions/extension.mjs and optional manifest extensions is validated when present. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Fix plugin clean extension pass and typo guard text Declare EXTENSIONS_DIR in clean-materialized-plugins and run extension cleanup once after plugin cleanup. Also normalize misspelled-key detection strings to satisfy spelling checks without changing validation behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Proper codespell fix * Separate canvas structure quality gate status Track canvas structure as its own gate status and output, include it in aggregate summaries, and enforce Git object types so extensions/ is a tree and extensions/extension.mjs is a blob. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Move extension bundles during materialization Change extension-plugin materialization to move root bundle entries into extensions/ instead of copying them, and assert originals are removed in test coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Nest materialized extension bundles by plugin name Materialize extension plugins into extensions//... (moved entries) so resulting paths are duplicated by design, and bump extension plugin versions to the next patch release. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Fix extension materialization publish and cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Preserve root extension logo asset Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 --- .github/plugin/marketplace.json | 36 ++++----- eng/clean-materialized-plugins.mjs | 79 ++++++++++++++++++- eng/generate-marketplace.mjs | 12 ++- eng/generate-website-data.mjs | 19 +++-- eng/materialize-plugins.mjs | 52 +++++++++--- eng/materialize-plugins.test.mjs | 51 ++++++++++-- .../.github/plugin/plugin.json | 2 +- .../apng-studio/.github/plugin/plugin.json | 2 +- .../arcade-canvas/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../color-orb/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../diagram-viewer/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../gesture-review/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../site-studio/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../token-pacman/.github/plugin/plugin.json | 2 +- .../where-was-i/.github/plugin/plugin.json | 2 +- .../work-hub/.github/plugin/plugin.json | 2 +- 24 files changed, 222 insertions(+), 63 deletions(-) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 37acb1e9..b77c5725 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "accessibility-kanban", "source": "extensions/accessibility-kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "acreadiness-cockpit", @@ -85,13 +85,13 @@ "name": "apng-studio", "source": "extensions/apng-studio", "description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "arcade-canvas", "source": "extensions/arcade-canvas", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "arch", @@ -158,7 +158,7 @@ "name": "backlog-swipe-triage", "source": "extensions/backlog-swipe-triage", "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "cast-imaging", @@ -195,7 +195,7 @@ "name": "chromium-control-canvas", "source": "extensions/chromium-control-canvas", "description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "clojure-interactive-programming", @@ -239,13 +239,13 @@ "name": "color-orb", "source": "extensions/color-orb", "description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "connector-namespaces", "source": "extensions/connector-namespaces", "description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.", - "version": "1.1.1" + "version": "1.1.2" }, { "name": "context-engineering", @@ -373,7 +373,7 @@ "name": "diagram-viewer", "source": "extensions/diagram-viewer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "dotnet", @@ -520,7 +520,7 @@ "name": "feedback-themes", "source": "extensions/feedback-themes", "description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "figma", @@ -593,7 +593,7 @@ "name": "gesture-review", "source": "extensions/gesture-review", "description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "gh-skills-builder", @@ -701,7 +701,7 @@ "name": "java-modernization-studio", "source": "extensions/java-modernization-studio", "description": "Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "kotlin-mcp-development", @@ -980,13 +980,13 @@ "name": "release-notes-showcase", "source": "extensions/release-notes-showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "repo-actions-hub", "source": "extensions/repo-actions-hub", "description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "roundup", @@ -1028,7 +1028,7 @@ "name": "site-studio", "source": "extensions/site-studio", "description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "skill-image-gen", @@ -1118,13 +1118,13 @@ "name": "tiny-tool-town-submitter", "source": "extensions/tiny-tool-town-submitter", "description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "token-pacman", "source": "extensions/token-pacman", "description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "typescript-mcp-development", @@ -1335,7 +1335,7 @@ "name": "where-was-i", "source": "extensions/where-was-i", "description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", - "version": "1.0.1" + "version": "1.0.2" }, { "name": "winappcli", @@ -1399,7 +1399,7 @@ "name": "work-hub", "source": "extensions/work-hub", "description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.", - "version": "1.0.1" + "version": "1.0.2" } ] } diff --git a/eng/clean-materialized-plugins.mjs b/eng/clean-materialized-plugins.mjs index 2fcbacc2..6b034276 100644 --- a/eng/clean-materialized-plugins.mjs +++ b/eng/clean-materialized-plugins.mjs @@ -28,6 +28,41 @@ const MATERIALIZED_SPECS = { }, }; +function copyDirRecursive(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + copyDirRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +function moveEntry(srcPath, destPath) { + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + try { + fs.renameSync(srcPath, destPath); + return; + } catch (error) { + if (!["EXDEV", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error?.code)) { + throw error; + } + } + + const stats = fs.statSync(srcPath); + if (stats.isDirectory()) { + copyDirRecursive(srcPath, destPath); + fs.rmSync(srcPath, { recursive: true, force: true }); + return; + } + + fs.copyFileSync(srcPath, destPath); + fs.rmSync(srcPath, { force: true }); +} + export function restoreManifestFromMaterializedFiles(pluginPath) { const pluginJsonPath = path.join(pluginPath, ".github/plugin", "plugin.json"); if (!fs.existsSync(pluginJsonPath)) { @@ -90,15 +125,22 @@ function cleanPlugin(pluginPath) { return { removed, manifestUpdated }; } -function cleanMaterializedExtensionPlugin(extensionPath) { +export function cleanMaterializedExtensionPlugin(extensionPath) { const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); let manifestUpdated = false; if (fs.existsSync(pluginJsonPath)) { const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + const extensionBundlePrefix = `extensions/${path.basename(extensionPath)}/`; if (plugin.extensions === "extensions") { plugin.extensions = "."; - fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8"); manifestUpdated = true; + } + if (typeof plugin.logo === "string" && plugin.logo.startsWith(extensionBundlePrefix)) { + plugin.logo = plugin.logo.slice(extensionBundlePrefix.length); + manifestUpdated = true; + } + if (manifestUpdated) { + fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8"); console.log(` Updated ${path.basename(extensionPath)}/.github/plugin/plugin.json`); } } @@ -108,12 +150,43 @@ function cleanMaterializedExtensionPlugin(extensionPath) { return { removed: 0, manifestUpdated }; } + const bundleRoot = path.join(target, path.basename(extensionPath)); const count = countFiles(target); + if (fs.existsSync(bundleRoot) && fs.statSync(bundleRoot).isDirectory()) { + for (const entry of fs.readdirSync(bundleRoot, { withFileTypes: true })) { + moveEntry(path.join(bundleRoot, entry.name), path.join(extensionPath, entry.name)); + } + console.log(` Restored ${path.basename(extensionPath)}/ from materialized extensions bundle`); + } + fs.rmSync(target, { recursive: true, force: true }); console.log(` Removed ${path.basename(extensionPath)}/extensions/ (${count} files)`); return { removed: count, manifestUpdated }; } +function isExtensionPluginDirectory(extensionPath) { + if (fs.existsSync(path.join(extensionPath, "extension.mjs"))) { + return true; + } + + const bundleEntry = path.join(extensionPath, "extensions", path.basename(extensionPath), "extension.mjs"); + if (fs.existsSync(bundleEntry)) { + return true; + } + + const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); + if (!fs.existsSync(pluginJsonPath)) { + return false; + } + + try { + const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + return plugin.extensions === "extensions"; + } catch { + return false; + } +} + function countFiles(dir) { let count = 0; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { @@ -204,7 +277,7 @@ function main() { for (const dirName of extensionDirs) { const extensionPath = path.join(EXTENSIONS_DIR, dirName); - if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) { + if (!isExtensionPluginDirectory(extensionPath)) { continue; } const { removed, manifestUpdated } = cleanMaterializedExtensionPlugin(extensionPath); diff --git a/eng/generate-marketplace.mjs b/eng/generate-marketplace.mjs index 45a54b21..6c40c463 100755 --- a/eng/generate-marketplace.mjs +++ b/eng/generate-marketplace.mjs @@ -62,6 +62,16 @@ function collectLocalPluginsFromRoot(rootDir, sourcePrefix, includeEntry = () => return plugins; } +function hasExtensionEntryPoint(extensionDir, extensionName) { + const candidateEntryPoints = [ + path.join(extensionDir, "extension.mjs"), + path.join(extensionDir, "extensions", "extension.mjs"), + path.join(extensionDir, "extensions", extensionName, "extension.mjs"), + ]; + + return candidateEntryPoints.some((entryPointPath) => fs.existsSync(entryPointPath)); +} + /** * Generate marketplace.json from plugin directories */ @@ -78,7 +88,7 @@ function generateMarketplace() { ...collectLocalPluginsFromRoot( EXTENSIONS_DIR, "extensions", - (entryName) => fs.existsSync(path.join(EXTENSIONS_DIR, entryName, "extension.mjs")) + (entryName) => hasExtensionEntryPoint(path.join(EXTENSIONS_DIR, entryName), entryName) ) ]; diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs index ef052693..f6fc2082 100755 --- a/eng/generate-website-data.mjs +++ b/eng/generate-website-data.mjs @@ -35,6 +35,16 @@ const WEBSITE_SOURCE_DATA_DIR = path.join(WEBSITE_DIR, "data"); const EXTERNAL_CANVAS_KEYWORD = "canvas"; const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png"; +function hasExtensionEntryPoint(extensionDir, extensionName) { + const candidateEntryPoints = [ + path.join(extensionDir, "extension.mjs"), + path.join(extensionDir, "extensions", "extension.mjs"), + path.join(extensionDir, "extensions", extensionName, "extension.mjs"), + ]; + + return candidateEntryPoints.some((entryPointPath) => fs.existsSync(entryPointPath)); +} + /** * Ensure the output directory exists */ @@ -544,7 +554,7 @@ function generatePluginsData(gitDates, resourceIndex = {}) { const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) .filter((entry) => { if (!entry.isDirectory()) return false; - return fs.existsSync(path.join(EXTENSIONS_DIR, entry.name, "extension.mjs")); + return hasExtensionEntryPoint(path.join(EXTENSIONS_DIR, entry.name), entry.name); }) .map((entry) => entry.name) .sort((a, b) => a.localeCompare(b)); @@ -1235,12 +1245,7 @@ function generateCanvasManifest(gitDates, commitSha) { .readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) .filter((entry) => { if (!entry.isDirectory()) return false; - const extensionEntryPoint = path.join( - EXTENSIONS_DIR, - entry.name, - "extension.mjs" - ); - return fs.existsSync(extensionEntryPoint); + return hasExtensionEntryPoint(path.join(EXTENSIONS_DIR, entry.name), entry.name); }) .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/eng/materialize-plugins.mjs b/eng/materialize-plugins.mjs index 2d91cb61..905b2faf 100644 --- a/eng/materialize-plugins.mjs +++ b/eng/materialize-plugins.mjs @@ -24,15 +24,34 @@ function copyDirRecursive(src, dest) { } } -function copyEntryRecursive(srcPath, destPath) { +function moveEntry(srcPath, destPath) { + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + try { + fs.renameSync(srcPath, destPath); + return; + } catch (error) { + if (error?.code !== "EXDEV") { + throw error; + } + } + const stats = fs.statSync(srcPath); if (stats.isDirectory()) { copyDirRecursive(srcPath, destPath); + fs.rmSync(srcPath, { recursive: true, force: true }); return; } - fs.mkdirSync(path.dirname(destPath), { recursive: true }); fs.copyFileSync(srcPath, destPath); + fs.rmSync(srcPath, { force: true }); +} + +function isRelativeAssetPath(assetPath) { + return typeof assetPath === "string" && + assetPath.length > 0 && + !/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(assetPath) && + !assetPath.startsWith("data:") && + !path.isAbsolute(assetPath); } /** @@ -61,7 +80,7 @@ function resolveSource(relPath) { export function materializeExtensionPlugin(extensionPath) { const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); if (!fs.existsSync(pluginJsonPath)) { - return { copiedEntries: 0, manifestUpdated: false, skipped: true }; + return { movedEntries: 0, manifestUpdated: false, skipped: true }; } let metadata; @@ -72,20 +91,31 @@ export function materializeExtensionPlugin(extensionPath) { } const extensionContainerPath = path.join(extensionPath, "extensions"); + const extensionBundlePath = path.join(extensionContainerPath, path.basename(extensionPath)); fs.rmSync(extensionContainerPath, { recursive: true, force: true }); - fs.mkdirSync(extensionContainerPath, { recursive: true }); + fs.mkdirSync(extensionBundlePath, { recursive: true }); - let copiedEntries = 0; + let movedEntries = 0; for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) { if (entry.name === ".github" || entry.name === "extensions") { continue; } - copyEntryRecursive( + moveEntry( path.join(extensionPath, entry.name), - path.join(extensionContainerPath, entry.name) + path.join(extensionBundlePath, entry.name) ); - copiedEntries++; + movedEntries++; + } + + if (isRelativeAssetPath(metadata.logo)) { + const normalizedLogoPath = metadata.logo.replace(/\\/g, "/").replace(/^\.\//, ""); + const bundledLogoPath = path.join(extensionBundlePath, normalizedLogoPath); + if (fs.existsSync(bundledLogoPath)) { + const rootLogoPath = path.join(extensionPath, normalizedLogoPath); + fs.mkdirSync(path.dirname(rootLogoPath), { recursive: true }); + fs.copyFileSync(bundledLogoPath, rootLogoPath); + } } let manifestUpdated = false; @@ -97,7 +127,7 @@ export function materializeExtensionPlugin(extensionPath) { fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8"); } - return { copiedEntries, manifestUpdated, skipped: false }; + return { movedEntries, manifestUpdated, skipped: false }; } function materializePlugins() { @@ -261,8 +291,8 @@ function materializePlugins() { } totalExtensionPlugins++; - totalExtensionPluginEntries += result.copiedEntries; - console.log(`✓ ${dirName}: materialized extension bundle into ./extensions (${result.copiedEntries} entries)`); + totalExtensionPluginEntries += result.movedEntries; + console.log(`✓ ${dirName}: materialized extension bundle into ./extensions (${result.movedEntries} entries)`); } catch (err) { console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`); errors++; diff --git a/eng/materialize-plugins.test.mjs b/eng/materialize-plugins.test.mjs index 7f42981c..d0bce457 100644 --- a/eng/materialize-plugins.test.mjs +++ b/eng/materialize-plugins.test.mjs @@ -4,6 +4,7 @@ import os from "os"; import path from "path"; import { after, test } from "node:test"; import { materializeExtensionPlugin } from "./materialize-plugins.mjs"; +import { cleanMaterializedExtensionPlugin } from "./clean-materialized-plugins.mjs"; const tempDirs = []; @@ -13,7 +14,7 @@ after(() => { } }); -test("materializeExtensionPlugin writes extension bundles to ./extensions and rewrites manifest", () => { +test("materializeExtensionPlugin writes extension bundles to ./extensions and preserves root logo assets", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "materialize-extension-plugin-")); tempDirs.push(tempDir); @@ -32,17 +33,57 @@ test("materializeExtensionPlugin writes extension bundles to ./extensions and re fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes"); const result = materializeExtensionPlugin(pluginDir); + const bundleRoot = path.join(pluginDir, "extensions", "extension-plugin"); assert.equal(result.skipped, false); assert.equal(result.manifestUpdated, true); - assert.equal(result.copiedEntries, 3); - assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "extension.mjs")), true); - assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "assets", "preview.png")), true); - assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "README.md")), true); + assert.equal(result.movedEntries, 3); + assert.equal(fs.existsSync(path.join(bundleRoot, "extension.mjs")), true); + assert.equal(fs.existsSync(path.join(bundleRoot, "assets", "preview.png")), true); + assert.equal(fs.existsSync(path.join(bundleRoot, "README.md")), true); assert.equal(fs.existsSync(path.join(pluginDir, "extensions", ".github")), false); + assert.equal(fs.existsSync(path.join(pluginDir, "extension.mjs")), false); + assert.equal(fs.existsSync(path.join(pluginDir, "README.md")), false); + assert.equal(fs.existsSync(path.join(pluginDir, "assets", "preview.png")), true); const pluginManifest = JSON.parse( fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8") ); assert.equal(pluginManifest.extensions, "extensions"); + assert.equal(pluginManifest.logo, "assets/preview.png"); +}); + +test("cleanMaterializedExtensionPlugin restores moved extension files to root", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "clean-materialized-extension-plugin-")); + tempDirs.push(tempDir); + + const pluginDir = path.join(tempDir, "extension-plugin"); + fs.mkdirSync(path.join(pluginDir, ".github", "plugin"), { recursive: true }); + fs.mkdirSync(path.join(pluginDir, "assets"), { recursive: true }); + fs.writeFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), JSON.stringify({ + name: "test-extension-plugin", + description: "test plugin", + version: "1.0.0", + logo: "assets/preview.png", + extensions: ".", + }, null, 2)); + fs.writeFileSync(path.join(pluginDir, "extension.mjs"), "export default {};\n"); + fs.writeFileSync(path.join(pluginDir, "README.md"), "# test\n"); + fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes"); + + materializeExtensionPlugin(pluginDir); + const result = cleanMaterializedExtensionPlugin(pluginDir); + + assert.equal(result.removed, 3); + assert.equal(result.manifestUpdated, true); + assert.equal(fs.existsSync(path.join(pluginDir, "extension.mjs")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "README.md")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "assets", "preview.png")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions")), false); + + const pluginManifest = JSON.parse( + fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8") + ); + assert.equal(pluginManifest.extensions, "."); + assert.equal(pluginManifest.logo, "assets/preview.png"); }); diff --git a/extensions/accessibility-kanban/.github/plugin/plugin.json b/extensions/accessibility-kanban/.github/plugin/plugin.json index 03903a27..25bfbc1a 100644 --- a/extensions/accessibility-kanban/.github/plugin/plugin.json +++ b/extensions/accessibility-kanban/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "accessibility-kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/apng-studio/.github/plugin/plugin.json b/extensions/apng-studio/.github/plugin/plugin.json index fcd5bfc8..9522900b 100644 --- a/extensions/apng-studio/.github/plugin/plugin.json +++ b/extensions/apng-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "apng-studio", "description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Andrea Griffiths", "url": "https://github.com/AndreaGriffiths11" diff --git a/extensions/arcade-canvas/.github/plugin/plugin.json b/extensions/arcade-canvas/.github/plugin/plugin.json index 975971d8..9c45471e 100644 --- a/extensions/arcade-canvas/.github/plugin/plugin.json +++ b/extensions/arcade-canvas/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "arcade-canvas", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Dan Wahlin", "url": "https://github.com/DanWahlin" diff --git a/extensions/backlog-swipe-triage/.github/plugin/plugin.json b/extensions/backlog-swipe-triage/.github/plugin/plugin.json index 6dda88bd..aa7f7c16 100644 --- a/extensions/backlog-swipe-triage/.github/plugin/plugin.json +++ b/extensions/backlog-swipe-triage/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "backlog-swipe-triage", "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/chromium-control-canvas/.github/plugin/plugin.json b/extensions/chromium-control-canvas/.github/plugin/plugin.json index 8c4d631d..301485a6 100644 --- a/extensions/chromium-control-canvas/.github/plugin/plugin.json +++ b/extensions/chromium-control-canvas/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "chromium-control-canvas", "description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Andrea Griffiths", "url": "https://github.com/AndreaGriffiths11" diff --git a/extensions/color-orb/.github/plugin/plugin.json b/extensions/color-orb/.github/plugin/plugin.json index 3b564245..e5e31f0b 100644 --- a/extensions/color-orb/.github/plugin/plugin.json +++ b/extensions/color-orb/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "color-orb", "description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/connector-namespaces/.github/plugin/plugin.json b/extensions/connector-namespaces/.github/plugin/plugin.json index 397859c5..a75cb84c 100644 --- a/extensions/connector-namespaces/.github/plugin/plugin.json +++ b/extensions/connector-namespaces/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "connector-namespaces", "description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.", - "version": "1.1.1", + "version": "1.1.2", "author": { "name": "Alex Yang", "url": "https://github.com/alexyaang" diff --git a/extensions/diagram-viewer/.github/plugin/plugin.json b/extensions/diagram-viewer/.github/plugin/plugin.json index 9e2b6ef2..5e9b0975 100644 --- a/extensions/diagram-viewer/.github/plugin/plugin.json +++ b/extensions/diagram-viewer/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-viewer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/feedback-themes/.github/plugin/plugin.json b/extensions/feedback-themes/.github/plugin/plugin.json index 09435104..7a93483d 100644 --- a/extensions/feedback-themes/.github/plugin/plugin.json +++ b/extensions/feedback-themes/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "feedback-themes", "description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/gesture-review/.github/plugin/plugin.json b/extensions/gesture-review/.github/plugin/plugin.json index 1ba5cf3b..db776361 100644 --- a/extensions/gesture-review/.github/plugin/plugin.json +++ b/extensions/gesture-review/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gesture-review", "description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/java-modernization-studio/.github/plugin/plugin.json b/extensions/java-modernization-studio/.github/plugin/plugin.json index c3f22658..4eba3dd3 100644 --- a/extensions/java-modernization-studio/.github/plugin/plugin.json +++ b/extensions/java-modernization-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "java-modernization-studio", "description": "Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Ayan Gupta", "url": "https://github.com/ayangupt" diff --git a/extensions/release-notes-showcase/.github/plugin/plugin.json b/extensions/release-notes-showcase/.github/plugin/plugin.json index dcc4ba18..aa8a7462 100644 --- a/extensions/release-notes-showcase/.github/plugin/plugin.json +++ b/extensions/release-notes-showcase/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "release-notes-showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Kayla Cinnamon", "url": "https://github.com/cinnamon-msft" diff --git a/extensions/repo-actions-hub/.github/plugin/plugin.json b/extensions/repo-actions-hub/.github/plugin/plugin.json index bd317192..f43e4af1 100644 --- a/extensions/repo-actions-hub/.github/plugin/plugin.json +++ b/extensions/repo-actions-hub/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "repo-actions-hub", "description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/site-studio/.github/plugin/plugin.json b/extensions/site-studio/.github/plugin/plugin.json index 2c1605b6..2cc5f51d 100644 --- a/extensions/site-studio/.github/plugin/plugin.json +++ b/extensions/site-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "site-studio", "description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Ayan Gupta", "url": "https://github.com/ayangupt" diff --git a/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json b/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json index 70b8c98a..53edf975 100644 --- a/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json +++ b/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "tiny-tool-town-submitter", "description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/token-pacman/.github/plugin/plugin.json b/extensions/token-pacman/.github/plugin/plugin.json index b7868bb1..b63359a8 100644 --- a/extensions/token-pacman/.github/plugin/plugin.json +++ b/extensions/token-pacman/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "token-pacman", "description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/where-was-i/.github/plugin/plugin.json b/extensions/where-was-i/.github/plugin/plugin.json index e43c6f26..7ef8a679 100644 --- a/extensions/where-was-i/.github/plugin/plugin.json +++ b/extensions/where-was-i/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "where-was-i", "description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/work-hub/.github/plugin/plugin.json b/extensions/work-hub/.github/plugin/plugin.json index c3999519..8f88dd01 100644 --- a/extensions/work-hub/.github/plugin/plugin.json +++ b/extensions/work-hub/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "work-hub", "description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno"