From 76f142b2b8abaab1d11478b19a267dab4a6db52c Mon Sep 17 00:00:00 2001 From: Max Haarhaus Date: Mon, 27 Jul 2026 15:07:47 -0400 Subject: [PATCH] feat(marketing): add skills-ref validation to CI and readme badges --- .github/badges/skills-ref.json | 6 ++ .github/workflows/skills-ref.yml | 47 ++++++++++++ README.md | 3 + package.json | 1 + scripts/skills-ref-badge.test.ts | 32 +++++++++ scripts/skills-ref-badge.ts | 119 +++++++++++++++++++++++++++++++ 6 files changed, 208 insertions(+) create mode 100644 .github/badges/skills-ref.json create mode 100644 .github/workflows/skills-ref.yml create mode 100644 scripts/skills-ref-badge.test.ts create mode 100644 scripts/skills-ref-badge.ts diff --git a/.github/badges/skills-ref.json b/.github/badges/skills-ref.json new file mode 100644 index 0000000..b96186f --- /dev/null +++ b/.github/badges/skills-ref.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "skills-ref", + "message": "8/8 valid", + "color": "brightgreen" +} diff --git a/.github/workflows/skills-ref.yml b/.github/workflows/skills-ref.yml new file mode 100644 index 0000000..a053e4b --- /dev/null +++ b/.github/workflows/skills-ref.yml @@ -0,0 +1,47 @@ +name: skills-ref badge + +on: + push: + branches: [dev] + paths-ignore: + - ".github/badges/skills-ref.json" + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + +permissions: + contents: write + +jobs: + badge: + name: Validate skills and update badge + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install skills-ref (tracks agentskills@main) + run: | + python -m pip install --upgrade pip + pip install "git+https://github.com/agentskills/agentskills@main#subdirectory=skills-ref" + skills-ref --version + + - uses: oven-sh/setup-bun@v2 + + - name: Validate skills and write badge JSON + run: bun run badge:skills-ref + + - name: Commit badge if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .github/badges/skills-ref.json + if git diff --staged --quiet; then + echo "Badge unchanged; nothing to commit." + exit 0 + fi + git commit -m "chore(badge): update skills-ref validation badge [skip ci]" + git push \ No newline at end of file diff --git a/README.md b/README.md index ab45241..af8db19 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ evals + skills-ref validation

# Slow-powers diff --git a/package.json b/package.json index e7fe375..4325f5d 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "evals:grade": "eval-magic grade --skill-dir ./skills", "evals:aggregate": "eval-magic aggregate --skill-dir ./skills", "evals:promote-baseline": "eval-magic promote-baseline --skill-dir ./skills", + "badge:skills-ref": "bun run scripts/skills-ref-badge.ts", "demo": "vhs assets/demo/tdd.tape", "check": "biome check --write .", "check:ci": "biome check --error-on-warnings .", diff --git a/scripts/skills-ref-badge.test.ts b/scripts/skills-ref-badge.test.ts new file mode 100644 index 0000000..3f7ae34 --- /dev/null +++ b/scripts/skills-ref-badge.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { buildBadgeJson } from "./skills-ref-badge"; + +describe("skills-ref-badge buildBadgeJson", () => { + test("all skills valid -> green score badge", () => { + expect(buildBadgeJson(8, 8)).toEqual({ + schemaVersion: 1, + label: "skills-ref", + message: "8/8 valid", + color: "brightgreen", + }); + }); + + test("one skill invalid -> red partial-score badge", () => { + expect(buildBadgeJson(7, 8)).toEqual({ + schemaVersion: 1, + label: "skills-ref", + message: "7/8 valid", + color: "red", + }); + }); + + test("skills-ref missing or errored -> grey unavailable badge", () => { + //total=0 signals the validator itself did not produce results + expect(buildBadgeJson(0, 0)).toEqual({ + schemaVersion: 1, + label: "skills-ref", + message: "unavailable", + color: "lightgrey", + }); + }); +}); diff --git a/scripts/skills-ref-badge.ts b/scripts/skills-ref-badge.ts new file mode 100644 index 0000000..95e8d42 --- /dev/null +++ b/scripts/skills-ref-badge.ts @@ -0,0 +1,119 @@ +#!/usr/bin/env bun +import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const SKILLS_DIR = "skills"; +const BADGE_FILE = ".github/badges/skills-ref.json"; +const SKILLS_REF_BIN = "skills-ref"; + +/** + * Pure helper: build the shields.io endpoint JSON for the skills-ref badge. + * `total === 0` signals the validator itself did not produce results + * (binary missing / errored), rendered as a neutral "unavailable" badge. + */ +export function buildBadgeJson( + valid: number, + total: number, +): { schemaVersion: number; label: string; message: string; color: string } { + if (total === 0) { + return { + schemaVersion: 1, + label: "skills-ref", + message: "unavailable", + color: "lightgrey", + }; + } + const allValid = valid === total; + return { + schemaVersion: 1, + label: "skills-ref", + message: `${valid}/${total} valid`, + color: allValid ? "brightgreen" : "red", + }; +} + +function listSkillDirs(): string[] { + return readdirSync(SKILLS_DIR) + .filter((entry) => statSync(join(SKILLS_DIR, entry)).isDirectory()) + .filter((entry) => { + try { + statSync(join(SKILLS_DIR, entry, "SKILL.md")); + return true; + } catch { + return false; + } + }) + .map((entry) => join(SKILLS_DIR, entry)) + .sort(); +} + +function validateSkill(skillDir: string): { ok: boolean; err?: string } { + const result = Bun.spawnSync([SKILLS_REF_BIN, "validate", skillDir], { + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + return { + ok: false, + err: `${result.stderr.toString().trim() || result.stdout.toString().trim()}`, + }; + } + return { ok: true }; +} + +function writeBadgeIfChanged(badge: { + schemaVersion: number; + label: string; + message: string; + color: string; +}): boolean { + const serialized = `${JSON.stringify(badge, null, 2)}\n`; + let previous = ""; + try { + previous = readFileSync(BADGE_FILE, "utf8"); + } catch { + // file does not exist yet + } + if (previous === serialized) { + return false; + } + writeFileSync(BADGE_FILE, serialized); + return true; +} + +if (import.meta.main) { + // If skills-ref is not installed, emit the neutral badge and exit 0 so the + // workflow never fails the job on a missing binary; the badge is the signal. + const binCheck = Bun.spawnSync([SKILLS_REF_BIN, "--version"], { + stdout: "pipe", + stderr: "pipe", + }); + if (binCheck.exitCode !== 0) { + console.error( + `warning: '${SKILLS_REF_BIN}' not found on PATH; writing unavailable badge`, + ); + writeBadgeIfChanged(buildBadgeJson(0, 0)); + process.exit(0); + } + + const skillDirs = listSkillDirs(); + let valid = 0; + for (const dir of skillDirs) { + const res = validateSkill(dir); + if (res.ok) { + valid += 1; + } else { + console.error(`✗ ${dir}\n${res.err}`); + } + } + + // Exit 0 even on validation failures: a red badge is the intended signal, + // not a workflow failure. Per-skill errors are logged above for the job log. + const changed = writeBadgeIfChanged(buildBadgeJson(valid, skillDirs.length)); + console.log( + `skills-ref: ${valid}/${skillDirs.length} valid — badge ${ + changed ? "updated" : "unchanged" + }`, + ); + process.exit(0); +}