@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.
Files changed (29) hide show
  1. package/dist/adapters/azure/__tests__/cloud-account-service.test.js +9 -1
  2. package/dist/adapters/azure/cloud-account-service.js +7 -0
  3. package/dist/adapters/commander/commands/__tests__/add.test.js +12 -4
  4. package/dist/adapters/commander/commands/__tests__/init.test.d.ts +4 -0
  5. package/dist/adapters/commander/commands/__tests__/init.test.js +48 -0
  6. package/dist/adapters/commander/commands/add.js +5 -2
  7. package/dist/adapters/commander/commands/init.d.ts +2 -0
  8. package/dist/adapters/commander/commands/init.js +30 -4
  9. package/dist/adapters/pagopa-technology/__tests__/authorization.test.js +349 -31
  10. package/dist/adapters/pagopa-technology/azure-authorization-config.d.ts +13 -0
  11. package/dist/adapters/pagopa-technology/azure-authorization-config.js +43 -0
  12. package/dist/adapters/pagopa-technology/{authorization.d.ts → azure-authorization.d.ts} +2 -2
  13. package/dist/adapters/pagopa-technology/azure-authorization.js +239 -0
  14. package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +7 -0
  15. package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +3 -0
  16. package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +1 -0
  17. package/dist/adapters/plop/generators/environment/__tests__/prompts.test.js +156 -2
  18. package/dist/adapters/plop/generators/environment/prompts.d.ts +10 -0
  19. package/dist/adapters/plop/generators/environment/prompts.js +45 -1
  20. package/dist/domain/authorization.d.ts +6 -9
  21. package/dist/domain/authorization.js +27 -10
  22. package/dist/domain/github.d.ts +1 -0
  23. package/dist/domain/github.js +1 -0
  24. package/dist/index.js +2 -2
  25. package/dist/use-cases/__tests__/request-authorization.test.js +5 -3
  26. package/dist/use-cases/request-authorization.d.ts +2 -2
  27. package/dist/use-cases/request-authorization.js +2 -2
  28. package/package.json +1 -1
  29. 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(6);
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, IdentityAlreadyExistsError, } from "../../../../domain/authorization.js";
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("handles IdentityAlreadyExistsError gracefully", async () => {
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
- authService.requestAuthorization.mockReturnValue(errAsync(new IdentityAlreadyExistsError("dx-d-itn-bootstrap-id-01")));
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
- expect(result._unsafeUnwrap()).toEqual([]);
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,4 @@
1
+ /**
2
+ * Tests for confirmGitHubRepoCreation in the init command.
3
+ */
4
+ export {};
@@ -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
- if (authorizationPrs.length > 0) {
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 authorizationPrs) {
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 = (initResult) => {
19
- const { pr, repository } = initResult;
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("https://dx.pagopa.it/getting-started")} to deploy your first project\n`);
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) => handleNewGitHubRepository(gitHubService)(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
  });