UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

301 lines (269 loc) 10.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, ConfigurationPostProcessor } from "@atomist/automation-client/lib/configuration"; import { logger } from "@atomist/automation-client/lib/util/logger"; import * as _ from "lodash"; import { SdmContext } from "../../api/context/SdmContext"; import { GoalContribution } from "../../api/dsl/goalContribution"; import { whenPushSatisfies } from "../../api/dsl/goalDsl"; import { Goal } from "../../api/goal/Goal"; import { goals, Goals } from "../../api/goal/Goals"; import { GoalWithFulfillment } from "../../api/goal/GoalWithFulfillment"; import { PushListenerInvocation } from "../../api/listener/PushListener"; import { SoftwareDeliveryMachine } from "../../api/machine/SoftwareDeliveryMachine"; import { notGoalOrOutputTest } from "../../api/mapping/goalTest"; import { PushTest } from "../../api/mapping/PushTest"; import { AnyPush } from "../../api/mapping/support/commonPushTests"; import { allSatisfied } from "../../api/mapping/support/pushTestUtils"; import { toArray } from "../util/misc/array"; import { ConfigureOptions, configureSdm } from "./configureSdm"; import { LocalSoftwareDeliveryMachineConfiguration } from "./LocalSoftwareDeliveryMachineOptions"; 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<string | Goal>; /** * 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<Goal | Goals | Array<Goal | Goals>>; } /** * Type to collect named GoalStructure instances * * The record key will be used to name the goal contribution. */ export type GoalData = Record<string, GoalStructure>; /** * Type to collect goal instances for this SDM */ export type DeliveryGoals = Record<string, Goal | GoalWithFulfillment>; /** * Type to create goal instances for this SDM */ export type GoalCreator<G extends DeliveryGoals> = (sdm: SoftwareDeliveryMachine) => Promise<G>; /** * Type to configure provided goals with fulfillments, listeners etc */ export type GoalConfigurer<G extends DeliveryGoals> = (sdm: SoftwareDeliveryMachine, goals: G) => Promise<void>; /** * Type to orchestrate the creation and configuration of goal instances for this SDM */ export type CreateGoals<G extends DeliveryGoals> = ( creator: GoalCreator<G>, configurers?: GoalConfigurer<G> | Array<GoalConfigurer<G>>, ) => Promise<G>; /** * 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<G extends DeliveryGoals, F extends SdmContext = PushListenerInvocation> = ( sdm: SoftwareDeliveryMachine & { createGoals: CreateGoals<G> }, ) => Promise<void | GoalData | Array<GoalContribution<F>>>; /** * Process the configuration before creating the SDM instance */ export type ConfigurationPreProcessor = ( cfg: LocalSoftwareDeliveryMachineConfiguration, ) => Promise<LocalSoftwareDeliveryMachineConfiguration>; 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<G extends DeliveryGoals, T extends SdmContext = PushListenerInvocation>( configurer: Configurer<G, T>, 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<GoalContribution<any>> { const goalContributions: Array<GoalContribution<any>> = []; _.forEach(goalData, (v, k) => { (v as any).__goals = []; const gs = goals(k.replace(/_/g, " ")); let lg: Array<Goal | Goals>; 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<any, any>, ): Promise<void | GoalData | Array<GoalContribution<any>>> { try { // Decorate the createGoals method onto the SDM (sdm as any).createGoals = async ( creator: GoalCreator<any>, configurers: GoalConfigurer<any> | Array<GoalConfigurer<any>>, ) => { 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 await configurer(sdm as any); } finally { delete (sdm as any).createGoals; } } function convertPushTest(test: PushTest | PushTest[]): PushTest { if (Array.isArray(test)) { const goalPushTests = test.filter(t => !!(t as any).pushTest); const convertedPushTest = allSatisfied(...test.map(wrapTest)); if (goalPushTests.length > 0) { (convertedPushTest as any).pushTest = allSatisfied(...goalPushTests.map(t => (t as any).pushTest)); } return convertedPushTest; } else { return wrapTest(test || AnyPush); } } function wrapTest(test: PushTest): PushTest { if (!!(test as any).pushTest) { return test; } else { return notGoalOrOutputTest(test); } } function convertGoals(gs: Goal | Goals | Array<Goal | Goals>): Array<Goal | Goals> { if (Array.isArray(gs)) { return gs; } else { return [gs]; } }