feat(marketplace): advanced plugin search with FTS, filters, and pagi… #372
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: Validate | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| release: | |
| types: [published] | |
| concurrency: | |
| group: validate-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate JSON manifests | |
| run: | | |
| echo "Checking marketplace.json..." | |
| python3 -c "import json; json.load(open('.claude-plugin/marketplace.json'))" | |
| echo "Checking plugin manifests..." | |
| for manifest in plugins/*/.claude-plugin/plugin.json; do | |
| echo " $manifest" | |
| python3 -c "import json; json.load(open('$manifest'))" | |
| done | |
| - name: Check version alignment | |
| run: | | |
| python3 -c " | |
| import json, sys | |
| marketplace = json.load(open('.claude-plugin/marketplace.json')) | |
| errors = [] | |
| for plugin in marketplace['plugins']: | |
| name = plugin['name'] | |
| mp_version = plugin['version'] | |
| manifest_path = f\"plugins/{name}/.claude-plugin/plugin.json\" | |
| try: | |
| manifest = json.load(open(manifest_path)) | |
| except FileNotFoundError: | |
| errors.append(f'{name}: plugin.json not found at {manifest_path}') | |
| continue | |
| pj_version = manifest.get('version', '(missing)') | |
| if mp_version != pj_version: | |
| errors.append(f'{name}: marketplace.json says {mp_version}, plugin.json says {pj_version}') | |
| if errors: | |
| print('Version mismatch:') | |
| for e in errors: | |
| print(f' {e}') | |
| sys.exit(1) | |
| else: | |
| for plugin in marketplace['plugins']: | |
| print(f\"{plugin['name']}: v{plugin['version']}\") | |
| print('All versions aligned.') | |
| " | |
| - name: Check all plugins registered | |
| run: | | |
| python3 -c " | |
| import json, sys, os, glob | |
| marketplace = json.load(open('.claude-plugin/marketplace.json')) | |
| registered = {p['name'] for p in marketplace['plugins']} | |
| on_disk = {os.path.basename(d) for d in glob.glob('plugins/*') if os.path.isdir(d)} | |
| errors = [] | |
| for name in sorted(on_disk - registered): | |
| errors.append(f'{name}: directory exists but not registered in marketplace.json') | |
| for name in sorted(registered - on_disk): | |
| errors.append(f'{name}: registered in marketplace.json but directory not found') | |
| if errors: | |
| print('Plugin registration mismatch:') | |
| for e in errors: | |
| print(f' {e}') | |
| sys.exit(1) | |
| else: | |
| print(f'All {len(on_disk)} plugins registered: {sorted(on_disk)}') | |
| " | |
| - name: Validate x-developed-with field | |
| run: | | |
| python3 -c " | |
| import json, sys, glob | |
| errors = [] | |
| for manifest_path in glob.glob('plugins/*/.claude-plugin/plugin.json'): | |
| manifest = json.load(open(manifest_path)) | |
| dw = manifest.get('x-developed-with') | |
| if dw is not None: | |
| if not isinstance(dw, str) or not dw.strip(): | |
| errors.append(f'{manifest_path}: x-developed-with must be non-empty string if present, got {dw!r}') | |
| if errors: | |
| print('x-developed-with validation errors:') | |
| for e in errors: | |
| print(f' {e}') | |
| sys.exit(1) | |
| else: | |
| print('x-developed-with field valid (optional).') | |
| " | |
| - name: Validate requires.env schema | |
| run: | | |
| python3 -c " | |
| import json, sys, glob | |
| errors = [] | |
| for manifest_path in glob.glob('plugins/*/.claude-plugin/plugin.json'): | |
| manifest = json.load(open(manifest_path)) | |
| requires = manifest.get('requires', {}) | |
| env_list = requires.get('env', []) | |
| for i, entry in enumerate(env_list): | |
| prefix = f'{manifest_path} requires.env[{i}]' | |
| if not isinstance(entry.get('name'), str) or not entry['name']: | |
| errors.append(f'{prefix}: missing or invalid \"name\" (must be non-empty string)') | |
| if not isinstance(entry.get('description'), str) or not entry['description']: | |
| errors.append(f'{prefix}: missing or invalid \"description\" (must be non-empty string)') | |
| if 'required' in entry and not isinstance(entry['required'], bool): | |
| errors.append(f'{prefix}: \"required\" must be boolean if present') | |
| if 'sensitive' in entry and not isinstance(entry['sensitive'], bool): | |
| errors.append(f'{prefix}: \"sensitive\" must be boolean if present') | |
| if errors: | |
| print('requires.env schema errors:') | |
| for e in errors: | |
| print(f' {e}') | |
| sys.exit(1) | |
| else: | |
| print('requires.env schema valid.') | |
| " | |
| - name: Validate plugin.json against Protocol schema | |
| # Schema is bundled at .github/schema/plugin.schema.json — sync from harness-protocol | |
| # repo (schema/draft/plugin.schema.json) when the upstream schema changes. | |
| run: | | |
| pip install -q 'jsonschema>=4.18,<5' || { | |
| echo "::error::Failed to install jsonschema" | |
| exit 1 | |
| } | |
| python3 -c " | |
| import json, sys, glob | |
| from jsonschema import Draft202012Validator | |
| schema = json.load(open('.github/schema/plugin.schema.json')) | |
| validator = Draft202012Validator(schema) | |
| errors = [] | |
| for path in sorted(glob.glob('plugins/*/.claude-plugin/plugin.json')): | |
| name = path.split('/')[1] | |
| manifest = json.load(open(path)) | |
| for err in validator.iter_errors(manifest): | |
| loc = ' → '.join(str(p) for p in err.absolute_path) or '(root)' | |
| errors.append(f'{name}: [{loc}] {err.message}') | |
| print(f'::error file={path}::{err.message}') | |
| if errors: | |
| print(f'\nSchema validation failed ({len(errors)} error(s)):') | |
| for e in errors: | |
| print(f' {e}') | |
| sys.exit(1) | |
| else: | |
| print('All plugin.json files valid against Protocol schema.') | |
| " | |
| security-scan: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build CLI | |
| run: | | |
| pnpm --filter @harness-kit/shared build | |
| pnpm --filter @harness-kit/core build | |
| pnpm --filter @harness-kit/cli build | |
| - name: Scan all plugins | |
| id: scan | |
| run: | | |
| FAILED=0 | |
| SUMMARY="" | |
| for plugin_dir in plugins/*/; do | |
| plugin_name=$(basename "$plugin_dir") | |
| # Validate plugin_name to prevent markdown injection in $GITHUB_STEP_SUMMARY | |
| if ! echo "$plugin_name" | grep -qE '^[a-zA-Z0-9_-]+$'; then | |
| echo "::warning::Skipping plugin with unexpected name: $plugin_name" | |
| continue | |
| fi | |
| # Run scan; exit 1 = critical findings, exit 0 = passed or warnings | |
| if output=$(node apps/cli/dist/index.js scan "$plugin_dir" 2>&1); then | |
| status="passed" | |
| else | |
| status="failed" | |
| FAILED=1 | |
| fi | |
| # Extract counts from formatter output (titles: "Critical Issues (N)", "Warnings (N)") | |
| # Validate extracted values are integers before interpolating into markdown | |
| raw_critical=$(echo "$output" | grep -oP 'Critical Issues \(\K[0-9]+' || true) | |
| raw_warnings=$(echo "$output" | grep -oP 'Warnings \(\K[0-9]+' || true) | |
| critical=$(echo "${raw_critical:-0}" | grep -oE '^[0-9]+$' || echo "0") | |
| warnings=$(echo "${raw_warnings:-0}" | grep -oE '^[0-9]+$' || echo "0") | |
| SUMMARY="${SUMMARY}\n| ${plugin_name} | ${status} | ${critical} | ${warnings} |" | |
| echo "--- ${plugin_name}: ${status} (critical=${critical}, warnings=${warnings}) ---" | |
| done | |
| # Write summary table to job summary | |
| { | |
| echo "## Security Scan Results" | |
| echo "" | |
| echo "| Plugin | Status | Critical | Warnings |" | |
| echo "| ------ | ------ | -------- | -------- |" | |
| echo -e "$SUMMARY" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$FAILED" -eq 1 ]; then | |
| echo "::error::One or more plugins have critical security findings. Review the scan output above." | |
| exit 1 | |
| fi | |
| test-all: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm -r build | |
| - name: Run all tests | |
| run: pnpm test:all | |
| core-build-test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Run dependency audit | |
| run: pnpm audit --audit-level=critical | |
| - name: Build shared package (core dependency) | |
| run: pnpm --filter @harness-kit/shared build | |
| - name: Build core package | |
| run: pnpm --filter @harness-kit/core build | |
| - name: Run core tests | |
| run: pnpm --filter @harness-kit/core test | |
| - name: TypeScript check (shared) | |
| run: pnpm --filter @harness-kit/shared exec tsc --noEmit | |
| - name: Build CLI package | |
| run: pnpm --filter @harness-kit/cli build | |
| - name: TypeScript check (cli) | |
| run: pnpm --filter @harness-kit/cli exec tsc --noEmit | |
| desktop-build-test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install system dependencies (Tauri v2) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: apps/desktop/src-tauri | |
| - name: Rust check (desktop backend) | |
| run: cargo check | |
| working-directory: apps/desktop/src-tauri | |
| - name: Rust clippy (desktop backend) | |
| run: cargo clippy -- -D warnings | |
| working-directory: apps/desktop/src-tauri | |
| - name: Rust test (desktop backend) | |
| run: cargo test | |
| working-directory: apps/desktop/src-tauri | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build workspace dependencies | |
| run: | | |
| pnpm --filter @harness-kit/shared build | |
| pnpm --filter @harness-kit/core build | |
| - name: TypeScript check (desktop) | |
| run: pnpm --filter harness-kit-desktop exec tsc --noEmit | |
| - name: Run desktop tests | |
| run: pnpm --filter harness-kit-desktop test | |
| - name: Build board-server (mirrors tauri beforeBuildCommand) | |
| run: pnpm --filter board-server build | |
| - name: Build desktop frontend | |
| run: pnpm --filter harness-kit-desktop exec vite build | |
| board-build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build board-server | |
| run: pnpm --filter board-server build | |
| - name: Start board-server (smoke test) | |
| run: node packages/board-server/dist/index.js & | |
| working-directory: . | |
| - name: Wait for board-server to be ready | |
| run: | | |
| for i in $(seq 1 10); do | |
| if curl -sf http://localhost:4800/health; then | |
| echo "board-server is up" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "board-server did not start in time" | |
| exit 1 | |
| - name: Kill board-server | |
| if: always() | |
| run: kill $(lsof -ti:4800) 2>/dev/null || true | |
| - name: Build board app | |
| run: pnpm --filter harness-board build | |
| docs-build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| package_json_file: website/package.json | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: website/pnpm-lock.yaml | |
| - name: Install dependencies | |
| working-directory: website | |
| run: pnpm install --frozen-lockfile | |
| - name: Build docs | |
| working-directory: website | |
| run: pnpm run build |