@@ -45,6 +45,90 @@ async function debugFetch(url: string, options: RequestInit = {}): Promise<Respo
4545 return response ;
4646}
4747
48+ /**
49+ * Retry wrapper for fetch operations with exponential backoff
50+ * Shows in-place retry counter updates without filling the terminal
51+ *
52+ * @param fetchFn - Function that performs the fetch operation
53+ * @param options - Configuration options
54+ * @param options.maxRetries - Maximum number of retry attempts after initial failure (default: 15). Total attempts = maxRetries + 1
55+ * @param options.initialDelay - Initial delay in ms before first retry (default: 1000)
56+ * @param options.maxDelay - Maximum delay in ms between retries (default: 10000)
57+ * @param options.operation - Name of the operation for display purposes (default: 'Fetch')
58+ * @param options.spinner - Optional ora spinner instance for UI updates
59+ * @param options.retryOnHttpErrors - Whether to retry on HTTP 5xx server errors. Default: false
60+ *
61+ * @throws Error if all retry attempts are exhausted
62+ *
63+ * @remarks
64+ * - Only retries on network errors by default (connection failures, timeouts, etc.)
65+ * - Set retryOnHttpErrors=true to also retry HTTP 5xx server errors (not 4xx client errors)
66+ * - Uses exponential backoff: 1s, 2s, 4s, 8s, 10s (max), 10s, ...
67+ * - With maxRetries=15: 1 initial attempt + 15 retries = 16 total attempts
68+ */
69+ async function fetchWithRetry (
70+ fetchFn : ( ) => Promise < Response > ,
71+ options : {
72+ maxRetries ?: number ;
73+ initialDelay ?: number ;
74+ maxDelay ?: number ;
75+ operation ?: string ;
76+ spinner ?: ReturnType < typeof ora > ;
77+ retryOnHttpErrors ?: boolean ;
78+ } = { }
79+ ) : Promise < Response > {
80+ const maxRetries = options . maxRetries ?? 15 ;
81+ const initialDelay = options . initialDelay ?? 1000 ;
82+ const maxDelay = options . maxDelay ?? 10000 ;
83+ const operation = options . operation ?? 'Fetch' ;
84+ const spinner = options . spinner ;
85+ const retryOnHttpErrors = options . retryOnHttpErrors ?? false ;
86+
87+ let lastError : Error | null = null ;
88+
89+ // Initial attempt (0) + retries (1 to maxRetries)
90+ for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
91+ try {
92+ const response = await fetchFn ( ) ;
93+
94+ // Check if we should retry on HTTP errors
95+ if ( retryOnHttpErrors && ! response . ok && response . status >= 500 ) {
96+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` ) ;
97+ }
98+
99+ return response ;
100+ } catch ( error ) {
101+ lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
102+
103+ if ( attempt < maxRetries ) {
104+ // Calculate delay with exponential backoff
105+ const delay = Math . min ( initialDelay * Math . pow ( 2 , attempt ) , maxDelay ) ;
106+
107+ // Show retry message - either via spinner or stdout
108+ const message = `${ operation } failed. Retrying... (${ attempt + 1 } /${ maxRetries } )` ;
109+ if ( spinner ) {
110+ spinner . text = message ;
111+ } else {
112+ process . stdout . write ( '\r' + chalk . yellow ( message ) ) ;
113+ }
114+
115+ debug ( `Retry ${ attempt + 1 } /${ maxRetries } : ${ lastError . message } , waiting ${ delay } ms` ) ;
116+
117+ await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
118+ }
119+ }
120+ }
121+
122+ // Clear the retry message if not using spinner
123+ if ( ! spinner ) {
124+ process . stdout . write ( '\x1b[2K\r' ) ; // ANSI escape to clear entire line
125+ }
126+
127+ throw new Error (
128+ `${ operation } failed after ${ maxRetries + 1 } attempts (1 initial + ${ maxRetries } retries): ${ lastError ?. message || 'Unknown error' } `
129+ ) ;
130+ }
131+
48132// API endpoints (relative to base URL)
49133const API_ENDPOINTS = {
50134 // Projects
@@ -171,9 +255,13 @@ async function waitForBuild(
171255 let attempts = 0 ;
172256
173257 while ( attempts < maxAttempts ) {
174- const response = await debugFetch ( `${ apiUrl } ${ API_ENDPOINTS . getBuild } /${ buildId } ` , {
175- headers : { Authorization : `Bearer ${ apiKey } ` } ,
176- } ) ;
258+ const response = await fetchWithRetry (
259+ ( ) =>
260+ debugFetch ( `${ apiUrl } ${ API_ENDPOINTS . getBuild } /${ buildId } ` , {
261+ headers : { Authorization : `Bearer ${ apiKey } ` } ,
262+ } ) ,
263+ { operation : 'Build status check' , spinner, maxRetries : 3 }
264+ ) ;
177265
178266 if ( ! response . ok ) {
179267 throw new Error ( `Failed to get build status: ${ response . statusText } ` ) ;
@@ -215,9 +303,13 @@ async function waitForDeployment(
215303 let attempts = 0 ;
216304
217305 while ( attempts < maxAttempts ) {
218- const response = await debugFetch ( `${ apiUrl } ${ API_ENDPOINTS . getDeployment } /${ deploymentId } ` , {
219- headers : { Authorization : `Bearer ${ apiKey } ` } ,
220- } ) ;
306+ const response = await fetchWithRetry (
307+ ( ) =>
308+ debugFetch ( `${ apiUrl } ${ API_ENDPOINTS . getDeployment } /${ deploymentId } ` , {
309+ headers : { Authorization : `Bearer ${ apiKey } ` } ,
310+ } ) ,
311+ { operation : 'Deployment status check' , spinner, maxRetries : 3 }
312+ ) ;
221313
222314 if ( ! response . ok ) {
223315 throw new Error ( `Failed to get deployment status: ${ response . statusText } ` ) ;
0 commit comments