UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

459 lines (414 loc) 17.4 kB
/* * 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. */ import { Configuration, configurationValue, } from "@atomist/automation-client/lib/configuration"; import { ConfigurationAware, HandlerContext, } from "@atomist/automation-client/lib/HandlerContext"; import { guid } from "@atomist/automation-client/lib/internal/util/string"; import { ProjectOperationCredentials } from "@atomist/automation-client/lib/operations/common/ProjectOperationCredentials"; import { RemoteRepoRef } from "@atomist/automation-client/lib/operations/common/RepoId"; import { logger } from "@atomist/automation-client/lib/util/logger"; import * as _ from "lodash"; import { AddressChannels, addressChannelsFor, } from "../../api/context/addressChannels"; import { ParameterPromptFactory } from "../../api/context/parameterPrompt"; import { NoPreferenceStore, PreferenceStore, PreferenceStoreFactory, } from "../../api/context/preferenceStore"; import { createSkillContext } from "../../api/context/skillContext"; import { StatefulPushListenerInvocation } from "../../api/dsl/goalContribution"; import { EnrichGoal } from "../../api/goal/enrichGoal"; import { Goal, GoalDefinition, GoalWithPrecondition, hasPreconditions, } from "../../api/goal/Goal"; import { Goals } from "../../api/goal/Goals"; import { getGoalDefinitionFrom, PlannedGoal, } from "../../api/goal/GoalWithFulfillment"; import { SdmGoalEvent } from "../../api/goal/SdmGoalEvent"; import { SdmGoalFulfillment, SdmGoalFulfillmentMethod, SdmGoalMessage, } from "../../api/goal/SdmGoalMessage"; import { GoalImplementationMapper, isGoalFulfillment, isGoalImplementation, isGoalSideEffect, } from "../../api/goal/support/GoalImplementationMapper"; import { GoalSetTag, TagGoalSet, } from "../../api/goal/tagGoalSet"; import { GoalsSetListener, GoalsSetListenerInvocation, } from "../../api/listener/GoalsSetListener"; import { PushListenerInvocation } from "../../api/listener/PushListener"; import { GoalSetter } from "../../api/mapping/GoalSetter"; import { ProjectLoader } from "../../spi/project/ProjectLoader"; import { RepoRefResolver } from "../../spi/repo-ref/RepoRefResolver"; import { PushFields, SdmGoalState, } from "../../typings/types"; import { minimalClone } from "./minimalClone"; import { constructGoalSet, constructSdmGoal, constructSdmGoalImplementation, storeGoal, storeGoalSet, } from "./storeGoals"; /** * Configuration for handling incoming pushes */ export interface ChooseAndSetGoalsRules { projectLoader: ProjectLoader; repoRefResolver: RepoRefResolver; goalsListeners: GoalsSetListener[]; goalSetter: GoalSetter; implementationMapping: GoalImplementationMapper; enrichGoal?: EnrichGoal; tagGoalSet?: TagGoalSet; preferencesFactory?: PreferenceStoreFactory; parameterPromptFactory?: ParameterPromptFactory<any>; } /** * Choose and set goals for this push * @param {ChooseAndSetGoalsRules} rules: configuration for handling incoming pushes * @param parameters details of incoming request * @return {Promise<Goals | undefined>} */ export async function chooseAndSetGoals(rules: ChooseAndSetGoalsRules, parameters: { context: HandlerContext, credentials: ProjectOperationCredentials, push: PushFields.Fragment, }): Promise<Goals | undefined> { const { projectLoader, goalsListeners, goalSetter, implementationMapping, repoRefResolver, preferencesFactory } = rules; const { context, credentials, push } = parameters; const enrichGoal = !!rules.enrichGoal ? rules.enrichGoal : async g => g; const tagGoalSet = !!rules.tagGoalSet ? rules.tagGoalSet : async () => []; const id = repoRefResolver.repoRefFromPush(push); const addressChannels = addressChannelsFor(push.repo, context); const preferences = !!preferencesFactory ? preferencesFactory(parameters.context) : NoPreferenceStore; const configuration = (context as any as ConfigurationAware).configuration; const goalSetId = guid(); const { determinedGoals, goalsToSave, tags } = await determineGoals( { projectLoader, repoRefResolver, goalSetter, implementationMapping, enrichGoal, tagGoalSet }, { credentials, id, context, push, addressChannels, preferences, goalSetId, configuration, }); if (goalsToSave.length > 0) { // First store the goals await Promise.all(goalsToSave.map(g => storeGoal(context, g))); // And then store the goalSet await storeGoalSet(context, constructGoalSet(context, goalSetId, determinedGoals.name, goalsToSave, tags, push)); } // Let GoalSetListeners know even if we determined no goals. // This is not an error const gsi: GoalsSetListenerInvocation = { id, context, credentials, addressChannels, configuration, preferences, goalSetId, goalSetName: determinedGoals ? determinedGoals.name : undefined, goalSet: determinedGoals, push, skill: createSkillContext(context), }; await Promise.all(goalsListeners.map(l => l(gsi))); return determinedGoals; } export async function determineGoals(rules: { projectLoader: ProjectLoader, repoRefResolver: RepoRefResolver, goalSetter: GoalSetter, implementationMapping: GoalImplementationMapper, enrichGoal: EnrichGoal, tagGoalSet?: TagGoalSet, }, circumstances: { credentials: ProjectOperationCredentials, id: RemoteRepoRef, context: HandlerContext, configuration: Configuration, push: PushFields.Fragment, addressChannels: AddressChannels, preferences?: PreferenceStore, goalSetId: string, }): Promise<{ determinedGoals: Goals | undefined, goalsToSave: SdmGoalMessage[], tags: GoalSetTag[], }> { const { enrichGoal, projectLoader, repoRefResolver, goalSetter, implementationMapping, tagGoalSet } = rules; const { credentials, id, context, push, addressChannels, goalSetId, preferences, configuration } = circumstances; return projectLoader.doWithProject({ credentials, id, context, readOnly: true, cloneOptions: minimalClone(push, { detachHead: true }), }, async project => { const pli: StatefulPushListenerInvocation = { project, credentials, id, push, context, addressChannels, configuration, preferences: preferences || NoPreferenceStore, facts: {}, skill: createSkillContext(context), }; const determinedGoals = await chooseGoalsForPushOnProject({ goalSetter }, pli); if (!determinedGoals) { return { determinedGoals: undefined, goalsToSave: [], tags: [] }; } const goalsToSave = await sdmGoalsFromGoals( implementationMapping, push, repoRefResolver, pli, determinedGoals, goalSetId); // Enrich all goals before they get saved await Promise.all(goalsToSave.map(async g1 => enrichGoal(g1, pli))); // Optain tags for the goal set let tags: GoalSetTag[] = []; if (!!tagGoalSet) { tags = (await tagGoalSet(goalsToSave, pli)) || []; } return { determinedGoals, goalsToSave, tags }; }); } async function sdmGoalsFromGoals(implementationMapping: GoalImplementationMapper, push: PushFields.Fragment, repoRefResolver: RepoRefResolver, pli: PushListenerInvocation, determinedGoals: Goals, goalSetId: string): Promise<SdmGoalMessage[]> { return Promise.all(determinedGoals.goals.map(async g => { const ge = constructSdmGoal(pli.context, { goalSet: determinedGoals.name, goalSetId, goal: g, state: (hasPreconditions(g) ? SdmGoalState.planned : (g.definition.preApprovalRequired ? SdmGoalState.waiting_for_pre_approval : SdmGoalState.requested)) as SdmGoalState, id: pli.id, providerId: repoRefResolver.providerIdFromPush(pli.push), fulfillment: await fulfillment({ implementationMapping }, g, pli), }); if (ge.state === SdmGoalState.requested) { const cbs = implementationMapping.findFulfillmentCallbackForGoal({ ...ge, push }) || []; let ng: SdmGoalEvent = { ...ge, push }; for (const cb of cbs) { ng = await cb.callback(ng, pli); } return { ...ge, data: ng.data, }; } else { return ge; } })); } async function fulfillment(rules: { implementationMapping: GoalImplementationMapper, }, g: Goal, inv: PushListenerInvocation): Promise<SdmGoalFulfillment> { const { implementationMapping } = rules; const plan = await implementationMapping.findFulfillmentByPush(g, inv); if (isGoalImplementation(plan)) { return constructSdmGoalImplementation(plan, inv.configuration.name); } else if (isGoalFulfillment(g.definition as any)) { const ff = (g.definition as any).fulfillment; return { method: SdmGoalFulfillmentMethod.SideEffect, name: ff.name, registration: ff.registration, }; } else if (isGoalSideEffect(plan)) { return { method: SdmGoalFulfillmentMethod.SideEffect, name: plan.sideEffectName, registration: plan.registration || configurationValue("name"), }; } else { return { method: SdmGoalFulfillmentMethod.Other, name: "unknown", registration: "unknown" }; } } async function chooseGoalsForPushOnProject(rules: { goalSetter: GoalSetter }, pi: PushListenerInvocation): Promise<Goals> { const { goalSetter } = rules; const { push, id } = pi; try { const determinedGoals: Goals = await goalSetter.mapping(pi); if (!determinedGoals) { logger.info("No goals set by push '%s' to '%s/%s/%s'", push.after.sha, id.owner, id.repo, push.branch); return determinedGoals; } else { const filteredGoals: Goal[] = []; const plannedGoals = await planGoals(determinedGoals, pi); plannedGoals.goals.forEach(g => { if ((g as any).dependsOn) { const preConditions = (g as any).dependsOn as Goal[]; if (preConditions) { const filteredPreConditions = preConditions.filter(pc => plannedGoals.goals.some(ag => ag.uniqueName === pc.uniqueName && ag.environment === pc.environment)); if (filteredPreConditions.length > 0) { filteredGoals.push(new GoalWithPrecondition(g.definition, ...filteredPreConditions)); } else { filteredGoals.push(new Goal(g.definition)); } } else { filteredGoals.push(g); } } else { filteredGoals.push(g); } }); logger.info("Goals for push '%s' on '%s/%s/%s' are '%s'", push.after.sha, id.owner, id.repo, push.branch, plannedGoals.name); return new Goals(plannedGoals.name, ...filteredGoals); } } catch (err) { logger.error("Error determining goals: %s", err); logger.error(err.stack); throw err; } } export async function planGoals(goals: Goals, pli: PushListenerInvocation): Promise<Goals> { const allGoals = [...goals.goals]; const names = []; for (const dg of goals.goals) { if (!!(dg as any).plan) { let planResult = await (dg as any).plan(pli, goals); if (!!planResult) { // Check if planResult is a PlannedGoal or PlannedGoals instance if (!_.some(planResult, v => !!v && !!v.goals)) { planResult = { "#": { goals: planResult } }; } const allNewGoals = []; const goalMapping = new Map<string, Goal[]>(); _.forEach(planResult, (planResultGoals, n) => { names.push(n.replace(/_/g, " ")); const plannedGoals: Array<PlannedGoal | PlannedGoal[]> = []; if (Array.isArray(planResultGoals.goals)) { plannedGoals.push(...planResultGoals.goals); } else { plannedGoals.push(planResultGoals.goals); } let previousGoals = []; const newGoals = []; plannedGoals.forEach(g => { if (Array.isArray(g)) { const gNewGoals = []; for (const gg of g) { const newGoal = createGoal( gg, dg, planResultGoals.dependsOn, allNewGoals.length + gNewGoals.length, previousGoals, goalMapping); gNewGoals.push(newGoal); } allNewGoals.push(...gNewGoals); newGoals.push(...gNewGoals); previousGoals = [...gNewGoals]; } else { const newGoal = createGoal( g, dg, planResultGoals.dependsOn, allNewGoals.length, previousGoals, goalMapping); allNewGoals.push(newGoal); newGoals.push(newGoal); previousGoals = [newGoal]; } }); goalMapping.set(n, newGoals); }); // Replace existing goal with new instances const ix = allGoals.findIndex(g => g.uniqueName === dg.uniqueName); allGoals.splice(ix, 1, ...allNewGoals); // Replace all preConditions that point back to the original goal with references to new goals allGoals.filter(hasPreconditions) .filter(g => (g.dependsOn || []).some(gr => gr.uniqueName === dg.uniqueName)) .forEach(g => { _.remove(g.dependsOn, gr => gr.uniqueName === dg.uniqueName); g.dependsOn.push(...allNewGoals); }); } } } return new Goals(goals.name, ...allGoals); } function createGoal(g: PlannedGoal, dg: Goal, preConditions: string | string[], plannedGoalsCounter: number, previousGoals: Goal[], goalMapping: Map<string, Goal[]>): Goal { const uniqueName = `${dg.uniqueName}#sdm:${plannedGoalsCounter}`; const definition: GoalDefinition & { parameters: PlannedGoal["parameters"], fulfillment: PlannedGoal["fulfillment"] } = _.merge( {}, dg.definition, getGoalDefinitionFrom(g.details, uniqueName)) as any; definition.uniqueName = uniqueName; definition.parameters = g.parameters; definition.fulfillment = g.fulfillment; const dependsOn = []; if (hasPreconditions(dg)) { dependsOn.push(...dg.dependsOn); } if (!!previousGoals) { dependsOn.push(...previousGoals); } if (!!preConditions) { if (Array.isArray(preConditions)) { dependsOn.push(..._.flatten(preConditions.map(d => goalMapping.get(d)).filter(d => !!d))); } else { dependsOn.push(...goalMapping.get(preConditions)); } } return new GoalWithPrecondition(definition, ..._.uniqBy(dependsOn.filter(d => !!d), "uniqueName")); }