Release 2.2.0 — unified onboard strategy + safety hardening

Folds `josh-sync adopt` into `josh-sync onboard <target>` as the adopt
strategy alongside the original reset flow. Strategy resolves by
precedence: --mode flag > targets[].history_lock config (preserve|rewrite)
> auto-detect via `git ls-remote --heads`. `josh-sync adopt` is kept as
a thin alias. Adds the new `targets[].history_lock` config field
(validated at parse time) and folds `<target>/adopt.json` state into
`<target>/onboard.json` with a `.strategy` field; legacy state files
are read with a backward-compat fallback.

Safety hardening (from a multi-angle review of the unification):
- auto-detect distinguishes auth/network failure from empty repo
- resume validates --mode against the strategy saved in state
- `josh-sync adopt` rejects a conflicting --mode in the forwarded args
- missing import-PR lookup dies in both strategies (was WARN+continue
  for reset, which could create duplicate import PRs on resume)
- --restart durably removes the legacy adopt.json from the state branch
- adopt_branch is now a subshell function (EXIT trap can't clobber callers)
- strategy value validated after resolve/load (reset|adopt)
- --mode with missing/empty value dies with a usage hint
- migrate-pr against an adopt-strategy target dies with a specific hint
- reset importing asserts archived_url is present (no "null" → git clone)

End-to-end + CLI bats coverage added (tests/unit/adopt_e2e.bats,
tests/unit/cli.bats). 72 tests, shellcheck clean.

Makefile dist bundle header now correctly interpolates VERSION and
line count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 03:32:04 +01:00
parent f2e39be5e1
commit dc2cdea360
13 changed files with 1284 additions and 439 deletions

View File

@@ -222,20 +222,31 @@ This validates:
For a new monorepo before import, preflight may warn that subfolders don't exist yet — that's expected.
## Step 5: Import Existing Subrepos
## Step 5: Onboard Existing Subrepos
This is the critical onboarding step. There are three approaches:
`josh-sync onboard <target>` is the single entry point for connecting an existing subrepo to the monorepo. It runs interactively with checkpoint/resume and picks one of two strategies:
- **`josh-sync adopt`** (recommended for active existing subrepos) — non-destructive, resumable, preserves existing subrepo history
- **`josh-sync onboard`** destructive replacement-repo workflow for teams that intentionally archive the old repo
- **Manual `import` → merge → `reset`** — lower-level destructive path for automation or empty replacement repos
- **adopt** (non-destructive) — keeps the subrepo's existing history and joins it to josh-filtered history via a single adoption merge commit. No force-push. Developers fast-forward; open PR branches stay valid.
- **reset** (destructive) — force-pushes josh-filtered history onto the subrepo, replacing its prior history. Use this for empty replacement repos (typically paired with renaming the old repo to `*-archived`).
### Option A: Adopt (recommended for active subrepos)
### Strategy selection
Use `adopt` when the subrepo already exists, developers already clone it, or open PR branches should remain based on existing history.
`josh-sync onboard` resolves the strategy in this order (highest precedence first):
1. **CLI flag**`--mode=adopt` or `--mode=reset` overrides everything.
2. **Config**`targets[].history_lock: preserve` selects adopt; `rewrite` selects reset.
3. **Auto-detect**`git ls-remote --heads` on the subrepo. Branches present → adopt. Empty repo → reset.
The chosen strategy is logged at preflight so you can see what will happen before any side effects.
`josh-sync adopt <target>` is kept as a back-compat alias for `josh-sync onboard --mode=adopt`.
### Adopt strategy (auto-selected for active subrepos)
```bash
josh-sync adopt billing
josh-sync onboard billing # auto-detect — picks adopt for non-empty subrepo
josh-sync onboard billing --mode=adopt # force adopt explicitly
josh-sync adopt billing # equivalent back-compat alias
```
The command will:
@@ -245,7 +256,7 @@ The command will:
4. **Adopt** — creates a merge commit on the subrepo with Josh-filtered HEAD as parent 1 and existing subrepo HEAD as parent 2
5. **Push** — pushes the adoption merge with a normal fast-forward push, never force-push
The adoption merge preserves the old subrepo history while giving Josh a first-parent path back to the monorepo. If interrupted, re-run `josh-sync adopt billing` to resume. Use `--restart` to start over.
The adoption merge preserves the old subrepo history while giving Josh a first-parent path back to the monorepo. If interrupted, re-run `josh-sync onboard billing` to resume. Use `--restart` to start over. See [ADR-013](adr/013-non-destructive-adoption.md) for the design rationale.
After adoption, developers can update existing clones with a normal fast-forward:
@@ -255,9 +266,9 @@ git checkout main
git merge --ff-only origin/main
```
### Option B: Onboard with replacement repo
### Reset strategy (auto-selected for empty replacement repos)
The `onboard` command walks through the destructive replacement-repo process interactively, with checkpoint/resume at every step.
Use the reset strategy when you intentionally want to archive the old subrepo and start fresh from a new empty repo.
**Before you start:**
@@ -269,7 +280,8 @@ The rename preserves the archived repo with all its history and open PRs. The ne
**Run onboard:**
```bash
josh-sync onboard billing
josh-sync onboard billing # auto-detect — picks reset for empty subrepo
josh-sync onboard billing --mode=reset # force reset explicitly
```
The command will:
@@ -298,13 +310,13 @@ josh-sync migrate-pr billing 5 8 12
PR migration works by fetching the diff from the archived repo's PR, applying it to the new repo, and creating a new PR. File content is identical after reset, so patches apply cleanly.
### Option C: Manual import → merge → reset
### Manual: import → merge → reset
Use this for scripted automation or when you intentionally want to replace subrepo history.
Use this for scripted automation or when you intentionally want to replace subrepo history without the interactive wrapper.
> Do this **one target at a time** to keep PRs reviewable.
#### 5c-1. Import
#### Manual: Import
```bash
josh-sync import billing
@@ -319,13 +331,13 @@ This:
Review the import PR — check for leaked credentials, environment-specific config, or files that shouldn't be in the monorepo.
#### 5c-2. Merge the import PR
#### Manual: Merge the import PR
Merge the PR using your Git platform's UI. This lands the subrepo content into the monorepo's main branch.
> At this point, the monorepo has the content but the histories are disconnected. Sync will **not** work until you complete the reset step.
#### 5c-3. Reset
#### Manual: Reset
```bash
josh-sync reset billing
@@ -352,7 +364,7 @@ git checkout stage && git reset --hard origin/stage # repeat for each branch
Or simply delete and re-clone the subrepo. Local-only branches (not pushed to the remote) will be lost either way.
#### 5c-4. Repeat for each target
#### Manual: Repeat for each target
```
For each target:
@@ -646,15 +658,13 @@ To add a new subrepo after initial setup:
1. Add the target to `.josh-sync.yml`
2. Update the forward workflow's `paths:` list to include the new subfolder
3. Commit and push
4. Import or adopt the target:
4. Onboard the target — `josh-sync onboard` auto-picks `adopt` for non-empty subrepos and `reset` for empty ones; override with `--mode={reset,adopt}` or pin via the target's `history_lock` config field.
```bash
# Recommended for an existing active subrepo
josh-sync adopt new-target
josh-sync onboard new-target # auto-detect strategy
josh-sync onboard new-target --mode=adopt # force adopt
josh-sync onboard new-target --mode=reset # force reset
# Replacement-repo workflow
josh-sync onboard new-target
# Or manual: import → merge PR → reset
# Manual lower-level path (when scripting):
josh-sync import new-target
# merge the PR
josh-sync reset new-target