-
-
Notifications
You must be signed in to change notification settings - Fork 5
149 lines (139 loc) · 6.06 KB
/
plugin-manifest.yml
File metadata and controls
149 lines (139 loc) · 6.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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