Skip to content

Commit 616f8cc

Browse files
committed
Fix PR creation logic to check PR existence instead of branch existence
This fixes a critical bug where PRs would fail to be created on subsequent runs if the branch already existed from a previous failed PR creation attempt. Changes: - Check for open PR existence instead of branch existence in coana-fix - Clean up stale branches that exist without open PRs - Add detailed error handling for PR creation failures - Return typed OpenPrResult instead of undefined on failures
1 parent 62b3aea commit 616f8cc

2 files changed

Lines changed: 105 additions & 22 deletions

File tree

src/commands/fix/coana-fix.mts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
gitCommit,
2626
gitCreateBranch,
2727
gitDeleteBranch,
28+
gitDeleteRemoteBranch,
2829
gitPushBranch,
2930
gitRemoteBranchExists,
3031
gitResetAndClean,
@@ -341,13 +342,33 @@ export async function coanaFix(
341342
const branch = getSocketFixBranchName(ghsaId)
342343

343344
try {
344-
// Check if branch already exists.
345+
// Check if an open PR already exists for this GHSA.
345346
// eslint-disable-next-line no-await-in-loop
346-
if (await gitRemoteBranchExists(branch, cwd)) {
347-
debugFn('notice', `skip: remote branch "${branch}" exists`)
347+
const existingOpenPrs = await getSocketFixPrs(
348+
fixEnv.repoInfo.owner,
349+
fixEnv.repoInfo.repo,
350+
{ ghsaId, states: GQL_PR_STATE_OPEN },
351+
)
352+
353+
if (existingOpenPrs.length > 0) {
354+
const prNum = existingOpenPrs[0]!.number
355+
logger.info(`PR #${prNum} already exists for ${ghsaId}, skipping.`)
356+
debugFn('notice', `skip: open PR #${prNum} exists for ${ghsaId}`)
348357
continue ghsaLoop
349358
}
350359

360+
// If branch exists but no open PR, delete the stale branch.
361+
// This handles cases where PR creation failed but branch was pushed.
362+
// eslint-disable-next-line no-await-in-loop
363+
if (await gitRemoteBranchExists(branch, cwd)) {
364+
logger.warn(
365+
`Stale branch ${branch} found without open PR, cleaning up...`,
366+
)
367+
debugFn('notice', `cleanup: deleting stale branch ${branch}`)
368+
// eslint-disable-next-line no-await-in-loop
369+
await gitDeleteRemoteBranch(branch)
370+
}
371+
351372
debugFn('notice', `pr: creating for ${ghsaId}`)
352373

353374
const details = ghsaDetails.get(ghsaId)
@@ -408,7 +429,7 @@ export async function coanaFix(
408429
)
409430

410431
// eslint-disable-next-line no-await-in-loop
411-
const prResponse = await openSocketFixPr(
432+
const prResult = await openSocketFixPr(
412433
fixEnv.repoInfo.owner,
413434
fixEnv.repoInfo.repo,
414435
branch,
@@ -421,8 +442,8 @@ export async function coanaFix(
421442
},
422443
)
423444

424-
if (prResponse) {
425-
const { data } = prResponse
445+
if (prResult.ok) {
446+
const { data } = prResult.pr
426447
const prRef = `PR #${data.number}`
427448

428449
logger.success(`Opened ${prRef} for ${ghsaId}.`)
@@ -443,6 +464,29 @@ export async function coanaFix(
443464
logger.dedent()
444465
spinner?.dedent()
445466
}
467+
} else {
468+
// Handle PR creation failures.
469+
if (prResult.reason === 'already_exists') {
470+
logger.info(
471+
`PR already exists for ${ghsaId} (this should not happen due to earlier check).`,
472+
)
473+
} else if (prResult.reason === 'validation_error') {
474+
logger.error(
475+
`Failed to create PR for ${ghsaId}:\n${prResult.details}`,
476+
)
477+
} else if (prResult.reason === 'permission_denied') {
478+
logger.error(
479+
`Failed to create PR for ${ghsaId}: Permission denied. Check SOCKET_CLI_GITHUB_TOKEN permissions.`,
480+
)
481+
} else if (prResult.reason === 'network_error') {
482+
logger.error(
483+
`Failed to create PR for ${ghsaId}: Network error. Please try again.`,
484+
)
485+
} else {
486+
logger.error(
487+
`Failed to create PR for ${ghsaId}: ${prResult.error.message}`,
488+
)
489+
}
446490
}
447491

448492
// Reset back to base branch for next iteration.

src/commands/fix/pull-request.mts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,26 @@ export type OpenSocketFixPrOptions = {
3535
ghsaDetails?: Map<string, GhsaDetails> | undefined
3636
}
3737

38+
export type OpenPrResult =
39+
| { ok: true; pr: OctokitResponse<Pr> }
40+
| { ok: false; reason: 'already_exists'; error: RequestError }
41+
| {
42+
ok: false
43+
reason: 'validation_error'
44+
error: RequestError
45+
details: string
46+
}
47+
| { ok: false; reason: 'permission_denied'; error: RequestError }
48+
| { ok: false; reason: 'network_error'; error: RequestError }
49+
| { ok: false; reason: 'unknown'; error: Error }
50+
3851
export async function openSocketFixPr(
3952
owner: string,
4053
repo: string,
4154
branch: string,
4255
ghsaIds: string[],
4356
options?: OpenSocketFixPrOptions | undefined,
44-
): Promise<OctokitResponse<Pr> | undefined> {
57+
): Promise<OpenPrResult> {
4558
const { baseBranch = 'main', ghsaDetails } = {
4659
__proto__: null,
4760
...options,
@@ -59,25 +72,51 @@ export async function openSocketFixPr(
5972
body: getSocketFixPullRequestBody(ghsaIds, ghsaDetails),
6073
}
6174
debugDir('inspect', { octokitPullsCreateParams })
62-
return await octokit.pulls.create(octokitPullsCreateParams)
75+
const pr = await octokit.pulls.create(octokitPullsCreateParams)
76+
return { ok: true, pr }
6377
} catch (e) {
64-
let message = `Failed to open pull request`
65-
const errors =
66-
e instanceof RequestError
67-
? (e.response?.data as any)?.['errors']
68-
: undefined
69-
if (Array.isArray(errors) && errors.length) {
70-
const details = errors
71-
.map(
72-
d =>
73-
`- ${d.message?.trim() ?? `${d.resource}.${d.field} (${d.code})`}`,
78+
// Handle RequestError from Octokit.
79+
if (e instanceof RequestError) {
80+
const errors = (e.response?.data as any)?.['errors']
81+
const errorMessages = Array.isArray(errors)
82+
? errors.map(
83+
d => d.message?.trim() ?? `${d.resource}.${d.field} (${d.code})`,
84+
)
85+
: []
86+
87+
// Check for "PR already exists" error.
88+
if (
89+
errorMessages.some(msg =>
90+
msg.toLowerCase().includes('pull request already exists'),
7491
)
75-
.join('\n')
76-
message += `:\n${details}`
92+
) {
93+
debugFn('error', 'Failed to open pull request: already exists')
94+
return { ok: false, reason: 'already_exists', error: e }
95+
}
96+
97+
// Check for validation errors (e.g., no commits between branches).
98+
if (errors && errors.length > 0) {
99+
const details = errorMessages.map(d => `- ${d}`).join('\n')
100+
debugFn('error', `Failed to open pull request:\n${details}`)
101+
return { ok: false, reason: 'validation_error', error: e, details }
102+
}
103+
104+
// Check HTTP status codes.
105+
if (e.status === 403 || e.status === 401) {
106+
debugFn('error', 'Failed to open pull request: permission denied')
107+
return { ok: false, reason: 'permission_denied', error: e }
108+
}
109+
110+
if (e.status && e.status >= 500) {
111+
debugFn('error', 'Failed to open pull request: network error')
112+
return { ok: false, reason: 'network_error', error: e }
113+
}
77114
}
78-
debugFn('error', message)
115+
116+
// Unknown error.
117+
debugFn('error', `Failed to open pull request: ${e}`)
118+
return { ok: false, reason: 'unknown', error: e as Error }
79119
}
80-
return undefined
81120
}
82121

83122
export type GQL_MERGE_STATE_STATUS =

0 commit comments

Comments
 (0)