From fb1b9e164b9a2523f31ec8c954276ff8f9bd1316 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 18 Feb 2026 16:50:07 +1100 Subject: [PATCH] ci: add workflow to detect materialized files in plugin dirs Checks PRs targeting staged for agent/command/skill files or symlinks inside plugin directories. These files should only exist on main (materialized during publish). Requests changes if found. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/check-plugin-structure.yml | 102 +++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/check-plugin-structure.yml diff --git a/.github/workflows/check-plugin-structure.yml b/.github/workflows/check-plugin-structure.yml new file mode 100644 index 00000000..97780e63 --- /dev/null +++ b/.github/workflows/check-plugin-structure.yml @@ -0,0 +1,102 @@ +name: Check Plugin Structure + +on: + pull_request: + branches: [staged] + paths: + - "plugins/**" + +permissions: + contents: read + pull-requests: write + +jobs: + check-materialized-files: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for materialized files in plugin directories + uses: actions/github-script@v7 + with: + script: | + const { execSync } = require('child_process'); + const fs = require('fs'); + const path = require('path'); + + const pluginsDir = 'plugins'; + const errors = []; + + 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 staged should only contain .github/plugin/plugin.json and README.md. ` + + `Agent, command, and skill files are materialized automatically during publish to main.` + ); + } + } + } + + // Check for symlinks anywhere in the plugin directory + try { + const allFiles = execSync(`find "${pluginPath}" -type l`, { encoding: 'utf-8' }).trim(); + if (allFiles) { + errors.push(`${pluginPath} contains symlinks:\n${allFiles}`); + } + } catch (e) { + // find returns non-zero if no matches, ignore + } + } + + if (errors.length > 0) { + const body = [ + '⚠️ **Materialized files or symlinks detected in plugin directories**', + '', + 'Plugin directories on the `staged` branch should only contain:', + '- `.github/plugin/plugin.json` (metadata)', + '- `README.md`', + '', + 'Agent, command, and skill files are copied in automatically when publishing to `main`.', + 'Please remove the following:', + '', + ...errors.map(e => `- ${e}`), + ].join('\n'); + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + event: 'REQUEST_CHANGES', + body + }); + + core.setFailed('Plugin directories contain materialized files or symlinks that should not be on staged'); + } else { + console.log('✅ All plugin directories are clean'); + }