diff --git a/docs/marketplace-polyrepo-migration/migration/10-extract.sh b/docs/marketplace-polyrepo-migration/migration/10-extract.sh new file mode 100644 index 0000000..0c1ba86 --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/10-extract.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Step 3 — Rename-aware extraction driver. +# Given a target key from plugin-map.json, clones the dedicated mirror ($WORK/_mirror, built by +# 00-preflight.sh — never the working checkout, R4/M6) into $WORK/, runs a SINGLE git filter-repo +# pass composing --path / --path-rename (both names for renamed plugins, F1) + the deterministic +# ms-ai-architect blob strip (F3), then strips carried-over tags and re-creates exactly one annotated +# tag v at the rewritten HEAD (F5). Idempotent (wipes $WORK/ first). NULL push (D8). +# +# Renamed plugins are extracted under BOTH historical names (F1), driven by plugin-map.json: +# voyage ← ultraplan-local, llm-security ← llm-security-copilot, linkedin-studio ← linkedin-thought-leadership. +# A single-path filter would silently drop the pre-rename history, so the map carries >=2 --path entries each. +# +# Usage: 10-extract.sh (e.g. voyage, llm-security, playground-design-system) +# Override WORK= to relocate the workspace (default /tmp/polyrepo-migration). +set -euo pipefail + +KEY="${1:-}" +[ -n "$KEY" ] || { echo "usage: 10-extract.sh " >&2; exit 2; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MAP="$SCRIPT_DIR/plugin-map.json" +WORK="${WORK:-/tmp/polyrepo-migration}" +MIRROR="$WORK/_mirror" +DEST="$WORK/$KEY" + +fail() { printf 'EXTRACT FAIL: %s\n' "$*" >&2; exit 1; } + +[ -f "$MAP" ] || fail "plugin-map.json missing at $MAP" +python3 -c "import json,sys; m=json.load(open('$MAP')); sys.exit(0 if '$KEY' in m['targets'] else 1)" \ + || fail "unknown target '$KEY' (not in plugin-map.json)" + +# Self-heal: if the mirror is absent, run preflight to build it (extraction never reads the working checkout). +if [ ! -e "$MIRROR/HEAD" ] && [ ! -d "$MIRROR/.git" ]; then + echo " mirror absent → running preflight to build it" + WORK="$WORK" bash "$SCRIPT_DIR/00-preflight.sh" >/dev/null +fi +[ -e "$MIRROR/HEAD" ] || [ -d "$MIRROR/.git" ] || fail "mirror still absent after preflight: $MIRROR" + +# Compose the filter-repo arg list from the map (one arg per line → bash 3.2 array). +ARGS_FILE="$(mktemp)" +python3 - "$MAP" "$KEY" >"$ARGS_FILE" <<'PY' +import json, sys +m = json.load(open(sys.argv[1])) +t = m["targets"][sys.argv[2]] +out = [] +for p in t["paths"]: + out += ["--path", p] +for old, new in t.get("path_renames", {}).items(): + out += ["--path-rename", old + ":" + new] +if t.get("blob_strip"): + if t.get("blob_strip_safe"): + out += ["--strip-blobs-bigger-than", "1M"] + else: + # Surgical fallback (unreached while blob_strip_safe is true): drop only the screenshots. + out += ["--path-glob", "!plugins/ms-ai-architect/playground/screenshots/*"] +for a in out: + print(a) +PY +FR_ARGS=() +while IFS= read -r line; do FR_ARGS+=("$line"); done <"$ARGS_FILE" +rm -f "$ARGS_FILE" + +TAG="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$KEY']['tag'])")" + +# Fresh --no-local clone from the mirror, then the single composing filter-repo pass. +rm -rf "$DEST" +git clone --no-local --quiet "$MIRROR" "$DEST" +git -C "$DEST" filter-repo --force "${FR_ARGS[@]}" + +# F5: strip every carried-over tag, re-create exactly one annotated tag at the rewritten HEAD. +OLD_TAGS="$(git -C "$DEST" tag)" +if [ -n "$OLD_TAGS" ]; then + printf '%s\n' "$OLD_TAGS" | while IFS= read -r tg; do + [ -n "$tg" ] && git -C "$DEST" tag -d "$tg" >/dev/null + done +fi +git -C "$DEST" tag -a "$TAG" -m "Release $TAG (extracted from ktg-plugin-marketplace monorepo)" + +COMMITS="$(git -C "$DEST" rev-list --count HEAD)" +echo "EXTRACT OK $KEY → $DEST ($COMMITS commits, tag $TAG)" diff --git a/docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs b/docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs new file mode 100644 index 0000000..36b3ad1 --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs @@ -0,0 +1,44 @@ +// Step 3 integration test — runs the extraction driver against the live monorepo (via the mirror) +// and asserts F1 (renamed-plugin history retained), F5 (single clean re-tag), root-level contents, +// and origin removal. Pattern: plugins/voyage/tests/integration/*.test.mjs. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const SCRIPT = path.join(here, '10-extract.sh'); +const WORK = process.env.WORK || '/tmp/polyrepo-migration'; +const EXTRACT = path.join(WORK, 'voyage'); + +const git = (args) => spawnSync('git', ['-C', EXTRACT, ...args], { encoding: 'utf8' }); + +// Reuse the extract if the verify already produced it (tagged v5.1.1); otherwise build it. +(function ensureExtract() { + const tag = git(['tag']); + if (tag.status === 0 && tag.stdout.trim() === 'v5.1.1') return; + const r = spawnSync('bash', [SCRIPT, 'voyage'], { encoding: 'utf8', env: { ...process.env, WORK } }); + if (r.status !== 0) throw new Error(`extract failed (status ${r.status}):\n${r.stdout}\n${r.stderr}`); +})(); + +test('voyage extract retains pre-rename (ultraplan-local) history — F1', () => { + const n = parseInt(git(['rev-list', '--count', 'HEAD']).stdout.trim(), 10); + assert.ok(n >= 150, `expected >=150 commits (voyage + ultraplan-local), got ${n}`); +}); + +test('voyage extract carries exactly one tag: v5.1.1 (F5)', () => { + const tags = git(['tag']).stdout.trim().split('\n').filter(Boolean).sort(); + assert.deepEqual(tags, ['v5.1.1']); +}); + +test('voyage extract has no plugins/ prefix — contents at repo root', () => { + const files = git(['ls-files']).stdout.trim().split('\n').filter(Boolean); + assert.ok(files.length > 0, 'extract should have tracked files'); + assert.ok(!files.some((f) => f.startsWith('plugins/')), 'no tracked path should start with plugins/'); + assert.ok(files.includes('.claude-plugin/plugin.json'), 'voyage plugin.json should be at repo root'); +}); + +test('origin remote is removed post-filter', () => { + assert.equal(git(['remote']).stdout.trim(), '', 'filter-repo should remove the origin remote'); +});