# Add shepherd-task plugin: end-to-end task automation via Copilot coding agent #808
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 .github/plugin/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:', | |
| '- `.github/plugin/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'); | |
| } |