phantomx-tool-client 1.1.4 → 1.1.5

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.
@@ -64,6 +64,19 @@ function executeShell(command) {
64
64
  });
65
65
  });
66
66
  }
67
+ // ---------------------------------------------------------------------------
68
+ // Per-repo async mutex — serialises all git remote set-url operations so that
69
+ // concurrent tool calls never race on .git/config.lock for the same repo.
70
+ // ---------------------------------------------------------------------------
71
+ const repoLocks = new Map();
72
+ function withRepoLock(repoName, fn) {
73
+ var _a;
74
+ const prev = (_a = repoLocks.get(repoName)) !== null && _a !== void 0 ? _a : Promise.resolve();
75
+ let releaseLock;
76
+ const lockAcquired = new Promise(res => { releaseLock = res; });
77
+ repoLocks.set(repoName, prev.then(() => lockAcquired));
78
+ return prev.then(() => fn()).finally(() => releaseLock());
79
+ }
67
80
  // Use the official 'octokit' package import via require to avoid missing types for '@octokit/rest'
68
81
  const { Octokit } = require('octokit');
69
82
  const logger = (0, Logger_1.createLogger)('GithubOperationsService');
@@ -597,25 +610,26 @@ class GithubOperationsService {
597
610
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
598
611
  const installationToken = await fetchInstallationToken();
599
612
  const gitOrgName = await getGithubOrganizationName();
600
- try {
601
- logger.info('Getting repository origin/default branch', { repoName });
602
- // Set authenticated remote URL
603
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
604
- // Fetch remote HEAD to find the default branch
605
- const result = await executeShell(`cd ${repoPath} && sudo git remote show origin | grep 'HEAD branch' | awk '{print $NF}'`);
606
- // Restore unauthenticated remote URL
607
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
608
- if (!result.success || !result.output.trim()) {
609
- throw new Error(`Failed to get origin branch for repo '${repoName}': ${result.error || result.output}`);
610
- }
611
- const originBranch = result.output.trim();
612
- logger.success('Repository origin branch retrieved', { repoName, originBranch });
613
- return {
614
- repoName,
615
- originBranch
616
- };
617
- }
618
- finally { }
613
+ return withRepoLock(repoName, async () => {
614
+ try {
615
+ logger.info('Getting repository origin/default branch', { repoName });
616
+ // Set token URL, query origin, restore URL single pipeline to prevent lock races.
617
+ const result = await executeShell(`cd ${repoPath} && rm -f .git/config.lock && ` +
618
+ `git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git && ` +
619
+ `sudo git remote show origin | grep 'HEAD branch' | awk '{print $NF}' && ` +
620
+ `git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
621
+ if (!result.success || !result.output.trim()) {
622
+ throw new Error(`Failed to get origin branch for repo '${repoName}': ${result.error || result.output}`);
623
+ }
624
+ const originBranch = result.output.trim();
625
+ logger.success('Repository origin branch retrieved', { repoName, originBranch });
626
+ return {
627
+ repoName,
628
+ originBranch
629
+ };
630
+ }
631
+ finally { }
632
+ }); // end withRepoLock
619
633
  }
620
634
  /**
621
635
  * List branches for a repository
@@ -636,40 +650,38 @@ class GithubOperationsService {
636
650
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
637
651
  const installationToken = await fetchInstallationToken();
638
652
  const gitOrgName = await getGithubOrganizationName();
639
- try {
640
- // Get current local branch name
641
- let result = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
642
- if (!result.success) {
643
- throw new Error(`Failed to get current branch: ${result.error}`);
644
- }
645
- const currentBranch = result.output.trim();
646
- logger.info('starting git pull operation', { repoName, currentBranch });
647
- // Set remote url with token
648
- result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
649
- if (!result.success) {
650
- throw new Error(`Failed to set remote URL: ${result.error}`);
651
- }
652
- result = await executeShell(`cd ${repoPath} && sudo git fetch origin`);
653
- if (!result.success) {
654
- throw new Error(`Failed to fetch from remote: ${result.error}`);
653
+ return withRepoLock(repoName, async () => {
654
+ try {
655
+ // Get current local branch name
656
+ let result = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
657
+ if (!result.success) {
658
+ throw new Error(`Failed to get current branch: ${result.error}`);
659
+ }
660
+ const currentBranch = result.output.trim();
661
+ logger.info('starting git pull operation', { repoName, currentBranch });
662
+ // Token-URL, fetch, pull, restore URL all in one pipeline to prevent
663
+ // concurrent operations from racing on .git/config.lock.
664
+ result = await executeShell(`cd ${repoPath} && rm -f .git/config.lock && ` +
665
+ `git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git && ` +
666
+ `sudo git fetch origin && ` +
667
+ `sudo git pull --no-rebase origin ${currentBranch} ; ` +
668
+ `git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
669
+ if (!result.success && result.code !== 128) {
670
+ throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
671
+ }
672
+ logger.success('Git pull completed successfully', { repoName, currentBranch });
673
+ // Get merge conflicts
674
+ const mergeConflicts = await this.getMergeConflicts(repoName);
675
+ return {
676
+ repoName,
677
+ branchName: currentBranch,
678
+ lastCommit: result.output.trim(),
679
+ mergeConflicts
680
+ };
655
681
  }
656
- result = await executeShell(`cd ${repoPath} && sudo git pull --no-rebase origin ${currentBranch}`);
657
- if (!result.success && result.code !== 128) {
658
- throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
682
+ finally {
659
683
  }
660
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
661
- logger.success('Git pull completed successfully', { repoName, currentBranch });
662
- // Get merge conflicts
663
- const mergeConflicts = await this.getMergeConflicts(repoName);
664
- return {
665
- repoName,
666
- branchName: currentBranch,
667
- lastCommit: result.output.trim(),
668
- mergeConflicts
669
- };
670
- }
671
- finally {
672
- }
684
+ }); // end withRepoLock
673
685
  }
674
686
  /**
675
687
  * Push repository to remote
@@ -678,55 +690,58 @@ class GithubOperationsService {
678
690
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
679
691
  const installationToken = await fetchInstallationToken();
680
692
  const gitOrgName = await getGithubOrganizationName();
681
- try {
682
- logger.info('Starting git push operation', { repoName });
683
- // Combine all commands into a single SSH call
684
- const combinedCommand = `
693
+ return withRepoLock(repoName, async () => {
694
+ try {
695
+ logger.info('Starting git push operation', { repoName });
696
+ // Combine all commands into a single SSH call
697
+ const combinedCommand = `
685
698
  cd ${repoPath} &&
699
+ rm -f .git/config.lock &&
686
700
  BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
687
701
  COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
688
702
  if [ "$COMMITS_AHEAD" = "0" ]; then
689
703
  echo "ERROR:NO_COMMITS";
690
704
  exit 1;
691
705
  fi &&
692
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
706
+ git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
693
707
  sudo git push origin $BRANCH &&
694
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
708
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
695
709
  COMMIT_HASH=$(sudo git rev-parse HEAD) &&
696
710
  echo "BRANCH:$BRANCH" &&
697
711
  echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
698
712
  echo "COMMIT_HASH:$COMMIT_HASH"
699
713
  `.replace(/\n\s+/g, ' ');
700
- const result = await executeShell(combinedCommand);
701
- if (!result.success) {
702
- if (result.output && result.output.includes('ERROR:NO_COMMITS')) {
703
- throw new Error('No commits to push');
714
+ const result = await executeShell(combinedCommand);
715
+ if (!result.success) {
716
+ if (result.output && result.output.includes('ERROR:NO_COMMITS')) {
717
+ throw new Error('No commits to push');
718
+ }
719
+ throw new Error('Failed to push repository');
720
+ }
721
+ // Parse output
722
+ const output = result.output;
723
+ const branchMatch = output.match(/BRANCH:([^\n]+)/);
724
+ const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
725
+ const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
726
+ const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
727
+ const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
728
+ const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
729
+ logger.success('Git push completed successfully', { repoName, currentBranch, commitHash, commitsAhead });
730
+ // Emit socket event to refresh repo on client (only when called by AI agent)
731
+ if (socket) {
732
+ socket.emit('refresh_repo', { repoName });
704
733
  }
705
- throw new Error('Failed to push repository');
734
+ return {
735
+ repoName,
736
+ branchName: currentBranch,
737
+ commitHash,
738
+ commitsAhead,
739
+ pushedAt: new Date().toISOString()
740
+ };
706
741
  }
707
- // Parse output
708
- const output = result.output;
709
- const branchMatch = output.match(/BRANCH:([^\n]+)/);
710
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
711
- const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
712
- const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
713
- const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
714
- const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
715
- logger.success('Git push completed successfully', { repoName, currentBranch, commitHash, commitsAhead });
716
- // Emit socket event to refresh repo on client (only when called by AI agent)
717
- if (socket) {
718
- socket.emit('refresh_repo', { repoName });
742
+ finally {
719
743
  }
720
- return {
721
- repoName,
722
- branchName: currentBranch,
723
- commitHash,
724
- commitsAhead,
725
- pushedAt: new Date().toISOString()
726
- };
727
- }
728
- finally {
729
- }
744
+ }); // end withRepoLock
730
745
  }
731
746
  /**
732
747
  * Check repository status
@@ -738,14 +753,15 @@ class GithubOperationsService {
738
753
  // const repoPath = 'mnt/f/PhantomX-workspace/93943752-2576-44ee-9b57-4b6fb3130fe4/AI_CODER_REMOTE'
739
754
  // const installationToken = "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";
740
755
  // const gitOrgName = "AI-PLAYGROUNDS";
741
- try {
742
- logger.info('Checking repository status', { repoName });
743
- // Combine all git commands into a single SSH call
744
- // Remove any stale .git/config.lock before touching remote config
745
- // (a crashed/interrupted previous run can leave this lock behind).
746
- // Also avoid sudo for git remote set-url so the lock file is never
747
- // created as root-owned, which would block subsequent cleanup.
748
- const combinedCommand = `
756
+ return withRepoLock(repoName, async () => {
757
+ try {
758
+ logger.info('Checking repository status', { repoName });
759
+ // Combine all git commands into a single SSH call
760
+ // Remove any stale .git/config.lock before touching remote config
761
+ // (a crashed/interrupted previous run can leave this lock behind).
762
+ // Also avoid sudo for git remote set-url so the lock file is never
763
+ // created as root-owned, which would block subsequent cleanup.
764
+ const combinedCommand = `
749
765
  cd ${repoPath} &&
750
766
  rm -f .git/config.lock &&
751
767
  BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
@@ -764,54 +780,55 @@ class GithubOperationsService {
764
780
  echo "CURRENT:$CURRENT_HASH" &&
765
781
  echo "REMOTE:$REMOTE_HASH"
766
782
  `.replace(/\n\s+/g, ' ');
767
- const result = await executeShell(combinedCommand);
768
- if (!result.success) {
769
- throw new Error('Failed to check repository status: ' + result.error);
783
+ const result = await executeShell(combinedCommand);
784
+ if (!result.success) {
785
+ throw new Error('Failed to check repository status: ' + result.error);
786
+ }
787
+ // Parse the output
788
+ const output = result.output;
789
+ const branchMatch = output.match(/BRANCH:([^\n]+)/);
790
+ const behindMatch = output.match(/BEHIND:(\d+)/);
791
+ const aheadMatch = output.match(/AHEAD:(\d+)/);
792
+ const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
793
+ const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
794
+ const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
795
+ const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
796
+ const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
797
+ const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
798
+ const statusOutput = statusMatch ? statusMatch[1].trim() : '';
799
+ const hasUncommittedChanges = statusOutput.length > 0;
800
+ const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
801
+ const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
802
+ const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
803
+ const needsPull = commitsBehind > 0;
804
+ const needsPush = commitsAhead > 0;
805
+ logger.info('Repository status checked', {
806
+ repoName,
807
+ commitsBehind,
808
+ commitsAhead,
809
+ hasUncommittedChanges,
810
+ isUpToDate
811
+ });
812
+ // Check for merge conflicts
813
+ const mergeConflicts = await this.getMergeConflicts(repoName);
814
+ return {
815
+ repoName,
816
+ branchName: currentBranch,
817
+ currentCommitHash,
818
+ remoteCommitHash,
819
+ commitsBehind,
820
+ commitsAhead,
821
+ hasUncommittedChanges,
822
+ isUpToDate,
823
+ needsPull,
824
+ needsPush,
825
+ mergeConflicts,
826
+ status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
827
+ };
770
828
  }
771
- // Parse the output
772
- const output = result.output;
773
- const branchMatch = output.match(/BRANCH:([^\n]+)/);
774
- const behindMatch = output.match(/BEHIND:(\d+)/);
775
- const aheadMatch = output.match(/AHEAD:(\d+)/);
776
- const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
777
- const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
778
- const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
779
- const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
780
- const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
781
- const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
782
- const statusOutput = statusMatch ? statusMatch[1].trim() : '';
783
- const hasUncommittedChanges = statusOutput.length > 0;
784
- const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
785
- const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
786
- const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
787
- const needsPull = commitsBehind > 0;
788
- const needsPush = commitsAhead > 0;
789
- logger.info('Repository status checked', {
790
- repoName,
791
- commitsBehind,
792
- commitsAhead,
793
- hasUncommittedChanges,
794
- isUpToDate
795
- });
796
- // Check for merge conflicts
797
- const mergeConflicts = await this.getMergeConflicts(repoName);
798
- return {
799
- repoName,
800
- branchName: currentBranch,
801
- currentCommitHash,
802
- remoteCommitHash,
803
- commitsBehind,
804
- commitsAhead,
805
- hasUncommittedChanges,
806
- isUpToDate,
807
- needsPull,
808
- needsPush,
809
- mergeConflicts,
810
- status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
811
- };
812
- }
813
- finally {
814
- }
829
+ finally {
830
+ }
831
+ }); // end withRepoLock
815
832
  }
816
833
  /**
817
834
  * Get repository history
@@ -1057,183 +1074,186 @@ class GithubOperationsService {
1057
1074
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1058
1075
  const installationToken = await fetchInstallationToken();
1059
1076
  const gitOrgName = await getGithubOrganizationName();
1060
- try {
1061
- logger.info('Creating pull request', { repoName, targetBranch });
1062
- // Combine commands into a single SSH call, with remote URL set/unset
1063
- const combinedCommand = `
1077
+ return withRepoLock(repoName, async () => {
1078
+ try {
1079
+ logger.info('Creating pull request', { repoName, targetBranch });
1080
+ // Combine commands into a single SSH call, with remote URL set/unset
1081
+ const combinedCommand = `
1064
1082
  cd ${repoPath} &&
1065
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1083
+ rm -f .git/config.lock &&
1084
+ git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1066
1085
  HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1067
1086
  sudo git fetch origin ${targetBranch} 2>/dev/null &&
1068
1087
  COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1069
1088
  if [ "$COMMITS_AHEAD" = "0" ]; then
1070
1089
  echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
1071
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
1090
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
1072
1091
  exit 1;
1073
1092
  fi &&
1074
1093
  echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1075
1094
  echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
1076
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1095
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1077
1096
  `.replace(/\n\s+/g, ' ');
1078
- let result = await executeShell(combinedCommand);
1079
- const output = result.output || '';
1080
- const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1081
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1082
- const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
1083
- let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1084
- const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1085
- if (!result.success && errorMatch) {
1086
- headBranch = errorMatch[1].trim();
1087
- }
1088
- // using 'octokit' package (already installed, @octokit/rest is not installed)
1089
- const { Octokit } = require('octokit');
1090
- const octokit = new Octokit({
1091
- auth: installationToken
1092
- });
1093
- // Always check for existing PR first even if there are no new commits
1094
- try {
1095
- const existingPRs = await octokit.rest.pulls.list({
1096
- owner: gitOrgName,
1097
- repo: repoName,
1098
- head: `${gitOrgName}:${headBranch}`,
1099
- base: targetBranch,
1100
- state: 'open'
1097
+ let result = await executeShell(combinedCommand);
1098
+ const output = result.output || '';
1099
+ const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1100
+ const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1101
+ const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
1102
+ let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1103
+ const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1104
+ if (!result.success && errorMatch) {
1105
+ headBranch = errorMatch[1].trim();
1106
+ }
1107
+ // using 'octokit' package (already installed, @octokit/rest is not installed)
1108
+ const { Octokit } = require('octokit');
1109
+ const octokit = new Octokit({
1110
+ auth: installationToken
1101
1111
  });
1102
- if (existingPRs.data.length > 0) {
1103
- const existingPR = existingPRs.data[0];
1104
- logger.info('Pull request already exists', {
1112
+ // Always check for existing PR first even if there are no new commits
1113
+ try {
1114
+ const existingPRs = await octokit.rest.pulls.list({
1115
+ owner: gitOrgName,
1116
+ repo: repoName,
1117
+ head: `${gitOrgName}:${headBranch}`,
1118
+ base: targetBranch,
1119
+ state: 'open'
1120
+ });
1121
+ if (existingPRs.data.length > 0) {
1122
+ const existingPR = existingPRs.data[0];
1123
+ logger.info('Pull request already exists', {
1124
+ repoName,
1125
+ prNumber: existingPR.number,
1126
+ prUrl: existingPR.html_url
1127
+ });
1128
+ const resultData = {
1129
+ repoName,
1130
+ headBranch,
1131
+ targetBranch,
1132
+ prNumber: existingPR.number,
1133
+ prUrl: existingPR.html_url,
1134
+ prTitle: existingPR.title,
1135
+ commitsAhead,
1136
+ createdAt: existingPR.created_at,
1137
+ alreadyExists: true
1138
+ };
1139
+ // Emit socket event to refresh PR status on client
1140
+ if (socket) {
1141
+ socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
1142
+ }
1143
+ // the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
1144
+ // this.updatePRMetadata(userId, resultData);
1145
+ return resultData;
1146
+ }
1147
+ }
1148
+ catch (fetchError) {
1149
+ logger.error('Failed to check for existing PR', fetchError);
1150
+ }
1151
+ // No existing PR found - check if we have commits to create one
1152
+ if (!result.success) {
1153
+ if (errorMatch) {
1154
+ throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
1155
+ }
1156
+ throw new Error('Failed to prepare pull request');
1157
+ }
1158
+ // Create new PR
1159
+ const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
1160
+ const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
1161
+ try {
1162
+ const prResponse = await octokit.rest.pulls.create({
1163
+ owner: gitOrgName,
1164
+ repo: repoName,
1165
+ title: prTitle,
1166
+ body: prBody,
1167
+ head: headBranch,
1168
+ base: targetBranch
1169
+ });
1170
+ const prUrl = prResponse.data.html_url;
1171
+ const prNumber = prResponse.data.number;
1172
+ logger.success('Pull request created successfully', {
1105
1173
  repoName,
1106
- prNumber: existingPR.number,
1107
- prUrl: existingPR.html_url
1174
+ prNumber,
1175
+ prUrl
1108
1176
  });
1177
+ // taskInfo.status = TaskStatus.Completed;
1178
+ // taskInfo.nonRunningSince = new Date();
1179
+ // //updating the task status in the db
1180
+ // let dbService = await getDBService();
1181
+ // let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
1182
+ // taskHanlder.updateOne({
1183
+ // taskId: taskId
1184
+ // },
1185
+ // {
1186
+ // "$set": {
1187
+ // status: TaskStatus.Completed
1188
+ // }
1189
+ // });
1109
1190
  const resultData = {
1110
1191
  repoName,
1111
1192
  headBranch,
1112
1193
  targetBranch,
1113
- prNumber: existingPR.number,
1114
- prUrl: existingPR.html_url,
1115
- prTitle: existingPR.title,
1194
+ prNumber,
1195
+ prUrl,
1196
+ prTitle,
1116
1197
  commitsAhead,
1117
- createdAt: existingPR.created_at,
1118
- alreadyExists: true
1198
+ createdAt: new Date().toISOString()
1119
1199
  };
1120
- // Emit socket event to refresh PR status on client
1121
- if (socket) {
1122
- socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
1123
- }
1124
- // the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
1125
- // this.updatePRMetadata(userId, resultData);
1200
+ // // Emit socket event to refresh PR status on client
1201
+ // if (socket) {
1202
+ // socket.emit('refresh_pr_status', { repoName, prNumber });
1203
+ // }
1204
+ // // Update database asynchronously (non-blocking)
1205
+ // this.updatePRMetadata(userId, resultData);
1126
1206
  return resultData;
1127
1207
  }
1128
- }
1129
- catch (fetchError) {
1130
- logger.error('Failed to check for existing PR', fetchError);
1131
- }
1132
- // No existing PR found - check if we have commits to create one
1133
- if (!result.success) {
1134
- if (errorMatch) {
1135
- throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
1136
- }
1137
- throw new Error('Failed to prepare pull request');
1138
- }
1139
- // Create new PR
1140
- const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
1141
- const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
1142
- try {
1143
- const prResponse = await octokit.rest.pulls.create({
1144
- owner: gitOrgName,
1145
- repo: repoName,
1146
- title: prTitle,
1147
- body: prBody,
1148
- head: headBranch,
1149
- base: targetBranch
1150
- });
1151
- const prUrl = prResponse.data.html_url;
1152
- const prNumber = prResponse.data.number;
1153
- logger.success('Pull request created successfully', {
1154
- repoName,
1155
- prNumber,
1156
- prUrl
1157
- });
1158
- // taskInfo.status = TaskStatus.Completed;
1159
- // taskInfo.nonRunningSince = new Date();
1160
- // //updating the task status in the db
1161
- // let dbService = await getDBService();
1162
- // let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
1163
- // taskHanlder.updateOne({
1164
- // taskId: taskId
1165
- // },
1166
- // {
1167
- // "$set": {
1168
- // status: TaskStatus.Completed
1169
- // }
1170
- // });
1171
- const resultData = {
1172
- repoName,
1173
- headBranch,
1174
- targetBranch,
1175
- prNumber,
1176
- prUrl,
1177
- prTitle,
1178
- commitsAhead,
1179
- createdAt: new Date().toISOString()
1180
- };
1181
- // // Emit socket event to refresh PR status on client
1182
- // if (socket) {
1183
- // socket.emit('refresh_pr_status', { repoName, prNumber });
1184
- // }
1185
- // // Update database asynchronously (non-blocking)
1186
- // this.updatePRMetadata(userId, resultData);
1187
- return resultData;
1188
- }
1189
- catch (prError) {
1190
- // Check if PR already exists
1191
- if (prError.status === 422) {
1192
- // Fetch existing PR
1193
- try {
1194
- const existingPRs = await octokit.rest.pulls.list({
1195
- owner: gitOrgName,
1196
- repo: repoName,
1197
- head: `${gitOrgName}:${headBranch}`,
1198
- base: targetBranch,
1199
- state: 'open'
1200
- });
1201
- if (existingPRs.data.length > 0) {
1202
- const existingPR = existingPRs.data[0];
1203
- logger.info('Pull request already exists', {
1204
- repoName,
1205
- prNumber: existingPR.number,
1206
- prUrl: existingPR.html_url
1208
+ catch (prError) {
1209
+ // Check if PR already exists
1210
+ if (prError.status === 422) {
1211
+ // Fetch existing PR
1212
+ try {
1213
+ const existingPRs = await octokit.rest.pulls.list({
1214
+ owner: gitOrgName,
1215
+ repo: repoName,
1216
+ head: `${gitOrgName}:${headBranch}`,
1217
+ base: targetBranch,
1218
+ state: 'open'
1207
1219
  });
1208
- const resultData = {
1209
- repoName,
1210
- headBranch,
1211
- targetBranch,
1212
- prNumber: existingPR.number,
1213
- prUrl: existingPR.html_url,
1214
- prTitle: existingPR.title,
1215
- commitsAhead,
1216
- createdAt: existingPR.created_at,
1217
- alreadyExists: true
1218
- };
1219
- // Emit socket event to refresh PR status on client
1220
- if (socket) {
1221
- socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
1220
+ if (existingPRs.data.length > 0) {
1221
+ const existingPR = existingPRs.data[0];
1222
+ logger.info('Pull request already exists', {
1223
+ repoName,
1224
+ prNumber: existingPR.number,
1225
+ prUrl: existingPR.html_url
1226
+ });
1227
+ const resultData = {
1228
+ repoName,
1229
+ headBranch,
1230
+ targetBranch,
1231
+ prNumber: existingPR.number,
1232
+ prUrl: existingPR.html_url,
1233
+ prTitle: existingPR.title,
1234
+ commitsAhead,
1235
+ createdAt: existingPR.created_at,
1236
+ alreadyExists: true
1237
+ };
1238
+ // Emit socket event to refresh PR status on client
1239
+ if (socket) {
1240
+ socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
1241
+ }
1242
+ // Update database asynchronously (non-blocking)
1243
+ // this.updatePRMetadata(userId, resultData);
1244
+ return resultData;
1222
1245
  }
1223
- // Update database asynchronously (non-blocking)
1224
- // this.updatePRMetadata(userId, resultData);
1225
- return resultData;
1246
+ }
1247
+ catch (fetchError) {
1248
+ logger.error('Failed to fetch existing PR', fetchError);
1226
1249
  }
1227
1250
  }
1228
- catch (fetchError) {
1229
- logger.error('Failed to fetch existing PR', fetchError);
1230
- }
1251
+ throw prError;
1231
1252
  }
1232
- throw prError;
1233
1253
  }
1234
- }
1235
- finally {
1236
- }
1254
+ finally {
1255
+ }
1256
+ }); // end withRepoLock
1237
1257
  }
1238
1258
  /**
1239
1259
  * Check if pull request exists
@@ -1242,35 +1262,37 @@ class GithubOperationsService {
1242
1262
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1243
1263
  const installationToken = await fetchInstallationToken();
1244
1264
  const gitOrgName = await getGithubOrganizationName();
1245
- try {
1246
- const trimmedSourceBranch = (sourceBranch || '').trim();
1247
- const trimmedTargetBranch = (targetBranch || '').trim();
1248
- if (!trimmedSourceBranch || !trimmedTargetBranch) {
1249
- throw new Error('Both sourceBranch and targetBranch are required');
1250
- }
1251
- if (trimmedSourceBranch === trimmedTargetBranch) {
1252
- throw new Error('sourceBranch and targetBranch cannot be the same');
1253
- }
1254
- logger.info('Starting branch merge operation', {
1255
- repoName,
1256
- sourceBranch: trimmedSourceBranch,
1257
- targetBranch: trimmedTargetBranch,
1258
- pushToRemote
1259
- });
1260
- let result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
1261
- if (!result.success) {
1262
- throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
1263
- }
1264
- const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
1265
- if (!fetchResult.success) {
1266
- throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
1267
- }
1268
- const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
1269
- const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
1270
- const targetLocalBranch = resolvedTarget.displayBranch;
1271
- const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
1272
- const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
1273
- const prepareBranchCommand = `
1265
+ return withRepoLock(repoName, async () => {
1266
+ try {
1267
+ const trimmedSourceBranch = (sourceBranch || '').trim();
1268
+ const trimmedTargetBranch = (targetBranch || '').trim();
1269
+ if (!trimmedSourceBranch || !trimmedTargetBranch) {
1270
+ throw new Error('Both sourceBranch and targetBranch are required');
1271
+ }
1272
+ if (trimmedSourceBranch === trimmedTargetBranch) {
1273
+ throw new Error('sourceBranch and targetBranch cannot be the same');
1274
+ }
1275
+ logger.info('Starting branch merge operation', {
1276
+ repoName,
1277
+ sourceBranch: trimmedSourceBranch,
1278
+ targetBranch: trimmedTargetBranch,
1279
+ pushToRemote
1280
+ });
1281
+ let result = await executeShell(`cd ${repoPath} && rm -f .git/config.lock && ` +
1282
+ `git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
1283
+ if (!result.success) {
1284
+ throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
1285
+ }
1286
+ const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
1287
+ if (!fetchResult.success) {
1288
+ throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
1289
+ }
1290
+ const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
1291
+ const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
1292
+ const targetLocalBranch = resolvedTarget.displayBranch;
1293
+ const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
1294
+ const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
1295
+ const prepareBranchCommand = `
1274
1296
  cd ${repoPath} &&
1275
1297
  TARGET_LOCAL='${safeTargetLocalBranch}' &&
1276
1298
  if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
@@ -1285,174 +1307,178 @@ class GithubOperationsService {
1285
1307
  sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
1286
1308
  fi
1287
1309
  `.replace(/\n\s+/g, ' ');
1288
- result = await executeShell(prepareBranchCommand);
1289
- if (!result.success) {
1290
- throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
1291
- }
1292
- const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
1293
- const mergeResult = await executeShell(mergeCommand);
1294
- if (!mergeResult.success) {
1295
- const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
1296
- const conflictFiles = (conflictResult.output || '')
1297
- .split('\n')
1298
- .map((line) => line.trim())
1299
- .filter((line) => line.length > 0);
1300
- if (conflictFiles.length > 0) {
1301
- logger.warn('Merge completed with conflicts', {
1302
- repoName,
1303
- sourceBranch: resolvedSource.displayBranch,
1304
- targetBranch: targetLocalBranch,
1305
- conflictFiles
1306
- });
1307
- return {
1308
- repoName,
1309
- sourceBranch: resolvedSource.displayBranch,
1310
- targetBranch: targetLocalBranch,
1311
- merged: false,
1312
- hasConflicts: true,
1313
- conflictFiles,
1314
- currentBranch: targetLocalBranch,
1315
- pushedToRemote: false,
1316
- mergedAt: new Date().toISOString(),
1317
- message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
1318
- };
1310
+ result = await executeShell(prepareBranchCommand);
1311
+ if (!result.success) {
1312
+ throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
1319
1313
  }
1320
- throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
1321
- }
1322
- const summaryResult = await executeShell(`cd ${repoPath} && COMMIT_HASH=$(sudo git rev-parse HEAD) && CURRENT_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) && echo "COMMIT_HASH:$COMMIT_HASH" && echo "CURRENT_BRANCH:$CURRENT_BRANCH"`);
1323
- if (!summaryResult.success) {
1324
- throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
1325
- }
1326
- const summaryOutput = summaryResult.output || '';
1327
- const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
1328
- const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
1329
- const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
1330
- const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
1331
- let pushedToRemote = false;
1332
- if (pushToRemote) {
1333
- const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
1334
- if (!pushResult.success) {
1335
- throw new Error(`Merge succeeded but push failed: ${pushResult.error || pushResult.output || 'Unknown error'}`);
1314
+ const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
1315
+ const mergeResult = await executeShell(mergeCommand);
1316
+ if (!mergeResult.success) {
1317
+ const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
1318
+ const conflictFiles = (conflictResult.output || '')
1319
+ .split('\n')
1320
+ .map((line) => line.trim())
1321
+ .filter((line) => line.length > 0);
1322
+ if (conflictFiles.length > 0) {
1323
+ logger.warn('Merge completed with conflicts', {
1324
+ repoName,
1325
+ sourceBranch: resolvedSource.displayBranch,
1326
+ targetBranch: targetLocalBranch,
1327
+ conflictFiles
1328
+ });
1329
+ return {
1330
+ repoName,
1331
+ sourceBranch: resolvedSource.displayBranch,
1332
+ targetBranch: targetLocalBranch,
1333
+ merged: false,
1334
+ hasConflicts: true,
1335
+ conflictFiles,
1336
+ currentBranch: targetLocalBranch,
1337
+ pushedToRemote: false,
1338
+ mergedAt: new Date().toISOString(),
1339
+ message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
1340
+ };
1341
+ }
1342
+ throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
1336
1343
  }
1337
- pushedToRemote = true;
1338
- }
1339
- logger.success('Branch merge completed successfully', {
1340
- repoName,
1341
- sourceBranch: resolvedSource.displayBranch,
1342
- targetBranch: targetLocalBranch,
1343
- commitHash,
1344
- pushedToRemote
1345
- });
1346
- if (socket) {
1347
- socket.emit('refresh_repo', { repoName });
1348
- }
1349
- return {
1350
- repoName,
1351
- sourceBranch: resolvedSource.displayBranch,
1352
- targetBranch: targetLocalBranch,
1353
- merged: true,
1354
- hasConflicts: false,
1355
- conflictFiles: [],
1356
- currentBranch,
1357
- commitHash,
1358
- pushedToRemote,
1359
- mergedAt: new Date().toISOString(),
1360
- message: pushedToRemote
1361
- ? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
1362
- : `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
1363
- };
1364
- }
1365
- finally {
1366
- try {
1367
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
1368
- }
1369
- catch (restoreError) {
1370
- logger.error('Failed to restore unauthenticated remote URL after merge operation', {
1344
+ const summaryResult = await executeShell(`cd ${repoPath} && COMMIT_HASH=$(sudo git rev-parse HEAD) && CURRENT_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) && echo "COMMIT_HASH:$COMMIT_HASH" && echo "CURRENT_BRANCH:$CURRENT_BRANCH"`);
1345
+ if (!summaryResult.success) {
1346
+ throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
1347
+ }
1348
+ const summaryOutput = summaryResult.output || '';
1349
+ const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
1350
+ const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
1351
+ const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
1352
+ const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
1353
+ let pushedToRemote = false;
1354
+ if (pushToRemote) {
1355
+ const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
1356
+ if (!pushResult.success) {
1357
+ throw new Error(`Merge succeeded but push failed: ${pushResult.error || pushResult.output || 'Unknown error'}`);
1358
+ }
1359
+ pushedToRemote = true;
1360
+ }
1361
+ logger.success('Branch merge completed successfully', {
1371
1362
  repoName,
1372
- error: restoreError instanceof Error ? restoreError.message : String(restoreError)
1363
+ sourceBranch: resolvedSource.displayBranch,
1364
+ targetBranch: targetLocalBranch,
1365
+ commitHash,
1366
+ pushedToRemote
1373
1367
  });
1368
+ if (socket) {
1369
+ socket.emit('refresh_repo', { repoName });
1370
+ }
1371
+ return {
1372
+ repoName,
1373
+ sourceBranch: resolvedSource.displayBranch,
1374
+ targetBranch: targetLocalBranch,
1375
+ merged: true,
1376
+ hasConflicts: false,
1377
+ conflictFiles: [],
1378
+ currentBranch,
1379
+ commitHash,
1380
+ pushedToRemote,
1381
+ mergedAt: new Date().toISOString(),
1382
+ message: pushedToRemote
1383
+ ? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
1384
+ : `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
1385
+ };
1374
1386
  }
1375
- }
1387
+ finally {
1388
+ try {
1389
+ await executeShell(`cd ${repoPath} && git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
1390
+ }
1391
+ catch (restoreError) {
1392
+ logger.error('Failed to restore unauthenticated remote URL after merge operation', {
1393
+ repoName,
1394
+ error: restoreError instanceof Error ? restoreError.message : String(restoreError)
1395
+ });
1396
+ }
1397
+ }
1398
+ }); // end withRepoLock
1376
1399
  }
1377
1400
  async checkPullRequestExists(repoName, targetBranch) {
1378
1401
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1379
1402
  const installationToken = await fetchInstallationToken();
1380
1403
  const gitOrgName = await getGithubOrganizationName();
1381
- try {
1382
- logger.info('Checking if PR exists', { repoName, targetBranch });
1383
- // Get current branch and commits ahead
1384
- const combinedCommand = `
1404
+ return withRepoLock(repoName, async () => {
1405
+ try {
1406
+ logger.info('Checking if PR exists', { repoName, targetBranch });
1407
+ // Get current branch and commits ahead
1408
+ const combinedCommand = `
1385
1409
  cd ${repoPath} &&
1386
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1410
+ rm -f .git/config.lock &&
1411
+ git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1387
1412
  HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1388
1413
  sudo git fetch origin --prune 2>/dev/null || true &&
1389
1414
  sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
1390
1415
  COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1391
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
1416
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
1392
1417
  echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1393
1418
  echo "COMMITS_AHEAD:$COMMITS_AHEAD"
1394
1419
  `.replace(/\n\s+/g, ' ');
1395
- const result = await executeShell(combinedCommand);
1396
- // Check for command execution failure
1397
- if (!result.success) {
1398
- logger.error('Failed to check PR status', {
1399
- repoName,
1400
- error: result.error,
1401
- code: result.code,
1402
- output: result.output
1420
+ const result = await executeShell(combinedCommand);
1421
+ // Check for command execution failure
1422
+ if (!result.success) {
1423
+ logger.error('Failed to check PR status', {
1424
+ repoName,
1425
+ error: result.error,
1426
+ code: result.code,
1427
+ output: result.output
1428
+ });
1429
+ throw new Error(`Failed to check PR status: ${result.error}`);
1430
+ }
1431
+ const output = result.output || '';
1432
+ const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1433
+ const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1434
+ const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1435
+ const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1436
+ logger.info('Branch info retrieved', { headBranch, commitsAhead });
1437
+ // Check for existing PR - using 'octokit' package (already installed)
1438
+ const { Octokit } = require('octokit');
1439
+ const octokit = new Octokit({
1440
+ auth: installationToken
1403
1441
  });
1404
- throw new Error(`Failed to check PR status: ${result.error}`);
1405
- }
1406
- const output = result.output || '';
1407
- const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1408
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1409
- const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1410
- const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1411
- logger.info('Branch info retrieved', { headBranch, commitsAhead });
1412
- // Check for existing PR - using 'octokit' package (already installed)
1413
- const { Octokit } = require('octokit');
1414
- const octokit = new Octokit({
1415
- auth: installationToken
1416
- });
1417
- const existingPRs = await octokit.rest.pulls.list({
1418
- owner: gitOrgName,
1419
- repo: repoName,
1420
- head: `${gitOrgName}:${headBranch}`,
1421
- base: targetBranch,
1422
- state: 'open'
1423
- });
1424
- if (existingPRs.data.length > 0) {
1425
- const existingPR = existingPRs.data[0];
1426
- logger.info('Pull request exists', {
1427
- repoName,
1428
- prNumber: existingPR.number,
1429
- prUrl: existingPR.html_url
1442
+ const existingPRs = await octokit.rest.pulls.list({
1443
+ owner: gitOrgName,
1444
+ repo: repoName,
1445
+ head: `${gitOrgName}:${headBranch}`,
1446
+ base: targetBranch,
1447
+ state: 'open'
1430
1448
  });
1431
- return {
1432
- repoName,
1433
- headBranch,
1434
- targetBranch,
1435
- prExists: true,
1436
- prNumber: existingPR.number,
1437
- prUrl: existingPR.html_url,
1438
- prTitle: existingPR.title,
1439
- commitsAhead,
1440
- createdAt: existingPR.created_at
1441
- };
1449
+ if (existingPRs.data.length > 0) {
1450
+ const existingPR = existingPRs.data[0];
1451
+ logger.info('Pull request exists', {
1452
+ repoName,
1453
+ prNumber: existingPR.number,
1454
+ prUrl: existingPR.html_url
1455
+ });
1456
+ return {
1457
+ repoName,
1458
+ headBranch,
1459
+ targetBranch,
1460
+ prExists: true,
1461
+ prNumber: existingPR.number,
1462
+ prUrl: existingPR.html_url,
1463
+ prTitle: existingPR.title,
1464
+ commitsAhead,
1465
+ createdAt: existingPR.created_at
1466
+ };
1467
+ }
1468
+ else {
1469
+ logger.info('No pull request found', { repoName, headBranch, targetBranch });
1470
+ return {
1471
+ repoName,
1472
+ headBranch,
1473
+ targetBranch,
1474
+ prExists: false,
1475
+ commitsAhead
1476
+ };
1477
+ }
1442
1478
  }
1443
- else {
1444
- logger.info('No pull request found', { repoName, headBranch, targetBranch });
1445
- return {
1446
- repoName,
1447
- headBranch,
1448
- targetBranch,
1449
- prExists: false,
1450
- commitsAhead
1451
- };
1479
+ finally {
1452
1480
  }
1453
- }
1454
- finally {
1455
- }
1481
+ }); // end withRepoLock
1456
1482
  }
1457
1483
  /**
1458
1484
  * Get commit list with commit IDs for a branch