@devramps/cli 0.1.1 → 0.1.3

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 +434 -116
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -74,6 +74,113 @@ 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
+ completed = 0;
79
+ total = 0;
80
+ label;
81
+ barWidth = 30;
82
+ lastLineCount = 0;
83
+ inProgressResources = /* @__PURE__ */ new Map();
84
+ isTTY;
85
+ constructor(label, total) {
86
+ this.label = label;
87
+ this.total = total;
88
+ this.isTTY = process.stdout.isTTY ?? false;
89
+ this.render();
90
+ }
91
+ /**
92
+ * Update progress with resource status
93
+ */
94
+ update(completed, resourceStatus) {
95
+ this.completed = completed;
96
+ if (resourceStatus) {
97
+ if (resourceStatus.status === "in_progress") {
98
+ this.inProgressResources.set(resourceStatus.logicalId, resourceStatus);
99
+ } else {
100
+ this.inProgressResources.delete(resourceStatus.logicalId);
101
+ }
102
+ }
103
+ this.render();
104
+ }
105
+ /**
106
+ * Set a resource as in-progress
107
+ */
108
+ setInProgress(logicalId, resourceType) {
109
+ this.inProgressResources.set(logicalId, {
110
+ logicalId,
111
+ resourceType,
112
+ status: "in_progress"
113
+ });
114
+ this.render();
115
+ }
116
+ /**
117
+ * Mark a resource as complete (removes from in-progress)
118
+ */
119
+ setComplete(logicalId) {
120
+ this.inProgressResources.delete(logicalId);
121
+ this.render();
122
+ }
123
+ /**
124
+ * Clear the progress bar from the terminal (only for TTY)
125
+ */
126
+ clear() {
127
+ if (!this.isTTY) return;
128
+ for (let i = 0; i < this.lastLineCount; i++) {
129
+ process.stdout.write("\x1B[A\x1B[2K");
130
+ }
131
+ this.lastLineCount = 0;
132
+ }
133
+ /**
134
+ * Finish the progress bar (don't clear - leave final state visible)
135
+ */
136
+ finish() {
137
+ this.clear();
138
+ this.inProgressResources.clear();
139
+ const percentage = this.total > 0 ? this.completed / this.total : 0;
140
+ const filled = Math.round(this.barWidth * percentage);
141
+ const empty = this.barWidth - filled;
142
+ const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
143
+ const count = chalk.cyan(`${this.completed}/${this.total}`);
144
+ const labelText = chalk.bold(this.label);
145
+ console.log(`${labelText} ${bar} ${count} resources`);
146
+ }
147
+ /**
148
+ * Render the progress bar with in-progress resources
149
+ */
150
+ render() {
151
+ this.clear();
152
+ const lines = [];
153
+ const percentage = this.total > 0 ? this.completed / this.total : 0;
154
+ const filled = Math.round(this.barWidth * percentage);
155
+ const empty = this.barWidth - filled;
156
+ const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
157
+ const count = chalk.cyan(`${this.completed}/${this.total}`);
158
+ const labelText = chalk.bold(this.label);
159
+ lines.push(`${labelText} ${bar} ${count} resources`);
160
+ const inProgress = Array.from(this.inProgressResources.values()).slice(0, 8);
161
+ if (inProgress.length > 0) {
162
+ for (const resource of inProgress) {
163
+ const spinner = chalk.yellow("\u22EF");
164
+ const type = chalk.gray(resource.resourceType);
165
+ lines.push(` ${spinner} ${resource.logicalId} ${type}`);
166
+ }
167
+ const remaining = this.inProgressResources.size - inProgress.length;
168
+ if (remaining > 0) {
169
+ lines.push(chalk.gray(` ... and ${remaining} more in progress`));
170
+ }
171
+ }
172
+ if (this.isTTY) {
173
+ for (const line of lines) {
174
+ process.stdout.write(line + "\n");
175
+ }
176
+ this.lastLineCount = lines.length;
177
+ } else {
178
+ if (this.completed === 0 || this.completed === this.total) {
179
+ console.log(lines[0]);
180
+ }
181
+ }
182
+ }
183
+ };
77
184
  function setVerbose(enabled) {
78
185
  verboseMode = enabled;
79
186
  }
@@ -134,6 +241,18 @@ function table(rows) {
134
241
  function newline() {
135
242
  console.log();
136
243
  }
244
+ function resourceProgress(stackName, resourceId, resourceType, status, reason) {
245
+ const stackLabel = chalk.cyan(`[${stackName}]`);
246
+ const typeLabel = chalk.gray(resourceType);
247
+ if (status === "in_progress") {
248
+ console.log(`${stackLabel} ${chalk.yellow("\u22EF")} ${resourceId} ${typeLabel}`);
249
+ } else if (status === "complete") {
250
+ console.log(`${stackLabel} ${chalk.green("\u2714")} ${resourceId} ${typeLabel}`);
251
+ } else if (status === "failed") {
252
+ const reasonText = reason ? ` - ${reason}` : "";
253
+ console.log(`${stackLabel} ${chalk.red("\u2716")} ${resourceId} ${typeLabel}${reasonText}`);
254
+ }
255
+ }
137
256
 
138
257
  // src/aws/credentials.ts
139
258
  var DEFAULT_REGION = "us-east-1";
@@ -234,13 +353,12 @@ import {
234
353
  CloudFormationClient,
235
354
  DescribeStacksCommand,
236
355
  DescribeStackResourcesCommand,
356
+ DescribeStackEventsCommand,
237
357
  CreateStackCommand,
238
358
  UpdateStackCommand,
239
359
  CreateChangeSetCommand,
240
360
  DescribeChangeSetCommand,
241
361
  DeleteChangeSetCommand,
242
- waitUntilStackCreateComplete,
243
- waitUntilStackUpdateComplete,
244
362
  waitUntilChangeSetCreateComplete,
245
363
  ChangeSetType
246
364
  } from "@aws-sdk/client-cloudformation";
@@ -280,7 +398,7 @@ async function previewStackChanges(options) {
280
398
  const stackStatus = await getStackStatus(stackName, credentials, region);
281
399
  const changeSetName = `devramps-preview-${Date.now()}`;
282
400
  if (!stackStatus.exists) {
283
- info(` Stack ${stackName} will be created (new stack)`);
401
+ info(` Stack ${stackName} will be created (new stack) in account ${options.accountId} (${region || "default region"})`);
284
402
  return;
285
403
  }
286
404
  try {
@@ -303,7 +421,7 @@ async function previewStackChanges(options) {
303
421
  ChangeSetName: changeSetName
304
422
  })
305
423
  );
306
- logStackChanges(stackName, changeSetResponse.Changes || [], stackStatus.exists);
424
+ logStackChanges(stackName, changeSetResponse.Changes || [], stackStatus.exists, options.accountId, region);
307
425
  await client.send(
308
426
  new DeleteChangeSetCommand({
309
427
  StackName: stackName,
@@ -328,13 +446,13 @@ async function previewStackChanges(options) {
328
446
  verbose(` Could not preview changes for ${stackName}: ${errorMessage}`);
329
447
  }
330
448
  }
331
- function logStackChanges(stackName, changes, isUpdate) {
449
+ function logStackChanges(stackName, changes, isUpdate, accountId, region) {
332
450
  if (changes.length === 0) {
333
451
  verbose(` Stack ${stackName}: No changes`);
334
452
  return;
335
453
  }
336
454
  const action = isUpdate ? "update" : "create";
337
- info(` Stack ${stackName} will ${action} ${changes.length} resource(s):`);
455
+ info(` Stack ${stackName} will ${action} ${changes.length} resource(s) in account ${accountId} (${region || "default region"}):`);
338
456
  for (const change of changes) {
339
457
  const resourceChange = change.ResourceChange;
340
458
  if (!resourceChange) continue;
@@ -361,21 +479,137 @@ function getActionSymbol(action) {
361
479
  return " ";
362
480
  }
363
481
  }
482
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
483
+ "CREATE_COMPLETE",
484
+ "CREATE_FAILED",
485
+ "DELETE_COMPLETE",
486
+ "DELETE_FAILED",
487
+ "ROLLBACK_COMPLETE",
488
+ "ROLLBACK_FAILED",
489
+ "UPDATE_COMPLETE",
490
+ "UPDATE_FAILED",
491
+ "UPDATE_ROLLBACK_COMPLETE",
492
+ "UPDATE_ROLLBACK_FAILED"
493
+ ]);
494
+ var SUCCESS_STATES = /* @__PURE__ */ new Set([
495
+ "CREATE_COMPLETE",
496
+ "UPDATE_COMPLETE"
497
+ ]);
498
+ function isResourceComplete(status) {
499
+ if (!status) return false;
500
+ return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
501
+ }
502
+ async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600, showProgress = true) {
503
+ const seenEventIds = /* @__PURE__ */ new Set();
504
+ const completedResources = /* @__PURE__ */ new Set();
505
+ const inProgressResources = /* @__PURE__ */ new Map();
506
+ const startTime = Date.now();
507
+ const pollInterval = 3e3;
508
+ const progressBar = showProgress ? new ProgressBar(stackName, totalResources) : null;
509
+ try {
510
+ while (true) {
511
+ if (Date.now() - startTime > maxWaitTime * 1e3) {
512
+ throw new Error(`Stack operation timed out after ${maxWaitTime} seconds`);
513
+ }
514
+ const stackResponse = await client.send(
515
+ new DescribeStacksCommand({ StackName: stackName })
516
+ );
517
+ const stack = stackResponse.Stacks?.[0];
518
+ if (!stack) {
519
+ throw new Error(`Stack ${stackName} not found`);
520
+ }
521
+ const eventsResponse = await client.send(
522
+ new DescribeStackEventsCommand({ StackName: stackName })
523
+ );
524
+ const newEvents = (eventsResponse.StackEvents || []).filter((event) => {
525
+ if (!event.Timestamp || event.Timestamp < operationStartTime) return false;
526
+ if (!event.EventId || seenEventIds.has(event.EventId)) return false;
527
+ return true;
528
+ }).reverse();
529
+ for (const event of newEvents) {
530
+ if (event.EventId) {
531
+ seenEventIds.add(event.EventId);
532
+ }
533
+ const logicalId = event.LogicalResourceId;
534
+ const resourceType = event.ResourceType || "Unknown";
535
+ const status = event.ResourceStatus || "";
536
+ if (logicalId && logicalId !== stackName) {
537
+ if (status.includes("IN_PROGRESS")) {
538
+ inProgressResources.set(logicalId, { resourceType });
539
+ if (progressBar) {
540
+ const resourceStatus = {
541
+ logicalId,
542
+ resourceType,
543
+ status: "in_progress"
544
+ };
545
+ progressBar.update(completedResources.size, resourceStatus);
546
+ } else {
547
+ resourceProgress(stackName, logicalId, resourceType, "in_progress");
548
+ }
549
+ } else if (isResourceComplete(status)) {
550
+ completedResources.add(logicalId);
551
+ inProgressResources.delete(logicalId);
552
+ if (progressBar) {
553
+ const resourceStatus = {
554
+ logicalId,
555
+ resourceType,
556
+ status: "complete"
557
+ };
558
+ progressBar.update(completedResources.size, resourceStatus);
559
+ } else {
560
+ resourceProgress(stackName, logicalId, resourceType, "complete");
561
+ }
562
+ } else if (status.includes("FAILED") || status.includes("ROLLBACK")) {
563
+ inProgressResources.delete(logicalId);
564
+ if (progressBar) {
565
+ const resourceStatus = {
566
+ logicalId,
567
+ resourceType,
568
+ status: "failed",
569
+ reason: event.ResourceStatusReason
570
+ };
571
+ progressBar.update(completedResources.size, resourceStatus);
572
+ } else {
573
+ resourceProgress(stackName, logicalId, resourceType, "failed", event.ResourceStatusReason);
574
+ }
575
+ }
576
+ }
577
+ }
578
+ const currentStatus = stack.StackStatus || "";
579
+ if (TERMINAL_STATES.has(currentStatus)) {
580
+ if (progressBar) {
581
+ progressBar.finish();
582
+ }
583
+ if (SUCCESS_STATES.has(currentStatus)) {
584
+ return;
585
+ }
586
+ throw new Error(`Stack operation failed with status: ${currentStatus}`);
587
+ }
588
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
589
+ }
590
+ } catch (error2) {
591
+ if (progressBar) {
592
+ progressBar.finish();
593
+ }
594
+ throw error2;
595
+ }
596
+ }
364
597
  async function deployStack(options) {
365
- const { stackName, template, accountId, region, credentials } = options;
598
+ const { stackName, template, accountId, region, credentials, showProgress = true } = options;
366
599
  const client = new CloudFormationClient({
367
600
  credentials,
368
601
  region
369
602
  });
370
603
  const templateBody = JSON.stringify(template);
604
+ const resourceCount = Object.keys(template.Resources || {}).length;
371
605
  try {
372
606
  const stackStatus = await getStackStatus(stackName, credentials, region);
373
607
  if (stackStatus.exists) {
374
608
  verbose(`Stack ${stackName} exists, updating...`);
375
- await updateStack(client, stackName, templateBody, accountId);
609
+ await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
376
610
  } else {
377
611
  verbose(`Stack ${stackName} does not exist, creating...`);
378
- await createStack(client, stackName, templateBody, accountId);
612
+ await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
379
613
  }
380
614
  } catch (error2) {
381
615
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
@@ -386,7 +620,8 @@ async function deployStack(options) {
386
620
  throw new CloudFormationError(stackName, accountId, errorMessage);
387
621
  }
388
622
  }
389
- async function createStack(client, stackName, templateBody, accountId) {
623
+ async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
624
+ const operationStartTime = /* @__PURE__ */ new Date();
390
625
  await client.send(
391
626
  new CreateStackCommand({
392
627
  StackName: stackName,
@@ -398,14 +633,12 @@ async function createStack(client, stackName, templateBody, accountId) {
398
633
  ]
399
634
  })
400
635
  );
401
- verbose(`Waiting for stack ${stackName} to be created...`);
402
- await waitUntilStackCreateComplete(
403
- { client, maxWaitTime: 600 },
404
- { StackName: stackName }
405
- );
636
+ info(`Creating stack ${stackName}...`);
637
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
406
638
  success(`Stack ${stackName} created successfully in account ${accountId}`);
407
639
  }
408
- async function updateStack(client, stackName, templateBody, accountId) {
640
+ async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
641
+ const operationStartTime = /* @__PURE__ */ new Date();
409
642
  await client.send(
410
643
  new UpdateStackCommand({
411
644
  StackName: stackName,
@@ -413,11 +646,8 @@ async function updateStack(client, stackName, templateBody, accountId) {
413
646
  Capabilities: ["CAPABILITY_NAMED_IAM"]
414
647
  })
415
648
  );
416
- verbose(`Waiting for stack ${stackName} to be updated...`);
417
- await waitUntilStackUpdateComplete(
418
- { client, maxWaitTime: 600 },
419
- { StackName: stackName }
420
- );
649
+ info(`Updating stack ${stackName}...`);
650
+ await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
421
651
  success(`Stack ${stackName} updated successfully in account ${accountId}`);
422
652
  }
423
653
  async function readExistingStack(stackName, accountId, region, credentials) {
@@ -473,49 +703,6 @@ async function readExistingStack(stackName, accountId, region, credentials) {
473
703
  }
474
704
  }
475
705
 
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
706
  // src/auth/browser-auth.ts
520
707
  import express from "express";
521
708
  import open from "open";
@@ -1154,6 +1341,16 @@ function getArtifactId(artifact) {
1154
1341
  return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1155
1342
  }
1156
1343
 
1344
+ // src/aws/oidc-provider.ts
1345
+ import {
1346
+ IAMClient,
1347
+ GetOpenIDConnectProviderCommand,
1348
+ ListOpenIDConnectProvidersCommand
1349
+ } from "@aws-sdk/client-iam";
1350
+ function getOidcThumbprint() {
1351
+ return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
1352
+ }
1353
+
1157
1354
  // src/templates/common.ts
1158
1355
  var STANDARD_TAGS = [
1159
1356
  { Key: "CreatedBy", Value: "DevRamps" },
@@ -1366,6 +1563,9 @@ function getPipelineStackName(pipelineSlug) {
1366
1563
  function getStageStackName(pipelineSlug, stageName) {
1367
1564
  return truncateName(`DevRamps-${pipelineSlug}-${stageName}-Stage`, CF_STACK_MAX_LENGTH);
1368
1565
  }
1566
+ function getAccountStackName() {
1567
+ return "DevRamps-Account-Bootstrap";
1568
+ }
1369
1569
  function getKmsKeyAlias(orgSlug) {
1370
1570
  return `alias/devramps-${normalizeName(orgSlug)}`;
1371
1571
  }
@@ -1821,6 +2021,21 @@ function generatePipelineStackTemplate(options) {
1821
2021
  return template;
1822
2022
  }
1823
2023
 
2024
+ // src/templates/account-stack.ts
2025
+ function generateAccountStackTemplate() {
2026
+ const template = createBaseTemplate(
2027
+ "DevRamps Account Bootstrap Stack - Creates OIDC provider for the account"
2028
+ );
2029
+ addOidcProviderResource(template, false);
2030
+ template.Outputs = {
2031
+ OIDCProviderArn: {
2032
+ Description: "ARN of the OIDC provider",
2033
+ Value: { "Fn::GetAtt": ["DevRampsOIDCProvider", "Arn"] }
2034
+ }
2035
+ };
2036
+ return template;
2037
+ }
2038
+
1824
2039
  // src/permissions/eks-deploy.ts
1825
2040
  var EKS_DEPLOY_PERMISSIONS = {
1826
2041
  actions: [
@@ -2008,7 +2223,6 @@ function generateStageStackTemplate(options) {
2008
2223
  const template = createBaseTemplate(
2009
2224
  `DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
2010
2225
  );
2011
- addOidcProviderResource(template, true);
2012
2226
  const roleName = generateStageRoleName(pipelineSlug, stageName);
2013
2227
  const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, stageName);
2014
2228
  const policies = buildStagePolicies(steps, additionalPolicies);
@@ -2054,6 +2268,7 @@ function generateStageStackTemplate(options) {
2054
2268
  );
2055
2269
  s3Outputs[artifact.name] = { resourceId };
2056
2270
  }
2271
+ const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${OIDC_PROVIDER_URL}`;
2057
2272
  template.Outputs = {
2058
2273
  StageRoleArn: {
2059
2274
  Description: "ARN of the stage deployment role",
@@ -2065,8 +2280,8 @@ function generateStageStackTemplate(options) {
2065
2280
  Value: { Ref: "StageDeploymentRole" }
2066
2281
  },
2067
2282
  OIDCProviderArn: {
2068
- Description: "ARN of the OIDC provider",
2069
- Value: getOidcProviderArn(accountId, true)
2283
+ Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
2284
+ Value: oidcProviderArn
2070
2285
  },
2071
2286
  PipelineSlug: {
2072
2287
  Description: "Pipeline slug",
@@ -2286,7 +2501,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2286
2501
  }
2287
2502
  const orgStackName = getOrgStackName(orgSlug);
2288
2503
  const orgStack = {
2289
- stackType: "Org",
2504
+ stackType: "Org" /* ORG */,
2290
2505
  stackName: orgStackName,
2291
2506
  accountId: cicdAccountId,
2292
2507
  region: cicdRegion,
@@ -2300,7 +2515,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2300
2515
  const filteredArtifacts = filterArtifactsForPipelineStack(artifacts);
2301
2516
  const stackName = getPipelineStackName(pipeline.slug);
2302
2517
  pipelineStacks.push({
2303
- stackType: "Pipeline",
2518
+ stackType: "Pipeline" /* PIPELINE */,
2304
2519
  stackName,
2305
2520
  accountId: cicdAccountId,
2306
2521
  region: cicdRegion,
@@ -2310,6 +2525,38 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2310
2525
  bundleArtifacts: filteredArtifacts.bundle
2311
2526
  });
2312
2527
  }
2528
+ const accountStacks = [];
2529
+ const accountStackName = getAccountStackName();
2530
+ const accountsWithStacks = /* @__PURE__ */ new Set();
2531
+ for (const pipeline of pipelines) {
2532
+ for (const stage of pipeline.stages) {
2533
+ if (accountsWithStacks.has(stage.account_id)) {
2534
+ continue;
2535
+ }
2536
+ accountsWithStacks.add(stage.account_id);
2537
+ let accountCredentials;
2538
+ try {
2539
+ if (stage.account_id !== currentAccountId) {
2540
+ const assumed = await assumeRoleForAccount({
2541
+ targetAccountId: stage.account_id,
2542
+ currentAccountId,
2543
+ targetRoleName
2544
+ });
2545
+ accountCredentials = assumed?.credentials;
2546
+ }
2547
+ } catch {
2548
+ verbose(`Could not assume role in ${stage.account_id} for status check`);
2549
+ }
2550
+ accountStacks.push({
2551
+ stackType: "Account" /* ACCOUNT */,
2552
+ stackName: accountStackName,
2553
+ accountId: stage.account_id,
2554
+ region: cicdRegion,
2555
+ // Deploy in CI/CD region for consistency
2556
+ action: await determineStackAction(accountStackName, accountCredentials, cicdRegion)
2557
+ });
2558
+ }
2559
+ }
2313
2560
  const stageStacks = [];
2314
2561
  for (const pipeline of pipelines) {
2315
2562
  const artifacts = pipelineArtifacts.get(pipeline.slug);
@@ -2329,7 +2576,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2329
2576
  verbose(`Could not assume role in ${stage.account_id} for status check`);
2330
2577
  }
2331
2578
  stageStacks.push({
2332
- stackType: "Stage",
2579
+ stackType: "Stage" /* STAGE */,
2333
2580
  stackName,
2334
2581
  accountId: stage.account_id,
2335
2582
  region: stage.region,
@@ -2350,6 +2597,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2350
2597
  cicdRegion,
2351
2598
  orgStack,
2352
2599
  pipelineStacks,
2600
+ accountStacks,
2353
2601
  stageStacks
2354
2602
  };
2355
2603
  }
@@ -2368,79 +2616,126 @@ async function showDryRunPlan(plan) {
2368
2616
  info(`CI/CD Account: ${plan.cicdAccountId}`);
2369
2617
  info(`CI/CD Region: ${plan.cicdRegion}`);
2370
2618
  newline();
2371
- info("Phase 1: Org Stack");
2619
+ info("Org Stack:");
2372
2620
  info(` ${plan.orgStack.action}: ${plan.orgStack.stackName}`);
2373
- info(` Account: ${plan.orgStack.accountId}`);
2621
+ info(` Account: ${plan.orgStack.accountId}, Region: ${plan.orgStack.region}`);
2374
2622
  info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
2375
- newline();
2376
- info("Phase 2: Pipeline Stacks");
2377
- for (const stack of plan.pipelineStacks) {
2378
- info(` ${stack.action}: ${stack.stackName}`);
2379
- info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2623
+ if (plan.pipelineStacks.length > 0) {
2624
+ newline();
2625
+ info("Pipeline Stacks:");
2626
+ for (const stack of plan.pipelineStacks) {
2627
+ info(` ${stack.action}: ${stack.stackName}`);
2628
+ info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2629
+ info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2630
+ }
2380
2631
  }
2381
- newline();
2382
- info("Phase 3: Stage Stacks");
2383
- for (const stack of plan.stageStacks) {
2384
- info(` ${stack.action}: ${stack.stackName}`);
2385
- info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2386
- info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2632
+ if (plan.accountStacks.length > 0) {
2633
+ newline();
2634
+ info("Account Stacks:");
2635
+ for (const stack of plan.accountStacks) {
2636
+ info(` ${stack.action}: ${stack.stackName}`);
2637
+ info(` Account: ${stack.accountId}, Region: ${stack.region} (OIDC provider)`);
2638
+ }
2639
+ }
2640
+ if (plan.stageStacks.length > 0) {
2641
+ newline();
2642
+ info("Stage Stacks:");
2643
+ for (const stack of plan.stageStacks) {
2644
+ info(` ${stack.action}: ${stack.stackName}`);
2645
+ info(` Account: ${stack.accountId}, Region: ${stack.region}`);
2646
+ info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2647
+ }
2387
2648
  }
2388
- const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
2649
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2389
2650
  newline();
2390
- info(`Total stacks to deploy: ${totalStacks}`);
2651
+ info(`Total stacks to deploy (in parallel): ${totalStacks}`);
2391
2652
  }
2392
2653
  async function confirmDeploymentPlan(plan) {
2393
- const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
2654
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2394
2655
  newline();
2395
2656
  info(`About to deploy ${totalStacks} stack(s):`);
2396
2657
  info(` - 1 Org stack (${plan.orgStack.action})`);
2397
2658
  info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
2659
+ info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
2398
2660
  info(` - ${plan.stageStacks.length} Stage stack(s)`);
2399
2661
  return confirmDeployment({
2400
2662
  orgSlug: plan.orgSlug,
2401
2663
  stacks: [
2402
2664
  { ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
2403
2665
  ...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
2666
+ ...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
2404
2667
  ...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
2405
2668
  ]
2406
2669
  });
2407
2670
  }
2408
2671
  async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options) {
2409
2672
  const results = { success: 0, failed: 0 };
2673
+ const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2410
2674
  newline();
2411
- header("Phase 1: Org Stack");
2412
- try {
2413
- await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
2414
- results.success++;
2415
- success("Org stack deployed successfully");
2416
- } catch (error2) {
2417
- results.failed++;
2418
- error(`Org stack failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
2419
- throw error2;
2420
- }
2675
+ header("Deploying All Stacks");
2676
+ info(`Deploying ${totalStacks} stack(s) in parallel...`);
2421
2677
  newline();
2422
- header("Phase 2: Pipeline Stacks");
2423
- for (const stack of plan.pipelineStacks) {
2424
- const spinner = ora(`Deploying ${stack.stackName}...`).start();
2678
+ const orgPromise = (async () => {
2679
+ try {
2680
+ await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
2681
+ return { stack: plan.orgStack.stackName, success: true };
2682
+ } catch (error2) {
2683
+ return {
2684
+ stack: plan.orgStack.stackName,
2685
+ success: false,
2686
+ error: error2 instanceof Error ? error2.message : String(error2)
2687
+ };
2688
+ }
2689
+ })();
2690
+ const pipelinePromises = plan.pipelineStacks.map(async (stack) => {
2425
2691
  try {
2426
2692
  await deployPipelineStack(stack, authData, currentAccountId, options);
2427
- spinner.succeed(`${stack.stackName} deployed`);
2428
- results.success++;
2693
+ return { stack: stack.stackName, success: true };
2429
2694
  } catch (error2) {
2430
- spinner.fail(`${stack.stackName} failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
2431
- results.failed++;
2695
+ return {
2696
+ stack: stack.stackName,
2697
+ success: false,
2698
+ error: error2 instanceof Error ? error2.message : String(error2)
2699
+ };
2432
2700
  }
2433
- }
2434
- 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();
2701
+ });
2702
+ const accountPromises = plan.accountStacks.map(async (stack) => {
2703
+ try {
2704
+ await deployAccountStack(stack, currentAccountId, options);
2705
+ return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
2706
+ } catch (error2) {
2707
+ return {
2708
+ stack: `${stack.stackName} (${stack.accountId})`,
2709
+ success: false,
2710
+ error: error2 instanceof Error ? error2.message : String(error2)
2711
+ };
2712
+ }
2713
+ });
2714
+ const stagePromises = plan.stageStacks.map(async (stack) => {
2438
2715
  try {
2439
2716
  await deployStageStack(stack, authData, currentAccountId, options);
2440
- spinner.succeed(`${stack.stackName} deployed to ${stack.accountId}`);
2441
- results.success++;
2717
+ return { stack: stack.stackName, success: true };
2442
2718
  } catch (error2) {
2443
- spinner.fail(`${stack.stackName} failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
2719
+ return {
2720
+ stack: stack.stackName,
2721
+ success: false,
2722
+ error: error2 instanceof Error ? error2.message : String(error2)
2723
+ };
2724
+ }
2725
+ });
2726
+ const allResults = await Promise.all([
2727
+ orgPromise,
2728
+ ...pipelinePromises,
2729
+ ...accountPromises,
2730
+ ...stagePromises
2731
+ ]);
2732
+ newline();
2733
+ for (const result of allResults) {
2734
+ if (result.success) {
2735
+ success(`${result.stack} deployed`);
2736
+ results.success++;
2737
+ } else {
2738
+ error(`${result.stack} failed: ${result.error}`);
2444
2739
  results.failed++;
2445
2740
  }
2446
2741
  }
@@ -2489,7 +2784,9 @@ async function deployOrgStack(plan, pipelines, authData, currentAccountId, optio
2489
2784
  template,
2490
2785
  accountId: cicdAccountId,
2491
2786
  region: cicdRegion,
2492
- credentials
2787
+ credentials,
2788
+ showProgress: false
2789
+ // Disable progress bar for parallel deployment
2493
2790
  };
2494
2791
  await previewStackChanges(deployOptions);
2495
2792
  await deployStack(deployOptions);
@@ -2512,7 +2809,28 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
2512
2809
  template,
2513
2810
  accountId: cicdAccountId,
2514
2811
  region: cicdRegion,
2515
- credentials
2812
+ credentials,
2813
+ showProgress: false
2814
+ // Disable progress bar for parallel deployment
2815
+ };
2816
+ await previewStackChanges(deployOptions);
2817
+ await deployStack(deployOptions);
2818
+ }
2819
+ async function deployAccountStack(stack, currentAccountId, options) {
2820
+ const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
2821
+ targetAccountId: stack.accountId,
2822
+ currentAccountId,
2823
+ targetRoleName: options.targetAccountRoleName
2824
+ }))?.credentials : void 0;
2825
+ const template = generateAccountStackTemplate();
2826
+ const deployOptions = {
2827
+ stackName: stack.stackName,
2828
+ template,
2829
+ accountId: stack.accountId,
2830
+ region: stack.region,
2831
+ credentials,
2832
+ showProgress: false
2833
+ // Disable progress bar for parallel deployment
2516
2834
  };
2517
2835
  await previewStackChanges(deployOptions);
2518
2836
  await deployStack(deployOptions);
@@ -2523,8 +2841,6 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2523
2841
  currentAccountId,
2524
2842
  targetRoleName: options.targetAccountRoleName
2525
2843
  }))?.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
2844
  const template = generateStageStackTemplate({
2529
2845
  pipelineSlug: stack.pipelineSlug,
2530
2846
  stageName: stack.stageName,
@@ -2540,7 +2856,9 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
2540
2856
  template,
2541
2857
  accountId: stack.accountId,
2542
2858
  region: stack.region,
2543
- credentials
2859
+ credentials,
2860
+ showProgress: false
2861
+ // Disable progress bar for parallel deployment
2544
2862
  };
2545
2863
  await previewStackChanges(deployOptions);
2546
2864
  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.3",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {