UNPKG

baldrick-broth

Version:
276 lines (275 loc) 8.34 kB
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: [], }); };