-
Notifications
You must be signed in to change notification settings - Fork 1
425 lines (371 loc) · 15.4 KB
/
Copy pathci.yml
File metadata and controls
425 lines (371 loc) · 15.4 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: src/go.mod
cache-dependency-path: src/go.sum
- name: Build
run: go build ./...
- name: Vet
run: go vet ./...
- name: Test
run: go test ./... -v -race -count=1
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "Files not formatted:"
echo "$unformatted"
exit 1
fi
# Windows gets binaries at release time (engine cross-compile + the
# mcpb launcher), but nothing validated Windows builds on PR — breakage
# only surfaced during the release pipeline. This job fails fast.
build-and-test-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: src/go.mod
cache-dependency-path: src/go.sum
- name: Build engine
working-directory: src
run: go build ./...
- name: Test engine
working-directory: src
run: go test ./... -count=1
- name: Build MCPB launcher (native Windows)
working-directory: mcpb/launcher
run: go build ./...
- name: Test MCPB launcher
working-directory: mcpb/launcher
run: go test ./... -count=1
# The fresh-install job shellchecks the shipped wrappers; this covers
# the hook layer, which runs on every tool call in every session.
lint-shell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: shellcheck hooks and wrappers
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
shellcheck -S warning hooks/*.sh bin/devkit bin/mcpb-build mcpb/server/devkit
lint-actions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Run the tool author's image directly rather than a third-party
# wrapper action — one less link in the supply chain.
- name: actionlint
run: docker run --rm -v "$PWD:/repo" --workdir /repo rhysd/actionlint:latest -color
hook-smoke-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The devkit-guard shell wrappers exec the Go engine binary.
# Provide Go so hooks_test.sh can build it on first run (the
# script auto-builds if $CLAUDE_PLUGIN_ROOT/bin/devkit-engine is
# missing) — without this, every "expected exit 2" fixture would
# silently fall through the no-binary path and fail.
- uses: actions/setup-go@v7
with:
go-version-file: src/go.mod
cache-dependency-path: src/go.sum
- name: Run hook smoke tests
run: bash hooks/hooks_test.sh
# Catches the class of bug where the shipped marketplace tree is
# missing something the plugin needs at runtime. PR #52 shipped a
# plugin.json pointing at bin/devkit, but bin/ was gitignored — the
# plugin installed but the MCP server silently failed to start.
# This job runs in a clean dir with no local build artifacts.
fresh-install-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Bootstrap wrapper must exist and be executable
run: |
test -f bin/devkit || { echo "FATAL: bin/devkit wrapper missing from repo"; exit 1; }
test -x bin/devkit || { echo "FATAL: bin/devkit not executable"; exit 1; }
- name: Wrapper must not contain devkit-engine binary (that is gitignored)
run: |
if [ -e bin/devkit-engine ]; then
echo "FATAL: bin/devkit-engine should not be tracked in git (it is the cached binary)"
exit 1
fi
- name: plugin.json mcpServers must point at MCPB bundle
run: |
ref=$(jq -r '.mcpServers' .claude-plugin/plugin.json)
expected='./devkit.mcpb'
if [ "$ref" != "$expected" ]; then
echo "FATAL: plugin.json mcpServers is '$ref', expected '$expected'"
exit 1
fi
- name: MCPB bundle must exist and contain launcher stubs
run: |
test -f devkit.mcpb || { echo "FATAL: devkit.mcpb bundle missing from repo root"; exit 1; }
entries=$(unzip -Z1 devkit.mcpb)
for required in manifest.json server/devkit server/devkit.exe; do
if ! printf '%s\n' "$entries" | grep -qx "$required"; then
echo "FATAL: devkit.mcpb missing required entry '$required'"
printf 'bundle contents:\n%s\n' "$entries"
exit 1
fi
done
unzip -p devkit.mcpb manifest.json | jq -e \
'.server.mcp_config.platform_overrides.win32.command == "${__dirname}/server/devkit.exe"' \
> /dev/null || {
echo "FATAL: mcpb manifest.json platform_overrides.win32.command is not the expected Windows launcher"
exit 1
}
- name: Bundled server/devkit.exe must be a real PE binary
run: |
magic=$(unzip -p devkit.mcpb server/devkit.exe | head -c 2 | xxd -p)
if [ "$magic" != "4d5a" ]; then
echo "FATAL: server/devkit.exe in bundle is not a PE binary (MZ header missing; got '$magic')"
echo "Did a probe stub or wrong-architecture binary get committed?"
exit 1
fi
- name: Bundled server/devkit must have a shell shebang
run: |
first=$(unzip -p devkit.mcpb server/devkit | head -c 2)
if [ "$first" != "#!" ]; then
echo "FATAL: server/devkit in bundle has no shebang; got '$first'"
exit 1
fi
- name: shellcheck wrappers
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
shellcheck bin/devkit bin/mcpb-build mcpb/server/devkit
- name: Wrapper --help must exit with a download attempt (no local engine)
run: |
# With no local bin/devkit-engine and no network cache, the wrapper
# should try to download and either succeed or fail loudly. It must
# NOT silently pass with exit 0 and no output.
set +e
output=$(./bin/devkit --version 2>&1)
exit_code=$?
set -e
echo "wrapper exit=$exit_code"
echo "wrapper output: $output"
# The wrapper must either succeed (downloaded + ran --version) or
# emit a clear error. An empty output with exit 0 would be the silent
# failure class we're guarding against.
if [ $exit_code -eq 0 ] && [ -z "$output" ]; then
echo "FATAL: wrapper exited 0 with no output — silent failure class bug"
exit 1
fi
mcpb-launcher-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: mcpb/launcher
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: mcpb/launcher/go.mod
- name: Vet
run: go vet ./...
- name: Test
run: go test ./... -v -count=1
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "Files not formatted:"
echo "$unformatted"
exit 1
fi
# Guards against the class of bug where mcpb/launcher/main.go,
# mcpb/manifest.json, or mcpb/server/devkit are edited but devkit.mcpb is
# not rebuilt — CI would otherwise greenlight a shipped bundle with stale
# runtime behavior.
#
# devkit.mcpb.sources.json is a sidecar manifest that records the sha256
# of each source file at bundle-build time. This job re-computes each
# hash and compares. We don't byte-compare the cross-compiled .exe
# because Go cross-builds aren't byte-identical across host OSes even
# with -trimpath (linker build ID leaks host state) — the sidecar is
# the portable equivalent.
mcpb-bundle-integrity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Sources sidecar must exist
run: |
test -f devkit.mcpb.sources.json || {
echo "FATAL: devkit.mcpb.sources.json is missing; did you rebuild the bundle with bin/mcpb-build?"
exit 1
}
- name: Source file hashes must match sidecar
run: |
set -eo pipefail
stale=0
# Read the tracked file list from the sidecar itself so there's
# one source of truth — bin/mcpb-build defines what's tracked,
# CI re-verifies each entry.
files=$(jq -r 'keys[]' devkit.mcpb.sources.json)
if [ -z "$files" ]; then
echo "FATAL: devkit.mcpb.sources.json is empty or invalid"
exit 1
fi
while IFS= read -r file; do
want=$(jq -r --arg f "$file" '.[$f]' devkit.mcpb.sources.json)
if [ ! -f "$file" ]; then
echo "FATAL: sidecar references $file but the file is missing"
stale=1
continue
fi
have=$(sha256sum "$file" | awk '{print $1}')
if [ "$want" != "$have" ]; then
echo "FATAL: $file has been edited since devkit.mcpb was built"
echo " sidecar sha256: $want"
echo " current sha256: $have"
stale=1
fi
done <<< "$files"
if [ "$stale" != "0" ]; then
echo ""
echo "Rebuild with: bin/mcpb-build"
exit 1
fi
- name: Bundled manifest.json must match source
run: |
unzip -p devkit.mcpb manifest.json > /tmp/bundled-manifest.json
if ! diff -u mcpb/manifest.json /tmp/bundled-manifest.json; then
echo "FATAL: devkit.mcpb manifest.json has drifted from mcpb/manifest.json"
exit 1
fi
- name: Bundled server/devkit must match source
run: |
unzip -p devkit.mcpb server/devkit > /tmp/bundled-proxy.sh
if ! diff -u mcpb/server/devkit /tmp/bundled-proxy.sh; then
echo "FATAL: devkit.mcpb server/devkit has drifted from mcpb/server/devkit"
exit 1
fi
validate-counts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Validate README and ROADMAP counts
run: |
set -uo pipefail
ERRORS=""
CHECKS=0
# Verify directories exist
for dir in commands skills workflows hooks; do
if [ ! -d "$dir" ]; then
echo "FATAL: $dir/ directory missing"
exit 1
fi
done
# Count actual files
CMD_COUNT=$(set -- commands/*.md; echo $#)
SKILL_COUNT=$(find skills -maxdepth 2 -name 'SKILL.md' | wc -l | tr -d ' ')
WORKFLOW_COUNT=$(set -- workflows/*.yml; echo $#)
HOOK_COUNT=$(python3 -c "
import sys, json
data = json.load(open('hooks/hooks.json'))
scripts = set()
for event in data.get('hooks', {}).values():
for entry in event:
for h in entry.get('hooks', []):
cmd = h.get('command', '')
scripts.add(cmd.split('/')[-1])
print(len(scripts))
") || { echo "FATAL: failed to parse hooks/hooks.json"; exit 1; }
# Validate counts are non-zero
for var_name in CMD_COUNT SKILL_COUNT WORKFLOW_COUNT HOOK_COUNT; do
val=$(eval echo \$$var_name)
if [ -z "$val" ] || [ "$val" = "0" ]; then
echo "FATAL: $var_name is empty or zero ($val)"
exit 1
fi
done
echo "Actual counts: commands=$CMD_COUNT skills=$SKILL_COUNT workflows=$WORKFLOW_COUNT hooks=$HOOK_COUNT"
# Check README and ROADMAP
for file in README.md ROADMAP.md; do
if [ ! -f "$file" ]; then continue; fi
# Commands
if grep -qE "\*\*[0-9]+ slash commands\*\*" "$file"; then
DOC_CMD=$(grep -m1 -oE '\*\*[0-9]+ slash commands\*\*' "$file" | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$DOC_CMD" != "$CMD_COUNT" ]; then
ERRORS="$ERRORS\n$file: says $DOC_CMD commands, actual is $CMD_COUNT"
fi
fi
# Skills
if grep -qE "\*\*[0-9]+ context-activated skills\*\*" "$file"; then
DOC_SKILL=$(grep -m1 -oE '\*\*[0-9]+ context-activated skills\*\*' "$file" | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$DOC_SKILL" != "$SKILL_COUNT" ]; then
ERRORS="$ERRORS\n$file: says $DOC_SKILL skills, actual is $SKILL_COUNT"
fi
fi
# Workflows
if grep -qE "\*\*[0-9]+ YAML workflows\*\*" "$file"; then
DOC_WF=$(grep -m1 -oE '\*\*[0-9]+ YAML workflows\*\*' "$file" | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$DOC_WF" != "$WORKFLOW_COUNT" ]; then
ERRORS="$ERRORS\n$file: says $DOC_WF workflows, actual is $WORKFLOW_COUNT"
fi
fi
# Hooks
if grep -qE "\*\*[0-9]+ hooks\*\*" "$file"; then
DOC_HOOK=$(grep -m1 -oE '\*\*[0-9]+ hooks\*\*' "$file" | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$DOC_HOOK" != "$HOOK_COUNT" ]; then
ERRORS="$ERRORS\n$file: says $DOC_HOOK hooks, actual is $HOOK_COUNT"
fi
fi
done
# Check README repo structure comments
if grep -qE '# [0-9]+ slash commands' README.md; then
TREE_CMD=$(grep -m1 -oE '# [0-9]+ slash commands' README.md | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$TREE_CMD" != "$CMD_COUNT" ]; then
ERRORS="$ERRORS\nREADME.md tree: says $TREE_CMD commands, actual is $CMD_COUNT"
fi
fi
if grep -qE '# [0-9]+ context-activated skills' README.md; then
TREE_SKILL=$(grep -m1 -oE '# [0-9]+ context-activated skills' README.md | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$TREE_SKILL" != "$SKILL_COUNT" ]; then
ERRORS="$ERRORS\nREADME.md tree: says $TREE_SKILL skills, actual is $SKILL_COUNT"
fi
fi
if grep -qE '# [0-9]+ YAML workflow' README.md; then
TREE_WF=$(grep -m1 -oE '# [0-9]+ YAML workflow' README.md | grep -oE '[0-9]+' | head -1)
CHECKS=$((CHECKS + 1))
if [ "$TREE_WF" != "$WORKFLOW_COUNT" ]; then
ERRORS="$ERRORS\nREADME.md tree: says $TREE_WF workflows, actual is $WORKFLOW_COUNT"
fi
fi
# Ensure at least some counts were validated
if [ "$CHECKS" -lt 1 ]; then
echo "FATAL: no count patterns found in README.md or ROADMAP.md — expected at least one"
exit 1
fi
if [ -n "$ERRORS" ]; then
echo ""
echo "COUNT MISMATCHES FOUND:"
printf '%b\n' "$ERRORS"
echo ""
echo "Fix the counts in README.md and/or ROADMAP.md to match actual file counts."
exit 1
else
echo "All counts match ($CHECKS checks passed)."
fi