@@ -6,6 +6,7 @@ import { EventType } from '../../rhtap/core/integration/ci';
66import { Git , PullRequest } from '../../rhtap/core/integration/git' ;
77import { TPA } from '../../rhtap/core/integration/tpa' ;
88import { SBOMResult } from '../../api/tpa/tpaClient' ;
9+ import { Component } from '../../rhtap/core/component' ;
910import { sleep } from '../util' ;
1011import { expectPipelineSuccess } from './assertionHelpers' ;
1112import { expect } from '@playwright/test' ;
@@ -14,6 +15,124 @@ import { LoggerFactory, Logger } from '../../logger/logger';
1415
1516const logger : Logger = LoggerFactory . getLogger ( 'utils.test.common' ) ;
1617
18+ /**
19+ * Determines if an error is non-retryable (should not trigger retry attempts).
20+ * For example "Name already taken" errors are retryable because we regenerate the name.
21+ */
22+ function isNonRetryableError ( errorMessage : string ) : boolean {
23+ const nonRetryablePatterns = [
24+ / u n a u t h o r i z e d / i,
25+ / s t a t u s c o d e 4 0 1 / i,
26+ / a u t h e n t i c a t i o n f a i l e d / i,
27+ / i n v a l i d .* t o k e n / i,
28+ / t e m p l a t e .* n o t f o u n d / i,
29+ / i n v a l i d .* t e m p l a t e / i,
30+ / p e r m i s s i o n d e n i e d / i,
31+ / e n v i r o n m e n t v a r i a b l e .* i s n o t d e f i n e d o r i s e m p t y / i,
32+ / f a i l e d t o r e t r i e v e s e c r e t / i,
33+ ] ;
34+
35+ return nonRetryablePatterns . some ( ( pattern ) => pattern . test ( errorMessage ) ) ;
36+ }
37+
38+ /**
39+ * Creates a component and waits for completion with retry support.
40+ *
41+ * This function handles transient failures and name conflicts by:
42+ * 1. Creating the component with the current testItem name
43+ * 2. Waiting for component creation to complete (prints detailed task logs on failure)
44+ * 3. If either step fails, regenerating a new component name and retrying
45+ * 4. Continuing until success or max retries reached
46+ *
47+ * @param testItem The test item containing configuration details (name will be modified on retry)
48+ * @param options Retry configuration options
49+ * @returns Promise<Component> The successfully created and completed component
50+ * @throws Error when component creation fails after all retry attempts
51+ */
52+ export async function createComponentAndWaitForCompletion (
53+ testItem : TestItem ,
54+ options : { maxRetries ?: number ; retryDelayMs ?: number ; regenerateNameOnRetry ?: boolean } = { }
55+ ) : Promise < Component > {
56+ const DEFAULT_MAX_RETRIES = 3 ;
57+ const DEFAULT_RETRY_DELAY_MS = 10000 ;
58+
59+ const {
60+ maxRetries = DEFAULT_MAX_RETRIES ,
61+ retryDelayMs = DEFAULT_RETRY_DELAY_MS ,
62+ regenerateNameOnRetry = true ,
63+ } = options ;
64+
65+ let lastError : Error | null = null ;
66+ let attemptNumber = 0 ;
67+ const originalName = testItem . getName ( ) ;
68+ const totalAttempts = maxRetries + 1 ;
69+
70+ while ( attemptNumber <= maxRetries ) {
71+ attemptNumber ++ ;
72+ const componentName = testItem . getName ( ) ;
73+ const imageName = componentName ;
74+
75+ logger . info ( `[Attempt ${ attemptNumber } /${ totalAttempts } ] Creating component '${ componentName } '...` ) ;
76+
77+ try {
78+ // Step 1: Create the component
79+ const component = await Component . new ( componentName , testItem , imageName , true ) ;
80+
81+ // Step 2: Wait for completion
82+ await component . waitUntilComponentIsCompleted ( ) ;
83+
84+ // Success!
85+ logger . info ( `✅ Component '${ componentName } ' created successfully on attempt ${ attemptNumber } /${ totalAttempts } ` ) ;
86+
87+ return component ;
88+ } catch ( error ) {
89+ lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
90+ const errorMessage = lastError . message ;
91+
92+ // Extract error summary (exclude task logs from summary display)
93+ const errorSummary = errorMessage . includes ( '---TASK_LOGS_START---' )
94+ ? errorMessage . split ( '---TASK_LOGS_START---' ) [ 0 ] . trim ( )
95+ : errorMessage ;
96+
97+ logger . error ( `❌ COMPONENT CREATION FAILED - Attempt ${ attemptNumber } /${ totalAttempts } | Component: ${ componentName } | Error: ${ errorSummary } ` ) ;
98+
99+ // Check if we've exhausted all retries
100+ if ( attemptNumber > maxRetries ) {
101+ logger . error ( `❌ All ${ totalAttempts } attempts exhausted. Original: '${ originalName } ', Last tried: '${ componentName } '` ) ;
102+ break ;
103+ }
104+
105+ // Check if this is a non-retryable error
106+ if ( isNonRetryableError ( errorMessage ) ) {
107+ logger . error ( `Non-retryable error detected. Stopping retry attempts.` ) ;
108+ break ;
109+ }
110+
111+ logger . info ( `🔄 Will retry. Attempts remaining: ${ totalAttempts - attemptNumber } ` ) ;
112+
113+ // Regenerate name for next attempt if enabled
114+ if ( regenerateNameOnRetry ) {
115+ const newName = testItem . regenerateName ( ) ;
116+ logger . info ( `New component name: '${ newName } '` ) ;
117+
118+ // Save the new name immediately to project-configs.json
119+ await testItem . saveComponentName ( ) ;
120+ }
121+
122+ // Wait before next retry
123+ logger . info ( `Waiting ${ retryDelayMs } ms before next attempt...` ) ;
124+ await sleep ( retryDelayMs ) ;
125+ }
126+ }
127+
128+ // If we reach here, all retries failed
129+ throw new Error (
130+ `Component creation failed after ${ attemptNumber } attempts. ` +
131+ `Original name: '${ originalName } '. ` +
132+ `Last error: ${ lastError ?. message ?. split ( '---TASK_LOGS_START---' ) [ 0 ] . trim ( ) || 'Unknown error' } `
133+ ) ;
134+ }
135+
17136/**
18137 * Promotes an application to a specific environment using a pull request workflow
19138 *
0 commit comments