diff --git a/.github/workflows/generate-agents.yml b/.github/workflows/generate-agents.yml index 5530fde..b93f15b 100644 --- a/.github/workflows/generate-agents.yml +++ b/.github/workflows/generate-agents.yml @@ -1,4 +1,4 @@ -name: Check AGENTS.md and marketplace.json +name: Check AGENTS.md, marketplace.json, README and manifest versions on: pull_request: @@ -8,6 +8,9 @@ on: - "**/SKILL.md" - "agents/AGENTS.md" - ".claude-plugin/marketplace.json" + - ".claude-plugin/plugin.json" + - "gemini-extension.json" + - "README.md" jobs: validate: @@ -19,7 +22,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v4 - - name: Generate AGENTS.md and validate marketplace.json + - name: Generate AGENTS.md and validate marketplace.json, README and manifest versions run: uv run scripts/generate_agents.py - name: Ensure AGENTS.md is up to date diff --git a/gemini-extension.json b/gemini-extension.json index e9daa47..b58c1a3 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "apify-agent-skills", "description": "Provides access to Apify Agent Skills for web scraping, data extraction, and automation.", - "version": "1.0.0", + "version": "2.0.0", "contextFileName": "agents/AGENTS.md" } diff --git a/scripts/generate_agents.py b/scripts/generate_agents.py index 434c2fe..acf4a5e 100644 --- a/scripts/generate_agents.py +++ b/scripts/generate_agents.py @@ -5,7 +5,9 @@ # /// """Generate AGENTS.md from AGENTS_TEMPLATE.md and SKILL.md frontmatter. -Also validates that marketplace.json is in sync with discovered skills. +Also validates the surfaces that still list skills by hand: marketplace.json and the +README skills table stay in sync with the discovered skills, and every manifest ships +the same version. Usage: uv run scripts/generate_agents.py @@ -23,6 +25,9 @@ TEMPLATE_PATH = ROOT / "scripts" / "AGENTS_TEMPLATE.md" OUTPUT_PATH = ROOT / "agents" / "AGENTS.md" MARKETPLACE_PATH = ROOT / ".claude-plugin" / "marketplace.json" +PLUGIN_PATH = ROOT / ".claude-plugin" / "plugin.json" +GEMINI_EXTENSION_PATH = ROOT / "gemini-extension.json" +README_PATH = ROOT / "README.md" def load_template() -> str: @@ -113,22 +118,107 @@ def validate_marketplace(skills: list[dict[str, str]]) -> list[str]: return errors +def validate_readme(skills: list[dict[str, str]]) -> list[str]: + """Validate the README skills table and badge count. Returns error messages. + + The table's prose is hand-written and richer than the SKILL.md descriptions, so + only the set of names and the count are checked - a new skill cannot land without + the README noticing, and the copy stays human. + """ + if not README_PATH.exists(): + return [f"README.md not found at {README_PATH}"] + + readme = README_PATH.read_text(encoding="utf-8") + section = re.search(r"^## Skills\n(.*?)^## ", readme, re.DOTALL | re.MULTILINE) + if not section: + return ["README.md has no '## Skills' section to validate"] + + errors: list[str] = [] + + # First cell of every table row, as `skill-name` in backticks. Scoped to the + # section so the unrelated tables elsewhere in the README are not matched. + listed = set(re.findall(r"^\|[^|]*`([a-z0-9][a-z0-9-]*)`", section.group(1), re.MULTILINE)) + discovered = {skill["name"] for skill in skills} + + for name in sorted(discovered - listed): + errors.append(f"Skill '{name}' is missing from the README '## Skills' table") + for name in sorted(listed - discovered): + errors.append(f"README '## Skills' table lists '{name}', which has no skills/{name}/SKILL.md") + + # The count is baked into the shields.io badge twice: its URL and its alt text. + for pattern, label in ((r"badge/Skills-(\d+)-", "badge URL"), (r'alt="(\d+) Skills"', "badge alt text")): + match = re.search(pattern, readme) + if not match: + errors.append(f"README.md has no skill count in the {label}") + elif int(match.group(1)) != len(skills): + errors.append(f"README.md {label} claims {match.group(1)} skills, found {len(skills)}") + + return errors + + +def validate_versions() -> list[str]: + """Validate that every manifest ships the same version. Returns error messages. + + Each reader yields (label suffix, version) pairs, so marketplace.json can report + its own metadata version alongside the per-plugin versions users actually install. + """ + readers = { + PLUGIN_PATH: lambda data: [("", data.get("version"))], + GEMINI_EXTENSION_PATH: lambda data: [("", data.get("version"))], + MARKETPLACE_PATH: lambda data: [("", data.get("metadata", {}).get("version"))] + + [ + (f" plugin '{plugin.get('name')}'", plugin.get("version")) + for plugin in data.get("plugins", []) + ], + } + + errors: list[str] = [] + versions: dict[str, str] = {} + + for path, read_versions in readers.items(): + if not path.exists(): + errors.append(f"{path.name} not found at {path}") + continue + data = json.loads(path.read_text(encoding="utf-8")) + for suffix, version in read_versions(data): + label = f"{path.name}{suffix}" + if not isinstance(version, str): + errors.append(f"{label} carries no version string") + continue + versions[label] = version + + if len(set(versions.values())) > 1: + listed = ", ".join(f"{label} {version}" for label, version in sorted(versions.items())) + errors.append(f"Manifest versions disagree: {listed}") + + return errors + + def main() -> None: template = load_template() skills = collect_skills() output = render(template, skills) OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUTPUT_PATH.write_text(output, encoding="utf-8") - print(f"Wrote {OUTPUT_PATH} with {len(skills)} skills.") - - # Validate marketplace.json - errors = validate_marketplace(skills) - if errors: - print("\nMarketplace.json validation errors:", file=sys.stderr) + # flush so this line stays ahead of the unbuffered error output below in CI logs + print(f"Wrote {OUTPUT_PATH} with {len(skills)} skills.", flush=True) + + # Validate the surfaces that still list skills by hand + checks = ( + ("Marketplace.json", validate_marketplace(skills)), + ("README.md", validate_readme(skills)), + ("Manifest version", validate_versions()), + ) + + failed = [(label, errors) for label, errors in checks if errors] + for label, errors in failed: + print(f"\n{label} validation errors:", file=sys.stderr) for error in errors: print(f" - {error}", file=sys.stderr) + if failed: sys.exit(1) - print("Marketplace.json validation passed.") + + print("Marketplace.json, README.md and manifest version validation passed.") if __name__ == "__main__":