diff --git a/.github/scripts/package-skills.sh b/.github/scripts/package-skills.sh new file mode 100755 index 0000000..06a2ff7 --- /dev/null +++ b/.github/scripts/package-skills.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Package each Skyflow skill directory into a portable zip. +# +# Each zip extracts to /SKILL.md so it can be dropped straight into +# ~/.claude/skills/ (or a project's .claude/skills/) or handed to any harness +# that understands the Agent Skills format. Repo-only cruft is excluded so the +# artifact is clean. +# +# Output: dist/.zip for every skill, plus dist/SHA256SUMS.txt. +set -euo pipefail + +SKILLS_DIR="skyflow-skills-plugin/skills" +OUT_DIR="dist" + +# Files that exist for repo/contributor purposes and don't belong in a +# portable, runtime-facing skill artifact. +EXCLUDES=( + '*/.DS_Store' + '*/CONTRIBUTING.md' + '*.tmp' + '*.log' +) + +if [[ ! -d "$SKILLS_DIR" ]]; then + echo "error: $SKILLS_DIR not found (run from repo root)" >&2 + exit 1 +fi + +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" +OUT_ABS="$(cd "$OUT_DIR" && pwd)" + +shopt -s nullglob +count=0 +for skill_path in "$SKILLS_DIR"/*/; do + skill="$(basename "$skill_path")" + echo "Packaging $skill ..." + # Zip from inside SKILLS_DIR so archive paths are /... (no leading dirs). + ( cd "$SKILLS_DIR" && zip -r -q "$OUT_ABS/$skill.zip" "$skill" -x "${EXCLUDES[@]}" ) + count=$((count + 1)) +done + +if [[ "$count" -eq 0 ]]; then + echo "error: no skills found under $SKILLS_DIR" >&2 + exit 1 +fi + +# Integrity manifest (paths relative to dist/). +( cd "$OUT_DIR" && sha256sum ./*.zip > SHA256SUMS.txt ) + +echo +echo "Packaged $count skill(s) into $OUT_DIR/:" +ls -1 "$OUT_DIR" diff --git a/.github/scripts/validate-skills.py b/.github/scripts/validate-skills.py new file mode 100644 index 0000000..d15eb5b --- /dev/null +++ b/.github/scripts/validate-skills.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Validate Skyflow skill directories before packaging. + +Each skill under skyflow-skills-plugin/skills// must contain a SKILL.md +with YAML frontmatter whose `name` matches the directory and whose +`description` is present. Rules mirror the Agent Skills spec so that every +published zip is a valid, portable skill. + +Exit code 0 = all valid, 1 = one or more problems (details printed). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import yaml + +SKILLS_DIR = Path("skyflow-skills-plugin/skills") +FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) +NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +NAME_MAX = 64 +DESC_MAX = 1024 + + +def parse_frontmatter(text: str) -> dict | None: + """Parse the leading --- fenced YAML block into a dict. + + Uses a full YAML parser so multi-line values (block scalars, quoted + strings spanning lines) are handled correctly. Returns None if there is + no frontmatter block or it is not a YAML mapping. + """ + m = FRONTMATTER_RE.match(text) + if not m: + return None + try: + data = yaml.safe_load(m.group(1)) + except yaml.YAMLError: + return None + return data if isinstance(data, dict) else None + + +def validate_skill(skill_dir: Path) -> list[str]: + errors: list[str] = [] + skill_md = skill_dir / "SKILL.md" + if not skill_md.is_file(): + return [f"{skill_dir.name}: missing SKILL.md"] + + fm = parse_frontmatter(skill_md.read_text(encoding="utf-8")) + if fm is None: + return [f"{skill_dir.name}: SKILL.md has no valid --- YAML frontmatter block"] + + name = fm.get("name") + if not name or not isinstance(name, str): + errors.append(f"{skill_dir.name}: frontmatter missing `name`") + else: + if name != skill_dir.name: + errors.append( + f"{skill_dir.name}: frontmatter name `{name}` does not match directory" + ) + if len(name) > NAME_MAX: + errors.append(f"{skill_dir.name}: name exceeds {NAME_MAX} chars") + if not NAME_RE.match(name): + errors.append( + f"{skill_dir.name}: name `{name}` must be lowercase letters, digits, and hyphens" + ) + + desc = fm.get("description") + if not desc or not isinstance(desc, str) or not desc.strip(): + errors.append(f"{skill_dir.name}: frontmatter missing `description`") + elif len(desc) > DESC_MAX: + errors.append( + f"{skill_dir.name}: description exceeds {DESC_MAX} chars ({len(desc)})" + ) + + return errors + + +def main() -> int: + if not SKILLS_DIR.is_dir(): + print(f"error: {SKILLS_DIR} not found (run from repo root)", file=sys.stderr) + return 1 + + skill_dirs = sorted(p for p in SKILLS_DIR.iterdir() if p.is_dir()) + if not skill_dirs: + print(f"error: no skills found under {SKILLS_DIR}", file=sys.stderr) + return 1 + + all_errors: list[str] = [] + for skill_dir in skill_dirs: + errs = validate_skill(skill_dir) + if errs: + all_errors.extend(errs) + else: + print(f" ok {skill_dir.name}") + + if all_errors: + print("\nValidation failed:", file=sys.stderr) + for e in all_errors: + print(f" - {e}", file=sys.stderr) + return 1 + + print(f"\nAll {len(skill_dirs)} skills valid.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/package-skills.yml b/.github/workflows/package-skills.yml new file mode 100644 index 0000000..6159fb0 --- /dev/null +++ b/.github/workflows/package-skills.yml @@ -0,0 +1,75 @@ +name: Package skills + +# Build portable, per-skill zips and publish them as GitHub Release assets. +# Source of truth stays in skyflow-skills-plugin/skills/; the zips are pure +# build output and never committed, so the plugin/marketplace install is +# unaffected. +# +# Triggers: +# - push of a version tag (v*) -> release for that tag +# - manual run (workflow_dispatch) -> release for the provided tag, +# defaulting to v + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Release tag (defaults to v)" + required: false + type: string + +permissions: + contents: write + +jobs: + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install dependencies + run: pip install pyyaml + + - name: Validate skills + run: python3 .github/scripts/validate-skills.py + + - name: Package skills + run: bash .github/scripts/package-skills.sh + + - name: Resolve release tag + id: tag + run: | + if [ "${{ github.event_name }}" = "push" ]; then + tag="${GITHUB_REF_NAME}" + elif [ -n "${{ inputs.tag }}" ]; then + tag="${{ inputs.tag }}" + else + version="$(python3 -c 'import json;print(json.load(open("skyflow-skills-plugin/.claude-plugin/plugin.json"))["version"])')" + tag="v${version}" + fi + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + echo "Release tag: ${tag}" + + - name: Publish release assets + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} + run: | + set -euo pipefail + assets=(dist/*.zip dist/SHA256SUMS.txt) + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG exists; uploading assets (clobber)." + gh release upload "$TAG" "${assets[@]}" --clobber + else + echo "Creating release $TAG." + gh release create "$TAG" "${assets[@]}" \ + --title "Skyflow skills $TAG" \ + --notes "Portable Skyflow skill packages. Download a skill's \`.zip\`, unzip it into \`~/.claude/skills/\` (or a project's \`.claude/skills/\`) or any Agent Skills-compatible harness. Verify downloads against \`SHA256SUMS.txt\`." + fi diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 0000000..ae73034 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,28 @@ +name: Validate skills + +# Lightweight check: every skill under skyflow-skills-plugin/skills/ must have a +# valid SKILL.md before it can be merged or packaged. No artifacts are produced. + +on: + pull_request: + paths: + - "skyflow-skills-plugin/skills/**" + - ".github/scripts/validate-skills.py" + - ".github/workflows/validate-skills.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: pip install pyyaml + - name: Validate skill frontmatter + run: python3 .github/scripts/validate-skills.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ea00c6..dc4dc56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -198,6 +198,34 @@ After creating a new skill, add it to the Skills section in the main [README.md] 2. Write a paragraph describing what the skill does and its key features 3. The table of contents will be updated automatically if using a markdown formatter +## Packaging and Releasing Skills + +Skills are distributed two ways from a single source of truth (the directories under `skyflow-skills-plugin/skills/`): + +1. **Plugin** — installed via the marketplace (`/plugin install skyflow-skills@skyflow-marketplace`). +2. **Standalone zips** — one portable `.zip` per skill, published as GitHub Release assets for use in other projects or harnesses. + +The zips are build artifacts. They are **not** committed to the repo (`dist/` is gitignored), so the plugin/marketplace install is never affected. + +### CI workflows + +- **`.github/workflows/validate-skills.yml`** — runs on pull requests that touch skills. It checks every `SKILL.md` has valid frontmatter (`name` matches the directory, lowercase-hyphen, ≤64 chars; `description` present, ≤1024 chars). +- **`.github/workflows/package-skills.yml`** — runs on a pushed version tag (`v*`) or manual dispatch. It validates, zips each skill into `dist/.zip`, generates `dist/SHA256SUMS.txt`, and attaches everything to the matching GitHub Release. + +### Cutting a release + +1. Bump `version` in `skyflow-skills-plugin/.claude-plugin/plugin.json`. +2. Tag and push: `git tag v0.6.0 && git push origin v0.6.0`. The package workflow creates the release and uploads the zips automatically. + +Alternatively, trigger the **Package skills** workflow manually (Actions tab → *Run workflow*); it defaults the tag to `v`. + +### Building locally + +```sh +python3 .github/scripts/validate-skills.py # validate frontmatter +bash .github/scripts/package-skills.sh # build dist/*.zip + SHA256SUMS.txt +``` + ## Learn More For complete documentation on Claude Code plugins, see the [Claude Code Plugins documentation](https://code.claude.com/docs/en/plugins). diff --git a/README.md b/README.md index da623fd..fbac52e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A Claude Code plugin marketplace that enables Skyflow's data privacy and protect - [Claude Code Plugins for Skyflow](#claude-code-plugins-for-skyflow) - [Plugins](#plugins) - [Quick Start](#quick-start) + - [Standalone Skill Downloads](#standalone-skill-downloads) - [Environment Variables Reference](#environment-variables-reference) - [Upgrading](#upgrading) - [Learn More](#learn-more) @@ -64,6 +65,24 @@ Most users want `skyflow-skills` plus `skyflow-developer-mcp`. Add `skyflow-runt - [skyflow-developer-mcp setup](skyflow-developer-mcp-plugin/README.md#set-up-environment-variables) - [skyflow-runtime-mcp setup](skyflow-runtime-mcp-plugin/README.md#set-up-environment-variables) +## Standalone Skill Downloads + +The `skyflow-skills` plugin is the easiest way to get the skills in Claude Code. If you instead want a single skill as a portable file — to drop into another project, share, or use with a different Agent Skills-compatible harness — each skill is also published as a standalone `.zip` on the [Releases page](https://github.com/SkyflowFoundry/claude/releases/latest). + +Each archive unzips to a self-contained skill folder (`/SKILL.md` plus its resources). To install one manually: + +```sh +# Download the latest build of a skill (stable URL always points at the newest release) +curl -L -O https://github.com/SkyflowFoundry/claude/releases/latest/download/create-vault.zip + +# Unzip into your user skills directory (or a project's .claude/skills/) +unzip create-vault.zip -d ~/.claude/skills/ +``` + +Available skills: `call-rest-apis`, `create-vault`, `migrate-sdk-v1-to-v2`, `plan-skyflow-implementation`, `quickstart-js-browser`, `quickstart-node`. A `SHA256SUMS.txt` is attached to each release so you can verify downloads. + +> These zips are build artifacts generated from the same skills in this repo — the plugin and the standalone downloads are always in sync. + ## Environment Variables Reference These variables are read by the MCP plugins. See each plugin's README for step-by-step setup.