Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: config based launch project creation, retry mechanism in case of name conflict #254

Merged
merged 1 commit into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
};
Loading