From c07f605274333a552af3cc85c86c0df54b4f2eff Mon Sep 17 00:00:00 2001 From: David Legrand <1110600+davlgd@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:46:50 +0100 Subject: [PATCH 1/3] feat(create): better post-creation instructions --- src/commands/create.js | 74 +++++++++++++++++++++++++++++++++++++----- src/models/git.js | 37 +++++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/commands/create.js b/src/commands/create.js index f0d388f..b45e0ae 100644 --- a/src/commands/create.js +++ b/src/commands/create.js @@ -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; @@ -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); } }; @@ -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(''); +} diff --git a/src/models/git.js b/src/models/git.js index b06b26e..419a42b 100644 --- a/src/models/git.js +++ b/src/models/git.js @@ -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'; @@ -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', { + 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; + } +} From b17761befc646af5aae0c9f615588fbd2f6ca605 Mon Sep 17 00:00:00 2001 From: David Legrand <1110600+davlgd@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:49:10 +0100 Subject: [PATCH 2/3] feat(deploy): better deployment messages --- src/commands/deploy.js | 86 ++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/commands/deploy.js b/src/commands/deploy.js index b5ce5ad..1e6533e 100644 --- a/src/commands/deploy.js +++ b/src/commands/deploy.js @@ -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': @@ -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 }); } @@ -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} + * @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.'); + } + else { + throw e; + } + }); + + Logger.println(` ${colors.green('✓ Code pushed to Clever Cloud')}`); +} From 5f1d3cf28135bd7a30e0916764cb287bb32d4745 Mon Sep 17 00:00:00 2001 From: David Legrand <1110600+davlgd@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:50:17 +0100 Subject: [PATCH 3/3] chore: adapt messages to new app deployment/creation --- src/commands/cancel-deploy.js | 3 ++- src/commands/delete.js | 10 ++++++---- src/commands/link.js | 5 +++-- src/commands/makeDefault.js | 4 ++-- src/commands/restart.js | 2 +- src/commands/stop.js | 3 ++- src/commands/unlink.js | 4 ++-- src/models/interact.js | 4 ++-- src/models/log-v4.js | 12 ++++++++---- 9 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/commands/cancel-deploy.js b/src/commands/cancel-deploy.js index 3846198..fd877e6 100644 --- a/src/commands/cancel-deploy.js +++ b/src/commands/cancel-deploy.js @@ -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; @@ -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!'); }; diff --git a/src/commands/delete.js b/src/commands/delete.js index 5669ac9..8a13ec5 100644 --- a/src/commands/delete.js +++ b/src/commands/delete.js @@ -1,6 +1,7 @@ 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; @@ -8,16 +9,17 @@ export async function deleteApp (params) { 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`); } } }; diff --git a/src/commands/link.js b/src/commands/link.js index c9a6797..6a9718c 100644 --- a/src/commands/link.js +++ b/src/commands/link.js @@ -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; @@ -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}!`); } diff --git a/src/commands/makeDefault.js b/src/commands/makeDefault.js index f346945..c413215 100644 --- a/src/commands/makeDefault.js +++ b/src/commands/makeDefault.js @@ -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`); }; diff --git a/src/commands/restart.js b/src/commands/restart.js index 069265c..fd9bf4a 100644 --- a/src/commands/restart.js +++ b/src/commands/restart.js @@ -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 diff --git a/src/commands/stop.js b/src/commands/stop.js index 4a2658b..6f366a7 100644 --- a/src/commands/stop.js +++ b/src/commands/stop.js @@ -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!'); } diff --git a/src/commands/unlink.js b/src/commands/unlink.js index b2ef491..4c1628c 100644 --- a/src/commands/unlink.js +++ b/src/commands/unlink.js @@ -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)}!`); }; diff --git a/src/models/interact.js b/src/models/interact.js index 3b99fac..425d6b8 100644 --- a/src/models/interact.js +++ b/src/models/interact.js @@ -1,5 +1,5 @@ import readline from 'node:readline'; - +import colors from 'colors/safe.js'; function ask (question) { const rl = readline.createInterface({ @@ -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); }); diff --git a/src/models/log-v4.js b/src/models/log-v4.js index 1ba0322..48f20ad 100644 --- a/src/models/log-v4.js +++ b/src/models/log-v4.js @@ -6,6 +6,7 @@ import { waitForDeploymentEnd, waitForDeploymentStart } from './deployments.js'; import { ApplicationLogStream } from '@clevercloud/client/esm/streams/application-logs.js'; import { JsonArray } from './json-array.js'; import * as ExitStrategy from '../models/exit-strategy-option.js'; +import { getBest } from './domain.js'; // 2000 logs per 100ms maximum const THROTTLE_ELEMENTS = 2000; @@ -98,9 +99,9 @@ export async function watchDeploymentAndDisplayLogs (options) { ExitStrategy.plotQuietWarning(exitStrategy, quiet); // If in quiet mode, we only log start/finished deployment messages - !quiet && Logger.println('Waiting for deployment to start…'); + !quiet && Logger.println(` ${colors.blue('→ Waiting for deployment to start…')}`); const deployment = await waitForDeploymentStart({ ownerId, appId, deploymentId, commitId, knownDeployments }); - Logger.println(colors.bold.blue(`Deployment started (${deployment.uuid})`)); + Logger.println(` ${colors.green(`✓ Deployment started ${colors.gray(`(${deployment.uuid})`)}`)}`); if (exitStrategy === 'deploy-start') { return; @@ -119,7 +120,7 @@ export async function watchDeploymentAndDisplayLogs (options) { logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred }); } - !quiet && Logger.println('Waiting for application logs…'); + !quiet && Logger.println(` ${colors.blue('→ Waiting for application logs…')}`); // Wait for deployment end (or an error thrown by logs with the deferred) const deploymentEnded = await Promise.race([ @@ -132,7 +133,10 @@ export async function watchDeploymentAndDisplayLogs (options) { } if (deploymentEnded.state === 'OK') { - Logger.println(colors.bold.green('Deployment successful')); + const favouriteDomain = await getBest(appId, ownerId); + Logger.println(''); + Logger.println(colors.bold.green('✓ Access your application at'), colors.underline.bold(`https://${favouriteDomain.fqdn}`)); + Logger.println(colors.bold.blue('→ Manage your application at'), colors.underline.bold(`https://console.clever-cloud.com/goto/${appId}`)); } else if (deploymentEnded.state === 'CANCELLED') { throw new Error('Deployment was cancelled. Please check the activity');