mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-07 01:28:46 +00:00
chore: publish from main
This commit is contained in:
@@ -59,7 +59,7 @@ The following instructions are only to be applied when performing a code review.
|
||||
|
||||
**Only apply to directories in the `plugins/` directory**
|
||||
|
||||
- [ ] The plugin directory contains a `.github/plugin/plugin.json` file.
|
||||
- [ ] The plugin directory contains a root `plugin.json` file.
|
||||
- [ ] The plugin directory contains a `README.md` file.
|
||||
- [ ] The plugin.json has a `name` field matching the directory name.
|
||||
- [ ] The plugin.json has a `description` field.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "accessibility-kanban",
|
||||
"source": "extensions/accessibility-kanban",
|
||||
"source": "plugins/accessibility-kanban",
|
||||
"description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
@@ -83,13 +83,13 @@
|
||||
},
|
||||
{
|
||||
"name": "apng-studio",
|
||||
"source": "extensions/apng-studio",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
{
|
||||
"name": "arcade-canvas",
|
||||
"source": "extensions/arcade-canvas",
|
||||
"source": "plugins/arcade-canvas",
|
||||
"description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
@@ -156,13 +156,13 @@
|
||||
},
|
||||
{
|
||||
"name": "backlog-swipe-triage",
|
||||
"source": "extensions/backlog-swipe-triage",
|
||||
"source": "plugins/backlog-swipe-triage",
|
||||
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "backrooms-canvas",
|
||||
"source": "extensions/backrooms-canvas",
|
||||
"source": "plugins/backrooms-canvas",
|
||||
"description": "Wander an endless first-person backrooms in a Copilot canvas while agents work; their status ghost-writes on the walls.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
{
|
||||
"name": "chromium-control-canvas",
|
||||
"source": "extensions/chromium-control-canvas",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -271,7 +271,7 @@
|
||||
},
|
||||
{
|
||||
"name": "color-orb",
|
||||
"source": "extensions/color-orb",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -426,7 +426,7 @@
|
||||
},
|
||||
{
|
||||
"name": "diagram-viewer",
|
||||
"source": "extensions/diagram-viewer",
|
||||
"source": "plugins/diagram-viewer",
|
||||
"description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
@@ -573,7 +573,7 @@
|
||||
},
|
||||
{
|
||||
"name": "feedback-themes",
|
||||
"source": "extensions/feedback-themes",
|
||||
"source": "plugins/feedback-themes",
|
||||
"description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
@@ -601,7 +601,7 @@
|
||||
},
|
||||
{
|
||||
"name": "flight-map-canvas",
|
||||
"source": "extensions/flight-map-canvas",
|
||||
"source": "plugins/flight-map-canvas",
|
||||
"description": "A GitHub Copilot canvas that generates a view where Google Maps can be explored using 3D controls, as if a flight simulator. Agents can send the flight anywhere and report what they are working on.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
@@ -625,7 +625,7 @@
|
||||
},
|
||||
{
|
||||
"name": "gesture-review",
|
||||
"source": "extensions/gesture-review",
|
||||
"source": "plugins/gesture-review",
|
||||
"description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
@@ -733,7 +733,7 @@
|
||||
},
|
||||
{
|
||||
"name": "java-modernization-studio",
|
||||
"source": "extensions/java-modernization-studio",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -1061,7 +1061,7 @@
|
||||
},
|
||||
{
|
||||
"name": "pr-artifact-explorer",
|
||||
"source": "extensions/pr-artifact-explorer",
|
||||
"source": "plugins/pr-artifact-explorer",
|
||||
"description": "Navigate pull requests and securely explore GitHub Actions artifacts, including test results, static sites, terminal recordings, and source files.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
@@ -1097,13 +1097,13 @@
|
||||
},
|
||||
{
|
||||
"name": "release-notes-showcase",
|
||||
"source": "extensions/release-notes-showcase",
|
||||
"source": "plugins/release-notes-showcase",
|
||||
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "repo-actions-hub",
|
||||
"source": "extensions/repo-actions-hub",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -1145,13 +1145,13 @@
|
||||
},
|
||||
{
|
||||
"name": "signals-dashboard",
|
||||
"source": "extensions/signals-dashboard",
|
||||
"source": "plugins/signals-dashboard",
|
||||
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
|
||||
"version": "0.2.0"
|
||||
},
|
||||
{
|
||||
"name": "site-studio",
|
||||
"source": "extensions/site-studio",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -1271,13 +1271,13 @@
|
||||
},
|
||||
{
|
||||
"name": "tiny-tool-town-submitter",
|
||||
"source": "extensions/tiny-tool-town-submitter",
|
||||
"source": "plugins/tiny-tool-town-submitter",
|
||||
"description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "token-pacman",
|
||||
"source": "extensions/token-pacman",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -1495,7 +1495,7 @@
|
||||
},
|
||||
{
|
||||
"name": "where-was-i",
|
||||
"source": "extensions/where-was-i",
|
||||
"source": "plugins/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.2"
|
||||
},
|
||||
@@ -1559,7 +1559,7 @@
|
||||
},
|
||||
{
|
||||
"name": "work-hub",
|
||||
"source": "extensions/work-hub",
|
||||
"source": "plugins/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.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: create-canvas-extension
|
||||
description: 'Create or register a canvas extension in the awesome-copilot repository. Use when asked to scaffold a new canvas extension, create its plugin.json, add a reusable extension to one or more plugins, or migrate extension metadata. Extensions are reusable source under extensions/; shippable plugin manifests belong under plugins/.'
|
||||
argument-hint: '[optional extension name or description]'
|
||||
---
|
||||
|
||||
# Create a canvas extension
|
||||
|
||||
Use this skill only for the `github/awesome-copilot` repository. Canvas extensions are reusable source components. They do not have a `plugin.json` under `extensions/`.
|
||||
|
||||
## Required decisions
|
||||
|
||||
Before creating files, ask for each missing value:
|
||||
|
||||
1. **Extension ID**: lowercase kebab-case, matching the source folder and plugin name.
|
||||
2. **Display metadata**:
|
||||
- description
|
||||
- version (default `1.0.0`)
|
||||
- author name and optional URL
|
||||
- keywords (lowercase, hyphenated, maximum 10)
|
||||
- repository and license (default to the repository URL and `MIT` when appropriate)
|
||||
3. **Canvas entrypoint**: confirm whether the extension already has `extension.mjs`. If not, create a minimal entrypoint only when the user provides enough implementation details; otherwise create the directory and leave an explicit TODO.
|
||||
4. **Preview image**: obtain an existing `assets/preview.png` path or ask the user to add it. Do not invent a binary image or silently use a misleading placeholder.
|
||||
5. **Plugin registration**:
|
||||
- For a standalone installable canvas plugin, create `plugins/<extension-id>`.
|
||||
- For an extension that belongs to an existing plugin, ask for the parent plugin ID and add `./extensions/<extension-id>` to that plugin's `extensions.com.github.awesome-copilot.extensions`.
|
||||
- If the extension should be shipped by multiple plugins, collect all plugin IDs and add the same extension ID to each mapping file.
|
||||
|
||||
## Files to create
|
||||
|
||||
For a new extension plugin, create this structure:
|
||||
|
||||
```text
|
||||
extensions/<extension-id>/
|
||||
├── extension.mjs
|
||||
└── assets/
|
||||
└── preview.png
|
||||
|
||||
plugins/<extension-id>/
|
||||
├── plugin.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
The extension source may contain additional files such as `package.json`, canvas assets, or supporting modules. Keep all reusable implementation files under `extensions/<extension-id>/`.
|
||||
|
||||
Create `plugins/<extension-id>/plugin.json` with this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "<extension-id>",
|
||||
"description": "<description>",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "<author>"
|
||||
},
|
||||
"repository": "https://github.com/github/awesome-copilot",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"<keyword>"
|
||||
],
|
||||
"extensions": {
|
||||
"com.github.copilot": {
|
||||
"logo": "assets/preview.png"
|
||||
},
|
||||
"com.github.awesome-copilot": {
|
||||
"extensions": [
|
||||
"./extensions/<extension-id>"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep Agent Plugins fields at the manifest top level. Repository composition belongs only under `extensions.com.github.awesome-copilot`; do not put `agents`, `commands`, `hooks`, `mcpServers`, or `skills` at the top level or directly under `extensions`. Do not add `x-awesome-copilot`, `standalone`, or other repository-specific top-level fields.
|
||||
|
||||
For an existing parent plugin, create or update:
|
||||
|
||||
```text
|
||||
plugins/<parent-plugin>/plugin.json (`extensions.com.github.awesome-copilot.extensions`)
|
||||
```
|
||||
|
||||
Its `extensions` property must contain sorted repository-relative paths:
|
||||
|
||||
```json
|
||||
{
|
||||
"extensions": [
|
||||
"./extensions/<extension-id>"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Do not copy the extension source into the parent plugin. Materialization resolves the IDs from the root `extensions/` directory, so the same source can be bundled by multiple plugins.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the repository before editing:
|
||||
- confirm `extensions/<extension-id>` and `plugins/<extension-id>` do not already conflict
|
||||
- inspect the target parent plugin, if any
|
||||
- check whether a preview image and entrypoint already exist
|
||||
2. Ask only the missing required questions from the decisions above.
|
||||
3. Create the source and plugin directories with the required files.
|
||||
4. If creating a new entrypoint, keep it minimal and clearly mark implementation TODOs rather than fabricating behavior.
|
||||
5. Add or update `extensions.com.github.awesome-copilot.extensions` for every parent plugin that should ship the extension. Keep paths alphabetically sorted and unique.
|
||||
6. Ensure there is no `extensions/<extension-id>/.github/plugin/plugin.json`.
|
||||
7. Run:
|
||||
|
||||
```bash
|
||||
npm run plugin:validate
|
||||
npm run build
|
||||
npm run website:data
|
||||
```
|
||||
|
||||
8. Report the created paths, the plugins that ship the extension, and any missing user-provided assets or TODOs.
|
||||
|
||||
## Existing extension migration
|
||||
|
||||
When migrating an existing extension:
|
||||
|
||||
1. Move its existing manifest to `plugins/<extension-id>/plugin.json`.
|
||||
2. Update the manifest to the namespace-based `extensions.com.github.copilot.logo` shape.
|
||||
3. Remove the old manifest from `extensions/<extension-id>`.
|
||||
4. Register the extension in any parent plugin's `extensions.com.github.awesome-copilot.extensions`.
|
||||
5. Run the validation and build commands above.
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
if (files.length > 0) {
|
||||
errors.push(
|
||||
`${pluginPath}/${subdir}/ contains ${files.length} file(s): ${files.join(', ')}. ` +
|
||||
`Plugin directories on main should only contain .github/plugin/plugin.json and README.md. ` +
|
||||
`Plugin directories on main should only contain plugin.json and README.md. ` +
|
||||
`Agent, command, and skill files are materialized automatically during publish to marketplace.`
|
||||
);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
'⚠️ **Materialized files or symlinks detected in plugin directories**',
|
||||
'',
|
||||
'Plugin directories on the `main` branch should only contain:',
|
||||
'- `.github/plugin/plugin.json` (metadata)',
|
||||
'- `plugin.json` (metadata)',
|
||||
'- `README.md`',
|
||||
'',
|
||||
'Agent, command, and skill files are copied in automatically when publishing to `marketplace`.',
|
||||
|
||||
@@ -750,6 +750,7 @@ jobs:
|
||||
if (process.env.QUALITY_JOB_RESULT === 'failure' || process.env.QUALITY_JOB_RESULT === 'cancelled') {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'warning',
|
||||
vally_lint_status: 'infra_error',
|
||||
smoke_status: 'infra_error',
|
||||
version_match_status: 'infra_error',
|
||||
@@ -762,6 +763,7 @@ jobs:
|
||||
} else {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'warning',
|
||||
vally_lint_status: 'infra_error',
|
||||
smoke_status: 'infra_error',
|
||||
version_match_status: 'infra_error',
|
||||
|
||||
@@ -113,6 +113,7 @@ jobs:
|
||||
if (process.env.QUALITY_JOB_RESULT === 'failure' || process.env.QUALITY_JOB_RESULT === 'cancelled') {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'warning',
|
||||
vally_lint_status: 'infra_error',
|
||||
smoke_status: 'infra_error',
|
||||
version_match_status: 'infra_error',
|
||||
@@ -125,6 +126,7 @@ jobs:
|
||||
} else {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'warning',
|
||||
vally_lint_status: 'infra_error',
|
||||
smoke_status: 'infra_error',
|
||||
version_match_status: 'infra_error',
|
||||
|
||||
@@ -147,6 +147,7 @@ jobs:
|
||||
|
||||
let qualityResult = {
|
||||
overall_status: 'not_run',
|
||||
spec_compliance_status: 'not_run',
|
||||
failure_class: 'none',
|
||||
checked_plugins: [],
|
||||
summary: 'No changed external plugin entries were detected in this PR.',
|
||||
@@ -155,6 +156,7 @@ jobs:
|
||||
if (detectJobResult === 'failure' || detectJobResult === 'cancelled') {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'not_run',
|
||||
failure_class: 'infra',
|
||||
checked_plugins: [],
|
||||
version_match_status: 'infra_error',
|
||||
@@ -165,6 +167,7 @@ jobs:
|
||||
if (qualityJobResult === 'failure' || qualityJobResult === 'cancelled') {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'not_run',
|
||||
failure_class: 'infra',
|
||||
checked_plugins: [],
|
||||
version_match_status: 'infra_error',
|
||||
@@ -176,6 +179,7 @@ jobs:
|
||||
} else {
|
||||
qualityResult = {
|
||||
overall_status: 'infra_error',
|
||||
spec_compliance_status: 'not_run',
|
||||
failure_class: 'infra',
|
||||
checked_plugins: [],
|
||||
version_match_status: 'infra_error',
|
||||
@@ -201,11 +205,29 @@ jobs:
|
||||
});
|
||||
|
||||
const checkedPlugins = Array.isArray(qualityResult.checked_plugins) ? qualityResult.checked_plugins : [];
|
||||
const hasSpecWarnings = checkedPlugins.some((entry) => String(entry?.quality?.spec_compliance_status || '') === 'warning');
|
||||
const header = qualityResult.failure_class === 'submitter_fixes'
|
||||
? '## ⚠️ External plugin PR checks require submitter fixes'
|
||||
: qualityResult.overall_status === 'pass' || !shouldRun
|
||||
? '## ✅ External plugin PR checks passed'
|
||||
: '## ⚠️ External plugin PR checks need maintainer follow-up';
|
||||
? '## 🛑 External plugin PR checks failed (submitter fixes required)'
|
||||
: qualityResult.overall_status === 'infra_error'
|
||||
? '## 🛑 External plugin PR checks failed (maintainer follow-up)'
|
||||
: hasSpecWarnings
|
||||
? '## ⚠️ External plugin PR checks passed with spec warnings'
|
||||
: qualityResult.overall_status === 'pass' || !shouldRun
|
||||
? '## ✅ External plugin PR checks passed'
|
||||
: '## ⚠️ External plugin PR checks need maintainer follow-up';
|
||||
const formatStatus = (rawStatus, gateName) => {
|
||||
const status = String(rawStatus || 'not_run');
|
||||
if (status === 'pass') {
|
||||
return '✅ pass';
|
||||
}
|
||||
if (status === 'warning' || (gateName === 'spec compliance' && status === 'fail')) {
|
||||
return '⚠️ warning';
|
||||
}
|
||||
if (status === 'fail' || status === 'infra_error') {
|
||||
return '🛑 fail';
|
||||
}
|
||||
return '⚪ not_run';
|
||||
};
|
||||
const MAX_GATE_OUTPUT_CHARS = 2000;
|
||||
const escapeHtml = (value) =>
|
||||
String(value || '')
|
||||
@@ -273,21 +295,16 @@ jobs:
|
||||
? checkedPlugins.map((entry) => {
|
||||
const name = escapeMarkdownTableCell(entry?.name || 'unknown');
|
||||
const quality = entry?.quality || {};
|
||||
const sourceUrl = normalizeGitHubUrl(entry?.source_tree_url || '');
|
||||
const sourceUrl = normalizeGitHubUrl(entry?.source_tree_url);
|
||||
const locator = escapeMarkdownTableCell(entry?.source?.sha || entry?.source?.ref || 'repository');
|
||||
const sourceCell = sourceUrl ? `[${locator}](<${sourceUrl}>)` : locator;
|
||||
const vallyLintStatus = escapeMarkdownTableCell(quality.vally_lint_status || 'not_run');
|
||||
const smokeStatus = escapeMarkdownTableCell(quality.smoke_status || 'not_run');
|
||||
const versionMatchStatus = escapeMarkdownTableCell(quality.version_match_status || 'not_run');
|
||||
const refShaConsistencyStatus = escapeMarkdownTableCell(quality.ref_sha_consistency_status || 'not_run');
|
||||
const canvasStructureStatus = escapeMarkdownTableCell(quality.canvas_structure_status || 'not_run');
|
||||
const overallStatus = escapeMarkdownTableCell(quality.overall_status || 'not_run');
|
||||
return `| ${name} | ${vallyLintStatus} | ${smokeStatus} | ${versionMatchStatus} | ${refShaConsistencyStatus} | ${canvasStructureStatus} | ${overallStatus} | ${sourceCell} |`;
|
||||
const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator;
|
||||
return `| ${name} | ${formatStatus(quality.spec_compliance_status, 'spec compliance')} | ${formatStatus(quality.vally_lint_status, 'vally lint')} | ${formatStatus(quality.smoke_status, 'install smoke test')} | ${formatStatus(quality.version_match_status, 'version match')} | ${formatStatus(quality.ref_sha_consistency_status, 'ref/sha consistency')} | ${formatStatus(quality.canvas_structure_status, 'canvas structure')} | ${formatStatus(quality.overall_status, 'overall')} | ${sourceCell} |`;
|
||||
})
|
||||
: ['| _none_ | not_run | not_run | not_run | not_run | not_run | not_run | _n/a_ |'];
|
||||
: ['| _none_ | ⚪ not_run | ⚪ not_run | ⚪ 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 shouldShowSpec = quality.spec_compliance_status === 'warning' || String(quality.spec_compliance_output || '').trim().length > 0;
|
||||
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;
|
||||
@@ -295,11 +312,14 @@ jobs:
|
||||
const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0;
|
||||
|
||||
const details = [];
|
||||
if (shouldShowSpec) {
|
||||
details.push(formatGateOutput(name, 'spec compliance', formatStatus(quality.spec_compliance_status, 'spec compliance'), quality.spec_compliance_output));
|
||||
}
|
||||
if (shouldShowVally) {
|
||||
details.push(formatGateOutput(name, 'vally lint', quality.vally_lint_status, quality.vally_lint_output));
|
||||
details.push(formatGateOutput(name, 'vally lint', formatStatus(quality.vally_lint_status, 'vally lint'), quality.vally_lint_output));
|
||||
}
|
||||
if (shouldShowSmoke) {
|
||||
details.push(formatGateOutput(name, 'install smoke test', quality.smoke_status, quality.smoke_output));
|
||||
details.push(formatGateOutput(name, 'install smoke test', formatStatus(quality.smoke_status, 'install smoke test'), quality.smoke_output));
|
||||
}
|
||||
if (shouldShowVersionMatch) {
|
||||
details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output));
|
||||
@@ -319,11 +339,12 @@ jobs:
|
||||
'',
|
||||
`- **Changed entries detected:** ${changedCount}`,
|
||||
`- **Workflow state label:** \`${stateLabel}\``,
|
||||
'- **Status legend:** ✅ pass · ⚠️ warning · 🛑 fail',
|
||||
'',
|
||||
'### Per-plugin quality summary',
|
||||
'',
|
||||
'| Plugin | vally lint | install smoke test | version match | ref/sha consistency | canvas structure | overall | source tree |',
|
||||
'|---|---|---|---|---|---|---|---|',
|
||||
'| Plugin | spec compliance (non-blocking) | vally lint | install smoke test | version match | ref/sha consistency | canvas structure | overall | source tree |',
|
||||
'|---|---|---|---|---|---|---|---|---|',
|
||||
...rows,
|
||||
'',
|
||||
...(failureDetails.length > 0
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Validate Plugins and Extensions
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "plugins/**"
|
||||
- "extensions/**"
|
||||
- "eng/validate-plugins.mjs"
|
||||
- ".github/workflows/validate-plugins.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Plugin & extension spec validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Validate plugins and extensions
|
||||
id: validate
|
||||
run: npm run plugin:validate
|
||||
|
||||
- name: Manage validation summary on PR
|
||||
if: always()
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- plugin-extension-validation -->';
|
||||
const validationOutcome = '${{ steps.validate.outcome }}';
|
||||
const validationFailed = validationOutcome === 'failure';
|
||||
const validationPassed = validationOutcome === 'success';
|
||||
const body = [
|
||||
marker,
|
||||
'🛑 **Plugin/extension validation failed**',
|
||||
'',
|
||||
'One or more plugins or extensions in this PR do not pass spec validation.',
|
||||
'',
|
||||
'All internal plugins and extensions must include:',
|
||||
'- `"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"` in `plugin.json`',
|
||||
'- A valid `name`, `description`, and `version`',
|
||||
'- Repository composition (`agents`, `commands`, `hooks`, `mcpServers`, `skills`, and reusable `extensions`) under `extensions.com.github.awesome-copilot`',
|
||||
'- For **extensions**: `extensions.com.github.copilot.logo` must be set to `"assets/preview.png"`',
|
||||
'',
|
||||
'Do not put repository composition fields at the manifest top level or directly under `extensions`; they must be nested under `extensions.com.github.awesome-copilot`.',
|
||||
'',
|
||||
'Run `npm run plugin:validate` locally to see the full list of errors. See `.github/skills/create-canvas-extension/SKILL.md` and `CONTRIBUTING.md` for examples.',
|
||||
].join('\n');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existing = comments.find((comment) => comment.body.includes(marker));
|
||||
|
||||
if (validationFailed && existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
console.log(`Updated existing validation comment ${existing.id}`);
|
||||
} else if (validationFailed) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
console.log('Created validation comment');
|
||||
} else if (validationPassed && existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
console.log(`Deleted stale validation comment ${existing.id}`);
|
||||
}
|
||||
Reference in New Issue
Block a user