UNPKG

baldrick-broth

Version:
248 lines (247 loc) 9.82 kB
import path from 'node:path'; import { Listr } from 'listr2'; import { executeCommandLine } from './execution.js'; import { expandBatchStep } from './expand-batch.js'; import { currentTaskLogger, replayLogToConsole, telemetryTaskLogger, telemetryTaskRefLogger, } from './logging.js'; import { succeed } from './railway.js'; import { getSupportedProperty, isTruthy, setDataValue, } from './data-value-utils.js'; import { coloration } from './coloration.js'; import { isStringArray } from './string-utils.js'; const SLEEP_KO = 800; const SLEEP_MIN = 150; async function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } const defaultOnResultFlags = { save: false, silent: false, debug: false, exit: false, }; const toOnResultFlags = (flags) => ({ save: flags.includes('save'), silent: flags.includes('silent'), debug: flags.includes('debug'), exit: flags.includes('exit'), }); const asJSONLog = (value) => coloration.jsonBlock(JSON.stringify(value, null, 2)); const debugContext = (ctx) => { currentTaskLogger.info(asJSONLog({ keys: Object.keys(ctx), runtime: ctx.runtime, data: ctx.data })); }; const toCommandLineAction = (ctx, commandLineInput) => { const title = commandLineInput.opts.title ? commandLineInput.opts.title : commandLineInput.opts.name; const commandTask = { title, enabled(_) { const ifPath = commandLineInput.opts.a === 'shell' && commandLineInput.opts.if; if (ifPath === undefined || ifPath === false) { return true; } const shouldEnable = isTruthy(getSupportedProperty(commandLineInput.memoryId, ctx, ifPath)); return shouldEnable; }, async task(taskContext, task) { const isPrompt = commandLineInput.opts.a.startsWith('prompt-'); if (isPrompt) { await interactivePrompt(commandLineInput, taskContext, task, ctx); return; } task.output = commandLineInput.line; const cmdLineResult = await executeCommandLine(ctx, commandLineInput); const successFlags = commandLineInput.opts.a === 'shell' ? toOnResultFlags(commandLineInput.opts.onSuccess) : defaultOnResultFlags; const failureFlags = commandLineInput.opts.a === 'shell' ? toOnResultFlags(commandLineInput.opts.onFailure) : defaultOnResultFlags; await sleep(SLEEP_MIN); if (cmdLineResult.status === 'success') { const { value: { data, name }, } = cmdLineResult; task.title = name; if (successFlags.save) { setDataValue(commandLineInput.memoryId, ctx, commandLineInput.name, data); } setDataValue(commandLineInput.memoryId, ctx, `result-of-${commandLineInput.name}`, true); if (!successFlags.silent) { const dataView = cmdLineResult.value.format === 'string' ? `${data}` : JSON.stringify(data, null, 2); currentTaskLogger.info(coloration.stepTitle(`◼ ${title}`)); currentTaskLogger.info(dataView); } if (successFlags.debug) { debugContext(ctx); } task.output = 'OK'; } else if (cmdLineResult.status === 'failure') { if (!failureFlags.silent) { currentTaskLogger.info([ cmdLineResult.error.stdout, cmdLineResult.error.stderr, cmdLineResult.error.message, ].join('\n\n')); } if (failureFlags.debug) { debugContext(ctx); } task.output = 'KO'; await sleep(SLEEP_KO); throw new Error(`KO: ${title}`); } await sleep(SLEEP_MIN); }, }; return commandTask; }; const capitalizeWord = (text) => text.length > 0 ? text[0]?.toUpperCase() + text.slice(1).toLowerCase() : ''; const toBatchStepAction = (ctx, batchStep) => { const title = capitalizeWord(batchStep.name); const batchTask = { title, async task(_, task) { const commandsForStep = expandBatchStep(ctx, batchStep); if (commandsForStep.status === 'failure') { currentTaskLogger.warn({ messages: commandsForStep.error.messages }); task.output = coloration.warn('KO'); } if (commandsForStep.status === 'success') { const commandTasks = commandsForStep.value.map((input) => toCommandLineAction(ctx, input)); return task.newListr([...commandTasks], { exitOnError: false }); } else { return; } }, }; return succeed(batchTask); }; const makeMessage = (title, messages) => `${title}: ${messages.join('\n')}`; export const createTaskAction = (buildCtx) => async (parameters) => { const pwd = process.cwd(); const telemetryName = buildCtx.build.engine?.telemetry.name; const projectName = telemetryName === undefined ? path.basename(pwd) : telemetryName; const runtime = { pwd, project: { name: projectName, }, parameters, }; currentTaskLogger.info(coloration.taskTitle(buildCtx.task.title)); const ctx = { ...buildCtx, runtime, data: { status: 'created' } }; const started = process.hrtime(); const { task } = ctx; const listTasks = []; if (task.before !== undefined) { const beforeStep = toBatchStepAction(ctx, task.before); if (beforeStep.status === 'failure') { currentTaskLogger.error(makeMessage(`Before ${buildCtx.task.name}`, beforeStep.error.messages)); } else { listTasks.push(beforeStep.value); } } const mainStep = toBatchStepAction(ctx, task.main); if (mainStep.status === 'failure') { currentTaskLogger.error(makeMessage(`Main ${buildCtx.task.name}`, mainStep.error.messages)); } else { listTasks.push(mainStep.value); } if (task.after !== undefined) { const afterStep = toBatchStepAction(ctx, task.after); if (afterStep.status === 'failure') { currentTaskLogger.error(makeMessage(`After ${buildCtx.task.name}`, afterStep.error.messages)); } else { listTasks.push(afterStep.value); } } if (listTasks.length > 0) { const mainTask = new Listr(listTasks, { exitOnError: false, }); try { await mainTask.run(ctx); logTaskStatistics(started, ctx); await replayLogToConsole(); } catch (error) { currentTaskLogger.error(error); } } }; async function interactivePrompt(commandLineInput, taskContext, task, ctx) { if (commandLineInput.opts.a === 'prompt-input') { taskContext.input = await task.prompt({ type: 'Input', message: commandLineInput.opts.message, }); setDataValue(commandLineInput.memoryId, ctx, commandLineInput.opts.name, taskContext.input); } if (commandLineInput.opts.a === 'prompt-password') { taskContext.input = await task.prompt({ type: 'Password', message: commandLineInput.opts.message, }); setDataValue(commandLineInput.memoryId, ctx, commandLineInput.opts.name, taskContext.input); } if (commandLineInput.opts.a === 'prompt-choices') { taskContext.input = await task.prompt({ type: 'Select', message: commandLineInput.opts.message, choices: commandLineInput.opts.choices, }); setDataValue(commandLineInput.memoryId, ctx, commandLineInput.opts.name, taskContext.input); } if (commandLineInput.opts.a === 'prompt-confirm') { taskContext.input = await task.prompt({ type: 'Confirm', message: commandLineInput.opts.message, }); setDataValue(commandLineInput.memoryId, ctx, commandLineInput.opts.name, taskContext.input); } if (commandLineInput.opts.a === 'prompt-select') { const possibleChoices = getSupportedProperty(commandLineInput.memoryId, ctx, commandLineInput.opts.select); const choices = isStringArray(possibleChoices) ? possibleChoices : ['The choice should be an array (645608)']; taskContext.input = await task.prompt({ type: 'Select', message: commandLineInput.opts.message, choices, }); setDataValue(commandLineInput.memoryId, ctx, commandLineInput.opts.name, taskContext.input); } } function logTaskStatistics(started, ctx) { const date = new Date(); const finished = process.hrtime(started); date.getMonth; telemetryTaskLogger.info([ ctx.runtime.project.name, ctx.task.name, date.getFullYear(), date.getMonth(), date.getDate(), date.getDay(), finished[0], ].join(',')); // Create a reference file for available tasks for (const workflowKey in ctx.build.workflows) { const tasks = Object.keys(ctx.build.workflows[workflowKey]?.tasks || {}); for (const taskId of tasks) { telemetryTaskRefLogger.info([ ctx.runtime.project.name, `${workflowKey}.${taskId}`, date.getFullYear(), date.getMonth(), ].join(',')); } } }