@pagopa/dx-cli 0.15.2 → 0.15.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 (195) hide show
  1. package/bin/index.js +8 -1280
  2. package/dist/adapters/azure/__tests__/cloud-account-repository.test.d.ts +1 -0
  3. package/dist/adapters/azure/__tests__/cloud-account-repository.test.js +95 -0
  4. package/dist/adapters/azure/__tests__/cloud-account-service.test.d.ts +1 -0
  5. package/dist/adapters/azure/__tests__/cloud-account-service.test.js +95 -0
  6. package/dist/adapters/azure/cloud-account-repository.d.ts +12 -0
  7. package/dist/adapters/azure/cloud-account-repository.js +23 -0
  8. package/dist/adapters/azure/cloud-account-service.d.ts +22 -0
  9. package/dist/adapters/azure/cloud-account-service.js +255 -0
  10. package/dist/adapters/azure/locations.d.ts +7 -0
  11. package/dist/adapters/azure/locations.js +21 -0
  12. package/dist/adapters/codemods/__tests__/registry.test.d.ts +1 -0
  13. package/dist/adapters/codemods/__tests__/registry.test.js +59 -0
  14. package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.d.ts +1 -0
  15. package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.js +77 -0
  16. package/dist/adapters/codemods/__tests__/use-pnpm.test.d.ts +1 -0
  17. package/dist/adapters/codemods/__tests__/use-pnpm.test.js +148 -0
  18. package/dist/adapters/codemods/git.d.ts +2 -0
  19. package/dist/adapters/codemods/git.js +18 -0
  20. package/dist/adapters/codemods/index.d.ts +3 -0
  21. package/dist/adapters/codemods/index.js +9 -0
  22. package/dist/adapters/codemods/registry.d.ts +8 -0
  23. package/dist/adapters/codemods/registry.js +16 -0
  24. package/dist/adapters/codemods/update-code-review.d.ts +3 -0
  25. package/dist/adapters/codemods/update-code-review.js +60 -0
  26. package/dist/adapters/codemods/use-azure-appsvc.d.ts +3 -0
  27. package/dist/adapters/codemods/use-azure-appsvc.js +84 -0
  28. package/dist/adapters/codemods/use-pnpm.d.ts +22 -0
  29. package/dist/adapters/codemods/use-pnpm.js +214 -0
  30. package/dist/adapters/codemods/yaml.d.ts +2 -0
  31. package/dist/adapters/codemods/yaml.js +8 -0
  32. package/dist/adapters/commander/commands/codemod.d.ts +8 -0
  33. package/dist/adapters/commander/commands/codemod.js +22 -0
  34. package/dist/adapters/commander/commands/doctor.d.ts +4 -0
  35. package/dist/adapters/commander/commands/doctor.js +12 -0
  36. package/dist/adapters/commander/commands/info.d.ts +3 -0
  37. package/dist/adapters/commander/commands/info.js +9 -0
  38. package/dist/adapters/commander/commands/init.d.ts +7 -0
  39. package/dist/adapters/commander/commands/init.js +126 -0
  40. package/dist/adapters/commander/commands/savemoney.d.ts +2 -0
  41. package/dist/adapters/commander/commands/savemoney.js +26 -0
  42. package/dist/adapters/commander/index.d.ts +7 -0
  43. package/dist/adapters/commander/index.js +19 -0
  44. package/dist/adapters/execa/terraform.d.ts +27 -0
  45. package/dist/adapters/execa/terraform.js +28 -0
  46. package/dist/adapters/github/__tests__/github-repo.spec.d.ts +1 -0
  47. package/dist/adapters/github/__tests__/github-repo.spec.js +67 -0
  48. package/dist/adapters/github/github-repo.d.ts +2 -0
  49. package/dist/adapters/github/github-repo.js +31 -0
  50. package/dist/adapters/logtape/validation-reporter.d.ts +2 -0
  51. package/dist/adapters/logtape/validation-reporter.js +14 -0
  52. package/dist/adapters/node/__tests__/data.d.ts +18 -0
  53. package/dist/adapters/node/__tests__/data.js +22 -0
  54. package/dist/adapters/node/__tests__/package-json.test.d.ts +1 -0
  55. package/dist/adapters/node/__tests__/package-json.test.js +86 -0
  56. package/dist/adapters/node/__tests__/repository.test.d.ts +1 -0
  57. package/dist/adapters/node/__tests__/repository.test.js +77 -0
  58. package/dist/adapters/node/fs/__tests__/file-reader.test.d.ts +1 -0
  59. package/dist/adapters/node/fs/__tests__/file-reader.test.js +80 -0
  60. package/dist/adapters/node/fs/file-reader.d.ts +24 -0
  61. package/dist/adapters/node/fs/file-reader.js +26 -0
  62. package/dist/adapters/node/json/__tests__/index.test.d.ts +1 -0
  63. package/dist/adapters/node/json/__tests__/index.test.js +14 -0
  64. package/dist/adapters/node/json/index.d.ts +2 -0
  65. package/dist/adapters/node/json/index.js +2 -0
  66. package/dist/adapters/node/package-json.d.ts +2 -0
  67. package/dist/adapters/node/package-json.js +22 -0
  68. package/dist/adapters/node/release.d.ts +2 -0
  69. package/dist/adapters/node/release.js +33 -0
  70. package/dist/adapters/node/repository.d.ts +2 -0
  71. package/dist/adapters/node/repository.js +47 -0
  72. package/dist/adapters/octokit/__tests__/index.test.d.ts +1 -0
  73. package/dist/adapters/octokit/__tests__/index.test.js +197 -0
  74. package/dist/adapters/octokit/index.d.ts +24 -0
  75. package/dist/adapters/octokit/index.js +65 -0
  76. package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.d.ts +1 -0
  77. package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +115 -0
  78. package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.d.ts +1 -0
  79. package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +116 -0
  80. package/dist/adapters/plop/actions/fetch-github-release.d.ts +12 -0
  81. package/dist/adapters/plop/actions/fetch-github-release.js +20 -0
  82. package/dist/adapters/plop/actions/get-node-version.d.ts +2 -0
  83. package/dist/adapters/plop/actions/get-node-version.js +9 -0
  84. package/dist/adapters/plop/actions/get-terraform-backend.d.ts +3 -0
  85. package/dist/adapters/plop/actions/get-terraform-backend.js +17 -0
  86. package/dist/adapters/plop/actions/init-cloud-accounts.d.ts +5 -0
  87. package/dist/adapters/plop/actions/init-cloud-accounts.js +13 -0
  88. package/dist/adapters/plop/actions/provision-terraform-backend.d.ts +10 -0
  89. package/dist/adapters/plop/actions/provision-terraform-backend.js +16 -0
  90. package/dist/adapters/plop/actions/semver.d.ts +19 -0
  91. package/dist/adapters/plop/actions/semver.js +27 -0
  92. package/dist/adapters/plop/actions/setup-pnpm.d.ts +2 -0
  93. package/dist/adapters/plop/actions/setup-pnpm.js +26 -0
  94. package/dist/adapters/plop/generators/environment/__tests__/actions.test.d.ts +2 -0
  95. package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +55 -0
  96. package/dist/adapters/plop/generators/environment/actions.d.ts +2 -0
  97. package/dist/adapters/plop/generators/environment/actions.js +54 -0
  98. package/dist/adapters/plop/generators/environment/index.d.ts +3 -0
  99. package/dist/adapters/plop/generators/environment/index.js +19 -0
  100. package/dist/adapters/plop/generators/environment/prompts.d.ts +66 -0
  101. package/dist/adapters/plop/generators/environment/prompts.js +166 -0
  102. package/dist/adapters/plop/generators/monorepo/actions.d.ts +51 -0
  103. package/dist/adapters/plop/generators/monorepo/actions.js +35 -0
  104. package/dist/adapters/plop/generators/monorepo/index.d.ts +6 -0
  105. package/dist/adapters/plop/generators/monorepo/index.js +17 -0
  106. package/dist/adapters/plop/generators/monorepo/prompts.d.ts +10 -0
  107. package/dist/adapters/plop/generators/monorepo/prompts.js +31 -0
  108. package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.d.ts +1 -0
  109. package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.js +113 -0
  110. package/dist/adapters/plop/helpers/env-short.d.ts +3 -0
  111. package/dist/adapters/plop/helpers/env-short.js +9 -0
  112. package/dist/adapters/plop/helpers/resource-prefix.d.ts +5 -0
  113. package/dist/adapters/plop/helpers/resource-prefix.js +18 -0
  114. package/dist/adapters/plop/index.d.ts +8 -0
  115. package/dist/adapters/plop/index.js +24 -0
  116. package/dist/adapters/terraform/fmt.d.ts +1 -0
  117. package/dist/adapters/terraform/fmt.js +17 -0
  118. package/dist/adapters/yaml/__tests__/index.test.d.ts +1 -0
  119. package/dist/adapters/yaml/__tests__/index.test.js +53 -0
  120. package/dist/adapters/yaml/index.d.ts +8 -0
  121. package/dist/adapters/yaml/index.js +9 -0
  122. package/dist/adapters/zod/index.d.ts +23 -0
  123. package/dist/adapters/zod/index.js +22 -0
  124. package/dist/config.d.ts +6 -0
  125. package/dist/config.js +5 -0
  126. package/dist/domain/__tests__/data.d.ts +17 -0
  127. package/dist/domain/__tests__/data.js +27 -0
  128. package/dist/domain/__tests__/environment.test.d.ts +1 -0
  129. package/dist/domain/__tests__/environment.test.js +282 -0
  130. package/dist/domain/__tests__/info.test.d.ts +1 -0
  131. package/dist/domain/__tests__/info.test.js +77 -0
  132. package/dist/domain/__tests__/package-json.test.d.ts +1 -0
  133. package/dist/domain/__tests__/package-json.test.js +39 -0
  134. package/dist/domain/__tests__/repository.test.d.ts +1 -0
  135. package/dist/domain/__tests__/repository.test.js +101 -0
  136. package/dist/domain/__tests__/workspace.test.d.ts +1 -0
  137. package/dist/domain/__tests__/workspace.test.js +57 -0
  138. package/dist/domain/cloud-account.d.ts +28 -0
  139. package/dist/domain/cloud-account.js +12 -0
  140. package/dist/domain/codemod.d.ts +11 -0
  141. package/dist/domain/codemod.js +1 -0
  142. package/dist/domain/dependencies.d.ts +10 -0
  143. package/dist/domain/dependencies.js +1 -0
  144. package/dist/domain/doctor.d.ts +10 -0
  145. package/dist/domain/doctor.js +50 -0
  146. package/dist/domain/environment.d.ts +40 -0
  147. package/dist/domain/environment.js +57 -0
  148. package/dist/domain/github-repo.d.ts +6 -0
  149. package/dist/domain/github-repo.js +8 -0
  150. package/dist/domain/github.d.ts +37 -0
  151. package/dist/domain/github.js +29 -0
  152. package/dist/domain/info.d.ts +11 -0
  153. package/dist/domain/info.js +52 -0
  154. package/dist/domain/package-json.d.ts +42 -0
  155. package/dist/domain/package-json.js +69 -0
  156. package/dist/domain/remote-backend.d.ts +8 -0
  157. package/dist/domain/remote-backend.js +9 -0
  158. package/dist/domain/repository.d.ts +13 -0
  159. package/dist/domain/repository.js +72 -0
  160. package/dist/domain/validation.d.ts +16 -0
  161. package/dist/domain/validation.js +1 -0
  162. package/dist/domain/workspace.d.ts +9 -0
  163. package/dist/domain/workspace.js +32 -0
  164. package/dist/index.d.ts +2 -0
  165. package/dist/index.js +35 -0
  166. package/dist/use-cases/__tests__/apply-codemod.test.d.ts +1 -0
  167. package/dist/use-cases/__tests__/apply-codemod.test.js +73 -0
  168. package/dist/use-cases/__tests__/list-codemods.test.d.ts +1 -0
  169. package/dist/use-cases/__tests__/list-codemods.test.js +38 -0
  170. package/dist/use-cases/apply-codemod.d.ts +5 -0
  171. package/dist/use-cases/apply-codemod.js +14 -0
  172. package/dist/use-cases/list-codemods.d.ts +4 -0
  173. package/dist/use-cases/list-codemods.js +1 -0
  174. package/package.json +19 -8
  175. package/templates/environment/bootstrapper/{{env.name}}/data.tf.hbs +13 -0
  176. package/templates/environment/bootstrapper/{{env.name}}/main.tf.hbs +70 -0
  177. package/templates/environment/bootstrapper/{{env.name}}/providers.tf.hbs +26 -0
  178. package/templates/environment/core/{{env.name}}/main.tf.hbs +21 -0
  179. package/templates/environment/core/{{env.name}}/outputs.tf.hbs +8 -0
  180. package/templates/environment/core/{{env.name}}/providers.tf.hbs +21 -0
  181. package/templates/environment/shared/backend.tf.hbs +14 -0
  182. package/templates/environment/shared/locals.tf.hbs +26 -0
  183. package/templates/monorepo/.editorconfig +8 -0
  184. package/templates/monorepo/.node-version.hbs +1 -0
  185. package/templates/monorepo/.pre-commit-config.yaml.hbs +34 -0
  186. package/templates/monorepo/.prettierignore +5 -0
  187. package/templates/monorepo/.terraform-version.hbs +1 -0
  188. package/templates/monorepo/.trivyignore +16 -0
  189. package/templates/monorepo/README.md.hbs +163 -0
  190. package/templates/monorepo/infra/repository/main.tf.hbs +11 -0
  191. package/templates/monorepo/infra/repository/outputs.tf.hbs +11 -0
  192. package/templates/monorepo/infra/repository/providers.tf.hbs +13 -0
  193. package/templates/monorepo/package.json.hbs +7 -0
  194. package/templates/monorepo/pnpm-workspace.yaml +7 -0
  195. package/templates/monorepo/turbo.json +29 -0
@@ -0,0 +1,197 @@
1
+ import { err, ok } from "neverthrow";
2
+ import { RequestError } from "octokit";
3
+ import { SemVer } from "semver";
4
+ import { beforeEach, describe, expect, it } from "vitest";
5
+ import { mockDeep, mockReset } from "vitest-mock-extended";
6
+ import { PullRequest, Repository, RepositoryNotFoundError, } from "../../../domain/github.js";
7
+ import { fetchLatestRelease, fetchLatestTag } from "../index.js";
8
+ import { OctokitGitHubService } from "../index.js";
9
+ const makeEnv = () => {
10
+ const mockOctokit = mockDeep();
11
+ const githubService = new OctokitGitHubService(mockOctokit);
12
+ return {
13
+ githubService,
14
+ mockOctokit,
15
+ };
16
+ };
17
+ describe("OctokitGitHubService", () => {
18
+ describe("getRepository", () => {
19
+ it("should return a Repository when the repository exists", async () => {
20
+ const { githubService, mockOctokit } = makeEnv();
21
+ const owner = "pagopa";
22
+ const name = "dx";
23
+ const mockResponse = {
24
+ data: {
25
+ full_name: "pagopa/dx",
26
+ name: "dx",
27
+ owner: {
28
+ login: "pagopa",
29
+ },
30
+ },
31
+ };
32
+ mockOctokit.rest.repos.get.mockResolvedValue(mockResponse);
33
+ const result = await githubService.getRepository(owner, name);
34
+ expect(result).toBeInstanceOf(Repository);
35
+ expect(result.name).toBe("dx");
36
+ expect(result.owner).toBe("pagopa");
37
+ expect(result.fullName).toBe("pagopa/dx");
38
+ expect(result.url).toBe("https://github.com/pagopa/dx");
39
+ expect(result.ssh).toBe("git@github.com:pagopa/dx.git");
40
+ expect(mockOctokit.rest.repos.get).toHaveBeenCalledWith({
41
+ owner,
42
+ repo: name,
43
+ });
44
+ });
45
+ it("should throw an error when the repository does not exist (404)", async () => {
46
+ const { githubService, mockOctokit } = makeEnv();
47
+ const owner = "pagopa";
48
+ const name = "non-existent";
49
+ const error = new RequestError("Not Found", 404, {
50
+ request: {
51
+ headers: {},
52
+ method: "GET",
53
+ url: "https://api.github.com/repos/pagopa/non-existent",
54
+ },
55
+ response: {
56
+ data: { message: "Not Found" },
57
+ headers: {},
58
+ status: 404,
59
+ url: "https://api.github.com/repos/pagopa/non-existent",
60
+ },
61
+ });
62
+ mockOctokit.rest.repos.get.mockRejectedValue(error);
63
+ await expect(githubService.getRepository(owner, name)).rejects.toThrow(RepositoryNotFoundError);
64
+ await expect(githubService.getRepository(owner, name)).rejects.toThrowError("Repository pagopa/non-existent not found");
65
+ });
66
+ it("should throw an error when the API call fails", async () => {
67
+ const { githubService, mockOctokit } = makeEnv();
68
+ const owner = "pagopa";
69
+ const name = "dx";
70
+ const error = new RequestError("API Error", 500, {
71
+ request: {
72
+ headers: {},
73
+ method: "GET",
74
+ url: "https://api.github.com/repos/pagopa/dx",
75
+ },
76
+ response: {
77
+ data: { message: "API Error" },
78
+ headers: {},
79
+ status: 500,
80
+ url: "https://api.github.com/repos/pagopa/dx",
81
+ },
82
+ });
83
+ mockOctokit.rest.repos.get.mockRejectedValue(error);
84
+ await expect(githubService.getRepository(owner, name)).rejects.toThrowError("Failed to fetch repository pagopa/dx");
85
+ });
86
+ });
87
+ describe("createPullRequest", () => {
88
+ it("should return a PullRequest when creation succeeds", async () => {
89
+ const { githubService, mockOctokit } = makeEnv();
90
+ const params = {
91
+ base: "main",
92
+ body: "This is a test PR",
93
+ head: "feature-branch",
94
+ owner: "pagopa",
95
+ repo: "dx",
96
+ title: "Test Pull Request",
97
+ };
98
+ const mockResponse = {
99
+ data: {
100
+ html_url: "https://github.com/pagopa/dx/pull/123",
101
+ },
102
+ };
103
+ mockOctokit.rest.pulls.create.mockResolvedValue(mockResponse);
104
+ const result = await githubService.createPullRequest(params);
105
+ expect(result).toBeInstanceOf(PullRequest);
106
+ expect(result.url).toBe("https://github.com/pagopa/dx/pull/123");
107
+ expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
108
+ base: params.base,
109
+ body: params.body,
110
+ head: params.head,
111
+ owner: params.owner,
112
+ repo: params.repo,
113
+ title: params.title,
114
+ });
115
+ });
116
+ it("should throw an error when PR creation fails", async () => {
117
+ const { githubService, mockOctokit } = makeEnv();
118
+ const params = {
119
+ base: "main",
120
+ body: "This is a test PR",
121
+ head: "feature-branch",
122
+ owner: "pagopa",
123
+ repo: "dx",
124
+ title: "Test Pull Request",
125
+ };
126
+ const error = new RequestError("Validation Failed", 422, {
127
+ request: {
128
+ headers: {},
129
+ method: "POST",
130
+ url: "https://api.github.com/repos/pagopa/dx/pulls",
131
+ },
132
+ response: {
133
+ data: { message: "Validation Failed" },
134
+ headers: {},
135
+ status: 422,
136
+ url: "https://api.github.com/repos/pagopa/dx/pulls",
137
+ },
138
+ });
139
+ mockOctokit.rest.pulls.create.mockRejectedValue(error);
140
+ await expect(githubService.createPullRequest(params)).rejects.toThrowError("Failed to create pull request in pagopa/dx");
141
+ });
142
+ });
143
+ });
144
+ describe("octokit adapter", () => {
145
+ const owner = "test-owner";
146
+ const repo = "test-repo";
147
+ const client = mockDeep();
148
+ beforeEach(() => {
149
+ mockReset(client);
150
+ });
151
+ describe("fetchLatestTag", () => {
152
+ it("should return the highest valid semver tag", async () => {
153
+ client.request.mockResolvedValueOnce({
154
+ data: [
155
+ { name: "v1.0.0" },
156
+ { name: "2.0.0-beta.1" },
157
+ { name: "2.0.0" },
158
+ { name: "not-a-version" },
159
+ ],
160
+ headers: {},
161
+ status: 200,
162
+ url: "",
163
+ });
164
+ const result = await fetchLatestTag({ client, owner, repo });
165
+ expect(result).toStrictEqual(ok(new SemVer("2.0.0")));
166
+ expect(client.request).toHaveBeenCalledWith("GET /repos/{owner}/{repo}/tags", { owner, repo });
167
+ });
168
+ it("returns null if there are no valid semver tags", async () => {
169
+ client.request.mockResolvedValueOnce({
170
+ data: [{ name: "foo" }, { name: "bar" }],
171
+ headers: {},
172
+ status: 200,
173
+ url: "",
174
+ });
175
+ const result = await fetchLatestTag({ client, owner, repo });
176
+ expect(result).toStrictEqual(ok(null));
177
+ });
178
+ it("should return an error when client promise fails", async () => {
179
+ client.request.mockRejectedValueOnce(new Error("an error"));
180
+ const result = await fetchLatestTag({ client, owner, repo });
181
+ expect(result).toStrictEqual(err(new Error("Failed to fetch tags for test-owner/test-repo")));
182
+ });
183
+ });
184
+ describe("fetchLatestRelease", () => {
185
+ it("should return parsed semver for the latest release tag", async () => {
186
+ client.request.mockResolvedValueOnce({
187
+ data: { tag_name: "v1.2.3" },
188
+ headers: {},
189
+ status: 200,
190
+ url: "",
191
+ });
192
+ const result = await fetchLatestRelease({ client, owner, repo });
193
+ expect(result).toStrictEqual(ok(new SemVer("v1.2.3")));
194
+ expect(client.request).toHaveBeenCalledWith("GET /repos/{owner}/{repo}/releases/latest", { owner, repo });
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,24 @@
1
+ import { ResultAsync } from "neverthrow";
2
+ import { Octokit } from "octokit";
3
+ import { GitHubService, PullRequest, Repository } from "../../domain/github.js";
4
+ type GitHubReleaseParam = {
5
+ client: Octokit;
6
+ owner: string;
7
+ repo: string;
8
+ };
9
+ export declare class OctokitGitHubService implements GitHubService {
10
+ #private;
11
+ constructor(octokit: Octokit);
12
+ createPullRequest(params: {
13
+ base: string;
14
+ body: string;
15
+ head: string;
16
+ owner: string;
17
+ repo: string;
18
+ title: string;
19
+ }): Promise<PullRequest>;
20
+ getRepository(owner: string, name: string): Promise<Repository>;
21
+ }
22
+ export declare const fetchLatestTag: ({ client, owner, repo }: GitHubReleaseParam) => ResultAsync<import("semver").SemVer | null, Error>;
23
+ export declare const fetchLatestRelease: ({ client, owner, repo, }: GitHubReleaseParam) => ResultAsync<import("semver").SemVer | null, Error>;
24
+ export {};
@@ -0,0 +1,65 @@
1
+ import { ResultAsync } from "neverthrow";
2
+ import { RequestError } from "octokit";
3
+ import semverParse from "semver/functions/parse.js";
4
+ import semverSort from "semver/functions/sort.js";
5
+ import { PullRequest, Repository, RepositoryNotFoundError, } from "../../domain/github.js";
6
+ export class OctokitGitHubService {
7
+ #octokit;
8
+ constructor(octokit) {
9
+ this.#octokit = octokit;
10
+ }
11
+ async createPullRequest(params) {
12
+ try {
13
+ const { data } = await this.#octokit.rest.pulls.create({
14
+ base: params.base,
15
+ body: params.body,
16
+ head: params.head,
17
+ owner: params.owner,
18
+ repo: params.repo,
19
+ title: params.title,
20
+ });
21
+ return new PullRequest(data.html_url);
22
+ }
23
+ catch (error) {
24
+ throw new Error(`Failed to create pull request in ${params.owner}/${params.repo}`, {
25
+ cause: error,
26
+ });
27
+ }
28
+ }
29
+ async getRepository(owner, name) {
30
+ try {
31
+ const { data } = await this.#octokit.rest.repos.get({
32
+ owner,
33
+ repo: name,
34
+ });
35
+ return new Repository(data.name, data.owner.login);
36
+ }
37
+ catch (error) {
38
+ if (error instanceof RequestError && error.status === 404) {
39
+ throw new RepositoryNotFoundError(owner, name);
40
+ }
41
+ throw new Error(`Failed to fetch repository ${owner}/${name}`, {
42
+ cause: error,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ export const fetchLatestTag = ({ client, owner, repo }) => ResultAsync.fromPromise(
48
+ // Get repository tags
49
+ client.request("GET /repos/{owner}/{repo}/tags", {
50
+ owner,
51
+ repo,
52
+ }), () => new Error(`Failed to fetch tags for ${owner}/${repo}`))
53
+ .map(({ data }) => data.map(({ name }) => name))
54
+ // Filter out tags that are not valid semver
55
+ .map((tags) => tags.map((tag) => semverParse(tag)).filter((tag) => tag !== null))
56
+ // Sort tags in ascending order
57
+ .map(semverSort)
58
+ // Get the latest tag
59
+ .map((tags) => tags.pop() ?? null);
60
+ export const fetchLatestRelease = ({ client, owner, repo, }) => ResultAsync.fromPromise(
61
+ // Get the latest release for a repository
62
+ client.request("GET /repos/{owner}/{repo}/releases/latest", {
63
+ owner,
64
+ repo,
65
+ }), () => new Error(`Failed to fetch latest release for ${owner}/${repo}`)).map(({ data }) => semverParse(data.tag_name));
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { initCloudAccounts } from "../init-cloud-accounts.js";
3
+ const createMockCloudAccountService = (overrides = {}) => ({
4
+ getTerraformBackend: vi.fn().mockResolvedValue(undefined),
5
+ hasUserPermissionToInitialize: vi.fn().mockResolvedValue(true),
6
+ initialize: vi.fn().mockResolvedValue(undefined),
7
+ isInitialized: vi.fn().mockResolvedValue(true),
8
+ provisionTerraformBackend: vi.fn().mockResolvedValue(undefined),
9
+ ...overrides,
10
+ });
11
+ const createMockCloudAccount = (overrides = {}) => ({
12
+ csp: "azure",
13
+ defaultLocation: "westeurope",
14
+ displayName: "Test-Account",
15
+ id: "test-subscription-id",
16
+ ...overrides,
17
+ });
18
+ const createMockPayload = (overrides = {}) => ({
19
+ env: {
20
+ cloudAccounts: [createMockCloudAccount()],
21
+ name: "dev",
22
+ prefix: "dx",
23
+ },
24
+ github: {
25
+ owner: "pagopa",
26
+ repo: "dx",
27
+ },
28
+ init: {
29
+ cloudAccountsToInitialize: [],
30
+ terraformBackend: {
31
+ cloudAccount: createMockCloudAccount(),
32
+ },
33
+ },
34
+ tags: {},
35
+ workspace: {
36
+ domain: "test",
37
+ },
38
+ ...overrides,
39
+ });
40
+ describe("initCloudAccounts", () => {
41
+ it("should initialize all cloud accounts in cloudAccountsToInitialize", async () => {
42
+ const cloudAccount1 = createMockCloudAccount({ id: "account-1" });
43
+ const cloudAccount2 = createMockCloudAccount({ id: "account-2" });
44
+ const initializeMock = vi.fn().mockResolvedValue(undefined);
45
+ const mockService = createMockCloudAccountService({
46
+ initialize: initializeMock,
47
+ });
48
+ const payload = createMockPayload({
49
+ env: {
50
+ cloudAccounts: [],
51
+ name: "prod",
52
+ prefix: "io",
53
+ },
54
+ init: {
55
+ cloudAccountsToInitialize: [cloudAccount1, cloudAccount2],
56
+ terraformBackend: {
57
+ cloudAccount: createMockCloudAccount(),
58
+ },
59
+ },
60
+ });
61
+ await initCloudAccounts(payload, mockService);
62
+ expect(initializeMock).toHaveBeenCalledTimes(2);
63
+ expect(initializeMock).toHaveBeenCalledWith(cloudAccount1, expect.objectContaining({ name: "prod", prefix: "io" }), {});
64
+ expect(initializeMock).toHaveBeenCalledWith(cloudAccount2, expect.objectContaining({ name: "prod", prefix: "io" }), {});
65
+ });
66
+ it("should not call initialize when cloudAccountsToInitialize is empty", async () => {
67
+ const initializeMock = vi.fn().mockResolvedValue(undefined);
68
+ const mockService = createMockCloudAccountService({
69
+ initialize: initializeMock,
70
+ });
71
+ const payload = createMockPayload({
72
+ init: {
73
+ cloudAccountsToInitialize: [],
74
+ terraformBackend: {
75
+ cloudAccount: createMockCloudAccount(),
76
+ },
77
+ },
78
+ });
79
+ await initCloudAccounts(payload, mockService);
80
+ expect(initializeMock).not.toHaveBeenCalled();
81
+ });
82
+ it("should not call initialize when payload.init is undefined", async () => {
83
+ const initializeMock = vi.fn().mockResolvedValue(undefined);
84
+ const mockService = createMockCloudAccountService({
85
+ initialize: initializeMock,
86
+ });
87
+ const payload = createMockPayload({
88
+ init: undefined,
89
+ });
90
+ await initCloudAccounts(payload, mockService);
91
+ expect(initializeMock).not.toHaveBeenCalled();
92
+ });
93
+ it("should use prefix and environment name from payload.env", async () => {
94
+ const cloudAccount = createMockCloudAccount();
95
+ const initializeMock = vi.fn().mockResolvedValue(undefined);
96
+ const mockService = createMockCloudAccountService({
97
+ initialize: initializeMock,
98
+ });
99
+ const payload = createMockPayload({
100
+ env: {
101
+ cloudAccounts: [],
102
+ name: "uat",
103
+ prefix: "pagopa",
104
+ },
105
+ init: {
106
+ cloudAccountsToInitialize: [cloudAccount],
107
+ terraformBackend: {
108
+ cloudAccount: createMockCloudAccount(),
109
+ },
110
+ },
111
+ });
112
+ await initCloudAccounts(payload, mockService);
113
+ expect(initializeMock).toHaveBeenCalledWith(cloudAccount, expect.objectContaining({ name: "uat", prefix: "pagopa" }), {});
114
+ });
115
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { provisionTerraformBackend } from "../provision-terraform-backend.js";
3
+ const createMockCloudAccountService = (overrides = {}) => ({
4
+ getTerraformBackend: vi.fn().mockResolvedValue(undefined),
5
+ hasUserPermissionToInitialize: vi.fn().mockResolvedValue(true),
6
+ initialize: vi.fn().mockResolvedValue(undefined),
7
+ isInitialized: vi.fn().mockResolvedValue(true),
8
+ provisionTerraformBackend: vi.fn().mockResolvedValue(undefined),
9
+ ...overrides,
10
+ });
11
+ const createMockCloudAccount = (overrides = {}) => ({
12
+ csp: "azure",
13
+ defaultLocation: "westeurope",
14
+ displayName: "Test-Account",
15
+ id: "test-subscription-id",
16
+ ...overrides,
17
+ });
18
+ const createMockTerraformBackend = (overrides = {}) => ({
19
+ resourceGroupName: "dx-d-itn-tf-rg",
20
+ storageAccountName: "dxditntfst",
21
+ subscriptionId: "00000000-0000-0000-0000-000000000000",
22
+ type: "azurerm",
23
+ ...overrides,
24
+ });
25
+ const createMockPayload = (overrides = {}) => ({
26
+ env: {
27
+ cloudAccounts: [createMockCloudAccount()],
28
+ name: "dev",
29
+ prefix: "dx",
30
+ },
31
+ github: {
32
+ owner: "pagopa",
33
+ repo: "dx",
34
+ },
35
+ init: {
36
+ cloudAccountsToInitialize: [],
37
+ terraformBackend: {
38
+ cloudAccount: createMockCloudAccount(),
39
+ },
40
+ },
41
+ tags: {},
42
+ workspace: {
43
+ domain: "test",
44
+ },
45
+ ...overrides,
46
+ });
47
+ describe("provisionTerraformBackend", () => {
48
+ it("should provision terraform backend and return it", async () => {
49
+ const mockBackend = createMockTerraformBackend();
50
+ const provisionMock = vi.fn().mockResolvedValue(mockBackend);
51
+ const mockService = createMockCloudAccountService({
52
+ provisionTerraformBackend: provisionMock,
53
+ });
54
+ const payload = createMockPayload();
55
+ const result = await provisionTerraformBackend(payload, mockService);
56
+ expect(result).toEqual(mockBackend);
57
+ });
58
+ it("should call provisionTerraformBackend with correct parameters", async () => {
59
+ const cloudAccount = createMockCloudAccount({ id: "my-subscription" });
60
+ const provisionMock = vi
61
+ .fn()
62
+ .mockResolvedValue(createMockTerraformBackend());
63
+ const mockService = createMockCloudAccountService({
64
+ provisionTerraformBackend: provisionMock,
65
+ });
66
+ const payload = createMockPayload({
67
+ env: {
68
+ cloudAccounts: [cloudAccount],
69
+ name: "prod",
70
+ prefix: "io",
71
+ },
72
+ init: {
73
+ cloudAccountsToInitialize: [],
74
+ terraformBackend: {
75
+ cloudAccount,
76
+ },
77
+ },
78
+ });
79
+ await provisionTerraformBackend(payload, mockService);
80
+ expect(provisionMock).toHaveBeenCalledWith(cloudAccount, expect.objectContaining({ name: "prod", prefix: "io" }), {});
81
+ });
82
+ it("should throw an error when payload.init is undefined", async () => {
83
+ const mockService = createMockCloudAccountService();
84
+ const payload = createMockPayload({
85
+ init: undefined,
86
+ });
87
+ await expect(provisionTerraformBackend(payload, mockService)).rejects.toThrow("This action requires initialization data in the payload");
88
+ });
89
+ it("should use the terraformBackend cloudAccount from init", async () => {
90
+ const envCloudAccount = createMockCloudAccount({ id: "env-account" });
91
+ const backendCloudAccount = createMockCloudAccount({
92
+ id: "backend-account",
93
+ });
94
+ const provisionMock = vi
95
+ .fn()
96
+ .mockResolvedValue(createMockTerraformBackend());
97
+ const mockService = createMockCloudAccountService({
98
+ provisionTerraformBackend: provisionMock,
99
+ });
100
+ const payload = createMockPayload({
101
+ env: {
102
+ cloudAccounts: [envCloudAccount],
103
+ name: "uat",
104
+ prefix: "dx",
105
+ },
106
+ init: {
107
+ cloudAccountsToInitialize: [],
108
+ terraformBackend: {
109
+ cloudAccount: backendCloudAccount,
110
+ },
111
+ },
112
+ });
113
+ await provisionTerraformBackend(payload, mockService);
114
+ expect(provisionMock).toHaveBeenCalledWith(backendCloudAccount, expect.objectContaining({ name: "uat", prefix: "dx" }), {});
115
+ });
116
+ });
@@ -0,0 +1,12 @@
1
+ import { type NodePlopAPI } from "node-plop";
2
+ import { Octokit } from "octokit";
3
+ import { z } from "zod/v4";
4
+ export declare const semverFetchOptionsSchema: z.ZodObject<{
5
+ repository: z.ZodObject<{
6
+ name: z.ZodString;
7
+ owner: z.ZodString;
8
+ }, z.core.$strip>;
9
+ resultKey: z.ZodString;
10
+ }, z.core.$strip>;
11
+ export type SemverFetchOptions = z.infer<typeof semverFetchOptionsSchema>;
12
+ export default function (plop: NodePlopAPI, octokit: Octokit): void;
@@ -0,0 +1,20 @@
1
+ import { z } from "zod/v4";
2
+ import { fetchLatestRelease } from "../../octokit/index.js";
3
+ import { fetchLatestSemver } from "./semver.js";
4
+ export const semverFetchOptionsSchema = z.object({
5
+ repository: z.object({
6
+ name: z.string(),
7
+ owner: z.string(),
8
+ }),
9
+ resultKey: z.string(),
10
+ });
11
+ export default function (plop, octokit) {
12
+ plop.setActionType("fetchGithubRelease", async (data, ctx) => {
13
+ const { repository, resultKey } = semverFetchOptionsSchema.parse(ctx);
14
+ return fetchLatestSemver(() => fetchLatestRelease({
15
+ client: octokit,
16
+ owner: repository.owner,
17
+ repo: repository.name,
18
+ }), data, resultKey);
19
+ });
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type NodePlopAPI } from "node-plop";
2
+ export default function (plop: NodePlopAPI): void;
@@ -0,0 +1,9 @@
1
+ import { ResultAsync } from "neverthrow";
2
+ import { getLatestByCodename } from "../../node/release.js";
3
+ import { fetchLatestSemver } from "./semver.js";
4
+ const fetchNodeVersion = () => ResultAsync.fromPromise(
5
+ // Jod is the codename for Node.js 22 LTS
6
+ getLatestByCodename("Jod"), (e) => new Error("Failed to fetch Node.js releases", { cause: e }));
7
+ export default function (plop) {
8
+ plop.setActionType("getNodeVersion", async (data) => fetchLatestSemver(fetchNodeVersion, data, "nodeVersion"));
9
+ }
@@ -0,0 +1,3 @@
1
+ import { type NodePlopAPI } from "node-plop";
2
+ import { CloudAccountService } from "../../../domain/cloud-account.js";
3
+ export default function (plop: NodePlopAPI, cloudAccountService: CloudAccountService): void;
@@ -0,0 +1,17 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import { getTerraformBackend } from "../../../domain/environment.js";
3
+ import { payloadSchema } from "../generators/environment/prompts.js";
4
+ export default function (plop, cloudAccountService) {
5
+ plop.setActionType("getTerraformBackend", async (data) => {
6
+ const logger = getLogger(["gen", "env"]);
7
+ if (data.terraform?.backend) {
8
+ return "Terraform Backend Retrieved";
9
+ }
10
+ const payload = payloadSchema.parse(data);
11
+ const backend = await getTerraformBackend(cloudAccountService, payload.env);
12
+ logger.debug("Retrieved terraform backend {backend}", { backend });
13
+ data.terraform ??= {};
14
+ data.terraform.backend = backend;
15
+ return "Terraform Backend Retrieved";
16
+ });
17
+ }
@@ -0,0 +1,5 @@
1
+ import { type NodePlopAPI } from "node-plop";
2
+ import { CloudAccountService } from "../../../domain/cloud-account.js";
3
+ 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;
@@ -0,0 +1,13 @@
1
+ import { payloadSchema, } from "../generators/environment/prompts.js";
2
+ export const initCloudAccounts = async (payload, cloudAccountService) => {
3
+ if (payload.init) {
4
+ await Promise.all(payload.init.cloudAccountsToInitialize.map((cloudAccount) => cloudAccountService.initialize(cloudAccount, payload.env, payload.tags)));
5
+ }
6
+ };
7
+ export default function (plop, cloudAccountService) {
8
+ plop.setActionType("initCloudAccounts", async (data) => {
9
+ const payload = payloadSchema.parse(data);
10
+ await initCloudAccounts(payload, cloudAccountService);
11
+ return "Cloud Accounts Initialized";
12
+ });
13
+ }
@@ -0,0 +1,10 @@
1
+ import { type NodePlopAPI } from "node-plop";
2
+ import { CloudAccountService } from "../../../domain/cloud-account.js";
3
+ import { type Payload } from "../generators/environment/prompts.js";
4
+ export declare const provisionTerraformBackend: (payload: Payload, cloudAccountService: CloudAccountService) => Promise<{
5
+ resourceGroupName: string;
6
+ storageAccountName: string;
7
+ subscriptionId: string;
8
+ type: "azurerm";
9
+ }>;
10
+ export default function (plop: NodePlopAPI, cloudAccountService: CloudAccountService): void;
@@ -0,0 +1,16 @@
1
+ import * as assert from "node:assert/strict";
2
+ import { payloadSchema, } from "../generators/environment/prompts.js";
3
+ export const provisionTerraformBackend = async (payload, cloudAccountService) => {
4
+ assert.ok(payload.init, "This action requires initialization data in the payload");
5
+ assert.ok(payload.init.terraformBackend, "This action requires terraformBackend data in the payload");
6
+ const terraformBackend = await cloudAccountService.provisionTerraformBackend(payload.init.terraformBackend.cloudAccount, payload.env, payload.tags);
7
+ return terraformBackend;
8
+ };
9
+ export default function (plop, cloudAccountService) {
10
+ plop.setActionType("provisionTerraformBackend", async (data) => {
11
+ const payload = payloadSchema.parse(data);
12
+ data.terraform ??= {};
13
+ data.terraform.backend = await provisionTerraformBackend(payload, cloudAccountService);
14
+ return "Terraform Backend Provisioned";
15
+ });
16
+ }
@@ -0,0 +1,19 @@
1
+ import { ResultAsync } from "neverthrow";
2
+ import { SemVer } from "semver";
3
+ export type FetchSemverFn = () => ResultAsync<null | SemVer, Error>;
4
+ /**
5
+ * Fetches the latest semantic version using the provided fetch function and writes
6
+ * a formatted version string into the given `answers` object under `answerKey`.
7
+ *
8
+ * @param fetchSemverFn - A zero-arg function that returns a `ResultAsync` resolving
9
+ * to a `SemVer` (or `null`) or rejecting with an `Error`. Typically wraps an
10
+ * Octokit call to fetch a release or tag and parse its semver.
11
+ * @param answers - Mutable answers object (plop prompts) where the resulting
12
+ * formatted version will be stored.
13
+ * @param answerKey - Key name to assign the formatted version into `answers`.
14
+ * @param semverFormatFn - Optional formatter that converts the `SemVer` into
15
+ * the desired string representation (defaults to `semver.toString()`).
16
+ * @returns A human-readable message indicating the fetched version. Throws an
17
+ * `Error` if the fetch fails or yields an invalid version.
18
+ */
19
+ export declare const fetchLatestSemver: (fetchSemverFn: FetchSemverFn, answers: Record<string, unknown>, answerKey: string, semverFormatFn?: (semver: SemVer) => string) => Promise<string>;