@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
355 lines • 15.5 kB
JavaScript
;
/*
* 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