/* * Copyright © 2019 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, ConfigurationPostProcessor, logger, } from "@atomist/automation-client"; import { allSatisfied, AnyPush, Goal, GoalContribution, goals, Goals, GoalWithFulfillment, notGoalTest, PushListenerInvocation, PushTest, SdmContext, SoftwareDeliveryMachine, whenPushSatisfies, } from "@atomist/sdm"; import * as _ from "lodash"; import { ConfigureOptions, configureSdm, } from "../internal/machine/configureSdm"; import { LocalSoftwareDeliveryMachineConfiguration } from "../internal/machine/LocalSoftwareDeliveryMachineOptions"; import { toArray } from "../util/misc/array"; import { createSoftwareDeliveryMachine } from "./machineFactory"; /** * Data structure to configure goal contributions */ export interface GoalStructure { /** * Optional push tests to determine when to schedule provided goals * * If an array of push tests is provided, they will get wrapped with allSatisfied/and. */ test?: PushTest | PushTest[]; /** Optional pre conditions for goals; can be actual goal instances or names of goal contributions */ dependsOn?: string | Goal | Array; /** * Goal instances to schedule * * The following cases are supported: * * goals: [ * autofix, * build * ] * * This means autofix will run after build * * goals: [ * [autofix, build] * ] * * This will schedule autofix and build concurrently * * goals: [ * [autofix, build], * dockerBuild * ] * * This will schedule autofix and build concurrently and dockerBuild once autofix and build are completed */ goals: Goal | Goals | Array>; } /** * Type to collect named GoalStructure instances * * The record key will be used to name the goal contribution. */ export type GoalData = Record; /** * Type to collect goal instances for this SDM */ export type DeliveryGoals = Record; /** * @deprecated use DeliveryGoals */ export type AllGoals = DeliveryGoals; /** * Type to create goal instances for this SDM */ export type GoalCreator = (sdm: SoftwareDeliveryMachine) => Promise; /** * Type to configure provided goals with fulfillments, listeners etc */ export type GoalConfigurer = (sdm: SoftwareDeliveryMachine, goals: G) => Promise; /** * Type to orchestrate the creation and configuration of goal instances for this SDM */ export type CreateGoals = (creator: GoalCreator, configurers?: GoalConfigurer | Array>) => Promise; /** * Configure a SoftwareDeliveryMachine instance by adding command, events etc and optionally returning * GoalData, an array of GoalContributions or void when no goals should be added to this SDM. */ export type Configurer = (sdm: SoftwareDeliveryMachine & { createGoals: CreateGoals }) => Promise>>; /** * Process the configuration before creating the SDM instance */ export type ConfigurationPreProcessor = (cfg: LocalSoftwareDeliveryMachineConfiguration) => Promise; export interface ConfigureMachineOptions extends ConfigureOptions { /** * SDM name if you want to override the default which uses the * package name. */ name?: string; /** * These functions are called in the first postProcessor. * Specifically, the first post-processor is [[configureSdm]] * these functions are called in its * [[SoftwareDeliveryMachineMaker]] function prior to it calling * the [[createSoftwareDeliveryMachine]]. */ preProcessors?: ConfigurationPreProcessor | ConfigurationPreProcessor[]; /** * These functions are called after the [[configureSdm]] post-processor. */ postProcessors?: ConfigurationPostProcessor | ConfigurationPostProcessor[]; } /** * Function to create an SDM configuration constant to be exported from an index.ts/js. */ export function configure( configurer: Configurer, options: ConfigureMachineOptions = {}): Configuration { return { postProcessors: [ configureSdm(async cfg => { let cfgToUse = cfg; // Modify the configuration before creating the SDM instance if (!!options.preProcessors) { for (const preProcessor of toArray(options.preProcessors)) { cfgToUse = await preProcessor(cfgToUse); } } const sdm = createSoftwareDeliveryMachine( { name: options.name || cfgToUse.name, configuration: cfgToUse, }); const configured = await invokeConfigurer(sdm, configurer); if (Array.isArray(configured)) { sdm.withPushRules(configured[0], ...configured.slice(1)); } else if (!!configured) { const goalContributions = convertGoalData(configured); if (goalContributions.length > 0) { sdm.withPushRules(goalContributions[0], ...(goalContributions.slice(1) || [])); } } return sdm; }, options), ...(toArray(options.postProcessors || [])), ], }; } /** * Convert the provided GoalData instance into an array of GoalContributions */ export function convertGoalData(goalData: GoalData): Array> { const goalContributions: Array> = []; _.forEach(goalData, (v, k) => { (v as any).__goals = []; const gs = goals(k.replace(/_/g, " ")); let lg: Array; if (!!v.dependsOn) { lg = []; toArray(v.dependsOn).forEach(d => { if (typeof d === "string") { if (!!goalData[d] && !!(goalData[d] as any).__goals) { lg.push(...(goalData[d] as any).__goals); } else { throw new Error( `Provided dependsOn goals with name '${d}' do not exist or is after current goals named '${k}'`); } } else { lg.push(...toArray(d)); } }); } toArray(v.goals || []).forEach(g => { (v as any).__goals.push(...(Array.isArray(g) ? (g) : [g])); if (!!lg) { gs.plan(...convertGoals(g)).after(...convertGoals(lg)); } else { gs.plan(...convertGoals(g)); } lg = toArray(g); }); goalContributions.push(whenPushSatisfies(convertPushTest(v.test)).setGoals(gs)); }); return goalContributions; } /** * Invoke the given configurer */ export async function invokeConfigurer(sdm: SoftwareDeliveryMachine, configurer: Configurer): Promise>> { try { // Decorate the createGoals method onto the SDM (sdm as any).createGoals = async (creator: GoalCreator, configurers: GoalConfigurer | Array>) => { let gc; try { gc = await creator(sdm); } catch (e) { e.message = `Creating goals failed: ${e.message}`; logger.error(e.message); throw e; } try { if (!!configurers) { for (const c of toArray(configurers)) { await c(sdm, gc); } } } catch (e) { e.message = `Configuring goals failed: ${e.message}`; logger.error(e.message); throw e; } return gc; }; return configurer(sdm as any); } finally { delete (sdm as any).createGoals; } } function convertPushTest(test: PushTest | PushTest[]): PushTest { if (Array.isArray(test)) { return allSatisfied(...test.map(wrapTest)); } else { return wrapTest(test || AnyPush); } } function wrapTest(test: PushTest): PushTest { if (!!(test as any).pushTest) { return test; } else { return notGoalTest(test); } } function convertGoals(gs: Goal | Goals | Array): Array { if (Array.isArray(gs)) { return gs; } else { return [gs]; } }