UNPKG

@mondaycom/apps-cli

Version:

A cli tool to manage apps (and monday-code projects) in monday.com

226 lines (225 loc) 10 kB
import axios from 'axios'; import chalk from 'chalk'; import { StatusCodes } from 'http-status-codes'; import { getAppVersionDeploymentStatusUrl, getDeploymentClientUpload, getDeploymentSignedUrl } from '../consts/urls.js'; import { execute } from './api-service.js'; import { getCurrentWorkingDirectory } from './env-service.js'; import { compressBuildToZip, createTarGzArchive, readFileData, readZipFileAsBuffer, verifyClientDirectory, } from './files-service.js'; import { pollPromise } from './polling-service.js'; import { appVersionDeploymentStatusSchema, signedUrlSchema } from './schemas/push-service-schemas.js'; import { HttpError } from '../types/errors/index.js'; import { TimeInMs } from '../types/general/time.js'; import { HttpMethodTypes } from '../types/services/api-service.js'; import { DeploymentStatusTypesSchema, } from '../types/services/push-service.js'; import logger from '../utils/logger.js'; import { createProgressBarString } from '../utils/progress-bar.js'; import { addRegionToQuery } from '../utils/region.js'; import { appsUrlBuilder } from '../utils/urls-builder.js'; export const getSignedStorageUrl = async (appVersionId, region) => { const DEBUG_TAG = 'get_signed_storage_url'; try { const baseSignUrl = getDeploymentSignedUrl(appVersionId); const url = appsUrlBuilder(baseSignUrl); const query = addRegionToQuery({}, region); const response = await execute({ query, url, headers: { Accept: 'application/json' }, method: HttpMethodTypes.POST, }, signedUrlSchema); return response.signed; } catch (error) { logger.debug(error, DEBUG_TAG); if (error instanceof HttpError) { throw error; } throw new Error('Failed to build remote location for upload.'); } }; export const uploadClientZipFile = async (appVersionId, buffer) => { const baseUrl = getDeploymentClientUpload(appVersionId); const url = appsUrlBuilder(baseUrl); const formData = new FormData(); formData.append('zipfile', new Blob([buffer])); const response = await execute({ url, headers: { Accept: 'application/json', 'Content-Type': 'multipart/form-data' }, method: HttpMethodTypes.POST, body: formData, }); return response.data; }; export const getAppVersionDeploymentStatus = async (appVersionId, region) => { try { const baseAppVersionIdStatusUrl = getAppVersionDeploymentStatusUrl(appVersionId); const url = appsUrlBuilder(baseAppVersionIdStatusUrl); const query = addRegionToQuery({}, region); const response = await execute({ query, url, headers: { Accept: 'application/json' }, method: HttpMethodTypes.GET, }, appVersionDeploymentStatusSchema); return response; } catch (error_) { const error = error_ instanceof HttpError ? error_ : new Error('Failed to check app version deployment status.'); throw error; } }; export const pollForDeploymentStatus = async (appVersionId, retryAfter, region, options = {}) => { const { ttl, progressLogger } = options; await pollPromise(async () => { const statusesToKeepPolling = [ DeploymentStatusTypesSchema.started, DeploymentStatusTypesSchema.pending, DeploymentStatusTypesSchema.building, DeploymentStatusTypesSchema['building-infra'], DeploymentStatusTypesSchema['building-app'], DeploymentStatusTypesSchema['deploying-app'], ]; const response = await getAppVersionDeploymentStatus(appVersionId, region); if (statusesToKeepPolling.includes(response.status)) { if (progressLogger) { progressLogger(response.status, response.tip); } return false; } return true; }, retryAfter, ttl || retryAfter * 60); const response = await getAppVersionDeploymentStatus(appVersionId, region); return response; }; export const uploadFileToStorage = async (cloudStorageUrl, fileData, fileType) => { const DEBUG_TAG = 'upload_file_to_storage'; try { const response = await axios.request({ method: 'put', url: cloudStorageUrl, data: fileData, headers: { 'Content-Type': fileType }, }); return response; } catch (error) { logger.debug(error, DEBUG_TAG); throw new Error('Failed in uploading the project.'); } }; export const buildClientZip = async (ctx, task) => { if (!ctx.directoryPath) { const currentDirectoryPath = getCurrentWorkingDirectory(); logger.debug(`Directory path not provided. using current directory: ${currentDirectoryPath}`); ctx.directoryPath = currentDirectoryPath; } task.output = `Building client zip from "${ctx.directoryPath}" directory`; verifyClientDirectory(ctx.directoryPath); ctx.archivePath = await compressBuildToZip(ctx.directoryPath); }; export const deployClientZip = async (ctx, task) => { task.output = `Deploying client zip (${ctx.archivePath}) to cdn`; const buffer = readZipFileAsBuffer(ctx.archivePath); const data = await uploadClientZipFile(ctx.appVersionId, buffer); task.title = `Your project is live at: ${data.url}\n You can download your source code here: ${data.sourceUrl}`; }; export const buildAssetToDeployTask = async (ctx, task) => { const DEBUG_TAG = 'build_asset_to_deploy_task'; try { if (!ctx.directoryPath) { const currentDirectoryPath = getCurrentWorkingDirectory(); logger.debug(`Directory path not provided. using current directory: ${currentDirectoryPath}`); ctx.directoryPath = currentDirectoryPath; } task.output = `Building asset to deploy from "${ctx.directoryPath}" directory`; const archivePath = await createTarGzArchive(ctx.directoryPath, 'code'); ctx.archivePath = archivePath; ctx.showPrepareEnvironmentTask = true; } catch (error) { logger.debug(error, DEBUG_TAG); throw error; } }; export const prepareEnvironmentTask = async (ctx) => { try { const signedCloudStorageUrl = await getSignedStorageUrl(ctx.appVersionId, ctx.region); const archiveContent = readFileData(ctx.archivePath); ctx.signedCloudStorageUrl = signedCloudStorageUrl; ctx.archiveContent = archiveContent; ctx.showUploadAssetTask = true; } catch (error) { if (error instanceof HttpError && error.code === StatusCodes.CONFLICT) { const msg = `This deployment could not start, as there is already an existing deployment in progress for app version ${ctx.appVersionId}. - Run the command "code:status -v ${ctx.appVersionId}" to check the existing deployment status. - It might take a few minutes to complete, or if enough time passes so it will fail, you can try a new deployment with "code:push".`; throw new Error(msg); } throw error; } }; export const uploadAssetTask = async (ctx, task) => { const { signedCloudStorageUrl, archiveContent } = ctx; await uploadFileToStorage(signedCloudStorageUrl, archiveContent, 'application/zip'); task.title = 'Asset uploaded successfully'; ctx.showHandleDeploymentTask = true; }; const MAX_PROGRESS_VALUE = 100; const PROGRESS_STEP = Math.round(MAX_PROGRESS_VALUE / 100); const STATUS_TO_PROGRESS_VALUE = { [DeploymentStatusTypesSchema.failed]: 0, [DeploymentStatusTypesSchema.started]: 0, [DeploymentStatusTypesSchema.pending]: PROGRESS_STEP * 5, [DeploymentStatusTypesSchema.building]: PROGRESS_STEP * 10, [DeploymentStatusTypesSchema['building-infra']]: PROGRESS_STEP * 25, [DeploymentStatusTypesSchema['building-app']]: PROGRESS_STEP * 50, [DeploymentStatusTypesSchema['deploying-app']]: PROGRESS_STEP * 75, [DeploymentStatusTypesSchema.successful]: PROGRESS_STEP * 100, }; const setCustomTip = (tip, color = 'green') => { let chalkColor = chalk.green; switch (color) { case 'yellow': { chalkColor = chalk.yellow; break; } } return tip ? `\n ${chalk.italic(chalkColor(tip))}` : ''; }; const finalizeDeployment = (deploymentStatus, task) => { switch (deploymentStatus.status) { case DeploymentStatusTypesSchema.failed: { const customTip = setCustomTip(deploymentStatus.tip, 'yellow'); task.title = (deploymentStatus.error?.message.trimStart() || 'Deployment process has failed') + customTip; throw new Error(task.title); } case DeploymentStatusTypesSchema.successful: { const deploymentUrl = `Deployment successfully finished, deployment url: ${deploymentStatus.deployment.url}`; task.title = deploymentUrl; break; } default: { const generalErrorMessage = 'Something went wrong, the deployment url is missing.'; task.title = generalErrorMessage; throw new Error(generalErrorMessage); } } }; export const handleDeploymentTask = async (ctx, task) => { task.output = createProgressBarString(MAX_PROGRESS_VALUE, 0); const now = Date.now(); const retryAfter = TimeInMs.second * 5; const ttl = TimeInMs.minute * 30; const deploymentStatus = await pollForDeploymentStatus(ctx.appVersionId, retryAfter, ctx.region, { ttl, progressLogger: (message, tip) => { const deltaInSeconds = (Date.now() - now) / TimeInMs.second; task.title = `Deployment in progress: ${message}`; const customTip = setCustomTip(tip); task.output = createProgressBarString(MAX_PROGRESS_VALUE, STATUS_TO_PROGRESS_VALUE[message], deltaInSeconds) + customTip; }, }); finalizeDeployment(deploymentStatus, task); };