From defffec899ad873460bd61934167f9d04868e1a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 16:52:17 +0000 Subject: [PATCH 1/2] Add CI packaging of skills as standalone downloadable zips Publish each Skyflow skill as a portable .zip via GitHub Releases, in addition to the existing plugin/marketplace install. Skill directories remain the single source of truth; zips are build artifacts and are not committed, so the plugin install is unaffected. - validate-skills.py / validate-skills.yml: PR check that every SKILL.md has valid frontmatter (name matches dir, lowercase-hyphen, <=64 chars; description present, <=1024 chars). - package-skills.sh / package-skills.yml: on a v* tag or manual dispatch, zip each skill (extracts to /SKILL.md), emit SHA256SUMS.txt, and attach to the matching GitHub Release. - README: Standalone Skill Downloads section with install steps. - CONTRIBUTING: packaging/release process and local build commands. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XxeXNVRY2tk864Dm65HM9R --- .github/scripts/package-skills.sh | 54 +++++++++++++ .github/scripts/validate-skills.py | 112 ++++++++++++++++++++++++++ .github/workflows/package-skills.yml | 72 +++++++++++++++++ .github/workflows/validate-skills.yml | 26 ++++++ CONTRIBUTING.md | 28 +++++++ README.md | 19 +++++ 6 files changed, 311 insertions(+) create mode 100755 .github/scripts/package-skills.sh create mode 100644 .github/scripts/validate-skills.py create mode 100644 .github/workflows/package-skills.yml create mode 100644 .github/workflows/validate-skills.yml 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..84300a1 --- /dev/null +++ b/.github/scripts/validate-skills.py @@ -0,0 +1,112 @@ +#!/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 + +SKILLS_DIR = Path("skyflow-skills-plugin/skills") +NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +NAME_MAX = 64 +DESC_MAX = 1024 + + +def parse_frontmatter(text: str) -> dict[str, str] | None: + """Extract the leading --- fenced block into a flat key->value dict. + + Handles simple single-line values, optionally quoted. Returns None if no + frontmatter block is found. + """ + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + return None + fm: dict[str, str] = {} + for line in lines[1:]: + if line.strip() == "---": + return fm + m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line) + if m: + key, val = m.group(1), m.group(2).strip() + if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'": + val = val[1:-1] + fm[key] = val + # Reached EOF without a closing fence. + return 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 --- frontmatter block"] + + name = fm.get("name", "") + if not name: + 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: + 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..18d8062 --- /dev/null +++ b/.github/workflows/package-skills.yml @@ -0,0 +1,72 @@ +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: 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..726d968 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,26 @@ +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: 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. From f315452334bfe3bbf85bd482f3a8581f4fc7ca67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 18:04:28 +0000 Subject: [PATCH 2/2] Harden skill frontmatter validation with a real YAML parser Parse the SKILL.md frontmatter block with yaml.safe_load instead of a line-by-line key:value scan, so multi-line values (folded/literal block scalars, quoted strings spanning lines) are handled correctly. The old parser would reduce a `description: >` block scalar to just ">" and pass validation with an effectively empty description. - validate-skills.py: extract the --- fenced block and yaml.safe_load it; type-check name/description are non-empty strings. - validate-skills.yml / package-skills.yml: pip install pyyaml before running the validator (setup-python provides a clean interpreter). Addresses review feedback on PR #21. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XxeXNVRY2tk864Dm65HM9R --- .github/scripts/validate-skills.py | 43 +++++++++++++-------------- .github/workflows/package-skills.yml | 3 ++ .github/workflows/validate-skills.yml | 2 ++ 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.github/scripts/validate-skills.py b/.github/scripts/validate-skills.py index 84300a1..d15eb5b 100644 --- a/.github/scripts/validate-skills.py +++ b/.github/scripts/validate-skills.py @@ -15,33 +15,30 @@ 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[str, str] | None: - """Extract the leading --- fenced block into a flat key->value dict. +def parse_frontmatter(text: str) -> dict | None: + """Parse the leading --- fenced YAML block into a dict. - Handles simple single-line values, optionally quoted. Returns None if no - frontmatter block is found. + 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. """ - lines = text.splitlines() - if not lines or lines[0].strip() != "---": + m = FRONTMATTER_RE.match(text) + if not m: + return None + try: + data = yaml.safe_load(m.group(1)) + except yaml.YAMLError: return None - fm: dict[str, str] = {} - for line in lines[1:]: - if line.strip() == "---": - return fm - m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line) - if m: - key, val = m.group(1), m.group(2).strip() - if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'": - val = val[1:-1] - fm[key] = val - # Reached EOF without a closing fence. - return None + return data if isinstance(data, dict) else None def validate_skill(skill_dir: Path) -> list[str]: @@ -52,10 +49,10 @@ def validate_skill(skill_dir: Path) -> list[str]: fm = parse_frontmatter(skill_md.read_text(encoding="utf-8")) if fm is None: - return [f"{skill_dir.name}: SKILL.md has no valid --- frontmatter block"] + return [f"{skill_dir.name}: SKILL.md has no valid --- YAML frontmatter block"] - name = fm.get("name", "") - if not name: + 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: @@ -69,8 +66,8 @@ def validate_skill(skill_dir: Path) -> list[str]: f"{skill_dir.name}: name `{name}` must be lowercase letters, digits, and hyphens" ) - desc = fm.get("description", "") - if not desc: + 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( diff --git a/.github/workflows/package-skills.yml b/.github/workflows/package-skills.yml index 18d8062..6159fb0 100644 --- a/.github/workflows/package-skills.yml +++ b/.github/workflows/package-skills.yml @@ -34,6 +34,9 @@ jobs: with: python-version: "3.x" + - name: Install dependencies + run: pip install pyyaml + - name: Validate skills run: python3 .github/scripts/validate-skills.py diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml index 726d968..ae73034 100644 --- a/.github/workflows/validate-skills.yml +++ b/.github/workflows/validate-skills.yml @@ -22,5 +22,7 @@ jobs: - 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