Skip to content

Commit 3cbcd9b

Browse files
committed
Add missing docs-skills skill descriptions
Signed-off-by: Aidan Reilly <aireilly@redhat.com> Signed-off-by: Aidan Reilly <aireilly@redhat.com> Signed-off-by: Aidan Reilly <aireilly@redhat.com> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED Auto-discover docs-skills from source repo via GitHub API Replace the manually-maintained skills list for docs-skills in registry.yaml with automatic discovery from the source repository. generate_site.py --fetch-remote-skills fetches SKILL.md frontmatter in parallel and caches results in _discovered.yaml (gitignored). The deploy workflow runs with the flag so the published site always shows the full skills table with links to the source repo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED
1 parent 26e3ec3 commit 3cbcd9b

9 files changed

Lines changed: 463 additions & 22 deletions

File tree

.github/workflows/deploy-site.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ jobs:
2525
python-version: '3.12'
2626

2727
- name: Install dependencies
28-
run: pip install -r site/requirements.txt
28+
run: |
29+
pip install -r site/requirements.txt
30+
pip install pyyaml
31+
32+
- name: Generate site content with remote skill discovery
33+
run: python3 scripts/generate_site.py --fetch-remote-skills
2934

3035
- name: Build site
3136
working-directory: site

.gitignore

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,2 @@
1-
.venv/
2-
site/_site/
3-
__pycache__/
4-
*.pyc
5-
.idea/
6-
.cache/skills-registry/
7-
.tmp/
8-
site/docs/plugins/*/artifacts/
9-
*.DS_Store
1+
scripts/__pycache__/
2+
site/docs/plugins/*/_discovered.yaml

scripts/generate_site.py

Lines changed: 152 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
"""
1111

1212
import argparse
13+
import json
1314
import os
1415
import shutil
1516
import sys
17+
import urllib.request
18+
from concurrent.futures import ThreadPoolExecutor
1619
from pathlib import Path
1720

1821
import yaml
@@ -421,6 +424,7 @@ def generate_plugin_page(plugin: dict, registry: dict, enrichment: dict | None,
421424

422425
# Skills table
423426
if skills:
427+
is_discovered = plugin.get("_discovered", False)
424428
lines.append("## Skills")
425429
lines.append("")
426430
lines.append("| Skill | Description | Invocable |")
@@ -430,7 +434,13 @@ def generate_plugin_page(plugin: dict, registry: dict, enrichment: dict | None,
430434
sdesc = " ".join(skill.get("description", "").split())
431435
invocable = skill.get("user-invocable", True)
432436
badge = ":material-check:" if invocable else ":material-close: internal"
433-
lines.append(f"| [`/{sname}`]({sname}.md) | {sdesc} | {badge} |")
437+
if is_discovered and repo:
438+
ref = plugin.get("source", {}).get("ref", "main")
439+
skill_path = skill.get("path", sname)
440+
link = f"https://github.com/{repo}/tree/{ref}/{skill_path}"
441+
lines.append(f"| [`/{sname}`]({link}) | {sdesc} | {badge} |")
442+
else:
443+
lines.append(f"| [`/{sname}`]({sname}.md) | {sdesc} | {badge} |")
434444
lines.append("")
435445

436446
# Agents table
@@ -770,6 +780,93 @@ def load_enrichment(plugin_dir: Path) -> dict | None:
770780
return None
771781

772782

783+
def _parse_frontmatter(content: str) -> dict:
784+
"""Parse YAML frontmatter from a SKILL.md file."""
785+
content = content.lstrip()
786+
if not content.startswith("---"):
787+
return {}
788+
end = content.find("\n---", 3)
789+
if end == -1:
790+
return {}
791+
try:
792+
return yaml.safe_load(content[3:end]) or {}
793+
except yaml.YAMLError:
794+
return {}
795+
796+
797+
def _github_get(url: str) -> bytes:
798+
"""HTTP GET with optional GitHub token auth."""
799+
headers = {}
800+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
801+
if token:
802+
headers["Authorization"] = f"Bearer {token}"
803+
req = urllib.request.Request(url, headers=headers)
804+
with urllib.request.urlopen(req, timeout=30) as resp:
805+
return resp.read()
806+
807+
808+
def discover_remote_skills(repo: str, ref: str = "main") -> list[dict]:
809+
"""Discover skills from a GitHub repo by fetching SKILL.md frontmatter."""
810+
tree_url = f"https://api.github.com/repos/{repo}/git/trees/{ref}?recursive=1"
811+
tree_data = json.loads(_github_get(tree_url))
812+
813+
if tree_data.get("truncated"):
814+
print(f" Warning: tree for {repo} was truncated, some skills may be missed")
815+
816+
skill_entries = []
817+
for item in tree_data.get("tree", []):
818+
if item["type"] == "blob" and item["path"].endswith("/SKILL.md"):
819+
parent = item["path"].rsplit("/", 1)[0]
820+
skill_name = parent.rsplit("/", 1)[-1]
821+
skill_entries.append((skill_name, parent, item["path"]))
822+
823+
if not skill_entries:
824+
return []
825+
826+
def fetch_one(entry):
827+
skill_name, parent_path, full_path = entry
828+
raw_url = f"https://raw.githubusercontent.com/{repo}/{ref}/{full_path}"
829+
try:
830+
content = _github_get(raw_url).decode("utf-8", errors="replace")
831+
fm = _parse_frontmatter(content)
832+
return {
833+
"name": fm.get("name", skill_name),
834+
"description": fm.get("description", ""),
835+
"user-invocable": fm.get("user-invocable", True),
836+
"path": parent_path,
837+
}
838+
except Exception:
839+
return {
840+
"name": skill_name,
841+
"description": "",
842+
"user-invocable": True,
843+
"path": parent_path,
844+
}
845+
846+
with ThreadPoolExecutor(max_workers=8) as pool:
847+
skills = list(pool.map(fetch_one, sorted(skill_entries)))
848+
849+
return skills
850+
851+
852+
def load_discovered(plugin_dir: Path) -> list[dict] | None:
853+
"""Load cached discovered skills from _discovered.yaml."""
854+
path = plugin_dir / "_discovered.yaml"
855+
if path.exists():
856+
with open(path) as f:
857+
data = yaml.safe_load(f)
858+
if isinstance(data, list):
859+
return data
860+
return None
861+
862+
863+
def save_discovered(plugin_dir: Path, skills: list[dict]) -> None:
864+
"""Write discovered skills cache."""
865+
path = plugin_dir / "_discovered.yaml"
866+
with open(path, "w") as f:
867+
yaml.dump(skills, f, default_flow_style=False, sort_keys=False)
868+
869+
773870
def generate_mkdocs_yml(registry: dict, categories: dict,
774871
cat_plugins: dict[str, list]) -> str:
775872
"""Generate complete mkdocs.yml with dynamic nav."""
@@ -787,9 +884,10 @@ def generate_mkdocs_yml(registry: dict, categories: dict,
787884
skills = plugin.get("skills", [])
788885
nav_lines.append(f" - {name}:")
789886
nav_lines.append(f" - plugins/{name}/index.md")
790-
for skill in skills:
791-
sname = skill["name"]
792-
nav_lines.append(f" - {sname}: plugins/{name}/{sname}.md")
887+
if not plugin.get("_discovered"):
888+
for skill in skills:
889+
sname = skill["name"]
890+
nav_lines.append(f" - {sname}: plugins/{name}/{sname}.md")
793891

794892
# Categories section — with index page
795893
nav_lines.append(" - Categories:")
@@ -823,21 +921,37 @@ def generate_llms_txt(registry: dict, site_url: str) -> str:
823921
lines.append("")
824922
for p in plugins:
825923
pname = p["name"]
924+
is_discovered = p.get("_discovered", False)
925+
repo = p.get("source", {}).get("repo", "")
926+
ref = p.get("source", {}).get("ref", "main")
826927
for s in p.get("skills", []):
827928
sname = s["name"]
828929
sdesc = s.get("description", "").strip()
829930
if s.get("user-invocable", True):
830-
lines.append(f"- [{sname}]({site_url}/plugins/{pname}/{sname}/): {sdesc}")
931+
if is_discovered and repo:
932+
skill_path = s.get("path", sname)
933+
link = f"https://github.com/{repo}/tree/{ref}/{skill_path}"
934+
else:
935+
link = f"{site_url}/plugins/{pname}/{sname}/"
936+
lines.append(f"- [{sname}]({link}): {sdesc}")
831937
lines.append("")
832938
lines.append("## Optional")
833939
lines.append("")
834940
for p in plugins:
835941
pname = p["name"]
942+
is_discovered = p.get("_discovered", False)
943+
repo = p.get("source", {}).get("repo", "")
944+
ref = p.get("source", {}).get("ref", "main")
836945
for s in p.get("skills", []):
837946
if not s.get("user-invocable", True):
838947
sname = s["name"]
839948
sdesc = s.get("description", "").strip()
840-
lines.append(f"- [{sname}]({site_url}/plugins/{pname}/{sname}/): {sdesc} (internal)")
949+
if is_discovered and repo:
950+
skill_path = s.get("path", sname)
951+
link = f"https://github.com/{repo}/tree/{ref}/{skill_path}"
952+
else:
953+
link = f"{site_url}/plugins/{pname}/{sname}/"
954+
lines.append(f"- [{sname}]({link}): {sdesc} (internal)")
841955
lines.append("")
842956
return "\n".join(lines)
843957

@@ -915,11 +1029,36 @@ def generate_llms_full_txt(registry: dict, docs_dir: Path) -> str:
9151029
return "\n".join(lines)
9161030

9171031

918-
def generate_site(registry: dict, output_dir: Path):
1032+
def generate_site(registry: dict, output_dir: Path, *,
1033+
fetch_remote: bool = False):
9191034
docs = output_dir / "docs"
9201035
categories = registry.get("categories", {})
9211036
cat_plugins = build_category_plugins(registry)
9221037

1038+
# Auto-discover skills before generating any pages (counts need to be accurate)
1039+
for plugin in registry.get("plugins", []):
1040+
if not plugin.get("skills"):
1041+
name = plugin["name"]
1042+
plugin_dir = docs / "plugins" / name
1043+
plugin_dir.mkdir(parents=True, exist_ok=True)
1044+
repo = plugin.get("source", {}).get("repo", "")
1045+
ref = plugin.get("source", {}).get("ref", "main")
1046+
discovered = load_discovered(plugin_dir)
1047+
if fetch_remote and repo:
1048+
print(f" Discovering skills for {name} from {repo}...")
1049+
try:
1050+
discovered = discover_remote_skills(repo, ref)
1051+
save_discovered(plugin_dir, discovered)
1052+
print(f" Found {len(discovered)} skills")
1053+
except Exception as e:
1054+
print(f" Warning: {e}")
1055+
if discovered:
1056+
plugin["skills"] = discovered
1057+
plugin["_discovered"] = True
1058+
1059+
# Rebuild category groups after discovery so counts are accurate
1060+
cat_plugins = build_category_plugins(registry)
1061+
9231062
# Clean generated content
9241063
clean_generated(output_dir)
9251064

@@ -945,6 +1084,8 @@ def generate_site(registry: dict, output_dir: Path):
9451084
generate_plugin_page(plugin, registry, enrichment, plugin_dir))
9461085

9471086
for skill in plugin.get("skills", []):
1087+
if plugin.get("_discovered"):
1088+
continue
9481089
sname = skill["name"]
9491090
(plugin_dir / f"{sname}.md").write_text(
9501091
generate_skill_page(skill, plugin, enrichment, plugin_dir))
@@ -978,11 +1119,14 @@ def main() -> None:
9781119
formatter_class=argparse.RawDescriptionHelpFormatter)
9791120
parser.add_argument("--registry", default="registry.yaml")
9801121
parser.add_argument("--output-dir", default="site")
1122+
parser.add_argument("--fetch-remote-skills", action="store_true",
1123+
help="Discover skills from source repos via GitHub API")
9811124
args = parser.parse_args()
9821125

9831126
registry = load_registry(args.registry)
9841127
output_dir = Path(args.output_dir)
985-
generate_site(registry, output_dir)
1128+
generate_site(registry, output_dir,
1129+
fetch_remote=args.fetch_remote_skills)
9861130

9871131
plugins = registry.get("plugins", [])
9881132
skills = sum(len(p.get("skills", [])) for p in plugins)

site/docs/categories/documentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ Autonomous knowledge management skills for keeping AI context files (CLAUDE.md,
2121

2222
Documentation review, writing, and workflow tools for AsciiDoc and Markdown documentation. Includes an orchestrated multi-step pipeline, standalone review skills, codebase analysis for onboarding, and JIRA/PR integration.
2323

24-
**0 skills** - v0.3.0
24+
**49 skills** - v0.3.0

site/docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ hide:
1010

1111
# Skills and plugins for AI-assisted software engineering workflows
1212

13-
14 plugins | 75 skills | 7 categories
13+
14 plugins | 124 skills | 7 categories
1414

1515
[Getting Started](getting-started.md){ .md-button .md-button--primary }
1616

@@ -114,7 +114,7 @@ hide:
114114

115115
Documentation review, writing, and workflow tools for AsciiDoc and Markdown documentation. Includes an orchestrated m...
116116

117-
**0 skills** - Documentation - v0.3.0
117+
**49 skills** - Documentation - v0.3.0
118118

119119
</div>
120120

0 commit comments

Comments
 (0)