@pagopa/dx-cli 0.22.3 → 0.23.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 +18 -0
- package/dist/adapters/commander/__tests__/spec.test.d.ts +8 -0
- package/dist/adapters/commander/__tests__/spec.test.js +264 -0
- package/dist/adapters/commander/commands/__tests__/init-command.test.d.ts +4 -0
- package/dist/adapters/commander/commands/__tests__/init-command.test.js +44 -0
- package/dist/adapters/commander/commands/__tests__/savemoney.test.d.ts +8 -0
- package/dist/adapters/commander/commands/__tests__/savemoney.test.js +60 -0
- package/dist/adapters/commander/commands/__tests__/spec.test.d.ts +7 -0
- package/dist/adapters/commander/commands/__tests__/spec.test.js +85 -0
- package/dist/adapters/commander/commands/add.d.ts +2 -6
- package/dist/adapters/commander/commands/add.js +4 -8
- package/dist/adapters/commander/commands/init.d.ts +2 -6
- package/dist/adapters/commander/commands/init.js +6 -5
- package/dist/adapters/commander/commands/savemoney.d.ts +11 -0
- package/dist/adapters/commander/commands/savemoney.js +30 -5
- package/dist/adapters/commander/commands/spec.d.ts +10 -0
- package/dist/adapters/commander/commands/spec.js +13 -0
- package/dist/adapters/commander/index.js +6 -2
- package/dist/adapters/commander/presenters/__tests__/index.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/index.test.js +23 -0
- package/dist/adapters/commander/presenters/__tests__/json.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/json.test.js +108 -0
- package/dist/adapters/commander/presenters/__tests__/text.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/text.test.js +60 -0
- package/dist/adapters/commander/presenters/index.d.ts +27 -0
- package/dist/adapters/commander/presenters/index.js +16 -0
- package/dist/adapters/commander/presenters/json-command-presenter.d.ts +19 -0
- package/dist/adapters/commander/presenters/json-command-presenter.js +26 -0
- package/dist/adapters/commander/presenters/text-command-presenter.d.ts +6 -0
- package/dist/adapters/commander/presenters/text-command-presenter.js +21 -0
- package/dist/adapters/commander/spec.d.ts +16 -0
- package/dist/adapters/commander/spec.js +54 -0
- package/dist/adapters/plop/generators/__tests__/temp-dir.d.ts +2 -0
- package/dist/adapters/plop/generators/__tests__/temp-dir.js +13 -0
- package/dist/adapters/plop/generators/environment/__tests__/generation.test.d.ts +1 -0
- package/dist/adapters/plop/generators/environment/__tests__/generation.test.js +213 -0
- package/dist/adapters/plop/generators/environment/actions.js +3 -3
- package/dist/adapters/plop/generators/monorepo/__tests__/generation.test.d.ts +1 -0
- package/dist/adapters/plop/generators/monorepo/__tests__/generation.test.js +79 -0
- package/dist/adapters/plop/index.js +3 -5
- package/dist/adapters/plop/templates-path.d.ts +5 -0
- package/dist/adapters/plop/templates-path.js +6 -0
- package/dist/domain/__tests__/data.d.ts +3 -7
- package/dist/domain/__tests__/data.js +2 -4
- package/dist/domain/dependencies.d.ts +15 -2
- package/dist/domain/spec.d.ts +50 -0
- package/dist/domain/spec.js +8 -0
- package/dist/index.js +15 -12
- package/package.json +2 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import nodePlop from "node-plop";
|
|
2
|
+
/**
|
|
3
|
+
* Contract tests for the environment generator.
|
|
4
|
+
*
|
|
5
|
+
* They generate real files in a temp directory while asserting only
|
|
6
|
+
* generator-specific behavior: which domain actions run for a given
|
|
7
|
+
* generator state and which high-value values are materialized into
|
|
8
|
+
* the generated infrastructure files.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
14
|
+
import setGetTerraformBackend from "../../../actions/get-terraform-backend.js";
|
|
15
|
+
import setInitCloudAccountsAction from "../../../actions/init-cloud-accounts.js";
|
|
16
|
+
import setProvisionTerraformBackendAction from "../../../actions/provision-terraform-backend.js";
|
|
17
|
+
import setEnvShortHelper from "../../../helpers/env-short.js";
|
|
18
|
+
import setEqHelper from "../../../helpers/eq.js";
|
|
19
|
+
import setResourcePrefixHelper from "../../../helpers/resource-prefix.js";
|
|
20
|
+
import setTerraformStateKeyHelper from "../../../helpers/terraform-state-key.js";
|
|
21
|
+
import { resolveTemplatesPath } from "../../../templates-path.js";
|
|
22
|
+
import { cleanupTempDir, readGeneratedFiles, } from "../../__tests__/temp-dir.js";
|
|
23
|
+
import getActions from "../actions.js";
|
|
24
|
+
import { PLOP_ENVIRONMENT_GENERATOR_NAME } from "../index.js";
|
|
25
|
+
vi.mock("../../../../terraform/fmt.js", () => ({
|
|
26
|
+
formatTerraformCode: vi.fn((content) => content),
|
|
27
|
+
}));
|
|
28
|
+
/**
|
|
29
|
+
* Register helpers and stub action types for the environment generator.
|
|
30
|
+
* Cloud-service action types are registered with DI mock objects.
|
|
31
|
+
*/
|
|
32
|
+
const registerEnvironmentSetup = (plop, mockCloudAccountService, mockGitHubService) => {
|
|
33
|
+
setEnvShortHelper(plop);
|
|
34
|
+
setResourcePrefixHelper(plop);
|
|
35
|
+
setEqHelper(plop);
|
|
36
|
+
setTerraformStateKeyHelper(plop);
|
|
37
|
+
setGetTerraformBackend(plop, mockCloudAccountService);
|
|
38
|
+
setProvisionTerraformBackendAction(plop, mockCloudAccountService);
|
|
39
|
+
setInitCloudAccountsAction(plop, mockCloudAccountService, mockGitHubService);
|
|
40
|
+
};
|
|
41
|
+
const mockTerraformBackend = {
|
|
42
|
+
resourceGroupName: "dx-d-itn-tf-rg",
|
|
43
|
+
storageAccountName: "dxditntfst",
|
|
44
|
+
subscriptionId: "00000000-0000-0000-0000-000000000000",
|
|
45
|
+
type: "azurerm",
|
|
46
|
+
};
|
|
47
|
+
const createMockCloudAccountService = (backend, isInitialized) => ({
|
|
48
|
+
getTerraformBackend: vi.fn().mockResolvedValue(backend),
|
|
49
|
+
hasUserPermissionToInitialize: vi.fn().mockResolvedValue(true),
|
|
50
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
isInitialized: vi.fn().mockResolvedValue(isInitialized),
|
|
52
|
+
provisionTerraformBackend: vi.fn().mockResolvedValue(backend),
|
|
53
|
+
});
|
|
54
|
+
const createMockGitHubService = () => ({
|
|
55
|
+
createBranch: vi.fn().mockResolvedValue(undefined),
|
|
56
|
+
createOrUpdateEnvironmentSecret: vi.fn().mockResolvedValue(undefined),
|
|
57
|
+
createPullRequest: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
getFileContent: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
getRepository: vi.fn().mockResolvedValue(undefined),
|
|
60
|
+
updateFile: vi.fn().mockResolvedValue(undefined),
|
|
61
|
+
});
|
|
62
|
+
const runEnvironmentGenerator = async ({ mockCloudAccountService, mockGitHubService, payload, tmpDirPrefix, }) => {
|
|
63
|
+
const originalCwd = process.cwd();
|
|
64
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), tmpDirPrefix));
|
|
65
|
+
process.chdir(tmpDir);
|
|
66
|
+
const plop = await nodePlop();
|
|
67
|
+
registerEnvironmentSetup(plop, mockCloudAccountService, mockGitHubService);
|
|
68
|
+
plop.setGenerator(PLOP_ENVIRONMENT_GENERATOR_NAME, {
|
|
69
|
+
actions: getActions(resolveTemplatesPath("environment")),
|
|
70
|
+
description: "Generate a new deployment environment",
|
|
71
|
+
prompts: [],
|
|
72
|
+
});
|
|
73
|
+
const generator = plop.getGenerator(PLOP_ENVIRONMENT_GENERATOR_NAME);
|
|
74
|
+
const result = await generator.runActions(payload);
|
|
75
|
+
const realFailures = result.failures.filter((f) => f.error !== "Aborted due to previous action failure");
|
|
76
|
+
if (realFailures.length > 0) {
|
|
77
|
+
const summary = realFailures.map((f) => `${f.type}: ${f.error}`).join("\n");
|
|
78
|
+
throw new Error(`Generator failed:\n${summary}`);
|
|
79
|
+
}
|
|
80
|
+
return { originalCwd, tmpDir };
|
|
81
|
+
};
|
|
82
|
+
describe("environment generator — file generation (no init)", () => {
|
|
83
|
+
let tmpDir;
|
|
84
|
+
let originalCwd;
|
|
85
|
+
const payload = {
|
|
86
|
+
env: {
|
|
87
|
+
cloudAccounts: [
|
|
88
|
+
{
|
|
89
|
+
csp: "azure",
|
|
90
|
+
defaultLocation: "italynorth",
|
|
91
|
+
displayName: "DEV-DX",
|
|
92
|
+
id: "sub-dev-123",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
name: "dev",
|
|
96
|
+
prefix: "dx",
|
|
97
|
+
},
|
|
98
|
+
github: {
|
|
99
|
+
owner: "pagopa",
|
|
100
|
+
repo: "my-project",
|
|
101
|
+
},
|
|
102
|
+
tags: {
|
|
103
|
+
BusinessUnit: "Platform",
|
|
104
|
+
CostCenter: "TS000",
|
|
105
|
+
ManagementTeam: "Engineering",
|
|
106
|
+
},
|
|
107
|
+
workspace: {
|
|
108
|
+
domain: "payments",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const mockCloudAccountService = createMockCloudAccountService(mockTerraformBackend, true);
|
|
112
|
+
const mockGitHubService = createMockGitHubService();
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
({ originalCwd, tmpDir } = await runEnvironmentGenerator({
|
|
115
|
+
mockCloudAccountService,
|
|
116
|
+
mockGitHubService,
|
|
117
|
+
payload,
|
|
118
|
+
tmpDirPrefix: "dx-cli-env-test-",
|
|
119
|
+
}));
|
|
120
|
+
});
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
process.chdir(originalCwd);
|
|
123
|
+
await cleanupTempDir(tmpDir);
|
|
124
|
+
});
|
|
125
|
+
it("materializes bootstrapper files from payload and backend state", async () => {
|
|
126
|
+
const generatedFiles = await readGeneratedFiles(tmpDir, [
|
|
127
|
+
`.github/workflows/_release-terraform-apply-bootstrapper-${payload.env.name}.yaml`,
|
|
128
|
+
`infra/bootstrapper/${payload.env.name}/main.tf`,
|
|
129
|
+
`infra/bootstrapper/${payload.env.name}/providers.tf`,
|
|
130
|
+
`infra/bootstrapper/${payload.env.name}/backend.tf`,
|
|
131
|
+
`infra/bootstrapper/${payload.env.name}/locals.tf`,
|
|
132
|
+
]);
|
|
133
|
+
expect(generatedFiles).toMatchSnapshot();
|
|
134
|
+
});
|
|
135
|
+
it("skips init-only side effects and core files when init is absent", async () => {
|
|
136
|
+
const corePath = path.join(tmpDir, "infra", "core", payload.env.name);
|
|
137
|
+
expect(mockCloudAccountService.getTerraformBackend).toHaveBeenCalledWith(payload.env.cloudAccounts[0].id, payload.env);
|
|
138
|
+
expect(mockCloudAccountService.initialize).not.toHaveBeenCalled();
|
|
139
|
+
expect(mockCloudAccountService.provisionTerraformBackend).not.toHaveBeenCalled();
|
|
140
|
+
await expect(fs.stat(corePath)).rejects.toThrow();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("environment generator — file generation (with init)", () => {
|
|
144
|
+
let tmpDir;
|
|
145
|
+
let originalCwd;
|
|
146
|
+
const cloudAccount = {
|
|
147
|
+
csp: "azure",
|
|
148
|
+
defaultLocation: "italynorth",
|
|
149
|
+
displayName: "DEV-DX",
|
|
150
|
+
id: "sub-dev-123",
|
|
151
|
+
};
|
|
152
|
+
const payload = {
|
|
153
|
+
env: {
|
|
154
|
+
cloudAccounts: [cloudAccount],
|
|
155
|
+
name: "dev",
|
|
156
|
+
prefix: "dx",
|
|
157
|
+
},
|
|
158
|
+
github: {
|
|
159
|
+
owner: "pagopa",
|
|
160
|
+
repo: "my-project",
|
|
161
|
+
},
|
|
162
|
+
init: {
|
|
163
|
+
cloudAccountsToInitialize: [cloudAccount],
|
|
164
|
+
runnerAppCredentials: {
|
|
165
|
+
clientId: "test-app-client-id",
|
|
166
|
+
id: "test-app-id",
|
|
167
|
+
installationId: "test-installation-id",
|
|
168
|
+
key: "test-private-key",
|
|
169
|
+
},
|
|
170
|
+
terraformBackend: {
|
|
171
|
+
cloudAccount,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
tags: {
|
|
175
|
+
BusinessUnit: "Platform",
|
|
176
|
+
CostCenter: "TS000",
|
|
177
|
+
ManagementTeam: "Engineering",
|
|
178
|
+
},
|
|
179
|
+
workspace: {
|
|
180
|
+
domain: "payments",
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
const mockCloudAccountService = createMockCloudAccountService(mockTerraformBackend, false);
|
|
184
|
+
const mockGitHubService = createMockGitHubService();
|
|
185
|
+
beforeAll(async () => {
|
|
186
|
+
({ originalCwd, tmpDir } = await runEnvironmentGenerator({
|
|
187
|
+
mockCloudAccountService,
|
|
188
|
+
mockGitHubService,
|
|
189
|
+
payload,
|
|
190
|
+
tmpDirPrefix: "dx-cli-env-init-test-",
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
afterAll(async () => {
|
|
194
|
+
process.chdir(originalCwd);
|
|
195
|
+
await cleanupTempDir(tmpDir);
|
|
196
|
+
});
|
|
197
|
+
it("runs init-specific actions when init is provided", () => {
|
|
198
|
+
expect(mockCloudAccountService.initialize).toHaveBeenCalledWith(cloudAccount, payload.env,
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
200
|
+
payload.init.runnerAppCredentials, payload.github, mockGitHubService, payload.tags);
|
|
201
|
+
expect(mockCloudAccountService.provisionTerraformBackend).toHaveBeenCalledWith(cloudAccount, payload.env, payload.tags);
|
|
202
|
+
expect(mockCloudAccountService.getTerraformBackend).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
it("materializes init-specific infrastructure files", async () => {
|
|
205
|
+
const generatedFiles = await readGeneratedFiles(tmpDir, [
|
|
206
|
+
`infra/core/${payload.env.name}/main.tf`,
|
|
207
|
+
`infra/core/${payload.env.name}/providers.tf`,
|
|
208
|
+
`infra/core/${payload.env.name}/backend.tf`,
|
|
209
|
+
`infra/bootstrapper/${payload.env.name}/main.tf`,
|
|
210
|
+
]);
|
|
211
|
+
expect(generatedFiles).toMatchSnapshot();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import { formatTerraformCode } from "../../../terraform/fmt.js";
|
|
4
4
|
import { terraformStateKey } from "../../helpers/terraform-state-key.js";
|
|
5
5
|
import { payloadSchema } from "./prompts.js";
|
|
@@ -11,7 +11,7 @@ const addModule = (context, templatesPath, init = false) => {
|
|
|
11
11
|
return (name) => [
|
|
12
12
|
{
|
|
13
13
|
base: templatesPath,
|
|
14
|
-
data: { cloudAccountsByCsp, includesProdIO, init },
|
|
14
|
+
data: { cloudAccountsByCsp, includesProdIO, init: init || undefined },
|
|
15
15
|
destination: path.join(cwd, "infra"),
|
|
16
16
|
force: true,
|
|
17
17
|
templateFiles: path.join(templatesPath, name),
|
|
@@ -23,7 +23,7 @@ const addModule = (context, templatesPath, init = false) => {
|
|
|
23
23
|
base: path.join(templatesPath, "shared"),
|
|
24
24
|
data: {
|
|
25
25
|
cloudAccountsByCsp,
|
|
26
|
-
init,
|
|
26
|
+
init: init || undefined,
|
|
27
27
|
terraformBackendKey: terraformStateKey(context, name),
|
|
28
28
|
},
|
|
29
29
|
destination: path.join(cwd, "infra", name, "{{env.name}}"),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import nodePlop from "node-plop";
|
|
2
|
+
/**
|
|
3
|
+
* Contract tests for the monorepo generator.
|
|
4
|
+
*
|
|
5
|
+
* They generate a real repository in a temp directory, but they only
|
|
6
|
+
* assert the generator-specific contract: payload interpolation,
|
|
7
|
+
* injected action outputs, and post-processing we own. They
|
|
8
|
+
* intentionally avoid asserting generic Plop copy/render behavior.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
14
|
+
import { resolveTemplatesPath } from "../../../templates-path.js";
|
|
15
|
+
import { cleanupTempDir, readGeneratedFiles, } from "../../__tests__/temp-dir.js";
|
|
16
|
+
import getActions from "../actions.js";
|
|
17
|
+
import { PLOP_MONOREPO_GENERATOR_NAME } from "../index.js";
|
|
18
|
+
/**
|
|
19
|
+
* Register stub versions of the custom action types that would normally
|
|
20
|
+
* call external services (GitHub API, Node.js releases, shell commands).
|
|
21
|
+
*/
|
|
22
|
+
const registerStubActions = (plop) => {
|
|
23
|
+
plop.setActionType("getNodeVersion", async (data) => {
|
|
24
|
+
data.nodeVersion = "22.14.0";
|
|
25
|
+
return "Fetched latest version: 22.14.0";
|
|
26
|
+
});
|
|
27
|
+
plop.setActionType("fetchGithubRelease", async (data, ctx) => {
|
|
28
|
+
const resultKey = ctx.resultKey;
|
|
29
|
+
data[resultKey] = "1.11.0";
|
|
30
|
+
return `Fetched latest version: 1.11.0`;
|
|
31
|
+
});
|
|
32
|
+
plop.setActionType("setupPnpm", async () => "Monorepo bootstrapped");
|
|
33
|
+
};
|
|
34
|
+
describe("monorepo generator — file generation", () => {
|
|
35
|
+
let tmpDir;
|
|
36
|
+
let originalCwd;
|
|
37
|
+
const payload = {
|
|
38
|
+
repoDescription: "A test repository for DX",
|
|
39
|
+
repoName: "my-test-repo",
|
|
40
|
+
repoOwner: "pagopa",
|
|
41
|
+
};
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
originalCwd = process.cwd();
|
|
44
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "dx-cli-monorepo-test-"));
|
|
45
|
+
process.chdir(tmpDir);
|
|
46
|
+
const plop = await nodePlop();
|
|
47
|
+
registerStubActions(plop);
|
|
48
|
+
plop.setGenerator(PLOP_MONOREPO_GENERATOR_NAME, {
|
|
49
|
+
actions: getActions(resolveTemplatesPath("monorepo")),
|
|
50
|
+
description: "A scaffold for a monorepo repository",
|
|
51
|
+
prompts: [],
|
|
52
|
+
});
|
|
53
|
+
const generator = plop.getGenerator(PLOP_MONOREPO_GENERATOR_NAME);
|
|
54
|
+
const result = await generator.runActions(payload);
|
|
55
|
+
const realFailures = result.failures.filter((f) => f.error !== "Aborted due to previous action failure");
|
|
56
|
+
if (realFailures.length > 0) {
|
|
57
|
+
const summary = realFailures
|
|
58
|
+
.map((f) => `${f.type}: ${f.error}`)
|
|
59
|
+
.join("\n");
|
|
60
|
+
throw new Error(`Generator failed:\n${summary}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
afterAll(async () => {
|
|
64
|
+
process.chdir(originalCwd);
|
|
65
|
+
await cleanupTempDir(tmpDir);
|
|
66
|
+
});
|
|
67
|
+
it("materializes repository metadata from the generator payload", async () => {
|
|
68
|
+
const generatedFiles = await readGeneratedFiles(path.join(tmpDir, payload.repoName), ["package.json"]);
|
|
69
|
+
expect(generatedFiles).toMatchSnapshot();
|
|
70
|
+
});
|
|
71
|
+
it("propagates action outputs into generated version files", async () => {
|
|
72
|
+
const generatedFiles = await readGeneratedFiles(path.join(tmpDir, payload.repoName), [".node-version", ".terraform-version", ".pre-commit-config.yaml"]);
|
|
73
|
+
expect(generatedFiles).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
it("applies the repository-specific gitignore customization", async () => {
|
|
76
|
+
const generatedFiles = await readGeneratedFiles(path.join(tmpDir, payload.repoName), [".gitignore"]);
|
|
77
|
+
expect(generatedFiles).toMatchSnapshot();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AzureCliCredential } from "@azure/identity";
|
|
2
2
|
import { getLogger } from "@logtape/logtape";
|
|
3
3
|
import nodePlop from "node-plop";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import { Octokit } from "octokit";
|
|
6
5
|
import { oraPromise } from "ora";
|
|
7
6
|
import { RepositoryNotFoundError } from "../../domain/github.js";
|
|
@@ -9,10 +8,10 @@ import { AzureSubscriptionRepository } from "../azure/cloud-account-repository.j
|
|
|
9
8
|
import { AzureCloudAccountService } from "../azure/cloud-account-service.js";
|
|
10
9
|
import createDeploymentEnvironmentGenerator, { payloadSchema as environmentPayloadSchema, PLOP_ENVIRONMENT_GENERATOR_NAME, } from "../plop/generators/environment/index.js";
|
|
11
10
|
import createMonorepoGenerator, { payloadSchema as monorepoPayloadSchema, PLOP_MONOREPO_GENERATOR_NAME, } from "../plop/generators/monorepo/index.js";
|
|
11
|
+
import { resolveTemplatesPath } from "./templates-path.js";
|
|
12
12
|
export const setMonorepoGenerator = (plop) => {
|
|
13
13
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
14
|
-
|
|
15
|
-
createMonorepoGenerator(plop, templatesPath, octokit);
|
|
14
|
+
createMonorepoGenerator(plop, resolveTemplatesPath("monorepo"), octokit);
|
|
16
15
|
};
|
|
17
16
|
const validatePayload = async (payload, github) => {
|
|
18
17
|
try {
|
|
@@ -94,6 +93,5 @@ export const setDeploymentEnvironmentGenerator = (plop, gitHubService, github) =
|
|
|
94
93
|
const credential = new AzureCliCredential();
|
|
95
94
|
const cloudAccountRepository = new AzureSubscriptionRepository(credential);
|
|
96
95
|
const cloudAccountService = new AzureCloudAccountService(credential);
|
|
97
|
-
|
|
98
|
-
createDeploymentEnvironmentGenerator(plop, templatesPath, cloudAccountRepository, cloudAccountService, gitHubService, github);
|
|
96
|
+
createDeploymentEnvironmentGenerator(plop, resolveTemplatesPath("environment"), cloudAccountRepository, cloudAccountService, gitHubService, github);
|
|
99
97
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves CLI plop templates from a single place so runtime wiring and tests
|
|
4
|
+
* stay aligned with the package layout.
|
|
5
|
+
*/
|
|
6
|
+
export const resolveTemplatesPath = (generatorName) => path.join(import.meta.dirname, "../../../templates", generatorName);
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DeepMockProxy, MockProxy } from "vitest-mock-extended";
|
|
1
|
+
import type { MockProxy } from "vitest-mock-extended";
|
|
3
2
|
import { Config } from "../../config.js";
|
|
4
|
-
import {
|
|
5
|
-
import { GitHubService } from "../github.js";
|
|
3
|
+
import { GitHubAuthFactory } from "../dependencies.js";
|
|
6
4
|
import { PackageJson, PackageJsonReader } from "../package-json.js";
|
|
7
5
|
import { RepositoryReader } from "../repository.js";
|
|
8
6
|
import { ValidationReporter } from "../validation.js";
|
|
9
7
|
export declare const makeMockPackageJson: (overrides?: Partial<PackageJson>) => PackageJson;
|
|
10
8
|
export declare const makeMockDependencies: () => {
|
|
11
|
-
authorizationService: MockProxy<AuthorizationService>;
|
|
12
|
-
gitHubService: MockProxy<GitHubService>;
|
|
13
|
-
octokit: DeepMockProxy<Octokit>;
|
|
14
9
|
packageJsonReader: MockProxy<PackageJsonReader>;
|
|
15
10
|
repositoryReader: MockProxy<RepositoryReader>;
|
|
11
|
+
requireGitHubAuth: GitHubAuthFactory;
|
|
16
12
|
validationReporter: MockProxy<ValidationReporter>;
|
|
17
13
|
};
|
|
18
14
|
export declare const makeMockConfig: () => Config;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mock
|
|
1
|
+
import { mock } from "vitest-mock-extended";
|
|
2
2
|
export const makeMockPackageJson = (overrides = {}) => {
|
|
3
3
|
const basePackageJson = {
|
|
4
4
|
dependencies: new Map(),
|
|
@@ -13,11 +13,9 @@ export const makeMockPackageJson = (overrides = {}) => {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
export const makeMockDependencies = () => ({
|
|
16
|
-
authorizationService: mock(),
|
|
17
|
-
gitHubService: mock(),
|
|
18
|
-
octokit: mockDeep(),
|
|
19
16
|
packageJsonReader: mock(),
|
|
20
17
|
repositoryReader: mock(),
|
|
18
|
+
requireGitHubAuth: mock(),
|
|
21
19
|
validationReporter: mock(),
|
|
22
20
|
});
|
|
23
21
|
export const makeMockConfig = () => ({
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import { ResultAsync } from "neverthrow";
|
|
1
2
|
import { AuthorizationService } from "./authorization.js";
|
|
2
3
|
import { GitHubService } from "./github.js";
|
|
3
4
|
import { PackageJsonReader } from "./package-json.js";
|
|
4
5
|
import { RepositoryReader } from "./repository.js";
|
|
5
6
|
import { ValidationReporter } from "./validation.js";
|
|
6
7
|
export type Dependencies = {
|
|
7
|
-
authorizationService: AuthorizationService;
|
|
8
|
-
gitHubService: GitHubService;
|
|
9
8
|
packageJsonReader: PackageJsonReader;
|
|
10
9
|
repositoryReader: RepositoryReader;
|
|
10
|
+
requireGitHubAuth: GitHubAuthFactory;
|
|
11
11
|
validationReporter: ValidationReporter;
|
|
12
12
|
};
|
|
13
|
+
/** Services that require a GitHub PAT to be instantiated. */
|
|
14
|
+
export type GitHubAuthDeps = {
|
|
15
|
+
authorizationService: AuthorizationService;
|
|
16
|
+
gitHubService: GitHubService;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Lazily resolves GitHub-authenticated services.
|
|
20
|
+
* Commands that need GitHub access call this factory inside their action
|
|
21
|
+
* handler so that auth is only required when those commands actually run.
|
|
22
|
+
* Returns a `ResultAsync` so auth failures (e.g. missing GH_TOKEN) can be
|
|
23
|
+
* routed through the same neverthrow error pipeline used by the commands.
|
|
24
|
+
*/
|
|
25
|
+
export type GitHubAuthFactory = () => ResultAsync<GitHubAuthDeps, Error>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Domain
|
|
3
|
+
*
|
|
4
|
+
* Pure types that describe the shape of the CLI spec output.
|
|
5
|
+
* Technology-agnostic: no dependency on Commander or any CLI framework.
|
|
6
|
+
* Consumed by the Commander adapter and by agent tooling that parses `dx spec`.
|
|
7
|
+
*/
|
|
8
|
+
export type ArgumentSpec = {
|
|
9
|
+
/** Allowed values when the argument is constrained to a set */
|
|
10
|
+
choices: string[];
|
|
11
|
+
defaultValue: unknown;
|
|
12
|
+
description: string;
|
|
13
|
+
name: string;
|
|
14
|
+
required: boolean;
|
|
15
|
+
variadic: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type CliSpec = {
|
|
18
|
+
/** Top-level subcommands with their own options, arguments, and nested commands. */
|
|
19
|
+
commands: CommandSpec[];
|
|
20
|
+
description: string;
|
|
21
|
+
/** Options defined directly on the root `dx` program (global flags). */
|
|
22
|
+
globalOptions: OptionSpec[];
|
|
23
|
+
name: string;
|
|
24
|
+
/** Stable version of the spec JSON shape. Increment on breaking changes. */
|
|
25
|
+
specVersion: "1";
|
|
26
|
+
version: string;
|
|
27
|
+
};
|
|
28
|
+
export type CommandSpec = {
|
|
29
|
+
arguments: ArgumentSpec[];
|
|
30
|
+
commands: CommandSpec[];
|
|
31
|
+
description: string;
|
|
32
|
+
name: string;
|
|
33
|
+
options: OptionSpec[];
|
|
34
|
+
};
|
|
35
|
+
export type OptionSpec = {
|
|
36
|
+
/** Allowed values when the option is an enum */
|
|
37
|
+
choices: string[];
|
|
38
|
+
defaultValue: unknown;
|
|
39
|
+
description: string;
|
|
40
|
+
/** Raw flags string as shown in help, e.g. "-v, --verbose" */
|
|
41
|
+
flags: string;
|
|
42
|
+
/** Long flag, e.g. "--verbose" */
|
|
43
|
+
long: string | undefined;
|
|
44
|
+
/** True when the option value is optional (e.g. `--output [mode]`) */
|
|
45
|
+
optional: boolean;
|
|
46
|
+
/** True when the option value is mandatory (e.g. `--output <mode>`) */
|
|
47
|
+
required: boolean;
|
|
48
|
+
/** Short flag, e.g. "-v" (undefined when not present) */
|
|
49
|
+
short: string | undefined;
|
|
50
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "core-js/actual/set/index.js";
|
|
2
2
|
import { configure, getConsoleSink } from "@logtape/logtape";
|
|
3
|
-
import
|
|
3
|
+
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
|
4
4
|
import { Octokit } from "octokit";
|
|
5
5
|
import codemodRegistry from "./adapters/codemods/index.js";
|
|
6
6
|
import { makeCli } from "./adapters/commander/index.js";
|
|
@@ -13,7 +13,6 @@ import { getConfig } from "./config.js";
|
|
|
13
13
|
import { getInfo } from "./domain/info.js";
|
|
14
14
|
import { applyCodemodById } from "./use-cases/apply-codemod.js";
|
|
15
15
|
import { listCodemods } from "./use-cases/list-codemods.js";
|
|
16
|
-
import { requestAuthorization } from "./use-cases/request-authorization.js";
|
|
17
16
|
/**
|
|
18
17
|
* Returns `true` when `-v` or `--verbose` is present in argv.
|
|
19
18
|
*
|
|
@@ -50,29 +49,33 @@ const configureLogging = async (verbose) => {
|
|
|
50
49
|
};
|
|
51
50
|
export const runCli = async (version) => {
|
|
52
51
|
await configureLogging(detectVerboseFromArgv(process.argv));
|
|
53
|
-
// Creating the adapters
|
|
54
52
|
const repositoryReader = makeRepositoryReader();
|
|
55
53
|
const packageJsonReader = makePackageJsonReader();
|
|
56
54
|
const validationReporter = makeValidationReporter();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Lazily creates GitHub-authenticated services on first call.
|
|
57
|
+
* Only commands that actually need GitHub (init, add) will trigger this,
|
|
58
|
+
* so credential-free commands (spec, doctor, info, …) never require a PAT.
|
|
59
|
+
*/
|
|
60
|
+
const requireGitHubAuth = () => ResultAsync.fromPromise(getGitHubPAT(), (cause) => new Error("Failed to read GitHub PAT", { cause })).andThen((auth) => {
|
|
61
|
+
if (!auth) {
|
|
62
|
+
return errAsync(new Error("GitHub PAT is required. Please set the GH_TOKEN environment variable or login using GitHub CLI."));
|
|
63
|
+
}
|
|
64
|
+
const octokit = new Octokit({ auth });
|
|
65
|
+
const gitHubService = new OctokitGitHubService(octokit);
|
|
66
|
+
const authorizationService = makeAzureAuthorizationService(gitHubService);
|
|
67
|
+
return okAsync({ authorizationService, gitHubService });
|
|
61
68
|
});
|
|
62
|
-
const gitHubService = new OctokitGitHubService(octokit);
|
|
63
|
-
const authorizationService = makeAzureAuthorizationService(gitHubService);
|
|
64
69
|
const deps = {
|
|
65
|
-
authorizationService,
|
|
66
|
-
gitHubService,
|
|
67
70
|
packageJsonReader,
|
|
68
71
|
repositoryReader,
|
|
72
|
+
requireGitHubAuth,
|
|
69
73
|
validationReporter,
|
|
70
74
|
};
|
|
71
75
|
const config = getConfig();
|
|
72
76
|
const useCases = {
|
|
73
77
|
applyCodemodById: applyCodemodById(codemodRegistry, getInfo(deps)),
|
|
74
78
|
listCodemods: listCodemods(codemodRegistry),
|
|
75
|
-
requestAuthorization: requestAuthorization(authorizationService),
|
|
76
79
|
};
|
|
77
80
|
const program = makeCli(deps, config, useCases, version);
|
|
78
81
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagopa/dx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A CLI useful to manage DX tools.",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"semver": "^7.7.4",
|
|
49
49
|
"yaml": "^2.8.4",
|
|
50
50
|
"zod": "^4.4.2",
|
|
51
|
-
"@pagopa/dx-savemoney": "^0.
|
|
51
|
+
"@pagopa/dx-savemoney": "^0.3.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@tsconfig/node24": "24.0.4",
|