docs: discover LLM models at runtime #61
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: Validate Skills | |
| on: | |
| pull_request: | |
| paths: | |
| - "higgsfield-*/**" | |
| - ".claude-plugin/**" | |
| - ".codex-plugin/**" | |
| - ".cursor-plugin/**" | |
| - "VERSION" | |
| - ".github/workflows/validate-skills.yml" | |
| push: | |
| branches: [main] | |
| paths: | |
| - "higgsfield-*/**" | |
| - ".claude-plugin/**" | |
| - ".codex-plugin/**" | |
| - ".cursor-plugin/**" | |
| - "VERSION" | |
| - ".github/workflows/validate-skills.yml" | |
| jobs: | |
| validate: | |
| name: Frontmatter, version sync, and references | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML | |
| run: pip install pyyaml --quiet | |
| - name: Validate frontmatter and name | |
| run: | | |
| set -euo pipefail | |
| fail=0 | |
| for skill_dir in higgsfield-*/; do | |
| skill_name="${skill_dir%/}" | |
| skill_md="${skill_dir}SKILL.md" | |
| if [ ! -f "$skill_md" ]; then | |
| echo "::error::$skill_md missing" | |
| fail=1 | |
| continue | |
| fi | |
| python3 - "$skill_md" "$skill_name" <<'PY' || fail=1 | |
| import sys, yaml, re, pathlib | |
| path = pathlib.Path(sys.argv[1]) | |
| expected_name = sys.argv[2] | |
| text = path.read_text() | |
| m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL) | |
| if not m: | |
| print(f"::error file={path}::no YAML frontmatter") | |
| sys.exit(1) | |
| fm = yaml.safe_load(m.group(1)) | |
| errors = [] | |
| if fm.get("name") != expected_name: | |
| errors.append(f"name '{fm.get('name')}' != directory '{expected_name}'") | |
| if not fm.get("version"): | |
| errors.append("version missing") | |
| if not fm.get("description"): | |
| errors.append("description missing") | |
| desc = fm.get("description", "") | |
| if len(desc) > 1024: | |
| errors.append(f"description exceeds 1024 characters ({len(desc)})") | |
| if "Use when" not in desc: | |
| errors.append("description missing 'Use when' trigger phrases") | |
| if "NOT for" not in desc: | |
| errors.append("description missing 'NOT for' boundary") | |
| for err in errors: | |
| print(f"::error file={path}::{err}") | |
| if errors: | |
| sys.exit(1) | |
| print(f"✓ {path} — frontmatter valid (name={fm['name']}, version={fm['version']})") | |
| PY | |
| done | |
| [ "$fail" -eq 0 ] | |
| - name: Validate version sync across all manifests | |
| run: | | |
| set -euo pipefail | |
| ROOT_VERSION="$(cat VERSION | tr -d '[:space:]')" | |
| echo "Root VERSION: $ROOT_VERSION" | |
| fail=0 | |
| for skill_dir in higgsfield-*/; do | |
| v=$(awk '/^---/{c++; next} c==1 && /^version:/{gsub(/^version:[[:space:]]*/,""); gsub(/[[:space:]]*#.*$/,""); gsub(/[[:space:]]/,""); print; exit}' "${skill_dir}SKILL.md") | |
| if [ "$v" != "$ROOT_VERSION" ]; then | |
| echo "::error file=${skill_dir}SKILL.md::version $v != VERSION $ROOT_VERSION" | |
| fail=1 | |
| fi | |
| done | |
| for f in .claude-plugin/plugin.json .codex-plugin/plugin.json .cursor-plugin/plugin.json; do | |
| v=$(python3 -c "import json,sys; print(json.load(open('$f'))['version'])") | |
| if [ "$v" != "$ROOT_VERSION" ]; then | |
| echo "::error file=$f::version $v != VERSION $ROOT_VERSION" | |
| fail=1 | |
| fi | |
| done | |
| # marketplace.json — version is nested under plugins[0].version | |
| v=$(python3 -c "import json; print(json.load(open('.claude-plugin/marketplace.json'))['plugins'][0]['version'])") | |
| if [ "$v" != "$ROOT_VERSION" ]; then | |
| echo "::error file=.claude-plugin/marketplace.json::plugins[0].version $v != VERSION $ROOT_VERSION" | |
| fail=1 | |
| fi | |
| [ "$fail" -eq 0 ] && echo "✓ all version locations match $ROOT_VERSION" | |
| - name: Validate marketplace.json lists every skill folder | |
| run: | | |
| set -euo pipefail | |
| fail=0 | |
| listed=$(python3 -c "import json; print(' '.join(s['path'] for s in json.load(open('.claude-plugin/marketplace.json'))['plugins'][0]['skills']))") | |
| for skill_dir in higgsfield-*/; do | |
| skill_name="${skill_dir%/}" | |
| if ! echo " $listed " | grep -q " $skill_name "; then | |
| echo "::error::skill folder '$skill_name' is not listed in .claude-plugin/marketplace.json" | |
| fail=1 | |
| fi | |
| done | |
| [ "$fail" -eq 0 ] && echo "✓ marketplace.json includes every skill folder" | |
| - name: Validate references resolve and no orphans | |
| run: | | |
| set -euo pipefail | |
| fail=0 | |
| for skill_dir in higgsfield-*/; do | |
| skill_md="${skill_dir}SKILL.md" | |
| if [ ! -d "${skill_dir}references" ]; then | |
| continue | |
| fi | |
| # every reference link in SKILL.md must exist | |
| for ref in $(grep -oE 'references/[a-zA-Z0-9_./-]+\.md' "$skill_md" | sort -u); do | |
| if [ ! -f "${skill_dir}${ref}" ]; then | |
| echo "::error file=$skill_md::references $ref but ${skill_dir}${ref} missing" | |
| fail=1 | |
| fi | |
| done | |
| # every references/*.md file must be linked from SKILL.md | |
| for f in "${skill_dir}references"/*.md; do | |
| base=$(basename "$f") | |
| if ! grep -q "$base" "$skill_md"; then | |
| echo "::error file=$f::orphaned reference (not linked from $skill_md)" | |
| fail=1 | |
| fi | |
| done | |
| done | |
| [ "$fail" -eq 0 ] && echo "✓ all references resolve, no orphans" | |
| - name: Validate no parent-dir references in SKILL.md or references/ | |
| run: | | |
| set -euo pipefail | |
| fail=0 | |
| for skill_dir in higgsfield-*/; do | |
| # Match ../ at start of line or after non-dot, non-slash to skip URLs like "..." in https://... | |
| if grep -nE '(^|[^./])\.\./' "${skill_dir}SKILL.md" 2>/dev/null; then | |
| echo "::error file=${skill_dir}SKILL.md::contains parent-dir (../) reference; skill must be self-contained" | |
| fail=1 | |
| fi | |
| if [ -d "${skill_dir}references" ]; then | |
| if grep -rnE '(^|[^./])\.\./\.\./' "${skill_dir}references/" 2>/dev/null; then | |
| echo "::error::${skill_dir}references/ contains ../../ paths that will break inside an installed bundle" | |
| fail=1 | |
| fi | |
| fi | |
| done | |
| [ "$fail" -eq 0 ] && echo "✓ no parent-dir references; skills are self-contained" |