UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

561 lines (558 loc) • 27.2 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.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