"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