@pagopa/dx-cli 0.21.0 → 0.21.1

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.
@@ -241,7 +241,10 @@ describe("isInitialized", () => {
241
241
  });
242
242
  });
243
243
  describe("initialize", () => {
244
- test("assigns bootstrap roles to the bootstrap identity", async ({ cloudAccountService, }) => {
244
+ test("assigns bootstrap roles and creates bootstrap environment secrets", async ({ cloudAccountService, }) => {
245
+ const createOrUpdateEnvironmentSecret = vi
246
+ .fn()
247
+ .mockResolvedValue(undefined);
245
248
  await cloudAccountService.initialize({
246
249
  csp: "azure",
247
250
  defaultLocation: "italynorth",
@@ -259,7 +262,7 @@ describe("initialize", () => {
259
262
  repo: "dx",
260
263
  }, {
261
264
  createBranch: vi.fn(),
262
- createOrUpdateEnvironmentSecret: vi.fn().mockResolvedValue(undefined),
265
+ createOrUpdateEnvironmentSecret,
263
266
  createPullRequest: vi.fn(),
264
267
  getFileContent: vi.fn(),
265
268
  getRepository: vi.fn(),
@@ -286,5 +289,48 @@ describe("initialize", () => {
286
289
  issuer: "https://token.actions.githubusercontent.com",
287
290
  subject: "repo:pagopa/dx:environment:bootstrapper-dev-cd",
288
291
  });
292
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledTimes(6);
293
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
294
+ environmentName: "bootstrapper-dev-cd",
295
+ owner: "pagopa",
296
+ repo: "dx",
297
+ secretName: "ARM_CLIENT_ID",
298
+ secretValue: "client-1",
299
+ });
300
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
301
+ environmentName: "bootstrapper-dev-cd",
302
+ owner: "pagopa",
303
+ repo: "dx",
304
+ secretName: "ARM_TENANT_ID",
305
+ secretValue: "tenant-1",
306
+ });
307
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
308
+ environmentName: "bootstrapper-dev-cd",
309
+ owner: "pagopa",
310
+ repo: "dx",
311
+ secretName: "ARM_SUBSCRIPTION_ID",
312
+ secretValue: "sub-1",
313
+ });
314
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
315
+ environmentName: "bootstrapper-dev-cd",
316
+ owner: "pagopa",
317
+ repo: "dx",
318
+ secretName: "GH_APP_ID",
319
+ secretValue: "app-id",
320
+ });
321
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
322
+ environmentName: "bootstrapper-dev-cd",
323
+ owner: "pagopa",
324
+ repo: "dx",
325
+ secretName: "GH_APP_INSTALLATION_ID",
326
+ secretValue: "installation-id",
327
+ });
328
+ expect(createOrUpdateEnvironmentSecret).toHaveBeenCalledWith({
329
+ environmentName: "bootstrapper-dev-cd",
330
+ owner: "pagopa",
331
+ repo: "dx",
332
+ secretName: "GH_APP_KEY",
333
+ secretValue: "private-key",
334
+ });
289
335
  });
290
336
  });
@@ -169,6 +169,7 @@ export class AzureCloudAccountService {
169
169
  const parameters = {
170
170
  location: cloudAccount.defaultLocation,
171
171
  tags: {
172
+ CreatedBy: "DX CLI",
172
173
  Environment: name,
173
174
  ...tags,
174
175
  },
@@ -185,6 +186,10 @@ export class AzureCloudAccountService {
185
186
  const identityClientId = identity.clientId;
186
187
  logger.debug("Created identity {identityName} in subscription {subscriptionId}", { identityName, subscriptionId: cloudAccount.id });
187
188
  const authorizationManagementClient = new AuthorizationManagementClient(this.#credential, cloudAccount.id);
189
+ const subscriptionClient = new SubscriptionClient(this.#credential);
190
+ const subscription = await subscriptionClient.subscriptions.get(cloudAccount.id);
191
+ assert.ok(subscription.tenantId, "Subscription tenant ID is undefined");
192
+ const tenantId = subscription.tenantId;
188
193
  const subscriptionScope = `/subscriptions/${cloudAccount.id}`;
189
194
  // Grant the bootstrap identity the Azure permissions it needs to operate autonomously in the bootstrap workflow.
190
195
  await Promise.all(bootstrapIdentityRoleDefinitionIds.map((roleDefinitionId) => authorizationManagementClient.roleAssignments.create(subscriptionScope, this.#createRoleAssignmentName(subscriptionScope, identityPrincipalId, roleDefinitionId), {
@@ -205,25 +210,14 @@ export class AzureCloudAccountService {
205
210
  identityName,
206
211
  subscriptionId: cloudAccount.id,
207
212
  });
208
- // These secrets let the GitHub workflow target the bootstrap identity and subscription without extra setup.
209
- await Promise.all([
210
- gitHubService.createOrUpdateEnvironmentSecret({
211
- environmentName: githubEnvironmentName,
212
- owner: github.owner,
213
- repo: github.repo,
214
- secretName: "ARM_CLIENT_ID",
215
- secretValue: identityClientId,
216
- }),
217
- gitHubService.createOrUpdateEnvironmentSecret({
218
- environmentName: githubEnvironmentName,
219
- owner: github.owner,
220
- repo: github.repo,
221
- secretName: "ARM_SUBSCRIPTION_ID",
222
- secretValue: cloudAccount.id,
223
- }),
224
- ]);
225
- logger.debug("Set GitHub environment secrets for {environmentName}", {
226
- environmentName: githubEnvironmentName,
213
+ await this.#storeBootstrapperEnvironmentSecrets({
214
+ cloudAccountId: cloudAccount.id,
215
+ github,
216
+ githubEnvironmentName,
217
+ gitHubService,
218
+ identityClientId,
219
+ runnerAppCredentials,
220
+ tenantId,
227
221
  });
228
222
  const keyVaultName = await this.#createCommonKeyVault({
229
223
  cloudAccount,
@@ -233,6 +227,7 @@ export class AzureCloudAccountService {
233
227
  shortEnv: short.env,
234
228
  shortLocation: short.location,
235
229
  tags,
230
+ tenantId,
236
231
  });
237
232
  await this.#storeRunnerAppSecrets({
238
233
  cloudAccountId: cloudAccount.id,
@@ -348,11 +343,8 @@ export class AzureCloudAccountService {
348
343
  }));
349
344
  return results.every(Boolean);
350
345
  }
351
- async #createCommonKeyVault({ cloudAccount, name, prefix, resourceGroupName, shortEnv, shortLocation, tags, }) {
346
+ async #createCommonKeyVault({ cloudAccount, name, prefix, resourceGroupName, shortEnv, shortLocation, tags, tenantId, }) {
352
347
  const logger = getLogger(["gen", "env"]);
353
- const subscriptionClient = new SubscriptionClient(this.#credential);
354
- const subscription = await subscriptionClient.subscriptions.get(cloudAccount.id);
355
- assert.ok(subscription.tenantId, "Subscription tenant ID is undefined");
356
348
  const kvClient = new KeyVaultManagementClient(this.#credential, cloudAccount.id);
357
349
  const keyVaultName = `${prefix}-${shortEnv}-${shortLocation}-common-kv-01`;
358
350
  const secretsProtectionEnabled = shortEnv === "p";
@@ -372,7 +364,7 @@ export class AzureCloudAccountService {
372
364
  name: "standard",
373
365
  },
374
366
  softDeleteRetentionInDays: secretsProtectionEnabled ? 14 : 7,
375
- tenantId: subscription.tenantId,
367
+ tenantId,
376
368
  },
377
369
  tags: {
378
370
  Environment: name,
@@ -437,6 +429,57 @@ export class AzureCloudAccountService {
437
429
  }));
438
430
  logger.info("All resource providers registered on subscription {subscriptionId}", { subscriptionId });
439
431
  }
432
+ async #storeBootstrapperEnvironmentSecrets({ cloudAccountId, github, githubEnvironmentName, gitHubService, identityClientId, runnerAppCredentials, tenantId, }) {
433
+ const logger = getLogger(["gen", "env"]);
434
+ await Promise.all([
435
+ gitHubService.createOrUpdateEnvironmentSecret({
436
+ environmentName: githubEnvironmentName,
437
+ owner: github.owner,
438
+ repo: github.repo,
439
+ secretName: "ARM_CLIENT_ID",
440
+ secretValue: identityClientId,
441
+ }),
442
+ gitHubService.createOrUpdateEnvironmentSecret({
443
+ environmentName: githubEnvironmentName,
444
+ owner: github.owner,
445
+ repo: github.repo,
446
+ secretName: "ARM_TENANT_ID",
447
+ secretValue: tenantId,
448
+ }),
449
+ gitHubService.createOrUpdateEnvironmentSecret({
450
+ environmentName: githubEnvironmentName,
451
+ owner: github.owner,
452
+ repo: github.repo,
453
+ secretName: "ARM_SUBSCRIPTION_ID",
454
+ secretValue: cloudAccountId,
455
+ }),
456
+ gitHubService.createOrUpdateEnvironmentSecret({
457
+ environmentName: githubEnvironmentName,
458
+ owner: github.owner,
459
+ repo: github.repo,
460
+ secretName: "GH_APP_ID",
461
+ secretValue: runnerAppCredentials.id,
462
+ }),
463
+ gitHubService.createOrUpdateEnvironmentSecret({
464
+ environmentName: githubEnvironmentName,
465
+ owner: github.owner,
466
+ repo: github.repo,
467
+ secretName: "GH_APP_INSTALLATION_ID",
468
+ secretValue: runnerAppCredentials.installationId,
469
+ }),
470
+ gitHubService.createOrUpdateEnvironmentSecret({
471
+ environmentName: githubEnvironmentName,
472
+ owner: github.owner,
473
+ repo: github.repo,
474
+ secretName: "GH_APP_KEY",
475
+ secretValue: runnerAppCredentials.key.trimEnd(),
476
+ }),
477
+ ]);
478
+ logger.debug("Set GitHub environment secrets for {environmentName}", {
479
+ environmentName: githubEnvironmentName,
480
+ subscriptionId: cloudAccountId,
481
+ });
482
+ }
440
483
  async #storeRunnerAppSecrets({ cloudAccountId, keyVaultName, runnerAppCredentials, }) {
441
484
  const logger = getLogger(["gen", "env"]);
442
485
  const secretClient = new SecretClient(`https://${keyVaultName}.vault.azure.net/`, this.#credential);
@@ -0,0 +1,32 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const mocks = vi.hoisted(() => ({
3
+ oraPromise: vi.fn((promise) => promise),
4
+ tf$: vi.fn(async (...args) => {
5
+ void args;
6
+ return { stdout: '{"user":{"name":"test@example.com"}}' };
7
+ }),
8
+ }));
9
+ vi.mock("ora", () => ({ oraPromise: mocks.oraPromise }));
10
+ vi.mock("../../../execa/terraform.js", () => ({ tf$: mocks.tf$ }));
11
+ import { checkAddEnvironmentPreconditions, checkInitPreconditions, } from "../init.js";
12
+ const calledCommands = () => mocks.tf$.mock.calls.map(([strings, ...values]) => strings.reduce((command, part, index) => command + part + String(values[index] ?? ""), ""));
13
+ describe("init preconditions", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+ it("checkInitPreconditions does not require Azure login", async () => {
18
+ const result = await checkInitPreconditions();
19
+ expect(result.isOk()).toBe(true);
20
+ expect(calledCommands()).toEqual(["terraform -version", "corepack -v"]);
21
+ });
22
+ it("checkAddEnvironmentPreconditions requires Azure login", async () => {
23
+ const result = await checkAddEnvironmentPreconditions();
24
+ expect(result.isOk()).toBe(true);
25
+ expect(calledCommands()).toEqual([
26
+ "terraform -version",
27
+ "az account show",
28
+ "az group list",
29
+ "corepack -v",
30
+ ]);
31
+ });
32
+ });
@@ -16,7 +16,7 @@ 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
18
  import { exitWithError } from "../index.js";
19
- import { checkPreconditions } from "./init.js";
19
+ import { checkAddEnvironmentPreconditions } from "./init.js";
20
20
  /**
21
21
  * Authorize a Cloud Account (Azure Subscription, AWS Account, ...), creating a Pull Request for each account that requires authorization.
22
22
  */
@@ -70,7 +70,7 @@ const displaySummary = (result) => {
70
70
  }
71
71
  console.log(`${step}. Visit ${chalk.underline("https://dx.pagopa.it/getting-started")} to deploy your first project\n`);
72
72
  };
73
- const addEnvironmentAction = (authorizationService, gitHubService) => checkPreconditions()
73
+ const addEnvironmentAction = (authorizationService, gitHubService) => checkAddEnvironmentPreconditions()
74
74
  .andThen(() => ResultAsync.fromPromise(getPlopInstance(), (cause) => new Error("Failed to initialize plop", { cause })))
75
75
  .andThen((plop) => ResultAsync.fromPromise(runDeploymentEnvironmentGenerator(plop, gitHubService), (cause) => new Error("Failed to run the deployment environment generator", {
76
76
  cause,
@@ -1,7 +1,15 @@
1
1
  import { Command } from "commander";
2
2
  import { ResultAsync } from "neverthrow";
3
3
  import { GitHubService } from "../../../domain/github.js";
4
- export declare const checkPreconditions: () => ResultAsync<import("execa").Result<{
4
+ export declare const checkInitPreconditions: () => ResultAsync<import("execa").Result<{
5
+ environment: {
6
+ NO_COLOR: string;
7
+ TF_IN_AUTOMATION: string;
8
+ TF_INPUT: string;
9
+ };
10
+ shell: true;
11
+ }>, Error>;
12
+ export declare const checkAddEnvironmentPreconditions: () => ResultAsync<import("execa").Result<{
5
13
  environment: {
6
14
  NO_COLOR: string;
7
15
  TF_IN_AUTOMATION: string;
@@ -54,7 +54,8 @@ const ensureAzLogin = async () => {
54
54
  };
55
55
  const checkAzLogin = () => withSpinner("Check Azure login status...", (userName) => `You are logged in to Azure (${userName})`, "Please log in to Azure CLI using `az login` before running this command.", ensureAzLogin());
56
56
  // TODO(CES-1810): Make these checks concurrent to speed up the preconditions check phase
57
- export const checkPreconditions = () => checkTerraformCliIsInstalled()
57
+ export const checkInitPreconditions = () => checkTerraformCliIsInstalled().andThen(() => checkCorepackIsInstalled());
58
+ export const checkAddEnvironmentPreconditions = () => checkTerraformCliIsInstalled()
58
59
  .andThen(() => checkAzLogin())
59
60
  .andThen(() => checkCorepackIsInstalled());
60
61
  const createRemoteRepository = ({ repoName, repoOwner, }) => {
@@ -121,7 +122,7 @@ export const makeInitCommand = ({ gitHubService, }) => new Command()
121
122
  .name("init")
122
123
  .description("Initialize a new DX workspace")
123
124
  .action(async function () {
124
- await checkPreconditions()
125
+ await checkInitPreconditions()
125
126
  .andThen(() => ResultAsync.fromPromise(getPlopInstance(), (cause) => new Error("Failed to initialize plop", { cause })))
126
127
  .andThen((plop) => ResultAsync.fromPromise(runMonorepoGenerator(plop, gitHubService), handleGeneratorError))
127
128
  .andTee((payload) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/dx-cli",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "type": "module",
5
5
  "description": "A CLI useful to manage DX tools.",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "@azure/identity": "^4.13.1",
32
32
  "@azure/keyvault-secrets": "^4.11.1",
33
33
  "@azure/storage-blob": "^12.31.0",
34
- "@logtape/logtape": "^1.3.7",
34
+ "@logtape/logtape": "^1.3.8",
35
35
  "@microsoft/microsoft-graph-client": "^3.0.7",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
@@ -39,16 +39,16 @@
39
39
  "execa": "^9.6.1",
40
40
  "glob": "^11.1.0",
41
41
  "inquirer": "^9.3.8",
42
- "libsodium-wrappers": "^0.8.2",
42
+ "libsodium-wrappers": "^0.8.4",
43
43
  "neverthrow": "^8.2.0",
44
44
  "node-plop": "^0.32.3",
45
45
  "octokit": "^5.0.5",
46
- "ora": "^9.3.0",
46
+ "ora": "^9.4.0",
47
47
  "replace-in-file": "^8.4.0",
48
48
  "semver": "^7.7.4",
49
- "yaml": "^2.8.3",
50
- "zod": "^4.3.6",
51
- "@pagopa/dx-savemoney": "^0.2.4"
49
+ "yaml": "^2.8.4",
50
+ "zod": "^4.4.2",
51
+ "@pagopa/dx-savemoney": "^0.2.5"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@tsconfig/node24": "24.0.4",
@@ -57,14 +57,14 @@
57
57
  "@types/node": "^22.19.17",
58
58
  "@types/semver": "^7.7.1",
59
59
  "@vitest/coverage-v8": "^3.2.4",
60
- "eslint": "^10.2.1",
61
- "memfs": "^4.57.1",
60
+ "eslint": "^10.3.0",
61
+ "memfs": "^4.57.2",
62
62
  "plop": "^4.0.5",
63
63
  "prettier": "3.8.3",
64
64
  "typescript": "~5.9.3",
65
65
  "vitest": "^3.2.4",
66
66
  "vitest-mock-extended": "^3.1.1",
67
- "@pagopa/eslint-config": "^6.0.3"
67
+ "@pagopa/eslint-config": "^6.0.4"
68
68
  },
69
69
  "engines": {
70
70
  "node": ">=22.0.0"
@@ -9,6 +9,7 @@ on:
9
9
 
10
10
  permissions:
11
11
  contents: read
12
+ id-token: write
12
13
 
13
14
  jobs:
14
15
  release:
@@ -2,4 +2,5 @@
2
2
  **/*.hbs
3
3
 
4
4
  # Ignore built files
5
- dist
5
+ dist
6
+ CHANGELOG.md