@pagopa/dx-cli 0.20.1 → 0.21.0

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 (36) hide show
  1. package/README.md +12 -1
  2. package/bin/index.js +0 -20
  3. package/dist/adapters/azure/__tests__/cloud-account-service.test.js +132 -1
  4. package/dist/adapters/azure/cloud-account-service.d.ts +3 -2
  5. package/dist/adapters/azure/cloud-account-service.js +127 -39
  6. package/dist/adapters/commander/__tests__/error-reporting.test.d.ts +1 -0
  7. package/dist/adapters/commander/__tests__/error-reporting.test.js +63 -0
  8. package/dist/adapters/commander/__tests__/exit-with-error.test.d.ts +1 -0
  9. package/dist/adapters/commander/__tests__/exit-with-error.test.js +92 -0
  10. package/dist/adapters/commander/commands/add.d.ts +2 -0
  11. package/dist/adapters/commander/commands/add.js +8 -5
  12. package/dist/adapters/commander/commands/codemod.js +3 -2
  13. package/dist/adapters/commander/commands/init.js +2 -2
  14. package/dist/adapters/commander/commands/savemoney.js +6 -3
  15. package/dist/adapters/commander/error-reporting.d.ts +10 -0
  16. package/dist/adapters/commander/error-reporting.js +68 -0
  17. package/dist/adapters/commander/index.d.ts +17 -1
  18. package/dist/adapters/commander/index.js +23 -2
  19. package/dist/adapters/octokit/index.d.ts +2 -1
  20. package/dist/adapters/octokit/index.js +33 -0
  21. package/dist/adapters/plop/__tests__/run-actions.test.d.ts +1 -0
  22. package/dist/adapters/plop/__tests__/run-actions.test.js +68 -0
  23. package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +24 -7
  24. package/dist/adapters/plop/actions/init-cloud-accounts.d.ts +3 -2
  25. package/dist/adapters/plop/actions/init-cloud-accounts.js +4 -4
  26. package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +6 -1
  27. package/dist/adapters/plop/generators/environment/actions.js +12 -0
  28. package/dist/adapters/plop/generators/environment/index.d.ts +2 -1
  29. package/dist/adapters/plop/generators/environment/index.js +2 -2
  30. package/dist/adapters/plop/index.d.ts +5 -3
  31. package/dist/adapters/plop/index.js +20 -12
  32. package/dist/domain/cloud-account.d.ts +3 -2
  33. package/dist/domain/github.d.ts +13 -0
  34. package/dist/index.js +36 -0
  35. package/package.json +4 -2
  36. package/templates/environment/workflow/_release-terraform-apply-bootstrapper-{{env.name}}.yaml.hbs +19 -0
@@ -15,6 +15,7 @@ import { requestAuthorizationInputSchema, } from "../../../domain/authorization.
15
15
  import { environmentShort } from "../../../domain/environment.js";
16
16
  import { isAzureLocation, locationShort } from "../../azure/locations.js";
17
17
  import { getPlopInstance, runDeploymentEnvironmentGenerator, } from "../../plop/index.js";
18
+ import { exitWithError } from "../index.js";
18
19
  import { checkPreconditions } from "./init.js";
19
20
  /**
20
21
  * Authorize a Cloud Account (Azure Subscription, AWS Account, ...), creating a Pull Request for each account that requires authorization.
@@ -69,9 +70,11 @@ const displaySummary = (result) => {
69
70
  }
70
71
  console.log(`${step}. Visit ${chalk.underline("https://dx.pagopa.it/getting-started")} to deploy your first project\n`);
71
72
  };
72
- const addEnvironmentAction = (authorizationService) => checkPreconditions()
73
- .andThen(() => ResultAsync.fromPromise(getPlopInstance(), () => new Error("Failed to initialize plop")))
74
- .andThen((plop) => ResultAsync.fromPromise(runDeploymentEnvironmentGenerator(plop), () => new Error("Failed to run the deployment environment generator")))
73
+ const addEnvironmentAction = (authorizationService, gitHubService) => checkPreconditions()
74
+ .andThen(() => ResultAsync.fromPromise(getPlopInstance(), (cause) => new Error("Failed to initialize plop", { cause })))
75
+ .andThen((plop) => ResultAsync.fromPromise(runDeploymentEnvironmentGenerator(plop, gitHubService), (cause) => new Error("Failed to run the deployment environment generator", {
76
+ cause,
77
+ })))
75
78
  .andThen((payload) => authorizeCloudAccounts(authorizationService)(payload).map((authorizationPrs) => ({
76
79
  authorizationPrs,
77
80
  })));
@@ -81,9 +84,9 @@ export const makeAddCommand = (deps) => new Command()
81
84
  .addCommand(new Command("environment")
82
85
  .description("Add a new deployment environment")
83
86
  .action(async function () {
84
- const result = await addEnvironmentAction(deps.authorizationService);
87
+ const result = await addEnvironmentAction(deps.authorizationService, deps.gitHubService);
85
88
  if (result.isErr()) {
86
- this.error(result.error.message);
89
+ exitWithError(this)(result.error);
87
90
  }
88
91
  else {
89
92
  displaySummary(result.value);
@@ -1,5 +1,6 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { Command } from "commander";
3
+ import { exitWithError } from "../index.js";
3
4
  export const makeCodemodCommand = ({ applyCodemodById, listCodemods, }) => new Command("codemod")
4
5
  .description("Manage and apply migration scripts to the repository")
5
6
  .addCommand(new Command("list")
@@ -7,7 +8,7 @@ export const makeCodemodCommand = ({ applyCodemodById, listCodemods, }) => new C
7
8
  .action(async function () {
8
9
  await listCodemods()
9
10
  .andTee((codemods) => console.table(codemods, ["id", "description"]))
10
- .orTee((error) => this.error(error.message));
11
+ .orTee(exitWithError(this));
11
12
  }))
12
13
  .addCommand(new Command("apply")
13
14
  .argument("<id>", "The id of the codemod to apply")
@@ -18,5 +19,5 @@ export const makeCodemodCommand = ({ applyCodemodById, listCodemods, }) => new C
18
19
  .andTee(() => {
19
20
  logger.info("Codemod applied ✅");
20
21
  })
21
- .orTee((error) => this.error(error.message));
22
+ .orTee(exitWithError(this));
22
23
  }));
@@ -115,14 +115,14 @@ const handleGeneratorError = (err) => {
115
115
  if (err instanceof Error) {
116
116
  logger.error(err.message);
117
117
  }
118
- return new Error("Failed to run the generator");
118
+ return new Error("Failed to run the generator", { cause: err });
119
119
  };
120
120
  export const makeInitCommand = ({ gitHubService, }) => new Command()
121
121
  .name("init")
122
122
  .description("Initialize a new DX workspace")
123
123
  .action(async function () {
124
124
  await checkPreconditions()
125
- .andThen(() => ResultAsync.fromPromise(getPlopInstance(), () => new Error("Failed to initialize plop")))
125
+ .andThen(() => ResultAsync.fromPromise(getPlopInstance(), (cause) => new Error("Failed to initialize plop", { cause })))
126
126
  .andThen((plop) => ResultAsync.fromPromise(runMonorepoGenerator(plop, gitHubService), handleGeneratorError))
127
127
  .andTee((payload) => {
128
128
  process.chdir(payload.repoName);
@@ -1,14 +1,15 @@
1
1
  import { azure, loadConfig } from "@pagopa/dx-savemoney";
2
2
  import { Command } from "commander";
3
+ import { exitWithError } from "../index.js";
3
4
  export const makeSavemoneyCommand = () => new Command("savemoney")
4
5
  .description("Analyze Azure subscriptions and report unused or inefficient resources")
5
6
  .option("-c, --config <path>", "Path to YAML configuration file")
6
7
  .option("-f, --format <format>", "Report format: json, table, detailed-json, or lint (default: table)", "table")
7
8
  .option("-l, --location <string>", "Preferred Azure location for resources (overrides config file)", "italynorth")
8
9
  .option("-d, --days <number>", "Number of days for metrics analysis (overrides config file)", "30")
9
- .option("-v, --verbose", "Enable verbose logging")
10
10
  .option("-t, --tags <tags...>", "Filter resources by tags (key=value key2=value2). Only resources matching ALL specified tags are analyzed.")
11
11
  .action(async function (options) {
12
+ const { verbose } = this.optsWithGlobals();
12
13
  try {
13
14
  // Load configuration from YAML (includes subscriptionIds, location, timespanDays, thresholds)
14
15
  const config = await loadConfig(options.config);
@@ -19,13 +20,15 @@ export const makeSavemoneyCommand = () => new Command("savemoney")
19
20
  filterTags,
20
21
  preferredLocation: options.location || config.preferredLocation,
21
22
  timespanDays: Number.parseInt(options.days, 10) || config.timespanDays,
22
- verbose: options.verbose || false,
23
+ verbose: verbose ?? false,
23
24
  };
24
25
  // Run analysis
25
26
  await azure.analyzeAzureResources(finalConfig, options.format);
26
27
  }
27
28
  catch (error) {
28
- this.error(`Analysis failed: ${error instanceof Error ? error.message : error}`);
29
+ exitWithError(this)(error instanceof Error
30
+ ? new Error(`Analysis failed: ${error.message}`, { cause: error })
31
+ : new Error(`Analysis failed: ${String(error)}`));
29
32
  }
30
33
  });
31
34
  /**
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Safely converts an unknown value to a human-readable error message.
3
+ * Preserves ExecaError.shortMessage when available and flattens AggregateError.
4
+ */
5
+ export declare const toErrorMessage: (value: unknown) => string;
6
+ /**
7
+ * Builds a detailed, multi-line representation of an error suitable for
8
+ * `--verbose` output: includes the full `cause` chain and stack traces.
9
+ */
10
+ export declare const formatErrorDetailed: (value: unknown) => string;
@@ -0,0 +1,68 @@
1
+ import { ExecaError } from "execa";
2
+ /**
3
+ * Safely converts an unknown value to a human-readable error message.
4
+ * Preserves ExecaError.shortMessage when available and flattens AggregateError.
5
+ */
6
+ export const toErrorMessage = (value) => {
7
+ if (value === null || value === undefined) {
8
+ return "Unknown error";
9
+ }
10
+ if (typeof value === "string") {
11
+ return value;
12
+ }
13
+ if (value instanceof ExecaError) {
14
+ return value.shortMessage || value.message || String(value);
15
+ }
16
+ if (value instanceof AggregateError) {
17
+ const parts = value.errors.map((inner) => toErrorMessage(inner));
18
+ return [value.message, ...parts].filter(Boolean).join("\n - ");
19
+ }
20
+ if (value instanceof Error) {
21
+ return value.message || value.name || "Error";
22
+ }
23
+ if (typeof value === "object") {
24
+ const maybeMessage = value.message;
25
+ if (typeof maybeMessage === "string" && maybeMessage.length > 0) {
26
+ return maybeMessage;
27
+ }
28
+ try {
29
+ return JSON.stringify(value);
30
+ }
31
+ catch {
32
+ return String(value);
33
+ }
34
+ }
35
+ return String(value);
36
+ };
37
+ /**
38
+ * Builds a detailed, multi-line representation of an error suitable for
39
+ * `--verbose` output: includes the full `cause` chain and stack traces.
40
+ */
41
+ export const formatErrorDetailed = (value) => {
42
+ const lines = [];
43
+ const seen = new Set();
44
+ let current = value;
45
+ let depth = 0;
46
+ while (current !== undefined && current !== null && !seen.has(current)) {
47
+ seen.add(current);
48
+ const prefix = depth === 0 ? "" : "Caused by: ";
49
+ if (current instanceof Error) {
50
+ lines.push(`${prefix}${current.name}: ${toErrorMessage(current)}`);
51
+ if (current.stack) {
52
+ // `stack` usually starts with "Name: message"; drop the first line to
53
+ // avoid duplication with the header we just printed.
54
+ const stackBody = current.stack.split("\n").slice(1).join("\n");
55
+ if (stackBody.trim().length > 0) {
56
+ lines.push(stackBody);
57
+ }
58
+ }
59
+ current = current.cause;
60
+ }
61
+ else {
62
+ lines.push(`${prefix}${toErrorMessage(current)}`);
63
+ current = undefined;
64
+ }
65
+ depth += 1;
66
+ }
67
+ return lines.join("\n");
68
+ };
@@ -3,5 +3,21 @@ import { Config } from "../../config.js";
3
3
  import { Dependencies } from "../../domain/dependencies.js";
4
4
  import { CodemodCommandDependencies } from "./commands/codemod.js";
5
5
  export type CliDependencies = CodemodCommandDependencies;
6
+ export type GlobalOptions = {
7
+ verbose?: boolean;
8
+ };
9
+ /**
10
+ * Returns true when the global `--verbose` flag is active on the closest
11
+ * ancestor command that defines it (the root `dx` program in our CLI).
12
+ */
13
+ export declare const isVerbose: (command: Command) => boolean;
6
14
  export declare const makeCli: (deps: Dependencies, config: Config, cliDeps: CliDependencies, version: string) => Command;
7
- export declare const exitWithError: (command: Command) => (error: Error) => never;
15
+ /**
16
+ * Builds a failure handler that ends the command via Commander's
17
+ * `Command#error`, with an output tailored to the active verbosity.
18
+ *
19
+ * - In normal mode, a single meaningful line is printed.
20
+ * - When `--verbose` is active, the full cause chain and stack trace are
21
+ * included so users can diagnose the underlying failure.
22
+ */
23
+ export declare const exitWithError: (command: Command) => (error: unknown) => never;
@@ -5,9 +5,19 @@ import { makeDoctorCommand } from "./commands/doctor.js";
5
5
  import { makeInfoCommand } from "./commands/info.js";
6
6
  import { makeInitCommand } from "./commands/init.js";
7
7
  import { makeSavemoneyCommand } from "./commands/savemoney.js";
8
+ import { formatErrorDetailed, toErrorMessage } from "./error-reporting.js";
9
+ /**
10
+ * Returns true when the global `--verbose` flag is active on the closest
11
+ * ancestor command that defines it (the root `dx` program in our CLI).
12
+ */
13
+ export const isVerbose = (command) => command.optsWithGlobals().verbose === true;
8
14
  export const makeCli = (deps, config, cliDeps, version) => {
9
15
  const program = new Command();
10
- program.name("dx").description("The CLI for DX-Platform").version(version);
16
+ program
17
+ .name("dx")
18
+ .description("The CLI for DX-Platform")
19
+ .version(version)
20
+ .option("-v, --verbose", "Enable verbose output: debug-level logs and full error chain (with stack traces) when a command fails", false);
11
21
  program.addCommand(makeDoctorCommand(deps, config));
12
22
  program.addCommand(makeCodemodCommand(cliDeps));
13
23
  program.addCommand(makeInitCommand(deps));
@@ -16,6 +26,17 @@ export const makeCli = (deps, config, cliDeps, version) => {
16
26
  program.addCommand(makeAddCommand(deps));
17
27
  return program;
18
28
  };
29
+ /**
30
+ * Builds a failure handler that ends the command via Commander's
31
+ * `Command#error`, with an output tailored to the active verbosity.
32
+ *
33
+ * - In normal mode, a single meaningful line is printed.
34
+ * - When `--verbose` is active, the full cause chain and stack trace are
35
+ * included so users can diagnose the underlying failure.
36
+ */
19
37
  export const exitWithError = (command) => (error) => {
20
- command.error(error.message);
38
+ const message = isVerbose(command)
39
+ ? formatErrorDetailed(error)
40
+ : toErrorMessage(error);
41
+ command.error(message);
21
42
  };
@@ -1,6 +1,6 @@
1
1
  import { ResultAsync } from "neverthrow";
2
2
  import { Octokit } from "octokit";
3
- import { CreateBranchParams, FileContent, GetFileContentParams, GitHubService, PullRequest, Repository, UpdateFileParams } from "../../domain/github.js";
3
+ import { CreateBranchParams, CreateOrUpdateEnvironmentSecretParams, FileContent, GetFileContentParams, GitHubService, PullRequest, Repository, UpdateFileParams } from "../../domain/github.js";
4
4
  type GitHubReleaseParam = {
5
5
  client: Octokit;
6
6
  owner: string;
@@ -10,6 +10,7 @@ export declare class OctokitGitHubService implements GitHubService {
10
10
  #private;
11
11
  constructor(octokit: Octokit);
12
12
  createBranch(params: CreateBranchParams): Promise<void>;
13
+ createOrUpdateEnvironmentSecret(params: CreateOrUpdateEnvironmentSecretParams): Promise<void>;
13
14
  createPullRequest(params: {
14
15
  base: string;
15
16
  body: string;
@@ -1,4 +1,5 @@
1
1
  import { $ } from "execa";
2
+ import sodium from "libsodium-wrappers";
2
3
  import { ResultAsync } from "neverthrow";
3
4
  import { RequestError } from "octokit";
4
5
  import semverParse from "semver/functions/parse.js";
@@ -31,6 +32,38 @@ export class OctokitGitHubService {
31
32
  });
32
33
  }
33
34
  }
35
+ async createOrUpdateEnvironmentSecret(params) {
36
+ try {
37
+ // GitHub requires environment secrets to be encrypted client-side with libsodium.
38
+ await sodium.ready;
39
+ // Ensure the target environment exists before resolving its public key and storing secrets.
40
+ await this.#octokit.request("PUT /repos/{owner}/{repo}/environments/{environment_name}", {
41
+ environment_name: params.environmentName,
42
+ owner: params.owner,
43
+ repo: params.repo,
44
+ });
45
+ const { data: publicKeyData } = await this.#octokit.rest.actions.getEnvironmentPublicKey({
46
+ environment_name: params.environmentName,
47
+ owner: params.owner,
48
+ repo: params.repo,
49
+ });
50
+ const publicKeyBytes = sodium.from_base64(publicKeyData.key, sodium.base64_variants.ORIGINAL);
51
+ const secretBytes = sodium.from_string(params.secretValue);
52
+ const encryptedBytes = sodium.crypto_box_seal(secretBytes, publicKeyBytes);
53
+ const encryptedValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
54
+ await this.#octokit.rest.actions.createOrUpdateEnvironmentSecret({
55
+ encrypted_value: encryptedValue,
56
+ environment_name: params.environmentName,
57
+ key_id: publicKeyData.key_id,
58
+ owner: params.owner,
59
+ repo: params.repo,
60
+ secret_name: params.secretName,
61
+ });
62
+ }
63
+ catch (error) {
64
+ throw new Error(`Failed to create or update secret ${params.secretName} in environment ${params.environmentName}`, { cause: error });
65
+ }
66
+ }
34
67
  async createPullRequest(params) {
35
68
  try {
36
69
  const { data } = await this.#octokit.rest.pulls.create({
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tests for the internal `runActions` helper that coordinates plop's
3
+ * generator execution and surfaces meaningful error messages (CES-1923).
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { mock } from "vitest-mock-extended";
7
+ import { runActions } from "../index.js";
8
+ const makeGenerator = (result) => {
9
+ const generator = mock();
10
+ generator.runActions.mockResolvedValue(result);
11
+ return generator;
12
+ };
13
+ describe("runActions", () => {
14
+ it("resolves silently when there are no failures", async () => {
15
+ const generator = makeGenerator({ changes: [], failures: [] });
16
+ await expect(runActions(generator, {})).resolves.toBeUndefined();
17
+ });
18
+ it("ignores 'Aborted due to previous action failure' entries", async () => {
19
+ const generator = makeGenerator({
20
+ changes: [],
21
+ failures: [
22
+ {
23
+ error: "Aborted due to previous action failure",
24
+ path: "",
25
+ type: "add",
26
+ },
27
+ ],
28
+ });
29
+ await expect(runActions(generator, {})).resolves.toBeUndefined();
30
+ });
31
+ it("surfaces the original failure message (no more 'undefined')", async () => {
32
+ const generator = makeGenerator({
33
+ changes: [],
34
+ failures: [
35
+ {
36
+ error: "Failed to create the key vault: quota exceeded",
37
+ path: "",
38
+ type: "initCloudAccounts",
39
+ },
40
+ ],
41
+ });
42
+ await expect(runActions(generator, {})).rejects.toThrow(/initCloudAccounts.*Failed to create the key vault: quota exceeded/);
43
+ });
44
+ it("aggregates multiple failures into the thrown message", async () => {
45
+ const generator = makeGenerator({
46
+ changes: [],
47
+ failures: [
48
+ { error: "Missing template", path: "", type: "add" },
49
+ {
50
+ error: "Aborted due to previous action failure",
51
+ path: "",
52
+ type: "modify",
53
+ },
54
+ { error: "Permission denied", path: "", type: "initCloudAccounts" },
55
+ ],
56
+ });
57
+ await expect(runActions(generator, {})).rejects.toThrow(/add: Missing template.*initCloudAccounts: Permission denied/s);
58
+ });
59
+ it("falls back to 'unknown error' when plop provides no error string", async () => {
60
+ const generator = makeGenerator({
61
+ changes: [],
62
+ failures: [
63
+ { error: undefined, path: "", type: "add" },
64
+ ],
65
+ });
66
+ await expect(runActions(generator, {})).rejects.toThrow(/unknown error/);
67
+ });
68
+ });
@@ -1,5 +1,13 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { initCloudAccounts } from "../init-cloud-accounts.js";
3
+ const createMockGitHubService = () => ({
4
+ createBranch: vi.fn(),
5
+ createOrUpdateEnvironmentSecret: vi.fn().mockResolvedValue(undefined),
6
+ createPullRequest: vi.fn(),
7
+ getFileContent: vi.fn(),
8
+ getRepository: vi.fn(),
9
+ updateFile: vi.fn(),
10
+ });
3
11
  const createMockCloudAccountService = (overrides = {}) => ({
4
12
  getTerraformBackend: vi.fn().mockResolvedValue(undefined),
5
13
  hasUserPermissionToInitialize: vi.fn().mockResolvedValue(true),
@@ -68,18 +76,24 @@ describe("initCloudAccounts", () => {
68
76
  },
69
77
  },
70
78
  });
71
- await initCloudAccounts(payload, mockService);
79
+ await initCloudAccounts(payload, mockService, createMockGitHubService());
72
80
  expect(initializeMock).toHaveBeenCalledTimes(2);
73
81
  expect(initializeMock).toHaveBeenCalledWith(cloudAccount1, expect.objectContaining({ name: "prod", prefix: "io" }), {
74
82
  id: "test-app-id",
75
83
  installationId: "test-installation-id",
76
84
  key: "test-private-key",
77
- }, {});
85
+ }, {
86
+ owner: "pagopa",
87
+ repo: "dx",
88
+ }, expect.any(Object), {});
78
89
  expect(initializeMock).toHaveBeenCalledWith(cloudAccount2, expect.objectContaining({ name: "prod", prefix: "io" }), {
79
90
  id: "test-app-id",
80
91
  installationId: "test-installation-id",
81
92
  key: "test-private-key",
82
- }, {});
93
+ }, {
94
+ owner: "pagopa",
95
+ repo: "dx",
96
+ }, expect.any(Object), {});
83
97
  });
84
98
  it("should not call initialize when cloudAccountsToInitialize is empty", async () => {
85
99
  const initializeMock = vi.fn().mockResolvedValue(undefined);
@@ -99,7 +113,7 @@ describe("initCloudAccounts", () => {
99
113
  },
100
114
  },
101
115
  });
102
- await initCloudAccounts(payload, mockService);
116
+ await initCloudAccounts(payload, mockService, createMockGitHubService());
103
117
  expect(initializeMock).not.toHaveBeenCalled();
104
118
  });
105
119
  it("should not call initialize when payload.init is undefined", async () => {
@@ -110,7 +124,7 @@ describe("initCloudAccounts", () => {
110
124
  const payload = createMockPayload({
111
125
  init: undefined,
112
126
  });
113
- await initCloudAccounts(payload, mockService);
127
+ await initCloudAccounts(payload, mockService, createMockGitHubService());
114
128
  expect(initializeMock).not.toHaveBeenCalled();
115
129
  });
116
130
  it("should use prefix and environment name from payload.env", async () => {
@@ -137,11 +151,14 @@ describe("initCloudAccounts", () => {
137
151
  },
138
152
  },
139
153
  });
140
- await initCloudAccounts(payload, mockService);
154
+ await initCloudAccounts(payload, mockService, createMockGitHubService());
141
155
  expect(initializeMock).toHaveBeenCalledWith(cloudAccount, expect.objectContaining({ name: "uat", prefix: "pagopa" }), {
142
156
  id: "test-app-id",
143
157
  installationId: "test-installation-id",
144
158
  key: "test-private-key",
145
- }, {});
159
+ }, {
160
+ owner: "pagopa",
161
+ repo: "dx",
162
+ }, expect.any(Object), {});
146
163
  });
147
164
  });
@@ -1,5 +1,6 @@
1
1
  import { type NodePlopAPI } from "node-plop";
2
2
  import { CloudAccountService } from "../../../domain/cloud-account.js";
3
+ import { type GitHubService } from "../../../domain/github.js";
3
4
  import { type Payload } from "../generators/environment/prompts.js";
4
- export declare const initCloudAccounts: (payload: Payload, cloudAccountService: CloudAccountService) => Promise<void>;
5
- export default function (plop: NodePlopAPI, cloudAccountService: CloudAccountService): void;
5
+ export declare const initCloudAccounts: (payload: Payload, cloudAccountService: CloudAccountService, gitHubService: GitHubService) => Promise<void>;
6
+ export default function (plop: NodePlopAPI, cloudAccountService: CloudAccountService, gitHubService: GitHubService): void;
@@ -1,16 +1,16 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { payloadSchema, } from "../generators/environment/prompts.js";
3
- export const initCloudAccounts = async (payload, cloudAccountService) => {
3
+ export const initCloudAccounts = async (payload, cloudAccountService, gitHubService) => {
4
4
  if (payload.init && payload.init.cloudAccountsToInitialize.length > 0) {
5
5
  const { runnerAppCredentials } = payload.init;
6
6
  assert.ok(runnerAppCredentials, "Runner app credentials are required");
7
- await Promise.all(payload.init.cloudAccountsToInitialize.map((cloudAccount) => cloudAccountService.initialize(cloudAccount, payload.env, runnerAppCredentials, payload.tags)));
7
+ await Promise.all(payload.init.cloudAccountsToInitialize.map((cloudAccount) => cloudAccountService.initialize(cloudAccount, payload.env, runnerAppCredentials, payload.github, gitHubService, payload.tags)));
8
8
  }
9
9
  };
10
- export default function (plop, cloudAccountService) {
10
+ export default function (plop, cloudAccountService, gitHubService) {
11
11
  plop.setActionType("initCloudAccounts", async (data) => {
12
12
  const payload = payloadSchema.parse(data);
13
- await initCloudAccounts(payload, cloudAccountService);
13
+ await initCloudAccounts(payload, cloudAccountService, gitHubService);
14
14
  return "Cloud Accounts Initialized";
15
15
  });
16
16
  }
@@ -46,7 +46,12 @@ describe("actions", () => {
46
46
  payload: getPayload(false),
47
47
  },
48
48
  ])("correct order of actions", ({ payload }) => {
49
- const actionsOrder = ["getTerraformBackend", "addMany", "addMany"];
49
+ const actionsOrder = [
50
+ "getTerraformBackend",
51
+ "addMany",
52
+ "addMany",
53
+ "addMany",
54
+ ];
50
55
  if (payload.init) {
51
56
  actionsOrder.unshift("initCloudAccounts", "provisionTerraformBackend");
52
57
  actionsOrder.push("addMany", "addMany");
@@ -29,6 +29,17 @@ const addModule = (env, templatesPath, init = false) => {
29
29
  },
30
30
  ];
31
31
  };
32
+ const addWorkflowModule = (templatesPath) => {
33
+ const cwd = process.cwd();
34
+ return {
35
+ base: path.join(templatesPath, "workflow"),
36
+ destination: path.join(cwd, ".github", "workflows"),
37
+ force: true,
38
+ templateFiles: path.join(templatesPath, "workflow"),
39
+ type: "addMany",
40
+ verbose: true,
41
+ };
42
+ };
32
43
  export default function getActions(templatesPath) {
33
44
  return (payload) => {
34
45
  const logger = getLogger(["gen", "env"]);
@@ -39,6 +50,7 @@ export default function getActions(templatesPath) {
39
50
  {
40
51
  type: "getTerraformBackend",
41
52
  },
53
+ addWorkflowModule(templatesPath),
42
54
  ...addEnvironmentModule("bootstrapper", `${github.repo}.bootstrapper.${env.name}.tfstate`),
43
55
  ];
44
56
  if (init) {
@@ -1,7 +1,8 @@
1
1
  import { type NodePlopAPI } from "node-plop";
2
2
  import { CloudAccountRepository, CloudAccountService } from "../../../../domain/cloud-account.js";
3
3
  import { GitHubRepo } from "../../../../domain/github-repo.js";
4
+ import { type GitHubService } from "../../../../domain/github.js";
4
5
  import { Payload, payloadSchema } from "./prompts.js";
5
6
  export declare const PLOP_ENVIRONMENT_GENERATOR_NAME = "DX_DeploymentEnvironment";
6
7
  export { Payload, payloadSchema };
7
- export default function (plop: NodePlopAPI, templatesPath: string, cloudAccountRepository: CloudAccountRepository, cloudAccountService: CloudAccountService, github?: GitHubRepo): void;
8
+ export default function (plop: NodePlopAPI, templatesPath: string, cloudAccountRepository: CloudAccountRepository, cloudAccountService: CloudAccountService, gitHubService: GitHubService, github?: GitHubRepo): void;
@@ -8,13 +8,13 @@ import getActions from "./actions.js";
8
8
  import getPrompts, { payloadSchema } from "./prompts.js";
9
9
  export const PLOP_ENVIRONMENT_GENERATOR_NAME = "DX_DeploymentEnvironment";
10
10
  export { payloadSchema };
11
- export default function (plop, templatesPath, cloudAccountRepository, cloudAccountService, github) {
11
+ export default function (plop, templatesPath, cloudAccountRepository, cloudAccountService, gitHubService, github) {
12
12
  setEnvShortHelper(plop);
13
13
  setResourcePrefixHelper(plop);
14
14
  setEqHelper(plop);
15
15
  setGetTerraformBackend(plop, cloudAccountService);
16
16
  setProvisionTerraformBackendAction(plop, cloudAccountService);
17
- setInitCloudAccountsAction(plop, cloudAccountService);
17
+ setInitCloudAccountsAction(plop, cloudAccountService, gitHubService);
18
18
  plop.setGenerator(PLOP_ENVIRONMENT_GENERATOR_NAME, {
19
19
  actions: getActions(templatesPath),
20
20
  description: "Generate a new deployment environment",
@@ -1,10 +1,12 @@
1
- import type { NodePlopAPI } from "plop";
1
+ import type { NodePlopAPI, PlopGenerator } from "plop";
2
+ import { Answers } from "inquirer";
2
3
  import { GitHubRepo } from "../../domain/github-repo.js";
3
4
  import { GitHubService } from "../../domain/github.js";
4
5
  import { Payload as EnvironmentPayload } from "../plop/generators/environment/index.js";
5
6
  import { Payload as MonorepoPayload } from "../plop/generators/monorepo/index.js";
6
7
  export declare const setMonorepoGenerator: (plop: NodePlopAPI) => void;
7
8
  export declare const getPlopInstance: () => Promise<NodePlopAPI>;
9
+ export declare const runActions: (generator: PlopGenerator, payload: Answers) => Promise<void>;
8
10
  export declare const runMonorepoGenerator: (plop: NodePlopAPI, githubService: GitHubService) => Promise<MonorepoPayload>;
9
11
  /**
10
12
  * Run the deployment environment generator
@@ -14,7 +16,7 @@ export declare const runMonorepoGenerator: (plop: NodePlopAPI, githubService: Gi
14
16
  * uses the explicitly passed repository. When omitted (by add command),
15
17
  * the generator infers it from the local git context.
16
18
  */
17
- export declare const runDeploymentEnvironmentGenerator: (plop: NodePlopAPI, github?: GitHubRepo) => Promise<EnvironmentPayload>;
19
+ export declare const runDeploymentEnvironmentGenerator: (plop: NodePlopAPI, gitHubService: GitHubService, github?: GitHubRepo) => Promise<EnvironmentPayload>;
18
20
  /**
19
21
  * Configure the deployment environment generator
20
22
  *
@@ -23,4 +25,4 @@ export declare const runDeploymentEnvironmentGenerator: (plop: NodePlopAPI, gith
23
25
  * uses the explicitly passed repository. When omitted (by add command),
24
26
  * the generator infers it from the local git context.
25
27
  */
26
- export declare const setDeploymentEnvironmentGenerator: (plop: NodePlopAPI, github?: GitHubRepo) => void;
28
+ export declare const setDeploymentEnvironmentGenerator: (plop: NodePlopAPI, gitHubService: GitHubService, github?: GitHubRepo) => void;