baldrick-broth
Version:
Build automation tool and task runner
248 lines (247 loc) • 9.82 kB
JavaScript
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(','));
}
}
}