@devramps/cli 0.1.30 → 0.1.32

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 +104 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { program } from "commander";
5
5
 
6
6
  // src/commands/bootstrap.ts
7
+ import { createHash as createHash2 } from "crypto";
7
8
  import ora from "ora";
8
9
 
9
10
  // src/aws/credentials.ts
@@ -1008,8 +1009,11 @@ async function authenticateViaBrowser(options = {}) {
1008
1009
  verbose(`CI/CD Account: ${cicdAccountId}, Region: ${awsConfig.defaultRegion}`);
1009
1010
  return {
1010
1011
  orgSlug: orgResponse.slug,
1012
+ organizationId: tokenResponse.organization_id,
1011
1013
  cicdAccountId,
1012
- cicdRegion: awsConfig.defaultRegion
1014
+ cicdRegion: awsConfig.defaultRegion,
1015
+ accessToken: tokenResponse.access_token,
1016
+ apiBaseUrl: baseUrl
1013
1017
  };
1014
1018
  } finally {
1015
1019
  process.removeListener("SIGINT", sigintHandler);
@@ -1378,15 +1382,30 @@ async function parsePipeline(basePath, slug) {
1378
1382
  throw new PipelineParseError(slug, `Stage "${stage.name}" is missing region`);
1379
1383
  }
1380
1384
  }
1385
+ if (definition.pipeline.ephemeral_environments) {
1386
+ for (const [name, env] of Object.entries(definition.pipeline.ephemeral_environments)) {
1387
+ if (!env.account_id) {
1388
+ throw new PipelineParseError(slug, `Ephemeral environment "${name}" is missing account_id`);
1389
+ }
1390
+ if (!env.region) {
1391
+ throw new PipelineParseError(slug, `Ephemeral environment "${name}" is missing region`);
1392
+ }
1393
+ }
1394
+ }
1381
1395
  const targetAccountIds = extractTargetAccountIds(definition);
1382
1396
  const steps = extractSteps(definition);
1383
1397
  const additionalPolicies = await parseAdditionalPoliciesForPipeline(basePath, slug);
1384
- verbose(`Pipeline ${slug}: ${targetAccountIds.length} accounts, ${steps.length} steps`);
1398
+ const ephemeralStages = ephemeralEnvironmentsAsStages(definition);
1399
+ const allStages = [...definition.pipeline.stages, ...ephemeralStages];
1400
+ if (ephemeralStages.length > 0) {
1401
+ verbose(`Pipeline ${slug}: ${ephemeralStages.length} ephemeral environment(s) will be bootstrapped as stages`);
1402
+ }
1403
+ verbose(`Pipeline ${slug}: ${targetAccountIds.length} accounts, ${allStages.length} stages, ${steps.length} steps`);
1385
1404
  return {
1386
1405
  slug,
1387
1406
  definition,
1388
1407
  targetAccountIds,
1389
- stages: definition.pipeline.stages,
1408
+ stages: allStages,
1390
1409
  steps,
1391
1410
  additionalPolicies
1392
1411
  };
@@ -1398,8 +1417,26 @@ function extractTargetAccountIds(definition) {
1398
1417
  accountIds.add(stage.account_id);
1399
1418
  }
1400
1419
  }
1420
+ if (definition.pipeline.ephemeral_environments) {
1421
+ for (const env of Object.values(definition.pipeline.ephemeral_environments)) {
1422
+ if (env.account_id) {
1423
+ accountIds.add(env.account_id);
1424
+ }
1425
+ }
1426
+ }
1401
1427
  return Array.from(accountIds);
1402
1428
  }
1429
+ function ephemeralEnvironmentsAsStages(definition) {
1430
+ const envs = definition.pipeline.ephemeral_environments;
1431
+ if (!envs) return [];
1432
+ return Object.entries(envs).map(([name, env]) => ({
1433
+ name: `ephemeral-${name}`,
1434
+ account_id: env.account_id,
1435
+ region: env.region,
1436
+ skip: env.skip,
1437
+ vars: env.vars
1438
+ }));
1439
+ }
1403
1440
  function extractSteps(definition) {
1404
1441
  return definition.pipeline.steps || [];
1405
1442
  }
@@ -3365,6 +3402,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
3365
3402
  header("Deployment Summary");
3366
3403
  if (results.failed === 0) {
3367
3404
  success(`All ${results.success} stack(s) deployed successfully!`);
3405
+ await markPipelinesBootstrapped(pipelines, authData);
3368
3406
  process.exit(0);
3369
3407
  } else {
3370
3408
  warn(`${results.success} stack(s) succeeded, ${results.failed} stack(s) failed.`);
@@ -3510,6 +3548,69 @@ async function deployImportStack(stack, currentAccountId, options, oidcProviderU
3510
3548
  await previewStackChanges(deployOptions);
3511
3549
  await deployStack(deployOptions);
3512
3550
  }
3551
+ async function markPipelinesBootstrapped(pipelines, authData) {
3552
+ newline();
3553
+ const spinner = ora("Registering bootstrap status...").start();
3554
+ let pipelineMap;
3555
+ try {
3556
+ const listUrl = `${authData.apiBaseUrl}/api/v1/organizations/${authData.organizationId}/pipelines?limit=100`;
3557
+ const listRes = await fetch(listUrl, {
3558
+ headers: {
3559
+ Authorization: `Bearer ${authData.accessToken}`,
3560
+ Accept: "application/json"
3561
+ }
3562
+ });
3563
+ if (!listRes.ok) {
3564
+ spinner.warn(`Could not register bootstrap status: failed to list pipelines (${listRes.status})`);
3565
+ return;
3566
+ }
3567
+ const listData = await listRes.json();
3568
+ pipelineMap = new Map(listData.pipelines.map((p) => [p.slug, p.id]));
3569
+ } catch (error2) {
3570
+ spinner.warn(`Could not register bootstrap status: ${error2 instanceof Error ? error2.message : String(error2)}`);
3571
+ return;
3572
+ }
3573
+ let succeeded = 0;
3574
+ let failed = 0;
3575
+ for (const pipeline of pipelines) {
3576
+ const pipelineId = pipelineMap.get(pipeline.slug);
3577
+ if (!pipelineId) {
3578
+ verbose(`Pipeline ${pipeline.slug} not found in backend \u2014 skipping mark-bootstrapped`);
3579
+ failed++;
3580
+ continue;
3581
+ }
3582
+ const definitionHash = createHash2("sha256").update(JSON.stringify(pipeline.definition)).digest("hex");
3583
+ try {
3584
+ const url = `${authData.apiBaseUrl}/api/v1/organizations/${authData.organizationId}/pipelines/${pipelineId}/mark-bootstrapped`;
3585
+ const res = await fetch(url, {
3586
+ method: "POST",
3587
+ headers: {
3588
+ Authorization: `Bearer ${authData.accessToken}`,
3589
+ "Content-Type": "application/json",
3590
+ Accept: "application/json"
3591
+ },
3592
+ body: JSON.stringify({ definitionHash })
3593
+ });
3594
+ if (res.ok) {
3595
+ verbose(`Marked ${pipeline.slug} as bootstrapped (hash: ${definitionHash.slice(0, 12)}...)`);
3596
+ succeeded++;
3597
+ } else {
3598
+ verbose(`Failed to mark ${pipeline.slug} as bootstrapped: ${res.status}`);
3599
+ failed++;
3600
+ }
3601
+ } catch (error2) {
3602
+ verbose(`Error marking ${pipeline.slug} as bootstrapped: ${error2 instanceof Error ? error2.message : String(error2)}`);
3603
+ failed++;
3604
+ }
3605
+ }
3606
+ if (failed === 0) {
3607
+ spinner.succeed(`Bootstrap status registered for ${succeeded} pipeline(s)`);
3608
+ } else if (succeeded > 0) {
3609
+ spinner.warn(`Bootstrap status registered for ${succeeded} pipeline(s), ${failed} failed (non-fatal)`);
3610
+ } else {
3611
+ spinner.warn("Could not register bootstrap status (non-fatal)");
3612
+ }
3613
+ }
3513
3614
 
3514
3615
  // src/index.ts
3515
3616
  program.name("devramps").description("DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines").version("0.1.0");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {