Skip to content

Commit

Permalink
Merge pull request #254 from contentstack/feat/DX-785
Browse files Browse the repository at this point in the history
feat: config based launch project creation, retry mechanism in case of name conflict
  • Loading branch information
aman19K authored Jun 17, 2024
2 parents 43b0570 + 3683642 commit b2bda01
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 59 deletions.
9 changes: 9 additions & 0 deletions examples/create-launch-project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "app name",
"type": "GitHub",
"environment": "Default",
"framework": "NextJs",
"build-command": "npm run build",
"out-dir": "./.next",
"branch": "master"
}
103 changes: 59 additions & 44 deletions src/commands/app/deploy.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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";
Expand All @@ -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",
Expand Down Expand Up @@ -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, {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}

Expand All @@ -218,9 +221,13 @@ export default class Deploy extends AppCLIBaseCommand {
): Promise<void> {
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");
Expand Down Expand Up @@ -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<string, string>,
updateHostingPayload: UpdateHostingParams
): Promise<string> {
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 "";
}
}
9 changes: 5 additions & 4 deletions src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down
27 changes: 22 additions & 5 deletions src/util/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
LogFn,
UpdateHostingParams,
} from "../types";
import { askProjectName } from "./inquirer";
import { deployAppMsg } from "../messages";

export type CommonOptions = {
log: LogFn;
Expand Down Expand Up @@ -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,
({
Expand All @@ -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,
})
);
}
Expand Down Expand Up @@ -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<string> => {
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,
Expand All @@ -404,4 +420,5 @@ export {
setupConfig,
disconnectApp,
formatUrl,
handleProjectNameConflict,
};
37 changes: 31 additions & 6 deletions src/util/inquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -342,13 +347,13 @@ async function selectProject(
}

const askProjectType = async (): Promise<string> => {
return cliux.inquire<string>({
return await cliux.inquire<string>({
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" },
],
});
};
Expand All @@ -361,6 +366,25 @@ async function askConfirmation(): Promise<boolean> {
});
}

const askProjectName = async (
projectName: string,
): Promise<string> => {
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,
Expand All @@ -375,4 +399,5 @@ export {
askProjectType,
askConfirmation,
selectProject,
askProjectName
};

0 comments on commit b2bda01

Please sign in to comment.