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>
149 lines
6.7 KiB
Bash
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"
|