edsger 0.30.0 → 0.30.1
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/.claude/settings.local.json +8 -0
- package/dist/phases/autonomous/index.js +9 -56
- package/dist/phases/code-implementation/branch-pr-creator.js +2 -58
- package/dist/phases/code-implementation/index.js +9 -67
- package/dist/phases/code-refine/index.js +6 -37
- package/dist/phases/code-review/index.js +1 -0
- package/dist/phases/pr-execution/index.js +1 -0
- package/dist/phases/pr-execution/pr-executor.d.ts +2 -1
- package/dist/phases/pr-execution/pr-executor.js +9 -38
- package/dist/phases/pull-request/creator.js +9 -35
- package/dist/utils/git-branch-manager.d.ts +2 -0
- package/dist/utils/git-branch-manager.js +8 -35
- package/dist/utils/git-push.d.ts +43 -0
- package/dist/utils/git-push.js +125 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ import { createBranches, getCurrentBranch, updateBranch, } from '../../services/
|
|
|
14
14
|
import { prepareCustomBranchGitEnvironmentAsync, resetUncommittedChanges, } from '../../utils/git-branch-manager.js';
|
|
15
15
|
import { createBranchPullRequest, } from '../code-implementation/branch-pr-creator.js';
|
|
16
16
|
import { getGitHubConfig } from '../../api/github.js';
|
|
17
|
+
import { gitPush } from '../../utils/git-push.js';
|
|
17
18
|
import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
|
|
18
19
|
import { createAutonomousSystemPrompt, createAutonomousUserPrompt, } from './prompts.js';
|
|
19
20
|
/**
|
|
@@ -35,59 +36,6 @@ async function* prompt(userPrompt) {
|
|
|
35
36
|
yield userMessage(userPrompt);
|
|
36
37
|
await new Promise((res) => setTimeout(res, 10000));
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Push branch to remote repository
|
|
40
|
-
*/
|
|
41
|
-
async function pushToRemote(branchName, verbose) {
|
|
42
|
-
try {
|
|
43
|
-
const { execSync } = await import('child_process');
|
|
44
|
-
if (verbose) {
|
|
45
|
-
logInfo(`📤 Pushing branch ${branchName} to remote...`);
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
execSync(`git push -u origin ${branchName}`, {
|
|
49
|
-
encoding: 'utf-8',
|
|
50
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
51
|
-
});
|
|
52
|
-
return { success: true };
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
try {
|
|
56
|
-
execSync(`git push origin ${branchName}`, {
|
|
57
|
-
encoding: 'utf-8',
|
|
58
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
59
|
-
});
|
|
60
|
-
return { success: true };
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
if (verbose) {
|
|
64
|
-
logInfo(`⚠️ Push rejected, attempting force push with lease...`);
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
execSync(`git push --force-with-lease origin ${branchName}`, {
|
|
68
|
-
encoding: 'utf-8',
|
|
69
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
70
|
-
});
|
|
71
|
-
return { success: true };
|
|
72
|
-
}
|
|
73
|
-
catch (forceError) {
|
|
74
|
-
return {
|
|
75
|
-
success: false,
|
|
76
|
-
error: forceError instanceof Error
|
|
77
|
-
? forceError.message
|
|
78
|
-
: String(forceError),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
return {
|
|
86
|
-
success: false,
|
|
87
|
-
error: error instanceof Error ? error.message : String(error),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
39
|
/**
|
|
92
40
|
* Run a single autonomous iteration using Claude Code SDK
|
|
93
41
|
*/
|
|
@@ -265,14 +213,19 @@ export async function runAutonomousDevelopment(options, config, _checklistContex
|
|
|
265
213
|
logInfo(`⏱️ Iteration ${totalIterations} took ${(iterationDuration / 1000).toFixed(0)}s`);
|
|
266
214
|
if (iterationResult.success) {
|
|
267
215
|
logInfo(`✅ Iteration ${totalIterations}: ${iterationResult.summary || 'completed'}`);
|
|
268
|
-
//
|
|
269
|
-
const
|
|
216
|
+
// Get GitHub config for push authentication and PR creation
|
|
217
|
+
const githubConfig = await getGitHubConfig(featureId, verbose);
|
|
218
|
+
// Push to remote with authentication
|
|
219
|
+
const pushResult = gitPush({
|
|
220
|
+
branchName: devBranchName,
|
|
221
|
+
token: githubConfig.configured ? githubConfig.token : undefined,
|
|
222
|
+
verbose,
|
|
223
|
+
});
|
|
270
224
|
if (!pushResult.success && verbose) {
|
|
271
225
|
logError(`⚠️ Failed to push: ${pushResult.error}`);
|
|
272
226
|
}
|
|
273
227
|
// Create PR after first successful iteration
|
|
274
228
|
if (!prUrl) {
|
|
275
|
-
const githubConfig = await getGitHubConfig(featureId, verbose);
|
|
276
229
|
if (githubConfig.configured &&
|
|
277
230
|
githubConfig.token &&
|
|
278
231
|
githubConfig.owner &&
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Creates pull requests from dev/ branches to feat/ branches
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
6
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
7
|
+
import { gitPush } from '../../utils/git-push.js';
|
|
8
8
|
// GitHub PR title best practice: keep under 72 characters
|
|
9
9
|
const MAX_PR_TITLE_LENGTH = 72;
|
|
10
10
|
const PR_TITLE_PREFIX = 'feat: ';
|
|
@@ -40,62 +40,6 @@ export function devBranchToFeatBranch(devBranchName) {
|
|
|
40
40
|
// If already feat/ or other format, return as-is
|
|
41
41
|
return devBranchName;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Push a branch to remote
|
|
45
|
-
* Falls back to force-with-lease if normal push fails (e.g., after rebase)
|
|
46
|
-
*/
|
|
47
|
-
async function pushBranch(branchName, verbose) {
|
|
48
|
-
try {
|
|
49
|
-
if (verbose) {
|
|
50
|
-
logInfo(`📤 Pushing branch ${branchName} to remote...`);
|
|
51
|
-
}
|
|
52
|
-
// Try to push with -u flag (sets upstream if not already set)
|
|
53
|
-
try {
|
|
54
|
-
execSync(`git push -u origin ${branchName}`, {
|
|
55
|
-
encoding: 'utf-8',
|
|
56
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
57
|
-
});
|
|
58
|
-
return { success: true };
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
// If push fails, try without -u flag
|
|
62
|
-
try {
|
|
63
|
-
execSync(`git push origin ${branchName}`, {
|
|
64
|
-
encoding: 'utf-8',
|
|
65
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
66
|
-
});
|
|
67
|
-
return { success: true };
|
|
68
|
-
}
|
|
69
|
-
catch (retryError) {
|
|
70
|
-
// If push still fails (likely non-fast-forward after rebase),
|
|
71
|
-
// use force-with-lease for safer force push
|
|
72
|
-
if (verbose) {
|
|
73
|
-
logInfo(`⚠️ Push rejected, attempting force push with lease...`);
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
execSync(`git push --force-with-lease origin ${branchName}`, {
|
|
77
|
-
encoding: 'utf-8',
|
|
78
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
79
|
-
});
|
|
80
|
-
if (verbose) {
|
|
81
|
-
logInfo(`✅ Successfully force pushed ${branchName}`);
|
|
82
|
-
}
|
|
83
|
-
return { success: true };
|
|
84
|
-
}
|
|
85
|
-
catch (forceError) {
|
|
86
|
-
const errorMessage = forceError instanceof Error
|
|
87
|
-
? forceError.message
|
|
88
|
-
: String(forceError);
|
|
89
|
-
return { success: false, error: errorMessage };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
96
|
-
return { success: false, error: errorMessage };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
43
|
/**
|
|
100
44
|
* Create a pull request from a dev/ branch to its corresponding feat/ branch
|
|
101
45
|
* This is used after code implementation in multi-branch features
|
|
@@ -115,7 +59,7 @@ export async function createBranchPullRequest(config, devBranchName, featureName
|
|
|
115
59
|
if (verbose) {
|
|
116
60
|
logInfo(`📤 Pushing ${devBranchName} to remote...`);
|
|
117
61
|
}
|
|
118
|
-
const pushResult =
|
|
62
|
+
const pushResult = gitPush({ branchName: devBranchName, token: githubToken, verbose });
|
|
119
63
|
if (!pushResult.success) {
|
|
120
64
|
return {
|
|
121
65
|
success: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
3
3
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
4
|
+
import { gitPush } from '../../utils/git-push.js';
|
|
4
5
|
import { formatChecklistsForContext, } from '../../services/checklist.js';
|
|
5
6
|
import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
|
|
6
7
|
import { fetchCodeImplementationContext, formatContextForPrompt, } from './context.js';
|
|
@@ -164,6 +165,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
|
|
|
164
165
|
// Sync feat branch to main before rebase to ensure it's up to date
|
|
165
166
|
// This prevents the PR (dev → feat) from showing extra commits
|
|
166
167
|
let featSyncedToMain = false;
|
|
168
|
+
let githubTokenForPush;
|
|
167
169
|
if (devBranchName.startsWith('dev/')) {
|
|
168
170
|
try {
|
|
169
171
|
const githubConfig = await getGitHubConfig(featureId, verbose);
|
|
@@ -171,6 +173,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
|
|
|
171
173
|
githubConfig.token &&
|
|
172
174
|
githubConfig.owner &&
|
|
173
175
|
githubConfig.repo) {
|
|
176
|
+
githubTokenForPush = githubConfig.token;
|
|
174
177
|
const { devBranchToFeatBranch } = await import('./branch-pr-creator.js');
|
|
175
178
|
const featBranchName = devBranchToFeatBranch(devBranchName);
|
|
176
179
|
if (verbose) {
|
|
@@ -200,6 +203,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
|
|
|
200
203
|
},
|
|
201
204
|
forcePushAfterRebase: featSyncedToMain, // Trigger GitHub to recalculate PR diff
|
|
202
205
|
baseBranchCompleted, // Tell conflict resolver to use --theirs for all conflicts
|
|
206
|
+
githubToken: githubTokenForPush,
|
|
203
207
|
});
|
|
204
208
|
try {
|
|
205
209
|
// Fetch all required context information via MCP endpoints
|
|
@@ -462,7 +466,11 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
|
|
|
462
466
|
if (verbose) {
|
|
463
467
|
logInfo(`📤 Pushing code to remote repository...`);
|
|
464
468
|
}
|
|
465
|
-
const pushResult =
|
|
469
|
+
const pushResult = gitPush({
|
|
470
|
+
branchName: structuredImplementationResult.branch_name || branchName,
|
|
471
|
+
token: githubTokenForPush,
|
|
472
|
+
verbose,
|
|
473
|
+
});
|
|
466
474
|
if (!pushResult.success && verbose) {
|
|
467
475
|
logError(`⚠️ Failed to push to remote: ${pushResult.error}`);
|
|
468
476
|
logInfo(' Code is committed locally and will be reviewed. Manual push may be needed.');
|
|
@@ -854,69 +862,3 @@ const parseImplementationResponse = (response, featureId) => {
|
|
|
854
862
|
};
|
|
855
863
|
}
|
|
856
864
|
};
|
|
857
|
-
/**
|
|
858
|
-
* Push branch to remote repository
|
|
859
|
-
* Falls back to force-with-lease if normal push fails (e.g., after rebase)
|
|
860
|
-
*/
|
|
861
|
-
async function pushToRemote(branchName, verbose) {
|
|
862
|
-
try {
|
|
863
|
-
// Import exec from child_process
|
|
864
|
-
const { execSync } = await import('child_process');
|
|
865
|
-
if (verbose) {
|
|
866
|
-
logInfo(`Pushing branch ${branchName} to remote...`);
|
|
867
|
-
}
|
|
868
|
-
// Try to push with -u flag (sets upstream if not already set)
|
|
869
|
-
try {
|
|
870
|
-
execSync(`git push -u origin ${branchName}`, {
|
|
871
|
-
encoding: 'utf-8',
|
|
872
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
873
|
-
});
|
|
874
|
-
return { success: true };
|
|
875
|
-
}
|
|
876
|
-
catch (error) {
|
|
877
|
-
// If push fails, it might be because branch already has upstream
|
|
878
|
-
// Try without -u flag
|
|
879
|
-
try {
|
|
880
|
-
execSync(`git push origin ${branchName}`, {
|
|
881
|
-
encoding: 'utf-8',
|
|
882
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
883
|
-
});
|
|
884
|
-
return { success: true };
|
|
885
|
-
}
|
|
886
|
-
catch (retryError) {
|
|
887
|
-
// If push still fails (likely non-fast-forward after rebase),
|
|
888
|
-
// use force-with-lease for safer force push
|
|
889
|
-
// force-with-lease ensures we don't overwrite others' work by checking remote state
|
|
890
|
-
if (verbose) {
|
|
891
|
-
logInfo(`⚠️ Push rejected, attempting force push with lease...`);
|
|
892
|
-
}
|
|
893
|
-
try {
|
|
894
|
-
execSync(`git push --force-with-lease origin ${branchName}`, {
|
|
895
|
-
encoding: 'utf-8',
|
|
896
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
897
|
-
});
|
|
898
|
-
if (verbose) {
|
|
899
|
-
logInfo(`✅ Successfully force pushed ${branchName}`);
|
|
900
|
-
}
|
|
901
|
-
return { success: true };
|
|
902
|
-
}
|
|
903
|
-
catch (forceError) {
|
|
904
|
-
const errorMessage = forceError instanceof Error
|
|
905
|
-
? forceError.message
|
|
906
|
-
: String(forceError);
|
|
907
|
-
return {
|
|
908
|
-
success: false,
|
|
909
|
-
error: errorMessage,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
catch (error) {
|
|
916
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
917
|
-
return {
|
|
918
|
-
success: false,
|
|
919
|
-
error: errorMessage,
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
7
7
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
8
8
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
9
|
-
import {
|
|
9
|
+
import { gitPushCurrentBranch } from '../../utils/git-push.js';
|
|
10
10
|
import { fetchCodeRefineContext, } from './context.js';
|
|
11
11
|
import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
|
|
12
12
|
import { createSystemPrompt, createCodeRefinePrompt } from './prompts.js';
|
|
@@ -26,41 +26,6 @@ async function* prompt(refinePrompt) {
|
|
|
26
26
|
yield userMessage(refinePrompt);
|
|
27
27
|
await new Promise((res) => setTimeout(res, 10000));
|
|
28
28
|
}
|
|
29
|
-
// Git utility functions are now imported from shared module
|
|
30
|
-
/**
|
|
31
|
-
* Push changes to remote branch
|
|
32
|
-
*/
|
|
33
|
-
const pushChanges = (verbose) => {
|
|
34
|
-
try {
|
|
35
|
-
if (verbose) {
|
|
36
|
-
logInfo(`📤 Pushing changes to remote...`);
|
|
37
|
-
}
|
|
38
|
-
execSync('git push origin $(git branch --show-current)', {
|
|
39
|
-
encoding: 'utf-8',
|
|
40
|
-
});
|
|
41
|
-
if (verbose) {
|
|
42
|
-
logInfo(`✅ Successfully pushed changes`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
// If push fails due to non-fast-forward, use force-with-lease for safer force push
|
|
47
|
-
// force-with-lease ensures we don't overwrite others' work by checking remote state
|
|
48
|
-
if (verbose) {
|
|
49
|
-
logInfo(`⚠️ Push rejected, attempting force push with lease...`);
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
execSync('git push --force-with-lease origin $(git branch --show-current)', {
|
|
53
|
-
encoding: 'utf-8',
|
|
54
|
-
});
|
|
55
|
-
if (verbose) {
|
|
56
|
-
logInfo(`✅ Successfully force pushed changes`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch (forceError) {
|
|
60
|
-
throw new Error(`Failed to push changes: ${forceError instanceof Error ? forceError.message : String(forceError)}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
29
|
/**
|
|
65
30
|
* Main code refine function with built-in verification loop
|
|
66
31
|
* Similar to technical-design, this includes an iterative improvement cycle:
|
|
@@ -178,6 +143,7 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
|
|
|
178
143
|
},
|
|
179
144
|
forcePushAfterRebase: featSyncedToMain,
|
|
180
145
|
baseBranchCompleted: baseBranchCompletedForRebase,
|
|
146
|
+
githubToken,
|
|
181
147
|
})
|
|
182
148
|
: await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, {
|
|
183
149
|
model: DEFAULT_MODEL,
|
|
@@ -258,7 +224,10 @@ Please ensure Claude Code commits all changes before completing the refine phase
|
|
|
258
224
|
logInfo('✅ All changes committed. Pushing to remote...');
|
|
259
225
|
}
|
|
260
226
|
try {
|
|
261
|
-
|
|
227
|
+
const pushResult = gitPushCurrentBranch({ token: githubToken, verbose });
|
|
228
|
+
if (!pushResult.success) {
|
|
229
|
+
throw new Error(pushResult.error || 'Push failed');
|
|
230
|
+
}
|
|
262
231
|
}
|
|
263
232
|
catch (pushError) {
|
|
264
233
|
logError(`Failed to push changes: ${pushError}`);
|
|
@@ -232,6 +232,7 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
|
|
|
232
232
|
},
|
|
233
233
|
forcePushAfterRebase: featSyncedToMain,
|
|
234
234
|
baseBranchCompleted: baseBranchCompletedForRebase,
|
|
235
|
+
githubToken,
|
|
235
236
|
})
|
|
236
237
|
: await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, {
|
|
237
238
|
model: DEFAULT_MODEL,
|
|
@@ -8,6 +8,7 @@ export interface PRExecutionConfig {
|
|
|
8
8
|
repo: string;
|
|
9
9
|
forkInfo: RepoForkInfo;
|
|
10
10
|
verbose?: boolean;
|
|
11
|
+
token?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface PRBranchResult {
|
|
13
14
|
branchName: string;
|
|
@@ -19,7 +20,7 @@ export interface PRBranchResult {
|
|
|
19
20
|
/**
|
|
20
21
|
* Push a branch to remote with force-with-lease fallback
|
|
21
22
|
*/
|
|
22
|
-
export declare function pushBranch(branchName: string, verbose?: boolean): void;
|
|
23
|
+
export declare function pushBranch(branchName: string, verbose?: boolean, token?: string): void;
|
|
23
24
|
/**
|
|
24
25
|
* Get the HEAD SHA of a local branch
|
|
25
26
|
*/
|
|
@@ -2,53 +2,24 @@
|
|
|
2
2
|
* PR Executor for pushing branches to remote and generating compare URLs
|
|
3
3
|
* Handles fork-aware URL construction for manual PR creation
|
|
4
4
|
*/
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
5
|
import { logInfo } from '../../utils/logger.js';
|
|
7
6
|
import { updatePullRequest } from '../../services/pull-requests.js';
|
|
7
|
+
import { gitPush } from '../../utils/git-push.js';
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
8
9
|
/**
|
|
9
10
|
* Push a branch to remote with force-with-lease fallback
|
|
10
11
|
*/
|
|
11
|
-
export function pushBranch(branchName, verbose) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
execSync(`git push -u origin ${branchName}`, {
|
|
18
|
-
encoding: 'utf-8',
|
|
19
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
try {
|
|
24
|
-
execSync(`git push origin ${branchName}`, {
|
|
25
|
-
encoding: 'utf-8',
|
|
26
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
if (verbose) {
|
|
31
|
-
logInfo(`⚠️ Push rejected, attempting force push with lease...`);
|
|
32
|
-
}
|
|
33
|
-
execSync(`git push --force-with-lease origin ${branchName}`, {
|
|
34
|
-
encoding: 'utf-8',
|
|
35
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
36
|
-
});
|
|
37
|
-
if (verbose) {
|
|
38
|
-
logInfo(`✅ Successfully force pushed ${branchName}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
throw new Error(`Failed to push branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
12
|
+
export function pushBranch(branchName, verbose, token) {
|
|
13
|
+
const result = gitPush({ branchName, token, verbose });
|
|
14
|
+
if (!result.success) {
|
|
15
|
+
throw new Error(`Failed to push branch ${branchName}: ${result.error}`);
|
|
45
16
|
}
|
|
46
17
|
}
|
|
47
18
|
/**
|
|
48
19
|
* Get the HEAD SHA of a local branch
|
|
49
20
|
*/
|
|
50
21
|
export function getLocalBranchSha(branchName) {
|
|
51
|
-
return
|
|
22
|
+
return execFileSync('git', ['rev-parse', branchName], {
|
|
52
23
|
encoding: 'utf-8',
|
|
53
24
|
}).trim();
|
|
54
25
|
}
|
|
@@ -68,10 +39,10 @@ export function buildCompareUrl(forkInfo, owner, repo, branchName, base = 'main'
|
|
|
68
39
|
* Push a branch and build the compare URL
|
|
69
40
|
*/
|
|
70
41
|
export function pushBranchAndBuildUrl(config, branchName) {
|
|
71
|
-
const { owner, repo, forkInfo, verbose } = config;
|
|
42
|
+
const { owner, repo, forkInfo, verbose, token } = config;
|
|
72
43
|
const headSha = getLocalBranchSha(branchName);
|
|
73
44
|
try {
|
|
74
|
-
pushBranch(branchName, verbose);
|
|
45
|
+
pushBranch(branchName, verbose, token);
|
|
75
46
|
const compareUrl = buildCompareUrl(forkInfo, owner, repo, branchName);
|
|
76
47
|
if (verbose) {
|
|
77
48
|
logInfo(`🔗 Compare URL: ${compareUrl}`);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { gitPush } from '../../utils/git-push.js';
|
|
7
8
|
// GitHub PR title best practice: keep under 72 characters
|
|
8
9
|
const MAX_PR_TITLE_LENGTH = 72;
|
|
9
10
|
const PR_TITLE_PREFIX = 'feat: ';
|
|
@@ -115,39 +116,6 @@ const switchToBranch = (branch, verbose) => {
|
|
|
115
116
|
throw new Error(`Failed to switch to branch ${branch}: ${error}`);
|
|
116
117
|
}
|
|
117
118
|
};
|
|
118
|
-
/**
|
|
119
|
-
* Push current branch to remote
|
|
120
|
-
* Falls back to force-with-lease if normal push fails (e.g., after rebase)
|
|
121
|
-
*/
|
|
122
|
-
const pushBranch = (branch, verbose) => {
|
|
123
|
-
try {
|
|
124
|
-
if (verbose) {
|
|
125
|
-
console.log(`📤 Pushing branch ${branch} to remote...`);
|
|
126
|
-
}
|
|
127
|
-
execSync(`git push -u origin ${branch}`, { encoding: 'utf-8' });
|
|
128
|
-
if (verbose) {
|
|
129
|
-
console.log(`✅ Branch pushed successfully`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
// If push fails (likely non-fast-forward after rebase),
|
|
134
|
-
// use force-with-lease for safer force push
|
|
135
|
-
if (verbose) {
|
|
136
|
-
console.log(`⚠️ Push rejected, attempting force push with lease...`);
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
execSync(`git push --force-with-lease origin ${branch}`, {
|
|
140
|
-
encoding: 'utf-8',
|
|
141
|
-
});
|
|
142
|
-
if (verbose) {
|
|
143
|
-
console.log(`✅ Successfully force pushed ${branch}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch (forceError) {
|
|
147
|
-
throw new Error(`Failed to push branch: ${forceError}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
119
|
/**
|
|
152
120
|
* Switch to main branch
|
|
153
121
|
*/
|
|
@@ -276,7 +244,10 @@ export async function createPullRequest(config, feature) {
|
|
|
276
244
|
}
|
|
277
245
|
try {
|
|
278
246
|
// Push current branch to remote first
|
|
279
|
-
|
|
247
|
+
const pushResult = gitPush({ branchName: currentBranch, token: githubToken, verbose });
|
|
248
|
+
if (!pushResult.success) {
|
|
249
|
+
throw new Error(`Failed to push branch: ${pushResult.error}`);
|
|
250
|
+
}
|
|
280
251
|
// Get the base branch (main) SHA to create feat/ branch from
|
|
281
252
|
const { data: baseBranchData } = await octokit.repos.getBranch({
|
|
282
253
|
owner,
|
|
@@ -306,7 +277,10 @@ export async function createPullRequest(config, feature) {
|
|
|
306
277
|
}
|
|
307
278
|
else {
|
|
308
279
|
// If not a dev/ branch, push it normally
|
|
309
|
-
|
|
280
|
+
const pushResult = gitPush({ branchName: currentBranch, token: githubToken, verbose });
|
|
281
|
+
if (!pushResult.success) {
|
|
282
|
+
throw new Error(`Failed to push branch: ${pushResult.error}`);
|
|
283
|
+
}
|
|
310
284
|
}
|
|
311
285
|
// Generate PR title and body
|
|
312
286
|
const title = generatePRTitle(feature.name);
|
|
@@ -212,6 +212,8 @@ export interface RebaseWithConflictResolutionOptions {
|
|
|
212
212
|
* GitHub to recalculate the PR diff.
|
|
213
213
|
*/
|
|
214
214
|
forcePushAfterRebase?: boolean;
|
|
215
|
+
/** GitHub App installation token for authenticated push */
|
|
216
|
+
githubToken?: string;
|
|
215
217
|
}
|
|
216
218
|
/**
|
|
217
219
|
* Switch to feature branch and rebase with base branch, with optional automatic conflict resolution
|
|
@@ -7,6 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { Octokit } from '@octokit/rest';
|
|
9
9
|
import { logInfo, logError } from './logger.js';
|
|
10
|
+
import { gitForcePush } from './git-push.js';
|
|
10
11
|
/**
|
|
11
12
|
* Get current Git branch name
|
|
12
13
|
*/
|
|
@@ -689,7 +690,7 @@ export async function syncFeatBranchWithMain(featBranch, githubToken, owner, rep
|
|
|
689
690
|
* @returns Object containing the previous branch name and conflict resolution result
|
|
690
691
|
*/
|
|
691
692
|
export async function switchToFeatureBranchAndRebaseAsync(options) {
|
|
692
|
-
const { featureBranch, baseBranch = 'main', rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, } = options;
|
|
693
|
+
const { featureBranch, baseBranch = 'main', rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, githubToken, } = options;
|
|
693
694
|
// Determine the actual target branch for rebase
|
|
694
695
|
// If rebaseTargetBranch is set (e.g., main), use it; otherwise use baseBranch
|
|
695
696
|
const actualRebaseTarget = rebaseTargetBranch || baseBranch;
|
|
@@ -864,23 +865,9 @@ export async function switchToFeatureBranchAndRebaseAsync(options) {
|
|
|
864
865
|
}
|
|
865
866
|
// Force push after rebase to trigger GitHub to recalculate PR diff
|
|
866
867
|
if (forcePushAfterRebase) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
execSync(`git push --force-with-lease origin ${featureBranch}`, {
|
|
872
|
-
encoding: 'utf-8',
|
|
873
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
874
|
-
});
|
|
875
|
-
if (verbose) {
|
|
876
|
-
logInfo(`✅ Successfully force pushed ${featureBranch}`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
catch (pushError) {
|
|
880
|
-
if (verbose) {
|
|
881
|
-
logInfo(`⚠️ Could not force push ${featureBranch}: ${pushError instanceof Error ? pushError.message : String(pushError)}`);
|
|
882
|
-
}
|
|
883
|
-
// Don't fail the whole process if push fails
|
|
868
|
+
const pushResult = gitForcePush({ branchName: featureBranch, token: githubToken, verbose });
|
|
869
|
+
if (!pushResult.success && verbose) {
|
|
870
|
+
logInfo(`⚠️ Could not force push ${featureBranch}: ${pushResult.error}`);
|
|
884
871
|
}
|
|
885
872
|
}
|
|
886
873
|
return { previousBranch };
|
|
@@ -908,23 +895,9 @@ export async function switchToFeatureBranchAndRebaseAsync(options) {
|
|
|
908
895
|
}
|
|
909
896
|
// Force push after rebase to trigger GitHub to recalculate PR diff
|
|
910
897
|
if (forcePushAfterRebase) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
execSync(`git push --force-with-lease origin ${featureBranch}`, {
|
|
916
|
-
encoding: 'utf-8',
|
|
917
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
918
|
-
});
|
|
919
|
-
if (verbose) {
|
|
920
|
-
logInfo(`✅ Successfully force pushed ${featureBranch}`);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
catch (pushError) {
|
|
924
|
-
if (verbose) {
|
|
925
|
-
logInfo(`⚠️ Could not force push ${featureBranch}: ${pushError instanceof Error ? pushError.message : String(pushError)}`);
|
|
926
|
-
}
|
|
927
|
-
// Don't fail the whole process if push fails
|
|
898
|
+
const pushResult = gitForcePush({ branchName: featureBranch, token: githubToken, verbose });
|
|
899
|
+
if (!pushResult.success && verbose) {
|
|
900
|
+
logInfo(`⚠️ Could not force push ${featureBranch}: ${pushResult.error}`);
|
|
928
901
|
}
|
|
929
902
|
}
|
|
930
903
|
return {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared git push utilities with GitHub App credential helper support.
|
|
3
|
+
*
|
|
4
|
+
* Uses `git -c credential.helper=...` to pass the token securely per-command,
|
|
5
|
+
* matching the pattern established in workspace-manager.ts for clone/fetch.
|
|
6
|
+
* Uses `execFileSync` instead of `execSync` to avoid shell metacharacter injection.
|
|
7
|
+
*/
|
|
8
|
+
export interface GitPushOptions {
|
|
9
|
+
branchName: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GitPushResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
method?: 'push-u' | 'push' | 'force-with-lease';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build git args with optional credential helper prefix.
|
|
21
|
+
* When a token is provided, prepends `-c credential.helper=...` so git
|
|
22
|
+
* authenticates with the GitHub App installation token.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildCredentialArgs(token?: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Push a branch to remote with three-level fallback:
|
|
27
|
+
* 1. git push -u origin <branch>
|
|
28
|
+
* 2. git push origin <branch>
|
|
29
|
+
* 3. git push --force-with-lease origin <branch>
|
|
30
|
+
*
|
|
31
|
+
* When `token` is provided, each command uses a credential helper for authentication.
|
|
32
|
+
*/
|
|
33
|
+
export declare function gitPush(options: GitPushOptions): GitPushResult;
|
|
34
|
+
/**
|
|
35
|
+
* Force push a branch using --force-with-lease.
|
|
36
|
+
* Used after rebase operations to update the remote branch.
|
|
37
|
+
*/
|
|
38
|
+
export declare function gitForcePush(options: GitPushOptions): GitPushResult;
|
|
39
|
+
/**
|
|
40
|
+
* Push the current branch to remote.
|
|
41
|
+
* Resolves the current branch name first, then delegates to gitPush.
|
|
42
|
+
*/
|
|
43
|
+
export declare function gitPushCurrentBranch(options: Omit<GitPushOptions, 'branchName'>): GitPushResult;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared git push utilities with GitHub App credential helper support.
|
|
3
|
+
*
|
|
4
|
+
* Uses `git -c credential.helper=...` to pass the token securely per-command,
|
|
5
|
+
* matching the pattern established in workspace-manager.ts for clone/fetch.
|
|
6
|
+
* Uses `execFileSync` instead of `execSync` to avoid shell metacharacter injection.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
9
|
+
import { logInfo } from './logger.js';
|
|
10
|
+
/**
|
|
11
|
+
* Build git args with optional credential helper prefix.
|
|
12
|
+
* When a token is provided, prepends `-c credential.helper=...` so git
|
|
13
|
+
* authenticates with the GitHub App installation token.
|
|
14
|
+
*/
|
|
15
|
+
export function buildCredentialArgs(token) {
|
|
16
|
+
if (!token)
|
|
17
|
+
return [];
|
|
18
|
+
const credentialHelper = `!f() { echo "username=x-access-token"; echo "password=${token}"; }; f`;
|
|
19
|
+
return ['-c', `credential.helper=${credentialHelper}`];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Push a branch to remote with three-level fallback:
|
|
23
|
+
* 1. git push -u origin <branch>
|
|
24
|
+
* 2. git push origin <branch>
|
|
25
|
+
* 3. git push --force-with-lease origin <branch>
|
|
26
|
+
*
|
|
27
|
+
* When `token` is provided, each command uses a credential helper for authentication.
|
|
28
|
+
*/
|
|
29
|
+
export function gitPush(options) {
|
|
30
|
+
const { branchName, token, verbose, cwd } = options;
|
|
31
|
+
const credArgs = buildCredentialArgs(token);
|
|
32
|
+
const execOpts = {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: (verbose ? 'inherit' : 'pipe'),
|
|
35
|
+
};
|
|
36
|
+
if (verbose) {
|
|
37
|
+
logInfo(`📤 Pushing branch ${branchName} to remote...`);
|
|
38
|
+
}
|
|
39
|
+
// Try 1: push -u (set upstream tracking)
|
|
40
|
+
try {
|
|
41
|
+
execFileSync('git', [...credArgs, 'push', '-u', 'origin', branchName], execOpts);
|
|
42
|
+
return { success: true, method: 'push-u' };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// fall through
|
|
46
|
+
}
|
|
47
|
+
// Try 2: plain push
|
|
48
|
+
try {
|
|
49
|
+
execFileSync('git', [...credArgs, 'push', 'origin', branchName], execOpts);
|
|
50
|
+
return { success: true, method: 'push' };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// fall through
|
|
54
|
+
}
|
|
55
|
+
// Try 3: force push with lease
|
|
56
|
+
if (verbose) {
|
|
57
|
+
logInfo(`⚠️ Push rejected, attempting force push with lease...`);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
execFileSync('git', [...credArgs, 'push', '--force-with-lease', 'origin', branchName], execOpts);
|
|
61
|
+
if (verbose) {
|
|
62
|
+
logInfo(`✅ Successfully force pushed ${branchName}`);
|
|
63
|
+
}
|
|
64
|
+
return { success: true, method: 'force-with-lease' };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error: error instanceof Error ? error.message : String(error),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Force push a branch using --force-with-lease.
|
|
75
|
+
* Used after rebase operations to update the remote branch.
|
|
76
|
+
*/
|
|
77
|
+
export function gitForcePush(options) {
|
|
78
|
+
const { branchName, token, verbose, cwd } = options;
|
|
79
|
+
const credArgs = buildCredentialArgs(token);
|
|
80
|
+
const execOpts = {
|
|
81
|
+
cwd,
|
|
82
|
+
stdio: (verbose ? 'inherit' : 'pipe'),
|
|
83
|
+
};
|
|
84
|
+
if (verbose) {
|
|
85
|
+
logInfo(`📤 Force pushing ${branchName} to remote...`);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
execFileSync('git', [...credArgs, 'push', '--force-with-lease', 'origin', branchName], execOpts);
|
|
89
|
+
if (verbose) {
|
|
90
|
+
logInfo(`✅ Successfully force pushed ${branchName}`);
|
|
91
|
+
}
|
|
92
|
+
return { success: true, method: 'force-with-lease' };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: error instanceof Error ? error.message : String(error),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Push the current branch to remote.
|
|
103
|
+
* Resolves the current branch name first, then delegates to gitPush.
|
|
104
|
+
*/
|
|
105
|
+
export function gitPushCurrentBranch(options) {
|
|
106
|
+
const { token, verbose, cwd } = options;
|
|
107
|
+
let branchName;
|
|
108
|
+
try {
|
|
109
|
+
branchName = execFileSync('git', ['branch', '--show-current'], {
|
|
110
|
+
encoding: 'utf-8',
|
|
111
|
+
cwd,
|
|
112
|
+
stdio: 'pipe',
|
|
113
|
+
}).trim();
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: `Failed to resolve current branch: ${error instanceof Error ? error.message : String(error)}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (!branchName) {
|
|
122
|
+
return { success: false, error: 'Could not determine current branch (detached HEAD?)' };
|
|
123
|
+
}
|
|
124
|
+
return gitPush({ branchName, token, verbose, cwd });
|
|
125
|
+
}
|