diff --git a/packages/amplify-e2e-tests/package.json b/packages/amplify-e2e-tests/package.json index 62f9a5c780..0eea9f865a 100644 --- a/packages/amplify-e2e-tests/package.json +++ b/packages/amplify-e2e-tests/package.json @@ -41,6 +41,7 @@ "@aws-sdk/client-amplify": "^3.812.0", "@aws-sdk/client-sts": "^3.812.0", "@aws-sdk/client-organizations": "^3.812.0", + "@smithy/util-retry": "^4.1.2", "amplify-category-api-e2e-core": "5.0.9", "aws-amplify": "^4.2.8", "aws-appsync": "^4.1.1", diff --git a/packages/amplify-e2e-tests/src/cleanup-e2e-resources.ts b/packages/amplify-e2e-tests/src/cleanup-e2e-resources.ts index 15967dec87..a0bbe369e6 100644 --- a/packages/amplify-e2e-tests/src/cleanup-e2e-resources.ts +++ b/packages/amplify-e2e-tests/src/cleanup-e2e-resources.ts @@ -26,12 +26,27 @@ import { DeleteStackCommand, Tag as CFNTag, waitUntilStackDeleteComplete, + Stack, + StackResourceSummary, + StackStatus, + StackSummary, + ResourceStatus, } from '@aws-sdk/client-cloudformation'; -import { AmplifyClient, DeleteAppCommand, ListAppsCommand, ListBackendEnvironmentsCommand } from '@aws-sdk/client-amplify'; +import { + AmplifyClient, + App, + DeleteAppCommand, + ListAppsCommand, + ListAppsCommandOutput, + ListBackendEnvironmentsCommand, +} from '@aws-sdk/client-amplify'; import { BatchGetBuildsCommand, Build, CodeBuildClient } from '@aws-sdk/client-codebuild'; import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; import { OrganizationsClient, ListAccountsCommand } from '@aws-sdk/client-organizations'; import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; +import { appendAmplifyInput } from './rds-v2-test-utils'; +import { ConfiguredRetryStrategy } from '@smithy/util-retry'; +import { paginate } from './utils/retries'; type TestRegion = { name: string; @@ -43,6 +58,11 @@ const supportedRegionsPath = path.join(repoRoot, 'scripts', 'e2e-test-regions.js const suportedRegions: TestRegion[] = JSON.parse(fs.readFileSync(supportedRegionsPath, 'utf-8')); const testRegions = suportedRegions.map((region) => region.name); +const retryStrategy = new ConfiguredRetryStrategy( + 10, // max attempts. + (attempt: number) => Math.floor(Math.random() * 2 ** attempt * 100), +); + const reportPathDir = path.normalize(path.join(__dirname, '..', 'amplify-e2e-reports')); const MULTI_JOB_APP = ''; @@ -50,6 +70,7 @@ const ORPHAN = ''; const UNKNOWN = ''; type StackInfo = { + stackId: string; stackName: string; stackStatus: string; resourcesFailedToDelete?: string[]; @@ -115,7 +136,9 @@ const BUCKET_TEST_REGEX = /test/; const IAM_TEST_REGEX = /!RotateE2eAwsToken-e2eTestContextRole|-integtest$|^amplify-|^eu-|^us-|^ap-|^auth-exhaustive-tests|rds-schema-inspector-integtest|^amplify_e2e_tests_lambda|^JsonMockStack-jsonMockApi|^SubscriptionAuth|^cdkamplifytable[0-9]*-|^MutationConditionTest-|^SearchableAuth|^SubscriptionRTFTests-|^NonModelAuthV2FunctionTransformerTests-|^MultiAuthV2Transformer|^FunctionTransformerTests|-integtest-/; const RDS_TEST_REGEX = /integtest/; -const STALE_DURATION_MS = 2 * 60 * 60 * 1000; // 2 hours in milliseconds +const STALE_DURATION_MS = 6 * 60 * 60 * 1000; // 6 hours in milliseconds + +const staleHorizonDate = new Date(Date.now() - STALE_DURATION_MS); /* * Exit on expired token as all future requests will fail. @@ -130,22 +153,30 @@ const handleExpiredTokenException = (): void => { */ const testBucketStalenessFilter = (resource: Bucket): boolean => { const isTestResource = resource.Name?.match(BUCKET_TEST_REGEX); - const isStaleResource = resource.CreationDate && Date.now() - new Date(resource.CreationDate).getTime() > STALE_DURATION_MS; + const isStaleResource = resource.CreationDate && before(resource.CreationDate, staleHorizonDate); return !!isTestResource && !!isStaleResource; }; +const testStackStalenessFilter = (resource: Stack): boolean => { + const isStaleResource = before(resource.CreationTime, staleHorizonDate); + return !!isStaleResource; +}; + +const testAppStalenessFilter = (resource: App): boolean => { + const isStaleResource = before(resource.createTime, staleHorizonDate); + return !!isStaleResource; +}; + const testRoleStalenessFilter = (resource: Role): boolean => { const isTestResource = resource.RoleName?.match(IAM_TEST_REGEX); - const isStaleResource = resource.CreateDate && Date.now() - new Date(resource.CreateDate).getTime() > STALE_DURATION_MS; + const isStaleResource = resource.CreateDate && before(resource.CreateDate, staleHorizonDate); return !!isTestResource && !!isStaleResource; }; const testInstanceStalenessFilter = (resource: DBInstance): boolean => { const isTestResource = resource.DBInstanceIdentifier?.match(RDS_TEST_REGEX); const isStaleResource = - resource.DBInstanceStatus === 'available' && - resource.InstanceCreateTime && - Date.now() - new Date(resource.InstanceCreateTime).getTime() > STALE_DURATION_MS; + resource.DBInstanceStatus === 'available' && resource.InstanceCreateTime && before(resource.InstanceCreateTime, staleHorizonDate); return !!isTestResource && !!isStaleResource; }; @@ -192,7 +223,9 @@ const getOrphanRdsInstances = async (account: AWSAccountInfo, region: string): P if (e?.name === 'InvalidClientTokenId') { // Do not fail the cleanup and continue // This is due to either child account or parent account not available in that region - console.log(`Listing RDS instances for account ${account.accountId}-${region} failed with error with code ${e?.name}. Skipping.`); + console.log( + `(opt-in region failure) Listing RDS instances for account ${account.accountId}-${region} failed with error with code ${e?.name}. Skipping.`, + ); return []; } else { console.log('Irrecoverable error in getOrphanedRdsInstances', JSON.stringify(e)); @@ -214,7 +247,7 @@ const getAmplifyApps = async (account: AWSAccountInfo, region: string): Promise< region, }); const result: AmplifyAppInfo[] = []; - let amplifyApps = { apps: [] }; + let amplifyApps: ListAppsCommandOutput | undefined; try { console.log(`Listing apps for ${account.accountId} in ${region}.`); const listAppsCommand = new ListAppsCommand({ maxResults: 50 }); @@ -222,7 +255,9 @@ const getAmplifyApps = async (account: AWSAccountInfo, region: string): Promise< } catch (e) { if (e?.name === 'UnrecognizedClientException' || e?.name === 'InvalidClientTokenId') { // Do not fail the cleanup and continue - console.log(`Listing apps for account ${account.accountId}-${region} failed with error with code ${e?.name}. Skipping.`); + console.log( + `(opt-in region failure) Listing apps for account ${account.accountId}-${region} failed with error with code ${e?.name}. Skipping.`, + ); return result; } else { console.log('Irrecoverable error in getAmplifyApps', JSON.stringify(e)); @@ -230,7 +265,7 @@ const getAmplifyApps = async (account: AWSAccountInfo, region: string): Promise< } } - for (const app of amplifyApps?.apps) { + for (const app of (amplifyApps?.apps ?? []).filter(testAppStalenessFilter)) { const backends: Record = {}; try { const listBackendEnvironments = new ListBackendEnvironmentsCommand({ appId: app.appId, maxResults: 50 }); @@ -251,6 +286,7 @@ const getAmplifyApps = async (account: AWSAccountInfo, region: string): Promise< backends, }); } + return result; }; @@ -275,7 +311,7 @@ const getJobId = (tags: CFNTag[] = []): string | undefined => { * @returns stack details */ const getStackDetails = async (stackName: string, account: AWSAccountInfo, region: string): Promise => { - const cfnClient = new CloudFormationClient({ credentials: account.credentials, region }); + const cfnClient = new CloudFormationClient({ credentials: account.credentials, region, retryStrategy }); const stack = await cfnClient.send(new DescribeStacksCommand({ StackName: stackName })); const tags = stack.Stacks.length && stack.Stacks[0].Tags; const stackStatus = stack.Stacks[0].StackStatus; @@ -289,6 +325,7 @@ const getStackDetails = async (stackName: string, account: AWSAccountInfo, regio } const jobId = getJobId(tags); return { + stackId: stack.Stacks[0].StackId, stackName, stackStatus, resourcesFailedToDelete, @@ -298,52 +335,115 @@ const getStackDetails = async (stackName: string, account: AWSAccountInfo, regio }; }; -const getStacks = async (account: AWSAccountInfo, region: string): Promise => { - const cfnClient = new CloudFormationClient({ credentials: account.credentials, region }); - const results: StackInfo[] = []; - let stacks; - try { - stacks = await cfnClient.send( - new ListStacksCommand({ - StackStatusFilter: [ - 'CREATE_COMPLETE', - 'ROLLBACK_FAILED', - 'DELETE_FAILED', - 'UPDATE_COMPLETE', - 'UPDATE_ROLLBACK_FAILED', - 'UPDATE_ROLLBACK_COMPLETE', - 'IMPORT_COMPLETE', - 'IMPORT_ROLLBACK_FAILED', - 'IMPORT_ROLLBACK_COMPLETE', - ], +const STABLE_STATUSES: StackStatus[] = [ + 'CREATE_COMPLETE', + 'ROLLBACK_FAILED', + 'DELETE_FAILED', + 'UPDATE_COMPLETE', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE', + 'IMPORT_COMPLETE', + 'IMPORT_ROLLBACK_FAILED', + 'IMPORT_ROLLBACK_COMPLETE', +]; + +const listStackResources = async (client: CloudFormationClient, stackName: string): Promise => { + return paginate(async (token) => { + const response = await client.send( + new ListStackResourcesCommand({ + StackName: stackName, + NextToken: token, }), ); - } catch (e) { + return { nextPage: response.NextToken, items: response.StackResourceSummaries }; + }); +}; + +const listStacks = async (client: CloudFormationClient, stackStatusFilter: StackStatus[] | undefined): Promise => { + try { + return await paginate(async (token) => { + const response = await client.send( + new ListStacksCommand({ + NextToken: token, + StackStatusFilter: stackStatusFilter, + }), + ); + return { token: response.NextToken, items: response.StackSummaries }; + }); + } catch (e: any) { if (e?.name === 'InvalidClientTokenId') { - // Do not fail the cleanup and continue - console.log(`Listing stacks for account ${account.accountId}-${region} failed with error with code ${e?.name}. Skipping.`); - return results; - } else { - console.log('Irrecoverable error in getStacks', JSON.stringify(e)); - throw e; + console.log(`(opt-in region failure) Listing stacks failed with error with code ${e?.name}. Skipping.`); + return []; } + throw e; } +}; + +const getStacks = async (account: AWSAccountInfo, region: string): Promise => { + const cfnClient = new CloudFormationClient({ credentials: account.credentials, region, retryStrategy }); + const stacks = await listStacks(cfnClient, STABLE_STATUSES); + const results: StackInfo[] = []; // We are interested in only the root stacks that are deployed by amplify-cli - const rootStacks = stacks.StackSummaries.filter((stack) => !stack.RootId); + const rootStacks = (stacks ?? []).filter((stack) => !stack.RootId).filter(testStackStalenessFilter); for (const stack of rootStacks) { try { const details = await getStackDetails(stack.StackName, account, region); if (details) { - results.push(details); + results[details.stackId] = details; } } catch { // don't want to barf and fail e2e tests } } + return results; }; +/** + * Return all resources managed by stacks in the entire account + * + * Returns all resources as a string in a set, so it's easy to test for membership. + */ +const getAllCfnManagedResources = async (account: AWSAccountInfo, region: string): Promise> => { + const liveResourceStates: ResourceStatus[] = [ + 'CREATE_IN_PROGRESS', + 'CREATE_COMPLETE', + 'DELETE_IN_PROGRESS', + 'IMPORT_IN_PROGRESS', + 'IMPORT_COMPLETE', + 'ROLLBACK_IN_PROGRESS', + 'ROLLBACK_FAILED', + 'UPDATE_COMPLETE', + 'UPDATE_FAILED', + 'UPDATE_ROLLBACK_COMPLETE', + 'UPDATE_ROLLBACK_IN_PROGRESS', + 'UPDATE_ROLLBACK_FAILED', + ]; + + const client = new CloudFormationClient({ credentials: account.credentials, region, retryStrategy }); + const ret = new Set(); + for (const stack of await listStacks(client, undefined)) { + try { + for (const resource of await listStackResources(client, stack.StackName)) { + if (resource.PhysicalResourceId && liveResourceStates.includes(resource.ResourceStatus)) { + ret.add(resourceId(resource.ResourceType, resource.PhysicalResourceId)); + } + } + } catch (e: any) { + if (e.name === 'ValidationError') { + continue; + } + throw e; + } + } + return ret; +}; + +function resourceId(resourceType: string, resourceId: string): string { + return `${resourceType}#${resourceId}`; +} + const getCodeBuildClient = (): CodeBuildClient => { return new CodeBuildClient({ region: 'us-east-1' }); }; @@ -373,7 +473,7 @@ const getS3Buckets = async (account: AWSAccountInfo): Promise => const s3Client = new S3Client({ credentials: account.credentials }); const buckets = await s3Client.send(new ListBucketsCommand({})); const result: S3BucketInfo[] = []; - for (const bucket of buckets.Buckets) { + for (const bucket of buckets.Buckets.filter(testBucketStalenessFilter)) { let region: string | undefined; try { region = await getBucketRegion(account, bucket.Name); @@ -410,6 +510,7 @@ const getS3Buckets = async (account: AWSAccountInfo): Promise => } } } + return result; }; @@ -669,8 +770,14 @@ const deleteCfnStack = async (account: AWSAccountInfo, accountIndex: number, sta const resourceToRetain = resourcesFailedToDelete && resourcesFailedToDelete.length ? resourcesFailedToDelete : undefined; console.log(`${generateAccountInfo(account, accountIndex)} Deleting CloudFormation stack ${stackName}`); try { - const cfnClient = new CloudFormationClient({ credentials: account.credentials, region }); - await cfnClient.send(new DeleteStackCommand({ StackName: stackName, RetainResources: resourceToRetain })); + const cfnClient = new CloudFormationClient({ credentials: account.credentials, region, retryStrategy }); + await cfnClient.send( + new DeleteStackCommand({ + StackName: stackName, + RetainResources: resourceToRetain, + DeletionMode: 'FORCE_DELETE_STACK', + }), + ); await waitUntilStackDeleteComplete({ client: cfnClient, maxWaitTime: 600 }, { StackName: stackName }); } catch (e) { console.log('Error', JSON.stringify(e)); @@ -746,10 +853,12 @@ const getFilterPredicate = (args: any): JobFilterPredicate => { * to get all accounts within the root account organization. */ const getAccountsToCleanup = async (): Promise => { + const cleanupTag = new Date().toISOString().replace(/:/g, '').replace(/\..+$/, ''); + const parentAccountCreds = fromTemporaryCredentials({ params: { RoleArn: process.env.TEST_ACCOUNT_ROLE, - RoleSessionName: `testSession${Math.floor(Math.random() * 100000)}`, + RoleSessionName: `cleanupSession${cleanupTag}`, }, clientConfig: { region: 'us-east-1', @@ -772,13 +881,12 @@ const getAccountsToCleanup = async (): Promise => { credentials: parentAccountCreds, }; } - const randomNumber = Math.floor(Math.random() * 100000); return { accountId: account.Id, credentials: fromTemporaryCredentials({ params: { RoleArn: `arn:aws:iam::${account.Id}:role/OrganizationAccountAccessRole`, - RoleSessionName: `testSession${randomNumber}`, + RoleSessionName: `cleanupSession${cleanupTag}`, }, masterCredentials: parentAccountCreds, clientConfig: { @@ -810,19 +918,28 @@ const cleanupAccount = async (account: AWSAccountInfo, accountIndex: number, fil const orphanBucketPromise = getOrphanS3TestBuckets(account); const orphanIamRolesPromise = getOrphanTestIamRoles(account); const orphanRdsInstancesPromise = testRegions.map((region) => getOrphanRdsInstances(account, region)); + const cfnResourcesPromise = testRegions.map((region) => getAllCfnManagedResources(account, region)); + + const cfnManaged = setUnion(...(await Promise.all(cfnResourcesPromise)).flat()); const apps = (await Promise.all(appPromises)).flat(); const stacks = (await Promise.all(stackPromises)).flat(); - const buckets = await bucketPromise; - const orphanBuckets = await orphanBucketPromise; - const orphanIamRoles = await orphanIamRolesPromise; - const orphanRdsInstances = (await Promise.all(orphanRdsInstancesPromise)).flat(); + const buckets = (await bucketPromise).filter((x) => !cfnManaged.has(resourceId('AWS::S3::Bucket', x.name))); + const orphanBuckets = (await orphanBucketPromise).filter((x) => !cfnManaged.has(resourceId('AWS::S3::Bucket', x.name))); + const orphanIamRoles = (await orphanIamRolesPromise).filter((x) => !cfnManaged.has(resourceId('AWS::IAM::Role', x.name))); + const orphanRdsInstances = (await Promise.all(orphanRdsInstancesPromise)) + .flat() + .filter((b) => !cfnManaged.has(resourceId('AWS::RDS::DBInstance', b.identifier))); const allResources = await mergeResourcesByCCIJob(apps, stacks, buckets, orphanBuckets, orphanIamRoles, orphanRdsInstances); const staleResources = _.pickBy(allResources, filterPredicate); generateReport(staleResources, accountIndex); - await deleteResources(account, accountIndex, staleResources); + if (process.env.SKIP_DELETE) { + console.log('🧸 Skipping delete ($SKIP_DELETE)'); + } else { + await deleteResources(account, accountIndex, staleResources); + } console.log(`${generateAccountInfo(account, accountIndex)} Cleanup done!`); }; @@ -858,11 +975,45 @@ const cleanup = async (): Promise => { const filterPredicate = getFilterPredicate(args); const accounts = await getAccountsToCleanup(); - accounts.map((account, i) => { - console.log(`${generateAccountInfo(account, i)} is under cleanup`); - }); - await Promise.all(accounts.map((account, i) => cleanupAccount(account, i, filterPredicate))); + + // Do a limited amount of accounts in parallel. Otherwise there are too many and the machine might + // have trouble resolving DNS, and generally doing the network things it needs to do. + for (const batch of chunk(2, accounts)) { + await Promise.all( + batch.map(async (account, i) => { + console.log(`${generateAccountInfo(account, i)} is under cleanup`); + return cleanupAccount(account, i, filterPredicate); + }), + ); + } + console.log('Done cleaning all accounts!'); }; -cleanup(); +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function before(a: Date, b: Date) { + return a.getTime() < b.getTime(); +} + +function setUnion(...xss: Set[]): Set { + const ret = new Set(); + for (const xs of xss) { + for (const x of Array.from(xs)) { + ret.add(x); + } + } + return ret; +} + +function chunk(n: number, xs: A[]): A[][] { + const ret: A[][] = []; + for (let i = 0; i < xs.length; i += n) { + ret.push(xs.slice(i, i + n)); + } + return ret; +} + +cleanup().catch((e) => { + console.error(e); + process.exitCode = 1; +}); diff --git a/packages/amplify-e2e-tests/src/utils/retries.ts b/packages/amplify-e2e-tests/src/utils/retries.ts new file mode 100644 index 0000000000..294ccc2616 --- /dev/null +++ b/packages/amplify-e2e-tests/src/utils/retries.ts @@ -0,0 +1,56 @@ +/* eslint-disable no-constant-condition */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ +/* eslint-disable func-style */ + +/** + * Retry a block of code up to a maximum number of times + */ +export async function retry(block: () => Promise) { + // Retry for 5 minutes + const deadline = Date.now() + 300_000; + + let delay = 100; + while (true) { + try { + return await block(); + } catch (e: any) { + if (Date.now() < deadline && isRetryableError(e)) { + await sleep(Math.floor(Math.random() * delay)); + delay *= 2; + } else { + throw e; + } + } + } +} + +function isRetryableError(e: Error) { + if (['Throttling'].includes(e.name)) { + return true; + } + + return false; +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * An SDK paginator that properly handles retries of throttles + * + * For some reason, I'm seeing unretried 'Throttling' errors from the CFN client. + */ +export async function paginate(pageFetcher: (token: string) => Promise<{ nextPage?: string; items?: A[] }>): Promise { + const ret: A[] = []; + + let token: undefined | string; + do { + const response = await retry(() => pageFetcher(token)); + ret.push(...(response.items ?? [])); + token = response.nextPage; + } while (token); + + return ret; +} diff --git a/yarn.lock b/yarn.lock index b4c67fd671..3779af2404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12067,6 +12067,13 @@ dependencies: "@smithy/types" "^4.3.1" +"@smithy/service-error-classification@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz#06839c332f4620a4b80c78a0c32377732dc6697a" + integrity sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ== + dependencies: + "@smithy/types" "^4.5.0" + "@smithy/shared-ini-file-loader@^3.1.12", "@smithy/shared-ini-file-loader@^3.1.4": version "3.1.12" resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.12.tgz#d98b1b663eb18935ce2cbc79024631d34f54042a" @@ -12220,6 +12227,13 @@ dependencies: tslib "^2.6.2" +"@smithy/types@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.5.0.tgz#850e334662a1ef1286c35814940c80880400a370" + integrity sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg== + dependencies: + tslib "^2.6.2" + "@smithy/url-parser@^3.0.11", "@smithy/url-parser@^3.0.3": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.11.tgz#e5f5ffabfb6230159167cf4cc970705fca6b8b2d" @@ -12552,6 +12566,15 @@ "@smithy/types" "^4.3.1" tslib "^2.6.2" +"@smithy/util-retry@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.1.2.tgz#8d28c27cf69643e173c75cc18ff0186deb7cefed" + integrity sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA== + dependencies: + "@smithy/service-error-classification" "^4.1.2" + "@smithy/types" "^4.5.0" + tslib "^2.6.2" + "@smithy/util-stream@^3.1.3", "@smithy/util-stream@^3.3.4": version "3.3.4" resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.4.tgz#c506ac41310ebcceb0c3f0ba20755e4fe0a90b8d" @@ -21981,16 +22004,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22112,7 +22126,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22133,13 +22147,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -23168,7 +23175,7 @@ workerpool@^6.5.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23186,15 +23193,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7, wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"