@pagopa/dx-cli
Version:
A CLI useful to manage DX tools.
127 lines (126 loc) • 6.64 kB
JavaScript
import chalk from "chalk";
import { Command } from "commander";
import { $ } from "execa";
import { errAsync, okAsync, ResultAsync } from "neverthrow";
import * as path from "node:path";
import { oraPromise } from "ora";
import { Repository, RepositoryNotFoundError, } from "../../../domain/github.js";
import { tf$ } from "../../execa/terraform.js";
import { payloadSchema as answersSchema, PLOP_MONOREPO_GENERATOR_NAME, } from "../../plop/generators/monorepo/index.js";
import { setMonorepoGenerator } from "../../plop/index.js";
import { getGenerator, getPrompts, initPlop } from "../../plop/index.js";
import { decode } from "../../zod/index.js";
import { exitWithError } from "../index.js";
const withSpinner = (text, successText, failText, promise) => ResultAsync.fromPromise(oraPromise(promise, {
failText,
successText,
text,
}), (cause) => {
console.error(`Something went wrong: ${JSON.stringify(cause, null, 2)}`);
return new Error(failText, { cause });
});
// TODO: Check Cloud Environment exists
// TODO: Check CSP CLI is installed
// TODO: Check user has permissions to handle Terraform state
const validateAnswers = (githubService) => (answers) => ResultAsync.fromPromise(githubService.getRepository(answers.repoOwner, answers.repoName), (error) => error)
.andThen(({ fullName }) => errAsync(new Error(`Repository ${fullName} already exists.`)))
.orElse((error) => error instanceof RepositoryNotFoundError
? // If repository is not found, it's safe to proceed
okAsync(answers)
: // Otherwise, propagate the error
errAsync(error))
.map(() => answers);
const runGeneratorActions = (generator) => (answers) => withSpinner("Creating workspace files...", "Workspace files created successfully!", "Failed to create workspace files.", generator.runActions(answers)).map(() => answers);
const displaySummary = (initResult) => {
const { pr, repository } = initResult;
console.log(chalk.green.bold("\nWorkspace created successfully!"));
if (repository) {
console.log(`- Name: ${chalk.cyan(repository.name)}`);
console.log(`- GitHub Repository: ${chalk.cyan(repository.url)}\n`);
}
else {
console.log(chalk.yellow(`\n⚠️ GitHub repository may not have been created automatically.`));
}
if (pr) {
console.log(chalk.green.bold("\nNext Steps:"));
console.log(`1. Review the Pull Request in the GitHub repository: ${chalk.underline(pr.url)}`);
console.log(`2. Visit ${chalk.underline("https://dx.pagopa.it/getting-started")} to deploy your first project\n`);
}
else {
console.log(chalk.yellow(`\n⚠️ There was an error during Pull Request creation.`));
console.log(`Please, manually create a Pull Request in the GitHub repository to review the scaffolded code.\n`);
}
};
const checkTerraformCliIsInstalled = (text, successText, failText) => withSpinner(text, successText, failText, tf$ `terraform -version`);
const checkPreconditions = () => checkTerraformCliIsInstalled("Checking Terraform CLI is installed...", "Terraform CLI is installed!", "Terraform CLI is not installed.");
const createRemoteRepository = ({ repoName, repoOwner, }) => {
const cwd = path.resolve(repoName, "infra", "repository");
const applyTerraform = async () => {
await tf$({ cwd }) `terraform init`;
await tf$({ cwd }) `terraform apply -auto-approve`;
};
return withSpinner("Creating GitHub repository...", "GitHub repository created successfully!", "Failed to create GitHub repository.", applyTerraform()).map(() => new Repository(repoName, repoOwner));
};
const initializeGitRepository = (repository) => {
const cwd = path.resolve(repository.name);
const branchName = "features/scaffold-workspace";
const git$ = $({
cwd,
shell: true,
});
const pushToOrigin = async () => {
await git$ `git init`;
await git$ `git add README.md`;
await git$ `git commit --no-gpg-sign -m "Create README.md"`;
await git$ `git branch -M main`;
await git$ `git remote add origin ${repository.ssh}`;
await git$ `git push -u origin main`;
await git$ `git switch -c ${branchName}`;
await git$ `git add .`;
await git$ `git commit --no-gpg-sign -m "Scaffold workspace"`;
await git$ `git push -u origin ${branchName}`;
};
return withSpinner("Pushing code to GitHub...", "Code pushed to GitHub successfully!", "Failed to push code to GitHub.", pushToOrigin()).map(() => ({ branchName, repository }));
};
const handleNewGitHubRepository = (githubService) => (answers) => createRemoteRepository(answers)
.andThen(initializeGitRepository)
.andThen((localWorkspace) => createPullRequest(githubService)(localWorkspace).map((pr) => ({
pr,
repository: localWorkspace.repository,
})));
const makeInitResult = (answers, { pr, repository }) => ({
pr,
repository,
});
const createPullRequest = (githubService) => ({ branchName, repository, }) => withSpinner("Creating Pull Request...", "Pull Request created successfully!", "Failed to create Pull Request.", githubService.createPullRequest({
base: "main",
body: "This PR contains the scaffolded monorepo structure.",
head: branchName,
owner: repository.owner,
repo: repository.name,
title: "Scaffold repository",
}))
// If PR creation fails, don't block the workflow
.orElse(() => okAsync(undefined));
export const makeInitCommand = ({ gitHubService, }) => new Command()
.name("init")
.description("Command to initialize resources (like projects, subscriptions, ...)")
.addCommand(new Command("project")
.description("Initialize a new monorepo project")
.action(async function () {
await checkPreconditions()
.andThen(initPlop)
.andTee(setMonorepoGenerator)
.andThen((plop) => getGenerator(plop)(PLOP_MONOREPO_GENERATOR_NAME))
.andThen((generator) =>
// Ask the user the questions defined in the plop generator
getPrompts(generator)
// Decode the answers to match the Answers schema
.andThen(decode(answersSchema))
// Validate the answers (like checking permissions, checking GitHub user or org existence, etc.)
.andThen(validateAnswers(gitHubService))
// Run the generator with the provided answers (this will create the files locally)
.andThen(runGeneratorActions(generator)))
.andThen((answers) => handleNewGitHubRepository(gitHubService)(answers).map((repoPr) => makeInitResult(answers, repoPr)))
.match(displaySummary, exitWithError(this));
}));