bump to v2.1.24 #348
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: 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@v4 | |
| - uses: actions/setup-go@v5 | |
| 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 | |
| hook-smoke-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # 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@v5 | |
| 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@v4 | |
| - 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@v4 | |
| - uses: actions/setup-go@v5 | |
| 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@v4 | |
| - 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@v4 | |
| - 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 |