ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
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

9.1 KiB
Raw Blame History

Polyrepo migration — operator-window RUNBOOK

The single document the operator follows in the authorized window. Everything before this (Steps 19, plus the Step 11 dry-run) was authored and verified locally with NULL push (D8). This runbook is the only place live Forgejo repos are created and the live marketplace.json is mutated.

Preconditions

  • git-filter-repo (a non-default git extension) and python3 >= 3.6 on PATH — the extraction (10-extract.sh) and every map-driven script depend on them. 00-preflight.sh and 10-extract.sh both assert them, but install first: brew install git-filter-repo python3.
  • All migration scripts committed locally; the Step 11 dry-run is green (it exercises every script below).
  • $FORGEJO_TOKEN is exported (macOS Keychain → ~/.zshenv) with write:org + repo scope.
  • The pre-polyrepo-archive tag exists locally (D2) — the rollback anchor for the whole operation.
  • Push window (private-work policy): this runbook pushes to Forgejo. Run it only inside the allowed push window — weekdays 20:0023:00 (Europe/Oslo), or any time on weekends / Norwegian holidays.
  • WORK=/tmp/polyrepo-migration (the mirror + per-target extracts the scripts build).
  • Visibility: public (§9) — every repo is created "private": false.

Paths below assume you are in the catalog repo root unless stated. Scripts live in docs/marketplace-polyrepo-migration/migration/ (referred to as MIG/).


(0) Pilot gate — graceful-handoff

The pilot proves the highest-stakes unverified link: HTTPS + external source + ref pin + Forgejo resolution (F2 / Assumption 1). Pilot = graceful-handoff (low-churn, has tests, no design-system vendor, no blob bloat).

B3 ordering — non-negotiable: the catalog entry MUST be flipped to external before the install-smoke. If you install while the entry is still ./plugins/graceful-handoff, the install resolves the local copy and the test is vacuous — it proves nothing about the Forgejo chain.

(0a) Create the repo on Forgejo (auto_init:false so the first push defines history — an auto-initialised repo would create a divergent root commit the rename-aware extract cannot fast-forward):

curl -fsS -X POST "https://git.fromaitochitta.com/api/v1/orgs/open/repos" \
  -H "Authorization: token $FORGEJO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"graceful-handoff","private":false,"auto_init":false,"default_branch":"main"}'

(0b) Push the extracted repo (the extract was prepared + validated by the harness; re-run it to be sure it is green, then push all branches + tags):

WORK=$WORK bash MIG/40-validate-standalone.sh graceful-handoff   # must print: graceful-handoff: PASS (...)
cd "$WORK/graceful-handoff"
git remote add origin https://git.fromaitochitta.com/open/graceful-handoff.git
git push origin --all
git push origin --tags        # publishes v2.1.0 — the ref the catalog will pin
cd -                          # back to the catalog repo

(0c) Flip the catalog entry to external and push the catalog in its still-mixed state (9× ./plugins/x + 1× external; plugins/ is still present — thinning is last):

node MIG/60-rewrite-marketplace.mjs --only graceful-handoff \
  --in .claude-plugin/marketplace.json --out /tmp/mp.json
# review /tmp/mp.json: graceful-handoff is now {source:"url", url:..., ref:"v2.1.0"}, the other 9 unchanged
cp /tmp/mp.json .claude-plugin/marketplace.json
git add .claude-plugin/marketplace.json
git commit -m "chore(marketplace): externalise graceful-handoff (pilot)"
git push origin main

(0d) Install-smoke from a FRESH Claude Code session — the actual Forgejo-chain test:

/plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
/plugin install graceful-handoff@ktg-plugin-marketplace

Because the entry is now external, the install resolves the Forgejo repo over HTTPS at ref: v2.1.0. Confirm the plugin's commands/skills load.

If 0d fails: STOP. Revert the flip (§ Rollback) — graceful-handoff goes back to ./plugins/graceful-handoff, the marketplace stays live — and diagnose the HTTPS/ref/Forgejo chain before touching any of the other 10. Do not proceed past a failed pilot.


(1)(3) Roll out the rest, one repo at a time

Order (lower risk last so problems surface while the blast radius is small):

  1. Design-system firstplayground-design-system (the 2 consumers, llm-security + ms-ai-architect, vendor it; standing it up first means their vendored copies have an upstream to point at).
  2. High-churnvoyagellm-securitylinkedin-studioms-ai-architect.
    • ms-ai-architect carries the 148 MB screenshot blob-bomb (F3); the extract strips >1 MB blobs when blob_strip_safe (verified true), so its push is a normal size. Confirm the push completes.
  3. Low-churnconfig-auditokrai-psychosishuman-friendly-styleclaude-design.

Per-repo procedure (identical for every target):

KEY=<target>     # e.g. voyage

# a. extract + validate standalone (config-audit uses its dedicated SC2 gate)
if [ "$KEY" = "config-audit" ]; then
  WORK=$WORK bash MIG/50-config-audit-sc2.sh                 # config-audit: SC2 PASS (...)
else
  WORK=$WORK bash MIG/40-validate-standalone.sh "$KEY"       # <KEY>: PASS (...)
fi

# b. create the Forgejo repo (auto_init:false, public)
curl -fsS -X POST "https://git.fromaitochitta.com/api/v1/orgs/open/repos" \
  -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
  -d "{\"name\":\"$KEY\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}"

# c. push the extracted repo
cd "$WORK/$KEY"
git remote add origin "https://git.fromaitochitta.com/open/$KEY.git"
git push origin --all
git push origin --tags
cd -

# d. flip this entry in the catalog (cumulative — previously-flipped entries pass through unchanged)
node MIG/60-rewrite-marketplace.mjs --only "$KEY" \
  --in .claude-plugin/marketplace.json --out /tmp/mp.json
cp /tmp/mp.json .claude-plugin/marketplace.json
git add .claude-plugin/marketplace.json
git commit -m "chore(marketplace): externalise $KEY"
git push origin main

# e. INSTALL-SMOKE (SC8) before moving to the next target — fresh Claude Code session:
#    /plugin marketplace update
#    /plugin install $KEY@ktg-plugin-marketplace

Install-smoke order by surface (largest command surface first, so the most likely to expose a resolution problem is caught early): linkedin-studio (29 commands) → llm-securityconfig-auditms-ai-architectvoyage → the rest. For each: confirm commands/skills/agents register and one representative command runs.

Do not start a target until the previous target's install-smoke has passed. A mixed-source catalog (some ./plugins/x, some external) is a fully valid live state (SC3/SC8) — that is exactly what makes the one-at-a-time rollout safe.


(4) Thin the catalog — LAST

Only after all 11 repos are pushed and every install-smoke has passed:

bash MIG/70-thin-catalog.sh --workspace /tmp/thin-catalog
# review /tmp/thin-catalog: no plugins/ or shared/, CONVENTIONS.md present, README.md all-external

Apply the thin state to the catalog repo (remove plugins/, shared/, scripts/sync-design-system.mjs, add CONVENTIONS.md, swap in the rewritten README.md + thinned CLAUDE.md), commit, then:

git push origin main
git push origin pre-polyrepo-archive     # publish the rollback anchor tag (D2)

The marketplace.json is now fully external; the catalog hosts only the manifest + landing/governance/ conventions docs.


Ref updates (steady state)

When a plugin cuts a new release, bump its ref in .claude-plugin/marketplace.json (re-run 60-rewrite-marketplace.mjs --only <key> after updating the tag in plugin-map.json, or edit the ref by hand), push the catalog, and consumers pick it up with /plugin marketplace update.


Rollback

A flip is reversible right up until the ./plugins/<key> source is removed (i.e. until thinning). Order of preference:

  • Single bad flip (pre-thinning): git revert <the externalise commit> and push — the entry returns to ./plugins/<key>, which is still present, so the marketplace works immediately. Then fix the external repo and re-flip.
  • Never remove a ./plugins/<key> source before that plugin's external repo is pushed AND its install-smoke has passed (R1 / SC8). Thinning (step 4) is the point of no easy return — do it only when every external repo is verified live.
  • Whole-operation anchor: the pre-polyrepo-archive tag is the pre-migration state of the monorepo. If the catalog needs to be reset wholesale, reset to that tag (local) before it was force-published.

Failure stops

  • Pilot (0d) fails → STOP, revert, diagnose the Forgejo/HTTPS/ref chain. Touch nothing else.
  • Any per-repo push or install-smoke fails → STOP at that target, revert its flip, leave the rest live.
  • Never thin the catalog while any plugin is still ./plugins/<key>-only without a verified external repo.