@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.
Files changed (67) hide show
  1. package/PARALLEL-EXECUTION-FIXES.md +132 -0
  2. package/PARALLEL_EXECUTION_FIX.md +146 -0
  3. package/RECOVERY-FIXES.md +72 -0
  4. package/SUBMODULE-LOCK-FIX.md +132 -0
  5. package/dist/arguments.js +26 -3
  6. package/dist/arguments.js.map +1 -1
  7. package/dist/commands/audio-commit.js +3 -3
  8. package/dist/commands/audio-commit.js.map +1 -1
  9. package/dist/commands/audio-review.js +13 -13
  10. package/dist/commands/audio-review.js.map +1 -1
  11. package/dist/commands/link.js +13 -13
  12. package/dist/commands/link.js.map +1 -1
  13. package/dist/commands/publish.js +200 -146
  14. package/dist/commands/publish.js.map +1 -1
  15. package/dist/commands/review.js +6 -6
  16. package/dist/commands/review.js.map +1 -1
  17. package/dist/commands/select-audio.js +4 -4
  18. package/dist/commands/select-audio.js.map +1 -1
  19. package/dist/commands/tree.js +242 -318
  20. package/dist/commands/tree.js.map +1 -1
  21. package/dist/commands/unlink.js +8 -8
  22. package/dist/commands/unlink.js.map +1 -1
  23. package/dist/commands/versions.js +3 -3
  24. package/dist/commands/versions.js.map +1 -1
  25. package/dist/constants.js +4 -4
  26. package/dist/constants.js.map +1 -1
  27. package/dist/content/diff.js +5 -2
  28. package/dist/content/diff.js.map +1 -1
  29. package/dist/content/files.js +4 -4
  30. package/dist/content/files.js.map +1 -1
  31. package/dist/execution/CommandValidator.js +160 -0
  32. package/dist/execution/CommandValidator.js.map +1 -0
  33. package/dist/execution/DependencyChecker.js +102 -0
  34. package/dist/execution/DependencyChecker.js.map +1 -0
  35. package/dist/execution/DynamicTaskPool.js +455 -0
  36. package/dist/execution/DynamicTaskPool.js.map +1 -0
  37. package/dist/execution/RecoveryManager.js +502 -0
  38. package/dist/execution/RecoveryManager.js.map +1 -0
  39. package/dist/execution/ResourceMonitor.js +125 -0
  40. package/dist/execution/ResourceMonitor.js.map +1 -0
  41. package/dist/execution/Scheduler.js +98 -0
  42. package/dist/execution/Scheduler.js.map +1 -0
  43. package/dist/execution/TreeExecutionAdapter.js +170 -0
  44. package/dist/execution/TreeExecutionAdapter.js.map +1 -0
  45. package/dist/logging.js +3 -3
  46. package/dist/logging.js.map +1 -1
  47. package/dist/ui/ProgressFormatter.js +230 -0
  48. package/dist/ui/ProgressFormatter.js.map +1 -0
  49. package/dist/util/checkpointManager.js +168 -0
  50. package/dist/util/checkpointManager.js.map +1 -0
  51. package/dist/util/dependencyGraph.js +224 -0
  52. package/dist/util/dependencyGraph.js.map +1 -0
  53. package/dist/util/fileLock.js +241 -0
  54. package/dist/util/fileLock.js.map +1 -0
  55. package/dist/util/general.js +5 -5
  56. package/dist/util/general.js.map +1 -1
  57. package/dist/util/gitMutex.js +116 -0
  58. package/dist/util/gitMutex.js.map +1 -0
  59. package/dist/util/mutex.js +96 -0
  60. package/dist/util/mutex.js.map +1 -0
  61. package/dist/util/performance.js +4 -4
  62. package/dist/util/performance.js.map +1 -1
  63. package/dist/util/safety.js +4 -4
  64. package/dist/util/safety.js.map +1 -1
  65. package/dist/util/storage.js +2 -2
  66. package/dist/util/storage.js.map +1 -1
  67. package/package.json +6 -6
@@ -1,7 +1,7 @@
1
- import path from 'path';
2
- import { execute as execute$1 } from './commit.js';
1
+ import path__default from 'path';
2
+ import { execute as execute$2 } from './commit.js';
3
3
  import { hasStagedChanges } from '../content/diff.js';
4
- import { execute as execute$2 } from './release.js';
4
+ import { execute as execute$1 } from './release.js';
5
5
  import { getDryRunLogger, getLogger } from '../logging.js';
6
6
  import { run, localBranchExists, runSecure, runWithDryRunSupport, safeJsonParse, validatePackageJson, validateGitRef, safeSyncBranchWithRemote, isBranchInSyncWithRemote } from '@eldrforge/git-tools';
7
7
  import * as GitHub from '@eldrforge/github-tools';
@@ -9,10 +9,11 @@ import { create } from '../util/storage.js';
9
9
  import { calculateBranchDependentVersion, calculateTargetVersion, checkIfTagExists, confirmVersionInteractively, getOutputPath } from '../util/general.js';
10
10
  import { DEFAULT_OUTPUT_DIRECTORY, KODRDRIV_DEFAULTS } from '../constants.js';
11
11
  import fs from 'fs/promises';
12
+ import { runGitWithLock } from '../util/gitMutex.js';
12
13
 
13
14
  const scanNpmrcForEnvVars = async (storage)=>{
14
15
  const logger = getLogger();
15
- const npmrcPath = path.join(process.cwd(), '.npmrc');
16
+ const npmrcPath = path__default.join(process.cwd(), '.npmrc');
16
17
  const envVars = [];
17
18
  if (await storage.exists(npmrcPath)) {
18
19
  try {
@@ -42,7 +43,7 @@ const scanNpmrcForEnvVars = async (storage)=>{
42
43
  * and cleans them up if found by removing package-lock.json and regenerating it.
43
44
  */ const cleanupNpmLinkReferences = async (isDryRun)=>{
44
45
  const logger = getDryRunLogger(isDryRun);
45
- const packageLockPath = path.join(process.cwd(), 'package-lock.json');
46
+ const packageLockPath = path__default.join(process.cwd(), 'package-lock.json');
46
47
  try {
47
48
  // Check if package-lock.json exists
48
49
  try {
@@ -238,7 +239,7 @@ const runPrechecks = async (runConfig, targetBranch)=>{
238
239
  }
239
240
  // Check if prepublishOnly script exists in package.json
240
241
  logger.info('Checking for prepublishOnly script...');
241
- const packageJsonPath = path.join(process.cwd(), 'package.json');
242
+ const packageJsonPath = path__default.join(process.cwd(), 'package.json');
242
243
  if (!await storage.exists(packageJsonPath)) {
243
244
  if (!isDryRun) {
244
245
  throw new Error('package.json not found in current directory.');
@@ -433,7 +434,10 @@ const execute = async (runConfig)=>{
433
434
  try {
434
435
  const remoteExists = await run(`git ls-remote --exit-code --heads origin ${currentBranch}`).then(()=>true).catch(()=>false);
435
436
  if (remoteExists) {
436
- await run(`git pull origin ${currentBranch} --no-edit`);
437
+ // Wrap git pull with lock to prevent concurrent pulls in same repo
438
+ await runGitWithLock(process.cwd(), async ()=>{
439
+ await run(`git pull origin ${currentBranch} --no-edit`);
440
+ }, `pull ${currentBranch}`);
437
441
  logger.info(`✅ Synced ${currentBranch} with remote`);
438
442
  } else {
439
443
  logger.info(`â„šī¸ No remote ${currentBranch} branch found, will be created on first push`);
@@ -490,20 +494,23 @@ const execute = async (runConfig)=>{
490
494
  if (!targetBranchExists) {
491
495
  logger.info(`🌟 Target branch '${targetBranch}' does not exist, creating it from current branch...`);
492
496
  try {
493
- // Create the target branch from the current HEAD
494
- await runSecure('git', [
495
- 'branch',
496
- targetBranch,
497
- 'HEAD'
498
- ]);
499
- logger.info(`✅ Created target branch: ${targetBranch}`);
500
- // Push the new branch to origin
501
- await runSecure('git', [
502
- 'push',
503
- 'origin',
504
- targetBranch
505
- ]);
506
- logger.info(`✅ Pushed new target branch to origin: ${targetBranch}`);
497
+ // Wrap git branch and push operations with lock
498
+ await runGitWithLock(process.cwd(), async ()=>{
499
+ // Create the target branch from the current HEAD
500
+ await runSecure('git', [
501
+ 'branch',
502
+ targetBranch,
503
+ 'HEAD'
504
+ ]);
505
+ logger.info(`✅ Created target branch: ${targetBranch}`);
506
+ // Push the new branch to origin
507
+ await runSecure('git', [
508
+ 'push',
509
+ 'origin',
510
+ targetBranch
511
+ ]);
512
+ logger.info(`✅ Pushed new target branch to origin: ${targetBranch}`);
513
+ }, `create and push target branch ${targetBranch}`);
507
514
  } catch (error) {
508
515
  throw new Error(`Failed to create target branch '${targetBranch}': ${error.message}`);
509
516
  }
@@ -565,14 +572,20 @@ const execute = async (runConfig)=>{
565
572
  // Check if package-lock.json exists before trying to stage it
566
573
  const packageLockExists = await storage.exists('package-lock.json');
567
574
  const filesToStage = packageLockExists ? 'package.json package-lock.json' : 'package.json';
568
- await runWithDryRunSupport(`git add ${filesToStage}`, isDryRun);
575
+ // Wrap git operations with repository lock to prevent .git/index.lock conflicts
576
+ await runGitWithLock(process.cwd(), async ()=>{
577
+ await runWithDryRunSupport(`git add ${filesToStage}`, isDryRun);
578
+ }, 'stage dependency updates');
569
579
  logger.verbose('Checking for staged dependency updates...');
570
580
  if (isDryRun) {
571
581
  logger.verbose('Would create dependency update commit if changes are staged');
572
582
  } else {
573
583
  if (await hasStagedChanges()) {
574
584
  logger.verbose('Staged dependency changes found, creating commit...');
575
- await execute$1(runConfig);
585
+ // Commit also needs git lock
586
+ await runGitWithLock(process.cwd(), async ()=>{
587
+ await execute$2(runConfig);
588
+ }, 'commit dependency updates');
576
589
  } else {
577
590
  logger.verbose('No dependency changes to commit, skipping commit.');
578
591
  }
@@ -586,102 +599,105 @@ const execute = async (runConfig)=>{
586
599
  if (isDryRun) {
587
600
  logger.info(`Would merge ${targetBranch} into current branch`);
588
601
  } else {
589
- // Fetch the latest target branch
590
- try {
591
- await run(`git fetch origin ${targetBranch}:${targetBranch}`);
592
- logger.info(`✅ Fetched latest ${targetBranch}`);
593
- } catch (fetchError) {
594
- logger.warn(`âš ī¸ Could not fetch ${targetBranch}: ${fetchError.message}`);
595
- logger.warn('Continuing without merge - PR may have conflicts...');
596
- }
597
- // Check if merge is needed (avoid unnecessary merge commits)
598
- try {
599
- const { stdout: mergeBase } = await run(`git merge-base HEAD ${targetBranch}`);
600
- const { stdout: targetCommit } = await run(`git rev-parse ${targetBranch}`);
601
- if (mergeBase.trim() === targetCommit.trim()) {
602
- logger.info(`â„šī¸ Already up-to-date with ${targetBranch}, no merge needed`);
603
- } else {
604
- // Try to merge target branch into current branch
605
- let mergeSucceeded = false;
606
- try {
607
- await run(`git merge ${targetBranch} --no-edit -m "Merge ${targetBranch} to sync before version bump"`);
608
- logger.info(`✅ Merged ${targetBranch} into current branch`);
609
- mergeSucceeded = true;
610
- } catch (mergeError) {
611
- // If merge conflicts occur, check if they're only in version-related files
612
- const errorText = [
613
- mergeError.message || '',
614
- mergeError.stdout || '',
615
- mergeError.stderr || ''
616
- ].join(' ');
617
- if (errorText.includes('CONFLICT')) {
618
- logger.warn(`âš ī¸ Merge conflicts detected, attempting automatic resolution...`);
619
- // Get list of conflicted files
620
- const { stdout: conflictedFiles } = await run('git diff --name-only --diff-filter=U');
621
- const conflicts = conflictedFiles.trim().split('\n').filter(Boolean);
622
- logger.verbose(`Conflicted files: ${conflicts.join(', ')}`);
623
- // Check if conflicts are only in package.json and package-lock.json
624
- const versionFiles = [
625
- 'package.json',
626
- 'package-lock.json'
627
- ];
628
- const nonVersionConflicts = conflicts.filter((f)=>!versionFiles.includes(f));
629
- if (nonVersionConflicts.length > 0) {
630
- logger.error(`❌ Cannot auto-resolve: conflicts in non-version files: ${nonVersionConflicts.join(', ')}`);
631
- logger.error('');
632
- logger.error('Please resolve conflicts manually:');
633
- logger.error(' 1. Resolve conflicts in the files listed above');
634
- logger.error(' 2. git add <resolved-files>');
635
- logger.error(' 3. git commit');
636
- logger.error(' 4. kodrdriv publish (to continue)');
637
- logger.error('');
638
- throw new Error(`Merge conflicts in non-version files. Please resolve manually.`);
639
- }
640
- // Auto-resolve version conflicts by accepting current branch versions
641
- // (keep our working branch's version, which is likely already updated)
642
- logger.info(`Auto-resolving version conflicts by keeping current branch versions...`);
643
- for (const file of conflicts){
644
- if (versionFiles.includes(file)) {
645
- await run(`git checkout --ours ${file}`);
646
- await run(`git add ${file}`);
647
- logger.verbose(`Resolved ${file} using current branch version`);
602
+ // Wrap entire merge process with git lock (involves fetch, merge, checkout, add, commit)
603
+ await runGitWithLock(process.cwd(), async ()=>{
604
+ // Fetch the latest target branch
605
+ try {
606
+ await run(`git fetch origin ${targetBranch}:${targetBranch}`);
607
+ logger.info(`✅ Fetched latest ${targetBranch}`);
608
+ } catch (fetchError) {
609
+ logger.warn(`âš ī¸ Could not fetch ${targetBranch}: ${fetchError.message}`);
610
+ logger.warn('Continuing without merge - PR may have conflicts...');
611
+ }
612
+ // Check if merge is needed (avoid unnecessary merge commits)
613
+ try {
614
+ const { stdout: mergeBase } = await run(`git merge-base HEAD ${targetBranch}`);
615
+ const { stdout: targetCommit } = await run(`git rev-parse ${targetBranch}`);
616
+ if (mergeBase.trim() === targetCommit.trim()) {
617
+ logger.info(`â„šī¸ Already up-to-date with ${targetBranch}, no merge needed`);
618
+ } else {
619
+ // Try to merge target branch into current branch
620
+ let mergeSucceeded = false;
621
+ try {
622
+ await run(`git merge ${targetBranch} --no-edit -m "Merge ${targetBranch} to sync before version bump"`);
623
+ logger.info(`✅ Merged ${targetBranch} into current branch`);
624
+ mergeSucceeded = true;
625
+ } catch (mergeError) {
626
+ // If merge conflicts occur, check if they're only in version-related files
627
+ const errorText = [
628
+ mergeError.message || '',
629
+ mergeError.stdout || '',
630
+ mergeError.stderr || ''
631
+ ].join(' ');
632
+ if (errorText.includes('CONFLICT')) {
633
+ logger.warn(`âš ī¸ Merge conflicts detected, attempting automatic resolution...`);
634
+ // Get list of conflicted files
635
+ const { stdout: conflictedFiles } = await run('git diff --name-only --diff-filter=U');
636
+ const conflicts = conflictedFiles.trim().split('\n').filter(Boolean);
637
+ logger.verbose(`Conflicted files: ${conflicts.join(', ')}`);
638
+ // Check if conflicts are only in package.json and package-lock.json
639
+ const versionFiles = [
640
+ 'package.json',
641
+ 'package-lock.json'
642
+ ];
643
+ const nonVersionConflicts = conflicts.filter((f)=>!versionFiles.includes(f));
644
+ if (nonVersionConflicts.length > 0) {
645
+ logger.error(`❌ Cannot auto-resolve: conflicts in non-version files: ${nonVersionConflicts.join(', ')}`);
646
+ logger.error('');
647
+ logger.error('Please resolve conflicts manually:');
648
+ logger.error(' 1. Resolve conflicts in the files listed above');
649
+ logger.error(' 2. git add <resolved-files>');
650
+ logger.error(' 3. git commit');
651
+ logger.error(' 4. kodrdriv publish (to continue)');
652
+ logger.error('');
653
+ throw new Error(`Merge conflicts in non-version files. Please resolve manually.`);
654
+ }
655
+ // Auto-resolve version conflicts by accepting current branch versions
656
+ // (keep our working branch's version, which is likely already updated)
657
+ logger.info(`Auto-resolving version conflicts by keeping current branch versions...`);
658
+ for (const file of conflicts){
659
+ if (versionFiles.includes(file)) {
660
+ await run(`git checkout --ours ${file}`);
661
+ await run(`git add ${file}`);
662
+ logger.verbose(`Resolved ${file} using current branch version`);
663
+ }
648
664
  }
665
+ // Complete the merge
666
+ await run(`git commit --no-edit -m "Merge ${targetBranch} to sync before version bump (auto-resolved version conflicts)"`);
667
+ logger.info(`✅ Auto-resolved version conflicts and completed merge`);
668
+ mergeSucceeded = true;
669
+ } else {
670
+ // Not a conflict error, re-throw
671
+ throw mergeError;
649
672
  }
650
- // Complete the merge
651
- await run(`git commit --no-edit -m "Merge ${targetBranch} to sync before version bump (auto-resolved version conflicts)"`);
652
- logger.info(`✅ Auto-resolved version conflicts and completed merge`);
653
- mergeSucceeded = true;
654
- } else {
655
- // Not a conflict error, re-throw
656
- throw mergeError;
657
673
  }
658
- }
659
- // Only run npm install if merge actually happened
660
- if (mergeSucceeded) {
661
- // Run npm install to update package-lock.json based on merged package.json
662
- logger.info('Running npm install after merge...');
663
- await run('npm install');
664
- logger.info('✅ npm install completed');
665
- // Commit any changes from npm install (e.g., package-lock.json updates)
666
- const { stdout: mergeChangesStatus } = await run('git status --porcelain');
667
- if (mergeChangesStatus.trim()) {
668
- logger.verbose('Staging post-merge changes for commit');
669
- // Check if package-lock.json exists before trying to stage it
670
- const packageLockExistsPostMerge = await storage.exists('package-lock.json');
671
- const filesToStagePostMerge = packageLockExistsPostMerge ? 'package.json package-lock.json' : 'package.json';
672
- await run(`git add ${filesToStagePostMerge}`);
673
- if (await hasStagedChanges()) {
674
- logger.verbose('Committing post-merge changes...');
675
- await execute$1(runConfig);
674
+ // Only run npm install if merge actually happened
675
+ if (mergeSucceeded) {
676
+ // Run npm install to update package-lock.json based on merged package.json
677
+ logger.info('Running npm install after merge...');
678
+ await run('npm install');
679
+ logger.info('✅ npm install completed');
680
+ // Commit any changes from npm install (e.g., package-lock.json updates)
681
+ const { stdout: mergeChangesStatus } = await run('git status --porcelain');
682
+ if (mergeChangesStatus.trim()) {
683
+ logger.verbose('Staging post-merge changes for commit');
684
+ // Check if package-lock.json exists before trying to stage it
685
+ const packageLockExistsPostMerge = await storage.exists('package-lock.json');
686
+ const filesToStagePostMerge = packageLockExistsPostMerge ? 'package.json package-lock.json' : 'package.json';
687
+ await run(`git add ${filesToStagePostMerge}`);
688
+ if (await hasStagedChanges()) {
689
+ logger.verbose('Committing post-merge changes...');
690
+ await execute$2(runConfig);
691
+ }
676
692
  }
677
693
  }
678
694
  }
695
+ } catch (error) {
696
+ // Only catch truly unexpected errors here
697
+ logger.error(`❌ Unexpected error during merge: ${error.message}`);
698
+ throw error;
679
699
  }
680
- } catch (error) {
681
- // Only catch truly unexpected errors here
682
- logger.error(`❌ Unexpected error during merge: ${error.message}`);
683
- throw error;
684
- }
700
+ }, `merge ${targetBranch} into current branch`);
685
701
  }
686
702
  }
687
703
  // STEP 4: Determine and set target version AFTER checks, dependency commit, and target branch merge
@@ -739,13 +755,18 @@ const execute = async (runConfig)=>{
739
755
  // Check if package-lock.json exists before trying to stage it
740
756
  const packageLockExistsVersionBump = await storage.exists('package-lock.json');
741
757
  const filesToStageVersionBump = packageLockExistsVersionBump ? 'package.json package-lock.json' : 'package.json';
742
- await runWithDryRunSupport(`git add ${filesToStageVersionBump}`, isDryRun);
758
+ // Wrap git operations with lock
759
+ await runGitWithLock(process.cwd(), async ()=>{
760
+ await runWithDryRunSupport(`git add ${filesToStageVersionBump}`, isDryRun);
761
+ }, 'stage version bump');
743
762
  if (isDryRun) {
744
763
  logger.verbose('Would create version bump commit');
745
764
  } else {
746
765
  if (await hasStagedChanges()) {
747
766
  logger.verbose('Creating version bump commit...');
748
- await execute$1(runConfig);
767
+ await runGitWithLock(process.cwd(), async ()=>{
768
+ await execute$2(runConfig);
769
+ }, 'commit version bump');
749
770
  } else {
750
771
  logger.verbose('No version changes to commit.');
751
772
  }
@@ -779,7 +800,7 @@ const execute = async (runConfig)=>{
779
800
  if ((_runConfig_publish11 = runConfig.publish) === null || _runConfig_publish11 === void 0 ? void 0 : _runConfig_publish11.fromMain) {
780
801
  logger.verbose('Forcing comparison against main branch for release notes');
781
802
  }
782
- const releaseSummary = await execute$2(releaseConfig);
803
+ const releaseSummary = await execute$1(releaseConfig);
783
804
  if (isDryRun) {
784
805
  logger.info('Would write release notes to RELEASE_NOTES.md and RELEASE_TITLE.md in output directory');
785
806
  } else {
@@ -794,7 +815,10 @@ const execute = async (runConfig)=>{
794
815
  logger.info('Pushing to origin...');
795
816
  // Get current branch name and push explicitly to avoid pushing to wrong remote/branch
796
817
  const branchName = await GitHub.getCurrentBranchName();
797
- await runWithDryRunSupport(`git push origin ${branchName}`, isDryRun);
818
+ // Wrap git push with lock
819
+ await runGitWithLock(process.cwd(), async ()=>{
820
+ await runWithDryRunSupport(`git push origin ${branchName}`, isDryRun);
821
+ }, `push ${branchName}`);
798
822
  logger.info('Creating pull request...');
799
823
  if (isDryRun) {
800
824
  logger.info('Would get commit title and create PR with GitHub API');
@@ -889,14 +913,19 @@ const execute = async (runConfig)=>{
889
913
  }
890
914
  }
891
915
  try {
892
- await runWithDryRunSupport(`git checkout ${targetBranch}`, isDryRun);
916
+ // Wrap git checkout and pull with lock
917
+ await runGitWithLock(process.cwd(), async ()=>{
918
+ await runWithDryRunSupport(`git checkout ${targetBranch}`, isDryRun);
919
+ }, `checkout ${targetBranch}`);
893
920
  // Sync target branch with remote to avoid conflicts during PR creation
894
921
  if (!isDryRun) {
895
922
  logger.info(`🔄 Syncing ${targetBranch} with remote to avoid PR conflicts...`);
896
923
  try {
897
924
  const remoteExists = await run(`git ls-remote --exit-code --heads origin ${targetBranch}`).then(()=>true).catch(()=>false);
898
925
  if (remoteExists) {
899
- await run(`git pull origin ${targetBranch} --no-edit`);
926
+ await runGitWithLock(process.cwd(), async ()=>{
927
+ await run(`git pull origin ${targetBranch} --no-edit`);
928
+ }, `pull ${targetBranch}`);
900
929
  logger.info(`✅ Synced ${targetBranch} with remote`);
901
930
  } else {
902
931
  logger.info(`â„šī¸ No remote ${targetBranch} branch found, will be created on first push`);
@@ -979,18 +1008,22 @@ const execute = async (runConfig)=>{
979
1008
  if (stdout.trim() === tagName) {
980
1009
  logger.info(`Tag ${tagName} already exists locally, skipping tag creation`);
981
1010
  } else {
982
- await runSecure('git', [
983
- 'tag',
984
- tagName
985
- ]);
1011
+ await runGitWithLock(process.cwd(), async ()=>{
1012
+ await runSecure('git', [
1013
+ 'tag',
1014
+ tagName
1015
+ ]);
1016
+ }, `create tag ${tagName}`);
986
1017
  logger.info(`Created local tag: ${tagName}`);
987
1018
  }
988
1019
  } catch (error) {
989
1020
  // If git tag -l fails, create the tag anyway
990
- await runSecure('git', [
991
- 'tag',
992
- tagName
993
- ]);
1021
+ await runGitWithLock(process.cwd(), async ()=>{
1022
+ await runSecure('git', [
1023
+ 'tag',
1024
+ tagName
1025
+ ]);
1026
+ }, `create tag ${tagName}`);
994
1027
  logger.info(`Created local tag: ${tagName}`);
995
1028
  }
996
1029
  // Check if tag exists on remote before pushing
@@ -1004,11 +1037,13 @@ const execute = async (runConfig)=>{
1004
1037
  if (stdout.trim()) {
1005
1038
  logger.info(`Tag ${tagName} already exists on remote, skipping push`);
1006
1039
  } else {
1007
- await runSecure('git', [
1008
- 'push',
1009
- 'origin',
1010
- tagName
1011
- ]);
1040
+ await runGitWithLock(process.cwd(), async ()=>{
1041
+ await runSecure('git', [
1042
+ 'push',
1043
+ 'origin',
1044
+ tagName
1045
+ ]);
1046
+ }, `push tag ${tagName}`);
1012
1047
  logger.info(`Pushed tag to remote: ${tagName}`);
1013
1048
  tagWasPushed = true;
1014
1049
  }
@@ -1127,22 +1162,35 @@ const execute = async (runConfig)=>{
1127
1162
  logger.info(`🔄 Syncing source branch with target after publish...`);
1128
1163
  await runWithDryRunSupport(`git checkout ${currentBranch}`, isDryRun);
1129
1164
  if (!isDryRun) {
1130
- // Merge target into source
1165
+ // Sync target into source
1131
1166
  // Note: With squash merging, fast-forward will fail because commit histories diverge
1132
- logger.info(`Merging ${targetBranch} into ${currentBranch}...`);
1133
- // Try fast-forward first (works with merge/rebase methods)
1134
- const fastForwardResult = await run(`git merge ${targetBranch} --ff-only`).catch(()=>null);
1135
- if (fastForwardResult) {
1136
- logger.info(`✅ Fast-forward merged ${targetBranch} into ${currentBranch}`);
1167
+ if (mergeMethod === 'squash') {
1168
+ // For squash merges, reset to target branch to avoid conflicts
1169
+ // The squash merge created a single commit on target that represents all source commits
1170
+ logger.info(`Resetting ${currentBranch} to ${targetBranch} (squash merge)...`);
1171
+ await run(`git reset --hard ${targetBranch}`);
1172
+ logger.info(`✅ Reset ${currentBranch} to ${targetBranch}`);
1137
1173
  } else {
1138
- // Fast-forward failed - expected when using squash merge method
1139
- if (mergeMethod === 'squash') {
1140
- logger.verbose('Fast-forward not possible (expected with squash merge), performing regular merge...');
1141
- } else {
1142
- logger.warn(`âš ī¸ Fast-forward merge failed, performing regular merge...`);
1174
+ // For merge/rebase methods, try to merge target back into source
1175
+ logger.info(`Merging ${targetBranch} into ${currentBranch}...`);
1176
+ // Try fast-forward first (works with merge/rebase methods)
1177
+ // Use runSecure to avoid error output for expected failure
1178
+ let fastForwardSucceeded = false;
1179
+ try {
1180
+ await runSecure('git', [
1181
+ 'merge',
1182
+ targetBranch,
1183
+ '--ff-only'
1184
+ ]);
1185
+ fastForwardSucceeded = true;
1186
+ logger.info(`✅ Fast-forward merged ${targetBranch} into ${currentBranch}`);
1187
+ } catch {
1188
+ logger.verbose(`Fast-forward merge not possible, performing regular merge...`);
1189
+ }
1190
+ if (!fastForwardSucceeded) {
1191
+ await run(`git merge ${targetBranch} --no-edit`);
1192
+ logger.info(`✅ Merged ${targetBranch} into ${currentBranch}`);
1143
1193
  }
1144
- await run(`git merge ${targetBranch} --no-edit`);
1145
- logger.info(`✅ Merged ${targetBranch} into ${currentBranch}`);
1146
1194
  }
1147
1195
  // Determine version bump based on branch configuration
1148
1196
  let versionCommand = 'prepatch'; // Default
@@ -1171,14 +1219,20 @@ const execute = async (runConfig)=>{
1171
1219
  // Push updated source branch
1172
1220
  logger.info(`Pushing updated ${currentBranch} branch...`);
1173
1221
  try {
1174
- await run(`git push origin ${currentBranch}`);
1222
+ await runGitWithLock(process.cwd(), async ()=>{
1223
+ await run(`git push origin ${currentBranch}`);
1224
+ }, `push ${currentBranch}`);
1175
1225
  logger.info(`✅ Pushed ${currentBranch} to origin`);
1176
1226
  } catch (pushError) {
1177
1227
  logger.warn(`âš ī¸ Failed to push ${currentBranch}: ${pushError.message}`);
1178
1228
  logger.warn(` Please push manually: git push origin ${currentBranch}`);
1179
1229
  }
1180
1230
  } else {
1181
- logger.info(`Would merge ${targetBranch} into ${currentBranch} with --ff-only`);
1231
+ if (mergeMethod === 'squash') {
1232
+ logger.info(`Would reset ${currentBranch} to ${targetBranch} (squash merge)`);
1233
+ } else {
1234
+ logger.info(`Would merge ${targetBranch} into ${currentBranch} with --ff-only`);
1235
+ }
1182
1236
  logger.info(`Would bump version to next development version`);
1183
1237
  logger.info(`Would push ${currentBranch} to origin`);
1184
1238
  }