feat: document BYOS and multi-action button chaining (#8) #51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: validate | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| structure: | |
| name: Plugin structure & JSON | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate plugin.json | |
| run: | | |
| test -f .claude-plugin/plugin.json | |
| python3 -c "import json; json.load(open('.claude-plugin/plugin.json'))" | |
| test -f purchasely/.claude-plugin/plugin.json | |
| python3 -c "import json; json.load(open('purchasely/.claude-plugin/plugin.json'))" | |
| test -f purchasely/.codex-plugin/plugin.json | |
| python3 -c "import json; json.load(open('purchasely/.codex-plugin/plugin.json'))" | |
| test -f .cursor-plugin/plugin.json | |
| python3 -c "import json; json.load(open('.cursor-plugin/plugin.json'))" | |
| test -f purchasely/.cursor-plugin/plugin.json | |
| python3 -c "import json; json.load(open('purchasely/.cursor-plugin/plugin.json'))" | |
| - name: Validate marketplace.json | |
| run: | | |
| test -f .claude-plugin/marketplace.json | |
| python3 -c "import json; json.load(open('.claude-plugin/marketplace.json'))" | |
| test -f .agents/plugins/marketplace.json | |
| python3 -c "import json; json.load(open('.agents/plugins/marketplace.json'))" | |
| python3 - <<'PY' | |
| import json | |
| marketplace = json.load(open('.agents/plugins/marketplace.json')) | |
| plugin = marketplace['plugins'][0] | |
| assert plugin['name'] == 'purchasely' | |
| assert plugin['source']['path'] == './purchasely' | |
| PY | |
| test -f .cursor-plugin/marketplace.json | |
| python3 -c "import json; json.load(open('.cursor-plugin/marketplace.json'))" | |
| python3 - <<'PY' | |
| import json | |
| marketplace = json.load(open('.cursor-plugin/marketplace.json')) | |
| assert marketplace['metadata']['pluginRoot'] == '.' | |
| plugin = marketplace['plugins'][0] | |
| assert plugin['name'] == 'purchasely' | |
| assert plugin['source'] == 'purchasely' | |
| PY | |
| - name: Validate Gemini extension | |
| run: | | |
| test -f gemini-extension.json | |
| python3 -c "import json; json.load(open('gemini-extension.json'))" | |
| - name: Validate package.json | |
| run: | | |
| test -f package.json | |
| python3 -c "import json; json.load(open('package.json'))" | |
| - name: Check required files | |
| run: | | |
| for f in README.md LICENSE CONTRIBUTING.md SECURITY.md CODE_OF_CONDUCT.md CHANGELOG.md .gitignore; do | |
| test -f "$f" || { echo "::error file=$f::Missing required file"; exit 1; } | |
| done | |
| - name: Check all skills have SKILL.md | |
| run: | | |
| for d in skills/*/; do | |
| test -f "$d/SKILL.md" || { echo "::error file=$d::Missing SKILL.md"; exit 1; } | |
| done | |
| - name: Check plugin folder links | |
| run: | | |
| for f in \ | |
| purchasely/skills/integrate/SKILL.md \ | |
| purchasely/references/android/initialization.md \ | |
| purchasely/commands/integrate.md \ | |
| purchasely/agents/sdk-expert.md \ | |
| purchasely/hooks/hooks.json | |
| do | |
| test -f "$f" || { echo "::error file=$f::Missing plugin folder link"; exit 1; } | |
| done | |
| - name: Validate SKILL.md front-matter | |
| run: | | |
| python3 <<'PY' | |
| import os, re, sys, glob | |
| required = ("name", "description") | |
| errors = [] | |
| for path in sorted(glob.glob("skills/*/SKILL.md")): | |
| with open(path) as f: | |
| content = f.read() | |
| m = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL) | |
| if not m: | |
| errors.append(f"{path}: missing YAML front-matter") | |
| continue | |
| fm = m.group(1) | |
| for key in required: | |
| if not re.search(rf"^{key}\s*:", fm, re.MULTILINE): | |
| errors.append(f"{path}: front-matter missing '{key}:'") | |
| if errors: | |
| for e in errors: | |
| print(f"::error::{e}") | |
| sys.exit(1) | |
| print("All SKILL.md front-matter OK") | |
| PY | |
| - name: Validate skill reference links | |
| run: | | |
| python3 <<'PY' | |
| import glob | |
| import pathlib | |
| import re | |
| import sys | |
| errors = [] | |
| pattern = re.compile(r"(?<![\w./-])(?:\.\./\.\./references/|\.\./references/|references/)[A-Za-z0-9_./-]+") | |
| for path in sorted(glob.glob("skills/*/SKILL.md")): | |
| skill_path = pathlib.Path(path) | |
| content = skill_path.read_text() | |
| for match in pattern.finditer(content): | |
| ref = match.group(0).rstrip(".,;:)") | |
| if not ref.startswith("../../references/"): | |
| errors.append(f"{path}: use ../../{ref.removeprefix('../')} so installed plugins resolve references from the skill directory") | |
| continue | |
| target = (skill_path.parent / ref).resolve() | |
| if not target.exists(): | |
| errors.append(f"{path}: missing reference target {ref}") | |
| if errors: | |
| for error in errors: | |
| print(f"::error::{error}") | |
| sys.exit(1) | |
| print("All skill reference links OK") | |
| PY | |
| - name: Reject committed real API keys | |
| run: | | |
| if grep -rE 'pk_(live|test)_[A-Za-z0-9]{20,}|sk_(live|test)_[A-Za-z0-9]{20,}' \ | |
| --include='*.md' --include='*.json' --include='*.sh' --include='*.yml' \ | |
| --exclude-dir=.git . ; then | |
| echo "::error::Real-looking API key detected — refusing to merge." | |
| exit 1 | |
| fi | |
| markdownlint: | |
| name: Lint Markdown | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: DavidAnson/markdownlint-cli2-action@v17 | |
| with: | |
| globs: | | |
| **/*.md | |
| !node_modules | |
| !.github/PULL_REQUEST_TEMPLATE.md | |
| !.github/ISSUE_TEMPLATE/** |