chore: migrate from pylint+mypy to ruff+ty #16
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: Claude plugin manifests | |
| on: | |
| pull_request: | |
| paths: | |
| - 'claude/**' | |
| - '.github/workflows/plugin-manifest.yml' | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'claude/**' | |
| permissions: | |
| contents: read | |
| jobs: | |
| validate: | |
| name: Validate plugin manifests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # Full history so the CHANGELOG-diff check below can reach the | |
| # PR base without needing to re-fetch. | |
| fetch-depth: 0 | |
| - name: Check manifests parse and versions match | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' | |
| import json, sys, pathlib | |
| repo = pathlib.Path(".") | |
| marketplace = repo / "claude" / ".claude-plugin" / "marketplace.json" | |
| if not marketplace.exists(): | |
| sys.exit(f"::error::{marketplace} missing") | |
| try: | |
| mk = json.loads(marketplace.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as e: | |
| sys.exit(f"::error file={marketplace}::invalid JSON: {e}") | |
| plugins = mk.get("plugins") or [] | |
| if not plugins: | |
| sys.exit(f"::error file={marketplace}::no plugins listed") | |
| failures = [] | |
| for entry in plugins: | |
| name = entry.get("name") | |
| source = entry.get("source", "") | |
| entry_version = entry.get("version") | |
| if not name or not source or not entry_version: | |
| failures.append(f"{entry}: missing name/source/version") | |
| continue | |
| if not isinstance(source, str) or not source.startswith("./"): | |
| failures.append(f"{name}: source must be a './plugins/...' relative path") | |
| continue | |
| plugin_root = (repo / "claude" / source[2:]).resolve() | |
| if not plugin_root.exists(): | |
| failures.append(f"{name}: source path does not exist: {plugin_root}") | |
| continue | |
| pj = plugin_root / ".claude-plugin" / "plugin.json" | |
| if not pj.exists(): | |
| failures.append(f"{name}: missing {pj}") | |
| continue | |
| try: | |
| pm = json.loads(pj.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as e: | |
| failures.append(f"{name}: {pj} invalid JSON: {e}") | |
| continue | |
| if pm.get("version") != entry_version: | |
| failures.append( | |
| f"{name}: version mismatch — plugin.json={pm.get('version')!r}, " | |
| f"marketplace.json={entry_version!r}" | |
| ) | |
| if pm.get("name") != name: | |
| failures.append( | |
| f"{name}: plugin.json name={pm.get('name')!r} != " | |
| f"marketplace entry name={name!r}" | |
| ) | |
| # .mcp.json must parse if present | |
| mcp = plugin_root / ".mcp.json" | |
| if mcp.exists(): | |
| try: | |
| json.loads(mcp.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as e: | |
| failures.append(f"{name}: {mcp} invalid JSON: {e}") | |
| # hooks.json must parse if present | |
| hooks = plugin_root / "hooks" / "hooks.json" | |
| if hooks.exists(): | |
| try: | |
| json.loads(hooks.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as e: | |
| failures.append(f"{name}: {hooks} invalid JSON: {e}") | |
| # Desktop extension (.mcpb) is keyed to the pywry plugin specifically | |
| desktop_manifest = repo / "claude" / "desktop-extension" / "manifest.json" | |
| pywry_entry = next((p for p in plugins if p.get("name") == "pywry"), None) | |
| if pywry_entry is not None and desktop_manifest.exists(): | |
| try: | |
| dm = json.loads(desktop_manifest.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as e: | |
| failures.append(f"{desktop_manifest}: invalid JSON: {e}") | |
| else: | |
| if dm.get("version") != pywry_entry.get("version"): | |
| failures.append( | |
| f"desktop-extension/manifest.json version " | |
| f"{dm.get('version')!r} != pywry plugin " | |
| f"{pywry_entry.get('version')!r}" | |
| ) | |
| if dm.get("name") != "pywry": | |
| failures.append( | |
| f"desktop-extension/manifest.json name " | |
| f"{dm.get('name')!r} != 'pywry'" | |
| ) | |
| elif pywry_entry is not None and not desktop_manifest.exists(): | |
| failures.append(f"missing {desktop_manifest}") | |
| if failures: | |
| for f in failures: | |
| print(f"::error::{f}") | |
| sys.exit(1) | |
| print(f"ok — {len(plugins)} plugin(s) validated, .mcpb manifest in sync") | |
| PY | |
| - name: Check CHANGELOG was updated when plugin.json version changes | |
| if: github.event_name == 'pull_request' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| base_sha="${{ github.event.pull_request.base.sha }}" | |
| # Two-dot diff: list files that differ between the PR base and | |
| # HEAD without needing a merge-base. fetch-depth: 0 above makes | |
| # both commits available locally. | |
| changed=$(git diff --name-only "$base_sha" HEAD -- 'claude/plugins/*/.claude-plugin/plugin.json') | |
| if [[ -z "$changed" ]]; then | |
| echo "No plugin.json changes — nothing to check." | |
| exit 0 | |
| fi | |
| missing=0 | |
| for pj in $changed; do | |
| plugin_dir=$(dirname "$(dirname "$pj")") | |
| changelog="$plugin_dir/CHANGELOG.md" | |
| if ! git diff --name-only "$base_sha" HEAD -- "$changelog" | grep -q .; then | |
| echo "::error file=$changelog::CHANGELOG.md not updated alongside $pj" | |
| missing=1 | |
| fi | |
| done | |
| exit $missing |