From e71761c36e16dc542226f54084a6b62b51e1d64f Mon Sep 17 00:00:00 2001 From: pratzm Date: Wed, 4 Nov 2026 14:28:54 -0800 Subject: [PATCH] Publish plugin: run the locked Claude Code through cli-wrapper.cjs `npm ci --ignore-scripts` skips the package's postinstall, which copies the native binary over bin/claude.exe; without it `claude` is a stub that exits with "claude native binary not installed", so every publish since v1.1.0 fails at "Validate the marketplace". cli-wrapper.cjs runs the binary from the same locked platform package with no install script. The step moves into scripts/validate-marketplace.sh with a test that runs the real locked CLI against a valid and an invalid marketplace. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish-plugin-test.yml | 7 ++ .github/workflows/reusable-publish-plugin.yml | 7 +- scripts/validate-marketplace.sh | 15 ++++ scripts/validate-marketplace.test.sh | 71 +++++++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 scripts/validate-marketplace.sh create mode 100644 scripts/validate-marketplace.test.sh diff --git a/.github/workflows/publish-plugin-test.yml b/.github/workflows/publish-plugin-test.yml index 037ce18..7bc3eaf 100644 --- a/.github/workflows/publish-plugin-test.yml +++ b/.github/workflows/publish-plugin-test.yml @@ -9,6 +9,9 @@ on: - scripts/publish-plugin.test.sh - scripts/requires-pull-requests.sh - scripts/requires-pull-requests.test.sh + - scripts/validate-marketplace.sh + - scripts/validate-marketplace.test.sh + - tools/claude-code/** - .github/workflows/reusable-publish-plugin.yml - .github/workflows/publish-plugin-test.yml @@ -25,3 +28,7 @@ jobs: persist-credentials: false - run: bash scripts/publish-plugin.test.sh - run: bash scripts/requires-pull-requests.test.sh + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: "22" + - run: bash scripts/validate-marketplace.test.sh diff --git a/.github/workflows/reusable-publish-plugin.yml b/.github/workflows/reusable-publish-plugin.yml index 9da7eaf..0474080 100644 --- a/.github/workflows/reusable-publish-plugin.yml +++ b/.github/workflows/reusable-publish-plugin.yml @@ -175,12 +175,7 @@ jobs: # The marketplace's own release rule: `claude plugin validate .` passes. - name: Validate the marketplace if: steps.copy.outputs.changed == 'true' - run: | - # Locked install (tools/claude-code/package-lock.json), integrity- - # checked; the CLI ships as prebuilt optional packages, no scripts. - (cd tooling/tools/claude-code && npm ci --ignore-scripts --no-audit --no-fund) - export PATH="$PWD/tooling/tools/claude-code/node_modules/.bin:$PATH" - cd marketplace && claude plugin validate . + run: bash tooling/scripts/validate-marketplace.sh tooling/tools/claude-code marketplace - name: Create the publish pull request if: steps.copy.outputs.changed == 'true' && !inputs.dry_run diff --git a/scripts/validate-marketplace.sh b/scripts/validate-marketplace.sh new file mode 100644 index 0000000..3bd5d81 --- /dev/null +++ b/scripts/validate-marketplace.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# ABOUTME: Installs the locked Claude Code CLI and runs `claude plugin validate` on a marketplace checkout. +# ABOUTME: Usage: validate-marketplace.sh ; exits non-zero if validation fails. +set -euo pipefail + +tool_dir=$(cd "$1" && pwd) +marketplace=$2 + +# Locked install (tools/claude-code/package-lock.json), integrity- +# checked; the CLI ships as prebuilt optional packages, no scripts. +(cd "$tool_dir" && npm ci --ignore-scripts --no-audit --no-fund) +# Without its postinstall the package's `claude` bin is a stub that refuses to run; +# cli-wrapper.cjs runs the platform package's binary from the same locked install. +cli="$tool_dir/node_modules/@anthropic-ai/claude-code/cli-wrapper.cjs" +cd "$marketplace" && node "$cli" plugin validate . diff --git a/scripts/validate-marketplace.test.sh b/scripts/validate-marketplace.test.sh new file mode 100644 index 0000000..5bf06c0 --- /dev/null +++ b/scripts/validate-marketplace.test.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# ABOUTME: Tests for validate-marketplace.sh, run against the real locked Claude Code CLI in tools/claude-code. +# ABOUTME: Run with `bash scripts/validate-marketplace.test.sh`; needs Node.js and npm; exits non-zero if any test fails. +set -uo pipefail + +SCRIPT="$(cd "$(dirname "$0")" && pwd)/validate-marketplace.sh" +TOOL="$(cd "$(dirname "$0")/../tools/claude-code" && pwd)" +failures=0 + +check() { + local label=$1; shift + if ! "$@"; then + echo " FAIL: $label" + failures=$((failures + 1)) + fi +} + +run_test() { + local name=$1 + local work + work=$(mktemp -d) + echo "$name" + pushd "$work" > /dev/null || exit 1 + "$name" || failures=$((failures + 1)) + popd > /dev/null || exit 1 + rm -rf "$work" +} + +# A copy of the locked tool, so the install never lands in the checkout. +make_tool() { + mkdir tool + cp "$TOOL/package.json" "$TOOL/package-lock.json" tool/ +} + +make_marketplace() { + mkdir -p marketplace/.claude-plugin marketplace/plugins/example/.claude-plugin + cat > marketplace/.claude-plugin/marketplace.json <<'JSON' +{"name": "example-marketplace", "description": "Example", "owner": {"name": "Example"}, + "plugins": [{"name": "example", "source": "./plugins/example", "description": "Example", "category": "productivity"}]} +JSON + echo '{"name": "example", "version": "1.0.0", "description": "Example", "author": {"name": "Example"}}' \ + > marketplace/plugins/example/.claude-plugin/plugin.json +} + +test_a_valid_marketplace_passes() { + make_tool + make_marketplace + local out + if ! out=$(bash "$SCRIPT" tool marketplace 2>&1); then echo "$out"; echo " FAIL: script failed"; return 1; fi + check "reports validation passed" grep -q "Validation passed" <<<"$out" +} + +test_an_invalid_marketplace_is_refused() { + make_tool + make_marketplace + echo '{"name": "example-marketplace", "owner": {"name": "Example"}, "plugins": "not a list"}' \ + > marketplace/.claude-plugin/marketplace.json + local out + if out=$(bash "$SCRIPT" tool marketplace 2>&1); then echo " FAIL: script succeeded"; return 1; fi + check "names the invalid field" grep -q "plugins: Invalid input" <<<"$out" +} + +for t in $(declare -F | awk '{print $3}' | grep '^test_'); do + run_test "$t" +done + +if [ "$failures" -gt 0 ]; then + echo "$failures failure(s)" + exit 1 +fi +echo "all passed"