UNPKG

9.69 kBPlain TextView Raw
1/*
2 * Copyright © 2019 Atomist, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18 Configuration,
19 ConfigurationPostProcessor,
20 logger,
21} from "@atomist/automation-client";
22import {
23 allSatisfied,
24 AnyPush,
25 Goal,
26 GoalContribution,
27 goals,
28 Goals,
29 GoalWithFulfillment,
30 notGoalTest,
31 PushListenerInvocation,
32 PushTest,
33 SdmContext,
34 SoftwareDeliveryMachine,
35 whenPushSatisfies,
36} from "@atomist/sdm";
37import * as _ from "lodash";
38import {
39 ConfigureOptions,
40 configureSdm,
41} from "../internal/machine/configureSdm";
42import { LocalSoftwareDeliveryMachineConfiguration } from "../internal/machine/LocalSoftwareDeliveryMachineOptions";
43import { toArray } from "../util/misc/array";
44import { createSoftwareDeliveryMachine } from "./machineFactory";
45
46/**
47 * Data structure to configure goal contributions
48 */
49export interface GoalStructure {
50
51 /**
52 * Optional push tests to determine when to schedule provided goals
53 *
54 * If an array of push tests is provided, they will get wrapped with allSatisfied/and.
55 */
56 test?: PushTest | PushTest[];
57
58 /** Optional pre conditions for goals; can be actual goal instances or names of goal contributions */
59 dependsOn?: string | Goal | Array<string | Goal>;
60
61 /**
62 * Goal instances to schedule
63 *
64 * The following cases are supported:
65 *
66 * goals: [
67 * autofix,
68 * build
69 * ]
70 *
71 * This means autofix will run after build
72 *
73 * goals: [
74 * [autofix, build]
75 * ]
76 *
77 * This will schedule autofix and build concurrently
78 *
79 * goals: [
80 * [autofix, build],
81 * dockerBuild
82 * ]
83 *
84 * This will schedule autofix and build concurrently and dockerBuild once autofix and build are completed
85 */
86 goals: Goal | Goals | Array<Goal | Goals | Array<Goal | Goals>>;
87}
88
89/**
90 * Type to collect named GoalStructure instances
91 *
92 * The record key will be used to name the goal contribution.
93 */
94export type GoalData = Record<string, GoalStructure>;
95
96/**
97 * Type to collect goal instances for this SDM
98 */
99export type DeliveryGoals = Record<string, Goal | GoalWithFulfillment>;
100
101/**
102 * @deprecated use DeliveryGoals
103 */
104export type AllGoals = DeliveryGoals;
105
106/**
107 * Type to create goal instances for this SDM
108 */
109export type GoalCreator<G extends DeliveryGoals> = (sdm: SoftwareDeliveryMachine) => Promise<G>;
110
111/**
112 * Type to configure provided goals with fulfillments, listeners etc
113 */
114export type GoalConfigurer<G extends DeliveryGoals> = (sdm: SoftwareDeliveryMachine, goals: G) => Promise<void>;
115
116/**
117 * Type to orchestrate the creation and configuration of goal instances for this SDM
118 */
119export type CreateGoals<G extends DeliveryGoals> = (creator: GoalCreator<G>,
120 configurers?: GoalConfigurer<G> | Array<GoalConfigurer<G>>) => Promise<G>;
121
122/**
123 * Configure a SoftwareDeliveryMachine instance by adding command, events etc and optionally returning
124 * GoalData, an array of GoalContributions or void when no goals should be added to this SDM.
125 */
126export type Configurer<G extends DeliveryGoals, F extends SdmContext = PushListenerInvocation> =
127 (sdm: SoftwareDeliveryMachine & { createGoals: CreateGoals<G> }) => Promise<void | GoalData | Array<GoalContribution<F>>>;
128
129/**
130 * Process the configuration before creating the SDM instance
131 */
132export type ConfigurationPreProcessor = (cfg: LocalSoftwareDeliveryMachineConfiguration) =>
133 Promise<LocalSoftwareDeliveryMachineConfiguration>;
134
135export interface ConfigureMachineOptions extends ConfigureOptions {
136 /**
137 * SDM name if you want to override the default which uses the
138 * package name.
139 */
140 name?: string;
141 /**
142 * These functions are called in the first postProcessor.
143 * Specifically, the first post-processor is [[configureSdm]]
144 * these functions are called in its
145 * [[SoftwareDeliveryMachineMaker]] function prior to it calling
146 * the [[createSoftwareDeliveryMachine]].
147 */
148 preProcessors?: ConfigurationPreProcessor | ConfigurationPreProcessor[];
149 /**
150 * These functions are called after the [[configureSdm]] post-processor.
151 */
152 postProcessors?: ConfigurationPostProcessor | ConfigurationPostProcessor[];
153}
154
155/**
156 * Function to create an SDM configuration constant to be exported from an index.ts/js.
157 */
158export function configure<G extends DeliveryGoals, T extends SdmContext = PushListenerInvocation>(
159 configurer: Configurer<G, T>,
160 options: ConfigureMachineOptions = {}): Configuration {
161 return {
162 postProcessors: [
163 configureSdm(async cfg => {
164 let cfgToUse = cfg;
165
166 // Modify the configuration before creating the SDM instance
167 if (!!options.preProcessors) {
168 for (const preProcessor of toArray(options.preProcessors)) {
169 cfgToUse = await preProcessor(cfgToUse);
170 }
171 }
172
173 const sdm = createSoftwareDeliveryMachine(
174 {
175 name: options.name || cfgToUse.name,
176 configuration: cfgToUse,
177 });
178
179 const configured = await invokeConfigurer(sdm, configurer);
180
181 if (Array.isArray(configured)) {
182 sdm.withPushRules(configured[0], ...configured.slice(1));
183 } else if (!!configured) {
184 const goalContributions = convertGoalData(configured);
185 if (goalContributions.length > 0) {
186 sdm.withPushRules(goalContributions[0], ...(goalContributions.slice(1) || []));
187 }
188 }
189
190 return sdm;
191 }, options),
192 ...(toArray(options.postProcessors || [])),
193 ],
194 };
195}
196
197/**
198 * Convert the provided GoalData instance into an array of GoalContributions
199 */
200export function convertGoalData(goalData: GoalData): Array<GoalContribution<any>> {
201 const goalContributions: Array<GoalContribution<any>> = [];
202
203 _.forEach(goalData, (v, k) => {
204 (v as any).__goals = [];
205
206 const gs = goals(k.replace(/_/g, " "));
207 let lg: Array<Goal | Goals>;
208
209 if (!!v.dependsOn) {
210 lg = [];
211 toArray(v.dependsOn).forEach(d => {
212 if (typeof d === "string") {
213 if (!!goalData[d] && !!(goalData[d] as any).__goals) {
214 lg.push(...(goalData[d] as any).__goals);
215 } else {
216 throw new Error(
217 `Provided dependsOn goals with name '${d}' do not exist or is after current goals named '${k}'`);
218 }
219 } else {
220 lg.push(...toArray(d));
221 }
222 });
223 }
224
225 toArray(v.goals || []).forEach(g => {
226 (v as any).__goals.push(...(Array.isArray(g) ? (g) : [g]));
227 if (!!lg) {
228 gs.plan(...convertGoals(g)).after(...convertGoals(lg));
229 } else {
230 gs.plan(...convertGoals(g));
231 }
232 lg = toArray(g);
233 });
234
235 goalContributions.push(whenPushSatisfies(convertPushTest(v.test)).setGoals(gs));
236 });
237
238 return goalContributions;
239}
240
241/**
242 * Invoke the given configurer
243 */
244export async function invokeConfigurer(sdm: SoftwareDeliveryMachine,
245 configurer: Configurer<any, any>): Promise<void | GoalData | Array<GoalContribution<any>>> {
246
247 try {
248 // Decorate the createGoals method onto the SDM
249 (sdm as any).createGoals = async (creator: GoalCreator<any>,
250 configurers: GoalConfigurer<any> | Array<GoalConfigurer<any>>) => {
251
252 let gc;
253 try {
254 gc = await creator(sdm);
255 } catch (e) {
256 e.message = `Creating goals failed: ${e.message}`;
257 logger.error(e.message);
258 throw e;
259 }
260
261 try {
262 if (!!configurers) {
263 for (const c of toArray(configurers)) {
264 await c(sdm, gc);
265 }
266 }
267 } catch (e) {
268 e.message = `Configuring goals failed: ${e.message}`;
269 logger.error(e.message);
270 throw e;
271 }
272 return gc;
273 };
274
275 return configurer(sdm as any);
276 } finally {
277 delete (sdm as any).createGoals;
278 }
279}
280
281function convertPushTest(test: PushTest | PushTest[]): PushTest {
282 if (Array.isArray(test)) {
283 return allSatisfied(...test.map(wrapTest));
284 } else {
285 return wrapTest(test || AnyPush);
286 }
287}
288
289function wrapTest(test: PushTest): PushTest {
290 if (!!(test as any).pushTest) {
291 return test;
292 } else {
293 return notGoalTest(test);
294 }
295}
296
297function convertGoals(gs: Goal | Goals | Array<Goal | Goals>): Array<Goal | Goals> {
298 if (Array.isArray(gs)) {
299 return gs;
300 } else {
301 return [gs];
302 }
303}