Files
awesome-copilot/.github/workflows/check-plugin-structure.yml
T
Aaron Powell a7fdcd5006 Migrate plugins and canvas extensions to Agent Plugins spec (#2546)
* feat: migrate plugins and extensions to Agent Plugins v1.0.0 spec

- Add \ to all 69 curated plugin manifests
- Migrate all 18 extension manifests: add \, move logo into
  xtensions.com.github.copilot.logo namespace, remove top-level
  logo and string xtensions: '.'
- Update eng/validate-plugins.mjs: require \, validate
  namespace-keyed extensions object for canvas extensions, widen
  name pattern to allow dots (spec §5.5, max 64 chars)
- Update eng/materialize-plugins.mjs: emit spec-clean served manifests
  (only spec fields: \, name, version, description, author,
  homepage, repository, license, keywords, extensions)
- Update eng/generate-website-data.mjs: read logo from namespace
  with fallback to top-level logo for compatibility
- Update eng/create-plugin.mjs: scaffold emits \
- Add .github/workflows/validate-plugins.yml: blocking CI for PRs
  touching plugins/** or extensions/**
- Add spec compliance check to external plugin quality gates:
  non-blocking warnings with /⚠️/🛑 emoji legend
- Update AGENTS.md: document new extension manifest shape,
  add \ to plugin checklist

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* refactor: consolidate canvas extension plugins

- Move all extension plugin manifests from extensions/<name> to plugins/<name>
- Keep extensions/<name> as reusable source only
- Remove standalone extension discovery from marketplace and website plugin catalogs
- Auto-bundle same-name extension sources during materialization
- Add build-only extensions.json references for sharing extensions across plugins
- Remove x-awesome-copilot extension metadata support
- Update validation and contributor documentation

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* feat: add canvas extension scaffolding skill

- Add repo-local skill for creating canvas extension sources
- Generate spec-compliant plugin manifests under plugins/
- Support registering reusable extensions with multiple plugins
- Remove guidance for extension-local plugin manifests and custom fields

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* fix: align extension namespaces with current guidance

- Use each extension ID as its manifest namespace key
- Update validation and website generation to resolve extension-specific namespaces
- Upsert plugin validation PR comments using the existing repository pattern

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* fix: use Copilot extension namespace

- Adopt com.github.copilot for all canvas extension manifests
- Require the namespace during validation and website generation
- Update extension scaffolding guidance

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* docs: regenerate plugin catalog after merge

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* refactor(plugins): move manifests to plugin roots

Use root plugin.json manifests and namespaced extension directories throughout local tooling, validation, generation, and contributor documentation. Restore materialize-plugins.mjs line breaks so the source remains readable in GitHub.

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

Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* feat(plugins): migrate manifests to namespaced composition

Move repository composition metadata under com.github.awesome-copilot, materialize reusable extensions into the plugin extensions directory, and improve contributor and PR validation guidance.

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

Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* fix(validation): address plugin review findings

Restore executable build scripts, validate namespaced manifests and hook directories, improve README item counts, and manage validation comments across reruns.

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

Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
2026-08-06 22:29:31 +10:00

186 lines
7.0 KiB
YAML

name: Check Plugin Structure
on:
pull_request:
branches: [main]
paths:
- "plugins/**"
permissions:
contents: read
pull-requests: write
jobs:
check-materialized-files:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Check for materialized files in plugin directories
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const fs = require('fs');
const path = require('path');
const pluginsDir = 'plugins';
const errors = [];
function findSymlinks(rootDir) {
const symlinks = [];
const dirsToScan = [rootDir];
while (dirsToScan.length > 0) {
const currentDir = dirsToScan.pop();
let entries;
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch (error) {
throw new Error(`Failed to read directory "${currentDir}": ${error.message}`);
}
for (const entry of entries) {
const entryPath = path.join(currentDir, entry.name);
let stat;
try {
stat = fs.lstatSync(entryPath);
} catch (error) {
throw new Error(`Failed to inspect "${entryPath}": ${error.message}`);
}
if (stat.isSymbolicLink()) {
symlinks.push(entryPath);
continue;
}
if (stat.isDirectory()) {
dirsToScan.push(entryPath);
}
}
}
return symlinks;
}
if (!fs.existsSync(pluginsDir)) {
console.log('No plugins directory found');
return;
}
const pluginDirs = fs.readdirSync(pluginsDir, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
for (const plugin of pluginDirs) {
const pluginPath = path.join(pluginsDir, plugin);
// Check for materialized agent/command/skill files
for (const subdir of ['agents', 'commands', 'skills']) {
const subdirPath = path.join(pluginPath, subdir);
if (!fs.existsSync(subdirPath)) continue;
const stat = fs.lstatSync(subdirPath);
if (stat.isSymbolicLink()) {
errors.push(`${pluginPath}/${subdir} is a symlink — symlinks should not exist in plugin directories`);
continue;
}
if (stat.isDirectory()) {
const files = fs.readdirSync(subdirPath);
if (files.length > 0) {
errors.push(
`${pluginPath}/${subdir}/ contains ${files.length} file(s): ${files.join(', ')}. ` +
`Plugin directories on main should only contain plugin.json and README.md. ` +
`Agent, command, and skill files are materialized automatically during publish to marketplace.`
);
}
}
}
// Check for symlinks anywhere in the plugin directory without invoking a shell
try {
const symlinkPaths = findSymlinks(pluginPath);
if (symlinkPaths.length > 0) {
const formattedPaths = symlinkPaths.map(filePath => `\`${filePath}\``).join(', ');
errors.push(`${pluginPath} contains symlinks: ${formattedPaths}`);
}
} catch (error) {
errors.push(`Failed to inspect ${pluginPath} for symlinks: ${error.message}`);
}
}
if (errors.length > 0) {
const prBranch = context.payload.pull_request.head.ref;
const prRepo = context.payload.pull_request.head.repo.full_name;
const isFork = context.payload.pull_request.head.repo.fork;
const body = [
'⚠️ **Materialized files or symlinks detected in plugin directories**',
'',
'Plugin directories on the `main` branch should only contain:',
'- `plugin.json` (metadata)',
'- `README.md`',
'',
'Agent, command, and skill files are copied in automatically when publishing to `marketplace`.',
'',
'**Issues found:**',
...errors.map(e => `- ${e}`),
'',
'---',
'',
'### How to fix',
'',
'It looks like your branch may include materialized plugin files that should not be on `main`. Here are two options:',
'',
'**Option 1: Rebase to drop materialized files** (recommended if you have few commits)',
'```bash',
`git fetch origin main`,
`git rebase --onto origin/main origin/main ${prBranch}`,
`git push --force-with-lease`,
'```',
'',
'**Option 2: Remove the extra files manually**',
'```bash',
'# Remove materialized files from plugin directories',
'find plugins/ -mindepth 2 -maxdepth 2 -type d \\( -name agents -o -name commands -o -name skills \\) -exec rm -rf {} +',
'# Remove any symlinks',
'find plugins/ -type l -delete',
'git add -A && git commit -m "fix: remove materialized plugin files"',
'git push',
'```',
].join('\n');
let reviewPosted = false;
if (!isFork) {
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
event: 'REQUEST_CHANGES',
body
});
reviewPosted = true;
} catch (error) {
core.warning(
`Could not create PR review (continuing with failure report): ${error.message}`
);
}
} else {
core.warning('PR is from a fork; skipping createReview to avoid permission errors.');
}
if (!reviewPosted) {
core.warning('Materialized plugin issues detected. Full details:');
core.warning(body);
}
core.setFailed('Plugin directories contain materialized files or symlinks that should not be on main');
} else {
console.log('✅ All plugin directories are clean');
}