feat: config profile multi-repo mounts
- Add mappings array support to git_mount entries - Clone repository once per git_mount entry, mount multiple subdirectories - Normalize legacy source_path+target_path to mappings on read - Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings - Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers - Update GitMountItem Pydantic model with GitMountMapping and model_validator - Update frontend GitMountEditor component with mappings UI - Auto-convert legacy git mount entries to mappings format on load - Add 15 backend unit tests for normalization, resolution, and glob expansion - Update existing config profile resolver tests for new merge behavior Quality gates: pytest 167 passed, frontend typecheck clean Addresses: config-profile-multi-repo-mounts
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# Spec: Config Profile Multi-Repo Mounts
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
1. **FR-1**: A `git_mount` entry MAY include a `mappings` array.
|
||||
2. **FR-2**: Each item in `mappings` MUST have `source_path` and `target_path`.
|
||||
3. **FR-3**: If `mappings` is absent, `source_path` and `target_path` at the entry level MUST be treated as a single mapping (backward compatibility).
|
||||
4. **FR-4**: The repository MUST be cloned exactly once per `git_mount` entry.
|
||||
5. **FR-5**: Each mapping MUST create a separate Docker bind mount.
|
||||
6. **FR-6**: Glob patterns in `source_path` MUST be expanded per mapping.
|
||||
7. **FR-7**: Relative `target_path` values MUST be resolved against `working_directory`.
|
||||
8. **FR-8**: The merge logic for included profiles MUST deduplicate by `(remote_url, branch)` within a single resolved profile's `git_mounts` list.
|
||||
|
||||
### Non-Functional
|
||||
|
||||
1. **NFR-1**: No database schema migration required.
|
||||
2. **NFR-2**: Existing API responses must remain backward-compatible.
|
||||
3. **NFR-3**: Frontend type-check must pass without errors.
|
||||
|
||||
## API Contracts
|
||||
|
||||
### ConfigProfile model (git_mounts field)
|
||||
|
||||
```json
|
||||
{
|
||||
"git_mounts": [
|
||||
{
|
||||
"remote_url": "https://github.com/user/monorepo.git",
|
||||
"branch": "main",
|
||||
"mappings": [
|
||||
{"source_path": "packages/api", "target_path": "/app/api"},
|
||||
{"source_path": "packages/web", "target_path": "/app/web"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"remote_url": "https://github.com/user/docs.git",
|
||||
"source_path": ".",
|
||||
"target_path": "/docs",
|
||||
"branch": "main"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Validation rules
|
||||
|
||||
1. `mappings` must be a non-empty array if present.
|
||||
2. Each mapping must have `source_path` (string) and `target_path` (string).
|
||||
3. Either `mappings` OR (`source_path` AND `target_path`) must be present.
|
||||
4. `remote_url` must be a valid HTTPS or SSH Git URL.
|
||||
|
||||
## Database Schema
|
||||
|
||||
No changes. `git_mounts` is stored as JSONB in `config_profiles.git_mounts`.
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Scenario 1: Monorepo with multiple packages
|
||||
|
||||
**Given** a Config Profile with:
|
||||
```json
|
||||
{"git_mounts": [{
|
||||
"remote_url": "https://github.com/corp/monorepo.git",
|
||||
"branch": "main",
|
||||
"mappings": [
|
||||
{"source_path": "packages/api", "target_path": "/app/api"},
|
||||
{"source_path": "packages/web", "target_path": "/app/web"},
|
||||
{"source_path": "packages/shared", "target_path": "/app/shared"}
|
||||
]
|
||||
}]}
|
||||
```
|
||||
|
||||
**When** the profile is applied to an instance
|
||||
|
||||
**Then**:
|
||||
1. `corp/monorepo` is cloned once to `git-mounts/monorepo-{hash}/repo-clone`
|
||||
2. Three bind mounts are created:
|
||||
- `{clone}/packages/api` → `/app/api`
|
||||
- `{clone}/packages/web` → `/app/web`
|
||||
- `{clone}/packages/shared` → `/app/shared`
|
||||
|
||||
### Scenario 2: Legacy single mapping (backward compatibility)
|
||||
|
||||
**Given** a Config Profile with:
|
||||
```json
|
||||
{"git_mounts": [{
|
||||
"remote_url": "https://github.com/user/repo.git",
|
||||
"source_path": "src",
|
||||
"target_path": "/workspace/src",
|
||||
"branch": "main"
|
||||
}]}
|
||||
```
|
||||
|
||||
**When** the profile is applied
|
||||
|
||||
**Then** the behavior is identical to before (single clone, single mount).
|
||||
|
||||
### Scenario 3: Glob expansion within mapping
|
||||
|
||||
**Given** a mapping with:
|
||||
```json
|
||||
{"source_path": "packages/*", "target_path": "/app/packages"}
|
||||
```
|
||||
|
||||
**When** the repo is cloned and the glob is expanded
|
||||
|
||||
**Then** each matched directory is mounted as a separate bind mount with the relative path appended to the target:
|
||||
- `{clone}/packages/api` → `/app/packages/api`
|
||||
- `{clone}/packages/web` → `/app/packages/web`
|
||||
|
||||
## Test Strategy
|
||||
|
||||
1. Unit test `_resolve_single_git_mount` with `mappings` array
|
||||
2. Unit test `_merge_git_mounts` with mappings deduplication
|
||||
3. Integration test: profile with 3 mappings from same repo → verify single clone
|
||||
4. Integration test: legacy profile without `mappings` → verify backward compatibility
|
||||
5. Frontend unit test: GitMountEditor renders mappings form correctly
|
||||
Reference in New Issue
Block a user