UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

355 lines 15.5 kB
"use strict"; /* * Copyright © 2020 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.prepareGoalInvocation = exports.prepareGoalExecutor = exports.markStatus = exports.executeHook = exports.executeGoal = void 0; const configuration_1 = require("@atomist/automation-client/lib/configuration"); const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult"); const logger_1 = require("@atomist/automation-client/lib/util/logger"); const _ = require("lodash"); const path = require("path"); const skillContext_1 = require("../../api/context/skillContext"); const ExecuteGoalResult_1 = require("../../api/goal/ExecuteGoalResult"); const GoalInvocation_1 = require("../../api/goal/GoalInvocation"); const commonPushTests_1 = require("../../api/mapping/support/commonPushTests"); const LazyProjectLoader_1 = require("../../spi/project/LazyProjectLoader"); const types_1 = require("../../typings/types"); const format_1 = require("../log/format"); const WriteToAllProgressLog_1 = require("../log/WriteToAllProgressLog"); const child_process_1 = require("../misc/child_process"); const toToken_1 = require("../misc/credentials/toToken"); const reportFailureInterpretation_1 = require("../misc/reportFailureInterpretation"); const result_1 = require("../misc/result"); const ProjectListenerInvokingProjectLoader_1 = require("../project/ProjectListenerInvokingProjectLoader"); const mock_1 = require("./mock"); const storeGoals_1 = require("./storeGoals"); class GoalExecutionError extends Error { constructor(params) { super("Failure in " + params.where); Object.setPrototypeOf(this, new.target.prototype); this.where = params.where; this.result = params.result; this.cause = params.cause; } get description() { const resultDescription = this.result ? ` Result code ${this.result.code} ${this.result.message}` : ""; const causeDescription = this.cause ? ` Caused by: ${this.cause.message}` : ""; return `Failure in ${this.where}:${resultDescription}${causeDescription}`; } } /** * Central function to execute a goal with progress logging */ async function executeGoal(rules, implementation, gi) { const { goal, goalEvent, addressChannels, progressLog, id, context, credentials, configuration, preferences } = gi; const { progressReporter, logInterpreter, projectListeners } = implementation; const implementationName = goalEvent.fulfillment.name; if (!!progressReporter) { gi.progressLog = new WriteToAllProgressLog_1.WriteToAllProgressLog(goalEvent.name, gi.progressLog, new ProgressReportingProgressLog(progressReporter, goalEvent, gi.context)); } const push = goalEvent.push; logger_1.logger.info(`Starting goal '%s' on '%s/%s/%s'`, goalEvent.uniqueName, push.repo.owner, push.repo.name, push.branch); async function notifyGoalExecutionListeners(sge, result, error) { const inProcessGoalExecutionListenerInvocation = { id, context, addressChannels, configuration, preferences, credentials, goal, goalEvent: sge, error, result, skill: skillContext_1.createSkillContext(context), }; await Promise.all(rules.goalExecutionListeners.map(gel => { try { return gel(inProcessGoalExecutionListenerInvocation); } catch (e) { logger_1.logger.warn(`GoalExecutionListener failed: ${e.message}`); logger_1.logger.debug(e); return undefined; } })); } const inProcessGoalEvent = await markGoalInProcess({ ctx: context, goalEvent, goal, progressLogUrl: progressLog.url, }); await notifyGoalExecutionListeners(inProcessGoalEvent); try { const goalInvocation = prepareGoalInvocation(gi, projectListeners); // execute pre hook const preHookResult = (await executeHook(rules, goalInvocation, inProcessGoalEvent, "pre").catch(async (err) => { throw new GoalExecutionError({ where: "executing pre-goal hook", cause: err }); })) || HandlerResult_1.Success; if (ExecuteGoalResult_1.isFailure(preHookResult)) { throw new GoalExecutionError({ where: "executing pre-goal hook", result: preHookResult }); } // execute the actual goal const goalResult = (await prepareGoalExecutor(implementation, inProcessGoalEvent, configuration)(goalInvocation).catch(async (err) => { throw new GoalExecutionError({ where: "executing goal", cause: err }); })) || HandlerResult_1.Success; if (ExecuteGoalResult_1.isFailure(goalResult)) { throw new GoalExecutionError({ where: "executing goal", result: goalResult }); } // execute post hook const postHookResult = (await executeHook(rules, goalInvocation, inProcessGoalEvent, "post").catch(async (err) => { throw new GoalExecutionError({ where: "executing post-goal hook", cause: err }); })) || HandlerResult_1.Success; if (ExecuteGoalResult_1.isFailure(postHookResult)) { throw new GoalExecutionError({ where: "executing post-goal hooks", result: postHookResult }); } const result = Object.assign(Object.assign(Object.assign({}, preHookResult), goalResult), postHookResult); await notifyGoalExecutionListeners(Object.assign(Object.assign({}, inProcessGoalEvent), { state: types_1.SdmGoalState.success }), result); logger_1.logger.info("Goal '%s' completed with: %j", goalEvent.uniqueName, result); await markStatus({ context, goalEvent, goal, result, progressLogUrl: progressLog.url }); return Object.assign(Object.assign({}, result), { code: 0 }); } catch (err) { logger_1.logger.warn("Error executing goal '%s': %s", goalEvent.uniqueName, err.message); const result = handleGitRefErrors(Object.assign({ code: 1 }, (err.result || {})), err); await notifyGoalExecutionListeners(Object.assign(Object.assign({}, inProcessGoalEvent), { state: result.state || types_1.SdmGoalState.failure }), result, err); await reportGoalError({ goal, implementationName, addressChannels, progressLog, id, logInterpreter, }, err); await markStatus({ context, goalEvent, goal, result, error: err, progressLogUrl: progressLog.url, }); return HandlerResult_1.failure(err); } } exports.executeGoal = executeGoal; async function executeHook(rules, goalInvocation, sdmGoal, stage) { const hook = goalToHookFile(sdmGoal, stage); // Check configuration to see if hooks should be skipped if (!configuration_1.configurationValue("sdm.goal.hooks", false)) { return HandlerResult_1.Success; } const { projectLoader } = rules; const { credentials, id, context, progressLog } = goalInvocation; return projectLoader.doWithProject({ credentials, id, context, readOnly: true, cloneOptions: { detachHead: true }, }, async (p) => { if (await p.hasFile(path.join(".atomist", "hooks", hook))) { progressLog.write("/--"); progressLog.write(`Invoking goal hook: ${hook}`); const opts = { cwd: path.join(p.baseDir, ".atomist", "hooks"), env: Object.assign(Object.assign({}, process.env), { GITHUB_TOKEN: toToken_1.toToken(credentials), ATOMIST_WORKSPACE: context.workspaceId, ATOMIST_CORRELATION_ID: context.correlationId, ATOMIST_REPO: sdmGoal.push.repo.name, ATOMIST_OWNER: sdmGoal.push.repo.owner }), log: progressLog, }; const cmd = path.join(p.baseDir, ".atomist", "hooks", hook); let result = await child_process_1.spawnLog(cmd, [], opts); if (!result) { result = HandlerResult_1.Success; } progressLog.write(`Result: ${result_1.serializeResult(result)}`); progressLog.write("\\--"); await progressLog.flush(); return result; } else { return HandlerResult_1.Success; } }); } exports.executeHook = executeHook; function goalToHookFile(sdmGoal, prefix) { return `${prefix}-${sdmGoal.environment.toLocaleLowerCase().slice(2)}-${sdmGoal.name .toLocaleLowerCase() .replace(" ", "_")}`; } function markStatus(parameters) { const { context, goalEvent, goal, result, error, progressLogUrl } = parameters; let newState = types_1.SdmGoalState.success; if (result.state) { newState = result.state; } else if (result.code !== 0) { newState = types_1.SdmGoalState.failure; } else if (goal.definition.approvalRequired) { newState = types_1.SdmGoalState.waiting_for_approval; } return storeGoals_1.updateGoal(context, goalEvent, { url: progressLogUrl, externalUrls: result.externalUrls || [], state: newState, phase: result.phase ? result.phase : goalEvent.phase, description: result.description ? result.description : storeGoals_1.descriptionFromState(goal, newState, goalEvent), error, data: result.data ? result.data : goalEvent.data, }); } exports.markStatus = markStatus; function handleGitRefErrors(result, error) { var _a, _b; if (!!((_a = error === null || error === void 0 ? void 0 : error.cause) === null || _a === void 0 ? void 0 : _a.stderr)) { const err = (_b = error === null || error === void 0 ? void 0 : error.cause) === null || _b === void 0 ? void 0 : _b.stderr; if (/Remote branch .* not found/.test(err)) { result.code = 0; result.state = types_1.SdmGoalState.canceled; result.phase = "branch not found"; } else if (/reference is not a tree/.test(err)) { result.code = 0; result.state = types_1.SdmGoalState.canceled; result.phase = "sha not found"; } } return result; } async function markGoalInProcess(parameters) { const { ctx, goalEvent, goal, progressLogUrl } = parameters; goalEvent.state = types_1.SdmGoalState.in_process; goalEvent.description = storeGoals_1.descriptionFromState(goal, types_1.SdmGoalState.in_process, goalEvent); goalEvent.url = progressLogUrl; await storeGoals_1.updateGoal(ctx, goalEvent, { url: progressLogUrl, description: storeGoals_1.descriptionFromState(goal, types_1.SdmGoalState.in_process, goalEvent), state: types_1.SdmGoalState.in_process, }); return goalEvent; } /** * Report an error executing a goal and present a retry button * @return {Promise<void>} */ async function reportGoalError(parameters, err) { const { implementationName, addressChannels, progressLog, id, logInterpreter } = parameters; if (err.cause) { logger_1.logger.warn(err.cause.stack); progressLog.write(err.cause.stack); } else if (err.result && err.result.error) { logger_1.logger.warn(err.result.error.stack); progressLog.write(err.result.error.stack); } else { logger_1.logger.warn(err.stack); } progressLog.write("Error: " + (err.description || err.message) + "\n"); const interpretation = logInterpreter(progressLog.log); // The executor might have information about the failure; report it in the channels if (interpretation) { if (!interpretation.doNotReportToUser) { await reportFailureInterpretation_1.reportFailureInterpretation(implementationName, interpretation, { url: progressLog.url, log: progressLog.log }, id, addressChannels); } } } function prepareGoalExecutor(gi, sdmGoal, configuration) { const mge = mock_1.mockGoalExecutor(gi.goal, sdmGoal, configuration); if (mge) { return mge; } else { return gi.goalExecutor; } } exports.prepareGoalExecutor = prepareGoalExecutor; function prepareGoalInvocation(gi, listeners) { let hs = listeners ? Array.isArray(listeners) ? listeners : [listeners] : []; if (LazyProjectLoader_1.isLazyProjectLoader(gi.configuration.sdm.projectLoader)) { // Register the materializing listener for LazyProject instances of those need to // get materialized before using in goal implementations const projectMaterializer = { name: "clone project", pushTest: commonPushTests_1.AnyPush, events: [GoalInvocation_1.GoalProjectListenerEvent.before], listener: async (p) => { if (!p.materialized()) { // Trigger project materialization await p.materialize(); } return { code: 0 }; }, }; hs = [projectMaterializer, ...hs]; } if (hs.length === 0) { return gi; } const configuration = _.cloneDeep(gi.configuration); configuration.sdm.projectLoader = new ProjectListenerInvokingProjectLoader_1.ProjectListenerInvokingProjectLoader(gi, hs); const newGi = Object.assign(Object.assign({}, gi), { configuration }); return newGi; } exports.prepareGoalInvocation = prepareGoalInvocation; /** * ProgressLog implementation that uses the configured ReportProgress * instance to report goal execution updates. */ class ProgressReportingProgressLog { constructor(progressReporter, sdmGoal, context) { this.progressReporter = progressReporter; this.sdmGoal = sdmGoal; this.context = context; this.name = sdmGoal.name; } async close() { return; } async flush() { return; } async isAvailable() { return true; } write(msg, ...args) { const progress = this.progressReporter(format_1.format(msg, ...args), this.sdmGoal); if (progress && progress.phase) { if (this.sdmGoal.phase !== progress.phase) { this.sdmGoal.phase = progress.phase; storeGoals_1.updateGoal(this.context, this.sdmGoal, { state: this.sdmGoal.state, phase: progress.phase, description: this.sdmGoal.description, url: this.sdmGoal.url, }) .then(() => { // Intentionally empty }) .catch(err => { logger_1.logger.debug(`Error occurred reporting progress: %s`, err.message); }); } } } } //# sourceMappingURL=executeGoal.js.map