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

193 lines
9.1 KiB
Markdown
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.

# 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):
```bash
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):
```bash
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):
```bash
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:
```text
/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 first**`playground-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-churn**`voyage``llm-security``linkedin-studio``ms-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-churn**`config-audit``okr``ai-psychosis``human-friendly-style``claude-design`.
**Per-repo procedure (identical for every target):**
```bash
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-security``config-audit`
`ms-ai-architect``voyage` → 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
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:
```bash
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.