Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/badges/skills-ref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"schemaVersion": 1,
"label": "skills-ref",
"message": "8/8 valid",
"color": "brightgreen"
}
47 changes: 47 additions & 0 deletions .github/workflows/skills-ref.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<a href="#why-trust-these-skills"><img
src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fslowdini%2Fslow-powers%2Fdev%2F.github%2Fbadges%2Fevals.json"
alt="evals" /></a>
<a href="https://github.com/agentskills/agentskills/tree/main/skills-ref"><img
src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fslowdini%2Fslow-powers%2Fdev%2F.github%2Fbadges%2Fskills-ref.json"
alt="skills-ref validation" /></a>
</p>

# Slow-powers
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
32 changes: 32 additions & 0 deletions scripts/skills-ref-badge.test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
119 changes: 119 additions & 0 deletions scripts/skills-ref-badge.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading