@@ -210,6 +210,10 @@ export async function runCLIWithRetry(args: string[], options: RetryOptions = {}
210210 let lastResult : ExecaReturnValue | null = null ;
211211
212212 for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
213+ if ( verbose ) {
214+ console . log ( ` → CLI: b2c ${ args . join ( ' ' ) } (attempt ${ attempt + 1 } /${ maxRetries + 1 } )` ) ;
215+ }
216+
213217 // eslint-disable-next-line no-await-in-loop
214218 const result = await runCLI ( args , { timeout, env, cwd} ) ;
215219
@@ -223,14 +227,22 @@ export async function runCLIWithRetry(args: string[], options: RetryOptions = {}
223227
224228 lastResult = result ;
225229
226- // Check if error is retryable
230+ // Check if error is retryable. Treat execa timeouts as retryable since
231+ // they kill the child process and produce empty stderr (no signal in text).
227232 const errorMsg = toString ( result . stderr ) || toString ( result . stdout ) ;
228- const isRetryable = retryableErrors . some ( ( pattern ) => pattern . test ( errorMsg ) ) ;
233+ const isTimeout = Boolean ( result . timedOut ) || result . signal === 'SIGTERM' ;
234+ const isRetryable = isTimeout || retryableErrors . some ( ( pattern ) => pattern . test ( errorMsg ) ) ;
229235
230- // If not retryable or last attempt, return result
236+ // If not retryable or last attempt, return result. Always surface what
237+ // happened on CI so failures aren't silent (empty stderr on timeout).
231238 if ( ! isRetryable || attempt === maxRetries ) {
232- if ( verbose && ! isRetryable ) {
233- console . log ( ` ✗ This looks non-transient; not retrying` ) ;
239+ if ( verbose ) {
240+ if ( isRetryable ) {
241+ console . log ( ` ✗ Exhausted ${ maxRetries + 1 } attempts; giving up` ) ;
242+ } else {
243+ console . log ( ` ✗ Non-retryable failure; giving up` ) ;
244+ }
245+ console . log ( ` ${ getErrorDetails ( result ) . split ( '\n' ) . join ( '\n ' ) } ` ) ;
234246 }
235247 return result ;
236248 }
@@ -239,8 +251,9 @@ export async function runCLIWithRetry(args: string[], options: RetryOptions = {}
239251 const delay = Math . min ( initialDelay * 2 ** attempt , maxDelay ) ;
240252
241253 if ( verbose ) {
242- console . log ( ` ⚠ Temporary issue (attempt ${ attempt + 1 } /${ maxRetries + 1 } ); trying again in ${ delay } ms...` ) ;
243- console . log ( ` Details: ${ errorMsg . slice ( 0 , 200 ) } ${ errorMsg . length > 200 ? '...' : '' } ` ) ;
254+ const reason = isTimeout ? 'execa timeout' : 'transient error' ;
255+ console . log ( ` ⚠ ${ reason } (attempt ${ attempt + 1 } /${ maxRetries + 1 } ); retrying in ${ delay } ms...` ) ;
256+ console . log ( ` ${ getErrorDetails ( result ) . split ( '\n' ) . join ( '\n ' ) } ` ) ;
244257 }
245258
246259 // eslint-disable-next-line no-await-in-loop
0 commit comments