@devramps/cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +340 -86
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -74,6 +74,80 @@ var CloudFormationError = class extends DevRampsError {
74
74
  // src/utils/logger.ts
75
75
  import chalk from "chalk";
76
76
  var verboseMode = false;
77
+ var ProgressBar = class {
78
+ current = 0;
79
+ total = 0;
80
+ label;
81
+ barWidth = 30;
82
+ lastLineCount = 0;
83
+ eventLines = [];
84
+ maxVisibleEvents = 5;
85
+ constructor(label, total) {
86
+ this.label = label;
87
+ this.total = total;
88
+ this.render();
89
+ }
90
+ /**
91
+ * Update progress and re-render
92
+ */
93
+ update(current, eventMessage) {
94
+ this.current = current;
95
+ if (eventMessage) {
96
+ this.eventLines.push(eventMessage);
97
+ if (this.eventLines.length > this.maxVisibleEvents) {
98
+ this.eventLines.shift();
99
+ }
100
+ }
101
+ this.render();
102
+ }
103
+ /**
104
+ * Add an event message without changing progress
105
+ */
106
+ addEvent(message) {
107
+ this.eventLines.push(message);
108
+ if (this.eventLines.length > this.maxVisibleEvents) {
109
+ this.eventLines.shift();
110
+ }
111
+ this.render();
112
+ }
113
+ /**
114
+ * Clear the progress bar from the terminal
115
+ */
116
+ clear() {
117
+ for (let i = 0; i < this.lastLineCount; i++) {
118
+ process.stdout.write("\x1B[A\x1B[2K");
119
+ }
120
+ this.lastLineCount = 0;
121
+ }
122
+ /**
123
+ * Finish and clear the progress bar
124
+ */
125
+ finish() {
126
+ this.clear();
127
+ }
128
+ /**
129
+ * Render the progress bar
130
+ */
131
+ render() {
132
+ this.clear();
133
+ const lines = [];
134
+ for (const event of this.eventLines) {
135
+ lines.push(event);
136
+ }
137
+ const percentage = this.total > 0 ? this.current / this.total : 0;
138
+ const filled = Math.round(this.barWidth * percentage);
139
+ const empty = this.barWidth - filled;
140
+ const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
141
+ const count = chalk.cyan(`${this.current}/${this.total}`);
142
+ const labelText = chalk.bold(this.label);
143
+ lines.push(`${labelText} ${bar} ${count} resources`);
144
+ lines.push("");
145
+ for (const line of lines) {
146
+ process.stdout.write(line + "\n");
147
+ }
148
+ this.lastLineCount = lines.length;
149
+ }
150
+ };
77
151
  function setVerbose(enabled) {
78
152
  verboseMode = enabled;
79
153
  }
@@ -234,13 +308,12 @@ import {
234
308
  CloudFormationClient,
235
309
  DescribeStacksCommand,
236
310
  DescribeStackResourcesCommand,
311
+ DescribeStackEventsCommand,
237
312
  CreateStackCommand,
238
313
  UpdateStackCommand,
239
314
  CreateChangeSetCommand,
240
315
  DescribeChangeSetCommand,
241
316
  DeleteChangeSetCommand,
242
- waitUntilStackCreateComplete,
243
- waitUntilStackUpdateComplete,
244
317
  waitUntilChangeSetCreateComplete,
245
318
  ChangeSetType
246
319
  } from "@aws-sdk/client-cloudformation";
@@ -361,21 +434,114 @@ function getActionSymbol(action) {
361
434
  return " ";
362
435
  }
363
436
  }
437
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
438
+ "CREATE_COMPLETE",
439
+ "CREATE_FAILED",
440
+ "DELETE_COMPLETE",
441
+ "DELETE_FAILED",
442
+ "ROLLBACK_COMPLETE",
443
+ "ROLLBACK_FAILED",
444
+ "UPDATE_COMPLETE",
445
+ "UPDATE_FAILED",
446
+ "UPDATE_ROLLBACK_COMPLETE",
447
+ "UPDATE_ROLLBACK_FAILED"
448
+ ]);
449
+ var SUCCESS_STATES = /* @__PURE__ */ new Set([
450
+ "CREATE_COMPLETE",
451
+ "UPDATE_COMPLETE"
452
+ ]);
453
+ function getStatusSymbol(status) {
454
+ if (!status) return "?";
455
+ if (status.includes("COMPLETE") && !status.includes("ROLLBACK")) return "\u2714";
456
+ if (status.includes("FAILED") || status.includes("ROLLBACK")) return "\u2716";
457
+ if (status.includes("IN_PROGRESS")) return "\u22EF";
458
+ return "?";
459
+ }
460
+ function formatStackEvent(event) {
461
+ const symbol = getStatusSymbol(event.ResourceStatus);
462
+ const resourceType = event.ResourceType || "Unknown";
463
+ const logicalId = event.LogicalResourceId || "Unknown";
464
+ const status = event.ResourceStatus || "Unknown";
465
+ const reason = event.ResourceStatusReason ? ` - ${event.ResourceStatusReason}` : "";
466
+ return ` ${symbol} ${resourceType} (${logicalId}): ${status}${reason}`;
467
+ }
468
+ function isResourceComplete(status) {
469
+ if (!status) return false;
470
+ return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
471
+ }
472
+ async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600, showProgress = true) {
473
+ const seenEventIds = /* @__PURE__ */ new Set();
474
+ const completedResources = /* @__PURE__ */ new Set();
475
+ const startTime = Date.now();
476
+ const pollInterval = 3e3;
477
+ const progressBar = showProgress ? new ProgressBar(stackName, totalResources) : null;
478
+ try {
479
+ while (true) {
480
+ if (Date.now() - startTime > maxWaitTime * 1e3) {
481
+ throw new Error(`Stack operation timed out after ${maxWaitTime} seconds`);
482
+ }
483
+ const stackResponse = await client.send(
484
+ new DescribeStacksCommand({ StackName: stackName })
485
+ );
486
+ const stack = stackResponse.Stacks?.[0];
487
+ if (!stack) {
488
+ throw new Error(`Stack ${stackName} not found`);
489
+ }
490
+ const eventsResponse = await client.send(
491
+ new DescribeStackEventsCommand({ StackName: stackName })
492
+ );
493
+ const newEvents = (eventsResponse.StackEvents || []).filter((event) => {
494
+ if (!event.Timestamp || event.Timestamp < operationStartTime) return false;
495
+ if (!event.EventId || seenEventIds.has(event.EventId)) return false;
496
+ return true;
497
+ }).reverse();
498
+ for (const event of newEvents) {
499
+ if (event.EventId) {
500
+ seenEventIds.add(event.EventId);
501
+ }
502
+ const logicalId = event.LogicalResourceId;
503
+ if (logicalId && logicalId !== stackName && isResourceComplete(event.ResourceStatus)) {
504
+ completedResources.add(logicalId);
505
+ }
506
+ if (progressBar) {
507
+ progressBar.update(completedResources.size, formatStackEvent(event));
508
+ }
509
+ }
510
+ const currentStatus = stack.StackStatus || "";
511
+ if (TERMINAL_STATES.has(currentStatus)) {
512
+ if (progressBar) {
513
+ progressBar.finish();
514
+ }
515
+ if (SUCCESS_STATES.has(currentStatus)) {
516
+ return;
517
+ }
518
+ throw new Error(`Stack operation failed with status: ${currentStatus}`);
519
+ }
520
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
521
+ }
522
+ } catch (error2) {
523
+ if (progressBar) {
524
+ progressBar.finish();
525
+ }
526
+ throw error2;
527
+ }
528
+ }
364
529
  async function deployStack(options) {
365
- const { stackName, template, accountId, region, credentials } = options;
530
+ const { stackName, template, accountId, region, credentials, showProgress = true } = options;
366
531
  const client = new CloudFormationClient({
367
532
  credentials,
368
533
  region
369
534
  });
370
535
  const templateBody = JSON.stringify(template);
536
+ const resourceCount = Object.keys(template.Resources || {}).length;
371
537
  try {
372
538
  const stackStatus = await getStackStatus(stackName, credentials, region);
373
539
  if (stackStatus.exists) {
374
540
  verbose(`Stack ${stackName} exists, updating...`);
375
- await updateStack(client, stackName, templateBody, accountId);
541
+ await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
376
542
  } else {
377
543
  verbose(`Stack ${stackName} does not exist, creating...`);
378
- await createStack(client, stackName, templateBody, accountId);
544
+ await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
379
545
  }
380
546
  } catch (error2) {
381
547
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
@@ -386,7 +552,8 @@ async function deployStack(options) {
386
552
  throw new CloudFormationError(stackName, accountId, errorMessage);
387
553
  }
388
554
  }
389
- async function createStack(client, stackName, templateBody, accountId) {
555
+ async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
556
+ const operationStartTime = /* @__PURE__ */ new Date();
390
557
  await client.send(
391
558
  new CreateStackCommand({
392
559
  StackName: stackName,
@@ -398,14 +565,12 @@ async function createStack(client, stackName, templateBody, accountId) {
398
565
  ]
399
566
  })
400
567
  );
401
- verbose(`Waiting for stack ${stackName} to be created...`);
402
- await waitUntilStackCreateComplete(
403
- { client, maxWaitTime: 600 },
404
- { StackName: stackName }
405
- );
568
+ info(`Creating stack ${stackName}...`);
569
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
406
570
  success(`Stack ${stackName} created successfully in account ${accountId}`);
407
571
  }
408
- async function updateStack(client, stackName, templateBody, accountId) {
572
+ async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
573
+ const operationStartTime = /* @__PURE__ */ new Date();
409
574
  await client.send(
410
575
  new UpdateStackCommand({
411
576
  StackName: stackName,
@@ -413,11 +578,8 @@ async function updateStack(client, stackName, templateBody, accountId) {
413
578
  Capabilities: ["CAPABILITY_NAMED_IAM"]
414
579
  })
415
580
  );
416
- verbose(`Waiting for stack ${stackName} to be updated...`);
417
- await waitUntilStackUpdateComplete(
418
- { client, maxWaitTime: 600 },
419
- { StackName: stackName }
420
- );
581
+ info(`Updating stack ${stackName}...`);
582
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
421
583
  success(`Stack ${stackName} updated successfully in account ${accountId}`);
422
584
  }
423
585
  async function readExistingStack(stackName, accountId, region, credentials) {
@@ -473,49 +635,6 @@ async function readExistingStack(stackName, accountId, region, credentials) {
473
635
  }
474
636
  }
475
637
 
476
- // src/aws/oidc-provider.ts
477
- import {
478
- IAMClient,
479
- GetOpenIDConnectProviderCommand,
480
- ListOpenIDConnectProvidersCommand
481
- } from "@aws-sdk/client-iam";
482
- async function checkOidcProviderExists(credentials, region) {
483
- const client = new IAMClient({
484
- credentials,
485
- region
486
- });
487
- try {
488
- const response = await client.send(new ListOpenIDConnectProvidersCommand({}));
489
- const providers = response.OpenIDConnectProviderList || [];
490
- for (const provider of providers) {
491
- if (!provider.Arn) continue;
492
- try {
493
- const providerDetails = await client.send(
494
- new GetOpenIDConnectProviderCommand({
495
- OpenIDConnectProviderArn: provider.Arn
496
- })
497
- );
498
- if (providerDetails.Url?.includes(OIDC_PROVIDER_URL)) {
499
- verbose(`Found existing OIDC provider: ${provider.Arn}`);
500
- return {
501
- exists: true,
502
- arn: provider.Arn
503
- };
504
- }
505
- } catch {
506
- }
507
- }
508
- verbose(`No existing OIDC provider found for ${OIDC_PROVIDER_URL}`);
509
- return { exists: false };
510
- } catch (error2) {
511
- verbose(`Error checking OIDC providers: ${error2 instanceof Error ? error2.message : String(error2)}`);
512
- return { exists: false };
513
- }
514
- }
515
- function getOidcThumbprint() {
516
- return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
517
- }
518
-
519
638
  // src/auth/browser-auth.ts
520
639
  import express from "express";
521
640
  import open from "open";
@@ -1154,6 +1273,16 @@ function getArtifactId(artifact) {
1154
1273
  return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1155
1274
  }
1156
1275
 
1276
+ // src/aws/oidc-provider.ts
1277
+ import {
1278
+ IAMClient,
1279
+ GetOpenIDConnectProviderCommand,
1280
+ ListOpenIDConnectProvidersCommand
1281
+ } from "@aws-sdk/client-iam";
1282
+ function getOidcThumbprint() {
1283
+ return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
1284
+ }
1285
+
1157
1286
  // src/templates/common.ts
1158
1287
  var STANDARD_TAGS = [
1159
1288
  { Key: "CreatedBy", Value: "DevRamps" },
@@ -1366,6 +1495,9 @@ function getPipelineStackName(pipelineSlug) {
1366
1495
  function getStageStackName(pipelineSlug, stageName) {
1367
1496
  return truncateName(`DevRamps-${pipelineSlug}-${stageName}-Stage`, CF_STACK_MAX_LENGTH);
1368
1497
  }
1498
+ function getAccountStackName() {
1499
+ return "DevRamps-Account-Bootstrap";
1500
+ }
1369
1501
  function getKmsKeyAlias(orgSlug) {
1370
1502
  return `alias/devramps-${normalizeName(orgSlug)}`;
1371
1503
  }
@@ -1821,6 +1953,21 @@ function generatePipelineStackTemplate(options) {
1821
1953
  return template;
1822
1954
  }
1823
1955
 
1956
+ // src/templates/account-stack.ts
1957
+ function generateAccountStackTemplate() {
1958
+ const template = createBaseTemplate(
1959
+ "DevRamps Account Bootstrap Stack - Creates OIDC provider for the account"
1960
+ );
1961
+ addOidcProviderResource(template, false);
1962
+ template.Outputs = {
1963
+ OIDCProviderArn: {
1964
+ Description: "ARN of the OIDC provider",
1965
+ Value: { "Fn::GetAtt": ["DevRampsOIDCProvider", "Arn"] }
1966
+ }
1967
+ };
1968
+ return template;
1969
+ }
1970
+
1824
1971
  // src/permissions/eks-deploy.ts
1825
1972
  var EKS_DEPLOY_PERMISSIONS = {
1826
1973
  actions: [
@@ -2008,7 +2155,6 @@ function generateStageStackTemplate(options) {
2008
2155
  const template = createBaseTemplate(
2009
2156
  `DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
2010
2157
  );
2011
- addOidcProviderResource(template, true);
2012
2158
  const roleName = generateStageRoleName(pipelineSlug, stageName);
2013
2159
  const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, stageName);
2014
2160
  const policies = buildStagePolicies(steps, additionalPolicies);
@@ -2054,6 +2200,7 @@ function generateStageStackTemplate(options) {
2054
2200
  );
2055
2201
  s3Outputs[artifact.name] = { resourceId };
2056
2202
  }
2203
+ const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${OIDC_PROVIDER_URL}`;
2057
2204
  template.Outputs = {
2058
2205
  StageRoleArn: {
2059
2206
  Description: "ARN of the stage deployment role",
@@ -2065,8 +2212,8 @@ function generateStageStackTemplate(options) {
2065
2212
  Value: { Ref: "StageDeploymentRole" }
2066
2213
  },
2067
2214
  OIDCProviderArn: {
2068
- Description: "ARN of the OIDC provider",
2069
- Value: getOidcProviderArn(accountId, true)
2215
+ Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
2216
+ Value: oidcProviderArn
2070
2217
  },
2071
2218
  PipelineSlug: {
2072
2219
  Description: "Pipeline slug",
@@ -2286,7 +2433,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2286
2433
  }
2287
2434
  const orgStackName = getOrgStackName(orgSlug);
2288
2435
  const orgStack = {
2289
- stackType: "Org",
2436
+ stackType: "Org" /* ORG */,
2290
2437
  stackName: orgStackName,
2291
2438
  accountId: cicdAccountId,
2292
2439
  region: cicdRegion,
@@ -2300,7 +2447,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2300
2447
  const filteredArtifacts = filterArtifactsForPipelineStack(artifacts);
2301
2448
  const stackName = getPipelineStackName(pipeline.slug);
2302
2449
  pipelineStacks.push({
2303
- stackType: "Pipeline",
2450
+ stackType: "Pipeline" /* PIPELINE */,
2304
2451
  stackName,
2305
2452
  accountId: cicdAccountId,
2306
2453
  region: cicdRegion,
@@ -2310,6 +2457,38 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2310
2457
  bundleArtifacts: filteredArtifacts.bundle
2311
2458
  });
2312
2459
  }
2460
+ const accountStacks = [];
2461
+ const accountStackName = getAccountStackName();
2462
+ const accountsWithStacks = /* @__PURE__ */ new Set();
2463
+ for (const pipeline of pipelines) {
2464
+ for (const stage of pipeline.stages) {
2465
+ if (accountsWithStacks.has(stage.account_id)) {
2466
+ continue;
2467
+ }
2468
+ accountsWithStacks.add(stage.account_id);
2469
+ let accountCredentials;
2470
+ try {
2471
+ if (stage.account_id !== currentAccountId) {
2472
+ const assumed = await assumeRoleForAccount({
2473
+ targetAccountId: stage.account_id,
2474
+ currentAccountId,
2475
+ targetRoleName
2476
+ });
2477
+ accountCredentials = assumed?.credentials;
2478
+ }
2479
+ } catch {
2480
+ verbose(`Could not assume role in ${stage.account_id} for status check`);
2481
+ }
2482
+ accountStacks.push({
2483
+ stackType: "Account" /* ACCOUNT */,
2484
+ stackName: accountStackName,
2485
+ accountId: stage.account_id,
2486
+ region: cicdRegion,
2487
+ // Deploy in CI/CD region for consistency
2488
+ action: await determineStackAction(accountStackName, accountCredentials, cicdRegion)
2489
+ });
2490
+ }
2491
+ }
2313
2492
  const stageStacks = [];
2314
2493
  for (const pipeline of pipelines) {
2315
2494
  const artifacts = pipelineArtifacts.get(pipeline.slug);
@@ -2329,7 +2508,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2329
2508
  verbose(`Could not assume role in ${stage.account_id} for status check`);
2330
2509
  }
2331
2510
  stageStacks.push({
2332
- stackType: "Stage",
2511
+ stackType: "Stage" /* STAGE */,
2333
2512
  stackName,
2334
2513
  accountId: stage.account_id,
2335
2514
  region: stage.region,
@@ -2350,6 +2529,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2350
2529
  cicdRegion,
2351
2530
  orgStack,
2352
2531
  pipelineStacks,
2532
+ accountStacks,
2353
2533
  stageStacks
2354
2534
  };
2355
2535
  }
@@ -2373,34 +2553,40 @@ async function showDryRunPlan(plan) {
2373
2553
  info(` Account: ${plan.orgStack.accountId}`);
2374
2554
  info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
2375
2555
  newline();
2376
- info("Phase 2: Pipeline Stacks");
2556
+ info("Phase 2: Pipeline & Account Stacks (parallel)");
2377
2557
  for (const stack of plan.pipelineStacks) {
2378
2558
  info(` ${stack.action}: ${stack.stackName}`);
2379
2559
  info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2380
2560
  }
2561
+ for (const stack of plan.accountStacks) {
2562
+ info(` ${stack.action}: ${stack.stackName}`);
2563
+ info(` Account: ${stack.accountId} (OIDC provider)`);
2564
+ }
2381
2565
  newline();
2382
- info("Phase 3: Stage Stacks");
2566
+ info("Phase 3: Stage Stacks (parallel)");
2383
2567
  for (const stack of plan.stageStacks) {
2384
2568
  info(` ${stack.action}: ${stack.stackName}`);
2385
2569
  info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2386
2570
  info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2387
2571
  }
2388
- const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
2572
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2389
2573
  newline();
2390
2574
  info(`Total stacks to deploy: ${totalStacks}`);
2391
2575
  }
2392
2576
  async function confirmDeploymentPlan(plan) {
2393
- const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
2577
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2394
2578
  newline();
2395
2579
  info(`About to deploy ${totalStacks} stack(s):`);
2396
2580
  info(` - 1 Org stack (${plan.orgStack.action})`);
2397
2581
  info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
2582
+ info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
2398
2583
  info(` - ${plan.stageStacks.length} Stage stack(s)`);
2399
2584
  return confirmDeployment({
2400
2585
  orgSlug: plan.orgSlug,
2401
2586
  stacks: [
2402
2587
  { ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
2403
2588
  ...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
2589
+ ...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
2404
2590
  ...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
2405
2591
  ]
2406
2592
  });
@@ -2419,28 +2605,75 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2419
2605
  throw error2;
2420
2606
  }
2421
2607
  newline();
2422
- header("Phase 2: Pipeline Stacks");
2423
- for (const stack of plan.pipelineStacks) {
2424
- const spinner = ora(`Deploying ${stack.stackName}...`).start();
2608
+ header("Phase 2: Pipeline & Account Stacks (parallel)");
2609
+ const totalPhase2Stacks = plan.pipelineStacks.length + plan.accountStacks.length;
2610
+ info(`Deploying ${totalPhase2Stacks} stack(s) in parallel...`);
2611
+ newline();
2612
+ const pipelinePromises = plan.pipelineStacks.map(async (stack) => {
2425
2613
  try {
2426
2614
  await deployPipelineStack(stack, authData, currentAccountId, options);
2427
- spinner.succeed(`${stack.stackName} deployed`);
2428
- results.success++;
2615
+ return { stack: stack.stackName, success: true };
2429
2616
  } catch (error2) {
2430
- spinner.fail(`${stack.stackName} failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
2617
+ return {
2618
+ stack: stack.stackName,
2619
+ success: false,
2620
+ error: error2 instanceof Error ? error2.message : String(error2)
2621
+ };
2622
+ }
2623
+ });
2624
+ const accountPromises = plan.accountStacks.map(async (stack) => {
2625
+ try {
2626
+ await deployAccountStack(stack, currentAccountId, options);
2627
+ return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
2628
+ } catch (error2) {
2629
+ return {
2630
+ stack: `${stack.stackName} (${stack.accountId})`,
2631
+ success: false,
2632
+ error: error2 instanceof Error ? error2.message : String(error2)
2633
+ };
2634
+ }
2635
+ });
2636
+ const pipelineResults = await Promise.all(pipelinePromises);
2637
+ const accountResults = await Promise.all(accountPromises);
2638
+ newline();
2639
+ let accountStacksFailed = false;
2640
+ for (const result of [...pipelineResults, ...accountResults]) {
2641
+ if (result.success) {
2642
+ success(`${result.stack} deployed`);
2643
+ results.success++;
2644
+ } else {
2645
+ error(`${result.stack} failed: ${result.error}`);
2431
2646
  results.failed++;
2432
2647
  }
2433
2648
  }
2649
+ accountStacksFailed = accountResults.some((r) => !r.success);
2650
+ if (accountStacksFailed) {
2651
+ warn("Some Account stacks failed. Stage stacks may fail if their account OIDC provider did not deploy.");
2652
+ }
2434
2653
  newline();
2435
- header("Phase 3: Stage Stacks");
2436
- for (const stack of plan.stageStacks) {
2437
- const spinner = ora(`Deploying ${stack.stackName} to ${stack.accountId}/${stack.region}...`).start();
2654
+ header("Phase 3: Stage Stacks (parallel)");
2655
+ info(`Deploying ${plan.stageStacks.length} stack(s) in parallel...`);
2656
+ newline();
2657
+ const stagePromises = plan.stageStacks.map(async (stack) => {
2438
2658
  try {
2439
2659
  await deployStageStack(stack, authData, currentAccountId, options);
2440
- spinner.succeed(`${stack.stackName} deployed to ${stack.accountId}`);
2441
- results.success++;
2660
+ return { stack: stack.stackName, success: true };
2442
2661
  } catch (error2) {
2443
- spinner.fail(`${stack.stackName} failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
2662
+ return {
2663
+ stack: stack.stackName,
2664
+ success: false,
2665
+ error: error2 instanceof Error ? error2.message : String(error2)
2666
+ };
2667
+ }
2668
+ });
2669
+ const phase3Results = await Promise.all(stagePromises);
2670
+ newline();
2671
+ for (const result of phase3Results) {
2672
+ if (result.success) {
2673
+ success(`${result.stack} deployed`);
2674
+ results.success++;
2675
+ } else {
2676
+ error(`${result.stack} failed: ${result.error}`);
2444
2677
  results.failed++;
2445
2678
  }
2446
2679
  }
@@ -2512,7 +2745,28 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
2512
2745
  template,
2513
2746
  accountId: cicdAccountId,
2514
2747
  region: cicdRegion,
2515
- credentials
2748
+ credentials,
2749
+ showProgress: false
2750
+ // Disable progress bar for parallel deployment
2751
+ };
2752
+ await previewStackChanges(deployOptions);
2753
+ await deployStack(deployOptions);
2754
+ }
2755
+ async function deployAccountStack(stack, currentAccountId, options) {
2756
+ const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
2757
+ targetAccountId: stack.accountId,
2758
+ currentAccountId,
2759
+ targetRoleName: options.targetAccountRoleName
2760
+ }))?.credentials : void 0;
2761
+ const template = generateAccountStackTemplate();
2762
+ const deployOptions = {
2763
+ stackName: stack.stackName,
2764
+ template,
2765
+ accountId: stack.accountId,
2766
+ region: stack.region,
2767
+ credentials,
2768
+ showProgress: false
2769
+ // Disable progress bar for parallel deployment
2516
2770
  };
2517
2771
  await previewStackChanges(deployOptions);
2518
2772
  await deployStack(deployOptions);
@@ -2523,8 +2777,6 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2523
2777
  currentAccountId,
2524
2778
  targetRoleName: options.targetAccountRoleName
2525
2779
  }))?.credentials : void 0;
2526
- const oidcInfo = await checkOidcProviderExists(credentials, stack.region);
2527
- verbose(`OIDC provider in ${stack.accountId}: ${oidcInfo.exists ? "exists" : "will be created"}`);
2528
2780
  const template = generateStageStackTemplate({
2529
2781
  pipelineSlug: stack.pipelineSlug,
2530
2782
  stageName: stack.stageName,
@@ -2540,7 +2792,9 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2540
2792
  template,
2541
2793
  accountId: stack.accountId,
2542
2794
  region: stack.region,
2543
- credentials
2795
+ credentials,
2796
+ showProgress: false
2797
+ // Disable progress bar for parallel deployment
2544
2798
  };
2545
2799
  await previewStackChanges(deployOptions);
2546
2800
  await deployStack(deployOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {