ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/00-preflight.sh
Kjell Tore Guttormsen fef4b33c97 fix(migration): remediate 6 MAJOR + 3 MINOR trekreview findings + stale rename test
MAJOR
- 9e97cd5 40-validate-standalone.sh: route a target's sc2_gate to its dedicated
  gate (config-audit → 50-config-audit-sc2.sh), mirroring 99-dryrun.sh, so --all
  no longer falsely FAILs config-audit on the machine-locked v5.0.0 tests.
- 1708e90 99-dryrun.sh: assert EXACTLY one tag survives (F5); a partial tag-strip
  no longer silently reports the wrong tag via head -1.
- 4e494c8 99-dryrun.sh: capture the SC2 standalone failing set from the dry-run's
  own prepped extract ($dest), not the 40-validate side-effect clean room.
- aeb6292 00-preflight.sh: assert every map path is whitespace/glob-free, making
  the word-split path handling in 99-dryrun.sh sound.
- 5d112cb extract the SC6 DROP + SC2 regression detectors into sc6-check.sh /
  sc2-regression.sh and add sc-checks.test.mjs — a negative test proving each
  detector FIRES (force-fresh re-extraction would undo a planted file-drop).
- 9e588ca 10-extract.sh re-asserts git filter-repo before use (self-heal runs
  preflight only on a missing mirror); RUNBOOK lists git-filter-repo + python3>=3.6.

MINOR
- bc0f8a7 plugin-map.json: reset ms-ai-architect blob_strip_safe to null
  (00-preflight.sh populates it per run).
- 8d649e9 99-dryrun.sh: gate SC6 behind extract success; a failed extract is
  labelled (extract failed), not a content DROP.
- 4044c49 99-dryrun.sh: guard mktemp — an empty capture is an error, not a
  false zero-regression PASS.

Also: 00-preflight.test.mjs asserted all 3 'renamed' plugins carry >=2 paths, but
llm-security became single-path in 836b8e9 (copilot was a coexisting plugin, not a
rename) — a stale pre-existing failure. Aligned the test to the ratified map and
added a positive single-path lock against re-introducing the 87-file-drop defect.

Verified: full dry-run 11/11, 0 pushes; sc-checks/99-dryrun/40-validate/00-preflight/
60-rewrite suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:17:12 +02:00

149 lines
6.7 KiB
Bash

#!/usr/bin/env bash
# Step 1 — Preflight, archive tag, and the plugin map.
# Asserts tooling + repo hygiene, builds the dedicated extraction mirror ($WORK/_mirror)
# via `git clone --no-local` (R4/M6 — extraction never reads the working checkout),
# enumerates >1MB blob-strip candidates in ms-ai-architect and emits blob_strip_safe
# into plugin-map.json (M4/F3), and creates the local archive tag (D2).
# NULL push (D8): never pushes, never mutates plugins/ shared/ marketplace.json.
#
# Usage: 00-preflight.sh [--dry-run]
# --dry-run : validate tooling + map + print the 11-target table; NO mirror, NO tag, NO map mutation.
#
# Bash 3.2 compatible (no associative arrays / mapfile). Override WORK= to relocate the workspace.
set -euo pipefail
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*) echo "preflight: unknown arg: $arg" >&2; exit 2 ;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
WORK="${WORK:-/tmp/polyrepo-migration}"
MIRROR="$WORK/_mirror"
ARCHIVE_TAG="pre-polyrepo-archive"
BASELINE="f2d41c8"
fail() { printf 'PREFLIGHT FAIL: %s\n' "$*" >&2; exit 1; }
# --- tooling ---
git filter-repo --version >/dev/null 2>&1 || fail "git filter-repo not available — brew install git-filter-repo"
command -v python3 >/dev/null 2>&1 || fail "python3 not found"
python3 -c 'import sys; sys.exit(0 if sys.version_info[:2] >= (3, 6) else 1)' \
|| fail "python3 >= 3.6 required"
# --- map presence + parse + count ---
[ -f "$MAP" ] || fail "plugin-map.json missing at $MAP"
python3 -c "import json; json.load(open('$MAP'))" >/dev/null 2>&1 || fail "plugin-map.json does not parse"
TARGET_COUNT="$(python3 -c "import json; print(len(json.load(open('$MAP'))['targets']))")"
[ "$TARGET_COUNT" = "11" ] || fail "expected 11 targets in plugin-map.json, found $TARGET_COUNT"
# --- path hygiene (aeb6292): every target path must be whitespace- and glob-free, so the space-joined
# `for p in $(mappaths …)` word-splitting in 99-dryrun.sh (live_files / SC6 baseline) is sound. ---
python3 - "$MAP" <<'PY' || fail "plugin-map.json has a path with whitespace or a glob metacharacter (breaks word-split path handling in 99-dryrun.sh)"
import json, re, sys
m = json.load(open(sys.argv[1]))
bad = []
for k, t in m["targets"].items():
for p in t.get("paths", []):
if re.search(r"\s", p) or any(c in p for c in "*?[]"):
bad.append("%s: %r" % (k, p))
if bad:
sys.stderr.write("offending paths:\n " + "\n ".join(bad) + "\n")
sys.exit(1)
PY
# --- repo baseline (HEAD descends from the ratified brief commit, on main, not behind remote) ---
BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
[ "$BRANCH" = "main" ] || fail "not on main (HEAD on '$BRANCH')"
git -C "$REPO_ROOT" merge-base --is-ancestor "$BASELINE" HEAD \
|| fail "HEAD does not descend from baseline $BASELINE"
if git -C "$REPO_ROOT" rev-parse --verify -q origin/main >/dev/null; then
BEHIND="$(git -C "$REPO_ROOT" rev-list --count HEAD..origin/main)"
[ "$BEHIND" = "0" ] || fail "behind origin/main by $BEHIND commit(s) — pull/rebase before migrating"
AHEAD="$(git -C "$REPO_ROOT" rev-list --count origin/main..HEAD)"
echo " baseline: descends $BASELINE on main; $AHEAD local commit(s) ahead of origin/main (unpushed — expected under D8)"
else
echo " baseline: descends $BASELINE on main (no origin/main ref present)"
fi
# --- hygiene (M12): tolerate untracked stray docs; abort only on a TRACKED uncommitted
# change to a migration-sensitive surface. The literal `git status --porcelain plugins ...`
# would also list untracked (??) files, which M12 explicitly tolerates, so we filter them. ---
DIRTY="$(git -C "$REPO_ROOT" status --porcelain plugins shared .claude-plugin/marketplace.json scripts \
| grep -v '^??' || true)"
[ -z "$DIRTY" ] || fail "uncommitted change to a migration-sensitive surface:
$DIRTY"
# --- print the 11-target table ---
echo "PREFLIGHT OK"
python3 - "$MAP" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))
t = m["targets"]
print(" %-26s %-9s %-5s %-7s %-5s %s" % ("target", "tag", "paths", "vendor", "blob", "repo_url"))
for k in sorted(t):
e = t[k]
print(" %-26s %-9s %-5d %-7s %-5s %s" % (
k, e["tag"], len(e["paths"]), str(e["has_vendor_ds"]), str(e.get("blob_strip", False)), e["repo_url"]))
print(" drop:", ", ".join(m.get("drop", [])) or "(none)")
print(" targets:", len(t))
PY
if [ "$DRY_RUN" = "1" ]; then
echo " (--dry-run: no mirror clone, no archive tag, no map mutation)"
exit 0
fi
# --- full run: build the dedicated extraction mirror once (R4/M6) ---
mkdir -p "$WORK"
if [ -e "$MIRROR/HEAD" ] || [ -d "$MIRROR/.git" ]; then
echo " mirror: present at $MIRROR (reused)"
else
rm -rf "$MIRROR"
git clone --no-local --quiet "$REPO_ROOT" "$MIRROR"
echo " mirror: built $MIRROR via clone --no-local (pinned at $(git -C "$MIRROR" rev-parse --short HEAD))"
fi
# --- enumerate >1MB blobs in ms-ai-architect history; safe iff all are screenshots (M4/F3) ---
BLOBLIST="$(git -C "$MIRROR" rev-list --objects --all \
| git -C "$MIRROR" cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" && $3+0>1048576 { p=""; for (i=4;i<=NF;i++) p = p (i>4?" ":"") $i; print $3"\t"p }' \
| grep -E 'plugins/ms-ai-architect/' || true)"
SAFE=true
if [ -n "$BLOBLIST" ]; then
NONSCREEN="$(printf '%s\n' "$BLOBLIST" | grep -vE 'plugins/ms-ai-architect/playground/screenshots/' || true)"
[ -z "$NONSCREEN" ] || SAFE=false
fi
echo " blob-strip candidates (>1MB) in ms-ai-architect history:"
if [ -n "$BLOBLIST" ]; then printf '%s\n' "$BLOBLIST" | sed 's/^/ /'; else echo " (none)"; fi
echo " blob_strip_safe = $SAFE"
# --- idempotent emit into plugin-map.json (only rewrites if the value actually changed) ---
python3 - "$MAP" "$SAFE" <<'PY'
import json, sys
path, safe = sys.argv[1], (sys.argv[2] == "true")
m = json.load(open(path))
cur = m["targets"].get("ms-ai-architect", {}).get("blob_strip_safe")
if cur != safe:
m["targets"]["ms-ai-architect"]["blob_strip_safe"] = safe
with open(path, "w") as f:
f.write(json.dumps(m, indent=2) + "\n")
print(" plugin-map.json: blob_strip_safe set to", safe)
else:
print(" plugin-map.json: blob_strip_safe already", safe, "(no change)")
PY
# --- create the local annotated archive tag idempotently (D2; pushed later, in the window) ---
if git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$ARCHIVE_TAG" >/dev/null; then
echo " tag: $ARCHIVE_TAG already exists (idempotent)"
else
git -C "$REPO_ROOT" tag -a "$ARCHIVE_TAG" -m "Archive of the monorepo before the polyrepo split (D2)"
echo " tag: created $ARCHIVE_TAG at HEAD (local only — pushed in the operator window)"
fi
echo "PREFLIGHT COMPLETE"