@devramps/cli 0.1.0 → 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 +345 -87
  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";
@@ -279,6 +352,10 @@ async function previewStackChanges(options) {
279
352
  const templateBody = JSON.stringify(template);
280
353
  const stackStatus = await getStackStatus(stackName, credentials, region);
281
354
  const changeSetName = `devramps-preview-${Date.now()}`;
355
+ if (!stackStatus.exists) {
356
+ info(` Stack ${stackName} will be created (new stack)`);
357
+ return;
358
+ }
282
359
  try {
283
360
  await client.send(
284
361
  new CreateChangeSetCommand({
@@ -286,7 +363,7 @@ async function previewStackChanges(options) {
286
363
  ChangeSetName: changeSetName,
287
364
  TemplateBody: templateBody,
288
365
  Capabilities: ["CAPABILITY_NAMED_IAM"],
289
- ChangeSetType: stackStatus.exists ? ChangeSetType.UPDATE : ChangeSetType.CREATE
366
+ ChangeSetType: ChangeSetType.UPDATE
290
367
  })
291
368
  );
292
369
  await waitUntilChangeSetCreateComplete(
@@ -357,21 +434,114 @@ function getActionSymbol(action) {
357
434
  return " ";
358
435
  }
359
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
+ }
360
529
  async function deployStack(options) {
361
- const { stackName, template, accountId, region, credentials } = options;
530
+ const { stackName, template, accountId, region, credentials, showProgress = true } = options;
362
531
  const client = new CloudFormationClient({
363
532
  credentials,
364
533
  region
365
534
  });
366
535
  const templateBody = JSON.stringify(template);
536
+ const resourceCount = Object.keys(template.Resources || {}).length;
367
537
  try {
368
538
  const stackStatus = await getStackStatus(stackName, credentials, region);
369
539
  if (stackStatus.exists) {
370
540
  verbose(`Stack ${stackName} exists, updating...`);
371
- await updateStack(client, stackName, templateBody, accountId);
541
+ await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
372
542
  } else {
373
543
  verbose(`Stack ${stackName} does not exist, creating...`);
374
- await createStack(client, stackName, templateBody, accountId);
544
+ await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
375
545
  }
376
546
  } catch (error2) {
377
547
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
@@ -382,7 +552,8 @@ async function deployStack(options) {
382
552
  throw new CloudFormationError(stackName, accountId, errorMessage);
383
553
  }
384
554
  }
385
- async function createStack(client, stackName, templateBody, accountId) {
555
+ async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
556
+ const operationStartTime = /* @__PURE__ */ new Date();
386
557
  await client.send(
387
558
  new CreateStackCommand({
388
559
  StackName: stackName,
@@ -394,14 +565,12 @@ async function createStack(client, stackName, templateBody, accountId) {
394
565
  ]
395
566
  })
396
567
  );
397
- verbose(`Waiting for stack ${stackName} to be created...`);
398
- await waitUntilStackCreateComplete(
399
- { client, maxWaitTime: 600 },
400
- { StackName: stackName }
401
- );
568
+ info(`Creating stack ${stackName}...`);
569
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
402
570
  success(`Stack ${stackName} created successfully in account ${accountId}`);
403
571
  }
404
- async function updateStack(client, stackName, templateBody, accountId) {
572
+ async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
573
+ const operationStartTime = /* @__PURE__ */ new Date();
405
574
  await client.send(
406
575
  new UpdateStackCommand({
407
576
  StackName: stackName,
@@ -409,11 +578,8 @@ async function updateStack(client, stackName, templateBody, accountId) {
409
578
  Capabilities: ["CAPABILITY_NAMED_IAM"]
410
579
  })
411
580
  );
412
- verbose(`Waiting for stack ${stackName} to be updated...`);
413
- await waitUntilStackUpdateComplete(
414
- { client, maxWaitTime: 600 },
415
- { StackName: stackName }
416
- );
581
+ info(`Updating stack ${stackName}...`);
582
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
417
583
  success(`Stack ${stackName} updated successfully in account ${accountId}`);
418
584
  }
419
585
  async function readExistingStack(stackName, accountId, region, credentials) {
@@ -469,49 +635,6 @@ async function readExistingStack(stackName, accountId, region, credentials) {
469
635
  }
470
636
  }
471
637
 
472
- // src/aws/oidc-provider.ts
473
- import {
474
- IAMClient,
475
- GetOpenIDConnectProviderCommand,
476
- ListOpenIDConnectProvidersCommand
477
- } from "@aws-sdk/client-iam";
478
- async function checkOidcProviderExists(credentials, region) {
479
- const client = new IAMClient({
480
- credentials,
481
- region
482
- });
483
- try {
484
- const response = await client.send(new ListOpenIDConnectProvidersCommand({}));
485
- const providers = response.OpenIDConnectProviderList || [];
486
- for (const provider of providers) {
487
- if (!provider.Arn) continue;
488
- try {
489
- const providerDetails = await client.send(
490
- new GetOpenIDConnectProviderCommand({
491
- OpenIDConnectProviderArn: provider.Arn
492
- })
493
- );
494
- if (providerDetails.Url?.includes(OIDC_PROVIDER_URL)) {
495
- verbose(`Found existing OIDC provider: ${provider.Arn}`);
496
- return {
497
- exists: true,
498
- arn: provider.Arn
499
- };
500
- }
501
- } catch {
502
- }
503
- }
504
- verbose(`No existing OIDC provider found for ${OIDC_PROVIDER_URL}`);
505
- return { exists: false };
506
- } catch (error2) {
507
- verbose(`Error checking OIDC providers: ${error2 instanceof Error ? error2.message : String(error2)}`);
508
- return { exists: false };
509
- }
510
- }
511
- function getOidcThumbprint() {
512
- return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
513
- }
514
-
515
638
  // src/auth/browser-auth.ts
516
639
  import express from "express";
517
640
  import open from "open";
@@ -1150,6 +1273,16 @@ function getArtifactId(artifact) {
1150
1273
  return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1151
1274
  }
1152
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
+
1153
1286
  // src/templates/common.ts
1154
1287
  var STANDARD_TAGS = [
1155
1288
  { Key: "CreatedBy", Value: "DevRamps" },
@@ -1362,6 +1495,9 @@ function getPipelineStackName(pipelineSlug) {
1362
1495
  function getStageStackName(pipelineSlug, stageName) {
1363
1496
  return truncateName(`DevRamps-${pipelineSlug}-${stageName}-Stage`, CF_STACK_MAX_LENGTH);
1364
1497
  }
1498
+ function getAccountStackName() {
1499
+ return "DevRamps-Account-Bootstrap";
1500
+ }
1365
1501
  function getKmsKeyAlias(orgSlug) {
1366
1502
  return `alias/devramps-${normalizeName(orgSlug)}`;
1367
1503
  }
@@ -1817,6 +1953,21 @@ function generatePipelineStackTemplate(options) {
1817
1953
  return template;
1818
1954
  }
1819
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
+
1820
1971
  // src/permissions/eks-deploy.ts
1821
1972
  var EKS_DEPLOY_PERMISSIONS = {
1822
1973
  actions: [
@@ -2004,7 +2155,6 @@ function generateStageStackTemplate(options) {
2004
2155
  const template = createBaseTemplate(
2005
2156
  `DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
2006
2157
  );
2007
- addOidcProviderResource(template, true);
2008
2158
  const roleName = generateStageRoleName(pipelineSlug, stageName);
2009
2159
  const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, stageName);
2010
2160
  const policies = buildStagePolicies(steps, additionalPolicies);
@@ -2050,6 +2200,7 @@ function generateStageStackTemplate(options) {
2050
2200
  );
2051
2201
  s3Outputs[artifact.name] = { resourceId };
2052
2202
  }
2203
+ const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${OIDC_PROVIDER_URL}`;
2053
2204
  template.Outputs = {
2054
2205
  StageRoleArn: {
2055
2206
  Description: "ARN of the stage deployment role",
@@ -2061,8 +2212,8 @@ function generateStageStackTemplate(options) {
2061
2212
  Value: { Ref: "StageDeploymentRole" }
2062
2213
  },
2063
2214
  OIDCProviderArn: {
2064
- Description: "ARN of the OIDC provider",
2065
- Value: getOidcProviderArn(accountId, true)
2215
+ Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
2216
+ Value: oidcProviderArn
2066
2217
  },
2067
2218
  PipelineSlug: {
2068
2219
  Description: "Pipeline slug",
@@ -2282,7 +2433,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2282
2433
  }
2283
2434
  const orgStackName = getOrgStackName(orgSlug);
2284
2435
  const orgStack = {
2285
- stackType: "Org",
2436
+ stackType: "Org" /* ORG */,
2286
2437
  stackName: orgStackName,
2287
2438
  accountId: cicdAccountId,
2288
2439
  region: cicdRegion,
@@ -2296,7 +2447,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2296
2447
  const filteredArtifacts = filterArtifactsForPipelineStack(artifacts);
2297
2448
  const stackName = getPipelineStackName(pipeline.slug);
2298
2449
  pipelineStacks.push({
2299
- stackType: "Pipeline",
2450
+ stackType: "Pipeline" /* PIPELINE */,
2300
2451
  stackName,
2301
2452
  accountId: cicdAccountId,
2302
2453
  region: cicdRegion,
@@ -2306,6 +2457,38 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2306
2457
  bundleArtifacts: filteredArtifacts.bundle
2307
2458
  });
2308
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
+ }
2309
2492
  const stageStacks = [];
2310
2493
  for (const pipeline of pipelines) {
2311
2494
  const artifacts = pipelineArtifacts.get(pipeline.slug);
@@ -2325,7 +2508,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2325
2508
  verbose(`Could not assume role in ${stage.account_id} for status check`);
2326
2509
  }
2327
2510
  stageStacks.push({
2328
- stackType: "Stage",
2511
+ stackType: "Stage" /* STAGE */,
2329
2512
  stackName,
2330
2513
  accountId: stage.account_id,
2331
2514
  region: stage.region,
@@ -2346,6 +2529,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2346
2529
  cicdRegion,
2347
2530
  orgStack,
2348
2531
  pipelineStacks,
2532
+ accountStacks,
2349
2533
  stageStacks
2350
2534
  };
2351
2535
  }
@@ -2369,34 +2553,40 @@ async function showDryRunPlan(plan) {
2369
2553
  info(` Account: ${plan.orgStack.accountId}`);
2370
2554
  info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
2371
2555
  newline();
2372
- info("Phase 2: Pipeline Stacks");
2556
+ info("Phase 2: Pipeline & Account Stacks (parallel)");
2373
2557
  for (const stack of plan.pipelineStacks) {
2374
2558
  info(` ${stack.action}: ${stack.stackName}`);
2375
2559
  info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2376
2560
  }
2561
+ for (const stack of plan.accountStacks) {
2562
+ info(` ${stack.action}: ${stack.stackName}`);
2563
+ info(` Account: ${stack.accountId} (OIDC provider)`);
2564
+ }
2377
2565
  newline();
2378
- info("Phase 3: Stage Stacks");
2566
+ info("Phase 3: Stage Stacks (parallel)");
2379
2567
  for (const stack of plan.stageStacks) {
2380
2568
  info(` ${stack.action}: ${stack.stackName}`);
2381
2569
  info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2382
2570
  info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2383
2571
  }
2384
- const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
2572
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2385
2573
  newline();
2386
2574
  info(`Total stacks to deploy: ${totalStacks}`);
2387
2575
  }
2388
2576
  async function confirmDeploymentPlan(plan) {
2389
- const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
2577
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2390
2578
  newline();
2391
2579
  info(`About to deploy ${totalStacks} stack(s):`);
2392
2580
  info(` - 1 Org stack (${plan.orgStack.action})`);
2393
2581
  info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
2582
+ info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
2394
2583
  info(` - ${plan.stageStacks.length} Stage stack(s)`);
2395
2584
  return confirmDeployment({
2396
2585
  orgSlug: plan.orgSlug,
2397
2586
  stacks: [
2398
2587
  { ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
2399
2588
  ...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
2589
+ ...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
2400
2590
  ...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
2401
2591
  ]
2402
2592
  });
@@ -2415,28 +2605,75 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2415
2605
  throw error2;
2416
2606
  }
2417
2607
  newline();
2418
- header("Phase 2: Pipeline Stacks");
2419
- for (const stack of plan.pipelineStacks) {
2420
- 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) => {
2421
2613
  try {
2422
2614
  await deployPipelineStack(stack, authData, currentAccountId, options);
2423
- spinner.succeed(`${stack.stackName} deployed`);
2424
- results.success++;
2615
+ return { stack: stack.stackName, success: true };
2616
+ } catch (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 };
2425
2628
  } catch (error2) {
2426
- spinner.fail(`${stack.stackName} failed: ${error2 instanceof Error ? error2.message : String(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}`);
2427
2646
  results.failed++;
2428
2647
  }
2429
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
+ }
2430
2653
  newline();
2431
- header("Phase 3: Stage Stacks");
2432
- for (const stack of plan.stageStacks) {
2433
- 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) => {
2434
2658
  try {
2435
2659
  await deployStageStack(stack, authData, currentAccountId, options);
2436
- spinner.succeed(`${stack.stackName} deployed to ${stack.accountId}`);
2437
- results.success++;
2660
+ return { stack: stack.stackName, success: true };
2438
2661
  } catch (error2) {
2439
- 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}`);
2440
2677
  results.failed++;
2441
2678
  }
2442
2679
  }
@@ -2508,7 +2745,28 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
2508
2745
  template,
2509
2746
  accountId: cicdAccountId,
2510
2747
  region: cicdRegion,
2511
- 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
2512
2770
  };
2513
2771
  await previewStackChanges(deployOptions);
2514
2772
  await deployStack(deployOptions);
@@ -2519,8 +2777,6 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2519
2777
  currentAccountId,
2520
2778
  targetRoleName: options.targetAccountRoleName
2521
2779
  }))?.credentials : void 0;
2522
- const oidcInfo = await checkOidcProviderExists(credentials, stack.region);
2523
- verbose(`OIDC provider in ${stack.accountId}: ${oidcInfo.exists ? "exists" : "will be created"}`);
2524
2780
  const template = generateStageStackTemplate({
2525
2781
  pipelineSlug: stack.pipelineSlug,
2526
2782
  stageName: stack.stageName,
@@ -2536,7 +2792,9 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2536
2792
  template,
2537
2793
  accountId: stack.accountId,
2538
2794
  region: stack.region,
2539
- credentials
2795
+ credentials,
2796
+ showProgress: false
2797
+ // Disable progress bar for parallel deployment
2540
2798
  };
2541
2799
  await previewStackChanges(deployOptions);
2542
2800
  await deployStack(deployOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.0",
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": {