55 */
66
77import { spawn } from 'node:child_process'
8+ import crypto from 'node:crypto'
89import { existsSync , promises as fs } from 'node:fs'
910import path from 'node:path'
1011import { fileURLToPath } from 'node:url'
@@ -543,14 +544,33 @@ async function checkIfCommitIsPartOfPR(sha, owner, repo) {
543544}
544545
545546/**
546- * Create a hash of error output for tracking duplicate errors.
547+ * Create a semantic hash of error output for tracking duplicate errors.
548+ * Normalizes errors to catch semantically identical issues with different line numbers.
547549 * @param {string } errorOutput - The error output to hash
548- * @returns {string } A simple hash of the error
550+ * @returns {string } A hex hash of the normalized error
549551 */
550552function hashError ( errorOutput ) {
551- // Simple hash: take first 200 chars of error, normalize whitespace
552- const normalized = errorOutput . trim ( ) . slice ( 0 , 200 ) . replace ( / \s + / g, ' ' )
553- return normalized
553+ // Normalize error for semantic comparison
554+ const normalized = errorOutput
555+ . trim ( )
556+ // Remove timestamps
557+ . replace ( / \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } [ ^ Z \s ] * / g, 'TIMESTAMP' )
558+ . replace ( / \d { 2 } : \d { 2 } : \d { 2 } / g, 'TIME' )
559+ // Remove line:column numbers (but keep file paths)
560+ . replace ( / : \d + : \d + / g, ':*:*' )
561+ . replace ( / l i n e \d + / gi, 'line *' )
562+ . replace ( / c o l u m n \d + / gi, 'column *' )
563+ // Remove specific SHAs and commit hashes
564+ . replace ( / \b [ 0 - 9 a - f ] { 7 , 40 } \b / g, 'SHA' )
565+ // Remove absolute file system paths (keep relative paths)
566+ . replace ( / \/ [ ^ \s ] * ?\/ ( [ ^ / \s ] + ) / g, '$1' )
567+ // Normalize whitespace
568+ . replace ( / \s + / g, ' ' )
569+ // Take first 500 chars (increased from 200 for better matching)
570+ . slice ( 0 , 500 )
571+
572+ // Use proper cryptographic hashing for consistent results
573+ return crypto . createHash ( 'sha256' ) . update ( normalized ) . digest ( 'hex' ) . slice ( 0 , 16 )
554574}
555575
556576/**
@@ -2892,6 +2912,120 @@ Commit message:`
28922912 return lines [ 0 ] || 'Fix local checks and update tests'
28932913}
28942914
2915+ /**
2916+ * Calculate adaptive poll delay based on CI state.
2917+ * Polls faster when jobs are running, slower when queued.
2918+ */
2919+ function calculatePollDelay ( status , attempt , hasActiveJobs = false ) {
2920+ // If jobs are actively running, poll more frequently
2921+ if ( hasActiveJobs || status === 'in_progress' ) {
2922+ // Start at 5s, gradually increase to 15s max
2923+ return Math . min ( 5000 + attempt * 2000 , 15000 )
2924+ }
2925+
2926+ // If queued or waiting, use longer intervals (30s)
2927+ if ( status === 'queued' || status === 'waiting' ) {
2928+ return 30000
2929+ }
2930+
2931+ // Default: moderate polling for unknown states (10s)
2932+ return 10000
2933+ }
2934+
2935+ /**
2936+ * Priority levels for different CI job types.
2937+ * Higher priority jobs are fixed first since they often block other jobs.
2938+ */
2939+ const JOB_PRIORITIES = {
2940+ build : 100 ,
2941+ compile : 100 ,
2942+ 'type check' : 90 ,
2943+ typecheck : 90 ,
2944+ typescript : 90 ,
2945+ tsc : 90 ,
2946+ lint : 80 ,
2947+ eslint : 80 ,
2948+ prettier : 80 ,
2949+ 'unit test' : 70 ,
2950+ test : 70 ,
2951+ jest : 70 ,
2952+ vitest : 70 ,
2953+ integration : 60 ,
2954+ e2e : 50 ,
2955+ coverage : 40 ,
2956+ report : 30 ,
2957+ }
2958+
2959+ /**
2960+ * Get priority for a CI job based on its name.
2961+ * @param {string } jobName - The name of the CI job
2962+ * @returns {number } Priority level (higher = more important)
2963+ */
2964+ function getJobPriority ( jobName ) {
2965+ const lowerName = jobName . toLowerCase ( )
2966+
2967+ // Check for exact or partial matches
2968+ for ( const [ pattern , priority ] of Object . entries ( JOB_PRIORITIES ) ) {
2969+ if ( lowerName . includes ( pattern ) ) {
2970+ return priority
2971+ }
2972+ }
2973+
2974+ // Default priority for unknown job types
2975+ return 50
2976+ }
2977+
2978+ /**
2979+ * Validate changes before pushing to catch common mistakes.
2980+ * @param {string } cwd - Working directory
2981+ * @returns {Promise<{valid: boolean, warnings: string[]}> } Validation result
2982+ */
2983+ async function validateBeforePush ( cwd ) {
2984+ const warnings = [ ]
2985+
2986+ // Check for common issues in staged changes
2987+ const diffResult = await runCommandWithOutput ( 'git' , [ 'diff' , '--cached' ] , {
2988+ cwd,
2989+ } )
2990+ const diff = diffResult . stdout
2991+
2992+ // Check 1: No console.log statements
2993+ if ( diff . match ( / ^ \+ .* c o n s o l e \. l o g \( / m) ) {
2994+ warnings . push ( '⚠️ Added console.log() statements detected' )
2995+ }
2996+
2997+ // Check 2: No .only in tests
2998+ if ( diff . match ( / ^ \+ .* \. ( o n l y | s k i p ) \( / m) ) {
2999+ warnings . push ( '⚠️ Test .only() or .skip() detected' )
3000+ }
3001+
3002+ // Check 3: No debugger statements
3003+ if ( diff . match ( / ^ \+ .* d e b u g g e r [ ; \s ] / m) ) {
3004+ warnings . push ( '⚠️ Debugger statement detected' )
3005+ }
3006+
3007+ // Check 4: No TODO/FIXME without issue link
3008+ const todoMatches = diff . match ( / ^ \+ .* \/ \/ \s * ( T O D O | F I X M E ) (? ! \s * \( # \d + \) ) / gim)
3009+ if ( todoMatches && todoMatches . length > 0 ) {
3010+ warnings . push (
3011+ `⚠️ ${ todoMatches . length } TODO/FIXME comment(s) without issue links` ,
3012+ )
3013+ }
3014+
3015+ // Check 5: Package.json is valid JSON
3016+ if ( diff . includes ( 'package.json' ) ) {
3017+ try {
3018+ const pkgPath = path . join ( cwd , 'package.json' )
3019+ const pkgContent = await fs . readFile ( pkgPath , 'utf8' )
3020+ JSON . parse ( pkgContent )
3021+ } catch ( e ) {
3022+ warnings . push ( `⚠️ Invalid package.json: ${ e . message } ` )
3023+ }
3024+ }
3025+
3026+ return { valid : warnings . length === 0 , warnings }
3027+ }
3028+
28953029/**
28963030 * Run all checks, push, and monitor CI until green.
28973031 * NOTE: This operates on the current repo by default. Use --cross-repo for all Socket projects.
@@ -3152,6 +3286,14 @@ Let's work through this together to get CI passing.`
31523286 cwd : rootPath ,
31533287 } )
31543288
3289+ // Validate before pushing
3290+ const validation = await validateBeforePush ( rootPath )
3291+ if ( ! validation . valid ) {
3292+ log . warn ( 'Pre-push validation warnings:' )
3293+ validation . warnings . forEach ( warning => log . substep ( warning ) )
3294+ log . substep ( 'Continuing with push (warnings are non-blocking)...' )
3295+ }
3296+
31553297 // Push
31563298 await runCommand ( 'git' , [ 'push' ] , { cwd : rootPath } )
31573299 log . done ( 'Changes pushed to remote' )
@@ -3244,11 +3386,14 @@ Let's work through this together to get CI passing.`
32443386 let fixedJobs = new Map ( )
32453387 // Track if we've made any commits during this workflow run
32463388 let hasPendingCommits = false
3389+ // Track polling attempts for adaptive delays
3390+ let pollAttempt = 0
32473391
32483392 while ( retryCount < maxRetries ) {
32493393 // Reset tracking for each new CI run
32503394 fixedJobs = new Map ( )
32513395 hasPendingCommits = false
3396+ pollAttempt = 0
32523397 log . progress ( `Checking CI status (attempt ${ retryCount + 1 } /${ maxRetries } )` )
32533398
32543399 // Wait a bit for CI to start
@@ -3355,8 +3500,11 @@ Let's work through this together to get CI passing.`
33553500 }
33563501
33573502 if ( ! matchingRun ) {
3358- log . substep ( 'No matching workflow runs found yet, waiting...' )
3359- await new Promise ( resolve => setTimeout ( resolve , 30_000 ) )
3503+ // Use moderate delay when no run found yet (10s)
3504+ const delay = 10000
3505+ log . substep ( `No matching workflow runs found yet, waiting ${ delay / 1000 } s...` )
3506+ await new Promise ( resolve => setTimeout ( resolve , delay ) )
3507+ pollAttempt ++
33603508 continue
33613509 }
33623510
@@ -3365,10 +3513,12 @@ Let's work through this together to get CI passing.`
33653513
33663514 log . substep ( `Workflow "${ run . name } " status: ${ run . status } ` )
33673515
3368- // If workflow is queued, just wait for it to start
3516+ // If workflow is queued, wait before checking again
33693517 if ( run . status === 'queued' || run . status === 'waiting' ) {
3370- log . substep ( 'Waiting for workflow to start...' )
3371- await new Promise ( resolve => setTimeout ( resolve , 30_000 ) )
3518+ const delay = calculatePollDelay ( run . status , pollAttempt )
3519+ log . substep ( `Waiting for workflow to start (${ delay / 1000 } s)...` )
3520+ await new Promise ( resolve => setTimeout ( resolve , delay ) )
3521+ pollAttempt ++
33723522 continue
33733523 }
33743524
@@ -3624,6 +3774,13 @@ Fix all issues by making necessary file changes. Be direct, don't ask questions.
36243774 )
36253775 log . substep ( `Commit message: ${ commitMessage } ` )
36263776
3777+ // Validate before committing
3778+ const validation = await validateBeforePush ( rootPath )
3779+ if ( ! validation . valid ) {
3780+ log . warn ( 'Pre-commit validation warnings:' )
3781+ validation . warnings . forEach ( warning => log . substep ( warning ) )
3782+ }
3783+
36273784 // Commit with generated message
36283785 const commitArgs = [ 'commit' , '-m' , commitMessage ]
36293786 if ( useNoVerify ) {
@@ -3715,8 +3872,24 @@ Fix all issues by making necessary file changes. Be direct, don't ask questions.
37153872 if ( newFailures . length > 0 ) {
37163873 log . failed ( `Detected ${ newFailures . length } new failed job(s)` )
37173874
3875+ // Sort by priority - fix blocking issues first (build, typecheck, lint, tests)
3876+ // Higher priority first
3877+ const sortedFailures = newFailures . sort ( ( a , b ) => {
3878+ const priorityA = getJobPriority ( a . name )
3879+ const priorityB = getJobPriority ( b . name )
3880+ return priorityB - priorityA
3881+ } )
3882+
3883+ if ( sortedFailures . length > 1 ) {
3884+ log . substep ( 'Processing in priority order (highest first):' )
3885+ sortedFailures . forEach ( job => {
3886+ const priority = getJobPriority ( job . name )
3887+ log . substep ( ` [Priority ${ priority } ] ${ job . name } ` )
3888+ } )
3889+ }
3890+
37183891 // Fix each failed job immediately
3719- for ( const job of newFailures ) {
3892+ for ( const job of sortedFailures ) {
37203893 log . substep ( `❌ ${ job . name } : ${ job . conclusion } ` )
37213894
37223895 // Fetch logs for this specific failed job using job ID
@@ -3916,6 +4089,13 @@ Fix the issue by making necessary file changes. Be direct, don't ask questions.`
39164089 )
39174090 log . substep ( `Commit message: ${ commitMessage } ` )
39184091
4092+ // Validate before committing
4093+ const validation = await validateBeforePush ( rootPath )
4094+ if ( ! validation . valid ) {
4095+ log . warn ( 'Pre-commit validation warnings:' )
4096+ validation . warnings . forEach ( warning => log . substep ( warning ) )
4097+ }
4098+
39194099 // Commit with generated message
39204100 const commitArgs = [ 'commit' , '-m' , commitMessage ]
39214101 if ( useNoVerify ) {
@@ -3957,9 +4137,12 @@ Fix the issue by making necessary file changes. Be direct, don't ask questions.`
39574137 }
39584138 }
39594139
3960- // Wait and check again
3961- log . substep ( 'Waiting 30 seconds before next check...' )
3962- await new Promise ( resolve => setTimeout ( resolve , 30_000 ) )
4140+ // Wait and check again with adaptive polling
4141+ // Jobs are running, so poll more frequently
4142+ const delay = calculatePollDelay ( 'in_progress' , pollAttempt , true )
4143+ log . substep ( `Checking again in ${ delay / 1000 } s...` )
4144+ await new Promise ( resolve => setTimeout ( resolve , delay ) )
4145+ pollAttempt ++
39634146 }
39644147 }
39654148
0 commit comments