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:
@@ -2,6 +2,7 @@
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-04
|
||||
**Update (2026-05):** The standalone `josh-sync adopt <target>` command described below has been folded into `josh-sync onboard <target>` as the `adopt` strategy (selectable via `--mode=adopt`, `targets[].history_lock: preserve`, or auto-detection when the subrepo has any branches). `josh-sync adopt` is retained as a thin CLI alias. The state file moved from `<target>/adopt.json` to `<target>/onboard.json` with a `.strategy` field; the legacy file is still read for backward compatibility. See the guide's Onboarding section.
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ Each target maps a monorepo subfolder to an external subrepo.
|
||||
| `branches` | object | Yes | — | Branch mapping: `mono_branch: subrepo_branch`. Each key-value pair syncs those branches bidirectionally. |
|
||||
| `forward_only` | string[] | No | `[]` | Branches that only sync mono → subrepo, never reverse. |
|
||||
| `exclude` | string[] | No | `[]` | File/directory patterns to exclude from sync via josh `:exclude` filter. Excluded files exist only in the monorepo, never in the subrepo. See [Excluding Files](guide.md#excluding-files-from-sync). |
|
||||
| `history_lock` | string | No | (auto-detect) | Onboarding strategy hint. `"preserve"` selects the non-destructive [adopt](guide.md#onboarding) strategy (keeps subrepo history via an adoption merge — ADR-013); `"rewrite"` selects the destructive reset strategy (force-pushes josh-filtered history). When omitted, `josh-sync onboard` auto-detects: subrepos with branches → adopt, empty subrepos → reset. The CLI flag `--mode={reset,adopt}` overrides this. |
|
||||
|
||||
## `bot` Section
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user