@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.
- package/README.md +12 -1
- package/bin/index.js +0 -20
- package/dist/adapters/azure/__tests__/cloud-account-service.test.js +132 -1
- package/dist/adapters/azure/cloud-account-service.d.ts +3 -2
- package/dist/adapters/azure/cloud-account-service.js +127 -39
- package/dist/adapters/commander/__tests__/error-reporting.test.d.ts +1 -0
- package/dist/adapters/commander/__tests__/error-reporting.test.js +63 -0
- package/dist/adapters/commander/__tests__/exit-with-error.test.d.ts +1 -0
- package/dist/adapters/commander/__tests__/exit-with-error.test.js +92 -0
- package/dist/adapters/commander/commands/add.d.ts +2 -0
- package/dist/adapters/commander/commands/add.js +8 -5
- package/dist/adapters/commander/commands/codemod.js +3 -2
- package/dist/adapters/commander/commands/init.js +2 -2
- package/dist/adapters/commander/commands/savemoney.js +6 -3
- package/dist/adapters/commander/error-reporting.d.ts +10 -0
- package/dist/adapters/commander/error-reporting.js +68 -0
- package/dist/adapters/commander/index.d.ts +17 -1
- package/dist/adapters/commander/index.js +23 -2
- package/dist/adapters/octokit/index.d.ts +2 -1
- package/dist/adapters/octokit/index.js +33 -0
- package/dist/adapters/plop/__tests__/run-actions.test.d.ts +1 -0
- package/dist/adapters/plop/__tests__/run-actions.test.js +68 -0
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +24 -7
- package/dist/adapters/plop/actions/init-cloud-accounts.d.ts +3 -2
- package/dist/adapters/plop/actions/init-cloud-accounts.js +4 -4
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +6 -1
- package/dist/adapters/plop/generators/environment/actions.js +12 -0
- package/dist/adapters/plop/generators/environment/index.d.ts +2 -1
- package/dist/adapters/plop/generators/environment/index.js +2 -2
- package/dist/adapters/plop/index.d.ts +5 -3
- package/dist/adapters/plop/index.js +20 -12
- package/dist/domain/cloud-account.d.ts +3 -2
- package/dist/domain/github.d.ts +13 -0
- package/dist/index.js +36 -0
- package/package.json +4 -2
- 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
|
|
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((
|
|
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((
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
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;
|