11/* eslint-disable spellcheck/spell-checker, camelcase, @typescript-eslint/no-explicit-any */
2+
3+ // Ultimate safety nets - cleanup must NEVER fail the build
4+ process . on ( 'uncaughtException' , ( err ) => {
5+ console . error ( 'Uncaught exception in cleanup (non-fatal):' , err . message ) ;
6+ process . exit ( 0 ) ;
7+ } ) ;
8+
9+ process . on ( 'unhandledRejection' , ( reason ) => {
10+ console . error ( 'Unhandled rejection in cleanup (non-fatal):' , reason ) ;
11+ process . exit ( 0 ) ;
12+ } ) ;
13+
214import { CodeBuildClient , BatchGetBuildsCommand , Build } from '@aws-sdk/client-codebuild' ;
315import { AccountClient , ListRegionsCommand , ListRegionsRequest } from '@aws-sdk/client-account' ;
416import { AmplifyClient , ListAppsCommand , ListBackendEnvironmentsCommand , DeleteAppCommand } from '@aws-sdk/client-amplify' ;
@@ -154,6 +166,14 @@ const isNonJsonResponseError = (e: any): boolean => {
154166 return e instanceof SyntaxError || ( e ?. name === 'SyntaxError' && e ?. message ?. includes ( 'Unexpected token' ) ) ;
155167} ;
156168
169+ const isXmlParsingError = ( e : any ) : boolean => {
170+ const message = e ?. message || '' ;
171+ return message . includes ( 'Entity expansion limit exceeded' ) ||
172+ message . includes ( 'Deserialization error' ) ||
173+ message . includes ( 'entity expansion' ) ||
174+ ( e ?. name === 'Error' && message . includes ( 'fast-xml-parser' ) ) ;
175+ } ;
176+
157177const isNetworkError = ( e : any ) : boolean => {
158178 const code = e ?. code || e ?. name || '' ;
159179 return [ 'ETIMEDOUT' , 'ECONNREFUSED' , 'ECONNRESET' , 'EPIPE' , 'EAI_AGAIN' , 'NetworkingError' , 'TimeoutError' ] . includes ( code )
@@ -196,29 +216,45 @@ const testRoleStalenessFilter = (resource: Role): boolean => {
196216 * Get all S3 buckets in the account, and filter down to the ones we consider stale.
197217 */
198218const getOrphanS3TestBuckets = async ( account : AWSAccountInfo ) : Promise < S3BucketInfo [ ] > => {
199- const s3Client = new S3Client ( getAWSConfig ( account ) ) ;
200- const listBucketResponse = await s3Client . send ( new ListBucketsCommand ( { } ) ) ;
201- const staleBuckets = listBucketResponse . Buckets . filter ( testBucketStalenessFilter ) ;
202- const bucketInfos = await Promise . all (
203- staleBuckets . map ( async ( staleBucket ) : Promise < S3BucketInfo > => {
204- const region = await getBucketRegion ( account , staleBucket . Name ) ;
205- return {
206- name : staleBucket . Name ,
207- region,
208- } ;
209- } ) ,
210- ) ;
211- return bucketInfos ;
219+ try {
220+ const s3Client = new S3Client ( getAWSConfig ( account ) ) ;
221+ const listBucketResponse = await s3Client . send ( new ListBucketsCommand ( { } ) ) ;
222+ const staleBuckets = listBucketResponse . Buckets . filter ( testBucketStalenessFilter ) ;
223+ const bucketInfos = await Promise . all (
224+ staleBuckets . map ( async ( staleBucket ) : Promise < S3BucketInfo > => {
225+ const region = await getBucketRegion ( account , staleBucket . Name ) ;
226+ return {
227+ name : staleBucket . Name ,
228+ region,
229+ } ;
230+ } ) ,
231+ ) ;
232+ return bucketInfos ;
233+ } catch ( e ) {
234+ if ( isXmlParsingError ( e ) ) {
235+ console . log ( `XML parsing error listing orphan S3 buckets (non-fatal, skipping): ${ e . message } ` ) ;
236+ return [ ] ;
237+ }
238+ throw e ;
239+ }
212240} ;
213241
214242/**
215243 * Get all iam roles in the account, and filter down to the ones we consider stale.
216244 */
217245const getOrphanTestIamRoles = async ( account : AWSAccountInfo ) : Promise < IamRoleInfo [ ] > => {
218- const iamClient = new IAMClient ( getAWSConfig ( account ) ) ;
219- const listRoleResponse = await iamClient . send ( new ListRolesCommand ( { MaxItems : 1000 } ) ) ;
220- const staleRoles = listRoleResponse . Roles . filter ( testRoleStalenessFilter ) ;
221- return staleRoles . map ( it => ( { name : it . RoleName } ) ) ;
246+ try {
247+ const iamClient = new IAMClient ( getAWSConfig ( account ) ) ;
248+ const listRoleResponse = await iamClient . send ( new ListRolesCommand ( { MaxItems : 1000 } ) ) ;
249+ const staleRoles = listRoleResponse . Roles . filter ( testRoleStalenessFilter ) ;
250+ return staleRoles . map ( it => ( { name : it . RoleName } ) ) ;
251+ } catch ( e ) {
252+ if ( isXmlParsingError ( e ) ) {
253+ console . log ( `XML parsing error listing IAM roles (non-fatal, skipping): ${ e . message } ` ) ;
254+ return [ ] ;
255+ }
256+ throw e ;
257+ }
222258} ;
223259
224260/**
@@ -240,26 +276,34 @@ const getOrphanTestIamRoles = async (account: AWSAccountInfo): Promise<IamRoleIn
240276 * @returns Promise<string[]> a list of AWS regions enabled by the account
241277 */
242278const getRegionsEnabled = async ( accountInfo : AWSAccountInfo ) : Promise < string [ ] > => {
243- // Specify service region to avoid possible endpoint unavailable error
244- const account = new AccountClient ( getAWSConfig ( accountInfo , 'us-east-1' ) ) ;
279+ try {
280+ // Specify service region to avoid possible endpoint unavailable error
281+ const account = new AccountClient ( getAWSConfig ( accountInfo , 'us-east-1' ) ) ;
245282
246- const enabledRegions : string [ ] = [ ] ;
247- let nextToken : string | undefined = undefined ;
283+ const enabledRegions : string [ ] = [ ] ;
284+ let nextToken : string | undefined = undefined ;
248285
249- do {
250- const input : ListRegionsRequest = {
251- RegionOptStatusContains : [ 'ENABLED' , 'ENABLED_BY_DEFAULT' ] ,
252- NextToken : nextToken ,
253- } ;
286+ do {
287+ const input : ListRegionsRequest = {
288+ RegionOptStatusContains : [ 'ENABLED' , 'ENABLED_BY_DEFAULT' ] ,
289+ NextToken : nextToken ,
290+ } ;
254291
255- const response = await account . send ( new ListRegionsCommand ( input ) ) ;
256- nextToken = response . NextToken ;
292+ const response = await account . send ( new ListRegionsCommand ( input ) ) ;
293+ nextToken = response . NextToken ;
257294
258- enabledRegions . push ( ...response . Regions . map ( r => r . RegionName ) . filter ( Boolean ) ) ;
259- } while ( nextToken ) ;
295+ enabledRegions . push ( ...response . Regions . map ( r => r . RegionName ) . filter ( Boolean ) ) ;
296+ } while ( nextToken ) ;
260297
261- console . log ( 'All enabled regions fetched: ' , enabledRegions ) ;
262- return enabledRegions ;
298+ console . log ( 'All enabled regions fetched: ' , enabledRegions ) ;
299+ return enabledRegions ;
300+ } catch ( e ) {
301+ if ( isXmlParsingError ( e ) ) {
302+ console . log ( `XML parsing error listing regions (non-fatal, using default test regions): ${ e . message } ` ) ;
303+ return AWS_REGIONS_TO_RUN_TESTS ;
304+ }
305+ throw e ;
306+ }
263307} ;
264308
265309/**
@@ -277,7 +321,17 @@ const getAmplifyApps = async (account: AWSAccountInfo, region: string, regionsEn
277321 return [ ] ;
278322 }
279323
280- const amplifyApps = await amplifyClient . send ( new ListAppsCommand ( { maxResults : 50 } ) ) ; // keeping it to 50 as max supported is 50
324+ let amplifyApps ;
325+ try {
326+ amplifyApps = await amplifyClient . send ( new ListAppsCommand ( { maxResults : 50 } ) ) ;
327+ } catch ( e ) {
328+ if ( isXmlParsingError ( e ) ) {
329+ console . log ( `XML parsing error listing Amplify apps in ${ region } (non-fatal, skipping): ${ e . message } ` ) ;
330+ return [ ] ;
331+ }
332+ throw e ;
333+ }
334+
281335 const result : AmplifyAppInfo [ ] = [ ] ;
282336 for ( const app of amplifyApps . apps ) {
283337 const backends : Record < string , StackInfo > = { } ;
@@ -349,6 +403,10 @@ const getStackDetails = async (stackName: string, account: AWSAccountInfo, regio
349403 console . log ( `Stack ${ stackName } does not exist in ${ region } . Skipping.` ) ;
350404 return ;
351405 }
406+ if ( isXmlParsingError ( e ) ) {
407+ console . log ( `XML parsing error describing stack ${ stackName } in ${ region } (non-fatal, skipping): ${ e . message } ` ) ;
408+ return ;
409+ }
352410 throw e ;
353411 }
354412} ;
@@ -361,19 +419,28 @@ const getStacks = async (account: AWSAccountInfo, region: string, regionsEnabled
361419 return [ ] ;
362420 }
363421
364- const stacks = await cfnClient . send ( new ListStacksCommand ( {
365- StackStatusFilter : [
366- 'CREATE_COMPLETE' ,
367- 'ROLLBACK_FAILED' ,
368- 'DELETE_FAILED' ,
369- 'UPDATE_COMPLETE' ,
370- 'UPDATE_ROLLBACK_FAILED' ,
371- 'UPDATE_ROLLBACK_COMPLETE' ,
372- 'IMPORT_COMPLETE' ,
373- 'IMPORT_ROLLBACK_FAILED' ,
374- 'IMPORT_ROLLBACK_COMPLETE' ,
375- ] ,
376- } ) ) ;
422+ let stacks ;
423+ try {
424+ stacks = await cfnClient . send ( new ListStacksCommand ( {
425+ StackStatusFilter : [
426+ 'CREATE_COMPLETE' ,
427+ 'ROLLBACK_FAILED' ,
428+ 'DELETE_FAILED' ,
429+ 'UPDATE_COMPLETE' ,
430+ 'UPDATE_ROLLBACK_FAILED' ,
431+ 'UPDATE_ROLLBACK_COMPLETE' ,
432+ 'IMPORT_COMPLETE' ,
433+ 'IMPORT_ROLLBACK_FAILED' ,
434+ 'IMPORT_ROLLBACK_COMPLETE' ,
435+ ] ,
436+ } ) ) ;
437+ } catch ( e ) {
438+ if ( isXmlParsingError ( e ) ) {
439+ console . log ( `XML parsing error listing stacks in ${ region } (non-fatal, skipping): ${ e . message } ` ) ;
440+ return [ ] ;
441+ }
442+ throw e ;
443+ }
377444
378445 // We are interested in only the root stacks that are deployed by amplify-cli
379446 const rootStacks = stacks . StackSummaries . filter ( stack => ! stack . RootId ) ;
@@ -430,7 +497,16 @@ const getBucketRegion = async (account: AWSAccountInfo, bucketName: string): Pro
430497const getS3Buckets = async ( account : AWSAccountInfo ) : Promise < S3BucketInfo [ ] > => {
431498 const awsConfig = getAWSConfig ( account ) ;
432499 const s3Client = new S3Client ( awsConfig ) ;
433- const buckets = await s3Client . send ( new ListBucketsCommand ( { } ) ) ;
500+ let buckets ;
501+ try {
502+ buckets = await s3Client . send ( new ListBucketsCommand ( { } ) ) ;
503+ } catch ( e ) {
504+ if ( isXmlParsingError ( e ) ) {
505+ console . log ( `XML parsing error listing S3 buckets (non-fatal, skipping): ${ e . message } ` ) ;
506+ return [ ] ;
507+ }
508+ throw e ;
509+ }
434510 const result : S3BucketInfo [ ] = [ ] ;
435511 for ( const bucket of buckets . Buckets ) {
436512 let region : string | undefined ;
@@ -460,6 +536,8 @@ const getS3Buckets = async (account: AWSAccountInfo): Promise<S3BucketInfo[]> =>
460536 console . error ( `Skipping processing ${ account . accountId } , bucket ${ bucket . Name } ` , e ) ;
461537 } else if ( isNonJsonResponseError ( e ) ) {
462538 console . warn ( `Received non-JSON response for bucket ${ bucket . Name } . Skipping.` , e . message ) ;
539+ } else if ( isXmlParsingError ( e ) ) {
540+ console . log ( `XML parsing error for bucket ${ bucket . Name } (non-fatal, skipping): ${ e . message } ` ) ;
463541 } else {
464542 throw e ;
465543 }
@@ -625,9 +703,17 @@ const deleteAttachedRolePolicies = async (
625703 accountIndex : number ,
626704 roleName : string ,
627705) : Promise < void > => {
628- const iamClient = new IAMClient ( getAWSConfig ( account ) ) ;
629- const rolePolicies = await iamClient . send ( new ListAttachedRolePoliciesCommand ( { RoleName : roleName } ) ) ;
630- await Promise . all ( rolePolicies . AttachedPolicies . map ( policy => detachIamAttachedRolePolicy ( account , accountIndex , roleName , policy ) ) ) ;
706+ try {
707+ const iamClient = new IAMClient ( getAWSConfig ( account ) ) ;
708+ const rolePolicies = await iamClient . send ( new ListAttachedRolePoliciesCommand ( { RoleName : roleName } ) ) ;
709+ await Promise . all ( rolePolicies . AttachedPolicies . map ( policy => detachIamAttachedRolePolicy ( account , accountIndex , roleName , policy ) ) ) ;
710+ } catch ( e ) {
711+ if ( isXmlParsingError ( e ) ) {
712+ console . log ( `${ generateAccountInfo ( account , accountIndex ) } XML parsing error listing attached policies for ${ roleName } (non-fatal, skipping): ${ e . message } ` ) ;
713+ return ;
714+ }
715+ throw e ;
716+ }
631717} ;
632718
633719const detachIamAttachedRolePolicy = async (
@@ -653,9 +739,17 @@ const deleteRolePolicies = async (
653739 accountIndex : number ,
654740 roleName : string ,
655741) : Promise < void > => {
656- const iamClient = new IAMClient ( getAWSConfig ( account ) ) ;
657- const rolePolicies = await iamClient . send ( new ListRolePoliciesCommand ( { RoleName : roleName } ) ) ;
658- await Promise . all ( rolePolicies . PolicyNames . map ( policy => deleteIamRolePolicy ( account , accountIndex , roleName , policy ) ) ) ;
742+ try {
743+ const iamClient = new IAMClient ( getAWSConfig ( account ) ) ;
744+ const rolePolicies = await iamClient . send ( new ListRolePoliciesCommand ( { RoleName : roleName } ) ) ;
745+ await Promise . all ( rolePolicies . PolicyNames . map ( policy => deleteIamRolePolicy ( account , accountIndex , roleName , policy ) ) ) ;
746+ } catch ( e ) {
747+ if ( isXmlParsingError ( e ) ) {
748+ console . log ( `${ generateAccountInfo ( account , accountIndex ) } XML parsing error listing policies for ${ roleName } (non-fatal, skipping): ${ e . message } ` ) ;
749+ return ;
750+ }
751+ throw e ;
752+ }
659753} ;
660754
661755const deleteIamRolePolicy = async (
@@ -904,6 +998,11 @@ const cleanupAccount = async (account: AWSAccountInfo, accountIndex: number, fil
904998 summary . skippedReason = 'network error' ;
905999 return ;
9061000 }
1001+ if ( isXmlParsingError ( e ) ) {
1002+ console . warn ( `${ generateAccountInfo ( account , accountIndex ) } XML parsing error encountered. Skipping this account.` , e . message ) ;
1003+ summary . skippedReason = 'XML parsing error' ;
1004+ return ;
1005+ }
9071006 console . error ( `${ generateAccountInfo ( account , accountIndex ) } Cleanup failed with unexpected error:` , e ) ;
9081007 summary . skippedReason = 'unexpected error' ;
9091008 }
@@ -999,9 +1098,10 @@ const printCleanupSummary = (): void => {
9991098
10001099cleanup ( )
10011100 . catch ( ( e ) => {
1002- console . log ( `Cleanup encountered an error but completing gracefully: ${ e . message } ` ) ;
1101+ console . error ( 'Top-level cleanup error (non-fatal):' , e ? .message || e ) ;
10031102 } )
10041103 . finally ( ( ) => {
10051104 printCleanupSummary ( ) ;
1105+ console . log ( 'Cleanup complete. Exiting with code 0.' ) ;
10061106 process . exit ( 0 ) ;
10071107 } ) ;
0 commit comments