@@ -32,10 +32,14 @@ jobs:
3232 working-directory : ./scripts
3333 run : npm ci
3434
35- - name : Typecheck
35+ - name : Typecheck tests
3636 working-directory : ./tests
3737 run : npm run typecheck
3838
39+ - name : Typecheck scripts
40+ working-directory : ./scripts
41+ run : npm run typecheck
42+
3943 - name : Lint test
4044 working-directory : ./tests
4145 run : npm run lint
@@ -186,6 +190,8 @@ jobs:
186190 steps :
187191 - name : Checkout repository
188192 uses : actions/checkout@v4
193+ with :
194+ fetch-depth : 0
189195
190196 - name : Check changed skill files
191197 id : changed-skills
@@ -195,41 +201,54 @@ jobs:
195201 plugin/skills/**/SKILL.md
196202 .github/skills/**/SKILL.md
197203
204+ - name : Check all changed plugin skill files
205+ id : changed-plugin-skills
206+ uses : tj-actions/changed-files@v46
207+ with :
208+ files : |
209+ plugin/skills/**
210+ .github/skills/**
211+
212+ - name : Setup Node.js
213+ if : steps.changed-skills.outputs.any_changed == 'true'
214+ uses : actions/setup-node@v4
215+ with :
216+ node-version : ' 20'
217+ cache : ' npm'
218+ cache-dependency-path : scripts/package.json
219+
220+ - name : Install scripts dependencies
221+ if : steps.changed-skills.outputs.any_changed == 'true'
222+ working-directory : ./scripts
223+ run : npm ci
224+
198225 - name : Validate skill frontmatter
199226 if : steps.changed-skills.outputs.any_changed == 'true'
227+ working-directory : ./scripts
200228 run : |
201229 echo "## Skill Validation" >> $GITHUB_STEP_SUMMARY
230+ echo "" >> $GITHUB_STEP_SUMMARY
202231
203- # Use IFS to safely handle filenames (though markdown files typically don't have spaces)
204- IFS=$'\n'
232+ # Pass changed SKILL.md file paths directly to the validator
233+ # Prefix with ../ since working-directory is ./scripts
234+ SKILL_FILES=""
205235 for file in ${{ steps.changed-skills.outputs.all_changed_files }}; do
206- echo "Checking $file..."
207-
208- # Check for required frontmatter
209- if ! head -1 "$file" | grep -q "^---"; then
210- echo "::error file=$file::Missing YAML frontmatter"
211- echo "❌ $file - Missing frontmatter" >> $GITHUB_STEP_SUMMARY
212- exit 1
213- fi
214-
215- # Check for name field
216- if ! grep -q "^name:" "$file"; then
217- echo "::error file=$file::Missing required 'name' field in frontmatter"
218- echo "❌ $file - Missing 'name' field" >> $GITHUB_STEP_SUMMARY
219- exit 1
220- fi
221-
222- # Check for description field
223- if ! grep -q "^description:" "$file"; then
224- echo "::error file=$file::Missing required 'description' field in frontmatter"
225- echo "❌ $file - Missing 'description' field" >> $GITHUB_STEP_SUMMARY
226- exit 1
227- fi
228-
229- echo "✅ $file - Valid" >> $GITHUB_STEP_SUMMARY
236+ SKILL_FILES="$SKILL_FILES ../$file"
230237 done
231238
232- echo "All skill files have valid frontmatter"
239+ # Run frontmatter spec validation
240+ if OUTPUT=$(npm run frontmatter -- $SKILL_FILES 2>&1); then
241+ echo "$OUTPUT"
242+ echo "✅ All skill frontmatter is valid" >> $GITHUB_STEP_SUMMARY
243+ else
244+ echo "$OUTPUT"
245+ echo "❌ Frontmatter validation failed" >> $GITHUB_STEP_SUMMARY
246+ echo "" >> $GITHUB_STEP_SUMMARY
247+ echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
248+ echo "$OUTPUT" >> $GITHUB_STEP_SUMMARY
249+ echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
250+ exit 1
251+ fi
233252
234253 - name : Validate skills.json
235254 uses : actions/github-script@v7
@@ -281,6 +300,67 @@ jobs:
281300 core.setFailed('tests/skills.json validation failed');
282301 }
283302
303+ - name : Check skill version bumps
304+ if : steps.changed-plugin-skills.outputs.any_changed == 'true'
305+ run : |
306+ echo "## Skill Version Check" >> $GITHUB_STEP_SUMMARY
307+ echo "" >> $GITHUB_STEP_SUMMARY
308+
309+ FAILED=false
310+
311+ # For each changed file, walk up to find the nearest SKILL.md
312+ SKILL_FILES=""
313+ for file in ${{ steps.changed-plugin-skills.outputs.all_changed_files }}; do
314+ dir=$(dirname "$file")
315+ while [ "$dir" != "plugin/skills" ] && [ "$dir" != ".github/skills" ] && [ "$dir" != "plugin" ] && [ "$dir" != ".github" ] && [ "$dir" != "." ]; do
316+ if [ -f "$dir/SKILL.md" ]; then
317+ SKILL_FILES="$SKILL_FILES $dir/SKILL.md"
318+ break
319+ fi
320+ dir=$(dirname "$dir")
321+ done
322+ done
323+
324+ # Deduplicate
325+ SKILL_FILES=$(echo "$SKILL_FILES" | tr ' ' '\n' | sort -u | grep -v '^$')
326+
327+ if [ -z "$SKILL_FILES" ]; then
328+ echo "No SKILL.md files affected by changes."
329+ echo "✅ No version bump needed" >> $GITHUB_STEP_SUMMARY
330+ exit 0
331+ fi
332+
333+ for skill_file in $SKILL_FILES; do
334+ SKILL_NAME=$(basename "$(dirname "$skill_file")")
335+
336+ # Get version in base branch (use [[:space:]] for portable whitespace matching)
337+ BASE_VERSION=$(git show origin/${{ github.base_ref }}:"$skill_file" 2>/dev/null | sed -n '/^---$/,/^---$/p' | grep -E '^[[:space:]]+version:' | head -1 | sed 's/.*version:[[:space:]]*"\{0,1\}\([^"]*\)"\{0,1\}/\1/' || echo "")
338+
339+ # Get version in PR branch
340+ HEAD_VERSION=$(sed -n '/^---$/,/^---$/p' "$skill_file" | grep -E '^[[:space:]]+version:' | head -1 | sed 's/.*version:[[:space:]]*"\{0,1\}\([^"]*\)"\{0,1\}/\1/')
341+
342+ # New skill (no base version) — skip, it starts at 1.0.0
343+ if [ -z "$BASE_VERSION" ]; then
344+ echo "✅ $skill_file — new skill (version: $HEAD_VERSION)"
345+ continue
346+ fi
347+
348+ if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
349+ echo "::error file=$skill_file::Skill '$SKILL_NAME' was modified but metadata.version was not bumped ($BASE_VERSION). Bump the version in your PR."
350+ echo "❌ \`$SKILL_NAME\` (\`$skill_file\`) — version not bumped ($BASE_VERSION)" >> $GITHUB_STEP_SUMMARY
351+ FAILED=true
352+ else
353+ echo "✅ $skill_file — $SKILL_NAME version bumped: $BASE_VERSION → $HEAD_VERSION"
354+ echo "✅ \`$SKILL_NAME\` (\`$skill_file\`) — $BASE_VERSION → $HEAD_VERSION" >> $GITHUB_STEP_SUMMARY
355+ fi
356+ done
357+
358+ if [ "$FAILED" = true ]; then
359+ echo "" >> $GITHUB_STEP_SUMMARY
360+ echo "> Every PR that modifies a skill must bump its \`metadata.version\` in the same PR." >> $GITHUB_STEP_SUMMARY
361+ exit 1
362+ fi
363+
284364 - name : Skip message
285365 if : steps.changed-skills.outputs.any_changed != 'true'
286366 run : echo "No skill files changed - skipping validation"
0 commit comments