diff --git a/packages/build/src/core/commands.js b/packages/build/src/core/commands.js index ef6795a039..06a31785bf 100644 --- a/packages/build/src/core/commands.js +++ b/packages/build/src/core/commands.js @@ -4,9 +4,9 @@ const execa = require('execa') const pReduce = require('p-reduce') const { setEnvChanges } = require('../env/changes.js') -const { cancelBuild } = require('../error/cancel') const { addErrorInfo, getErrorInfo } = require('../error/info') const { reportBuildError } = require('../error/monitor/report') +const { serializeErrorStatus } = require('../error/parse/serialize_status') const { logCommand, logBuildCommandStart, logCommandSuccess, logPluginError } = require('../log/main') const { pipeOutput, unpipeOutput } = require('../log/stream') const { startTimer, endTimer } = require('../log/timer') @@ -58,7 +58,7 @@ const isSuccessCommand = function({ event }) { // list of `failedPlugins` (that ran `utils.build.failPlugin()`). // If an error arises, runs `onError` events. // Runs `onEnd` events at the end, whether an error was thrown or not. -const runCommands = async function({ commands, configPath, buildDir, nodePath, childEnv, errorMonitor, api }) { +const runCommands = async function({ commands, configPath, buildDir, nodePath, childEnv, errorMonitor }) { const { index: commandsCount, error: errorA, statuses: statusesB } = await pReduce( commands, async ( @@ -81,7 +81,6 @@ const runCommands = async function({ commands, configPath, buildDir, nodePath, c envChanges, statuses, errorMonitor, - api, error, failedPlugins, }, @@ -116,7 +115,6 @@ const runCommand = async function({ envChanges, statuses, errorMonitor, - api, error, failedPlugins, }) { @@ -147,7 +145,7 @@ const runCommand = async function({ const newValues = newError === undefined ? handleCommandSuccess({ event, package, newEnvChanges, newStatus, methodTimer }) - : await handleCommandError({ newError, errorMonitor, api }) + : await handleCommandError({ newError, errorMonitor }) return { ...newValues, newIndex: index + 1 } } @@ -257,20 +255,21 @@ const handleCommandSuccess = function({ event, package, newEnvChanges, newStatus // handlers of the same type have been triggered before propagating // - if `utils.build.failPlugin()` was used, print an error and skip next event // handlers of that plugin. But do not stop build. -const handleCommandError = async function({ newError, errorMonitor, api }) { +const handleCommandError = async function({ newError, errorMonitor }) { const { type, location: { package } = {} } = getErrorInfo(newError) + const newStatus = serializeErrorStatus(newError) if (type === 'failPlugin') { - logPluginError(newError) - await reportBuildError(newError, errorMonitor) - return { failedPlugin: [package] } + return handleFailPlugin({ newStatus, package, newError, errorMonitor }) } - if (type === 'cancelBuild') { - await cancelBuild(api) - } + return { newError, newStatus } +} - return { newError } +const handleFailPlugin = async function({ newStatus, package, newError, errorMonitor }) { + logPluginError(newError) + await reportBuildError(newError, errorMonitor) + return { failedPlugin: [package], newStatus } } module.exports = { getCommands, isSuccessCommand, runCommands } diff --git a/packages/build/src/core/main.js b/packages/build/src/core/main.js index 4ec70d897b..562e883a30 100644 --- a/packages/build/src/core/main.js +++ b/packages/build/src/core/main.js @@ -9,6 +9,7 @@ setColorLevel() require('../error/process') const { getChildEnv } = require('../env/main') +const { maybeCancelBuild } = require('../error/cancel') const { removeErrorColors } = require('../error/colors') const { reportBuildError } = require('../error/monitor/report') const { startErrorMonitor } = require('../error/monitor/start') @@ -79,7 +80,6 @@ const build = async function(flags) { branch, mode, errorMonitor, - api, }) if (dry) { @@ -97,6 +97,7 @@ const build = async function(flags) { await trackBuildComplete({ commandsCount, netlifyConfig, duration, siteInfo, mode }) return true } catch (error) { + await maybeCancelBuild(error, api) await logOldCliVersionError(mode) throw error } @@ -122,7 +123,6 @@ const buildRun = async function({ branch, mode, errorMonitor, - api, }) { const utilsData = await startUtils(buildDir) const childEnv = await getChildEnv({ netlifyConfig, buildDir, context, branch, siteInfo, mode }) @@ -142,7 +142,6 @@ const buildRun = async function({ dry, constants, errorMonitor, - api, }) } finally { await stopPlugins(childProcesses) @@ -162,7 +161,6 @@ const executeCommands = async function({ dry, constants, errorMonitor, - api, }) { const pluginsCommands = await loadPlugins({ pluginsOptions, @@ -187,7 +185,6 @@ const executeCommands = async function({ nodePath, childEnv, errorMonitor, - api, }) } diff --git a/packages/build/src/error/cancel.js b/packages/build/src/error/cancel.js index d5efd60333..8b2d4b9a99 100644 --- a/packages/build/src/error/cancel.js +++ b/packages/build/src/error/cancel.js @@ -2,13 +2,21 @@ const { env: { DEPLOY_ID }, } = require('process') +const { getErrorInfo } = require('./info') + // Cancel builds, for example when a plugin uses `utils.build.cancelBuild()` -const cancelBuild = async function(api) { +const maybeCancelBuild = async function(error, api) { if (api === undefined || !DEPLOY_ID) { return } + const { type } = getErrorInfo(error) + + if (type !== 'cancelBuild') { + return + } + await api.cancelSiteDeploy({ deploy_id: DEPLOY_ID }) } -module.exports = { cancelBuild } +module.exports = { maybeCancelBuild } diff --git a/packages/build/src/error/parse/parse.js b/packages/build/src/error/parse/parse.js index ff76365cdf..95d0815e37 100644 --- a/packages/build/src/error/parse/parse.js +++ b/packages/build/src/error/parse/parse.js @@ -7,13 +7,14 @@ const { getErrorProps } = require('./properties') const { getStackInfo } = require('./stack') // Parse all error information into a normalized sets of properties -const parseError = function(error) { +const parseError = function({ error, colors }) { const { message, stack, errorProps, errorInfo, errorInfo: { location = {}, plugin = {} }, + state, title, isSuccess, stackType, @@ -28,20 +29,21 @@ const parseError = function(error) { const pluginInfo = getPluginInfo(plugin, location) const locationInfo = getLocationInfo({ stack: stackA, location, locationType }) - const errorPropsA = getErrorProps(errorProps, showErrorProps) - return { title: titleA, message: messageA, pluginInfo, locationInfo, errorProps: errorPropsA, isSuccess } + const errorPropsA = getErrorProps({ errorProps, showErrorProps, colors }) + return { state, title: titleA, message: messageA, pluginInfo, locationInfo, errorProps: errorPropsA, isSuccess } } // Parse error instance into all the basic properties containing information const parseErrorInfo = function(error) { const { message, stack, ...errorProps } = normalizeError(error) const errorInfo = getErrorInfo(errorProps) - const { title, isSuccess, stackType, locationType, showErrorProps, rawStack } = getTypeInfo(errorInfo) + const { state, title, isSuccess, stackType, locationType, showErrorProps, rawStack } = getTypeInfo(errorInfo) return { message, stack, errorProps, errorInfo, + state, title, isSuccess, stackType, diff --git a/packages/build/src/error/parse/properties.js b/packages/build/src/error/parse/properties.js index 2f4bcb5b97..0e46d9384d 100644 --- a/packages/build/src/error/parse/properties.js +++ b/packages/build/src/error/parse/properties.js @@ -4,14 +4,14 @@ const { omit } = require('../../utils/omit') const { INFO_SYM } = require('../info') // In uncaught exceptions, print error static properties -const getErrorProps = function(errorProps, showErrorProps) { +const getErrorProps = function({ errorProps, showErrorProps, colors }) { const errorPropsA = omit(errorProps, CLEANED_ERROR_PROPS) if (!showErrorProps || Object.keys(errorPropsA).length === 0) { return } - return inspect(errorPropsA) + return inspect(errorPropsA, { colors }) } // Remove error static properties that should not be logged diff --git a/packages/build/src/error/parse/serialize_log.js b/packages/build/src/error/parse/serialize_log.js index bfc736948a..f8137256ac 100644 --- a/packages/build/src/error/parse/serialize_log.js +++ b/packages/build/src/error/parse/serialize_log.js @@ -4,7 +4,7 @@ const { parseError } = require('./parse') // Serialize an error object into a title|body string to print in logs const serializeLogError = function(error) { - const { title, message, pluginInfo, locationInfo, errorProps, isSuccess } = parseError(error) + const { title, message, pluginInfo, locationInfo, errorProps, isSuccess } = parseError({ error, colors: true }) const body = getBody({ message, pluginInfo, locationInfo, errorProps, isSuccess }) return { title, body, isSuccess } } diff --git a/packages/build/src/error/parse/serialize_status.js b/packages/build/src/error/parse/serialize_status.js new file mode 100644 index 0000000000..2b07c044bf --- /dev/null +++ b/packages/build/src/error/parse/serialize_status.js @@ -0,0 +1,28 @@ +const { parseError } = require('./parse') + +// Serialize an error object to `statuses` properties +const serializeErrorStatus = function(error) { + const { state, title, message, locationInfo, errorProps } = parseError({ error, colors: false }) + const text = getText({ locationInfo, errorProps }) + return { state, title, summary: message, text } +} + +const getText = function({ locationInfo, errorProps }) { + const parts = [locationInfo, getErrorProps(errorProps)].filter(Boolean) + + if (parts.length === 0) { + return + } + + return parts.join('\n\n') +} + +const getErrorProps = function(errorProps) { + if (errorProps === undefined) { + return + } + + return `Error properties:\n${errorProps}` +} + +module.exports = { serializeErrorStatus } diff --git a/packages/build/src/error/type.js b/packages/build/src/error/type.js index 1b9496668a..7a834715b2 100644 --- a/packages/build/src/error/type.js +++ b/packages/build/src/error/type.js @@ -1,7 +1,7 @@ // Retrieve error-type specific information const getTypeInfo = function({ type }) { const typeA = TYPES[type] === undefined ? DEFAULT_TYPE : type - return { type: typeA, ...TYPES[typeA] } + return { type: typeA, state: DEFAULT_STATE, ...TYPES[typeA] } } // List of error types, and their related properties @@ -20,6 +20,8 @@ const getTypeInfo = function({ type }) { // - `message`: printed as is, but taken from `error.message`. // Used when `error.stack` is not being correct due to the error being // passed between different processes. +// Related to error statuses: +// - `state`: error status state. Defaults to `failed_build` // Related to Bugsnag: // - `group`: main title shown in Bugsnag. Also used to group errors together // in Bugsnag, combined with `error.message`. @@ -67,6 +69,7 @@ const TYPES = { stackType: 'stack', locationType: 'buildFail', severity: 'info', + state: 'failed_plugin', }, // Plugin called `utils.build.cancelBuild()` @@ -76,6 +79,7 @@ const TYPES = { locationType: 'buildFail', isSuccess: true, severity: 'info', + state: 'canceled_plugin', }, // Plugin has an invalid shape @@ -129,7 +133,11 @@ const TYPES = { severity: 'error', }, } + // When no error type matches, it's an uncaught exception, i.e. a bug const DEFAULT_TYPE = 'exception' +// When no `state` is provided +const DEFAULT_STATE = 'failed_build' + module.exports = { getTypeInfo }