docs: point Cursor install at the live marketplace listing #298
Workflow file for this run
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: Fusion Skills CI | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| jobs: | |
| shellcheck: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install ShellCheck | |
| run: command -v shellcheck || { sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck; } | |
| - name: Run ShellCheck | |
| run: | | |
| shellcheck hooks/*.sh scripts/*.sh | |
| shellcheck --severity=error *.sh | |
| test-hooks: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Install dependencies | |
| run: | | |
| python -m venv .venv | |
| source .venv/bin/activate | |
| pip install --upgrade pip | |
| pip install pyyaml crowdstrike-falconpy | |
| - name: Run hook tests | |
| run: | | |
| source .venv/bin/activate | |
| ./test-hooks.sh | |
| - name: Run structural validation | |
| run: | | |
| source .venv/bin/activate | |
| ./test-validate.sh | |
| - name: Run scorecard parser tests | |
| run: | | |
| ./test-scorecard-parser.sh | |
| ./test-skill-scorecard.sh | |
| ./test-skill-isolation.sh | |
| ./test-verdict-parser.sh | |
| ./test-bootstrap-symlink.sh | |
| pytest: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Install dependencies | |
| run: | | |
| python -m venv .venv | |
| source .venv/bin/activate | |
| pip install --upgrade pip | |
| pip install -r requirements-test.txt | |
| - name: Run unit tests | |
| run: | | |
| source .venv/bin/activate | |
| pytest tests/ -v \ | |
| --cov=common/scripts \ | |
| --cov=skills/authoring/scripts \ | |
| --cov=skills/deployment/scripts \ | |
| --cov=skills/execution/scripts \ | |
| --cov=skills/lookup-files/scripts \ | |
| --cov=scripts \ | |
| --cov-report=term-missing \ | |
| --cov-fail-under=90 | |
| pylint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Install dependencies | |
| run: | | |
| python -m venv .venv | |
| source .venv/bin/activate | |
| pip install --upgrade pip | |
| pip install pylint crowdstrike-falconpy pyyaml tomli | |
| - name: Run pylint (fail-under=10 enforced by .pylintrc) | |
| run: | | |
| source .venv/bin/activate | |
| pylint --rcfile=.pylintrc \ | |
| common/scripts/*.py \ | |
| skills/authoring/scripts/*.py \ | |
| skills/deployment/scripts/*.py \ | |
| skills/execution/scripts/*.py \ | |
| skills/lookup-files/scripts/*.py \ | |
| scripts/*.py | |
| validate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Validate JSON files | |
| run: | | |
| for f in \ | |
| hooks/hooks.json \ | |
| plugin.json \ | |
| .claude-plugin/plugin.json \ | |
| .claude-plugin/marketplace.json \ | |
| .codex-plugin/plugin.json \ | |
| test-result-schema.json \ | |
| verify-result-schema.json; do | |
| echo "Validating $f" | |
| python -m json.tool "$f" > /dev/null | |
| done | |
| - name: Validate SKILL.md frontmatter | |
| run: | | |
| fail=0 | |
| for skill in skills/*/SKILL.md; do | |
| for field in name description version; do | |
| if ! head -20 "$skill" | grep -q "^${field}:"; then | |
| echo "FAIL: $skill missing '$field' in frontmatter" | |
| fail=1 | |
| fi | |
| done | |
| done | |
| exit $fail | |
| - name: Validate skill size budgets | |
| run: | | |
| fail=0 | |
| total_desc_chars=0 | |
| max_tokens=5500 | |
| for skill in skills/*/SKILL.md; do | |
| name=$(basename "$(dirname "$skill")") | |
| chars=$(wc -c < "$skill") | |
| approx_tokens=$((chars / 4)) | |
| pct=$((approx_tokens * 100 / max_tokens)) | |
| if [ "$approx_tokens" -gt "$max_tokens" ]; then | |
| echo "FAIL: $name ~${approx_tokens} tokens (${pct}% of ${max_tokens} limit)" | |
| fail=1 | |
| elif [ "$approx_tokens" -gt 4500 ]; then | |
| echo "WARN: $name ~${approx_tokens} tokens (${pct}% of ${max_tokens} limit)" | |
| fi | |
| desc=$(sed -n 's/^description: *//p' "$skill") | |
| desc_len=${#desc} | |
| total_desc_chars=$((total_desc_chars + desc_len)) | |
| if [ "$desc_len" -gt 1536 ]; then | |
| echo "FAIL: $name description ${desc_len} chars (max 1536)" | |
| fail=1 | |
| fi | |
| done | |
| desc_budget=8000 | |
| desc_pct=$((total_desc_chars * 100 / desc_budget)) | |
| echo "" | |
| echo "Description budget: ${total_desc_chars} / ~${desc_budget} chars (${desc_pct}%) — 1% of 200k context at ~4 chars/token" | |
| exit $fail | |
| - name: Validate hook scripts are executable | |
| run: | | |
| fail=0 | |
| for script in hooks/*.sh; do | |
| [ -x "$script" ] || { echo "FAIL: $script is not executable"; fail=1; } | |
| done | |
| exit $fail | |
| - name: Validate Python scripts are syntactically valid | |
| run: | | |
| fail=0 | |
| while IFS= read -r py; do | |
| if ! python -c "import ast, sys; ast.parse(open(sys.argv[1], encoding='utf-8').read())" "$py"; then | |
| echo "FAIL: $py has a syntax error" | |
| fail=1 | |
| fi | |
| done < <(find . -name '*.py' -not -path '*/__pycache__/*' -not -path '*/.venv/*') | |
| exit $fail | |
| - name: Validate no PLACEHOLDER values in example workflows | |
| run: | | |
| fail=0 | |
| while IFS= read -r wf; do | |
| if grep -qE 'PLACEHOLDER_[A-Z_]+' "$wf"; then | |
| echo "FAIL: $wf contains PLACEHOLDER_* values (action IDs must be resolved)" | |
| grep -nE 'PLACEHOLDER_[A-Z_]+' "$wf" | |
| fail=1 | |
| fi | |
| done < <(find skills/authoring/examples -name '*.yaml' -o -name '*.yml') | |
| [ $fail -eq 0 ] && echo "No PLACEHOLDER_* values in example workflows" | |
| exit $fail | |
| - name: Validate version consistency | |
| run: | | |
| fail=0 | |
| plugin_version=$(jq -r '.version' .claude-plugin/plugin.json) | |
| marketplace_version=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json) | |
| echo "plugin.json version: $plugin_version" | |
| echo "marketplace.json plugins[0].version: $marketplace_version" | |
| # marketplace.json must match plugin.json | |
| if [ "$marketplace_version" != "$plugin_version" ]; then | |
| echo "FAIL: marketplace.json plugins[0].version '$marketplace_version' != plugin.json '$plugin_version'" | |
| fail=1 | |
| fi | |
| # Root Agent Plugins manifest must match .claude-plugin/plugin.json | |
| root_plugin_version=$(jq -r '.version' plugin.json) | |
| if [ "$root_plugin_version" != "$plugin_version" ]; then | |
| echo "FAIL: plugin.json version '$root_plugin_version' != .claude-plugin/plugin.json '$plugin_version'" | |
| fail=1 | |
| fi | |
| # Codex manifest must match .claude-plugin/plugin.json | |
| codex_plugin_version=$(jq -r '.version' .codex-plugin/plugin.json) | |
| if [ "$codex_plugin_version" != "$plugin_version" ]; then | |
| echo "FAIL: .codex-plugin/plugin.json version '$codex_plugin_version' != .claude-plugin/plugin.json '$plugin_version'" | |
| fail=1 | |
| fi | |
| # Agent Plugins marketplace catalog pins the plugin to the release tag; | |
| # its source.ref must match v<version> so it can't drift on a manual edit | |
| if [ -f .agents/plugins/marketplace.json ]; then | |
| agents_ref=$(jq -r '.plugins[0].source.ref' .agents/plugins/marketplace.json) | |
| if [ "${agents_ref#v}" != "$plugin_version" ]; then | |
| echo "FAIL: .agents/plugins/marketplace.json ref '$agents_ref' != v$plugin_version" | |
| fail=1 | |
| fi | |
| fi | |
| # All SKILL.md versions must match plugin.json | |
| for skill in skills/*/SKILL.md; do | |
| skill_version=$(sed -n 's/^version: *//p' "$skill") | |
| if [ "$skill_version" != "$plugin_version" ]; then | |
| echo "FAIL: $skill version '$skill_version' != plugin.json '$plugin_version'" | |
| fail=1 | |
| fi | |
| done | |
| # CHANGELOG must have an entry for this version | |
| if ! grep -q "## \[${plugin_version}\]" CHANGELOG.md; then | |
| echo "FAIL: CHANGELOG.md missing entry for version $plugin_version" | |
| fail=1 | |
| fi | |
| # README badge must match | |
| if ! grep -q "version-${plugin_version}-blue" README.md; then | |
| echo "FAIL: README.md badge doesn't match version $plugin_version" | |
| fail=1 | |
| fi | |
| [ $fail -eq 0 ] && echo "All versions consistent: $plugin_version" | |
| exit $fail | |
| - name: Validate assistant prompt consistency | |
| run: | | |
| # The canonical workflow-creation prompt lives in the README example block. | |
| # test-assistants.sh must BEGIN with it verbatim, so the harness measures the | |
| # same prompt users are actually shown. (Reporting instructions are appended | |
| # at run time, never spliced into this line.) | |
| readme_prompt=$(grep -m1 '^> Generate a Falcon Fusion workflow' README.md | sed 's/^> //') | |
| if [ -z "$readme_prompt" ]; then | |
| echo "FAIL: could not find the canonical prompt in README.md" | |
| exit 1 | |
| fi | |
| harness_prompt=$(sed -n 's/^PROMPT="\(.*\)"$/\1/p' test-assistants.sh | head -1) | |
| if [ -z "$harness_prompt" ]; then | |
| echo "FAIL: could not extract the PROMPT from test-assistants.sh" | |
| exit 1 | |
| fi | |
| case "$harness_prompt" in | |
| "$readme_prompt"*) echo "OK: test-assistants.sh prompt starts with the README canonical prompt" ;; | |
| *) echo "FAIL: test-assistants.sh PROMPT does not start with the README canonical prompt"; exit 1 ;; | |
| esac | |
| markdownlint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version: 22 | |
| - name: Run markdownlint | |
| run: npx markdownlint-cli2 "**/*.md" | |
| - name: Check relative markdown links resolve | |
| # markdownlint checks link SYNTAX, not whether the target exists. This walks | |
| # every relative link in a tracked .md and fails on any that does not resolve, | |
| # so a moved file or a bad ../ path is caught here instead of only by the | |
| # release-time bundle build. Root-relative (/foo) links resolve from the repo | |
| # root, as GitHub renders them; external, mailto, and pure-anchor links are skipped. | |
| run: | | |
| python3 - <<'PY' | |
| import os, re, subprocess, sys | |
| files = subprocess.check_output(["git", "ls-files", "*.md"], text=True).split() | |
| link_re = re.compile(r'\]\(\s*<?([^)>\s]+)') | |
| bad = [] | |
| for f in files: | |
| d = os.path.dirname(f) | |
| with open(f, encoding="utf-8") as fh: | |
| text = fh.read() | |
| for m in link_re.finditer(text): | |
| target = m.group(1) | |
| if target.startswith(("http://", "https://", "mailto:", "tel:", "#")): | |
| continue | |
| path = target.split("#")[0].split("?")[0] | |
| if not path: | |
| continue | |
| resolved = path.lstrip("/") if path.startswith("/") else os.path.join(d, path) | |
| if not os.path.exists(os.path.normpath(resolved)): | |
| bad.append(f"{f}: [{target}] -> {os.path.normpath(resolved)}") | |
| if bad: | |
| print("Broken relative markdown links:") | |
| for b in bad: | |
| print(" " + b) | |
| sys.exit(1) | |
| print(f"OK: all relative markdown links resolve ({len(files)} files checked)") | |
| PY |