edsger 0.52.0 ā 0.54.0
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/dist/phases/autonomous/index.js +9 -6
- package/dist/phases/branch-planning/index.js +1 -1
- package/dist/phases/branch-planning/prompts.d.ts +1 -1
- package/dist/phases/branch-planning/prompts.js +3 -2
- package/dist/phases/bug-fixing/analyzer.js +6 -2
- package/dist/phases/code-implementation/branch-pr-creator.js +7 -5
- package/dist/phases/code-implementation/index.js +6 -2
- package/dist/phases/code-refine/index.js +8 -5
- package/dist/phases/code-review/index.js +8 -5
- package/dist/phases/code-testing/analyzer.js +11 -7
- package/dist/phases/pr-execution/context.d.ts +2 -0
- package/dist/phases/pr-execution/context.js +41 -32
- package/dist/phases/pr-execution/index.js +26 -17
- package/dist/phases/pr-execution/prompts.d.ts +2 -1
- package/dist/phases/pr-execution/prompts.js +12 -10
- package/dist/phases/pr-splitting/context.js +18 -13
- package/dist/phases/pr-splitting/index.js +1 -1
- package/dist/phases/pr-splitting/prompts.d.ts +1 -1
- package/dist/phases/pr-splitting/prompts.js +3 -2
- package/dist/phases/pull-request/creator.js +10 -7
- package/dist/phases/pull-request/handler.js +3 -1
- package/dist/services/repo-config.d.ts +17 -0
- package/dist/services/repo-config.js +50 -0
- package/dist/utils/git-branch-manager-async.js +9 -5
- package/dist/utils/git-branch-manager.js +17 -7
- package/dist/utils/github-repo-info.d.ts +13 -1
- package/dist/utils/github-repo-info.js +32 -6
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import { getIssue } from '../../api/issues/get-issue.js';
|
|
|
12
12
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
13
13
|
import { logIssuePhaseEvent } from '../../services/audit-logs.js';
|
|
14
14
|
import { createBranches, getCurrentBranch, updateBranch, } from '../../services/branches.js';
|
|
15
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
15
16
|
import { prepareCustomBranchGitEnvironmentAsync, resetUncommittedChanges, } from '../../utils/git-branch-manager.js';
|
|
16
17
|
import { gitPush } from '../../utils/git-push.js';
|
|
17
18
|
import { logDebug, logError, logInfo } from '../../utils/logger.js';
|
|
@@ -128,7 +129,7 @@ function parseIterationResult(responseText) {
|
|
|
128
129
|
* Handle a successful iteration: push, create PR, and log
|
|
129
130
|
*/
|
|
130
131
|
async function handleSuccessfulIteration(opts) {
|
|
131
|
-
const { issueId, devBranchName, currentBranch, iterationResult, totalIterations, iterationDuration, startTime, endTime, existingPrUrl, verbose, } = opts;
|
|
132
|
+
const { issueId, devBranchName, currentBranch, iterationResult, totalIterations, iterationDuration, startTime, endTime, existingPrUrl, defaultBranch, verbose, } = opts;
|
|
132
133
|
// Get GitHub config for push authentication and PR creation
|
|
133
134
|
const githubConfig = await getGitHubConfig(issueId, verbose);
|
|
134
135
|
// Push to remote with authentication
|
|
@@ -144,7 +145,7 @@ async function handleSuccessfulIteration(opts) {
|
|
|
144
145
|
let prUrl = existingPrUrl;
|
|
145
146
|
let prNumber;
|
|
146
147
|
if (!prUrl) {
|
|
147
|
-
const prInfo = await createAutonomousPR(githubConfig, devBranchName, currentBranch, verbose);
|
|
148
|
+
const prInfo = await createAutonomousPR(githubConfig, devBranchName, currentBranch, defaultBranch, verbose);
|
|
148
149
|
({ prUrl, prNumber } = prInfo);
|
|
149
150
|
}
|
|
150
151
|
// Log iteration completion to audit logs
|
|
@@ -173,7 +174,7 @@ async function handleSuccessfulIteration(opts) {
|
|
|
173
174
|
*/
|
|
174
175
|
async function createAutonomousPR(
|
|
175
176
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
-
githubConfig, devBranchName, currentBranch, verbose) {
|
|
177
|
+
githubConfig, devBranchName, currentBranch, defaultBranch, verbose) {
|
|
177
178
|
if (!githubConfig.configured ||
|
|
178
179
|
!githubConfig.token ||
|
|
179
180
|
!githubConfig.owner ||
|
|
@@ -190,7 +191,7 @@ githubConfig, devBranchName, currentBranch, verbose) {
|
|
|
190
191
|
verbose,
|
|
191
192
|
};
|
|
192
193
|
logInfo(`š Creating pull request for autonomous work...`);
|
|
193
|
-
const prResult = await createBranchPullRequest(prConfig, devBranchName, currentBranch.name, currentBranch.description || 'Autonomous development implementation',
|
|
194
|
+
const prResult = await createBranchPullRequest(prConfig, devBranchName, currentBranch.name, currentBranch.description || 'Autonomous development implementation', defaultBranch);
|
|
194
195
|
if (prResult.success) {
|
|
195
196
|
logInfo(`ā
Pull request created: ${prResult.pullRequestUrl}`);
|
|
196
197
|
return {
|
|
@@ -299,10 +300,11 @@ export async function runAutonomousDevelopment(options, config, _checklistContex
|
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
|
-
// 4. Prepare git environment (checkout branch + rebase with
|
|
303
|
+
// 4. Prepare git environment (checkout branch + rebase with default branch)
|
|
304
|
+
const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
|
|
303
305
|
const { cleanup: cleanupGit } = await prepareCustomBranchGitEnvironmentAsync({
|
|
304
306
|
issueBranch: devBranchName,
|
|
305
|
-
baseBranch:
|
|
307
|
+
baseBranch: defaultBranch,
|
|
306
308
|
verbose,
|
|
307
309
|
resolveConflicts: true,
|
|
308
310
|
conflictResolverConfig: {
|
|
@@ -334,6 +336,7 @@ export async function runAutonomousDevelopment(options, config, _checklistContex
|
|
|
334
336
|
startTime,
|
|
335
337
|
endTime,
|
|
336
338
|
existingPrUrl: prUrl,
|
|
339
|
+
defaultBranch,
|
|
337
340
|
verbose,
|
|
338
341
|
});
|
|
339
342
|
prUrl = prInfo.prUrl || prUrl;
|
|
@@ -169,7 +169,7 @@ export const planIssueBranches = async (options, config) => {
|
|
|
169
169
|
// Build the user prompt based on mode
|
|
170
170
|
const contextInfo = formatContextForPrompt(context.issue, context.product, context.user_stories, context.test_cases);
|
|
171
171
|
const existingBranchesInfo = formatExistingBranchesForPrompt(context.existing_branches);
|
|
172
|
-
const systemPrompt = await createBranchPlanningSystemPrompt(config, issueId);
|
|
172
|
+
const systemPrompt = await createBranchPlanningSystemPrompt(config, issueId, undefined, verbose);
|
|
173
173
|
const userPrompt = await buildUserPrompt({
|
|
174
174
|
isIncrementalUpdate,
|
|
175
175
|
feedbacksContext,
|
|
@@ -2,7 +2,7 @@ import { type EdsgerConfig } from '../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Create the system prompt for branch planning
|
|
4
4
|
*/
|
|
5
|
-
export declare function createBranchPlanningSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string): Promise<string>;
|
|
5
|
+
export declare function createBranchPlanningSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string, verbose?: boolean): Promise<string>;
|
|
6
6
|
/**
|
|
7
7
|
* Create the user prompt with context for branch planning
|
|
8
8
|
*/
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
1
2
|
import { resolveSkill, substituteVariables, } from '../../services/skill-resolver.js';
|
|
2
3
|
import { OUTPUT_CONTRACTS } from '../output-contracts.js';
|
|
3
4
|
/**
|
|
4
5
|
* Create the system prompt for branch planning
|
|
5
6
|
*/
|
|
6
|
-
export async function createBranchPlanningSystemPrompt(config, issueId, projectDir) {
|
|
7
|
+
export async function createBranchPlanningSystemPrompt(config, issueId, projectDir, verbose) {
|
|
7
8
|
const skill = await resolveSkill('phase/branch-planning', {
|
|
8
9
|
projectDir,
|
|
9
10
|
});
|
|
@@ -12,7 +13,7 @@ export async function createBranchPlanningSystemPrompt(config, issueId, projectD
|
|
|
12
13
|
}
|
|
13
14
|
let { prompt } = skill;
|
|
14
15
|
prompt = substituteVariables(prompt, {
|
|
15
|
-
BASE_BRANCH:
|
|
16
|
+
BASE_BRANCH: await getDefaultBranchForIssue(issueId, verbose),
|
|
16
17
|
ISSUE_ID: issueId,
|
|
17
18
|
});
|
|
18
19
|
return `${prompt}\n\n${OUTPUT_CONTRACTS['branch-planning']}`;
|
|
@@ -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 { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
|
|
4
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
4
5
|
import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
|
|
5
6
|
import { extractJsonFromResponse } from '../../utils/json-extractor.js';
|
|
6
7
|
import { logDebug, logError, logInfo } from '../../utils/logger.js';
|
|
@@ -38,8 +39,11 @@ export const fixTestFailures = async (options, config) => {
|
|
|
38
39
|
if (verbose) {
|
|
39
40
|
logInfo(`Starting bug fixing for issue ID: ${issueId} (Attempt ${attemptNumber})`);
|
|
40
41
|
}
|
|
41
|
-
// Prepare git environment: switch to issue branch and rebase with
|
|
42
|
-
|
|
42
|
+
// Prepare git environment: switch to issue branch and rebase with the
|
|
43
|
+
// repo's default branch (resolved via GitHub API for consistency with
|
|
44
|
+
// other phases ā same source of truth for forks where origin/HEAD may lag).
|
|
45
|
+
const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
|
|
46
|
+
const cleanupGit = preparePhaseGitEnvironment(issueId, defaultBranch, verbose);
|
|
43
47
|
try {
|
|
44
48
|
// Fetch all required context information via MCP endpoints
|
|
45
49
|
if (verbose) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
6
|
import { gitPush } from '../../utils/git-push.js';
|
|
7
|
+
import { resolveDefaultBranch } from '../../utils/github-repo-info.js';
|
|
7
8
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
8
9
|
// GitHub PR title best practice: keep under 72 characters
|
|
9
10
|
const MAX_PR_TITLE_LENGTH = 72;
|
|
@@ -44,7 +45,8 @@ export function devBranchToFeatBranch(devBranchName) {
|
|
|
44
45
|
* Create a pull request from a dev/ branch to its corresponding feat/ branch
|
|
45
46
|
* This is used after code implementation in multi-branch issues
|
|
46
47
|
*/
|
|
47
|
-
export async function createBranchPullRequest(config, devBranchName, issueName, branchDescription, baseBranch
|
|
48
|
+
export async function createBranchPullRequest(config, devBranchName, issueName, branchDescription, baseBranch) {
|
|
49
|
+
const resolvedBaseBranch = baseBranch ?? resolveDefaultBranch();
|
|
48
50
|
const { githubToken, owner, repo, verbose } = config;
|
|
49
51
|
if (!devBranchName.startsWith('dev/')) {
|
|
50
52
|
return {
|
|
@@ -75,13 +77,13 @@ export async function createBranchPullRequest(config, devBranchName, issueName,
|
|
|
75
77
|
logInfo(`š Ensuring target branch ${featBranchName} exists...`);
|
|
76
78
|
}
|
|
77
79
|
try {
|
|
78
|
-
// Get the base branch
|
|
80
|
+
// Get the base branch SHA
|
|
79
81
|
const { data: baseBranchData } = await octokit.repos.getBranch({
|
|
80
82
|
owner,
|
|
81
83
|
repo,
|
|
82
|
-
branch:
|
|
84
|
+
branch: resolvedBaseBranch,
|
|
83
85
|
});
|
|
84
|
-
// Create the feat/ branch from
|
|
86
|
+
// Create the feat/ branch from the resolved base
|
|
85
87
|
await octokit.git.createRef({
|
|
86
88
|
owner,
|
|
87
89
|
repo,
|
|
@@ -89,7 +91,7 @@ export async function createBranchPullRequest(config, devBranchName, issueName,
|
|
|
89
91
|
sha: baseBranchData.commit.sha,
|
|
90
92
|
});
|
|
91
93
|
if (verbose) {
|
|
92
|
-
logInfo(`ā
Created target branch ${featBranchName} from ${
|
|
94
|
+
logInfo(`ā
Created target branch ${featBranchName} from ${resolvedBaseBranch}`);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
catch (error) {
|
|
@@ -8,6 +8,7 @@ import { createBranches, getBaseBranchInfo, getBranches, getCurrentBranch, updat
|
|
|
8
8
|
import { formatChecklistsForContext, } from '../../services/checklist.js';
|
|
9
9
|
import { extractChecklistItems, runPhaseCoaching, } from '../../services/coaching/index.js';
|
|
10
10
|
import { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
|
|
11
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
11
12
|
import { prepareCustomBranchGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
|
|
12
13
|
import { gitPush } from '../../utils/git-push.js';
|
|
13
14
|
import { extractJsonFromResponse } from '../../utils/json-extractor.js';
|
|
@@ -172,7 +173,10 @@ message, lastAssistantResponse, issueId, verbose
|
|
|
172
173
|
}
|
|
173
174
|
// eslint-disable-next-line complexity
|
|
174
175
|
export const implementIssueCode = async (options, config, checklistContext) => {
|
|
175
|
-
const { issueId, verbose
|
|
176
|
+
const { issueId, verbose } = options;
|
|
177
|
+
// Resolve the repo's default branch (handles forks where default is master, etc.).
|
|
178
|
+
// Caller may override via options.baseBranch (e.g. branch-chained PR base).
|
|
179
|
+
const baseBranch = options.baseBranch ?? (await getDefaultBranchForIssue(issueId, verbose));
|
|
176
180
|
if (verbose) {
|
|
177
181
|
logInfo(`Starting code implementation for issue ID: ${issueId}`);
|
|
178
182
|
logInfo(`Base branch: ${baseBranch}`);
|
|
@@ -322,7 +326,7 @@ export const implementIssueCode = async (options, config, checklistContext) => {
|
|
|
322
326
|
githubToken: githubConfig.token,
|
|
323
327
|
owner: githubConfig.owner,
|
|
324
328
|
repo: githubConfig.repo,
|
|
325
|
-
baseBranch
|
|
329
|
+
baseBranch,
|
|
326
330
|
verbose,
|
|
327
331
|
});
|
|
328
332
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getGitHubConfig } from '../../api/github.js';
|
|
7
7
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
8
8
|
import { getBaseBranchInfo, getBranches, updateBranch, } from '../../services/branches.js';
|
|
9
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
9
10
|
import { getUncommittedFiles, hasUncommittedChanges, prepareCustomBranchGitEnvironmentAsync, preparePhaseGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
|
|
10
11
|
import { gitPushCurrentBranch } from '../../utils/git-push.js';
|
|
11
12
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
@@ -47,12 +48,14 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
|
|
|
47
48
|
if (verbose) {
|
|
48
49
|
logInfo(`Starting code refine for issue ID: ${issueId}`);
|
|
49
50
|
}
|
|
51
|
+
// Resolve the repo's default branch (handles forks where default is master, etc.)
|
|
52
|
+
const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
|
|
50
53
|
// For multi-branch issues, find the branch that has been reviewed
|
|
51
54
|
// (status = 'reviewed' after code-review phase) and use its branch_name for refine
|
|
52
55
|
let branchName = `dev/${issueId}`; // Default for single-branch issues
|
|
53
56
|
let currentBranch = null;
|
|
54
57
|
let allBranches = [];
|
|
55
|
-
let baseBranchForRebase =
|
|
58
|
+
let baseBranchForRebase = defaultBranch; // Default base branch for rebase
|
|
56
59
|
let rebaseTargetBranchForRebase; // Target for --onto rebase (e.g., main)
|
|
57
60
|
let originalBaseBranchForRebase; // For --onto when base was squash-merged
|
|
58
61
|
let baseBranchCompletedForRebase = false;
|
|
@@ -68,7 +71,7 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
|
|
|
68
71
|
logInfo(` Using dev branch: ${branchName}`);
|
|
69
72
|
}
|
|
70
73
|
// Determine the correct base branch for rebase using base_branch_id
|
|
71
|
-
const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches,
|
|
74
|
+
const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches, defaultBranch);
|
|
72
75
|
// Always use the baseBranch from getBaseBranchInfo - it handles all cases:
|
|
73
76
|
// - No base branch: returns 'main'
|
|
74
77
|
// - Base branch merged: returns feat branch (new branches created from feat/xxx)
|
|
@@ -114,14 +117,14 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
|
|
|
114
117
|
githubConfig.repo) {
|
|
115
118
|
const featBranchName = branchName.replace(/^dev\//, 'feat/');
|
|
116
119
|
if (verbose) {
|
|
117
|
-
logInfo(`š„ Syncing ${featBranchName} with
|
|
120
|
+
logInfo(`š„ Syncing ${featBranchName} with ${defaultBranch} before rebase...`);
|
|
118
121
|
}
|
|
119
122
|
featSyncedToMain = await syncFeatBranchWithMain({
|
|
120
123
|
featBranch: featBranchName,
|
|
121
124
|
githubToken: githubConfig.token,
|
|
122
125
|
owner: githubConfig.owner,
|
|
123
126
|
repo: githubConfig.repo,
|
|
124
|
-
baseBranch:
|
|
127
|
+
baseBranch: defaultBranch,
|
|
125
128
|
verbose,
|
|
126
129
|
});
|
|
127
130
|
}
|
|
@@ -150,7 +153,7 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
|
|
|
150
153
|
baseBranchCompleted: baseBranchCompletedForRebase,
|
|
151
154
|
githubToken,
|
|
152
155
|
})
|
|
153
|
-
: await preparePhaseGitEnvironmentAsync(issueId,
|
|
156
|
+
: await preparePhaseGitEnvironmentAsync(issueId, defaultBranch, verbose, true, {
|
|
154
157
|
model: DEFAULT_MODEL,
|
|
155
158
|
});
|
|
156
159
|
const cleanupGit = gitEnvResult.cleanup;
|
|
@@ -10,6 +10,7 @@ import { DEFAULT_MODEL } from '../../constants.js';
|
|
|
10
10
|
import { getBaseBranchInfo, getBranches, updateBranch, } from '../../services/branches.js';
|
|
11
11
|
import { formatChecklistsForContext, } from '../../services/checklist.js';
|
|
12
12
|
import { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
|
|
13
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
13
14
|
import { prepareCustomBranchGitEnvironmentAsync, preparePhaseGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
|
|
14
15
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
15
16
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
@@ -73,12 +74,14 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
|
|
|
73
74
|
if (verbose) {
|
|
74
75
|
logInfo(`Starting code review for issue ID: ${issueId}`);
|
|
75
76
|
}
|
|
77
|
+
// Resolve the repo's default branch (handles forks where default is master, etc.)
|
|
78
|
+
const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
|
|
76
79
|
// For multi-branch issues, find the branch that is ready for review
|
|
77
80
|
// and use its branch_name for review (branch_name is stored as dev/...)
|
|
78
81
|
let branchName = `dev/${issueId}`; // Default for single-branch issues
|
|
79
82
|
let currentBranch = null;
|
|
80
83
|
let allBranches = [];
|
|
81
|
-
let baseBranchForRebase =
|
|
84
|
+
let baseBranchForRebase = defaultBranch; // Default base branch for rebase
|
|
82
85
|
let rebaseTargetBranchForRebase; // Target for --onto rebase (e.g., main)
|
|
83
86
|
let originalBaseBranchForRebase; // For --onto when base was squash-merged
|
|
84
87
|
let baseBranchCompletedForRebase = false;
|
|
@@ -95,7 +98,7 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
|
|
|
95
98
|
logInfo(` Using dev branch: ${branchName}`);
|
|
96
99
|
}
|
|
97
100
|
// Determine the correct base branch for rebase using base_branch_id
|
|
98
|
-
const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches,
|
|
101
|
+
const baseBranchInfo = getBaseBranchInfo(currentBranch, allBranches, defaultBranch);
|
|
99
102
|
// Always use the baseBranch from getBaseBranchInfo - it handles all cases:
|
|
100
103
|
// - No base branch: returns 'main'
|
|
101
104
|
// - Base branch merged: returns feat branch (new branches created from feat/xxx)
|
|
@@ -141,14 +144,14 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
|
|
|
141
144
|
githubConfig.repo) {
|
|
142
145
|
const featBranchName = branchName.replace(/^dev\//, 'feat/');
|
|
143
146
|
if (verbose) {
|
|
144
|
-
logInfo(`š„ Syncing ${featBranchName} with
|
|
147
|
+
logInfo(`š„ Syncing ${featBranchName} with ${defaultBranch} before rebase...`);
|
|
145
148
|
}
|
|
146
149
|
featSyncedToMain = await syncFeatBranchWithMain({
|
|
147
150
|
featBranch: featBranchName,
|
|
148
151
|
githubToken: githubConfig.token,
|
|
149
152
|
owner: githubConfig.owner,
|
|
150
153
|
repo: githubConfig.repo,
|
|
151
|
-
baseBranch:
|
|
154
|
+
baseBranch: defaultBranch,
|
|
152
155
|
verbose,
|
|
153
156
|
});
|
|
154
157
|
}
|
|
@@ -177,7 +180,7 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
|
|
|
177
180
|
baseBranchCompleted: baseBranchCompletedForRebase,
|
|
178
181
|
githubToken,
|
|
179
182
|
})
|
|
180
|
-
: await preparePhaseGitEnvironmentAsync(issueId,
|
|
183
|
+
: await preparePhaseGitEnvironmentAsync(issueId, defaultBranch, verbose, true, {
|
|
181
184
|
model: DEFAULT_MODEL,
|
|
182
185
|
});
|
|
183
186
|
const cleanupGit = gitEnvResult.cleanup;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
3
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
4
|
+
import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
|
|
5
|
+
import { extractJsonFromResponse } from '../../utils/json-extractor.js';
|
|
6
|
+
import { logDebug, logError, logInfo } from '../../utils/logger.js';
|
|
7
|
+
import { fetchCodeTestingContext, formatContextForPrompt, } from './context-fetcher.js';
|
|
8
|
+
import { createCodeTestingPromptWithContext, createCodeTestingSystemPrompt, } from './prompts.js';
|
|
3
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
10
|
function processAssistantContent(content, verbose) {
|
|
5
11
|
let text = '';
|
|
@@ -14,11 +20,6 @@ function processAssistantContent(content, verbose) {
|
|
|
14
20
|
}
|
|
15
21
|
return text;
|
|
16
22
|
}
|
|
17
|
-
import { preparePhaseGitEnvironment } from '../../utils/git-branch-manager.js';
|
|
18
|
-
import { extractJsonFromResponse } from '../../utils/json-extractor.js';
|
|
19
|
-
import { logDebug, logError, logInfo } from '../../utils/logger.js';
|
|
20
|
-
import { fetchCodeTestingContext, formatContextForPrompt, } from './context-fetcher.js';
|
|
21
|
-
import { createCodeTestingPromptWithContext, createCodeTestingSystemPrompt, } from './prompts.js';
|
|
22
23
|
function userMessage(content) {
|
|
23
24
|
return {
|
|
24
25
|
type: 'user',
|
|
@@ -37,8 +38,11 @@ export const writeCodeTests = async (options, config) => {
|
|
|
37
38
|
if (verbose) {
|
|
38
39
|
logInfo(`Starting code testing phase for issue ID: ${issueId}`);
|
|
39
40
|
}
|
|
40
|
-
// Prepare git environment: switch to issue branch and rebase with
|
|
41
|
-
|
|
41
|
+
// Prepare git environment: switch to issue branch and rebase with the
|
|
42
|
+
// repo's default branch (resolved via GitHub API for consistency with
|
|
43
|
+
// other phases ā same source of truth for forks where origin/HEAD may lag).
|
|
44
|
+
const defaultBranch = await getDefaultBranchForIssue(issueId, verbose);
|
|
45
|
+
const cleanupGit = preparePhaseGitEnvironment(issueId, defaultBranch, verbose);
|
|
42
46
|
try {
|
|
43
47
|
// Fetch all required context information via MCP endpoints
|
|
44
48
|
if (verbose) {
|
|
@@ -19,6 +19,8 @@ export interface PRExecutionContext {
|
|
|
19
19
|
devBranchHeadSha: string;
|
|
20
20
|
githubConfig: GitHubConfigInfo;
|
|
21
21
|
forkInfo: RepoForkInfo;
|
|
22
|
+
/** Resolved default branch (e.g. main, master). Used for checkout/diff. */
|
|
23
|
+
defaultBranch: string;
|
|
22
24
|
isIncrementalSync: boolean;
|
|
23
25
|
lastSyncedCommit: string | null;
|
|
24
26
|
diffStat: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFileSync
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { getGitHubConfig } from '../../api/github.js';
|
|
3
3
|
import { getIssue } from '../../api/issues/index.js';
|
|
4
4
|
import { getPullRequests, } from '../../services/pull-requests.js';
|
|
5
5
|
import { branchExists, remoteBranchExists, } from '../../utils/git-branch-manager.js';
|
|
6
6
|
import { buildCredentialArgs } from '../../utils/git-push.js';
|
|
7
|
-
import { getRepoForkInfo, } from '../../utils/github-repo-info.js';
|
|
7
|
+
import { getRepoForkInfo, resolveDefaultBranch, } from '../../utils/github-repo-info.js';
|
|
8
8
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
9
9
|
/**
|
|
10
10
|
* Get the dev branch name for an issue
|
|
@@ -16,14 +16,16 @@ function getDevBranchName(issueId) {
|
|
|
16
16
|
* Get the HEAD SHA of a branch
|
|
17
17
|
*/
|
|
18
18
|
function getBranchHeadSha(branchName) {
|
|
19
|
-
return
|
|
19
|
+
return execFileSync('git', ['rev-parse', branchName], {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
}).trim();
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Get diff stat between two refs
|
|
23
25
|
*/
|
|
24
26
|
function getDiffStat(baseRef, headRef) {
|
|
25
27
|
try {
|
|
26
|
-
return
|
|
28
|
+
return execFileSync('git', ['diff', '--stat', `${baseRef}...${headRef}`], {
|
|
27
29
|
encoding: 'utf-8',
|
|
28
30
|
}).trim();
|
|
29
31
|
}
|
|
@@ -51,9 +53,7 @@ function parseGitStatus(status) {
|
|
|
51
53
|
*/
|
|
52
54
|
function getChangedFiles(baseRef, headRef) {
|
|
53
55
|
try {
|
|
54
|
-
const output =
|
|
55
|
-
encoding: 'utf-8',
|
|
56
|
-
}).trim();
|
|
56
|
+
const output = execFileSync('git', ['diff', '--name-status', `${baseRef}...${headRef}`], { encoding: 'utf-8' }).trim();
|
|
57
57
|
if (!output) {
|
|
58
58
|
return [];
|
|
59
59
|
}
|
|
@@ -77,7 +77,7 @@ function getChangedFiles(baseRef, headRef) {
|
|
|
77
77
|
*/
|
|
78
78
|
function isAncestor(commitSha, ref) {
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
execFileSync('git', ['merge-base', '--is-ancestor', commitSha, ref], {
|
|
81
81
|
encoding: 'utf-8',
|
|
82
82
|
stdio: 'pipe',
|
|
83
83
|
});
|
|
@@ -121,25 +121,50 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
121
121
|
getPullRequests({ issueId, verbose }),
|
|
122
122
|
getGitHubConfig(issueId, verbose),
|
|
123
123
|
]);
|
|
124
|
-
//
|
|
124
|
+
// Detect fork status + default branch (used by the fast-forward step below)
|
|
125
|
+
let forkInfo = { isFork: false };
|
|
126
|
+
if (githubConfig.token && githubConfig.owner && githubConfig.repo) {
|
|
127
|
+
try {
|
|
128
|
+
forkInfo = await getRepoForkInfo(githubConfig.token, githubConfig.owner, githubConfig.repo);
|
|
129
|
+
if (verbose) {
|
|
130
|
+
logInfo(forkInfo.isFork
|
|
131
|
+
? `š Repository is a fork of ${forkInfo.upstream?.owner}/${forkInfo.upstream?.repo}`
|
|
132
|
+
: `š Repository is not a fork`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (verbose) {
|
|
137
|
+
logError(`Failed to detect fork status: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Resolve the repo's default branch (e.g. main, master, develop). Falls back
|
|
142
|
+
// through GitHub API ā local symbolic-ref ā literal 'main'.
|
|
143
|
+
const defaultBranch = resolveDefaultBranch(forkInfo.defaultBranch);
|
|
144
|
+
// Fetch latest remote refs and fast-forward the local default branch.
|
|
145
|
+
// Use execFileSync (not execSync) so defaultBranch ā which can flow in from
|
|
146
|
+
// the GitHub API's default_branch field ā is never passed through a shell.
|
|
125
147
|
try {
|
|
126
148
|
const credArgs = buildCredentialArgs(githubConfig.token);
|
|
127
149
|
execFileSync('git', [...credArgs, 'fetch', 'origin'], {
|
|
128
150
|
encoding: 'utf-8',
|
|
129
151
|
stdio: 'pipe',
|
|
130
152
|
});
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
execFileSync('git', ['checkout', defaultBranch], {
|
|
154
|
+
encoding: 'utf-8',
|
|
155
|
+
stdio: 'pipe',
|
|
156
|
+
});
|
|
157
|
+
execFileSync('git', ['merge', '--ff-only', `origin/${defaultBranch}`], {
|
|
133
158
|
encoding: 'utf-8',
|
|
134
159
|
stdio: 'pipe',
|
|
135
160
|
});
|
|
136
161
|
if (verbose) {
|
|
137
|
-
logInfo(
|
|
162
|
+
logInfo(`ā
Local ${defaultBranch} synced with origin/${defaultBranch}`);
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
catch (error) {
|
|
141
166
|
if (verbose) {
|
|
142
|
-
logInfo(`ā ļø Could not sync
|
|
167
|
+
logInfo(`ā ļø Could not sync ${defaultBranch} with origin: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
168
|
}
|
|
144
169
|
}
|
|
145
170
|
// Verify dev branch exists
|
|
@@ -168,23 +193,6 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
168
193
|
if (!githubConfig.configured) {
|
|
169
194
|
throw new Error(`GitHub is not configured. ${githubConfig.message || 'Please configure GitHub integration.'}`);
|
|
170
195
|
}
|
|
171
|
-
// Detect fork status
|
|
172
|
-
let forkInfo = { isFork: false };
|
|
173
|
-
if (githubConfig.token && githubConfig.owner && githubConfig.repo) {
|
|
174
|
-
try {
|
|
175
|
-
forkInfo = await getRepoForkInfo(githubConfig.token, githubConfig.owner, githubConfig.repo);
|
|
176
|
-
if (verbose) {
|
|
177
|
-
logInfo(forkInfo.isFork
|
|
178
|
-
? `š Repository is a fork of ${forkInfo.upstream?.owner}/${forkInfo.upstream?.repo}`
|
|
179
|
-
: `š Repository is not a fork`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
if (verbose) {
|
|
184
|
-
logError(`Failed to detect fork status: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
196
|
// Determine sync mode
|
|
189
197
|
const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
|
|
190
198
|
const devBranchHeadSha = getBranchHeadSha(devRef);
|
|
@@ -202,8 +210,8 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
202
210
|
lastSyncedCommit = null;
|
|
203
211
|
isIncrementalSync = false;
|
|
204
212
|
}
|
|
205
|
-
// Get diff info: for incremental, diff from last sync; for first run, diff from
|
|
206
|
-
const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit :
|
|
213
|
+
// Get diff info: for incremental, diff from last sync; for first run, diff from default branch
|
|
214
|
+
const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : defaultBranch;
|
|
207
215
|
const diffStat = getDiffStat(diffBase, devRef);
|
|
208
216
|
const changedFiles = getChangedFiles(diffBase, devRef);
|
|
209
217
|
if (verbose) {
|
|
@@ -223,6 +231,7 @@ export async function fetchPRExecutionContext(issueId, verbose) {
|
|
|
223
231
|
devBranchHeadSha,
|
|
224
232
|
githubConfig,
|
|
225
233
|
forkInfo,
|
|
234
|
+
defaultBranch,
|
|
226
235
|
isIncrementalSync,
|
|
227
236
|
lastSyncedCommit,
|
|
228
237
|
diffStat,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
3
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
4
4
|
import { logIssuePhaseEvent } from '../../services/audit-logs.js';
|
|
5
5
|
import { getPullRequests, } from '../../services/pull-requests.js';
|
|
6
6
|
import { getCurrentBranch, returnToMainBranch, } from '../../utils/git-branch-manager.js';
|
|
7
|
+
import { resolveDefaultBranch } from '../../utils/github-repo-info.js';
|
|
7
8
|
import { logDebug, logError, logInfo } from '../../utils/logger.js';
|
|
8
9
|
import { fetchPRExecutionContext } from './context.js';
|
|
9
10
|
import { assignNewFilesToPRs, removeDeletedFilesFromPRs, removeStaleFilesFromPRs, } from './file-assigner.js';
|
|
@@ -118,13 +119,16 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
118
119
|
? '\nš Syncing PR branches with latest changes...'
|
|
119
120
|
: '\nš§ Creating PR branches and moving code...');
|
|
120
121
|
}
|
|
121
|
-
// Ensure we're on
|
|
122
|
+
// Ensure we're on the repo's default branch before the agent starts
|
|
122
123
|
const currentBranch = getCurrentBranch();
|
|
123
|
-
if (currentBranch !==
|
|
124
|
+
if (currentBranch !== context.defaultBranch) {
|
|
124
125
|
if (verbose) {
|
|
125
|
-
logInfo(`Switching from ${currentBranch} to
|
|
126
|
+
logInfo(`Switching from ${currentBranch} to ${context.defaultBranch}...`);
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
execFileSync('git', ['checkout', context.defaultBranch], {
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
stdio: 'pipe',
|
|
131
|
+
});
|
|
128
132
|
}
|
|
129
133
|
let systemPrompt;
|
|
130
134
|
let userPrompt;
|
|
@@ -137,11 +141,12 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
137
141
|
lastSyncedCommit: context.lastSyncedCommit,
|
|
138
142
|
diffStat: context.diffStat,
|
|
139
143
|
changedFiles: context.changedFiles,
|
|
144
|
+
defaultBranch: context.defaultBranch,
|
|
140
145
|
});
|
|
141
146
|
}
|
|
142
147
|
else {
|
|
143
148
|
systemPrompt = await createPRExecutionSystemPrompt(issueId, context.devBranchName);
|
|
144
|
-
userPrompt = createPRExecutionPrompt(issueId, context.devBranchName, activePullRequests);
|
|
149
|
+
userPrompt = createPRExecutionPrompt(issueId, context.devBranchName, activePullRequests, context.defaultBranch);
|
|
145
150
|
}
|
|
146
151
|
// Execute agent query
|
|
147
152
|
const agentResult = await executeAgentQuery(userPrompt, systemPrompt, config, verbose);
|
|
@@ -151,12 +156,15 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
151
156
|
if (verbose) {
|
|
152
157
|
logInfo('\nš Pushing branches to remote...');
|
|
153
158
|
}
|
|
154
|
-
// Ensure we're on
|
|
159
|
+
// Ensure we're back on the default branch before pushing
|
|
155
160
|
try {
|
|
156
|
-
|
|
161
|
+
execFileSync('git', ['checkout', context.defaultBranch], {
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
stdio: 'pipe',
|
|
164
|
+
});
|
|
157
165
|
}
|
|
158
166
|
catch {
|
|
159
|
-
// Ignore if already on
|
|
167
|
+
// Ignore if already on the default branch
|
|
160
168
|
}
|
|
161
169
|
if (!context.githubConfig.owner || !context.githubConfig.repo) {
|
|
162
170
|
throw new Error('GitHub owner and repo must be configured for PR execution');
|
|
@@ -191,7 +199,7 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
191
199
|
}
|
|
192
200
|
// Verify the branch exists locally
|
|
193
201
|
try {
|
|
194
|
-
|
|
202
|
+
execFileSync('git', ['rev-parse', '--verify', pr.branch_name], {
|
|
195
203
|
encoding: 'utf-8',
|
|
196
204
|
stdio: 'pipe',
|
|
197
205
|
});
|
|
@@ -209,10 +217,10 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
209
217
|
else {
|
|
210
218
|
executionSummary.branchesCreated++;
|
|
211
219
|
}
|
|
212
|
-
// Resolve base branch for stacked PRs
|
|
220
|
+
// Resolve base branch for stacked PRs (default to repo's default branch)
|
|
213
221
|
const baseBranch = pr.base_pr_id
|
|
214
|
-
? prIdToBranch.get(pr.base_pr_id) ||
|
|
215
|
-
:
|
|
222
|
+
? prIdToBranch.get(pr.base_pr_id) || context.defaultBranch
|
|
223
|
+
: context.defaultBranch;
|
|
216
224
|
// Push branch and build compare URL (using stacked base)
|
|
217
225
|
const result = pushBranchAndBuildUrl(executionConfig, pr.branch_name, baseBranch);
|
|
218
226
|
if (result.success) {
|
|
@@ -225,9 +233,9 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
225
233
|
executionSummary.failedBranches.push(pr.branch_name);
|
|
226
234
|
}
|
|
227
235
|
}
|
|
228
|
-
// Return to
|
|
236
|
+
// Return to the repo's default branch
|
|
229
237
|
try {
|
|
230
|
-
returnToMainBranch(
|
|
238
|
+
returnToMainBranch(context.defaultBranch, verbose);
|
|
231
239
|
}
|
|
232
240
|
catch {
|
|
233
241
|
// Best effort
|
|
@@ -278,9 +286,10 @@ export const executeIssuePRs = async (options, config) => {
|
|
|
278
286
|
catch (error) {
|
|
279
287
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
280
288
|
logError(`PR execution failed: ${errorMessage}`);
|
|
281
|
-
// Try to return to
|
|
289
|
+
// Try to return to the repo's default branch (best effort ā context may
|
|
290
|
+
// have failed before defaultBranch was resolved, so look it up locally)
|
|
282
291
|
try {
|
|
283
|
-
returnToMainBranch(
|
|
292
|
+
returnToMainBranch(resolveDefaultBranch(), false);
|
|
284
293
|
}
|
|
285
294
|
catch {
|
|
286
295
|
// Best effort
|
|
@@ -11,7 +11,7 @@ export declare function createIncrementalSyncSystemPrompt(issueId: string, devBr
|
|
|
11
11
|
/**
|
|
12
12
|
* Create the user prompt for first-time branch creation
|
|
13
13
|
*/
|
|
14
|
-
export declare function createPRExecutionPrompt(issueId: string, devBranchName: string, pullRequests: PullRequest[]): string;
|
|
14
|
+
export declare function createPRExecutionPrompt(issueId: string, devBranchName: string, pullRequests: PullRequest[], defaultBranch: string): string;
|
|
15
15
|
/**
|
|
16
16
|
* Create the user prompt for incremental sync
|
|
17
17
|
*/
|
|
@@ -22,5 +22,6 @@ export interface IncrementalSyncPromptOptions {
|
|
|
22
22
|
lastSyncedCommit: string;
|
|
23
23
|
diffStat: string;
|
|
24
24
|
changedFiles: ChangedFileInfo[];
|
|
25
|
+
defaultBranch: string;
|
|
25
26
|
}
|
|
26
27
|
export declare function createIncrementalSyncPrompt(opts: IncrementalSyncPromptOptions): string;
|
|
@@ -35,25 +35,27 @@ export async function createIncrementalSyncSystemPrompt(issueId, devBranchName,
|
|
|
35
35
|
return `${prompt}\n\n${OUTPUT_CONTRACTS['incremental-sync']}`;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* Resolve the base branch for a PR based on its dependency chain
|
|
38
|
+
* Resolve the base branch for a PR based on its dependency chain.
|
|
39
|
+
* Falls back to the repo's default branch (main, master, ā¦) when the PR has
|
|
40
|
+
* no upstream dependency.
|
|
39
41
|
*/
|
|
40
|
-
function resolveBaseBranch(pr, allPRs) {
|
|
42
|
+
function resolveBaseBranch(pr, allPRs, defaultBranch) {
|
|
41
43
|
if (!pr.base_pr_id) {
|
|
42
|
-
return
|
|
44
|
+
return defaultBranch;
|
|
43
45
|
}
|
|
44
46
|
const basePR = allPRs.find((p) => p.id === pr.base_pr_id);
|
|
45
|
-
return basePR?.branch_name ||
|
|
47
|
+
return basePR?.branch_name || defaultBranch;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* Create the user prompt for first-time branch creation
|
|
49
51
|
*/
|
|
50
|
-
export function createPRExecutionPrompt(issueId, devBranchName, pullRequests) {
|
|
52
|
+
export function createPRExecutionPrompt(issueId, devBranchName, pullRequests, defaultBranch) {
|
|
51
53
|
const prList = pullRequests
|
|
52
54
|
.map((pr) => {
|
|
53
55
|
const files = pr.files
|
|
54
56
|
? pr.files.map((f) => ` - ${f.path} (${f.change_type})`).join('\n')
|
|
55
57
|
: ' (no files specified)';
|
|
56
|
-
const baseBranch = resolveBaseBranch(pr, pullRequests);
|
|
58
|
+
const baseBranch = resolveBaseBranch(pr, pullRequests, defaultBranch);
|
|
57
59
|
return `### PR ${pr.sequence}: ${pr.name}
|
|
58
60
|
- Branch: \`${pr.branch_name}\`
|
|
59
61
|
- Base: \`${baseBranch}\`
|
|
@@ -71,22 +73,22 @@ ${prList}
|
|
|
71
73
|
## Instructions
|
|
72
74
|
|
|
73
75
|
For each PR above (in sequence order):
|
|
74
|
-
1. Switch to the **base branch** specified in "Base" above (either \`
|
|
76
|
+
1. Switch to the **base branch** specified in "Base" above (either \`${defaultBranch}\` or a previous PR's branch)
|
|
75
77
|
2. Create the branch: \`git checkout -b <branch_name>\`
|
|
76
78
|
3. Apply only the listed files from \`${devBranchName}\`
|
|
77
79
|
4. Commit with a descriptive message
|
|
78
80
|
5. Verify with \`git diff --stat <base>...<branch_name>\`
|
|
79
81
|
|
|
80
|
-
After all branches are created, switch back to \`
|
|
82
|
+
After all branches are created, switch back to \`${defaultBranch}\` and provide the execution result JSON.`;
|
|
81
83
|
}
|
|
82
84
|
export function createIncrementalSyncPrompt(opts) {
|
|
83
|
-
const { issueId: _issueId, devBranchName, pullRequests, lastSyncedCommit, diffStat, changedFiles, } = opts;
|
|
85
|
+
const { issueId: _issueId, devBranchName, pullRequests, lastSyncedCommit, diffStat, changedFiles, defaultBranch, } = opts;
|
|
84
86
|
const prList = pullRequests
|
|
85
87
|
.map((pr) => {
|
|
86
88
|
const files = pr.files
|
|
87
89
|
? pr.files.map((f) => ` - ${f.path} (${f.change_type})`).join('\n')
|
|
88
90
|
: ' (no files specified)';
|
|
89
|
-
const baseBranch = resolveBaseBranch(pr, pullRequests);
|
|
91
|
+
const baseBranch = resolveBaseBranch(pr, pullRequests, defaultBranch);
|
|
90
92
|
return `### PR ${pr.sequence}: ${pr.name}
|
|
91
93
|
- Branch: \`${pr.branch_name}\`
|
|
92
94
|
- Base: \`${baseBranch}\`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { getGitHubConfig } from '../../api/github.js';
|
|
3
3
|
import { getIssue } from '../../api/issues/index.js';
|
|
4
4
|
import { getProduct } from '../../api/products.js';
|
|
@@ -6,7 +6,7 @@ import { getBranches } from '../../services/branches.js';
|
|
|
6
6
|
import { getPullRequests, } from '../../services/pull-requests.js';
|
|
7
7
|
import { branchExists, remoteBranchExists, } from '../../utils/git-branch-manager.js';
|
|
8
8
|
import { buildCredentialArgs } from '../../utils/git-push.js';
|
|
9
|
-
import { getRepoForkInfo, } from '../../utils/github-repo-info.js';
|
|
9
|
+
import { getRepoForkInfo, resolveDefaultBranch, } from '../../utils/github-repo-info.js';
|
|
10
10
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
11
11
|
/**
|
|
12
12
|
* Get the dev branch name for an issue
|
|
@@ -18,14 +18,16 @@ function getDevBranchName(issueId) {
|
|
|
18
18
|
* Get the HEAD SHA of a branch
|
|
19
19
|
*/
|
|
20
20
|
function getBranchHeadSha(branchName) {
|
|
21
|
-
return
|
|
21
|
+
return execFileSync('git', ['rev-parse', branchName], {
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
}).trim();
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Get diff stat between two refs
|
|
25
27
|
*/
|
|
26
28
|
function getDiffStat(baseRef, headRef) {
|
|
27
29
|
try {
|
|
28
|
-
return
|
|
30
|
+
return execFileSync('git', ['diff', '--stat', `${baseRef}...${headRef}`], {
|
|
29
31
|
encoding: 'utf-8',
|
|
30
32
|
}).trim();
|
|
31
33
|
}
|
|
@@ -38,9 +40,7 @@ function getDiffStat(baseRef, headRef) {
|
|
|
38
40
|
*/
|
|
39
41
|
function getChangedFiles(baseRef, headRef) {
|
|
40
42
|
try {
|
|
41
|
-
const output =
|
|
42
|
-
encoding: 'utf-8',
|
|
43
|
-
}).trim();
|
|
43
|
+
const output = execFileSync('git', ['diff', '--name-only', `${baseRef}...${headRef}`], { encoding: 'utf-8' }).trim();
|
|
44
44
|
if (!output) {
|
|
45
45
|
return [];
|
|
46
46
|
}
|
|
@@ -53,18 +53,18 @@ function getChangedFiles(baseRef, headRef) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Determine the diff base ref for incremental re-runs
|
|
55
55
|
* If existing PRs have last_synced_commit, use the earliest one
|
|
56
|
-
* Otherwise use origin
|
|
56
|
+
* Otherwise use origin/<defaultBranch> (remote-tracking ref, always up-to-date after fetch)
|
|
57
57
|
*/
|
|
58
|
-
function determineDiffBaseRef(existingPRs, replaceExisting) {
|
|
58
|
+
function determineDiffBaseRef(existingPRs, defaultBranchRef, replaceExisting) {
|
|
59
59
|
if (replaceExisting || existingPRs.length === 0) {
|
|
60
|
-
return
|
|
60
|
+
return defaultBranchRef;
|
|
61
61
|
}
|
|
62
62
|
// Find the minimum last_synced_commit (earliest sync point)
|
|
63
63
|
const syncedCommits = existingPRs
|
|
64
64
|
.map((pr) => pr.last_synced_commit)
|
|
65
65
|
.filter((c) => c !== null);
|
|
66
66
|
if (syncedCommits.length === 0) {
|
|
67
|
-
return
|
|
67
|
+
return defaultBranchRef;
|
|
68
68
|
}
|
|
69
69
|
// All PRs should have been synced to the same commit
|
|
70
70
|
// Use the first one (they should all be equal after a successful sync)
|
|
@@ -131,12 +131,17 @@ export async function fetchPRSplittingContext(issueId, verbose, replaceExisting)
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
// Resolve the repo's default branch (handles non-main defaults and forks).
|
|
135
|
+
// Prefer the GitHub API value (already fetched in forkInfo above); fall back
|
|
136
|
+
// to the local symbolic-ref, then to literal 'main'.
|
|
137
|
+
const defaultBranch = resolveDefaultBranch(forkInfo.defaultBranch);
|
|
138
|
+
const defaultBranchRef = `origin/${defaultBranch}`;
|
|
134
139
|
// Determine diff range
|
|
135
140
|
const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
|
|
136
|
-
const baseRef = determineDiffBaseRef(existingPullRequests, replaceExisting);
|
|
141
|
+
const baseRef = determineDiffBaseRef(existingPullRequests, defaultBranchRef, replaceExisting);
|
|
137
142
|
const devBranchHeadSha = getBranchHeadSha(devRef);
|
|
138
143
|
// Check if there are new changes since last sync
|
|
139
|
-
if (baseRef !==
|
|
144
|
+
if (baseRef !== defaultBranchRef && baseRef === devBranchHeadSha) {
|
|
140
145
|
if (verbose) {
|
|
141
146
|
logInfo(`No new changes since last sync (HEAD: ${devBranchHeadSha})`);
|
|
142
147
|
}
|
|
@@ -102,7 +102,7 @@ export const splitIssueIntoPRs = async (options, config) => {
|
|
|
102
102
|
}
|
|
103
103
|
const contextInfo = formatContextForPrompt(context.issue, context.product, context.existing_branches);
|
|
104
104
|
const existingPRsInfo = formatExistingPRsForPrompt(context.existing_pull_requests);
|
|
105
|
-
const systemPrompt = await createPRSplittingSystemPrompt(config, issueId);
|
|
105
|
+
const systemPrompt = await createPRSplittingSystemPrompt(config, issueId, undefined, verbose);
|
|
106
106
|
let userPrompt;
|
|
107
107
|
if (isIncrementalUpdate && feedbacksContext) {
|
|
108
108
|
if (verbose) {
|
|
@@ -2,7 +2,7 @@ import { type EdsgerConfig } from '../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Create the system prompt for PR splitting: Diff analysis and PR planning
|
|
4
4
|
*/
|
|
5
|
-
export declare function createPRSplittingSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string): Promise<string>;
|
|
5
|
+
export declare function createPRSplittingSystemPrompt(config: EdsgerConfig, issueId: string, projectDir?: string, verbose?: boolean): Promise<string>;
|
|
6
6
|
/**
|
|
7
7
|
* Create the user prompt with diff context for PR splitting
|
|
8
8
|
*/
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
1
2
|
import { resolveSkill, substituteVariables, } from '../../services/skill-resolver.js';
|
|
2
3
|
import { OUTPUT_CONTRACTS } from '../output-contracts.js';
|
|
3
4
|
/**
|
|
4
5
|
* Create the system prompt for PR splitting: Diff analysis and PR planning
|
|
5
6
|
*/
|
|
6
|
-
export async function createPRSplittingSystemPrompt(config, issueId, projectDir) {
|
|
7
|
+
export async function createPRSplittingSystemPrompt(config, issueId, projectDir, verbose) {
|
|
7
8
|
const skill = await resolveSkill('phase/pr-splitting', {
|
|
8
9
|
projectDir,
|
|
9
10
|
});
|
|
@@ -12,7 +13,7 @@ export async function createPRSplittingSystemPrompt(config, issueId, projectDir)
|
|
|
12
13
|
}
|
|
13
14
|
let { prompt } = skill;
|
|
14
15
|
prompt = substituteVariables(prompt, {
|
|
15
|
-
BASE_BRANCH:
|
|
16
|
+
BASE_BRANCH: await getDefaultBranchForIssue(issueId, verbose),
|
|
16
17
|
ISSUE_ID: issueId,
|
|
17
18
|
});
|
|
18
19
|
return `${prompt}\n\n${OUTPUT_CONTRACTS['pr-splitting']}`;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
6
|
import { execFileSync, execSync } from 'child_process';
|
|
7
7
|
import { buildCredentialArgs, gitPush } from '../../utils/git-push.js';
|
|
8
|
+
import { resolveDefaultBranch } from '../../utils/github-repo-info.js';
|
|
8
9
|
import { logDebug } from '../../utils/logger.js';
|
|
9
10
|
// GitHub PR title best practice: keep under 72 characters
|
|
10
11
|
const MAX_PR_TITLE_LENGTH = 72;
|
|
@@ -108,18 +109,20 @@ const switchToBranch = (branch, verbose, githubToken) => {
|
|
|
108
109
|
}
|
|
109
110
|
};
|
|
110
111
|
/**
|
|
111
|
-
* Switch to main
|
|
112
|
+
* Switch to the repo's default branch (e.g. main, master). Caller passes the
|
|
113
|
+
* resolved branch name; falls back to local origin/HEAD if undefined.
|
|
112
114
|
*/
|
|
113
|
-
const switchToMainBranch = (mainBranch
|
|
115
|
+
const switchToMainBranch = (mainBranch, verbose) => {
|
|
116
|
+
const target = mainBranch ?? resolveDefaultBranch();
|
|
114
117
|
try {
|
|
115
|
-
logDebug(`Switching to ${
|
|
118
|
+
logDebug(`Switching to ${target} branch...`, verbose);
|
|
116
119
|
// Discard any uncommitted changes before switching
|
|
117
120
|
discardUncommittedChanges(verbose);
|
|
118
|
-
|
|
119
|
-
logDebug(`Switched to ${
|
|
121
|
+
execFileSync('git', ['checkout', target], { encoding: 'utf-8' });
|
|
122
|
+
logDebug(`Switched to ${target} branch`, verbose);
|
|
120
123
|
}
|
|
121
124
|
catch (error) {
|
|
122
|
-
throw new Error(`Failed to switch to ${
|
|
125
|
+
throw new Error(`Failed to switch to ${target} branch: ${error}`);
|
|
123
126
|
}
|
|
124
127
|
};
|
|
125
128
|
/**
|
|
@@ -189,7 +192,7 @@ const generatePRBody = (issue) => {
|
|
|
189
192
|
* Create a pull request for the issue
|
|
190
193
|
*/
|
|
191
194
|
export async function createPullRequest(config, issue) {
|
|
192
|
-
const { githubToken, owner, repo, baseBranch =
|
|
195
|
+
const { githubToken, owner, repo, baseBranch = resolveDefaultBranch(), verbose, } = config;
|
|
193
196
|
try {
|
|
194
197
|
// Initialize Octokit with personal access token
|
|
195
198
|
const octokit = new Octokit({
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { getGitHubConfig } from '../../api/github.js';
|
|
5
5
|
import { getIssue } from '../../api/issues/index.js';
|
|
6
6
|
import { getCurrentBranch, updateBranch, } from '../../services/branches.js';
|
|
7
|
+
import { getDefaultBranchForIssue } from '../../services/repo-config.js';
|
|
7
8
|
import { logDebug } from '../../utils/logger.js';
|
|
8
9
|
import { createPullRequest } from './creator.js';
|
|
9
10
|
/**
|
|
@@ -45,11 +46,12 @@ export async function handlePullRequestCreation({ issueId, results, testingResul
|
|
|
45
46
|
}
|
|
46
47
|
const { token: githubToken, owner: githubOwner, repo: githubRepo, } = githubConfig;
|
|
47
48
|
logDebug(`Using GitHub config from product developer: ${githubOwner}/${githubRepo}`, verbose);
|
|
49
|
+
const baseBranch = await getDefaultBranchForIssue(issueId, verbose);
|
|
48
50
|
const prResult = await createPullRequest({
|
|
49
51
|
githubToken,
|
|
50
52
|
owner: githubOwner,
|
|
51
53
|
repo: githubRepo,
|
|
52
|
-
baseBranch
|
|
54
|
+
baseBranch,
|
|
53
55
|
verbose,
|
|
54
56
|
}, {
|
|
55
57
|
id: issue.id,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-issue repo configuration helpers.
|
|
3
|
+
*
|
|
4
|
+
* Right now this just exposes the resolved default branch for a given issue,
|
|
5
|
+
* combining the GitHub API answer (most authoritative) with local-git and
|
|
6
|
+
* literal-`main` fallbacks. Phases that previously hardcoded `'main'` should
|
|
7
|
+
* call `getDefaultBranchForIssue(issueId)` instead so forks whose default
|
|
8
|
+
* branch is `master` / `develop` / `trunk` work correctly.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the default branch for the repository attached to this issue.
|
|
12
|
+
* Order of precedence: GitHub API `default_branch` ā local symbolic-ref ā 'main'.
|
|
13
|
+
*
|
|
14
|
+
* Cached per `issueId` for the lifetime of the process. Never throws; on any
|
|
15
|
+
* error it falls back to local resolution.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getDefaultBranchForIssue(issueId: string, verbose?: boolean): Promise<string>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-issue repo configuration helpers.
|
|
3
|
+
*
|
|
4
|
+
* Right now this just exposes the resolved default branch for a given issue,
|
|
5
|
+
* combining the GitHub API answer (most authoritative) with local-git and
|
|
6
|
+
* literal-`main` fallbacks. Phases that previously hardcoded `'main'` should
|
|
7
|
+
* call `getDefaultBranchForIssue(issueId)` instead so forks whose default
|
|
8
|
+
* branch is `master` / `develop` / `trunk` work correctly.
|
|
9
|
+
*/
|
|
10
|
+
import { getGitHubConfig } from '../api/github.js';
|
|
11
|
+
import { getRepoForkInfo, resolveDefaultBranch, } from '../utils/github-repo-info.js';
|
|
12
|
+
// Memoized resolutions keyed by issueId. A worker process handles one issue
|
|
13
|
+
// per invocation, but a phase can call getDefaultBranchForIssue from several
|
|
14
|
+
// places (entry, prompt builders, helper functions) ā caching avoids
|
|
15
|
+
// re-hitting the GitHub API for each one.
|
|
16
|
+
const defaultBranchCache = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the default branch for the repository attached to this issue.
|
|
19
|
+
* Order of precedence: GitHub API `default_branch` ā local symbolic-ref ā 'main'.
|
|
20
|
+
*
|
|
21
|
+
* Cached per `issueId` for the lifetime of the process. Never throws; on any
|
|
22
|
+
* error it falls back to local resolution.
|
|
23
|
+
*/
|
|
24
|
+
export async function getDefaultBranchForIssue(issueId, verbose) {
|
|
25
|
+
const cached = defaultBranchCache.get(issueId);
|
|
26
|
+
if (cached) {
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
const promise = resolveOnce(issueId, verbose).catch((error) => {
|
|
30
|
+
// If the lookup itself blows up unexpectedly, drop the cached failure so
|
|
31
|
+
// a later call can retry; then fall back locally.
|
|
32
|
+
defaultBranchCache.delete(issueId);
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
defaultBranchCache.set(issueId, promise);
|
|
36
|
+
return promise;
|
|
37
|
+
}
|
|
38
|
+
async function resolveOnce(issueId, verbose) {
|
|
39
|
+
try {
|
|
40
|
+
const config = await getGitHubConfig(issueId, verbose);
|
|
41
|
+
if (config.token && config.owner && config.repo) {
|
|
42
|
+
const info = await getRepoForkInfo(config.token, config.owner, config.repo);
|
|
43
|
+
return resolveDefaultBranch(info.defaultBranch);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Fall through to local resolution
|
|
48
|
+
}
|
|
49
|
+
return resolveDefaultBranch();
|
|
50
|
+
}
|
|
@@ -7,10 +7,11 @@ import { Octokit } from '@octokit/rest';
|
|
|
7
7
|
import { execFileSync, execSync } from 'child_process';
|
|
8
8
|
import { abortRebase, branchExists, getCurrentBranch, hasConflicts, hasUncommittedChanges, isRebaseInProgress, pullLatestFromBranch, resetUncommittedChanges, returnToMainBranch, switchToBranch, } from './git-branch-manager.js';
|
|
9
9
|
import { buildCredentialArgs, gitForcePush } from './git-push.js';
|
|
10
|
+
import { resolveDefaultBranch } from './github-repo-info.js';
|
|
10
11
|
import { logError, logInfo } from './logger.js';
|
|
11
12
|
// eslint-disable-next-line complexity -- git sync with fetch, merge/rebase, and conflict handling
|
|
12
13
|
export async function syncFeatBranchWithMain(opts) {
|
|
13
|
-
const { featBranch, githubToken, owner, repo, baseBranch =
|
|
14
|
+
const { featBranch, githubToken, owner, repo, baseBranch = resolveDefaultBranch(), verbose, } = opts;
|
|
14
15
|
try {
|
|
15
16
|
const octokit = new Octokit({ auth: githubToken });
|
|
16
17
|
// Check if feat branch exists
|
|
@@ -108,7 +109,7 @@ export async function syncFeatBranchWithMain(opts) {
|
|
|
108
109
|
*/
|
|
109
110
|
// eslint-disable-next-line complexity -- async git branch management with rebase, conflict resolution, and recovery
|
|
110
111
|
export async function switchToIssueBranchAndRebaseAsync(options) {
|
|
111
|
-
const { issueBranch, baseBranch =
|
|
112
|
+
const { issueBranch, baseBranch = resolveDefaultBranch(), rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, githubToken, } = options;
|
|
112
113
|
// Determine the actual target branch for rebase
|
|
113
114
|
// If rebaseTargetBranch is set (e.g., main), use it; otherwise use baseBranch
|
|
114
115
|
const actualRebaseTarget = rebaseTargetBranch || baseBranch;
|
|
@@ -375,9 +376,12 @@ export async function switchToIssueBranchAndRebaseAsync(options) {
|
|
|
375
376
|
*/
|
|
376
377
|
export async function prepareCustomBranchGitEnvironmentAsync(options) {
|
|
377
378
|
const { verbose } = options;
|
|
378
|
-
|
|
379
|
+
const resolvedDefault = resolveDefaultBranch();
|
|
380
|
+
// Create cleanup function BEFORE any operations that might fail.
|
|
381
|
+
// Always return to the repo's default branch (main, master, ā¦), not the
|
|
382
|
+
// baseBranch ā baseBranch may be an intermediate branch in a chained PR.
|
|
379
383
|
const cleanup = () => {
|
|
380
|
-
returnToMainBranch(
|
|
384
|
+
returnToMainBranch(resolvedDefault, verbose);
|
|
381
385
|
};
|
|
382
386
|
try {
|
|
383
387
|
const result = await switchToIssueBranchAndRebaseAsync(options);
|
|
@@ -409,7 +413,7 @@ export async function prepareCustomBranchGitEnvironmentAsync(options) {
|
|
|
409
413
|
* @param conflictResolverConfig - Configuration for the conflict resolver
|
|
410
414
|
* @returns Cleanup function and conflict resolution info
|
|
411
415
|
*/
|
|
412
|
-
export async function preparePhaseGitEnvironmentAsync(issueId, baseBranch
|
|
416
|
+
export async function preparePhaseGitEnvironmentAsync(issueId, baseBranch, verbose, resolveConflicts, conflictResolverConfig) {
|
|
413
417
|
const issueBranch = `dev/${issueId}`;
|
|
414
418
|
return prepareCustomBranchGitEnvironmentAsync({
|
|
415
419
|
issueBranch,
|
|
@@ -6,6 +6,7 @@ import { execFileSync, execSync } from 'child_process';
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { buildCredentialArgs } from './git-push.js';
|
|
9
|
+
import { resolveDefaultBranch } from './github-repo-info.js';
|
|
9
10
|
import { logError, logInfo } from './logger.js';
|
|
10
11
|
/**
|
|
11
12
|
* Get current Git branch name
|
|
@@ -359,7 +360,9 @@ export function pullLatestFromBranch(branch, verbose, githubToken) {
|
|
|
359
360
|
* @returns Object containing the previous branch name
|
|
360
361
|
*/
|
|
361
362
|
// eslint-disable-next-line complexity -- git branch management with rebase, conflict handling, and fallback strategies
|
|
362
|
-
export function switchToIssueBranchAndRebase(issueBranch, baseBranch
|
|
363
|
+
export function switchToIssueBranchAndRebase(issueBranch, baseBranch, verbose, githubToken) {
|
|
364
|
+
// eslint-disable-next-line no-param-reassign -- assigning default to optional param
|
|
365
|
+
baseBranch ??= resolveDefaultBranch();
|
|
363
366
|
const previousBranch = getCurrentBranch();
|
|
364
367
|
if (verbose) {
|
|
365
368
|
logInfo(`\nš Preparing issue branch: ${issueBranch}`);
|
|
@@ -502,7 +505,9 @@ export function switchToIssueBranchAndRebase(issueBranch, baseBranch = 'main', v
|
|
|
502
505
|
* Return to main branch after phase completion
|
|
503
506
|
* This should be called at the END of each phase
|
|
504
507
|
*/
|
|
505
|
-
export function returnToMainBranch(baseBranch
|
|
508
|
+
export function returnToMainBranch(baseBranch, verbose) {
|
|
509
|
+
// eslint-disable-next-line no-param-reassign -- assigning default to optional param
|
|
510
|
+
baseBranch ??= resolveDefaultBranch();
|
|
506
511
|
const currentBranch = getCurrentBranch();
|
|
507
512
|
if (currentBranch === baseBranch) {
|
|
508
513
|
if (verbose) {
|
|
@@ -551,7 +556,7 @@ export function returnToMainBranch(baseBranch = 'main', verbose) {
|
|
|
551
556
|
* @param verbose - Whether to log verbose output
|
|
552
557
|
* @returns Cleanup function that will return to main branch
|
|
553
558
|
*/
|
|
554
|
-
export function preparePhaseGitEnvironment(issueId, baseBranch
|
|
559
|
+
export function preparePhaseGitEnvironment(issueId, baseBranch, verbose) {
|
|
555
560
|
const issueBranch = `dev/${issueId}`;
|
|
556
561
|
return prepareCustomBranchGitEnvironment(issueBranch, baseBranch, verbose);
|
|
557
562
|
}
|
|
@@ -572,15 +577,20 @@ export function preparePhaseGitEnvironment(issueId, baseBranch = 'main', verbose
|
|
|
572
577
|
* @param verbose - Whether to log verbose output
|
|
573
578
|
* @returns Cleanup function that will return to main branch
|
|
574
579
|
*/
|
|
575
|
-
export function prepareCustomBranchGitEnvironment(issueBranch, baseBranch
|
|
580
|
+
export function prepareCustomBranchGitEnvironment(issueBranch, baseBranch, verbose) {
|
|
581
|
+
// Resolve once so cleanup and rebase agree on the same target branch.
|
|
582
|
+
const resolvedDefault = resolveDefaultBranch();
|
|
583
|
+
const resolvedBase = baseBranch ?? resolvedDefault;
|
|
576
584
|
// Create cleanup function BEFORE any operations that might fail
|
|
577
|
-
// This ensures cleanup can be called even if switchToIssueBranchAndRebase throws
|
|
585
|
+
// This ensures cleanup can be called even if switchToIssueBranchAndRebase throws.
|
|
586
|
+
// We always return to the repo's default branch (e.g. main, master), not
|
|
587
|
+
// baseBranch ā baseBranch may be an intermediate branch in a chained PR.
|
|
578
588
|
const cleanup = () => {
|
|
579
|
-
returnToMainBranch(
|
|
589
|
+
returnToMainBranch(resolvedDefault, verbose);
|
|
580
590
|
};
|
|
581
591
|
try {
|
|
582
592
|
// Switch to issue branch and rebase with base branch
|
|
583
|
-
switchToIssueBranchAndRebase(issueBranch,
|
|
593
|
+
switchToIssueBranchAndRebase(issueBranch, resolvedBase, verbose);
|
|
584
594
|
}
|
|
585
595
|
catch (error) {
|
|
586
596
|
// If setup fails, try to cleanup before re-throwing
|
|
@@ -7,8 +7,20 @@ export interface RepoForkInfo {
|
|
|
7
7
|
owner: string;
|
|
8
8
|
repo: string;
|
|
9
9
|
};
|
|
10
|
+
defaultBranch?: string;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* Detect if a repository is a fork and get upstream info
|
|
13
|
+
* Detect if a repository is a fork and get upstream + default branch info
|
|
13
14
|
*/
|
|
14
15
|
export declare function getRepoForkInfo(token: string, owner: string, repo: string): Promise<RepoForkInfo>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the repo's default branch with a multi-tier fallback:
|
|
18
|
+
* 1. `apiValue` from GitHub API (most authoritative)
|
|
19
|
+
* 2. local `git symbolic-ref refs/remotes/origin/HEAD`
|
|
20
|
+
* 3. literal `'main'`
|
|
21
|
+
*
|
|
22
|
+
* Use this anywhere we need to diff/merge against the upstream default branch.
|
|
23
|
+
* The hardcoded `origin/main` assumption breaks for repos whose default is
|
|
24
|
+
* `master`, `develop`, etc., or for forks that inherited a non-`main` default.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDefaultBranch(apiValue?: string): string;
|
|
@@ -2,18 +2,44 @@
|
|
|
2
2
|
* GitHub repository information utilities
|
|
3
3
|
*/
|
|
4
4
|
import { Octokit } from '@octokit/rest';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
5
6
|
/**
|
|
6
|
-
* Detect if a repository is a fork and get upstream info
|
|
7
|
+
* Detect if a repository is a fork and get upstream + default branch info
|
|
7
8
|
*/
|
|
8
9
|
export async function getRepoForkInfo(token, owner, repo) {
|
|
9
10
|
const octokit = new Octokit({ auth: token });
|
|
10
11
|
const { data } = await octokit.repos.get({ owner, repo });
|
|
12
|
+
const result = {
|
|
13
|
+
isFork: false,
|
|
14
|
+
defaultBranch: data.default_branch,
|
|
15
|
+
};
|
|
11
16
|
if (data.fork && data.parent) {
|
|
12
17
|
const [upstreamOwner, upstreamRepo] = data.parent.full_name.split('/');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
result.isFork = true;
|
|
19
|
+
result.upstream = { owner: upstreamOwner, repo: upstreamRepo };
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the repo's default branch with a multi-tier fallback:
|
|
25
|
+
* 1. `apiValue` from GitHub API (most authoritative)
|
|
26
|
+
* 2. local `git symbolic-ref refs/remotes/origin/HEAD`
|
|
27
|
+
* 3. literal `'main'`
|
|
28
|
+
*
|
|
29
|
+
* Use this anywhere we need to diff/merge against the upstream default branch.
|
|
30
|
+
* The hardcoded `origin/main` assumption breaks for repos whose default is
|
|
31
|
+
* `master`, `develop`, etc., or for forks that inherited a non-`main` default.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveDefaultBranch(apiValue) {
|
|
34
|
+
if (apiValue) {
|
|
35
|
+
return apiValue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const ref = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
39
|
+
const stripped = ref.replace(/^origin\//, '');
|
|
40
|
+
return stripped || 'main';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return 'main';
|
|
17
44
|
}
|
|
18
|
-
return { isFork: false };
|
|
19
45
|
}
|