From 786513f465b271fb16f2cc68f4effe6d98877608 Mon Sep 17 00:00:00 2001 From: operetz Date: Sun, 5 Jul 2026 13:01:47 +0300 Subject: [PATCH 1/4] add skill ZIP packaging script and GitHub Actions workflow --- .github/workflows/release-skill-zips.yaml | 47 ++++++++ Makefile | 10 +- scripts/package_skills.py | 140 ++++++++++++++++++++++ 3 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/release-skill-zips.yaml create mode 100644 scripts/package_skills.py diff --git a/.github/workflows/release-skill-zips.yaml b/.github/workflows/release-skill-zips.yaml new file mode 100644 index 0000000..307f3f7 --- /dev/null +++ b/.github/workflows/release-skill-zips.yaml @@ -0,0 +1,47 @@ +name: Package & Release Skill ZIPs + +on: + release: + types: [published] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + +jobs: + package-and-release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies + run: | + source $HOME/.cargo/env + make install + + - name: Package skills + run: | + source $HOME/.cargo/env + uv run python scripts/package_skills.py + + - name: Upload release assets + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + TAG="${{ github.event.release.tag_name }}" + find dist -name '*.zip' | while read zip; do + gh release upload "$TAG" "$zip" --clobber + done diff --git a/Makefile b/Makefile index 82ce119..3747000 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -.PHONY: help install validate validate-structure validate-collection-schema validate-collection-compliance validate-skill-design validate-skill-design-changed validate-mcp-tools clean check-uv +.PHONY: help install validate validate-structure validate-collection-schema validate-collection-compliance validate-skill-design validate-skill-design-changed validate-mcp-tools package clean check-uv help: - @echo "agentic-plugins" + @echo "agentic-collections-skills" @echo "" @echo "Available targets:" @echo " install - Install Python dependencies (requires uv)" @@ -12,6 +12,7 @@ help: @echo " validate-skill-design - Validate all skills (use PACK=rh-sre for a specific pack)" @echo " validate-skill-design-changed - Validate only changed skills (staged + unstaged, for local dev)" @echo " validate-mcp-tools - Validate allowed-tools against live MCP servers (requires podman)" + @echo " package - Package skills into ZIPs for ChatGPT upload (output: dist/)" @echo " clean - Remove generated files" @echo "" @echo "Requirements:" @@ -97,7 +98,10 @@ validate-mcp-tools: check-uv @uv run python scripts/validate_mcp_tools.py $(if $(PACK),$(PACK)) @echo "MCP tool validation complete!" +package: check-uv + @uv run python scripts/package_skills.py + clean: @echo "Cleaning generated files..." - @rm -rf .validate/ + @rm -rf .validate/ dist/ @echo "Cleaned!" diff --git a/scripts/package_skills.py b/scripts/package_skills.py new file mode 100644 index 0000000..c126ed9 --- /dev/null +++ b/scripts/package_skills.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Package skill packs into self-contained ZIPs for ChatGPT upload.""" + +import argparse +import logging +import os +import sys +import zipfile +from dataclasses import dataclass, field +from pathlib import Path + +logger = logging.getLogger(__name__) + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_PACK_EXCLUDE = {"scripts", "catalog", ".claude", ".github", ".lola", "docs", "eval"} + + +@dataclass +class PackageReport: + total_packs: int = 0 + total_skills: int = 0 + total_size_bytes: int = 0 + broken_symlinks: list[str] = field(default_factory=list) + errors: list[str] = field(default_factory=list) + + +def discover_packs(root: Path) -> list[str]: + return sorted( + d.name for d in root.iterdir() + if d.is_dir() and d.name not in _PACK_EXCLUDE and not d.name.startswith(".") + and ((d / "AGENTS.md").exists() or (d / "skills").is_dir()) + ) + + +def discover_skills(pack_dir: Path) -> list[Path]: + skills_dir = pack_dir / "skills" + if not skills_dir.is_dir(): + return [] + return sorted( + d for d in skills_dir.iterdir() + if d.is_dir() and (d / "SKILL.md").exists() + ) + + +def create_pack_zip(pack_dir: Path, output_path: Path) -> tuple[int, int, list[str]]: + """Create a single ZIP containing all skills for a pack. Returns (skill_count, file_count, broken_symlinks).""" + skills = discover_skills(pack_dir) + if not skills: + return 0, 0, [] + + broken_symlinks: list[str] = [] + file_count = 0 + + output_path.parent.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: + for skill_dir in skills: + skill_name = skill_dir.name + for file_path in sorted(skill_dir.rglob("*")): + if file_path.is_dir(): + continue + + arcname = f"{skill_name}/{file_path.relative_to(skill_dir)}" + + if file_path.is_symlink(): + resolved = file_path.resolve() + if not resolved.exists(): + logger.warning("Broken symlink: %s -> %s", file_path, os.readlink(file_path)) + broken_symlinks.append(str(file_path)) + continue + zf.write(resolved, arcname) + else: + zf.write(file_path, arcname) + + file_count += 1 + + return len(skills), file_count, broken_symlinks + + +def package_all( + root: Path, + output_dir: Path, + packs: list[str] | None = None, +) -> PackageReport: + report = PackageReport() + + all_packs = discover_packs(root) + target_packs = [p for p in all_packs if p in packs] if packs else all_packs + report.total_packs = len(target_packs) + + for pack_name in target_packs: + pack_dir = root / pack_name + zip_path = output_dir / f"{pack_name}.zip" + + try: + skill_count, file_count, broken = create_pack_zip(pack_dir, zip_path) + if skill_count == 0: + logger.info("Pack %s has no skills, skipping", pack_name) + continue + report.broken_symlinks.extend(broken) + report.total_skills += skill_count + zip_size = zip_path.stat().st_size + report.total_size_bytes += zip_size + logger.info("Packaged %s (%d skills, %d files, %d KB)", pack_name, skill_count, file_count, zip_size // 1024) + except Exception as e: + report.errors.append(f"{pack_name}: {e}") + logger.error("Failed to package %s: %s", pack_name, e) + + return report + + +def main() -> int: + parser = argparse.ArgumentParser(description="Package skill packs into ZIPs for ChatGPT upload") + parser.add_argument("--output-dir", default="dist", help="Output directory (default: dist)") + parser.add_argument("--packs", nargs="*", help="Only package these packs (default: all)") + parser.add_argument("--root", default=str(_REPO_ROOT), help="Repository root (default: auto-detect)") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(message)s") + + root = Path(args.root).resolve() + output_dir = Path(args.output_dir) + if not output_dir.is_absolute(): + output_dir = root / output_dir + + report = package_all(root=root, output_dir=output_dir, packs=args.packs) + + print(f"\nPackaging complete: {report.total_packs} packs, {report.total_skills} skills, {report.total_size_bytes // 1024} KB") + if report.broken_symlinks: + print(f" Warnings: {len(report.broken_symlinks)} broken symlinks skipped") + if report.errors: + print(f" Errors: {len(report.errors)}") + for err in report.errors: + print(f" - {err}") + + return 1 if report.errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 384f656dd8b5c1a21c22439c44278f8d18a5a2c5 Mon Sep 17 00:00:00 2001 From: operetz Date: Mon, 6 Jul 2026 14:47:28 +0300 Subject: [PATCH 2/4] add rolling latest release and update README download instructions --- .github/workflows/release-skill-zips.yaml | 13 ++++++++++++- README.md | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-skill-zips.yaml b/.github/workflows/release-skill-zips.yaml index 307f3f7..02d2028 100644 --- a/.github/workflows/release-skill-zips.yaml +++ b/.github/workflows/release-skill-zips.yaml @@ -35,8 +35,19 @@ jobs: source $HOME/.cargo/env uv run python scripts/package_skills.py + - name: Update rolling latest release + if: github.event_name == 'push' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh release delete latest --yes 2>/dev/null || true + git tag -f latest + git push origin latest --force + gh release create latest dist/*.zip --title "Latest Skill Packs" --notes "Auto-updated on every merge to main. Download the ZIP for your pack and upload it to ChatGPT." --latest + - name: Upload release assets - if: github.event_name == 'release' + if: github.event_name == 'release' && github.event.release.tag_name != 'latest' env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} diff --git a/README.md b/README.md index 907b73b..019726b 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,15 @@ See each pack's README for available skills and usage examples. The `eval/` directory contains skill evaluation reports (`report.json` + `report.md` per skill) generated by an external evaluation pipeline. +### Download Skill Packs for ChatGPT + +Pre-packaged ZIP files are available on the [Releases page](https://github.com/RHEcosystemAppEng/agentic-plugins/releases). The `latest` release is auto-updated on every merge to main. Each ZIP contains all skills for a pack with documentation included. + +To use them: +1. Go to the [latest release](https://github.com/RHEcosystemAppEng/agentic-plugins/releases/tag/latest) +2. Download the ZIP for your pack (e.g., `ocp-admin.zip`) +3. In ChatGPT, create or edit a Custom GPT and upload the ZIP under **Knowledge** + --- ## Prerequisites From 510c0ed9e1b4bf1e5e60f98e9bb4bd58d5a9a5f4 Mon Sep 17 00:00:00 2001 From: operetz Date: Thu, 9 Jul 2026 11:33:59 +0300 Subject: [PATCH 3/4] remove chatgpt references --- .github/workflows/release-skill-zips.yaml | 2 +- Makefile | 4 ++-- README.md | 4 ++-- scripts/package_skills.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-skill-zips.yaml b/.github/workflows/release-skill-zips.yaml index 02d2028..72aba6e 100644 --- a/.github/workflows/release-skill-zips.yaml +++ b/.github/workflows/release-skill-zips.yaml @@ -44,7 +44,7 @@ jobs: gh release delete latest --yes 2>/dev/null || true git tag -f latest git push origin latest --force - gh release create latest dist/*.zip --title "Latest Skill Packs" --notes "Auto-updated on every merge to main. Download the ZIP for your pack and upload it to ChatGPT." --latest + gh release create latest dist/*.zip --title "Latest Skill Packs" --notes "Auto-updated on every merge to main. Download the ZIP for your pack." --latest - name: Upload release assets if: github.event_name == 'release' && github.event.release.tag_name != 'latest' diff --git a/Makefile b/Makefile index 3747000..6e23ed7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: help install validate validate-structure validate-collection-schema validate-collection-compliance validate-skill-design validate-skill-design-changed validate-mcp-tools package clean check-uv help: - @echo "agentic-collections-skills" + @echo "agentic-plugins" @echo "" @echo "Available targets:" @echo " install - Install Python dependencies (requires uv)" @@ -12,7 +12,7 @@ help: @echo " validate-skill-design - Validate all skills (use PACK=rh-sre for a specific pack)" @echo " validate-skill-design-changed - Validate only changed skills (staged + unstaged, for local dev)" @echo " validate-mcp-tools - Validate allowed-tools against live MCP servers (requires podman)" - @echo " package - Package skills into ZIPs for ChatGPT upload (output: dist/)" + @echo " package - Package skills into ZIPs (output: dist/)" @echo " clean - Remove generated files" @echo "" @echo "Requirements:" diff --git a/README.md b/README.md index 019726b..1c2dcfc 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,14 @@ See each pack's README for available skills and usage examples. The `eval/` directory contains skill evaluation reports (`report.json` + `report.md` per skill) generated by an external evaluation pipeline. -### Download Skill Packs for ChatGPT +### Download Skill Packs Pre-packaged ZIP files are available on the [Releases page](https://github.com/RHEcosystemAppEng/agentic-plugins/releases). The `latest` release is auto-updated on every merge to main. Each ZIP contains all skills for a pack with documentation included. To use them: 1. Go to the [latest release](https://github.com/RHEcosystemAppEng/agentic-plugins/releases/tag/latest) 2. Download the ZIP for your pack (e.g., `ocp-admin.zip`) -3. In ChatGPT, create or edit a Custom GPT and upload the ZIP under **Knowledge** +3. Upload to your AI agent of choice (e.g., ChatGPT Custom GPT, Claude) --- diff --git a/scripts/package_skills.py b/scripts/package_skills.py index c126ed9..1766b32 100644 --- a/scripts/package_skills.py +++ b/scripts/package_skills.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Package skill packs into self-contained ZIPs for ChatGPT upload.""" +"""Package skill packs into self-contained ZIPs.""" import argparse import logging @@ -110,7 +110,7 @@ def package_all( def main() -> int: - parser = argparse.ArgumentParser(description="Package skill packs into ZIPs for ChatGPT upload") + parser = argparse.ArgumentParser(description="Package skill packs into self-contained ZIPs") parser.add_argument("--output-dir", default="dist", help="Output directory (default: dist)") parser.add_argument("--packs", nargs="*", help="Only package these packs (default: all)") parser.add_argument("--root", default=str(_REPO_ROOT), help="Repository root (default: auto-detect)") From ad65921c307af7332ebbbb7a24e8fa4027458b39 Mon Sep 17 00:00:00 2001 From: operetz Date: Thu, 9 Jul 2026 13:40:20 +0300 Subject: [PATCH 4/4] fix glob safety check and --packs argument parsing per code review --- .github/workflows/release-skill-zips.yaml | 4 ++++ scripts/package_skills.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-skill-zips.yaml b/.github/workflows/release-skill-zips.yaml index 72aba6e..2bcdecc 100644 --- a/.github/workflows/release-skill-zips.yaml +++ b/.github/workflows/release-skill-zips.yaml @@ -41,6 +41,10 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | + if ! ls dist/*.zip 1>/dev/null 2>&1; then + echo "No ZIPs found in dist/, skipping release update" + exit 1 + fi gh release delete latest --yes 2>/dev/null || true git tag -f latest git push origin latest --force diff --git a/scripts/package_skills.py b/scripts/package_skills.py index 1766b32..6117547 100644 --- a/scripts/package_skills.py +++ b/scripts/package_skills.py @@ -112,7 +112,7 @@ def package_all( def main() -> int: parser = argparse.ArgumentParser(description="Package skill packs into self-contained ZIPs") parser.add_argument("--output-dir", default="dist", help="Output directory (default: dist)") - parser.add_argument("--packs", nargs="*", help="Only package these packs (default: all)") + parser.add_argument("--packs", nargs="+", help="Only package these packs (default: all)") parser.add_argument("--root", default=str(_REPO_ROOT), help="Repository root (default: auto-detect)") args = parser.parse_args()