@pagopa/dx-cli 0.21.1 → 0.21.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.
- package/dist/adapters/azure/__tests__/cloud-account-service.test.js +9 -1
- package/dist/adapters/azure/cloud-account-service.js +7 -0
- package/dist/adapters/commander/commands/__tests__/add.test.js +12 -4
- package/dist/adapters/commander/commands/__tests__/init.test.d.ts +4 -0
- package/dist/adapters/commander/commands/__tests__/init.test.js +48 -0
- package/dist/adapters/commander/commands/add.js +5 -2
- package/dist/adapters/commander/commands/init.d.ts +2 -0
- package/dist/adapters/commander/commands/init.js +30 -4
- package/dist/adapters/pagopa-technology/__tests__/authorization.test.js +349 -31
- package/dist/adapters/pagopa-technology/azure-authorization-config.d.ts +13 -0
- package/dist/adapters/pagopa-technology/azure-authorization-config.js +43 -0
- package/dist/adapters/pagopa-technology/{authorization.d.ts → azure-authorization.d.ts} +2 -2
- package/dist/adapters/pagopa-technology/azure-authorization.js +239 -0
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +7 -0
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +3 -0
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +1 -0
- package/dist/adapters/plop/generators/environment/__tests__/prompts.test.js +156 -2
- package/dist/adapters/plop/generators/environment/prompts.d.ts +10 -0
- package/dist/adapters/plop/generators/environment/prompts.js +45 -1
- package/dist/domain/authorization.d.ts +6 -9
- package/dist/domain/authorization.js +27 -10
- package/dist/domain/github.d.ts +1 -0
- package/dist/domain/github.js +1 -0
- package/dist/index.js +2 -2
- package/dist/use-cases/__tests__/request-authorization.test.js +5 -3
- package/dist/use-cases/request-authorization.d.ts +2 -2
- package/dist/use-cases/request-authorization.js +2 -2
- package/package.json +1 -1
- package/dist/adapters/pagopa-technology/authorization.js +0 -124
|
@@ -254,6 +254,7 @@ describe("initialize", () => {
|
|
|
254
254
|
name: "dev",
|
|
255
255
|
prefix: "dx",
|
|
256
256
|
}, {
|
|
257
|
+
clientId: "app-client-id",
|
|
257
258
|
id: "app-id",
|
|
258
259
|
installationId: "installation-id",
|
|
259
260
|
key: "private-key\n",
|
|
@@ -289,7 +290,7 @@ describe("initialize", () => {
|
|
|
289
290
|
issuer: "https://token.actions.githubusercontent.com",
|
|
290
291
|
subject: "repo:pagopa/dx:environment:bootstrapper-dev-cd",
|
|
291
292
|
});
|
|
292
|
-
expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledTimes(
|
|
293
|
+
expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledTimes(7);
|
|
293
294
|
expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
|
|
294
295
|
environmentName: "bootstrapper-dev-cd",
|
|
295
296
|
owner: "pagopa",
|
|
@@ -318,6 +319,13 @@ describe("initialize", () => {
|
|
|
318
319
|
secretName: "GH_APP_ID",
|
|
319
320
|
secretValue: "app-id",
|
|
320
321
|
});
|
|
322
|
+
expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
|
|
323
|
+
environmentName: "bootstrapper-dev-cd",
|
|
324
|
+
owner: "pagopa",
|
|
325
|
+
repo: "dx",
|
|
326
|
+
secretName: "GH_APP_CLIENT_ID",
|
|
327
|
+
secretValue: "app-client-id",
|
|
328
|
+
});
|
|
321
329
|
expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
|
|
322
330
|
environmentName: "bootstrapper-dev-cd",
|
|
323
331
|
owner: "pagopa",
|
|
@@ -460,6 +460,13 @@ export class AzureCloudAccountService {
|
|
|
460
460
|
secretName: "GH_APP_ID",
|
|
461
461
|
secretValue: runnerAppCredentials.id,
|
|
462
462
|
}),
|
|
463
|
+
gitHubService.createOrUpdateEnvironmentSecret({
|
|
464
|
+
environmentName: githubEnvironmentName,
|
|
465
|
+
owner: github.owner,
|
|
466
|
+
repo: github.repo,
|
|
467
|
+
secretName: "GH_APP_CLIENT_ID",
|
|
468
|
+
secretValue: runnerAppCredentials.clientId,
|
|
469
|
+
}),
|
|
463
470
|
gitHubService.createOrUpdateEnvironmentSecret({
|
|
464
471
|
environmentName: githubEnvironmentName,
|
|
465
472
|
owner: github.owner,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { errAsync, okAsync } from "neverthrow";
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import { mock } from "vitest-mock-extended";
|
|
7
|
-
import { AuthorizationError, AuthorizationResult,
|
|
7
|
+
import { AuthorizationError, AuthorizationResult, } from "../../../../domain/authorization.js";
|
|
8
8
|
import { authorizeCloudAccounts } from "../add.js";
|
|
9
9
|
const makeEnvPayload = (overrides = {}) => ({
|
|
10
10
|
env: {
|
|
@@ -65,6 +65,8 @@ describe("authorizeCloudAccounts", () => {
|
|
|
65
65
|
expect(result._unsafeUnwrap()).toEqual([expectedPr]);
|
|
66
66
|
expect(authService.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
67
67
|
bootstrapIdentityId: "dx-d-itn-bootstrap-id-01",
|
|
68
|
+
envShort: "d",
|
|
69
|
+
prefix: "dx",
|
|
68
70
|
subscriptionName: "DEV-FooBar",
|
|
69
71
|
}));
|
|
70
72
|
});
|
|
@@ -89,6 +91,8 @@ describe("authorizeCloudAccounts", () => {
|
|
|
89
91
|
expect(result.isOk()).toBe(true);
|
|
90
92
|
expect(authService.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
91
93
|
bootstrapIdentityId: "io-p-weu-bootstrap-id-01",
|
|
94
|
+
envShort: "p",
|
|
95
|
+
prefix: "io",
|
|
92
96
|
subscriptionName: "PROD-Bar",
|
|
93
97
|
}));
|
|
94
98
|
});
|
|
@@ -140,7 +144,7 @@ describe("authorizeCloudAccounts", () => {
|
|
|
140
144
|
expect(prs).toHaveLength(1);
|
|
141
145
|
expect(prs[0]).toEqual(expectedPr);
|
|
142
146
|
});
|
|
143
|
-
it("
|
|
147
|
+
it("returns a no-op authorization result when nothing changed", async () => {
|
|
144
148
|
const authService = mock();
|
|
145
149
|
const account = {
|
|
146
150
|
csp: "azure",
|
|
@@ -148,12 +152,16 @@ describe("authorizeCloudAccounts", () => {
|
|
|
148
152
|
displayName: "DEV-Existing",
|
|
149
153
|
id: "sub-exists",
|
|
150
154
|
};
|
|
151
|
-
|
|
155
|
+
// No-op result: Ok with no URL (identity + groups already configured)
|
|
156
|
+
authService.requestAuthorization.mockReturnValue(okAsync(new AuthorizationResult()));
|
|
152
157
|
const envPayload = makeEnvPayload({
|
|
153
158
|
init: { cloudAccountsToInitialize: [account] },
|
|
154
159
|
});
|
|
155
160
|
const result = await authorizeCloudAccounts(authService)(envPayload);
|
|
156
161
|
expect(result.isOk()).toBe(true);
|
|
157
|
-
|
|
162
|
+
// The no-op result is still collected but has no URL
|
|
163
|
+
const prs = result._unsafeUnwrap();
|
|
164
|
+
expect(prs).toHaveLength(1);
|
|
165
|
+
expect(prs[0].url).toBeUndefined();
|
|
158
166
|
});
|
|
159
167
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for confirmGitHubRepoCreation in the init command.
|
|
3
|
+
*/
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { confirmGitHubRepoCreation } from "../init.js";
|
|
7
|
+
vi.mock("inquirer");
|
|
8
|
+
const makePayload = (overrides = {}) => ({
|
|
9
|
+
repoDescription: "A test repo",
|
|
10
|
+
repoName: "test-repo",
|
|
11
|
+
repoOwner: "pagopa",
|
|
12
|
+
...overrides,
|
|
13
|
+
});
|
|
14
|
+
describe("confirmGitHubRepoCreation", () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
it("returns true when the user confirms", async () => {
|
|
19
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({ confirm: true });
|
|
20
|
+
const result = await confirmGitHubRepoCreation(makePayload());
|
|
21
|
+
expect(result.isOk()).toBe(true);
|
|
22
|
+
expect(result._unsafeUnwrap()).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it("returns false when the user declines", async () => {
|
|
25
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({ confirm: false });
|
|
26
|
+
const result = await confirmGitHubRepoCreation(makePayload());
|
|
27
|
+
expect(result.isOk()).toBe(true);
|
|
28
|
+
expect(result._unsafeUnwrap()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("prompts with the correct repository name and owner", async () => {
|
|
31
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({ confirm: true });
|
|
32
|
+
const payload = makePayload({ repoName: "my-repo", repoOwner: "my-org" });
|
|
33
|
+
await confirmGitHubRepoCreation(payload);
|
|
34
|
+
expect(inquirer.prompt).toHaveBeenCalledWith(expect.objectContaining({
|
|
35
|
+
message: expect.stringContaining("my-org/my-repo"),
|
|
36
|
+
type: "confirm",
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
it("returns an error result when the prompt rejects", async () => {
|
|
40
|
+
const cause = new Error("non-interactive TTY");
|
|
41
|
+
vi.mocked(inquirer.prompt).mockRejectedValue(cause);
|
|
42
|
+
const result = await confirmGitHubRepoCreation(makePayload());
|
|
43
|
+
expect(result.isErr()).toBe(true);
|
|
44
|
+
const err = result._unsafeUnwrapErr();
|
|
45
|
+
expect(err).toBeInstanceOf(Error);
|
|
46
|
+
expect(err.cause).toBe(cause);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -38,6 +38,8 @@ export const authorizeCloudAccounts = (authorizationService) => (envPayload) =>
|
|
|
38
38
|
const locShort = locationShort[account.defaultLocation];
|
|
39
39
|
const input = requestAuthorizationInputSchema.safeParse({
|
|
40
40
|
bootstrapIdentityId: `${prefix}-${envShort}-${locShort}-bootstrap-id-01`,
|
|
41
|
+
envShort,
|
|
42
|
+
prefix,
|
|
41
43
|
repoName: envPayload.github.repo,
|
|
42
44
|
subscriptionName: account.displayName,
|
|
43
45
|
});
|
|
@@ -62,9 +64,10 @@ const displaySummary = (result) => {
|
|
|
62
64
|
console.log(chalk.green.bold("\nCloud environment created successfully!"));
|
|
63
65
|
let step = 1;
|
|
64
66
|
console.log(chalk.green.bold("\nNext Steps:"));
|
|
65
|
-
|
|
67
|
+
const prsWithUrl = authorizationPrs.filter((pr) => pr.url != null);
|
|
68
|
+
if (prsWithUrl.length > 0) {
|
|
66
69
|
console.log(`${step++}. Review the Azure authorization Pull Request(s):`);
|
|
67
|
-
for (const authPr of
|
|
70
|
+
for (const authPr of prsWithUrl) {
|
|
68
71
|
console.log(` - ${chalk.underline(authPr.url)}`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { ResultAsync } from "neverthrow";
|
|
3
3
|
import { GitHubService } from "../../../domain/github.js";
|
|
4
|
+
import { Payload as MonorepoPayload } from "../../plop/generators/monorepo/index.js";
|
|
4
5
|
export declare const checkInitPreconditions: () => ResultAsync<import("execa").Result<{
|
|
5
6
|
environment: {
|
|
6
7
|
NO_COLOR: string;
|
|
@@ -17,6 +18,7 @@ export declare const checkAddEnvironmentPreconditions: () => ResultAsync<import(
|
|
|
17
18
|
};
|
|
18
19
|
shell: true;
|
|
19
20
|
}>, Error>;
|
|
21
|
+
export declare const confirmGitHubRepoCreation: (payload: MonorepoPayload) => ResultAsync<boolean, Error>;
|
|
20
22
|
type InitCommandDependencies = {
|
|
21
23
|
gitHubService: GitHubService;
|
|
22
24
|
};
|
|
@@ -2,6 +2,7 @@ import { getLogger } from "@logtape/logtape";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { $, ExecaError } from "execa";
|
|
5
|
+
import inquirer from "inquirer";
|
|
5
6
|
import { okAsync, ResultAsync } from "neverthrow";
|
|
6
7
|
import * as path from "node:path";
|
|
7
8
|
import { oraPromise } from "ora";
|
|
@@ -10,13 +11,28 @@ import { Repository, } from "../../../domain/github.js";
|
|
|
10
11
|
import { tf$ } from "../../execa/terraform.js";
|
|
11
12
|
import { getPlopInstance, runMonorepoGenerator } from "../../plop/index.js";
|
|
12
13
|
import { exitWithError } from "../index.js";
|
|
14
|
+
const isGitHubRepoCreationSkipped = (input) => "gitHubRepoCreationSkipped" in input;
|
|
13
15
|
const withSpinner = (text, successText, failText, promise) => ResultAsync.fromPromise(oraPromise(promise, {
|
|
14
16
|
failText,
|
|
15
17
|
successText,
|
|
16
18
|
text,
|
|
17
19
|
}), (cause) => new Error(failText, { cause }));
|
|
18
|
-
const displaySummary = (
|
|
19
|
-
const
|
|
20
|
+
const displaySummary = (input) => {
|
|
21
|
+
const docsUrl = "https://dx.pagopa.it/getting-started";
|
|
22
|
+
if (isGitHubRepoCreationSkipped(input)) {
|
|
23
|
+
const { payload } = input;
|
|
24
|
+
console.log(chalk.yellow.bold("\nGitHub repository creation skipped."));
|
|
25
|
+
console.log(`The workspace files have been scaffolded in ${chalk.cyan(payload.repoName + "/")}.`);
|
|
26
|
+
console.log(chalk.bold("\nTo finish the setup manually:"));
|
|
27
|
+
let step = 1;
|
|
28
|
+
console.log(`${step++}. Create the GitHub repository by applying the Terraform config scaffolded at ${chalk.cyan(`${payload.repoName}/infra/repository`)}:`);
|
|
29
|
+
console.log(` ${chalk.cyan(`cd ${payload.repoName}/infra/repository && terraform init && terraform apply`)}`);
|
|
30
|
+
console.log(`${step++}. Push the scaffolded code to the newly created repository:`);
|
|
31
|
+
console.log(` ${chalk.cyan(`cd ${payload.repoName} && git init && git remote add origin <url> && git push`)}`);
|
|
32
|
+
console.log(`${step}. Visit ${chalk.underline(docsUrl)} to deploy your first project\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { pr, repository } = input;
|
|
20
36
|
console.log(chalk.green.bold("\nWorkspace created successfully!"));
|
|
21
37
|
if (repository) {
|
|
22
38
|
console.log(`- Name: ${chalk.cyan(repository.name)}`);
|
|
@@ -29,7 +45,7 @@ const displaySummary = (initResult) => {
|
|
|
29
45
|
let step = 1;
|
|
30
46
|
console.log(chalk.green.bold("\nNext Steps:"));
|
|
31
47
|
console.log(`${step++}. Review the Pull Request in the GitHub repository: ${chalk.underline(pr.url)}`);
|
|
32
|
-
console.log(`${step}. Visit ${chalk.underline(
|
|
48
|
+
console.log(`${step}. Visit ${chalk.underline(docsUrl)} to deploy your first project\n`);
|
|
33
49
|
}
|
|
34
50
|
else {
|
|
35
51
|
console.log(chalk.yellow(`\n⚠️ There was an error during Pull Request creation.`));
|
|
@@ -118,6 +134,14 @@ const handleGeneratorError = (err) => {
|
|
|
118
134
|
}
|
|
119
135
|
return new Error("Failed to run the generator", { cause: err });
|
|
120
136
|
};
|
|
137
|
+
export const confirmGitHubRepoCreation = (payload) => ResultAsync.fromPromise(inquirer
|
|
138
|
+
.prompt({
|
|
139
|
+
default: true,
|
|
140
|
+
message: `The project is created on ${chalk.green(payload.repoName)}. Would you like to publish it to GitHub at ${chalk.green(`${payload.repoOwner}/${payload.repoName}`)} now?`,
|
|
141
|
+
name: "confirm",
|
|
142
|
+
type: "confirm",
|
|
143
|
+
})
|
|
144
|
+
.then(({ confirm }) => confirm), (cause) => new Error("Failed to read GitHub publish confirmation", { cause }));
|
|
121
145
|
export const makeInitCommand = ({ gitHubService, }) => new Command()
|
|
122
146
|
.name("init")
|
|
123
147
|
.description("Initialize a new DX workspace")
|
|
@@ -128,6 +152,8 @@ export const makeInitCommand = ({ gitHubService, }) => new Command()
|
|
|
128
152
|
.andTee((payload) => {
|
|
129
153
|
process.chdir(payload.repoName);
|
|
130
154
|
})
|
|
131
|
-
.andThen((payload) =>
|
|
155
|
+
.andThen((payload) => confirmGitHubRepoCreation(payload).andThen((confirmed) => confirmed
|
|
156
|
+
? handleNewGitHubRepository(gitHubService)(payload)
|
|
157
|
+
: okAsync({ gitHubRepoCreationSkipped: true, payload })))
|
|
132
158
|
.match(displaySummary, exitWithError(this));
|
|
133
159
|
});
|