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

Beautify apps commands #862

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion src/commands/cancel-deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Application from '../models/application.js';
import { Logger } from '../logger.js';
import { getAllDeployments, cancelDeployment } from '@clevercloud/client/esm/api/v2/application.js';
import { sendToApi } from '../models/send-to-api.js';
import colors from 'colors/safe.js';

export async function cancelDeploy (params) {
const { alias, app: appIdOrName } = params.options;
Expand All @@ -16,5 +17,5 @@ export async function cancelDeploy (params) {
const deploymentId = deployments[0].id;
await cancelDeployment({ id: ownerId, appId, deploymentId }).then(sendToApi);

Logger.println('Deployment cancelled!');
Logger.println(colors.bold.green('✓'), 'Deployment successfully cancelled!');
};
74 changes: 65 additions & 9 deletions src/commands/create.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import path from 'node:path';
import colors from 'colors/safe.js';
import * as Application from '../models/application.js';
import * as AppConfig from '../models/app_configuration.js';
import { Logger } from '../logger.js';
import { isGitRepo, isGitWorkingDirectoryClean } from '../models/git.js';

export async function create (params) {
const { type: typeName } = params.options;
Expand Down Expand Up @@ -37,15 +39,7 @@ export async function create (params) {

case 'human':
default:
if (isTask) {
Logger.println('Your application has been successfully created as a task!');
Logger.println(`The "CC_RUN_COMMAND" environment variable has been set to "${taskCommand}"`);
}
else {
Logger.println('Your application has been successfully created!');
}
Logger.println(`ID: ${app.id}`);
Logger.println(`Name: ${name}`);
displayAppCreation(app, alias, github, taskCommand);
}
};

Expand All @@ -59,3 +53,65 @@ function getGithubDetails (githubOwnerRepo) {
function getCurrentDirectoryName () {
return path.basename(process.cwd());
}

/**
* Display the application creation message in a human-readable format
* @param {Object} app - The application object
* @param {string} alias - The alias of the application
* @param {Object} github - The GitHub details
* @param {string} taskCommand - The task command
*/
function displayAppCreation (app, alias, github, taskCommand) {

// If it's not a GitHub application return an object, otherwise return false
const gitStatus = !github && {
isClean: isGitWorkingDirectoryClean(),
getMessage: () => !isGitRepo()
? 'Initialize a git repository first, for example:'
: 'Commit your changes first:',
getCommands: () => !isGitRepo()
? ['git init', 'git add .', 'git commit -m "Initial commit"']
: ['git add .', 'git commit -m "Initial commit"'],
};

const isTask = app.instance.lifetime === 'TASK';
const fields = [
['Type', colors.blue(`⬢ ${app.instance.variant.name}`)],
['ID', app.id],
['Org ID', app.ownerId],
['Name', app.name],
github && ['GitHub', `${github.owner}/${github.name}`],
alias && alias !== app.name && ['Alias', alias],
['Region', app.zone.toLocaleUpperCase()],
isTask && ['Task', `"${taskCommand}"`],
].filter(Boolean);

Logger.println(`${colors.green('✓')} ${colors.bold('Application')} ${colors.green(app.name)} ${colors.bold('successfully created!')}`);
Logger.println();

fields.forEach(([label, value]) => {
if (value == null) return;
Logger.println(` ${colors.bold(label.padEnd(8))} ${colors.grey(value)}`);
});

Logger.println();
Logger.println(' ' + colors.bold('Next steps:'));

if (gitStatus) {
Logger.println(` ${colors.yellow('!')} ${colors.white(gitStatus.getMessage())}`);
gitStatus.getCommands().forEach((cmd) => {
Logger.println(` ${colors.grey('$')} ${colors.yellow(cmd)}`);
});
Logger.println();
}

const deployCommand = github ? 'clever restart' : 'clever deploy';
if (github) {
Logger.println(` ${colors.blue('→')} Push changes to ${colors.blue(`${github.owner}/${github.name}`)} GitHub repository to trigger a deployment, or ${colors.blue(deployCommand)} the latest pushed commit`);
}
else {
Logger.println(` ${colors.blue('→')} Run ${colors.blue(deployCommand)} ${isTask ? 'to execute your task' : 'to deploy your application'}`);
}
Logger.println(` ${colors.blue('→')} View your application at: ${colors.underline(`https://console.clever-cloud.com/goto/${app.id}`)}`);
Logger.println('');
}
10 changes: 6 additions & 4 deletions src/commands/delete.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import * as AppConfig from '../models/app_configuration.js';
import * as Application from '../models/application.js';
import { Logger } from '../logger.js';
import colors from 'colors/safe.js';

export async function deleteApp (params) {
const { alias, app: appIdOrName, yes: skipConfirmation } = params.options;
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);

const app = await Application.get(ownerId, appId);
if (app == null) {
Logger.println('The application doesn\'t exist');
throw new Error('The application doesn\'t exist');
}
else {
// delete app
await Application.deleteApp(app, skipConfirmation);
Logger.println('The application has been deleted');
// unlink app
Logger.println(`${colors.green('✓')} Application ${colors.green(colors.bold(`${app.name}`))} successfully deleted!`);
Logger.println(` ${colors.gray('•')} Application ID: ${colors.gray(app.id)}`);

const wasUnlinked = await AppConfig.removeLinkedApplication({ appId, alias });
if (wasUnlinked) {
Logger.println('The application has been unlinked');
Logger.println(` ${colors.blue('→')} Local alias ${colors.blue(alias || app.name)} unlinked`);
}
}
};
86 changes: 54 additions & 32 deletions src/commands/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ export async function deploy (params) {

await git.addRemote(appData.alias, appData.deployUrl);

Logger.println(colors.bold.blue(`Remote application is app_id=${appId}, alias=${appData.alias}, name=${appData.name}`));

Logger.println(colors.bold.blue(`Remote application belongs to ${ownerId}`));

if (commitIdToPush === remoteHeadCommitId) {
switch (sameCommitPolicy) {
case 'ignore':
Expand All @@ -42,43 +38,19 @@ export async function deploy (params) {
return restartOnSameCommit(ownerId, appId, commitIdToPush, quiet, true, exitStrategy);
case 'error':
default: {
const upToDateMessage = `The clever-cloud application is up-to-date (${colors.green(remoteHeadCommitId)}).\nYou can set a policy with 'same-commit-policy' to handle differently when remote HEAD has the same commit as the one to push.\nOr try this command to restart the application:`;
if (commitIdToPush !== deployedCommitId) {
const restartWithId = `clever restart --commit ${commitIdToPush}`;
throw new Error(`${upToDateMessage}\n${colors.yellow(restartWithId)}`);
}
throw new Error(`${upToDateMessage}\n${colors.yellow('clever restart')}`);
const restartCommand = commitIdToPush !== deployedCommitId ? `clever restart --commit ${commitIdToPush}` : 'clever restart';
const upToDateMessage = `Remote HEAD has the same commit as the one to push ${colors.grey(`(${remoteHeadCommitId})`)}, your application is up-to-date.\nCreate a new commit, use ${colors.blue(restartCommand)} or the ${colors.blue('--same-commit-policy')} option.`;
throw new Error(upToDateMessage);
}
}
}

if (remoteHeadCommitId == null || deployedCommitId == null) {
Logger.println('App is brand new, no commits on remote yet');
}
else {
Logger.println(`Remote git head commit is ${colors.green(remoteHeadCommitId)}`);
Logger.println(`Current deployed commit is ${colors.green(deployedCommitId)}`);
}
Logger.println(`New local commit to push is ${colors.green(commitIdToPush)} (from ${colors.green(branchRefspec)})`);

// It's sometimes tricky to figure out the deployment ID for the current git push.
// We on have the commit ID but there in a situation where the last deployment was cancelled, it may have the same commit ID.
// So before pushing, we get the last deployments so we can after the push figure out which deployment is new…
const knownDeployments = await getAllDeployments({ id: ownerId, appId, limit: 5 }).then(sendToApi);

Logger.println('Pushing source code to Clever Cloud…');

await git.push(appData.deployUrl, commitIdToPush, force)
.catch(async (e) => {
const isShallow = await git.isShallow();
if (isShallow) {
throw new Error('Failed to push your source code because your repository is shallow and therefore cannot be pushed to the Clever Cloud remote.');
}
else {
throw e;
}
});
Logger.println(colors.bold.green('Your source code has been pushed to Clever Cloud.'));
await pushAndDisplay(appData.name, appData.deployUrl, ownerId, appId, remoteHeadCommitId, deployedCommitId, commitIdToPush, branchRefspec, force);

return Log.watchDeploymentAndDisplayLogs({ ownerId, appId, commitId: commitIdToPush, knownDeployments, quiet, exitStrategy });
}
Expand All @@ -103,3 +75,53 @@ async function getBranchToDeploy (branchName, tagName) {
return await git.getFullBranch(branchName);
}
}

/**
* Push the code to Clever Cloud and display the progress
* @param {string} name
* @param {string} deployUrl
* @param {string} ownerId
* @param {string} appId
* @param {string} remoteHeadCommitId
* @param {string} deployedCommitId
* @param {string} commitIdToPush
* @param {string} branchRefspec
* @param {boolean} force
* @returns {Promise<void>}
* @throws {Error} if the push fails
* @private
*/
async function pushAndDisplay (name, deployUrl, ownerId, appId, remoteHeadCommitId, deployedCommitId, commitIdToPush, branchRefspec, force) {

Logger.println(`${colors.blue('')}${colors.bold(`🚀 Deploying ${colors.green(name)}`)}`);
Logger.println(` Application ID ${colors.gray(`${appId}`)}`);
Logger.println(` Organization ID ${colors.gray(`${ownerId}`)}`);
Logger.println();

Logger.println(colors.bold('🔀 Git information'));
if (remoteHeadCommitId == null || deployedCommitId == null) {
Logger.println(` ${colors.yellow('!')} App is brand new, no commits on remote yet`);
}
else {
Logger.println(` Remote head ${colors.yellow(remoteHeadCommitId)} (${branchRefspec})`);
Logger.println(` Deployed commit ${colors.yellow(deployedCommitId)}`);
}
Logger.println(` Local commit ${colors.yellow(commitIdToPush)} ${colors.blue('[will be deployed]')}`);
Logger.println();

Logger.println(colors.bold('🔄 Deployment progress'));
Logger.println(` ${colors.blue('→ Pushing source code to Clever Cloud…')}`);

await git.push(deployUrl, commitIdToPush, force)
.catch(async (e) => {
const isShallow = await git.isShallow();
if (isShallow) {
throw new Error('Failed to push your source code because your repository is shallow and therefore cannot be pushed to the Clever Cloud remote.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be interested in the reason of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was there before. My point here was mainly to change how things are printed, not how the CLI works (except to add some info about git repo):

const isShallow = await git.isShallow();

}
else {
throw e;
}
});

Logger.println(` ${colors.green('✓ Code pushed to Clever Cloud')}`);
}
5 changes: 3 additions & 2 deletions src/commands/link.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Application from '../models/application.js';
import { Logger } from '../logger.js';

import colors from 'colors/safe.js';
export async function link (params) {
const [app] = params.args;
const { org: orgaIdOrName, alias } = params.options;
Expand All @@ -11,5 +11,6 @@ export async function link (params) {

await Application.linkRepo(app, orgaIdOrName, alias);

Logger.println('Your application has been successfully linked!');
const linkedMessage = alias ? ` to local alias ${colors.green(alias)}` : '';
Logger.println(colors.bold.green('✓'), `Application ${colors.green(app.app_name || app.app_id)} has been successfully linked${linkedMessage}!`);
}
4 changes: 2 additions & 2 deletions src/commands/makeDefault.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as AppConfig from '../models/app_configuration.js';
import { Logger } from '../logger.js';

import colors from 'colors/safe.js';
export async function makeDefault (params) {
const [alias] = params.args;

await AppConfig.setDefault(alias);

Logger.println(`The application ${alias} has been set as default`);
Logger.println(colors.bold.green('✓'), `The application ${colors.green(alias)} has been set as default`);
};
2 changes: 1 addition & 1 deletion src/commands/restart.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function restart (params) {
const commitId = fullCommitId || remoteCommitId;
if (commitId != null) {
const cacheSuffix = withoutCache ? ' without using cache' : '';
Logger.println(`Restarting ${app.name} on commit ${colors.green(commitId)}${cacheSuffix}`);
Logger.println(`🔄 Restarting ${colors.bold(app.name)}${cacheSuffix} ${colors.gray(`(commit ${commitId})`)}`);
}

// This should be handled by the API when a deployment ID is set but we'll do this for now
Expand Down
3 changes: 2 additions & 1 deletion src/commands/stop.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import * as Application from '../models/application.js';
import * as application from '@clevercloud/client/esm/api/v2/application.js';
import { Logger } from '../logger.js';
import { sendToApi } from '../models/send-to-api.js';
import colors from 'colors/safe.js';

export async function stop (params) {
const { alias, app: appIdOrName } = params.options;
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);

await application.undeploy({ id: ownerId, appId }).then(sendToApi);
Logger.println('App successfully stopped!');
Logger.println(colors.bold.green('✓'), 'Application successfully stopped!');
}
4 changes: 2 additions & 2 deletions src/commands/unlink.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as AppConfig from '../models/app_configuration.js';
import * as Application from '../models/application.js';
import { Logger } from '../logger.js';

import colors from 'colors/safe.js';
export async function unlink (params) {
const [alias] = params.args;
const app = await AppConfig.getAppDetails({ alias });

await Application.unlinkRepo(app.alias);
Logger.println('Your application has been successfully unlinked!');
Logger.println(colors.bold.green('✓'), `Application ${colors.green(app.appId)} has been successfully unlinked from local alias ${colors.green(app.alias)}!`);
};
37 changes: 37 additions & 0 deletions src/models/git.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import childProcess from 'node:child_process';

import _ from 'lodash';
import git from 'isomorphic-git';
Expand Down Expand Up @@ -130,3 +131,39 @@ export async function isShallow () {
return false;
}
}

/**
* Check if the current directory is a git repository
* @returns {boolean}
* @public
*/
export function isGitRepo () {
try {
childProcess.execSync('git rev-parse --is-inside-work-tree', {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we start assuming user has got git binary on its machine.

This is something we need to document. I feel like it is a breaking change right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made it as is, we can also check if git is here, if not, don't print anything related. But as the user need a git repo with commits, I think it's could be better to print a warning if no git is found.

I think we already tell in doc git is mandatory, but we can insist more on this.

stdio: 'ignore',
stderr: 'ignore',
});
return true;
}
catch (e) {
return false;
}
}

/**
* Check if the current git working directory is clean
* @returns {boolean}
* @public
*/
export function isGitWorkingDirectoryClean () {
try {
const status = childProcess.execSync('git status --porcelain=v1', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
return status.trim() === '';
}
catch (e) {
return false;
}
}
4 changes: 2 additions & 2 deletions src/models/interact.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import readline from 'node:readline';

import colors from 'colors/safe.js';
function ask (question) {

const rl = readline.createInterface({
Expand All @@ -8,7 +8,7 @@ function ask (question) {
});

return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.question(`${colors.bold.blue('?')} ${question} `, (answer) => {
rl.close();
resolve(answer);
});
Expand Down
Loading
Loading