From 36836425d6b550839b577a37d1a33765b3a2dbf6 Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Jun 2024 16:02:45 +0530 Subject: [PATCH] feat: config based launch project creation --- examples/create-launch-project.json | 9 +++ src/commands/app/deploy.ts | 103 ++++++++++++++++------------ src/messages/index.ts | 9 +-- src/util/common-utils.ts | 27 ++++++-- src/util/inquirer.ts | 37 ++++++++-- 5 files changed, 126 insertions(+), 59 deletions(-) create mode 100644 examples/create-launch-project.json diff --git a/examples/create-launch-project.json b/examples/create-launch-project.json new file mode 100644 index 0000000..ca12e2a --- /dev/null +++ b/examples/create-launch-project.json @@ -0,0 +1,9 @@ +{ + "name": "app name", + "type": "GitHub", + "environment": "Default", + "framework": "NextJs", + "build-command": "npm run build", + "out-dir": "./.next", + "branch": "master" +} \ No newline at end of file diff --git a/src/commands/app/deploy.ts b/src/commands/app/deploy.ts index da70318..bef6635 100644 --- a/src/commands/app/deploy.ts +++ b/src/commands/app/deploy.ts @@ -1,8 +1,10 @@ import { ApolloClient } from "@apollo/client/core"; -import { Flags, FlagInput } from "@contentstack/cli-utilities"; +import { Flags, FlagInput, cliux } from "@contentstack/cli-utilities"; import config from "@contentstack/cli-launch/dist/config"; import { GraphqlApiClient } from "@contentstack/cli-launch/dist/util"; import Launch from "@contentstack/cli-launch/dist/commands/launch/index"; + +import { UpdateHostingParams } from "../../types"; import { commonMsg, deployAppMsg } from "../../messages"; import { AppCLIBaseCommand } from "../../app-cli-base-command"; import { @@ -19,8 +21,8 @@ import { setupConfig, disconnectApp, formatUrl, + handleProjectNameConflict, } from "../../util"; -import { UpdateHostingParams } from "../../types"; export default class Deploy extends AppCLIBaseCommand { static description = "Deploy an app"; @@ -47,10 +49,10 @@ export default class Deploy extends AppCLIBaseCommand { description: deployAppMsg.FORCE_DISCONNECT, default: false, }), - "project-type": Flags.string({ + "launch-project-type": Flags.string({ multiple: false, options: ["existing-project", "new-project"], - description: deployAppMsg.PROJECT_TYPE, + description: deployAppMsg.LAUNCH_PROJECT_TYPE, }), config: Flags.string({ char: "c", @@ -88,15 +90,19 @@ export default class Deploy extends AppCLIBaseCommand { return; } - await updateApp( - flags, - this.sharedConfig.org, - { - managementSdk: this.managementAppSdk, - log: this.log, - }, - updateHostingPayload - ); + if (flags["app-url"]) { + const spinner = cliux.loaderV2("Updating App..."); + await updateApp( + flags, + this.sharedConfig.org, + { + managementSdk: this.managementAppSdk, + log: this.log, + }, + updateHostingPayload + ); + cliux.loaderV2("done", spinner); + } this.log( this.$t(deployAppMsg.APP_DEPLOYED, { @@ -106,7 +112,6 @@ export default class Deploy extends AppCLIBaseCommand { ); this.log(`App URL: ${flags["app-url"]}`, "info"); } catch (error: any) { - console.log("error", error); this.log(error?.errorMessage || error?.message || error, "error"); this.exit(1); } @@ -187,20 +192,18 @@ export default class Deploy extends AppCLIBaseCommand { if (isProjectConnected?.length) { this.flags["yes"] = this.flags["yes"] || (await askConfirmation()); if (!this.flags["yes"]) { - this.log( - deployAppMsg.LAUNCH_PROJECT_SKIP_MSG, - "info" - ); - return; + throw new Error(deployAppMsg.APP_UPDATE_TERMINATION_MSG); } + const spinner = cliux.loaderV2("Disconnecting launch project..."); await disconnectApp( this.flags, this.sharedConfig.org, this.developerHubBaseUrl ); + cliux.loaderV2("disconnected...", spinner); } - this.flags["project-type"] = - this.flags["project-type"] || (await askProjectType()); + this.flags["launch-project-type"] = + this.flags["launch-project-type"] || (await askProjectType()); await this.handleProjectType(config, updateHostingPayload, projects); } @@ -218,9 +221,13 @@ export default class Deploy extends AppCLIBaseCommand { ): Promise { let url: string = ""; - if (this.flags["project-type"] === "existing-project") { + if (this.flags["launch-project-type"] === "existing-project") { url = await this.handleExistingProject(updateHostingPayload, projects); - } else if (this.flags["project-type"] === "new-project") { + } else if (this.flags["launch-project-type"] === "new-project") { + config["name"] = await handleProjectNameConflict( + config["name"], + projects + ); url = await this.handleNewProject(config, updateHostingPayload); } else { this.log("Invalid project type", "error"); @@ -252,34 +259,42 @@ export default class Deploy extends AppCLIBaseCommand { /** * Handles the deployment of a new project. - * + * * @param config - The configuration object containing project details. * @param updateHostingPayload - The payload for updating hosting parameters. - * @returns A Promise that resolves to a string. + * @returns The URL of the deployed project. */ async handleNewProject( config: Record, updateHostingPayload: UpdateHostingParams ): Promise { - await Launch.run([ - "--org", - this.sharedConfig.org, - "--name", - config["name"], - "--type", - config["type"], - "--environment", - config["environment"], - "--framework", - config["framework"], - "--build-command", - config["build-command"], - "--out-dir", - config["out-dir"], - "--branch", - config["branch"], - ]); - updateHostingPayload["deployment_url"] = this.flags["app-url"]; + const args = []; + const configMappings = { + org: this.sharedConfig.org, + name: config["name"], + type: config["type"], + environment: config["environment"], + framework: config["framework"], + "build-command": config["build-command"], + "out-dir": config["out-dir"], + branch: config["branch"], + }; + + for (const [key, value] of Object.entries(configMappings)) { + if (config[key]) { + args.push(`--${key}`, value); + } + } + + await Launch.run(args); + const apolloClient = await this.getApolloClient(); + const projects = await getProjects(apolloClient); + const project = projects.find((project) => project.name === config["name"]); + if (project) { + updateHostingPayload["environment_uid"] = project.environmentUid; + updateHostingPayload["project_uid"] = project.uid; + return project.url || ""; + } return ""; } } diff --git a/src/messages/index.ts b/src/messages/index.ts index 988d323..89a9e5c 100644 --- a/src/messages/index.ts +++ b/src/messages/index.ts @@ -113,15 +113,16 @@ const reinstallAppMsg = { } const deployAppMsg = { - APP_UID: "Provide the app UID of an existing app to be reinstalled.", APP_DEPLOYED: "{app} deployed successfully.", FORCE_DISCONNECT: "Force disconnect launch project by skipping the confirmation", - PROJECT_TYPE: "Project Type", + LAUNCH_PROJECT_TYPE: "Launch Project Type", APP_URL: "App URL", HOSTING_TYPE: "Hosting Type", CONFIG_FILE: "[optional] path of config file", - LAUNCH_PROJECT_SKIP_MSG: "Launch Project connected! Skipping deployment process", - DISCONNECT_PROJECT: "Are you sure you want to disconnect launch Project ?" + APP_UPDATE_TERMINATION_MSG: "Launch Project connected! Skipping app hosting updates process...", + DISCONNECT_PROJECT: "Are you sure you want to disconnect launch Project ?", + PROJECT_NOT_FOUND: "Project not found!", + PROJECT_NAME_CONFLICT_FAILED: "Project name conflict resolution failed!" } const messages: typeof errors & diff --git a/src/util/common-utils.ts b/src/util/common-utils.ts index 94914cf..f8da28d 100644 --- a/src/util/common-utils.ts +++ b/src/util/common-utils.ts @@ -17,6 +17,8 @@ import { LogFn, UpdateHostingParams, } from "../types"; +import { askProjectName } from "./inquirer"; +import { deployAppMsg } from "../messages"; export type CommonOptions = { log: LogFn; @@ -304,7 +306,7 @@ async function getProjects( const projects: any = await apolloClient .query({ query: projectsQuery, variables: { query: {} } }) .then(({ data: { projects } }: { data: { projects: any } }) => projects); - + return map( projects.edges, ({ @@ -315,16 +317,14 @@ async function getProjects( deployment: { url }, environment: { uid: environmentUid }, }, - integrations: { - developerHubApp: { uid: developerHubAppUid }, - }, + integrations: { developerHubApp }, }, }) => ({ name, uid, url, environmentUid, - developerHubAppUid, + developerHubAppUid: developerHubApp?.uid || null, }) ); } @@ -384,6 +384,22 @@ function formatUrl(url: string): string { return url ? (url.startsWith("https") ? url : `https://${url}`) : ""; } +const handleProjectNameConflict = async ( + projectName: string, + projects: any[], + retry = 1 +): Promise => { + if (retry > 3) { + throw new Error(deployAppMsg.PROJECT_NAME_CONFLICT_FAILED); + } + const project = projects.find((project) => project.name === projectName); + if (project) { + projectName = await askProjectName(projectName); + return handleProjectNameConflict(projectName, projects, retry + 1); + } + return projectName; +}; + export { getOrganizations, getOrgAppUiLocation, @@ -404,4 +420,5 @@ export { setupConfig, disconnectApp, formatUrl, + handleProjectNameConflict, }; diff --git a/src/util/inquirer.ts b/src/util/inquirer.ts index 709ab44..4f31d13 100644 --- a/src/util/inquirer.ts +++ b/src/util/inquirer.ts @@ -12,7 +12,12 @@ import { import { Installation } from "@contentstack/management/types/app/installation"; import { AppTarget } from "@contentstack/management/types/app/index"; -import messages, { $t, deployAppMsg, errors, uninstallAppMsg } from "../messages"; +import messages, { + $t, + deployAppMsg, + errors, + uninstallAppMsg, +} from "../messages"; import { CommonOptions, getOrganizations, @@ -342,13 +347,13 @@ async function selectProject( } const askProjectType = async (): Promise => { - return cliux.inquire({ + return await cliux.inquire({ type: "list", - name: "selectedProject", - message: "Project type", + name: "selected_project_type", + message: "Launch Project type", choices: [ - { name: "Existing project", value: "existing-project" }, - { name: "New Project", value: "new-project" }, + { name: "Existing", value: "existing-project" }, + { name: "New", value: "new-project" }, ], }); }; @@ -361,6 +366,25 @@ async function askConfirmation(): Promise { }); } +const askProjectName = async ( + projectName: string, +): Promise => { + return await cliux.inquire({ + type: "input", + name: "name", + validate: inquireRequireValidation, + message: `${projectName} project already exist. Enter a new name to create a project.?`, + }); +}; + +function inquireRequireValidation(input: any): string | boolean { + if (isEmpty(input)) { + return "This field can't be empty."; + } + + return true; +} + export { getOrg, getAppName, @@ -375,4 +399,5 @@ export { askProjectType, askConfirmation, selectProject, + askProjectName };