ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh

130 lines
4.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Step 6 — Standalone validation harness (SC2 + SC7).
# For each target: ensures the extract is prepped (extract -> rehome -> fix-references, all idempotent),
# copies it to a clean room /tmp/claude-<key> (no marketplace parent), then:
# SC2: runs the runner the plugin itself declares in plugin-map.json (test_cmd) — always the glob form
# `node --test 'tests/**/*.test.mjs'` or `node --test <dir>/*.test.mjs`, NEVER a bare dir (Node 25 gotcha),
# or `bash tests/validate-plugin.sh` / `bash validate-plugin.generic.sh <key>` for the test-less plugins.
# linkedin-studio is two-tier (M11): the .mjs core is the HARD gate; its TS analytics suite is
# reported as ADVISORY (npm/network — not a clean-room gate).
# SC7: no tracked STATE.md / *.local.md (except okr/templates/okr.local.md.template), no ../../README.md,
# .gitignore ignores .claude/.
# voyage: .forgejo/ISSUE_TEMPLATE/ survives the extraction and holds no monorepo-relative path (M17).
# Emits a per-repo PASS/FAIL table; exits non-zero if any target FAILs (escalate — never mask). NULL push (D8).
#
# Usage: 40-validate-standalone.sh <target-key>
# 40-validate-standalone.sh --all
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
GENERIC_VALIDATOR="$SCRIPT_DIR/templates/validate-plugin.generic.sh"
WORK="${WORK:-/tmp/polyrepo-migration}"
[ -f "$MAP" ] || { echo "plugin-map.json missing at $MAP" >&2; exit 1; }
ARG="${1:-}"
[ -n "$ARG" ] || { echo "usage: 40-validate-standalone.sh <target-key>|--all" >&2; exit 2; }
if [ "$ARG" = "--all" ]; then
KEYS="$(python3 -c "import json; print('\n'.join(sorted(json.load(open('$MAP'))['targets'])))")"
else
KEYS="$ARG"
fi
FAILS=0
prep_target() {
local key="$1"
local dest="$WORK/$key"
if [ ! -d "$dest/.git" ]; then
WORK="$WORK" bash "$SCRIPT_DIR/10-extract.sh" "$key" >/dev/null || return 1
fi
WORK="$WORK" bash "$SCRIPT_DIR/20-rehome-config.sh" "$key" >/dev/null || return 1
WORK="$WORK" node "$SCRIPT_DIR/30-fix-references.mjs" "$key" >/dev/null || return 1
return 0
}
validate_target() {
local key="$1"
local dest="$WORK/$key"
local cr="/tmp/claude-$key"
local problems=""
rm -rf "$cr"
cp -R "$dest" "$cr"
# --- SC7: no tracked STATE.md / *.local.md (except the okr template) ---
local leaks
leaks="$(git -C "$cr" ls-files | grep -E 'STATE\.md|\.local\.md$' | grep -v 'templates/okr\.local\.md\.template' || true)"
[ -z "$leaks" ] || problems="$problems; tracked state-file leak: $(echo "$leaks" | tr '\n' ' ')"
# --- SC7: no ../../README.md remains ---
if grep -rn '\.\./\.\./README\.md' "$cr" --include='*.md' >/dev/null 2>&1; then
problems="$problems; ../../README.md reference remains"
fi
# --- SC7: .gitignore ignores .claude/ ---
if ! git -C "$cr" check-ignore .claude/x >/dev/null 2>&1; then
problems="$problems; .gitignore does not ignore .claude/"
fi
# --- voyage: .forgejo survival (M17) ---
if [ "$key" = "voyage" ]; then
if [ ! -d "$cr/.forgejo/ISSUE_TEMPLATE" ]; then
problems="$problems; .forgejo/ISSUE_TEMPLATE missing"
elif grep -rn '\.\./\.\.' "$cr/.forgejo" >/dev/null 2>&1; then
problems="$problems; .forgejo holds a monorepo-relative path"
fi
fi
# --- SC2: run the plugin's declared runner in the clean room ---
local test_cmd advisory
test_cmd="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd',''))")"
advisory="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd_advisory',''))")"
# The generic structure validator (Step 7) is referenced by test-less plugins — vendor it into the clean room.
case "$test_cmd" in
*validate-plugin.generic.sh*)
if [ -f "$GENERIC_VALIDATOR" ]; then cp "$GENERIC_VALIDATOR" "$cr/validate-plugin.generic.sh"; fi
;;
esac
local sc2_label="standalone-safe"
local out status tests
out="$(cd "$cr" && eval "$test_cmd" 2>&1)"; status=$?
if [ $status -ne 0 ]; then
problems="$problems; SC2 runner failed (exit $status)"
fi
case "$test_cmd" in
*node\ --test*)
# Node 25's default reporter prints " tests N"; older/TAP prints "# tests N". Match either.
tests="$(printf '%s\n' "$out" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)"
[ -n "$tests" ] && sc2_label="$tests tests, standalone-safe"
;;
*validate-plugin*) sc2_label="structure, standalone-safe" ;;
esac
if [ -n "$problems" ]; then
echo "$key: FAIL${problems}"
FAILS=$((FAILS+1))
else
echo "$key: PASS ($sc2_label)"
[ -n "$advisory" ] && echo " advisory (not a clean-room gate): $advisory"
fi
}
for key in $KEYS; do
if ! prep_target "$key"; then
echo "$key: FAIL (prep/extract error)"
FAILS=$((FAILS+1))
continue
fi
validate_target "$key"
done
if [ "$FAILS" -ne 0 ]; then
echo "VALIDATE: $FAILS target(s) FAILED"
exit 1
fi
echo "VALIDATE OK"