@pagopa/dx-cli 0.15.1 → 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.
- package/bin/index.js +8 -1280
- package/dist/adapters/azure/__tests__/cloud-account-repository.test.d.ts +1 -0
- package/dist/adapters/azure/__tests__/cloud-account-repository.test.js +95 -0
- package/dist/adapters/azure/__tests__/cloud-account-service.test.d.ts +1 -0
- package/dist/adapters/azure/__tests__/cloud-account-service.test.js +95 -0
- package/dist/adapters/azure/cloud-account-repository.d.ts +12 -0
- package/dist/adapters/azure/cloud-account-repository.js +23 -0
- package/dist/adapters/azure/cloud-account-service.d.ts +22 -0
- package/dist/adapters/azure/cloud-account-service.js +255 -0
- package/dist/adapters/azure/locations.d.ts +7 -0
- package/dist/adapters/azure/locations.js +21 -0
- package/dist/adapters/codemods/__tests__/registry.test.d.ts +1 -0
- package/dist/adapters/codemods/__tests__/registry.test.js +59 -0
- package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.d.ts +1 -0
- package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.js +77 -0
- package/dist/adapters/codemods/__tests__/use-pnpm.test.d.ts +1 -0
- package/dist/adapters/codemods/__tests__/use-pnpm.test.js +148 -0
- package/dist/adapters/codemods/git.d.ts +2 -0
- package/dist/adapters/codemods/git.js +18 -0
- package/dist/adapters/codemods/index.d.ts +3 -0
- package/dist/adapters/codemods/index.js +9 -0
- package/dist/adapters/codemods/registry.d.ts +8 -0
- package/dist/adapters/codemods/registry.js +16 -0
- package/dist/adapters/codemods/update-code-review.d.ts +3 -0
- package/dist/adapters/codemods/update-code-review.js +60 -0
- package/dist/adapters/codemods/use-azure-appsvc.d.ts +3 -0
- package/dist/adapters/codemods/use-azure-appsvc.js +84 -0
- package/dist/adapters/codemods/use-pnpm.d.ts +22 -0
- package/dist/adapters/codemods/use-pnpm.js +214 -0
- package/dist/adapters/codemods/yaml.d.ts +2 -0
- package/dist/adapters/codemods/yaml.js +8 -0
- package/dist/adapters/commander/commands/codemod.d.ts +8 -0
- package/dist/adapters/commander/commands/codemod.js +22 -0
- package/dist/adapters/commander/commands/doctor.d.ts +4 -0
- package/dist/adapters/commander/commands/doctor.js +12 -0
- package/dist/adapters/commander/commands/info.d.ts +3 -0
- package/dist/adapters/commander/commands/info.js +9 -0
- package/dist/adapters/commander/commands/init.d.ts +7 -0
- package/dist/adapters/commander/commands/init.js +126 -0
- package/dist/adapters/commander/commands/savemoney.d.ts +2 -0
- package/dist/adapters/commander/commands/savemoney.js +26 -0
- package/dist/adapters/commander/index.d.ts +7 -0
- package/dist/adapters/commander/index.js +19 -0
- package/dist/adapters/execa/terraform.d.ts +27 -0
- package/dist/adapters/execa/terraform.js +28 -0
- package/dist/adapters/github/__tests__/github-repo.spec.d.ts +1 -0
- package/dist/adapters/github/__tests__/github-repo.spec.js +67 -0
- package/dist/adapters/github/github-repo.d.ts +2 -0
- package/dist/adapters/github/github-repo.js +31 -0
- package/dist/adapters/logtape/validation-reporter.d.ts +2 -0
- package/dist/adapters/logtape/validation-reporter.js +14 -0
- package/dist/adapters/node/__tests__/data.d.ts +18 -0
- package/dist/adapters/node/__tests__/data.js +22 -0
- package/dist/adapters/node/__tests__/package-json.test.d.ts +1 -0
- package/dist/adapters/node/__tests__/package-json.test.js +86 -0
- package/dist/adapters/node/__tests__/repository.test.d.ts +1 -0
- package/dist/adapters/node/__tests__/repository.test.js +77 -0
- package/dist/adapters/node/fs/__tests__/file-reader.test.d.ts +1 -0
- package/dist/adapters/node/fs/__tests__/file-reader.test.js +80 -0
- package/dist/adapters/node/fs/file-reader.d.ts +24 -0
- package/dist/adapters/node/fs/file-reader.js +26 -0
- package/dist/adapters/node/json/__tests__/index.test.d.ts +1 -0
- package/dist/adapters/node/json/__tests__/index.test.js +14 -0
- package/dist/adapters/node/json/index.d.ts +2 -0
- package/dist/adapters/node/json/index.js +2 -0
- package/dist/adapters/node/package-json.d.ts +2 -0
- package/dist/adapters/node/package-json.js +22 -0
- package/dist/adapters/node/release.d.ts +2 -0
- package/dist/adapters/node/release.js +33 -0
- package/dist/adapters/node/repository.d.ts +2 -0
- package/dist/adapters/node/repository.js +47 -0
- package/dist/adapters/octokit/__tests__/index.test.d.ts +1 -0
- package/dist/adapters/octokit/__tests__/index.test.js +197 -0
- package/dist/adapters/octokit/index.d.ts +24 -0
- package/dist/adapters/octokit/index.js +65 -0
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.d.ts +1 -0
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +115 -0
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.d.ts +1 -0
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +116 -0
- package/dist/adapters/plop/actions/fetch-github-release.d.ts +12 -0
- package/dist/adapters/plop/actions/fetch-github-release.js +20 -0
- package/dist/adapters/plop/actions/get-node-version.d.ts +2 -0
- package/dist/adapters/plop/actions/get-node-version.js +9 -0
- package/dist/adapters/plop/actions/get-terraform-backend.d.ts +3 -0
- package/dist/adapters/plop/actions/get-terraform-backend.js +17 -0
- package/dist/adapters/plop/actions/init-cloud-accounts.d.ts +5 -0
- package/dist/adapters/plop/actions/init-cloud-accounts.js +13 -0
- package/dist/adapters/plop/actions/provision-terraform-backend.d.ts +10 -0
- package/dist/adapters/plop/actions/provision-terraform-backend.js +16 -0
- package/dist/adapters/plop/actions/semver.d.ts +19 -0
- package/dist/adapters/plop/actions/semver.js +27 -0
- package/dist/adapters/plop/actions/setup-pnpm.d.ts +2 -0
- package/dist/adapters/plop/actions/setup-pnpm.js +26 -0
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.d.ts +2 -0
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +55 -0
- package/dist/adapters/plop/generators/environment/actions.d.ts +2 -0
- package/dist/adapters/plop/generators/environment/actions.js +54 -0
- package/dist/adapters/plop/generators/environment/index.d.ts +3 -0
- package/dist/adapters/plop/generators/environment/index.js +19 -0
- package/dist/adapters/plop/generators/environment/prompts.d.ts +66 -0
- package/dist/adapters/plop/generators/environment/prompts.js +166 -0
- package/dist/adapters/plop/generators/monorepo/actions.d.ts +51 -0
- package/dist/adapters/plop/generators/monorepo/actions.js +35 -0
- package/dist/adapters/plop/generators/monorepo/index.d.ts +6 -0
- package/dist/adapters/plop/generators/monorepo/index.js +17 -0
- package/dist/adapters/plop/generators/monorepo/prompts.d.ts +10 -0
- package/dist/adapters/plop/generators/monorepo/prompts.js +31 -0
- package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.d.ts +1 -0
- package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.js +113 -0
- package/dist/adapters/plop/helpers/env-short.d.ts +3 -0
- package/dist/adapters/plop/helpers/env-short.js +9 -0
- package/dist/adapters/plop/helpers/resource-prefix.d.ts +5 -0
- package/dist/adapters/plop/helpers/resource-prefix.js +18 -0
- package/dist/adapters/plop/index.d.ts +8 -0
- package/dist/adapters/plop/index.js +24 -0
- package/dist/adapters/terraform/fmt.d.ts +1 -0
- package/dist/adapters/terraform/fmt.js +17 -0
- package/dist/adapters/yaml/__tests__/index.test.d.ts +1 -0
- package/dist/adapters/yaml/__tests__/index.test.js +53 -0
- package/dist/adapters/yaml/index.d.ts +8 -0
- package/dist/adapters/yaml/index.js +9 -0
- package/dist/adapters/zod/index.d.ts +23 -0
- package/dist/adapters/zod/index.js +22 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +5 -0
- package/dist/domain/__tests__/data.d.ts +17 -0
- package/dist/domain/__tests__/data.js +27 -0
- package/dist/domain/__tests__/environment.test.d.ts +1 -0
- package/dist/domain/__tests__/environment.test.js +282 -0
- package/dist/domain/__tests__/info.test.d.ts +1 -0
- package/dist/domain/__tests__/info.test.js +77 -0
- package/dist/domain/__tests__/package-json.test.d.ts +1 -0
- package/dist/domain/__tests__/package-json.test.js +39 -0
- package/dist/domain/__tests__/repository.test.d.ts +1 -0
- package/dist/domain/__tests__/repository.test.js +101 -0
- package/dist/domain/__tests__/workspace.test.d.ts +1 -0
- package/dist/domain/__tests__/workspace.test.js +57 -0
- package/dist/domain/cloud-account.d.ts +28 -0
- package/dist/domain/cloud-account.js +12 -0
- package/dist/domain/codemod.d.ts +11 -0
- package/dist/domain/codemod.js +1 -0
- package/dist/domain/dependencies.d.ts +10 -0
- package/dist/domain/dependencies.js +1 -0
- package/dist/domain/doctor.d.ts +10 -0
- package/dist/domain/doctor.js +50 -0
- package/dist/domain/environment.d.ts +40 -0
- package/dist/domain/environment.js +57 -0
- package/dist/domain/github-repo.d.ts +6 -0
- package/dist/domain/github-repo.js +8 -0
- package/dist/domain/github.d.ts +37 -0
- package/dist/domain/github.js +29 -0
- package/dist/domain/info.d.ts +11 -0
- package/dist/domain/info.js +52 -0
- package/dist/domain/package-json.d.ts +42 -0
- package/dist/domain/package-json.js +69 -0
- package/dist/domain/remote-backend.d.ts +8 -0
- package/dist/domain/remote-backend.js +9 -0
- package/dist/domain/repository.d.ts +13 -0
- package/dist/domain/repository.js +72 -0
- package/dist/domain/validation.d.ts +16 -0
- package/dist/domain/validation.js +1 -0
- package/dist/domain/workspace.d.ts +9 -0
- package/dist/domain/workspace.js +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -0
- package/dist/use-cases/__tests__/apply-codemod.test.d.ts +1 -0
- package/dist/use-cases/__tests__/apply-codemod.test.js +73 -0
- package/dist/use-cases/__tests__/list-codemods.test.d.ts +1 -0
- package/dist/use-cases/__tests__/list-codemods.test.js +38 -0
- package/dist/use-cases/apply-codemod.d.ts +5 -0
- package/dist/use-cases/apply-codemod.js +14 -0
- package/dist/use-cases/list-codemods.d.ts +4 -0
- package/dist/use-cases/list-codemods.js +1 -0
- package/package.json +19 -8
- package/templates/environment/bootstrapper/{{env.name}}/data.tf.hbs +13 -0
- package/templates/environment/bootstrapper/{{env.name}}/main.tf.hbs +70 -0
- package/templates/environment/bootstrapper/{{env.name}}/providers.tf.hbs +26 -0
- package/templates/environment/core/{{env.name}}/main.tf.hbs +21 -0
- package/templates/environment/core/{{env.name}}/outputs.tf.hbs +8 -0
- package/templates/environment/core/{{env.name}}/providers.tf.hbs +21 -0
- package/templates/environment/shared/backend.tf.hbs +14 -0
- package/templates/environment/shared/locals.tf.hbs +26 -0
- package/templates/monorepo/.editorconfig +8 -0
- package/templates/monorepo/.node-version.hbs +1 -0
- package/templates/monorepo/.pre-commit-config.yaml.hbs +34 -0
- package/templates/monorepo/.prettierignore +5 -0
- package/templates/monorepo/.terraform-version.hbs +1 -0
- package/templates/monorepo/.trivyignore +16 -0
- package/templates/monorepo/README.md.hbs +163 -0
- package/templates/monorepo/infra/repository/main.tf.hbs +11 -0
- package/templates/monorepo/infra/repository/outputs.tf.hbs +11 -0
- package/templates/monorepo/infra/repository/providers.tf.hbs +13 -0
- package/templates/monorepo/package.json.hbs +7 -0
- package/templates/monorepo/pnpm-workspace.yaml +7 -0
- package/templates/monorepo/turbo.json +29 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
import { $ } from "execa";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import { replaceInFile } from "replace-in-file";
|
|
6
|
+
import semver from "semver";
|
|
7
|
+
import YAML from "yaml";
|
|
8
|
+
import { getLatestCommitShaOrRef } from "./git.js";
|
|
9
|
+
import { updateJSCodeReviewJob } from "./update-code-review.js";
|
|
10
|
+
import { migrateWorkflow } from "./use-azure-appsvc.js";
|
|
11
|
+
export class NPM {
|
|
12
|
+
lockFileName = "package-lock.json";
|
|
13
|
+
async listWorkspaces() {
|
|
14
|
+
const { stdout } = await $ `npm query .workspace`;
|
|
15
|
+
const workspaces = JSON.parse(stdout);
|
|
16
|
+
const workspaceNames = [];
|
|
17
|
+
if (Array.isArray(workspaces)) {
|
|
18
|
+
for (const ws of workspaces) {
|
|
19
|
+
if (Object.hasOwn(ws, "name")) {
|
|
20
|
+
workspaceNames.push(ws.name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return workspaceNames;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class Yarn {
|
|
28
|
+
lockFileName = "yarn.lock";
|
|
29
|
+
async listWorkspaces() {
|
|
30
|
+
const { stdout } = await $({ lines: true }) `yarn workspaces list --json`;
|
|
31
|
+
const workspaceNames = [];
|
|
32
|
+
for (const line of stdout) {
|
|
33
|
+
const ws = JSON.parse(line);
|
|
34
|
+
if (Object.hasOwn(ws, "name")) {
|
|
35
|
+
workspaceNames.push(ws.name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return workspaceNames;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function extractPackageExtensions() {
|
|
42
|
+
// Read the .yarnrc.yaml file if it exists and extract the packageExtensions field
|
|
43
|
+
try {
|
|
44
|
+
const yarnrc = await fs.readFile(".yarnrc.yml", "utf-8");
|
|
45
|
+
const parsed = YAML.parse(yarnrc);
|
|
46
|
+
if (parsed.packageExtensions) {
|
|
47
|
+
return parsed.packageExtensions;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// File does not exist or is not readable, ignore
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export async function preparePackageJsonForPnpm() {
|
|
56
|
+
const packageJson = await fs.readFile("package.json", "utf-8");
|
|
57
|
+
const manifest = JSON.parse(packageJson);
|
|
58
|
+
let workspaces = [];
|
|
59
|
+
if (Object.hasOwn(manifest, "packageManager")) {
|
|
60
|
+
delete manifest.packageManager;
|
|
61
|
+
}
|
|
62
|
+
if (Object.hasOwn(manifest, "workspaces")) {
|
|
63
|
+
if (Array.isArray(manifest.workspaces)) {
|
|
64
|
+
workspaces = manifest.workspaces;
|
|
65
|
+
}
|
|
66
|
+
delete manifest.workspaces;
|
|
67
|
+
}
|
|
68
|
+
await fs.writeFile("package.json", JSON.stringify(manifest, null, 2));
|
|
69
|
+
return workspaces;
|
|
70
|
+
}
|
|
71
|
+
export async function writePnpmWorkspaceFile(workspaces, packageExtensions) {
|
|
72
|
+
// We inline all the default settings here because Renovate
|
|
73
|
+
// does not support PNPM's config dependencies yet.
|
|
74
|
+
const pnpmWorkspace = {
|
|
75
|
+
cleanupUnusedCatalogs: true,
|
|
76
|
+
linkWorkspacePackages: true,
|
|
77
|
+
packageExtensions,
|
|
78
|
+
packageImportMethod: "clone-or-copy",
|
|
79
|
+
packages: workspaces.length > 0 ? workspaces : ["apps/*", "packages/*"],
|
|
80
|
+
};
|
|
81
|
+
const yamlContent = YAML.stringify(pnpmWorkspace);
|
|
82
|
+
await fs.writeFile("pnpm-workspace.yaml", yamlContent, "utf-8");
|
|
83
|
+
}
|
|
84
|
+
async function removeFiles(...files) {
|
|
85
|
+
await Promise.all(files.map((file) =>
|
|
86
|
+
// Remove the file if it exists, fail silently if it doesn't.
|
|
87
|
+
fs.rm(file, { force: true, recursive: true }).catch(() => undefined)));
|
|
88
|
+
}
|
|
89
|
+
async function replacePMOccurrences() {
|
|
90
|
+
const logger = getLogger(["dx-cli", "codemod"]);
|
|
91
|
+
logger.info("Replacing yarn and npm occurrences in files...");
|
|
92
|
+
const results = await replaceInFile({
|
|
93
|
+
allowEmptyPaths: true,
|
|
94
|
+
files: ["**/*.json", "**/*.md", "**/Dockerfile", "**/docker-compose.yml"],
|
|
95
|
+
from: [
|
|
96
|
+
"https://yarnpkg.com/",
|
|
97
|
+
"https://classic.yarnpkg.com/",
|
|
98
|
+
/\b(yarn workspace|npm -(\b-workspace\b|\bw\b)) (\S+)\b/g,
|
|
99
|
+
/\b(yarn workspace|npm -(\b-workspace\b|\bw\b))\b/g,
|
|
100
|
+
/\b(yarn install --immutable|npm ci)\b/g,
|
|
101
|
+
/\b(yarn -q dlx|npx)\b/g,
|
|
102
|
+
/\b((yarn|npm) run)\b/g,
|
|
103
|
+
/(^|\s|")(yarn|npm)(?!\S)/gi,
|
|
104
|
+
],
|
|
105
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
106
|
+
to: [
|
|
107
|
+
"https://pnpm.io/",
|
|
108
|
+
"https://pnpm.io/",
|
|
109
|
+
"pnpm --filter $3",
|
|
110
|
+
"pnpm --filter <package-selector>",
|
|
111
|
+
"pnpm install --frozen-lockfile",
|
|
112
|
+
"pnpm dlx",
|
|
113
|
+
"pnpm run",
|
|
114
|
+
"$1pnpm",
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
const count = results.reduce((acc, file) => (file.hasChanged ? acc + 1 : acc), 0);
|
|
118
|
+
logger.info("Replaced yarn occurrences in {count} files", { count });
|
|
119
|
+
}
|
|
120
|
+
async function updateDXWorkflows() {
|
|
121
|
+
const logger = getLogger(["dx-cli", "codemod"]);
|
|
122
|
+
logger.info("Updating Github Workflows workflows...");
|
|
123
|
+
// Get the latest commit sha from the main branch of the dx repository
|
|
124
|
+
const sha = await getLatestCommitShaOrRef("pagopa", "dx");
|
|
125
|
+
// Update the js_code_review workflow to use the latest commit sha
|
|
126
|
+
const results = await replaceInFile({
|
|
127
|
+
allowEmptyPaths: true,
|
|
128
|
+
files: [".github/workflows/*.yaml"],
|
|
129
|
+
processor: updateJSCodeReviewJob(sha),
|
|
130
|
+
});
|
|
131
|
+
const ignore = results.filter((r) => r.hasChanged).map((r) => r.file);
|
|
132
|
+
// Update the legacy deployment workflow to release-azure-appsvc-v1.yaml
|
|
133
|
+
await replaceInFile({
|
|
134
|
+
allowEmptyPaths: true,
|
|
135
|
+
files: [".github/workflows/*.yaml"],
|
|
136
|
+
ignore,
|
|
137
|
+
processor: migrateWorkflow(sha),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export const usePnpm = async (packageManager, currentNodeVersion) => {
|
|
141
|
+
const minNodeVersion = "20.19.5";
|
|
142
|
+
assert.notEqual(packageManager, "pnpm", "Project is already using pnpm");
|
|
143
|
+
assert.ok(semver.gte(currentNodeVersion, minNodeVersion), `his codemod requires Node.js >= ${minNodeVersion}. Current version: ${currentNodeVersion}`);
|
|
144
|
+
const logger = getLogger(["dx-cli", "codemod"]);
|
|
145
|
+
const pm = packageManager === "yarn" ? new Yarn() : new NPM();
|
|
146
|
+
const localWorkspaces = await pm.listWorkspaces();
|
|
147
|
+
// Update local dependencies to use "workspace:" protocol
|
|
148
|
+
if (localWorkspaces.length > 0) {
|
|
149
|
+
logger.info("Using the {protocol} protocol for local dependencies", {
|
|
150
|
+
protocol: "workspace:",
|
|
151
|
+
});
|
|
152
|
+
await replaceInFile({
|
|
153
|
+
allowEmptyPaths: true,
|
|
154
|
+
files: ["**/package.json"],
|
|
155
|
+
from: localWorkspaces.map((ws) => new RegExp(`"${ws}": ".*?"`, "g")),
|
|
156
|
+
to: localWorkspaces.map((ws) => `"${ws}": "workspace:^"`),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Remove unused field from package.json
|
|
160
|
+
logger.info("Remove unused fields from {file}", {
|
|
161
|
+
file: "package.json",
|
|
162
|
+
});
|
|
163
|
+
const workspaces = await preparePackageJsonForPnpm();
|
|
164
|
+
// Extract custom packageExtensions from .yarnrc.yml if any
|
|
165
|
+
const packageExtensions = packageManager === "yarn" ? await extractPackageExtensions() : undefined;
|
|
166
|
+
// Create pnpm-workspace.yaml
|
|
167
|
+
logger.info("Create {file}", {
|
|
168
|
+
file: "pnpm-workspace.yaml",
|
|
169
|
+
});
|
|
170
|
+
await writePnpmWorkspaceFile(workspaces, packageExtensions);
|
|
171
|
+
// Remove yarn and node_modules files and folders
|
|
172
|
+
logger.info("Remove node_modules and yarn files");
|
|
173
|
+
await removeFiles(".yarnrc", ".yarnrc.yml", "yarn.config.cjs", ".yarn", ".pnp.cjs", ".pnp.loader.cjs", "node_modules");
|
|
174
|
+
// Import lockfile
|
|
175
|
+
const stat = await fs.stat(pm.lockFileName);
|
|
176
|
+
if (stat.isFile()) {
|
|
177
|
+
logger.info("Importing {source} to {target}", {
|
|
178
|
+
source: pm.lockFileName,
|
|
179
|
+
target: "pnpm-lock.yaml",
|
|
180
|
+
});
|
|
181
|
+
await $ `corepack pnpm@latest import ${pm.lockFileName}`;
|
|
182
|
+
await removeFiles(pm.lockFileName);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
logger.info("No {source} file found, skipping import.", {
|
|
186
|
+
source: pm.lockFileName,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Replace yarn and npm occurrences in files and update workflows
|
|
190
|
+
await replacePMOccurrences();
|
|
191
|
+
await updateDXWorkflows();
|
|
192
|
+
// Add pnpm store to .gitignore
|
|
193
|
+
logger.info("Adding pnpm store to .gitignore...");
|
|
194
|
+
await fs.appendFile(".gitignore", "\n\n# PNPM\n.pnpm-store");
|
|
195
|
+
// Set pnpm as the package manager
|
|
196
|
+
logger.info("Setting pnpm as the package manager...");
|
|
197
|
+
await $ `corepack use pnpm@latest`;
|
|
198
|
+
};
|
|
199
|
+
const apply = async (info) => {
|
|
200
|
+
const logger = getLogger(["dx-cli", "codemod"]);
|
|
201
|
+
try {
|
|
202
|
+
await usePnpm(info.packageManager, process.versions.node);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof Error) {
|
|
206
|
+
logger.error(error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
export default {
|
|
211
|
+
apply,
|
|
212
|
+
description: "Migrate the project to use pnpm as the package manager",
|
|
213
|
+
id: "use-pnpm",
|
|
214
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ApplyCodemodById } from "../../../use-cases/apply-codemod.js";
|
|
3
|
+
import { ListCodemods } from "../../../use-cases/list-codemods.js";
|
|
4
|
+
export type CodemodCommandDependencies = {
|
|
5
|
+
applyCodemodById: ApplyCodemodById;
|
|
6
|
+
listCodemods: ListCodemods;
|
|
7
|
+
};
|
|
8
|
+
export declare const makeCodemodCommand: ({ applyCodemodById, listCodemods, }: CodemodCommandDependencies) => Command;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
export const makeCodemodCommand = ({ applyCodemodById, listCodemods, }) => new Command("codemod")
|
|
4
|
+
.description("Manage and apply migration scripts to the repository")
|
|
5
|
+
.addCommand(new Command("list")
|
|
6
|
+
.description("List available migration scripts")
|
|
7
|
+
.action(async function () {
|
|
8
|
+
await listCodemods()
|
|
9
|
+
.andTee((codemods) => console.table(codemods, ["id", "description"]))
|
|
10
|
+
.orTee((error) => this.error(error.message));
|
|
11
|
+
}))
|
|
12
|
+
.addCommand(new Command("apply")
|
|
13
|
+
.argument("<id>", "The id of the codemod to apply")
|
|
14
|
+
.description("Apply migration scripts to the repository")
|
|
15
|
+
.action(async function (id) {
|
|
16
|
+
const logger = getLogger(["dx-cli", "codemod"]);
|
|
17
|
+
await applyCodemodById(id)
|
|
18
|
+
.andTee(() => {
|
|
19
|
+
logger.info("Codemod applied ✅");
|
|
20
|
+
})
|
|
21
|
+
.orTee((error) => this.error(error.message));
|
|
22
|
+
}));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as process from "node:process";
|
|
3
|
+
import { printDoctorResult, runDoctor } from "../../../domain/doctor.js";
|
|
4
|
+
export const makeDoctorCommand = (dependencies, config) => new Command()
|
|
5
|
+
.name("doctor")
|
|
6
|
+
.description("Verify the repository setup according to the DevEx guidelines")
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const result = await runDoctor(dependencies, config);
|
|
9
|
+
printDoctorResult(dependencies, result);
|
|
10
|
+
const exitCode = result.hasErrors ? 1 : 0;
|
|
11
|
+
process.exit(exitCode);
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getInfo, printInfo } from "../../../domain/info.js";
|
|
3
|
+
export const makeInfoCommand = (dependencies) => new Command()
|
|
4
|
+
.name("info")
|
|
5
|
+
.description("Display information about the project")
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const result = await getInfo(dependencies)();
|
|
8
|
+
printInfo(result);
|
|
9
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { GitHubService } from "../../../domain/github.js";
|
|
3
|
+
type InitCommandDependencies = {
|
|
4
|
+
gitHubService: GitHubService;
|
|
5
|
+
};
|
|
6
|
+
export declare const makeInitCommand: ({ gitHubService, }: InitCommandDependencies) => Command;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { $ } from "execa";
|
|
4
|
+
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { oraPromise } from "ora";
|
|
7
|
+
import { Repository, RepositoryNotFoundError, } from "../../../domain/github.js";
|
|
8
|
+
import { tf$ } from "../../execa/terraform.js";
|
|
9
|
+
import { payloadSchema as answersSchema, PLOP_MONOREPO_GENERATOR_NAME, } from "../../plop/generators/monorepo/index.js";
|
|
10
|
+
import { setMonorepoGenerator } from "../../plop/index.js";
|
|
11
|
+
import { getGenerator, getPrompts, initPlop } from "../../plop/index.js";
|
|
12
|
+
import { decode } from "../../zod/index.js";
|
|
13
|
+
import { exitWithError } from "../index.js";
|
|
14
|
+
const withSpinner = (text, successText, failText, promise) => ResultAsync.fromPromise(oraPromise(promise, {
|
|
15
|
+
failText,
|
|
16
|
+
successText,
|
|
17
|
+
text,
|
|
18
|
+
}), (cause) => {
|
|
19
|
+
console.error(`Something went wrong: ${JSON.stringify(cause, null, 2)}`);
|
|
20
|
+
return new Error(failText, { cause });
|
|
21
|
+
});
|
|
22
|
+
// TODO: Check Cloud Environment exists
|
|
23
|
+
// TODO: Check CSP CLI is installed
|
|
24
|
+
// TODO: Check user has permissions to handle Terraform state
|
|
25
|
+
const validateAnswers = (githubService) => (answers) => ResultAsync.fromPromise(githubService.getRepository(answers.repoOwner, answers.repoName), (error) => error)
|
|
26
|
+
.andThen(({ fullName }) => errAsync(new Error(`Repository ${fullName} already exists.`)))
|
|
27
|
+
.orElse((error) => error instanceof RepositoryNotFoundError
|
|
28
|
+
? // If repository is not found, it's safe to proceed
|
|
29
|
+
okAsync(answers)
|
|
30
|
+
: // Otherwise, propagate the error
|
|
31
|
+
errAsync(error))
|
|
32
|
+
.map(() => answers);
|
|
33
|
+
const runGeneratorActions = (generator) => (answers) => withSpinner("Creating workspace files...", "Workspace files created successfully!", "Failed to create workspace files.", generator.runActions(answers)).map(() => answers);
|
|
34
|
+
const displaySummary = (initResult) => {
|
|
35
|
+
const { pr, repository } = initResult;
|
|
36
|
+
console.log(chalk.green.bold("\nWorkspace created successfully!"));
|
|
37
|
+
if (repository) {
|
|
38
|
+
console.log(`- Name: ${chalk.cyan(repository.name)}`);
|
|
39
|
+
console.log(`- GitHub Repository: ${chalk.cyan(repository.url)}\n`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(chalk.yellow(`\n⚠️ GitHub repository may not have been created automatically.`));
|
|
43
|
+
}
|
|
44
|
+
if (pr) {
|
|
45
|
+
console.log(chalk.green.bold("\nNext Steps:"));
|
|
46
|
+
console.log(`1. Review the Pull Request in the GitHub repository: ${chalk.underline(pr.url)}`);
|
|
47
|
+
console.log(`2. Visit ${chalk.underline("https://dx.pagopa.it/getting-started")} to deploy your first project\n`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(chalk.yellow(`\n⚠️ There was an error during Pull Request creation.`));
|
|
51
|
+
console.log(`Please, manually create a Pull Request in the GitHub repository to review the scaffolded code.\n`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const checkTerraformCliIsInstalled = (text, successText, failText) => withSpinner(text, successText, failText, tf$ `terraform -version`);
|
|
55
|
+
const checkPreconditions = () => checkTerraformCliIsInstalled("Checking Terraform CLI is installed...", "Terraform CLI is installed!", "Terraform CLI is not installed.");
|
|
56
|
+
const createRemoteRepository = ({ repoName, repoOwner, }) => {
|
|
57
|
+
const cwd = path.resolve(repoName, "infra", "repository");
|
|
58
|
+
const applyTerraform = async () => {
|
|
59
|
+
await tf$({ cwd }) `terraform init`;
|
|
60
|
+
await tf$({ cwd }) `terraform apply -auto-approve`;
|
|
61
|
+
};
|
|
62
|
+
return withSpinner("Creating GitHub repository...", "GitHub repository created successfully!", "Failed to create GitHub repository.", applyTerraform()).map(() => new Repository(repoName, repoOwner));
|
|
63
|
+
};
|
|
64
|
+
const initializeGitRepository = (repository) => {
|
|
65
|
+
const cwd = path.resolve(repository.name);
|
|
66
|
+
const branchName = "features/scaffold-workspace";
|
|
67
|
+
const git$ = $({
|
|
68
|
+
cwd,
|
|
69
|
+
shell: true,
|
|
70
|
+
});
|
|
71
|
+
const pushToOrigin = async () => {
|
|
72
|
+
await git$ `git init`;
|
|
73
|
+
await git$ `git add README.md`;
|
|
74
|
+
await git$ `git commit --no-gpg-sign -m "Create README.md"`;
|
|
75
|
+
await git$ `git branch -M main`;
|
|
76
|
+
await git$ `git remote add origin ${repository.ssh}`;
|
|
77
|
+
await git$ `git push -u origin main`;
|
|
78
|
+
await git$ `git switch -c ${branchName}`;
|
|
79
|
+
await git$ `git add .`;
|
|
80
|
+
await git$ `git commit --no-gpg-sign -m "Scaffold workspace"`;
|
|
81
|
+
await git$ `git push -u origin ${branchName}`;
|
|
82
|
+
};
|
|
83
|
+
return withSpinner("Pushing code to GitHub...", "Code pushed to GitHub successfully!", "Failed to push code to GitHub.", pushToOrigin()).map(() => ({ branchName, repository }));
|
|
84
|
+
};
|
|
85
|
+
const handleNewGitHubRepository = (githubService) => (answers) => createRemoteRepository(answers)
|
|
86
|
+
.andThen(initializeGitRepository)
|
|
87
|
+
.andThen((localWorkspace) => createPullRequest(githubService)(localWorkspace).map((pr) => ({
|
|
88
|
+
pr,
|
|
89
|
+
repository: localWorkspace.repository,
|
|
90
|
+
})));
|
|
91
|
+
const makeInitResult = (answers, { pr, repository }) => ({
|
|
92
|
+
pr,
|
|
93
|
+
repository,
|
|
94
|
+
});
|
|
95
|
+
const createPullRequest = (githubService) => ({ branchName, repository, }) => withSpinner("Creating Pull Request...", "Pull Request created successfully!", "Failed to create Pull Request.", githubService.createPullRequest({
|
|
96
|
+
base: "main",
|
|
97
|
+
body: "This PR contains the scaffolded monorepo structure.",
|
|
98
|
+
head: branchName,
|
|
99
|
+
owner: repository.owner,
|
|
100
|
+
repo: repository.name,
|
|
101
|
+
title: "Scaffold repository",
|
|
102
|
+
}))
|
|
103
|
+
// If PR creation fails, don't block the workflow
|
|
104
|
+
.orElse(() => okAsync(undefined));
|
|
105
|
+
export const makeInitCommand = ({ gitHubService, }) => new Command()
|
|
106
|
+
.name("init")
|
|
107
|
+
.description("Command to initialize resources (like projects, subscriptions, ...)")
|
|
108
|
+
.addCommand(new Command("project")
|
|
109
|
+
.description("Initialize a new monorepo project")
|
|
110
|
+
.action(async function () {
|
|
111
|
+
await checkPreconditions()
|
|
112
|
+
.andThen(initPlop)
|
|
113
|
+
.andTee(setMonorepoGenerator)
|
|
114
|
+
.andThen((plop) => getGenerator(plop)(PLOP_MONOREPO_GENERATOR_NAME))
|
|
115
|
+
.andThen((generator) =>
|
|
116
|
+
// Ask the user the questions defined in the plop generator
|
|
117
|
+
getPrompts(generator)
|
|
118
|
+
// Decode the answers to match the Answers schema
|
|
119
|
+
.andThen(decode(answersSchema))
|
|
120
|
+
// Validate the answers (like checking permissions, checking GitHub user or org existence, etc.)
|
|
121
|
+
.andThen(validateAnswers(gitHubService))
|
|
122
|
+
// Run the generator with the provided answers (this will create the files locally)
|
|
123
|
+
.andThen(runGeneratorActions(generator)))
|
|
124
|
+
.andThen((answers) => handleNewGitHubRepository(gitHubService)(answers).map((repoPr) => makeInitResult(answers, repoPr)))
|
|
125
|
+
.match(displaySummary, exitWithError(this));
|
|
126
|
+
}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { azure, loadConfig } from "@pagopa/dx-savemoney";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
export const makeSavemoneyCommand = () => new Command("savemoney")
|
|
4
|
+
.description("Analyze Azure subscriptions and report unused or inefficient resources")
|
|
5
|
+
.option("-c, --config <path>", "Path to configuration file (JSON)")
|
|
6
|
+
.option("-f, --format <format>", "Report format: json, table, or detailed-json (default: table)", "table")
|
|
7
|
+
.option("-l, --location <string>", "Preferred Azure location for resources", "italynorth")
|
|
8
|
+
.option("-d, --days <number>", "Number of days for metrics analysis", "30")
|
|
9
|
+
.option("-v, --verbose", "Enable verbose logging")
|
|
10
|
+
.action(async function (options) {
|
|
11
|
+
try {
|
|
12
|
+
// Load configuration
|
|
13
|
+
const config = await loadConfig(options.config);
|
|
14
|
+
const finalConfig = {
|
|
15
|
+
...config,
|
|
16
|
+
preferredLocation: options.location || config.preferredLocation,
|
|
17
|
+
timespanDays: Number.parseInt(options.days, 10) || config.timespanDays,
|
|
18
|
+
verbose: options.verbose || false,
|
|
19
|
+
};
|
|
20
|
+
// Run analysis
|
|
21
|
+
await azure.analyzeAzureResources(finalConfig, options.format);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.error(`Analysis failed: ${error instanceof Error ? error.message : error}`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { Config } from "../../config.js";
|
|
3
|
+
import { Dependencies } from "../../domain/dependencies.js";
|
|
4
|
+
import { CodemodCommandDependencies } from "./commands/codemod.js";
|
|
5
|
+
export type CliDependencies = CodemodCommandDependencies;
|
|
6
|
+
export declare const makeCli: (deps: Dependencies, config: Config, cliDeps: CliDependencies, version: string) => Command;
|
|
7
|
+
export declare const exitWithError: (command: Command) => (error: Error) => never;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { makeCodemodCommand, } from "./commands/codemod.js";
|
|
3
|
+
import { makeDoctorCommand } from "./commands/doctor.js";
|
|
4
|
+
import { makeInfoCommand } from "./commands/info.js";
|
|
5
|
+
import { makeInitCommand } from "./commands/init.js";
|
|
6
|
+
import { makeSavemoneyCommand } from "./commands/savemoney.js";
|
|
7
|
+
export const makeCli = (deps, config, cliDeps, version) => {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program.name("dx").description("The CLI for DX-Platform").version(version);
|
|
10
|
+
program.addCommand(makeDoctorCommand(deps, config));
|
|
11
|
+
program.addCommand(makeCodemodCommand(cliDeps));
|
|
12
|
+
program.addCommand(makeInitCommand(deps));
|
|
13
|
+
program.addCommand(makeSavemoneyCommand());
|
|
14
|
+
program.addCommand(makeInfoCommand(deps));
|
|
15
|
+
return program;
|
|
16
|
+
};
|
|
17
|
+
export const exitWithError = (command) => (error) => {
|
|
18
|
+
command.error(error.message);
|
|
19
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pre-configured instance of the `execa` library for running Terraform commands.
|
|
3
|
+
*
|
|
4
|
+
* This instance is customized with specific environment variables and shell settings
|
|
5
|
+
* to ensure compatibility with Terraform automation workflows.
|
|
6
|
+
*
|
|
7
|
+
* Environment Variables:
|
|
8
|
+
* - `NO_COLOR`: Disables colored output to make logs cleaner and easier to parse.
|
|
9
|
+
* - `TF_IN_AUTOMATION`: Indicates that Terraform is running in an automated environment.
|
|
10
|
+
* - `TF_INPUT`: Disables interactive prompts, ensuring non-interactive execution.
|
|
11
|
+
*
|
|
12
|
+
* Configuration:
|
|
13
|
+
* - `shell: true`: Enables the use of the system shell for command execution.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* @example
|
|
17
|
+
* await tf$`terraform init`;
|
|
18
|
+
* await tf$`terraform apply -auto-approve`;
|
|
19
|
+
*/
|
|
20
|
+
export declare const tf$: import("execa").ExecaScriptMethod<{
|
|
21
|
+
environment: {
|
|
22
|
+
NO_COLOR: string;
|
|
23
|
+
TF_IN_AUTOMATION: string;
|
|
24
|
+
TF_INPUT: string;
|
|
25
|
+
};
|
|
26
|
+
shell: true;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { $ } from "execa";
|
|
2
|
+
/**
|
|
3
|
+
* A pre-configured instance of the `execa` library for running Terraform commands.
|
|
4
|
+
*
|
|
5
|
+
* This instance is customized with specific environment variables and shell settings
|
|
6
|
+
* to ensure compatibility with Terraform automation workflows.
|
|
7
|
+
*
|
|
8
|
+
* Environment Variables:
|
|
9
|
+
* - `NO_COLOR`: Disables colored output to make logs cleaner and easier to parse.
|
|
10
|
+
* - `TF_IN_AUTOMATION`: Indicates that Terraform is running in an automated environment.
|
|
11
|
+
* - `TF_INPUT`: Disables interactive prompts, ensuring non-interactive execution.
|
|
12
|
+
*
|
|
13
|
+
* Configuration:
|
|
14
|
+
* - `shell: true`: Enables the use of the system shell for command execution.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* @example
|
|
18
|
+
* await tf$`terraform init`;
|
|
19
|
+
* await tf$`terraform apply -auto-approve`;
|
|
20
|
+
*/
|
|
21
|
+
export const tf$ = $({
|
|
22
|
+
environment: {
|
|
23
|
+
NO_COLOR: "1",
|
|
24
|
+
TF_IN_AUTOMATION: "1",
|
|
25
|
+
TF_INPUT: "0",
|
|
26
|
+
},
|
|
27
|
+
shell: true,
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
vi.mock("execa", () => ({
|
|
3
|
+
$: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
import { $ } from "execa";
|
|
6
|
+
import { getGithubRepo } from "../github-repo.js";
|
|
7
|
+
const mock$ = $;
|
|
8
|
+
describe("getGithubRepo", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it("should return undefined if no remote URL is set", async () => {
|
|
13
|
+
mock$.mockResolvedValue({ stdout: "" });
|
|
14
|
+
const result = await getGithubRepo();
|
|
15
|
+
expect(result).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
it("should parse GitHub repository URL and return owner and repo", async () => {
|
|
18
|
+
mock$.mockResolvedValue({ stdout: "https://github.com/pagopa/dx" });
|
|
19
|
+
const result = await getGithubRepo();
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
owner: "pagopa",
|
|
22
|
+
repo: "dx",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it("should handle repository URLs with .git suffix", async () => {
|
|
26
|
+
mock$.mockResolvedValue({ stdout: "https://github.com/pagopa/dx.git" });
|
|
27
|
+
const result = await getGithubRepo();
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
owner: "pagopa",
|
|
30
|
+
repo: "dx",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it("should throw an error for non-GitHub repositories", async () => {
|
|
34
|
+
mock$.mockResolvedValue({ stdout: "https://gitlab.com/owner/repo" });
|
|
35
|
+
await expect(getGithubRepo()).rejects.toThrow("Only GitHub repositories are supported");
|
|
36
|
+
});
|
|
37
|
+
it("should handle repository names with hyphens", async () => {
|
|
38
|
+
mock$.mockResolvedValue({
|
|
39
|
+
stdout: "https://github.com/my-org/my-repo-name",
|
|
40
|
+
});
|
|
41
|
+
const result = await getGithubRepo();
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
owner: "my-org",
|
|
44
|
+
repo: "my-repo-name",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it("should handle repository names with underscores", async () => {
|
|
48
|
+
mock$.mockResolvedValue({
|
|
49
|
+
stdout: "https://github.com/my_org/my_repo_name",
|
|
50
|
+
});
|
|
51
|
+
const result = await getGithubRepo();
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
owner: "my_org",
|
|
54
|
+
repo: "my_repo_name",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it("should handle ssh repository URLs", async () => {
|
|
58
|
+
mock$.mockResolvedValue({
|
|
59
|
+
stdout: "git@github.com:my-org/my-repo-name.git",
|
|
60
|
+
});
|
|
61
|
+
const result = await getGithubRepo();
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
owner: "my-org",
|
|
64
|
+
repo: "my-repo-name",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { $ } from "execa";
|
|
2
|
+
import * as assert from "node:assert/strict";
|
|
3
|
+
import { githubRepoSchema } from "../../domain/github-repo.js";
|
|
4
|
+
export const getGithubRepo = async () => {
|
|
5
|
+
const result = await $ `git config --get remote.origin.url`;
|
|
6
|
+
const repoUrl = result.stdout.trim();
|
|
7
|
+
if (repoUrl === "") {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
let owner;
|
|
11
|
+
let repo;
|
|
12
|
+
// Handle SSH URLs (git@github.com:owner/repo.git)
|
|
13
|
+
if (repoUrl.startsWith("git@github.com:")) {
|
|
14
|
+
const sshPattern = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/;
|
|
15
|
+
const match = repoUrl.match(sshPattern);
|
|
16
|
+
assert.ok(match, "Invalid GitHub SSH URL format");
|
|
17
|
+
[, owner, repo] = match;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Handle HTTPS URLs (https://github.com/owner/repo.git)
|
|
21
|
+
const url = new URL(repoUrl);
|
|
22
|
+
assert.equal(url.origin, "https://github.com", "Only GitHub repositories are supported");
|
|
23
|
+
const [, urlOwner, urlRepo] = url.pathname.split("/");
|
|
24
|
+
owner = urlOwner;
|
|
25
|
+
repo = urlRepo.replace(/\.git$/, "");
|
|
26
|
+
}
|
|
27
|
+
return githubRepoSchema.parse({
|
|
28
|
+
owner,
|
|
29
|
+
repo,
|
|
30
|
+
});
|
|
31
|
+
};
|