Skip to content

feat(marketplace): advanced plugin search with FTS, filters, and pagi… #372

feat(marketplace): advanced plugin search with FTS, filters, and pagi…

feat(marketplace): advanced plugin search with FTS, filters, and pagi… #372

Workflow file for this run

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