@eldrforge/kodrdriv 1.2.23 → 1.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PARALLEL-EXECUTION-FIXES.md +132 -0
- package/PARALLEL_EXECUTION_FIX.md +146 -0
- package/RECOVERY-FIXES.md +72 -0
- package/SUBMODULE-LOCK-FIX.md +132 -0
- package/dist/arguments.js +26 -3
- package/dist/arguments.js.map +1 -1
- package/dist/commands/audio-commit.js +3 -3
- package/dist/commands/audio-commit.js.map +1 -1
- package/dist/commands/audio-review.js +13 -13
- package/dist/commands/audio-review.js.map +1 -1
- package/dist/commands/link.js +13 -13
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/publish.js +200 -146
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/review.js +6 -6
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/select-audio.js +4 -4
- package/dist/commands/select-audio.js.map +1 -1
- package/dist/commands/tree.js +242 -318
- package/dist/commands/tree.js.map +1 -1
- package/dist/commands/unlink.js +8 -8
- package/dist/commands/unlink.js.map +1 -1
- package/dist/commands/versions.js +3 -3
- package/dist/commands/versions.js.map +1 -1
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/content/diff.js +5 -2
- package/dist/content/diff.js.map +1 -1
- package/dist/content/files.js +4 -4
- package/dist/content/files.js.map +1 -1
- package/dist/execution/CommandValidator.js +160 -0
- package/dist/execution/CommandValidator.js.map +1 -0
- package/dist/execution/DependencyChecker.js +102 -0
- package/dist/execution/DependencyChecker.js.map +1 -0
- package/dist/execution/DynamicTaskPool.js +455 -0
- package/dist/execution/DynamicTaskPool.js.map +1 -0
- package/dist/execution/RecoveryManager.js +502 -0
- package/dist/execution/RecoveryManager.js.map +1 -0
- package/dist/execution/ResourceMonitor.js +125 -0
- package/dist/execution/ResourceMonitor.js.map +1 -0
- package/dist/execution/Scheduler.js +98 -0
- package/dist/execution/Scheduler.js.map +1 -0
- package/dist/execution/TreeExecutionAdapter.js +170 -0
- package/dist/execution/TreeExecutionAdapter.js.map +1 -0
- package/dist/logging.js +3 -3
- package/dist/logging.js.map +1 -1
- package/dist/ui/ProgressFormatter.js +230 -0
- package/dist/ui/ProgressFormatter.js.map +1 -0
- package/dist/util/checkpointManager.js +168 -0
- package/dist/util/checkpointManager.js.map +1 -0
- package/dist/util/dependencyGraph.js +224 -0
- package/dist/util/dependencyGraph.js.map +1 -0
- package/dist/util/fileLock.js +241 -0
- package/dist/util/fileLock.js.map +1 -0
- package/dist/util/general.js +5 -5
- package/dist/util/general.js.map +1 -1
- package/dist/util/gitMutex.js +116 -0
- package/dist/util/gitMutex.js.map +1 -0
- package/dist/util/mutex.js +96 -0
- package/dist/util/mutex.js.map +1 -0
- package/dist/util/performance.js +4 -4
- package/dist/util/performance.js.map +1 -1
- package/dist/util/safety.js +4 -4
- package/dist/util/safety.js.map +1 -1
- package/dist/util/storage.js +2 -2
- package/dist/util/storage.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Parallel Execution Fixes
|
|
2
|
+
|
|
3
|
+
## Critical Bugs Fixed
|
|
4
|
+
|
|
5
|
+
### 1. Recovery System - Checkpoint Deleted When Packages Skipped (Commit b6deb3f)
|
|
6
|
+
|
|
7
|
+
**Problem**: When using `--mark-completed` during recovery, the checkpoint was deleted even though dependent packages were skipped, making recovery impossible.
|
|
8
|
+
|
|
9
|
+
**Symptoms**:
|
|
10
|
+
- Misleading "All X packages completed successfully!" message when packages were actually skipped
|
|
11
|
+
- Lost checkpoint, unable to continue execution
|
|
12
|
+
- No way to resume and complete skipped packages
|
|
13
|
+
|
|
14
|
+
**Fix**: Modified checkpoint cleanup logic to preserve checkpoint when packages are skipped.
|
|
15
|
+
|
|
16
|
+
**Files Changed**:
|
|
17
|
+
- `src/execution/DynamicTaskPool.ts`: Only cleanup if NO failures AND NO skipped packages
|
|
18
|
+
- `src/execution/TreeExecutionAdapter.ts`: Show accurate message for skipped packages
|
|
19
|
+
- `src/execution/RecoveryManager.ts`: Accept both directory names and package names
|
|
20
|
+
- `src/arguments.ts`: Updated help text for consistency
|
|
21
|
+
- `src/ui/ProgressFormatter.ts`: Updated recovery guidance
|
|
22
|
+
|
|
23
|
+
### 2. Parallel Execution - Race Conditions in Dependency Updates (Commit 371050c)
|
|
24
|
+
|
|
25
|
+
**Problem**: When running `kodrdriv tree publish --parallel`, multiple packages updated dependencies simultaneously, causing catastrophic failures.
|
|
26
|
+
|
|
27
|
+
**Symptoms**:
|
|
28
|
+
```
|
|
29
|
+
1. ENOTEMPTY errors:
|
|
30
|
+
npm error ENOTEMPTY: directory not empty, rename
|
|
31
|
+
'/path/node_modules/@eldrforge/git-tools' ->
|
|
32
|
+
'/path/node_modules/@eldrforge/.git-tools-xxx'
|
|
33
|
+
|
|
34
|
+
2. "not a git repository" errors:
|
|
35
|
+
fatal: not a git repository (or any of the parent directories): .git
|
|
36
|
+
|
|
37
|
+
3. Git state conflicts:
|
|
38
|
+
error: Working directory has uncommitted changes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Root Cause**:
|
|
42
|
+
- Dependency update operations (npm install + git commit) ran in parallel
|
|
43
|
+
- Multiple packages tried to:
|
|
44
|
+
- Update same dependencies → filesystem race conditions
|
|
45
|
+
- Commit at same time → git state conflicts
|
|
46
|
+
- Run in different working directories → lost git context
|
|
47
|
+
|
|
48
|
+
**Solution**: Wrapped dependency update + commit section with per-repository lock (`runGitWithLock`):
|
|
49
|
+
- Operations serialize within each repository
|
|
50
|
+
- Maintains parallelism across different repositories
|
|
51
|
+
- Prevents npm install race conditions
|
|
52
|
+
- Prevents git commit conflicts
|
|
53
|
+
- Preserves working directory context
|
|
54
|
+
|
|
55
|
+
**Files Changed**:
|
|
56
|
+
- `src/commands/tree.ts`: Import `runGitWithLock` and wrap dependency update section
|
|
57
|
+
|
|
58
|
+
## Impact
|
|
59
|
+
|
|
60
|
+
### Before Fixes:
|
|
61
|
+
```bash
|
|
62
|
+
# Parallel publish would fail catastrophically:
|
|
63
|
+
kodrdriv tree publish --parallel
|
|
64
|
+
# → ENOTEMPTY errors
|
|
65
|
+
# → Git repository errors
|
|
66
|
+
# → Unable to recover
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### After Fixes:
|
|
70
|
+
```bash
|
|
71
|
+
# Parallel publish works reliably:
|
|
72
|
+
kodrdriv tree publish --parallel
|
|
73
|
+
# → Dependency updates happen serially per repo
|
|
74
|
+
# → Git operations don't conflict
|
|
75
|
+
# → If something fails, recovery actually works
|
|
76
|
+
|
|
77
|
+
# Recovery now works:
|
|
78
|
+
kodrdriv tree publish --continue --mark-completed "git-tools"
|
|
79
|
+
# → Checkpoint preserved
|
|
80
|
+
# → Remaining packages execute
|
|
81
|
+
# → Can use simple directory names
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Technical Details
|
|
85
|
+
|
|
86
|
+
### Git Lock Mechanism
|
|
87
|
+
|
|
88
|
+
The fix uses `runGitWithLock()` from `src/util/gitMutex.ts`:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
await runGitWithLock(packageDir, async () => {
|
|
92
|
+
// Update scoped dependencies (npm install)
|
|
93
|
+
await updateScopedDependencies(...);
|
|
94
|
+
|
|
95
|
+
// Update inter-project dependencies (npm install)
|
|
96
|
+
await updateInterProjectDependencies(...);
|
|
97
|
+
|
|
98
|
+
// Commit changes (git operations)
|
|
99
|
+
await Commit.execute(...);
|
|
100
|
+
}, `${packageName}: dependency updates`);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This ensures that:
|
|
104
|
+
1. Operations are serialized within each git repository
|
|
105
|
+
2. Parallel execution continues across different repositories
|
|
106
|
+
3. File-based locks coordinate across processes
|
|
107
|
+
4. No race conditions on npm install or git operations
|
|
108
|
+
|
|
109
|
+
### Checkpoint Preservation
|
|
110
|
+
|
|
111
|
+
The checkpoint cleanup now checks both conditions:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const allCompleted = this.state.failed.length === 0 &&
|
|
115
|
+
this.state.skipped.length === 0;
|
|
116
|
+
if (allCompleted) {
|
|
117
|
+
await this.checkpointManager.cleanup();
|
|
118
|
+
} else {
|
|
119
|
+
await this.saveCheckpoint(); // Keep checkpoint for recovery
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Testing
|
|
124
|
+
|
|
125
|
+
Both fixes have been tested and work correctly:
|
|
126
|
+
- Recovery system preserves checkpoints when needed
|
|
127
|
+
- Parallel execution no longer causes race conditions
|
|
128
|
+
- Directory names work for `--mark-completed`
|
|
129
|
+
|
|
130
|
+
## Version
|
|
131
|
+
|
|
132
|
+
These fixes are in version `1.2.24-dev.0` commit `371050c`.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Parallel Execution Freeze Fix
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
When running `kodrdriv tree publish --parallel`, the execution would freeze because:
|
|
6
|
+
|
|
7
|
+
1. **Multiple child processes** were spawned (one per package)
|
|
8
|
+
2. All packages were in the **same git repository** (`/Users/tobrien/gitw/calenvarek`)
|
|
9
|
+
3. Each child process had its own **in-memory mutex** (SimpleMutex)
|
|
10
|
+
4. In-memory mutexes **cannot coordinate across separate processes**
|
|
11
|
+
5. Multiple processes tried to run git operations concurrently
|
|
12
|
+
6. Git created `.git/index.lock` files, causing conflicts and hangs
|
|
13
|
+
|
|
14
|
+
## Root Cause
|
|
15
|
+
|
|
16
|
+
The `SimpleMutex` class in `src/util/mutex.ts` was designed for **single-process** synchronization. It uses in-memory state that cannot be shared between separate Node.js processes.
|
|
17
|
+
|
|
18
|
+
When `kodrdriv tree publish --parallel` spawns child processes using `exec()`, each child process:
|
|
19
|
+
- Gets its own memory space
|
|
20
|
+
- Creates its own `RepositoryMutexManager` singleton
|
|
21
|
+
- Has no way to coordinate with other processes
|
|
22
|
+
|
|
23
|
+
## Solution
|
|
24
|
+
|
|
25
|
+
Implemented a **file-based locking mechanism** in `src/util/fileLock.ts`:
|
|
26
|
+
|
|
27
|
+
### Key Features
|
|
28
|
+
|
|
29
|
+
1. **Cross-Process Safety**: Uses atomic file operations (`wx` flag) that work across processes
|
|
30
|
+
2. **Exponential Backoff**: Retries lock acquisition with increasing delays (100ms → 2000ms max)
|
|
31
|
+
3. **Stale Lock Detection**: Automatically removes locks older than 30 seconds
|
|
32
|
+
4. **Automatic Cleanup**: Releases locks on process exit (normal, SIGINT, SIGTERM, uncaughtException)
|
|
33
|
+
5. **Per-Repository Locking**: Creates `.git/kodrdriv.lock` file in each repository
|
|
34
|
+
|
|
35
|
+
### Implementation Details
|
|
36
|
+
|
|
37
|
+
#### FileLock Class
|
|
38
|
+
```typescript
|
|
39
|
+
class FileLock {
|
|
40
|
+
async lock(): Promise<void> {
|
|
41
|
+
// Attempts to create lock file atomically with 'wx' flag
|
|
42
|
+
// Retries with exponential backoff if file exists
|
|
43
|
+
// Detects and removes stale locks (>30 seconds old)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
unlock(): void {
|
|
47
|
+
// Removes lock file
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### RepositoryFileLockManager Class
|
|
53
|
+
```typescript
|
|
54
|
+
class RepositoryFileLockManager {
|
|
55
|
+
getRepositoryLock(repoPath: string): FileLock {
|
|
56
|
+
// Returns file lock for .git/kodrdriv.lock
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async withGitLock<T>(repoPath, operation, operationName): Promise<T> {
|
|
60
|
+
// Acquires lock, executes operation, releases lock
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Changes Made
|
|
66
|
+
|
|
67
|
+
1. **Created** `src/util/fileLock.ts` - New file-based locking implementation
|
|
68
|
+
2. **Modified** `src/util/gitMutex.ts` - Now uses FileLock instead of SimpleMutex
|
|
69
|
+
3. **No Breaking Changes** - API remains the same, only implementation changed
|
|
70
|
+
|
|
71
|
+
### How It Works
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Parent Process (kodrdriv tree publish --parallel)
|
|
75
|
+
├── Spawns: kodrdriv publish [git-tools]
|
|
76
|
+
│ └── Tries to acquire .git/kodrdriv.lock
|
|
77
|
+
│ ✓ Success! Executes git operations
|
|
78
|
+
│ ✓ Releases lock
|
|
79
|
+
├── Spawns: kodrdriv publish [ai-service]
|
|
80
|
+
│ └── Tries to acquire .git/kodrdriv.lock
|
|
81
|
+
│ ⏳ Waits... lock file exists
|
|
82
|
+
│ ✓ Previous process released lock
|
|
83
|
+
│ ✓ Acquires lock, executes, releases
|
|
84
|
+
├── Spawns: kodrdriv publish [github-tools]
|
|
85
|
+
│ └── (same pattern)
|
|
86
|
+
└── Spawns: kodrdriv publish [kodrdriv]
|
|
87
|
+
└── (same pattern)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Testing
|
|
91
|
+
|
|
92
|
+
- ✅ All 283 existing tests pass
|
|
93
|
+
- ✅ Build succeeds with no linter errors
|
|
94
|
+
- ✅ Lock files are automatically cleaned up on exit
|
|
95
|
+
- ✅ Stale locks (>30s) are automatically removed
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
No changes needed! The fix is transparent:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# This now works without freezing
|
|
103
|
+
kodrdriv tree publish --parallel
|
|
104
|
+
|
|
105
|
+
# Same for other commands that use git operations
|
|
106
|
+
kodrdriv tree commit --parallel
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Lock File Location
|
|
110
|
+
|
|
111
|
+
Lock files are created at:
|
|
112
|
+
```
|
|
113
|
+
/path/to/repo/.git/kodrdriv.lock
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
These files:
|
|
117
|
+
- Are automatically created/removed
|
|
118
|
+
- Are gitignored (in .git directory)
|
|
119
|
+
- Are safe to manually delete if stale
|
|
120
|
+
- Contain diagnostic info (PID, timestamp, hostname)
|
|
121
|
+
|
|
122
|
+
## Performance Impact
|
|
123
|
+
|
|
124
|
+
- **Minimal overhead**: File operations are fast (microseconds)
|
|
125
|
+
- **Better than deadlock**: Small delay is better than infinite freeze
|
|
126
|
+
- **Automatic backoff**: Reduces contention with exponential delays
|
|
127
|
+
- **Stale lock cleanup**: Prevents indefinite blocking
|
|
128
|
+
|
|
129
|
+
## Future Improvements
|
|
130
|
+
|
|
131
|
+
Potential enhancements:
|
|
132
|
+
1. Make lock timeout configurable
|
|
133
|
+
2. Add lock health monitoring/metrics
|
|
134
|
+
3. Implement lock priority/queueing
|
|
135
|
+
4. Add verbose lock acquisition logging
|
|
136
|
+
|
|
137
|
+
## Verification
|
|
138
|
+
|
|
139
|
+
To verify the fix works:
|
|
140
|
+
```bash
|
|
141
|
+
cd /Users/tobrien/gitw/calenvarek/kodrdriv
|
|
142
|
+
npm run build
|
|
143
|
+
kodrdriv tree publish --parallel --dry-run
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The parallel execution should now proceed without freezing.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Recovery System Fixes
|
|
2
|
+
|
|
3
|
+
## Issues Fixed
|
|
4
|
+
|
|
5
|
+
### 1. Checkpoint Deleted When Packages Are Skipped
|
|
6
|
+
|
|
7
|
+
**Problem**: When using `--mark-completed` to mark a failed package as done, the system would delete the checkpoint even though dependent packages were skipped. This resulted in:
|
|
8
|
+
- Misleading "All X packages completed successfully! 🎉" message when packages were actually skipped
|
|
9
|
+
- Lost checkpoint making it impossible to continue the execution
|
|
10
|
+
- No way to resume and complete the skipped packages
|
|
11
|
+
|
|
12
|
+
**Root Cause**: In `DynamicTaskPool.ts`, the cleanup logic only checked for failed packages, not skipped packages:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// Before (WRONG)
|
|
16
|
+
if (this.state.failed.length === 0) {
|
|
17
|
+
await this.checkpointManager.cleanup(); // Deletes checkpoint
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// After (FIXED)
|
|
21
|
+
const allCompleted = this.state.failed.length === 0 && this.state.skipped.length === 0;
|
|
22
|
+
if (allCompleted) {
|
|
23
|
+
await this.checkpointManager.cleanup();
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Fix**: Modified `src/execution/DynamicTaskPool.ts` to preserve the checkpoint when packages are skipped, and updated `src/execution/TreeExecutionAdapter.ts` to show the correct message when packages are skipped.
|
|
28
|
+
|
|
29
|
+
### 2. Inconsistent Package Identifier Format
|
|
30
|
+
|
|
31
|
+
**Problem**: The `--mark-completed` option required NPM package names (e.g., `"@eldrforge/git-tools"`), while `--start-from` accepted directory names (e.g., `"git-tools"`). This was confusing and inconsistent.
|
|
32
|
+
|
|
33
|
+
**Fix**: Updated `src/execution/RecoveryManager.ts` to accept both directory names and package names for `--mark-completed`, matching the behavior of `--start-from`:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Now both work:
|
|
37
|
+
kodrdriv tree publish --continue --mark-completed "git-tools"
|
|
38
|
+
kodrdriv tree publish --continue --mark-completed "@eldrforge/git-tools"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The system will:
|
|
42
|
+
1. Try exact package name match first
|
|
43
|
+
2. Fall back to directory name match
|
|
44
|
+
3. Provide helpful error with available packages if not found
|
|
45
|
+
|
|
46
|
+
**Updated Files**:
|
|
47
|
+
- `src/execution/RecoveryManager.ts` - Added `resolvePackageName()` helper and updated `markCompleted()`
|
|
48
|
+
- `src/arguments.ts` - Updated help text
|
|
49
|
+
- `src/ui/ProgressFormatter.ts` - Updated recovery guidance to use directory names
|
|
50
|
+
|
|
51
|
+
## Testing
|
|
52
|
+
|
|
53
|
+
All existing tests pass, including the specific RecoveryManager tests.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Now you can use directory names (much simpler):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Start a parallel publish
|
|
61
|
+
kodrdriv tree publish --parallel
|
|
62
|
+
|
|
63
|
+
# If git-tools fails due to merge conflict, fix it manually, then:
|
|
64
|
+
kodrdriv tree publish --continue --mark-completed "git-tools"
|
|
65
|
+
|
|
66
|
+
# The remaining packages (ai-service, github-tools, kodrdriv) will now execute
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The system will:
|
|
70
|
+
1. Keep the checkpoint if packages are skipped
|
|
71
|
+
2. Show accurate status messages
|
|
72
|
+
3. Allow you to continue execution to complete the skipped packages
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Git Submodule Lock File Fix
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
The file-based lock mechanism used to prevent concurrent git operations was failing when used with git submodules. The issue occurred because:
|
|
6
|
+
|
|
7
|
+
1. In regular git repositories, `.git` is a directory
|
|
8
|
+
2. In git submodules, `.git` is a file containing a `gitdir:` reference pointing to the actual git directory
|
|
9
|
+
3. The code assumed `.git` was always a directory and tried to create lock files in `.git/kodrdriv.lock`
|
|
10
|
+
4. This caused `ENOTDIR` errors when operating on submodules
|
|
11
|
+
|
|
12
|
+
## Example Submodule Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
my-submodule/
|
|
16
|
+
├── .git # FILE (not directory) containing: "gitdir: ../.git/modules/my-submodule"
|
|
17
|
+
├── src/
|
|
18
|
+
└── package.json
|
|
19
|
+
|
|
20
|
+
parent-repo/
|
|
21
|
+
└── .git/
|
|
22
|
+
└── modules/
|
|
23
|
+
└── my-submodule/ # Actual git directory for the submodule
|
|
24
|
+
├── HEAD
|
|
25
|
+
├── refs/
|
|
26
|
+
└── objects/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Solution
|
|
30
|
+
|
|
31
|
+
Modified `src/util/fileLock.ts` to handle both regular repositories and submodules:
|
|
32
|
+
|
|
33
|
+
### Key Changes
|
|
34
|
+
|
|
35
|
+
1. **Added `resolveGitDirectory()` method** that:
|
|
36
|
+
- Checks if `.git` is a directory (regular repo) or file (submodule)
|
|
37
|
+
- If it's a file, reads and parses the `gitdir:` reference
|
|
38
|
+
- Resolves the gitdir path (handles both relative and absolute paths)
|
|
39
|
+
- Returns the actual git directory path where locks can be created
|
|
40
|
+
|
|
41
|
+
2. **Updated `getRepositoryLock()` method** to:
|
|
42
|
+
- Use `resolveGitDirectory()` instead of assuming `.git` is a directory
|
|
43
|
+
- Create lock files in the resolved git directory
|
|
44
|
+
- Log the actual lock path for debugging
|
|
45
|
+
|
|
46
|
+
### Code Implementation
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
private resolveGitDirectory(repoPath: string): string {
|
|
50
|
+
const gitPath = path.join(repoPath, '.git');
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const stat = fs.statSync(gitPath);
|
|
54
|
+
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
// Regular git repository
|
|
57
|
+
return gitPath;
|
|
58
|
+
} else if (stat.isFile()) {
|
|
59
|
+
// Git submodule - .git is a file with format: gitdir: <path>
|
|
60
|
+
const gitFileContent = fs.readFileSync(gitPath, 'utf-8').trim();
|
|
61
|
+
const match = gitFileContent.match(/^gitdir:\s*(.+)$/);
|
|
62
|
+
|
|
63
|
+
if (match && match[1]) {
|
|
64
|
+
// Resolve the gitdir path (it's relative to the repo path)
|
|
65
|
+
const gitDirPath = path.resolve(repoPath, match[1]);
|
|
66
|
+
this.logger.debug(`Resolved submodule gitdir: ${gitDirPath}`);
|
|
67
|
+
|
|
68
|
+
// Ensure the git directory exists
|
|
69
|
+
if (!fs.existsSync(gitDirPath)) {
|
|
70
|
+
throw new Error(`Submodule git directory does not exist: ${gitDirPath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return gitDirPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`Invalid .git file format in ${gitPath}: ${gitFileContent}`);
|
|
77
|
+
}
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
// Check if error is from statSync (file doesn't exist)
|
|
80
|
+
if (error.code === 'ENOENT') {
|
|
81
|
+
throw new Error(`No .git directory or file found in ${repoPath}`);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Failed to resolve git directory for ${repoPath}: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`No .git directory or file found in ${repoPath}`);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Testing
|
|
91
|
+
|
|
92
|
+
Comprehensive test suite added in `tests/fileLock.test.ts` covering:
|
|
93
|
+
|
|
94
|
+
1. **Basic lock operations**
|
|
95
|
+
- Acquire and release locks
|
|
96
|
+
- Block concurrent lock acquisition
|
|
97
|
+
- Handle stale locks
|
|
98
|
+
|
|
99
|
+
2. **Regular repository support**
|
|
100
|
+
- Create locks in `.git` directory
|
|
101
|
+
|
|
102
|
+
3. **Submodule support** (NEW)
|
|
103
|
+
- Handle `.git` file with `gitdir:` reference
|
|
104
|
+
- Resolve relative gitdir paths
|
|
105
|
+
- Create locks in the actual git directory
|
|
106
|
+
- Proper error handling for missing submodule directories
|
|
107
|
+
- Proper error handling for invalid `.git` file format
|
|
108
|
+
|
|
109
|
+
4. **Lock manager functionality**
|
|
110
|
+
- Execute operations under lock
|
|
111
|
+
- Release locks on operation failure
|
|
112
|
+
- Serialize multiple operations on same repo
|
|
113
|
+
|
|
114
|
+
All 12 tests pass successfully with 86%+ coverage on the fileLock module.
|
|
115
|
+
|
|
116
|
+
## Impact
|
|
117
|
+
|
|
118
|
+
This fix enables kodrdriv to work correctly in monorepo setups where packages are organized as git submodules, such as:
|
|
119
|
+
|
|
120
|
+
- Multi-repository projects using git submodules for shared libraries
|
|
121
|
+
- Projects with external dependencies included as submodules
|
|
122
|
+
- Monorepos with complex submodule hierarchies
|
|
123
|
+
|
|
124
|
+
The fix maintains backward compatibility with regular git repositories while adding robust support for submodules.
|
|
125
|
+
|
|
126
|
+
## Related Files
|
|
127
|
+
|
|
128
|
+
- `src/util/fileLock.ts` - Core fix implementation
|
|
129
|
+
- `tests/fileLock.test.ts` - Comprehensive test suite
|
|
130
|
+
- `src/util/gitMutex.ts` - Uses RepositoryFileLockManager
|
|
131
|
+
- `src/commands/publish.ts` - Uses git locks during publish operations
|
|
132
|
+
|
package/dist/arguments.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import
|
|
2
|
+
import path__default from 'path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { PROGRAM_NAME, VERSION, KODRDRIV_DEFAULTS, ALLOWED_COMMANDS, DEFAULT_COMMAND } from './constants.js';
|
|
5
5
|
import { getLogger } from './logging.js';
|
|
@@ -263,6 +263,29 @@ const transformCliArgs = (finalCliArgs, commandName)=>{
|
|
|
263
263
|
if (packageArgument !== undefined) transformedCliArgs.tree.packageArgument = packageArgument;
|
|
264
264
|
if (finalCliArgs.cleanNodeModules !== undefined) transformedCliArgs.tree.cleanNodeModules = finalCliArgs.cleanNodeModules;
|
|
265
265
|
if (finalCliArgs.externals !== undefined) transformedCliArgs.tree.externals = finalCliArgs.externals;
|
|
266
|
+
// Parallel execution options (using any cast for new properties)
|
|
267
|
+
const cliArgs = finalCliArgs;
|
|
268
|
+
if (cliArgs.parallel !== undefined) transformedCliArgs.tree.parallel = cliArgs.parallel;
|
|
269
|
+
if (cliArgs.maxConcurrency !== undefined) transformedCliArgs.tree.maxConcurrency = cliArgs.maxConcurrency;
|
|
270
|
+
if (cliArgs.maxRetries !== undefined || cliArgs.retryDelay !== undefined) {
|
|
271
|
+
transformedCliArgs.tree.retry = {
|
|
272
|
+
maxAttempts: cliArgs.maxRetries || 3,
|
|
273
|
+
initialDelayMs: cliArgs.retryDelay || 5000,
|
|
274
|
+
maxDelayMs: 60000,
|
|
275
|
+
backoffMultiplier: 2
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// Recovery options
|
|
279
|
+
if (cliArgs.statusParallel !== undefined) transformedCliArgs.tree.statusParallel = cliArgs.statusParallel;
|
|
280
|
+
if (cliArgs.markCompleted !== undefined) {
|
|
281
|
+
transformedCliArgs.tree.markCompleted = cliArgs.markCompleted.split(',').map((s)=>s.trim());
|
|
282
|
+
}
|
|
283
|
+
if (cliArgs.skip !== undefined) {
|
|
284
|
+
transformedCliArgs.tree.skipPackages = cliArgs.skip.split(',').map((s)=>s.trim());
|
|
285
|
+
}
|
|
286
|
+
if (cliArgs.retryFailed !== undefined) transformedCliArgs.tree.retryFailed = cliArgs.retryFailed;
|
|
287
|
+
if (cliArgs.skipFailed !== undefined) transformedCliArgs.tree.skipFailed = cliArgs.skipFailed;
|
|
288
|
+
if (cliArgs.validateState !== undefined) transformedCliArgs.tree.validateState = cliArgs.validateState;
|
|
266
289
|
}
|
|
267
290
|
}
|
|
268
291
|
// Nested mappings for 'development' options
|
|
@@ -364,7 +387,7 @@ const configure = async (cardigantime)=>{
|
|
|
364
387
|
const transformedCliArgs = transformCliArgs(cliArgs);
|
|
365
388
|
// Use CardiganTime's built-in generateConfig method
|
|
366
389
|
const configDir = transformedCliArgs.configDirectory || KODRDRIV_DEFAULTS.configDirectory;
|
|
367
|
-
const absoluteConfigDir =
|
|
390
|
+
const absoluteConfigDir = path__default.isAbsolute(configDir) ? configDir : path__default.resolve(process.cwd(), configDir);
|
|
368
391
|
await cardigantime.generateConfig(absoluteConfigDir);
|
|
369
392
|
// Return minimal config for consistency, but main processing is done
|
|
370
393
|
const config = await validateAndProcessOptions({});
|
|
@@ -598,7 +621,7 @@ async function getCliConfig(program, commands) {
|
|
|
598
621
|
addSharedOptions(releaseCommand);
|
|
599
622
|
const publishCommand = program.command('publish').option('--merge-method <method>', 'method to merge PR (merge, squash, rebase)', 'squash').option('--from <from>', 'branch/tag to generate release notes from (default: previous release tag)').option('--target-version <targetVersion>', 'target version for release (explicit version like "4.30.0" or semantic bump: "patch", "minor", "major")').option('--interactive', 'present release notes for interactive review and editing').option('--sendit', 'skip all confirmation prompts and proceed automatically').option('--sync-target', 'attempt to automatically sync target branch with remote before publishing').option('--no-milestones', 'disable GitHub milestone integration').option('--from-main', 'force comparison against main branch instead of previous release tag').description('Publish a release');
|
|
600
623
|
addSharedOptions(publishCommand);
|
|
601
|
-
const treeCommand = program.command('tree [command] [packageArgument]').option('--directory <directory>', 'target directory containing multiple packages (defaults to current directory)').option('--directories [directories...]', 'target directories containing multiple packages (defaults to current directory)').option('--start-from <startFrom>', 'resume execution from this package directory name (useful for restarting failed builds)').option('--stop-at <stopAt>', 'stop execution at this package directory name (the specified package will not be executed)').option('--cmd <cmd>', 'shell command to execute in each package directory (e.g., "npm install", "git status")').option('--parallel', 'execute packages in parallel when dependencies allow (packages with no interdependencies run simultaneously)').option('--excluded-patterns [excludedPatterns...]', 'patterns to exclude packages from processing (e.g., "**/node_modules/**", "dist/*")').option('--continue', 'continue from previous tree publish execution').option('--status', 'check status of running tree publish processes').option('--promote <packageName>', 'mark a package as completed in the execution context (useful for recovery after timeouts)').option('--clean-node-modules', 'for unlink command: remove node_modules and package-lock.json, then reinstall dependencies').description('Analyze package dependencies in workspace and execute commands in dependency order. Supports built-in commands: commit, publish, link, unlink, development, branches, run, checkout');
|
|
624
|
+
const treeCommand = program.command('tree [command] [packageArgument]').option('--directory <directory>', 'target directory containing multiple packages (defaults to current directory)').option('--directories [directories...]', 'target directories containing multiple packages (defaults to current directory)').option('--start-from <startFrom>', 'resume execution from this package directory name (useful for restarting failed builds)').option('--stop-at <stopAt>', 'stop execution at this package directory name (the specified package will not be executed)').option('--cmd <cmd>', 'shell command to execute in each package directory (e.g., "npm install", "git status")').option('--parallel', 'execute packages in parallel when dependencies allow (packages with no interdependencies run simultaneously)').option('--max-concurrency <number>', 'maximum number of packages to execute concurrently (default: number of CPU cores)', parseInt).option('--max-retries <number>', 'maximum retry attempts for failed packages (default: 3)', parseInt).option('--retry-delay <ms>', 'initial retry delay in milliseconds (default: 5000)', parseInt).option('--excluded-patterns [excludedPatterns...]', 'patterns to exclude packages from processing (e.g., "**/node_modules/**", "dist/*")').option('--continue', 'continue from previous tree publish execution').option('--status', 'check status of running tree publish processes').option('--status-parallel', 'show detailed parallel execution status').option('--promote <packageName>', 'mark a package as completed in the execution context (useful for recovery after timeouts)').option('--mark-completed <packages>', 'mark packages as completed using directory names (comma-separated, for recovery)').option('--skip <packages>', 'skip packages and their dependents (comma-separated)').option('--retry-failed', 'retry all previously failed packages').option('--skip-failed', 'skip failed packages and continue with remaining').option('--validate-state', 'validate checkpoint state integrity').option('--clean-node-modules', 'for unlink command: remove node_modules and package-lock.json, then reinstall dependencies').description('Analyze package dependencies in workspace and execute commands in dependency order. Supports built-in commands: commit, publish, link, unlink, development, branches, run, checkout');
|
|
602
625
|
addSharedOptions(treeCommand);
|
|
603
626
|
const linkCommand = program.command('link [packageArgument]').option('--scope-roots <scopeRoots>', 'JSON mapping of scopes to root directories (e.g., \'{"@company": "../"}\')').description('Create npm file: dependencies for local development');
|
|
604
627
|
addSharedOptions(linkCommand);
|