From 8a1dcc657ec3a568f7c7810c61bb75388c234617 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Sun, 25 Jan 2026 20:34:53 +1100 Subject: [PATCH] ci(workflows): add skill sync and validation workflows - Add sync-claude-plugin workflow to auto-update marketplace.json when skills change - Add validate-skill workflow to validate SKILL.md files on push/PR - Add sync-marketplace.js script for skill discovery and count updates --- .github/scripts/sync-marketplace.js | 67 ++++++++++++++++++++++++ .github/workflows/sync-claude-plugin.yml | 28 ++++++++++ .github/workflows/validate-skill.yml | 65 +++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 .github/scripts/sync-marketplace.js create mode 100644 .github/workflows/sync-claude-plugin.yml create mode 100644 .github/workflows/validate-skill.yml diff --git a/.github/scripts/sync-marketplace.js b/.github/scripts/sync-marketplace.js new file mode 100644 index 0000000..26b3444 --- /dev/null +++ b/.github/scripts/sync-marketplace.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * Sync marketplace.json with skills directory. + * + * Scans the skills/ directory for valid skills (directories containing SKILL.md) + * and updates marketplace.json to match. + */ + +const fs = require("fs"); +const path = require("path"); + +const SKILLS_DIR = "skills"; +const MARKETPLACE_FILE = ".claude-plugin/marketplace.json"; + +function getSkillsFromDirectory() { + if (!fs.existsSync(SKILLS_DIR)) { + return []; + } + + return fs + .readdirSync(SKILLS_DIR, { withFileTypes: true }) + .filter((entry) => { + if (!entry.isDirectory()) return false; + const skillFile = path.join(SKILLS_DIR, entry.name, "SKILL.md"); + return fs.existsSync(skillFile); + }) + .map((entry) => `./${SKILLS_DIR}/${entry.name}`) + .sort(); +} + +function updateSkillCount(description, count) { + return description.replace(/\d+ marketing skills/, `${count} marketing skills`); +} + +function main() { + const currentSkills = getSkillsFromDirectory(); + + const marketplace = JSON.parse(fs.readFileSync(MARKETPLACE_FILE, "utf8")); + const plugin = marketplace.plugins[0]; + const existingSkills = plugin.skills || []; + + // Check if update needed + if (JSON.stringify(currentSkills) === JSON.stringify(existingSkills)) { + console.log("marketplace.json is already in sync"); + return; + } + + // Update skills list + plugin.skills = currentSkills; + + // Update description with new count + plugin.description = updateSkillCount(plugin.description, currentSkills.length); + + // Write updated marketplace.json + fs.writeFileSync(MARKETPLACE_FILE, JSON.stringify(marketplace, null, 2) + "\n"); + + // Report changes + const added = currentSkills.filter((s) => !existingSkills.includes(s)); + const removed = existingSkills.filter((s) => !currentSkills.includes(s)); + + if (added.length) console.log(`Added: ${added.join(", ")}`); + if (removed.length) console.log(`Removed: ${removed.join(", ")}`); + + console.log(`Updated marketplace.json (${currentSkills.length} skills)`); +} + +main(); diff --git a/.github/workflows/sync-claude-plugin.yml b/.github/workflows/sync-claude-plugin.yml new file mode 100644 index 0000000..5af4098 --- /dev/null +++ b/.github/workflows/sync-claude-plugin.yml @@ -0,0 +1,28 @@ +name: Sync Claude Plugin + +on: + push: + branches: [main] + paths: + - 'skills/**' + +jobs: + sync: + runs-on: ubuntu-slim + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: true + + - name: Sync skills to marketplace.json + run: node .github/scripts/sync-marketplace.js + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore: sync marketplace.json with skills directory" + file_pattern: .claude-plugin/marketplace.json diff --git a/.github/workflows/validate-skill.yml b/.github/workflows/validate-skill.yml new file mode 100644 index 0000000..35e31b9 --- /dev/null +++ b/.github/workflows/validate-skill.yml @@ -0,0 +1,65 @@ +name: Validate Agent Skill + +on: + push: + branches: [main] + paths: + - "**/SKILL.md" + pull_request: + branches: [main] + paths: + - "**/SKILL.md" + +concurrency: + group: validate-skill-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect-changes: + runs-on: ubuntu-slim + if: github.event.pull_request.draft != true && github.actor != 'dependabot[bot]' + outputs: + skills: ${{ steps.changed-skills.outputs.skills }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get changed skills + id: changed-skills + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE=${{ github.event.pull_request.base.sha }} + HEAD=${{ github.event.pull_request.head.sha }} + else + BASE=${{ github.event.before }} + HEAD=${{ github.event.after }} + fi + + # Find changed SKILL.md files and extract skill directories + SKILLS=$(git diff --name-only $BASE $HEAD | \ + grep 'SKILL.md$' | \ + xargs -I {} dirname {} | \ + sort -u | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "skills=$SKILLS" >> $GITHUB_OUTPUT + echo "Changed skills: $SKILLS" + + validate: + needs: detect-changes + if: needs.detect-changes.outputs.skills != '[]' + runs-on: ubuntu-slim + strategy: + fail-fast: false + matrix: + skill: ${{ fromJson(needs.detect-changes.outputs.skills) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Validate ${{ matrix.skill }} + uses: Flash-Brew-Digital/validate-skill@v1 + with: + path: ${{ matrix.skill }} \ No newline at end of file