@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
561 lines (558 loc) • 27.2 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.resolveCredentialsPromise = exports.gitBranchCompatible = exports.toRepoTargetingParametersMaker = exports.toScalarProjectEditor = exports.toParametersListing = exports.JobRequiredParameter = exports.JobDescriptionParameter = exports.JobNameParameter = exports.MsgIdParameter = exports.DryRunParameter = exports.toCommandListenerInvocation = exports.CommandListenerExecutionInterruptError = exports.eventHandlerRegistrationToEvent = exports.commandHandlerRegistrationToCommand = exports.generatorRegistrationToCommand = exports.codeInspectionRegistrationToCommand = exports.codeTransformRegistrationToCommand = exports.TransformTag = exports.InspectionTag = exports.GeneratorTag = void 0;
const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult");
const decoratorSupport_1 = require("@atomist/automation-client/lib/internal/metadata/decoratorSupport");
const onEvent_1 = require("@atomist/automation-client/lib/onEvent");
const GitHubRepoRef_1 = require("@atomist/automation-client/lib/operations/common/GitHubRepoRef");
const repoFilter_1 = require("@atomist/automation-client/lib/operations/common/repoFilter");
const repoUtils_1 = require("@atomist/automation-client/lib/operations/common/repoUtils");
const editAll_1 = require("@atomist/automation-client/lib/operations/edit/editAll");
const editModes_1 = require("@atomist/automation-client/lib/operations/edit/editModes");
const projectEditor_1 = require("@atomist/automation-client/lib/operations/edit/projectEditor");
const projectEditorOps_1 = require("@atomist/automation-client/lib/operations/edit/projectEditorOps");
const GitHubRepoCreationParameters_1 = require("@atomist/automation-client/lib/operations/generate/GitHubRepoCreationParameters");
const Project_1 = require("@atomist/automation-client/lib/project/Project");
const SmartParameters_1 = require("@atomist/automation-client/lib/SmartParameters");
const constructionUtils_1 = require("@atomist/automation-client/lib/util/constructionUtils");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const slack_messages_1 = require("@atomist/slack-messages");
const _ = require("lodash");
const GitHubRepoTargets_1 = require("../../api/command/target/GitHubRepoTargets");
const TransformModeSuggestion_1 = require("../../api/command/target/TransformModeSuggestion");
const parameterPrompt_1 = require("../../api/context/parameterPrompt");
const preferenceStore_1 = require("../../api/context/preferenceStore");
const skillContext_1 = require("../../api/context/skillContext");
const RepoTargets_1 = require("../../api/machine/RepoTargets");
const ParametersBuilder_1 = require("../../api/registration/ParametersBuilder");
const ParametersDefinition_1 = require("../../api/registration/ParametersDefinition");
const createCommand_1 = require("../command/createCommand");
const generatorCommand_1 = require("../command/generator/generatorCommand");
const chattyDryRunAwareEditor_1 = require("../command/transform/chattyDryRunAwareEditor");
const LoggingProgressLog_1 = require("../log/LoggingProgressLog");
const dateFormat_1 = require("../misc/dateFormat");
const createJob_1 = require("../misc/job/createJob");
const messages_1 = require("../misc/slack/messages");
const projectLoaderRepoLoader_1 = require("./projectLoaderRepoLoader");
const RepoTargetingParameters_1 = require("./RepoTargetingParameters");
const toMachineOptions_1 = require("./toMachineOptions");
exports.GeneratorTag = "generator";
exports.InspectionTag = "inspection";
exports.TransformTag = "transform";
function codeTransformRegistrationToCommand(sdm, ctr) {
tagWith(ctr, exports.TransformTag);
const mo = toMachineOptions_1.toMachineOptions(sdm);
addDryRunParameters(ctr);
addJobParameters(ctr);
addParametersDefinedInBuilder(ctr);
ctr.paramsMaker = toRepoTargetingParametersMaker(ctr.paramsMaker || SmartParameters_1.NoParameters, ctr.targets || mo.targets || GitHubRepoTargets_1.GitHubRepoTargets);
const description = ctr.description || ctr.name;
const asCommand = Object.assign(Object.assign({ description }, ctr), { listener: async (ci) => {
ci.credentials = await resolveCredentialsPromise(ci.credentials);
const targets = ci.parameters.targets;
const vr = targets.bindAndValidate();
if (RepoTargets_1.isValidationError(vr)) {
return ci.addressChannels(messages_1.slackErrorMessage(`Code Transform`, `Invalid parameters to code transform ${slack_messages_1.italic(ci.commandName)}:
${vr.message}`, ci.context));
}
const repoFinder = !!ci.parameters.targets.repoRef
? () => Promise.resolve([ci.parameters.targets.repoRef])
: ctr.repoFinder || toMachineOptions_1.toMachineOptions(sdm).repoFinder;
const repoLoader = !!ctr.repoLoader
? ctr.repoLoader(ci.parameters)
: projectLoaderRepoLoader_1.projectLoaderRepoLoader(mo.projectLoader, ci.credentials, false, ci.context);
const concurrency = Object.assign({ maxConcurrent: 2, requiresJob: false }, (ctr.concurrency || {}));
try {
const ids = await repoUtils_1.relevantRepos(ci.context, repoFinder, repoFilter_1.andFilter(targets.test, ctr.repoFilter));
const requiresJob = _.get(ci.parameters, "job.required", concurrency.requiresJob);
if (ids.length > 1 || !!requiresJob) {
const params = Object.assign({}, ci.parameters);
params.targets.repos = undefined;
params.targets.repo = undefined;
delete params["job.name"];
delete params["job.description"];
delete params["job.required"];
await createJob_1.createJob({
name: _.get(ci.parameters, "job.name") || `CodeTransform/${ci.commandName}`,
command: ci.commandName,
parameters: ids.map(id => (Object.assign(Object.assign({}, params), { "targets": {
owner: id.owner,
repo: id.repo,
branch: id.branch,
sha: id.sha,
}, "job.required": false }))),
description: _.get(ci.parameters, "job.description") ||
`Running code transform ${slack_messages_1.italic(ci.commandName)} on ${ids.length} ${ids.length === 1 ? "repository" : "repositories"}`,
concurrentTasks: concurrency.maxConcurrent,
}, ci.context);
}
else {
const editMode = toEditModeOrFactory(ctr, ci);
const result = await editAll_1.editOne(ci.context, ci.credentials, chattyDryRunAwareEditor_1.chattyDryRunAwareEditor(ctr, toScalarProjectEditor(ctr.transform, toMachineOptions_1.toMachineOptions(sdm), ctr.projectTest)), editMode, ids[0], ci.parameters, repoLoader);
if (!!ctr.onTransformResults) {
await ctr.onTransformResults([result], Object.assign(Object.assign({}, ci), { progressLog: new LoggingProgressLog_1.LoggingProgressLog(ctr.name, "debug") }));
}
else if (!!result && !!result.error) {
const error = result.error;
return ci.addressChannels(messages_1.slackErrorMessage(`Code Transform`, `Code transform ${slack_messages_1.italic(ci.commandName)} failed${!!error.message ? ":\n\n" + slack_messages_1.codeBlock(error.message) : "."}`, ci.context));
}
else {
logger_1.logger.debug("No react function to react to result of code transformation '%s'", ctr.name);
}
}
}
catch (e) {
return ci.addressChannels(messages_1.slackErrorMessage(`Code Transform`, `Code transform ${slack_messages_1.italic(ci.commandName)} failed${!!e.message ? ":\n\n" + slack_messages_1.codeBlock(e.message) : "."}`, ci.context));
}
} });
return commandHandlerRegistrationToCommand(sdm, asCommand);
}
exports.codeTransformRegistrationToCommand = codeTransformRegistrationToCommand;
function codeInspectionRegistrationToCommand(sdm, cir) {
tagWith(cir, exports.InspectionTag);
const mo = toMachineOptions_1.toMachineOptions(sdm);
addJobParameters(cir);
addParametersDefinedInBuilder(cir);
cir.paramsMaker = toRepoTargetingParametersMaker(cir.paramsMaker || SmartParameters_1.NoParameters, cir.targets || mo.targets || GitHubRepoTargets_1.GitHubRepoTargets);
const description = cir.description || cir.name;
const asCommand = Object.assign(Object.assign({ description }, cir), { listener: async (ci) => {
ci.credentials = await resolveCredentialsPromise(ci.credentials);
const targets = ci.parameters.targets;
const vr = targets.bindAndValidate();
if (RepoTargets_1.isValidationError(vr)) {
return ci.addressChannels(messages_1.slackErrorMessage(`Code Inspection`, `Invalid parameters to code inspection ${slack_messages_1.italic(ci.commandName)}:
${slack_messages_1.codeBlock(vr.message)}`, ci.context));
}
const action = async (p) => {
if (!!cir.projectTest && !(await cir.projectTest(p))) {
return { repoId: p.id, result: undefined };
}
return {
repoId: p.id,
result: await cir.inspection(p, Object.assign(Object.assign({}, ci), { progressLog: new LoggingProgressLog_1.LoggingProgressLog(cir.name, "debug") })),
};
};
const repoFinder = !!ci.parameters.targets.repoRef
? () => Promise.resolve([ci.parameters.targets.repoRef])
: cir.repoFinder || toMachineOptions_1.toMachineOptions(sdm).repoFinder;
const repoLoader = !!cir.repoLoader
? cir.repoLoader(ci.parameters)
: projectLoaderRepoLoader_1.projectLoaderRepoLoader(mo.projectLoader, ci.parameters.targets.credentials, true, ci.context);
const concurrency = Object.assign({ maxConcurrent: 2, requiresJob: false }, (cir.concurrency || {}));
try {
const ids = await repoUtils_1.relevantRepos(ci.context, repoFinder, repoFilter_1.andFilter(targets.test, cir.repoFilter));
const requiresJob = _.get(ci.parameters, "job.required", concurrency.requiresJob);
if (ids.length > 1 || !!requiresJob) {
const params = Object.assign({}, ci.parameters);
params.targets.repos = undefined;
params.targets.repo = undefined;
delete params["job.name"];
delete params["job.description"];
delete params["job.required"];
await createJob_1.createJob({
name: `CodeInspection/${ci.commandName}`,
command: ci.commandName,
parameters: ids.map(id => (Object.assign(Object.assign({}, params), { "targets": {
owner: id.owner,
repo: id.repo,
branch: id.branch,
sha: id.sha,
}, "job.required": false }))),
description: `Running code inspection ${slack_messages_1.italic(ci.commandName)} on ${ids.length} repositories`,
}, ci.context);
}
else {
const project = await repoLoader(ids[0]);
const result = await action(project, ci.parameters);
if (!!cir.onInspectionResults) {
await cir.onInspectionResults([result], ci);
}
else {
logger_1.logger.debug("No react function to react to results of code inspection '%s'", cir.name);
}
}
}
catch (e) {
return ci.addressChannels(messages_1.slackErrorMessage(`Code Inspection`, `Code Inspection ${slack_messages_1.italic(ci.commandName)} failed:
${slack_messages_1.codeBlock(e.message)}`, ci.context));
}
} });
return commandHandlerRegistrationToCommand(sdm, asCommand);
}
exports.codeInspectionRegistrationToCommand = codeInspectionRegistrationToCommand;
/**
* Tag the command details with the given tag if it isn't already
* @param {Partial<CommandDetails>} e
* @param {string} tag
*/
function tagWith(e, tag) {
if (!e.tags) {
e.tags = [];
}
if (typeof e.tags === "string") {
e.tags = [e.tags];
}
if (!e.tags.includes(tag)) {
e.tags.push(tag);
}
}
function generatorRegistrationToCommand(sdm, e) {
tagWith(e, exports.GeneratorTag);
if (!e.paramsMaker) {
e.paramsMaker = SmartParameters_1.NoParameters;
}
if (e.startingPoint && Project_1.isProject(e.startingPoint) && !e.startingPoint.id) {
// TODO should probably be handled in automation-client
e.startingPoint.id = new GitHubRepoRef_1.GitHubRepoRef("ignore", "this");
}
addParametersDefinedInBuilder(e);
return () => generatorCommand_1.generatorCommand(sdm, () => toScalarProjectEditor(e.transform, toMachineOptions_1.toMachineOptions(sdm)), e.name, e.paramsMaker, e.fallbackTarget || GitHubRepoCreationParameters_1.GitHubRepoCreationParameters, e.startingPoint, e, // required because we redefine the afterAction
e);
}
exports.generatorRegistrationToCommand = generatorRegistrationToCommand;
function commandHandlerRegistrationToCommand(sdm, c) {
return () => createCommand_1.createCommand(sdm, toOnCommand(c), c.name, c.paramsMaker, c);
}
exports.commandHandlerRegistrationToCommand = commandHandlerRegistrationToCommand;
function eventHandlerRegistrationToEvent(sdm, e) {
addParametersDefinedInBuilder(e);
return () => onEvent_1.eventHandlerFrom(e.listener, e.paramsMaker || SmartParameters_1.NoParameters, e.subscription, e.name, e.description, e.tags);
}
exports.eventHandlerRegistrationToEvent = eventHandlerRegistrationToEvent;
class CommandListenerExecutionInterruptError extends Error {
constructor(message) {
super(message);
this.message = message;
}
}
exports.CommandListenerExecutionInterruptError = CommandListenerExecutionInterruptError;
function toOnCommand(c) {
addParametersDefinedInBuilder(c);
return sdm => async (context, parameters) => {
const cli = toCommandListenerInvocation(c, context, parameters, toMachineOptions_1.toMachineOptions(sdm));
cli.credentials = await resolveCredentialsPromise(cli.credentials);
try {
const result = await c.listener(cli);
return !!result ? result : HandlerResult_1.Success;
}
catch (err) {
if (err instanceof CommandListenerExecutionInterruptError) {
return {
code: -1,
};
}
else {
logger_1.logger.error("Error executing command '%s': %s", cli.commandName, err.message);
logger_1.logger.error(err.stack);
return {
code: 1,
message: err.message,
};
}
}
};
}
function toCommandListenerInvocation(c, context, parameters, sdm) {
// It may already be there
let credentials = !!context ? context.credentials : undefined;
let ids;
if (generatorCommand_1.isSeedDrivenGeneratorParameters(parameters)) {
credentials = parameters.target.credentials;
ids = [parameters.target.repoRef];
}
else if (RepoTargetingParameters_1.isRepoTargetingParameters(parameters)) {
credentials = parameters.targets.credentials;
ids = !!parameters.targets.repoRef ? [parameters.targets.repoRef] : undefined;
}
if (!!sdm.credentialsResolver) {
try {
credentials = sdm.credentialsResolver.commandHandlerCredentials(context, ids ? ids[0] : undefined);
}
catch (e) {
logger_1.logger.debug(`Failed to obtain credentials from credentialsResolver: ${e.message}`);
}
}
let matches;
if (c.intent instanceof RegExp) {
matches = c.intent.exec(context.trigger.raw_message);
}
const addressChannels = (msg, opts) => context.messageClient.respond(msg, opts);
const promptFor = sdm.parameterPromptFactory ? sdm.parameterPromptFactory(context) : parameterPrompt_1.NoParameterPrompt;
const preferences = sdm.preferenceStoreFactory ? sdm.preferenceStoreFactory(context) : preferenceStore_1.NoPreferenceStore;
const configuration = (context || {}).configuration;
return {
commandName: c.name,
context,
parameters,
addressChannels,
configuration,
promptFor,
preferences,
credentials,
ids,
matches,
skill: skillContext_1.createSkillContext(context),
};
}
exports.toCommandListenerInvocation = toCommandListenerInvocation;
exports.DryRunParameter = {
name: "dry-run",
description: "Run Code Transform in dry run mode so that changes aren't committed to the repository",
required: false,
defaultValue: false,
type: "boolean",
};
exports.MsgIdParameter = {
name: "msgId",
description: "Id of the code transform message",
required: false,
type: "string",
displayable: false,
};
/**
* Add the dryRun parameter into the list of parameters
*/
function addDryRunParameters(c) {
const params = toParametersListing(c.parameters || {});
params.parameters.push(exports.DryRunParameter, exports.MsgIdParameter);
c.parameters = params;
}
exports.JobNameParameter = {
name: "job.name",
description: "Name of the job to create",
required: false,
type: "string",
displayable: false,
};
exports.JobDescriptionParameter = {
name: "job.description",
description: "Description of the job to create",
required: false,
type: "string",
displayable: false,
};
exports.JobRequiredParameter = {
name: "job.required",
description: "Is job required",
required: false,
type: "boolean",
};
/**
* Add the job parameter into the list of parameters
*/
function addJobParameters(c) {
const params = toParametersListing(c.parameters || {});
params.parameters.push(exports.JobNameParameter, exports.JobDescriptionParameter, exports.JobRequiredParameter);
c.parameters = params;
}
/**
* Add to the existing ParametersMaker any parameters defined in the builder itself
* @param {CommandHandlerRegistration<PARAMS>} c
*/
function addParametersDefinedInBuilder(c) {
const oldMaker = c.paramsMaker || SmartParameters_1.NoParameters;
if (!!c.parameters) {
c.paramsMaker = () => {
const paramsInstance = constructionUtils_1.toFactory(oldMaker)();
const paramListing = toParametersListing(c.parameters);
paramListing.parameters.forEach(p => {
paramsInstance[p.name] = p.defaultValue;
decoratorSupport_1.declareParameter(paramsInstance, p.name, p);
});
paramListing.mappedParameters.forEach(mp => decoratorSupport_1.declareMappedParameter(paramsInstance, mp.name, mp.uri, mp.required));
paramListing.secrets.forEach(s => decoratorSupport_1.declareSecret(paramsInstance, s.name, s.uri));
paramListing.values.forEach(v => decoratorSupport_1.declareValue(paramsInstance, v.name, { path: v.path, required: v.required, type: v.type }));
return paramsInstance;
};
}
}
function isMappedParameterOrSecretDeclaration(x) {
const maybe = x;
return !!maybe && !!maybe.declarationType;
}
function isValueDeclaration(x) {
const maybe = x;
return !!maybe && maybe.path !== undefined && maybe.path !== null;
}
function isParametersListing(p) {
const maybe = p;
return maybe.parameters !== undefined && maybe.mappedParameters !== undefined;
}
function toParametersListing(p) {
if (isParametersListing(p)) {
return p;
}
const builder = new ParametersBuilder_1.ParametersBuilder();
for (const name of Object.getOwnPropertyNames(p)) {
const value = p[name];
if (isMappedParameterOrSecretDeclaration(value)) {
switch (value.declarationType) {
case ParametersDefinition_1.DeclarationType.Mapped:
builder.addMappedParameters({ name, uri: value.uri, required: value.required });
break;
case ParametersDefinition_1.DeclarationType.Secret:
builder.addSecrets({ name, uri: value.uri });
break;
}
}
else if (isValueDeclaration(value)) {
builder.addValues({ name, path: value.path, required: value.required, type: value.type });
}
else {
builder.addParameters(Object.assign({ name }, value));
}
}
return builder;
}
exports.toParametersListing = toParametersListing;
/**
* Convert to legacy automation-client "editor" signature
* @param {CodeTransformOrTransforms<PARAMS>} ctot
* @param {ProjectPredicate} projectPredicate
* @return {ProjectEditor<PARAMS>}
*/
function toScalarProjectEditor(ctot, sdm, projectPredicate) {
const unguarded = Array.isArray(ctot)
? projectEditorOps_1.chainEditors(...ctot.map(c => toProjectEditor(c, sdm)))
: toProjectEditor(ctot, sdm);
if (!!projectPredicate) {
// Filter out this project if it doesn't match the predicate
return async (p, context, params) => {
return (await projectPredicate(p))
? unguarded(p, context, params)
: Promise.resolve({ success: true, edited: false, target: p });
};
}
return unguarded;
}
exports.toScalarProjectEditor = toScalarProjectEditor;
// Convert to an old style, automation-client, ProjectEditor to allow
// underlying code to work for now
function toProjectEditor(ct, sdm) {
return async (p, ctx, params) => {
const ci = toCommandListenerInvocation(p, ctx, params, toMachineOptions_1.toMachineOptions(sdm));
ci.credentials = await resolveCredentialsPromise(ci.credentials);
// Mix in handler context for old style callers
const n = await ct(p, Object.assign(Object.assign({}, ctx), ci), params);
if (n === undefined) {
// The transform returned void
return { target: p, edited: await isDirty(p), success: true };
}
const r = n;
try {
return Project_1.isProject(r) ? projectEditor_1.successfulEdit(r, await isDirty(r)) : r;
}
catch (e) {
return projectEditor_1.failedEdit(p, e);
}
};
}
async function isDirty(p) {
if (isGitProject(p)) {
try {
const status = await p.gitStatus();
return !status.isClean;
}
catch (_a) {
// Ignore
}
}
return undefined;
}
function isGitProject(p) {
const maybe = p;
return !!maybe.gitStatus;
}
/**
* Return a parameters maker that is targeting aware
* @param {Maker<PARAMS>} paramsMaker
* @param targets targets parameters to set if necessary
* @return {Maker<EditorOrReviewerParameters & PARAMS>}
*/
function toRepoTargetingParametersMaker(paramsMaker, targets) {
const sampleParams = constructionUtils_1.toFactory(paramsMaker)();
return RepoTargetingParameters_1.isRepoTargetingParameters(sampleParams)
? paramsMaker
: () => {
const rawParms = constructionUtils_1.toFactory(paramsMaker)();
const allParms = rawParms;
const targetsInstance = constructionUtils_1.toFactory(targets)();
allParms.targets = targetsInstance;
return allParms;
};
}
exports.toRepoTargetingParametersMaker = toRepoTargetingParametersMaker;
function toEditModeOrFactory(ctr, ci) {
const description = ctr.description || ctr.name;
if (!!ctr.transformPresentation) {
return (p) => ctr.transformPresentation(Object.assign(Object.assign({}, ci), { progressLog: new LoggingProgressLog_1.LoggingProgressLog(ctr.name, "debug") }), p);
}
// Get EditMode from parameters if possible
if (TransformModeSuggestion_1.isTransformModeSuggestion(ci.parameters)) {
const tms = ci.parameters;
return new editModes_1.PullRequest(tms.desiredBranchName, tms.desiredPullRequestTitle || description);
}
// Default it if not supplied
return new editModes_1.PullRequest(`transform-${gitBranchCompatible(ctr.name)}-${dateFormat_1.formatDate()}`, description);
}
/**
* Takes a potential git branch name and returns a legalised iteration of it
*
* @param name the git branch name to sanitise.
*/
function gitBranchCompatible(name) {
// handles spaces and .. ~ : ^ ? * [ @{
let branchName = name.replace(/\s+|(\.\.)+|~+|:+|\^+|\?+|\*+|\[+|(\@\{)+/g, "_");
// handles double slashes
branchName = branchName.replace(/(\/\/)+|(\\)+/g, "/");
// handles back slashes
branchName = branchName.replace(/\\+/g, "/");
if (branchName.startsWith(".") || branchName.startsWith("/")) {
branchName = branchName.substring(1);
}
if (branchName.endsWith(".") || branchName.endsWith("/")) {
branchName = branchName.slice(0, -1);
}
const lock = ".lock";
if (branchName.endsWith(lock)) {
branchName = branchName.slice(0, -lock.length);
}
if (branchName === "@") {
branchName = "at";
}
// handles ascii control characters
branchName = branchName.replace(/[\x00-\x1F\x7F]+/g, "");
return branchName;
}
exports.gitBranchCompatible = gitBranchCompatible;
async function resolveCredentialsPromise(creds) {
if (creds instanceof Promise) {
try {
return await creds;
}
catch (e) {
logger_1.logger.debug(e.message);
}
}
else if (!!creds) {
return creds;
}
return undefined;
}
exports.resolveCredentialsPromise = resolveCredentialsPromise;
//# sourceMappingURL=handlerRegistrations.js.map