Skip to content

Commit 627a0ba

Browse files
committed
Fix PR creation logic to check PR existence instead of branch
Previously, the socket fix command would only create a PR once and then never again because it checked for branch existence instead of PR existence. Since branches persist after PR creation, subsequent runs would skip PR creation entirely. Changes: - Check for open PR existence instead of branch existence - Add stale branch cleanup before creating new branches - Move GitHub token check before git operations for early validation - Integrate cleanup functions for proper branch lifecycle management - Add comprehensive error handling for all PR creation failure types - Remove unused gitDeleteRemoteBranch import This ensures PRs can be recreated after being closed/merged and prevents branch accumulation from failed PR attempts.
1 parent 85ebc36 commit 627a0ba

File tree

4 files changed

+97
-27
lines changed

4 files changed

+97
-27
lines changed

src/commands/fix/coana-fix.mts

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { readJsonSync } from '@socketsecurity/registry/lib/fs'
88
import { logger } from '@socketsecurity/registry/lib/logger'
99
import { pluralize } from '@socketsecurity/registry/lib/words'
1010

11+
import {
12+
cleanupErrorBranches,
13+
cleanupFailedPrBranches,
14+
cleanupStaleBranch,
15+
cleanupSuccessfulPrLocalBranch,
16+
} from './branch-cleanup.mts'
1117
import {
1218
checkCiEnvVars,
1319
getCiEnvInstructions,
@@ -341,10 +347,39 @@ export async function coanaFix(
341347
const branch = getSocketFixBranchName(ghsaId)
342348

343349
try {
344-
// Check if branch already exists.
350+
// Check if an open PR already exists for this GHSA.
351+
// eslint-disable-next-line no-await-in-loop
352+
const existingOpenPrs = await getSocketFixPrs(
353+
fixEnv.repoInfo.owner,
354+
fixEnv.repoInfo.repo,
355+
{ ghsaId, states: GQL_PR_STATE_OPEN },
356+
)
357+
358+
if (existingOpenPrs.length > 0) {
359+
const prNum = existingOpenPrs[0]!.number
360+
logger.info(`PR #${prNum} already exists for ${ghsaId}, skipping.`)
361+
debugFn('notice', `skip: open PR #${prNum} exists for ${ghsaId}`)
362+
continue ghsaLoop
363+
}
364+
365+
// If branch exists but no open PR, delete the stale branch.
366+
// This handles cases where PR creation failed but branch was pushed.
345367
// eslint-disable-next-line no-await-in-loop
346368
if (await gitRemoteBranchExists(branch, cwd)) {
347-
debugFn('notice', `skip: remote branch "${branch}" exists`)
369+
// eslint-disable-next-line no-await-in-loop
370+
const shouldContinue = await cleanupStaleBranch(branch, ghsaId, cwd)
371+
if (!shouldContinue) {
372+
continue ghsaLoop
373+
}
374+
}
375+
376+
// Check for GitHub token before doing any git operations.
377+
if (!fixEnv.githubToken) {
378+
logger.error(
379+
'Cannot create pull request: SOCKET_CLI_GITHUB_TOKEN environment variable is not set.\n' +
380+
'Set SOCKET_CLI_GITHUB_TOKEN or GITHUB_TOKEN to enable PR creation.',
381+
)
382+
debugFn('error', `skip: missing GitHub token for ${ghsaId}`)
348383
continue ghsaLoop
349384
}
350385

@@ -386,19 +421,6 @@ export async function coanaFix(
386421
}
387422

388423
// Set up git remote.
389-
if (!fixEnv.githubToken) {
390-
logger.error(
391-
'Cannot create pull request: SOCKET_CLI_GITHUB_TOKEN environment variable is not set.\n' +
392-
'Set SOCKET_CLI_GITHUB_TOKEN or GITHUB_TOKEN to enable PR creation.',
393-
)
394-
// eslint-disable-next-line no-await-in-loop
395-
await gitResetAndClean(fixEnv.baseBranch, cwd)
396-
// eslint-disable-next-line no-await-in-loop
397-
await gitCheckoutBranch(fixEnv.baseBranch, cwd)
398-
// eslint-disable-next-line no-await-in-loop
399-
await gitDeleteBranch(branch, cwd)
400-
continue ghsaLoop
401-
}
402424
// eslint-disable-next-line no-await-in-loop
403425
await setGitRemoteGithubRepoUrl(
404426
fixEnv.repoInfo.owner,
@@ -408,7 +430,7 @@ export async function coanaFix(
408430
)
409431

410432
// eslint-disable-next-line no-await-in-loop
411-
const prResponse = await openSocketFixPr(
433+
const prResult = await openSocketFixPr(
412434
fixEnv.repoInfo.owner,
413435
fixEnv.repoInfo.repo,
414436
branch,
@@ -421,8 +443,8 @@ export async function coanaFix(
421443
},
422444
)
423445

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

428450
logger.success(`Opened ${prRef} for ${ghsaId}.`)
@@ -443,18 +465,59 @@ export async function coanaFix(
443465
logger.dedent()
444466
spinner?.dedent()
445467
}
468+
469+
// Clean up local branch only - keep remote branch for PR merge.
470+
// eslint-disable-next-line no-await-in-loop
471+
await cleanupSuccessfulPrLocalBranch(branch, cwd)
472+
} else {
473+
// Handle PR creation failures.
474+
if (prResult.reason === 'already_exists') {
475+
logger.info(
476+
`PR already exists for ${ghsaId} (this should not happen due to earlier check).`,
477+
)
478+
// Don't delete branch - PR exists and needs it.
479+
} else if (prResult.reason === 'validation_error') {
480+
logger.error(
481+
`Failed to create PR for ${ghsaId}:\n${prResult.details}`,
482+
)
483+
// eslint-disable-next-line no-await-in-loop
484+
await cleanupFailedPrBranches(branch, cwd)
485+
} else if (prResult.reason === 'permission_denied') {
486+
logger.error(
487+
`Failed to create PR for ${ghsaId}: Permission denied. Check SOCKET_CLI_GITHUB_TOKEN permissions.`,
488+
)
489+
// eslint-disable-next-line no-await-in-loop
490+
await cleanupFailedPrBranches(branch, cwd)
491+
} else if (prResult.reason === 'network_error') {
492+
logger.error(
493+
`Failed to create PR for ${ghsaId}: Network error. Please try again.`,
494+
)
495+
// eslint-disable-next-line no-await-in-loop
496+
await cleanupFailedPrBranches(branch, cwd)
497+
} else {
498+
logger.error(
499+
`Failed to create PR for ${ghsaId}: ${prResult.error.message}`,
500+
)
501+
// eslint-disable-next-line no-await-in-loop
502+
await cleanupFailedPrBranches(branch, cwd)
503+
}
446504
}
447505

448506
// Reset back to base branch for next iteration.
449507
// eslint-disable-next-line no-await-in-loop
450-
await gitResetAndClean(branch, cwd)
508+
await gitResetAndClean(fixEnv.baseBranch, cwd)
451509
// eslint-disable-next-line no-await-in-loop
452510
await gitCheckoutBranch(fixEnv.baseBranch, cwd)
453511
} catch (e) {
454512
logger.warn(
455513
`Unexpected condition: Push failed for ${ghsaId}, skipping PR creation.`,
456514
)
457515
debugDir('error', e)
516+
// Clean up branches (push may have succeeded before error).
517+
// eslint-disable-next-line no-await-in-loop
518+
const remoteBranchExists = await gitRemoteBranchExists(branch, cwd)
519+
// eslint-disable-next-line no-await-in-loop
520+
await cleanupErrorBranches(branch, cwd, remoteBranchExists)
458521
// eslint-disable-next-line no-await-in-loop
459522
await gitResetAndClean(fixEnv.baseBranch, cwd)
460523
// eslint-disable-next-line no-await-in-loop

src/commands/scan/cmd-scan-create.test.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ describe('socket scan create', async () => {
5656
Reachability Options (when --reach is used)
5757
--reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB.
5858
--reach-analysis-timeout Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly.
59+
--reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. NPM reachability analysis does not support concurrent execution, so the concurrency level is ignored for NPM.
60+
--reach-disable-analysis-splitting Limits Coana to at most 1 reachability analysis run per workspace.
5961
--reach-disable-analytics Disable reachability analytics sharing with Socket. Also disables caching-based optimizations.
6062
--reach-ecosystems List of ecosystems to conduct reachability analysis on, as either a comma separated value or as multiple flags. Defaults to all ecosystems.
6163
--reach-exclude-paths List of paths to exclude from reachability analysis, as either a comma separated value or as multiple flags.

src/commands/scan/cmd-scan-reach.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ describe('socket scan reach', async () => {
3939
--reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB.
4040
--reach-analysis-timeout Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly.
4141
--reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. NPM reachability analysis does not support concurrent execution, so the concurrency level is ignored for NPM.
42-
--reach-disable-analytics Disable reachability analytics sharing with Socket. Also disables caching-based optimizations.
4342
--reach-disable-analysis-splitting Limits Coana to at most 1 reachability analysis run per workspace.
43+
--reach-disable-analytics Disable reachability analytics sharing with Socket. Also disables caching-based optimizations.
4444
--reach-ecosystems List of ecosystems to conduct reachability analysis on, as either a comma separated value or as multiple flags. Defaults to all ecosystems.
4545
--reach-exclude-paths List of paths to exclude from reachability analysis, as either a comma separated value or as multiple flags.
4646
--reach-skip-cache Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous runs to speed up the analysis.

src/utils/dlx.mts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ export async function spawnCoanaDlx(
216216
const localCoanaPath = process.env['SOCKET_CLI_COANA_LOCAL_PATH']
217217
// Use local Coana CLI if path is provided.
218218
if (localCoanaPath) {
219-
const isBinary = !localCoanaPath.endsWith('.js') && !localCoanaPath.endsWith('.mjs');
219+
const isBinary =
220+
!localCoanaPath.endsWith('.js') && !localCoanaPath.endsWith('.mjs')
220221

221222
const finalEnv = {
222223
...process.env,
@@ -225,12 +226,16 @@ export async function spawnCoanaDlx(
225226
...spawnEnv,
226227
}
227228

228-
const spawnArgs = isBinary ? args : [localCoanaPath, ...args];
229-
const spawnResult = await spawn(isBinary ? localCoanaPath : 'node', spawnArgs, {
230-
cwd: dlxOptions.cwd,
231-
env: finalEnv,
232-
stdio: spawnExtra?.['stdio'] || 'inherit',
233-
})
229+
const spawnArgs = isBinary ? args : [localCoanaPath, ...args]
230+
const spawnResult = await spawn(
231+
isBinary ? localCoanaPath : 'node',
232+
spawnArgs,
233+
{
234+
cwd: dlxOptions.cwd,
235+
env: finalEnv,
236+
stdio: spawnExtra?.['stdio'] || 'inherit',
237+
},
238+
)
234239

235240
return { ok: true, data: spawnResult.stdout }
236241
}

0 commit comments

Comments
 (0)