baldrick-broth
Version:
Build automation tool and task runner
276 lines (275 loc) • 8.34 kB
JavaScript
import { execaCommand } from 'execa';
import YAML from 'yaml';
import CSV from 'papaparse';
import { succeed, willFail } from './railway.js';
import { getSupportedProperty } from './data-value-utils.js';
import { basicCommandExecution } from './basic-execution.js';
import { getSingleCommandLine, mergeTemplateContext } from './templating.js';
import { currentTaskLogger } from './logging.js';
import { coloration } from './coloration.js';
import { appendFile, writeFile } from 'fs/promises';
const toStatus = (params) => {
const { exitCode, failed, isCanceled, timedOut, killed } = params;
if (failed) {
return 'failed';
}
if (isCanceled) {
return 'canceled';
}
if (timedOut) {
return 'timeout';
}
if (killed) {
return 'killed';
}
if (exitCode > 0) {
return 'failed';
}
return 'success';
};
function getErrorMessage(error) {
if (error instanceof Error)
return error.message;
return String(error);
}
const parseJson = (content) => {
try {
const parsed = JSON.parse(content);
return succeed(parsed);
}
catch (error) {
return willFail({ message: getErrorMessage(error) });
}
};
const parseYaml = (content) => {
try {
const parsed = YAML.parse(content);
return succeed(parsed);
}
catch (error) {
return willFail({ message: getErrorMessage(error) });
}
};
const prepareCsv = (content) => content
.split('\n')
.filter((line) => line.length > 2)
.join('\n');
const parseCsv = (content) => {
try {
const parsed = CSV.parse(prepareCsv(content), {
header: true,
});
const { data, errors } = parsed;
if (errors.length > 0) {
return willFail({
message: errors
.map((err) => `Row ${err.row}: ${err.message}`)
.join('\n'),
});
}
if (data.length === 0) {
return willFail({
message: 'Length of csv data should be more than zero',
});
}
return succeed(data);
}
catch (error) {
return willFail({ message: getErrorMessage(error) });
}
};
const forceString = (value) => typeof value === 'string' ? value : JSON.stringify(value, null, 2);
const executeShellCommandLine = async (ctx, params) => {
const { line, name, opts, memoryId, extra } = params;
const templateCtx = mergeTemplateContext({
memoryId,
ctx,
command: opts,
extra: { ...ctx.data, ...extra },
});
const runnableLine = opts.multiline
? line
: getSingleCommandLine(line, templateCtx);
currentTaskLogger.info(`> ${coloration.running(runnableLine)}`);
const { onSuccess, onFailure, stdin } = opts;
let maybeStdin;
if (stdin !== undefined) {
const stdinPropValue = getSupportedProperty(memoryId, ctx, stdin);
if (stdinPropValue === undefined) {
return willFail({
category: 'failed',
line: runnableLine,
stdout: '',
stderr: '',
exitCode: 1,
onFailure,
message: `Could not get property for stdin ${stdin}`,
});
}
maybeStdin = { input: forceString(stdinPropValue) };
}
else {
maybeStdin = {};
}
const { stdout, stderr, all, exitCode, failed, isCanceled, timedOut, killed, } = await execaCommand(runnableLine, {
reject: false,
all: true,
env: { FORCE_COLOR: 'true' },
...maybeStdin,
});
const status = toStatus({ exitCode, failed, isCanceled, timedOut, killed });
if (status === 'success') {
if (onSuccess.includes('json')) {
const parsed = parseJson(stdout);
return parsed.status === 'failure'
? willFail({
category: 'parse-json-failed',
line,
stdout,
stderr,
exitCode,
onFailure,
message: parsed.error.message,
})
: succeed({
format: 'json',
name,
line,
data: parsed.value,
onSuccess,
});
}
if (onSuccess.includes('yaml')) {
const parsed = parseYaml(stdout);
return parsed.status === 'failure'
? willFail({
category: 'parse-yaml-failed',
line,
stdout,
stderr,
exitCode,
onFailure,
message: parsed.error.message,
})
: succeed({
format: 'json',
name,
line,
data: parsed.value,
onSuccess,
});
}
if (onSuccess.includes('csv')) {
const parsed = parseCsv(stdout);
return parsed.status === 'failure'
? willFail({
category: 'parse-csv-failed',
line,
stdout,
stderr,
exitCode,
onFailure,
message: parsed.error.message,
})
: succeed({
format: 'csv',
name,
line,
data: parsed.value,
onSuccess,
});
}
const data = onSuccess.includes('trim')
? (all || stdout).trim()
: all || stdout;
return succeed({ format: 'string', name, line, data, onSuccess });
}
return willFail({
category: 'failed',
line,
stdout,
stderr,
exitCode,
onFailure,
message: `Failed with exit code ${exitCode}`,
});
};
const appendVarToFile = async (memoryId, ctx, anyCommand) => {
const objectValue = getSupportedProperty(memoryId, ctx, anyCommand.value) || {};
const content = forceString(objectValue);
try {
await appendFile(anyCommand.filename, content, { encoding: 'utf8' });
return succeed({
format: 'json',
name: anyCommand.name,
line: '',
data: {},
onSuccess: [],
});
}
catch (e) {
return willFail({
category: 'failed',
line: '',
stdout: '',
stderr: '',
exitCode: 1,
onFailure: [],
message: `Could not append to file ${anyCommand.filename}: ${e}`,
});
}
};
const writeVarToFile = async (memoryId, ctx, anyCommand) => {
const objectValue = getSupportedProperty(memoryId, ctx, anyCommand.value) || {};
const content = forceString(objectValue);
try {
await writeFile(anyCommand.filename, content, { encoding: 'utf8' });
return succeed({
format: 'json',
name: anyCommand.name,
line: '',
data: {},
onSuccess: [],
});
}
catch (e) {
return willFail({
category: 'failed',
line: '',
stdout: '',
stderr: '',
exitCode: 1,
onFailure: [],
message: `Could not write to file ${anyCommand.filename}: ${e}`,
});
}
};
/**
* Executes a command after template expansion
*/
export const executeCommandLine = async (ctx, params) => {
const { line, name, opts, memoryId, extra } = params;
if (opts.a === 'shell') {
return executeShellCommandLine(ctx, {
line,
name,
opts,
memoryId,
extra,
});
}
if (opts.a === 'append-to-file') {
return await appendVarToFile(memoryId, ctx, opts);
}
if (opts.a === 'write-to-file') {
return await writeVarToFile(memoryId, ctx, opts);
}
basicCommandExecution(memoryId, ctx, opts, extra);
return succeed({
format: 'json',
name,
line,
data: {},
onSuccess: [],
});
};