1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import {
|
18 | Configuration,
|
19 | NoParameters,
|
20 | } from "@atomist/automation-client";
|
21 | import { deepMergeConfigs } from "@atomist/automation-client/lib/configuration";
|
22 | import {
|
23 | CommandHandlerRegistration,
|
24 | CommandListener,
|
25 | EventHandlerRegistration,
|
26 | ExtensionPack,
|
27 | PushTest,
|
28 | SoftwareDeliveryMachine,
|
29 | SoftwareDeliveryMachineConfiguration,
|
30 | } from "@atomist/sdm";
|
31 | import * as camelcaseKeys from "camelcase-keys";
|
32 | import * as changeCase from "change-case";
|
33 | import * as fs from "fs-extra";
|
34 | import * as glob from "glob";
|
35 | import * as yaml from "js-yaml";
|
36 | import * as stringify from "json-stringify-safe";
|
37 | import * as _ from "lodash";
|
38 | import * as path from "path";
|
39 | import * as trace from "stack-trace";
|
40 | import * as util from "util";
|
41 | import { githubGoalStatusSupport } from "../../pack/github-goal-status/github";
|
42 | import { goalStateSupport } from "../../pack/goal-state/goalState";
|
43 | import { toArray } from "../../util/misc/array";
|
44 | import {
|
45 | configure,
|
46 | ConfigureMachineOptions,
|
47 | CreateGoals,
|
48 | DeliveryGoals,
|
49 | GoalConfigurer,
|
50 | GoalCreator,
|
51 | GoalData,
|
52 | } from "../configure";
|
53 | import {
|
54 | GoalMaker,
|
55 | mapGoals,
|
56 | } from "./mapGoals";
|
57 | import { PushTestMaker } from "./mapPushTests";
|
58 | import { mapRules } from "./mapRules";
|
59 | import { watchPaths } from "./util";
|
60 |
|
61 | export interface YamlSoftwareDeliveryMachineConfiguration {
|
62 | extensionPacks?: ExtensionPack[];
|
63 | extensions?: {
|
64 | commands?: string[];
|
65 | events?: string[];
|
66 | ingesters?: string[];
|
67 |
|
68 | goals?: string[];
|
69 | tests?: string[];
|
70 | };
|
71 | }
|
72 |
|
73 | export type CommandMaker<PARAMS = NoParameters> =
|
74 | (sdm: SoftwareDeliveryMachine) => Promise<CommandListener> | CommandListener;
|
75 | export type EventHandler<PARAMS = NoParameters> =
|
76 | Omit<EventHandlerRegistration<PARAMS>, "name">;
|
77 | export type EventMaker<PARAMS = NoParameters> =
|
78 | (sdm: SoftwareDeliveryMachine) => Promise<EventHandler<PARAMS>> | EventHandler<PARAMS>;
|
79 |
|
80 | export type ConfigurationMaker = (cfg: Configuration) =>
|
81 | Promise<SoftwareDeliveryMachineConfiguration<YamlSoftwareDeliveryMachineConfiguration>> |
|
82 | SoftwareDeliveryMachineConfiguration<YamlSoftwareDeliveryMachineConfiguration>;
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | export interface ConfigureYamlOptions<G extends DeliveryGoals> {
|
88 | options?: ConfigureMachineOptions;
|
89 |
|
90 | tests?: Record<string, PushTest>;
|
91 |
|
92 | goals?: GoalCreator<G>;
|
93 | configurers?: GoalConfigurer<G> | Array<GoalConfigurer<G>>;
|
94 |
|
95 | cwd?: string;
|
96 | }
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | export async function configureYaml<G extends DeliveryGoals>(patterns: string | string[],
|
105 | options: ConfigureYamlOptions<G> = {}): Promise<Configuration> {
|
106 |
|
107 | const callerCallSite = trace.get().filter(t => t.getFileName() !== __filename)
|
108 | .filter(t => !!t.getFileName())[0];
|
109 | const cwd = options.cwd || path.dirname(callerCallSite.getFileName());
|
110 |
|
111 | const cfg = await createConfiguration(cwd, options);
|
112 |
|
113 | return configure<G>(async sdm => {
|
114 | await createExtensions(cwd, cfg, sdm);
|
115 | return createGoalData(patterns, cwd, options, cfg, sdm);
|
116 | }, options.options || {});
|
117 | }
|
118 |
|
119 | async function createConfiguration(cwd: string, options: ConfigureYamlOptions<any>)
|
120 | : Promise<YamlSoftwareDeliveryMachineConfiguration> {
|
121 | const cfg: any = {};
|
122 | await awaitIterable(await requireConfiguration(cwd), async v => {
|
123 | const c = await v(cfg);
|
124 | deepMergeConfigs(cfg, c);
|
125 | });
|
126 | _.update(options, "options.preProcessors",
|
127 | old => !!old ? old : []);
|
128 | options.options.preProcessors = [
|
129 | async c => deepMergeConfigs(c, cfg) as any,
|
130 | ...toArray(options.options.preProcessors),
|
131 | ];
|
132 | return cfg;
|
133 | }
|
134 |
|
135 | async function createExtensions(cwd: string,
|
136 | cfg: YamlSoftwareDeliveryMachineConfiguration,
|
137 | sdm: SoftwareDeliveryMachine): Promise<void> {
|
138 | await awaitIterable(
|
139 | await requireCommands(cwd, _.get(cfg, "extensions.commands")),
|
140 | async (c, k) => {
|
141 | let registration: CommandHandlerRegistration;
|
142 | try {
|
143 | const makerResult = await c(sdm);
|
144 | registration = { name: k, listener: makerResult };
|
145 | } catch (e) {
|
146 | e.message = `Failed to make command using CommandMaker ${k}: ${e.message}`;
|
147 | throw e;
|
148 | }
|
149 | try {
|
150 | sdm.addCommand(registration);
|
151 | } catch (e) {
|
152 | e.message = `Failed to add command ${k} '${stringify(registration)}': ${e.message}`;
|
153 | throw e;
|
154 | }
|
155 | });
|
156 | await awaitIterable(
|
157 | await requireEvents(cwd, _.get(cfg, "extensions.events")),
|
158 | async (e, k) => {
|
159 | let registration: EventHandlerRegistration;
|
160 | try {
|
161 | const makerResult = await e(sdm);
|
162 | registration = { name: k, ...makerResult };
|
163 | } catch (e) {
|
164 | e.message = `Failed to make event using EventMaker ${k}: ${e.message}`;
|
165 | throw e;
|
166 | }
|
167 | try {
|
168 | sdm.addEvent(registration);
|
169 | } catch (e) {
|
170 | e.message = `Failed to add event ${k} '${stringify(registration)}': ${e.message}`;
|
171 | throw e;
|
172 | }
|
173 | });
|
174 | await requireIngesters(cwd, _.get(cfg, "extensions.ingesters"));
|
175 | sdm.addExtensionPacks(...(sdm.configuration.sdm?.extensionPacks || [
|
176 | goalStateSupport({
|
177 | cancellation: {
|
178 | enabled: true,
|
179 | },
|
180 | }),
|
181 | githubGoalStatusSupport(),
|
182 | ]));
|
183 | }
|
184 |
|
185 |
|
186 | async function createGoalData<G extends DeliveryGoals>(patterns: string | string[],
|
187 | cwd: string,
|
188 | options: ConfigureYamlOptions<G>,
|
189 | cfg: YamlSoftwareDeliveryMachineConfiguration,
|
190 | sdm: SoftwareDeliveryMachine & { createGoals: CreateGoals<G> })
|
191 | : Promise<GoalData> {
|
192 | const additionalGoals = options.goals ? await sdm.createGoals(options.goals, options.configurers) : {};
|
193 | const goalMakers = await requireGoals(cwd, _.get(cfg, "extensions.goals"));
|
194 | const testMakers = await requireTests(cwd, _.get(cfg, "extensions.tests"));
|
195 |
|
196 | const files = await resolvePaths(cwd, patterns, true);
|
197 |
|
198 | const goalData: GoalData = {};
|
199 |
|
200 | for (const file of files) {
|
201 |
|
202 | const configs = yaml.safeLoadAll(
|
203 | await fs.readFile(
|
204 | path.join(cwd, file),
|
205 | { encoding: "UTF-8" },
|
206 | ));
|
207 |
|
208 | for (const config of configs) {
|
209 | if (!!config.name) {
|
210 | (sdm as any).name = config.name;
|
211 | }
|
212 |
|
213 | if (!!config.configuration) {
|
214 | _.merge(sdm.configuration, camelcaseKeys(config.configuration, { deep: true }));
|
215 | }
|
216 |
|
217 | if (!!config.item) {
|
218 | _.merge(sdm.configuration, camelcaseKeys(config.item, { deep: true }));
|
219 | }
|
220 |
|
221 | for (const k in config) {
|
222 | if (config.hasOwnProperty(k)) {
|
223 | const value = config[k];
|
224 |
|
225 |
|
226 | if (k === "name" || k === "configuration" || k === "skill") {
|
227 | continue;
|
228 | }
|
229 |
|
230 |
|
231 | if (k === "goals") {
|
232 | await mapGoals(
|
233 | sdm,
|
234 | value,
|
235 | additionalGoals,
|
236 | goalMakers,
|
237 | options.tests || {},
|
238 | testMakers);
|
239 | }
|
240 |
|
241 | if (k === "rules") {
|
242 | await mapRules(value, goalData, sdm, options, additionalGoals, goalMakers, testMakers);
|
243 | }
|
244 | }
|
245 | }
|
246 | }
|
247 | }
|
248 |
|
249 | return goalData;
|
250 | }
|
251 |
|
252 | async function requireExtensions<EXT>(cwd: string,
|
253 | pattern: string[],
|
254 | cb: (v: EXT, k: string, e: Record<string, EXT>) => void = () => {
|
255 | },
|
256 | ): Promise<Record<string, EXT>> {
|
257 | const extensions: Record<string, EXT> = {};
|
258 | const files = await resolvePaths(cwd, pattern);
|
259 | for (const file of files) {
|
260 | const testJs = require(`${cwd}/${file}`);
|
261 | _.forEach(testJs, (v: EXT, k: string) => {
|
262 | if (!!cb) {
|
263 | cb(v, k, extensions);
|
264 | }
|
265 | extensions[k] = v;
|
266 | });
|
267 | }
|
268 | return extensions;
|
269 | }
|
270 |
|
271 | async function requireTests(cwd: string, pattern: string[] = ["tests |
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
\ | No newline at end of file |