UNPKG

37 kBMarkdownView Raw
1<p align="center">
2 <img src="https://github.com/atomist/sdm/raw/master/docs/SDM-Logo-Dark.png">
3</p>
4
5# Software Delivery Machine - Core - `@atomist/sdm-core`
6
7[![atomist sdm goals](http://badge.atomist.com/T29E48P34/atomist/sdm-core/e93405f2-a313-4da8-92fd-833de3b90cde)](https://app.atomist.com/workspace/T29E48P34)
8[![npm version](https://img.shields.io/npm/v/@atomist/sdm-core.svg)](https://www.npmjs.com/package/@atomist/sdm-core)
9
10Atomist framework enabling you to control your delivery and development process in code. Think of it as Spring Boot for software delivery.
11
12## What is a Software Delivery Machine?
13A **software delivery machine** (SDM) is a development process in a box. It automates all steps in the flow from commit to production (potentially via staging environments), and many other actions, using the consistent model provided by the Atomist *API for software*.
14
15> Many teams have a blueprint in their mind for how they'd like to deliver software and ease their day to day work, but find it hard to realize. A Software Delivery Machine makes it possible.
16
17The concept is explained in detail in Rod Johnson's blog [Why you need a Software Delivery Machine](https://the-composition.com/why-you-need-a-software-delivery-machine-85e8399cdfc0). This [video](https://vimeo.com/260496136) shows it in action.
18
19> Atomist is about developing your development experience by using your coding skills. Change the code, restart, and see your new automations and changed behavior across all your projects, within seconds.
20
21## Get Started
22This repository contains an SDM framework built on lower level Atomist capabilities.
23
24SDMs based on this framework process events from the Atomist SaaS event hub. The architecture is as follows, with events coming in from the systems that matter in your development process:
25
26<img src="https://atomist.com/img/Atomist-Team-Development.jpg"/>
27
28You'll need to be a member of an Atomist workspace to run an SDM.
29Create your own by [enrolling](https://github.com/atomist/welcome/blob/master/enroll.md) at [atomist.com](https://atomist.com).
30Things work best if you install an org webhook, so that Atomist receives events for all your GitHub repos.
31
32Once the Atomist bot is in your Slack team, type `@atomist create sdm` to have Atomist create a personalized SDM instance using this project. You can also clone the `sample-sdm` project.
33
34Once your SDM is running, type `@atomist show skills` in any channel to see a list of all available Atomist commands.
35
36## Run Locally
37
38SDM projects are Atomist automation clients, written in [TypeScript](https://www.typescriptlang.org) or JavaScript. See [run an automation client](https://github.com/atomist/welcome/blob/master/runClient.md) for instructions on how to set up your environment and run it under Node.js.
39
40See [set up](./docs/Setup.md) for additional prerequisites depending on the projects you're building.
41
42See the [sample-sdm project](https://github.com/atomist/sample-sdm) project for instructions on how to run an SDM instance, and description of the out of the box functionality.
43
44## Core Concepts
45Atomist is a flexible platform, enabling you to build your own automations or use those provided by Atomist or third parties. Because you're using a real programming language (not YAML or Bash), and you have access to a real ecosystem (Node), you can create a richer delivery experience than you've even imagined.
46
47This project demonstrates Atomist as the *API for software*, exposing:
48
49- *What we know*: The Atomist cortex, accessible through GraphQL queries and subscription joins
50- *What just happened*: An event, triggered by a GraphQL subscription, which is contextualized with the existing knowledge
51- *What you're working on*: A library that enables you to comprehend and manipulate the source code you're working on.
52
53This project builds on other Atomist core functionality available from global automations, such as: Atomist **lifecycle**, showing commit, pull request and other activity through actionable messages.
54
55Atomist is not tied to GitHub, but this repository focuses on using Atomist with GitHub.com or
56GitHub Enterprise.
57
58
59### Events
60The heart of Atomist is its event handling. As your code flows from commit
61through to deployment and beyond, Atomist receives events, correlates the incoming data
62with its previous knowledge, and invokes your event handlers with rich context. This enables your automations to perform tasks such as:
63
64- Scanning code for security or quality issues on every push
65- Driving deployments and promotion between environments
66- Performing custom actions on deployment, such as kicking off integration test suites.
67
68The Atomist correlated event model also enables Atomist to provide you with visibility throughout the commit to deployment flow, in Slack or through the Atomist web dashboard.
69
70#### Under the Hood: How it Works
71Event handlers subscribe to events using [GraphQL](http://graphql.org) subscriptions against the Atomist cortex. The following GraphQL subscribes to completed builds, returning related data such as the last commit and any linked Slack channels:
72
73```graphql
74subscription OnBuildComplete {
75 Build {
76 buildId
77 buildUrl
78 compareUrl
79 name
80 status
81 commit {
82 sha
83 message
84 repo {
85 name
86 owner
87 gitHubId
88 allowRebaseMerge
89 channels {
90 name
91 id
92 }
93 }
94 statuses {
95 context
96 description
97 state
98 targetUrl
99 }
100 }
101 }
102}
103```
104When using TypeScript (our recommended language), an event handler can subscribe to such events with the benefit of strong typing. For example, this Atomist event handler can respond to the above GraphQL subscription:
105
106```typescript
107@EventHandler("Set status on build complete",
108 GraphQL.subscriptionFromFile("graphql/subscription/OnBuildComplete.graphql"))
109export class SetStatusOnBuildComplete implements HandleEvent<OnBuildComplete.Subscription> {
110
111 public async handle(event: EventFired<OnBuildComplete.Subscription>,
112 ctx: HandlerContext,
113 params: this): Promise<HandlerResult> {
114```
115
116This underlying GraphQL/event handler infrastructure is generic and powerful. However, many things are better done at a higher level. This project provides a framework above this infrastructure that makes typical tasks far easier, while not preventing you from breaking out into lower level functionality.
117
118> This repository
119> includes event handlers that subscribe to the most important events in a typical
120> delivery flow. This enables dynamic and sophisticated delivery processes that are consistent across
121> multiple projects.
122
123### Goals and Listeners
124
125The most important higher level SDM functionality relates to what happens on a push to a repository. An SDM allows you to process a push in any way you choose, but typically you want it to initiate a delivery flow.
126
127#### Goals
128
129An SDM allows you to set **goals** on push. Goals correspond to the actions that make up a delivery flow, such as build and deployment. Goals are not necessarily sequential--some may be executed in parallel--but certain goals, such as deployment, have preconditions (goals that must have previously completed successfully).
130
131Goals are set using **rules**, which are typically expressed in a simple internal DSL. For example, the following rules use `PushTest` predicates such as `ToDefaultBranch` and `IsMaven` to determine what goals to set for incoming pushes:
132
133```typescript
134whenPushSatisfies(ToDefaultBranch, IsMaven, HasSpringBootApplicationClass, HasCloudFoundryManifest,
135 ToPublicRepo, not(NamedSeedRepo), not(FromAtomist), IsDeployEnabled)
136 .setGoals(HttpServiceGoals),
137whenPushSatisfies(IsMaven, HasSpringBootApplicationClass, not(FromAtomist))
138 .itMeans("Spring Boot service local deploy")
139 .setGoals(LocalDeploymentGoals),
140```
141
142Push test predicates are easy to write using the Atomist API. For example:
143
144```typescript
145export const IsMaven: PredicatePushTest = predicatePushTest(
146 "Is Maven",
147 async p => !!(await p.getFile("pom.xml")));
148```
149
150Goals are defined as follows:
151
152```typescript
153export const HttpServiceGoals = new Goals(
154 "HTTP Service",
155 FingerprintGoal,
156 AutofixGoal,
157 ReviewGoal,
158 PushReactionGoal,
159 BuildGoal,
160 ArtifactGoal,
161 StagingDeploymentGoal,
162 StagingEndpointGoal,
163 StagingVerifiedGoal,
164 ProductionDeploymentGoal,
165 ProductionEndpointGoal);
166```
167
168It is possible to define new goals with accompanying implementations, making this approach highly extensible.
169
170#### Listeners
171We'll return to push tests shortly, but first let's consider the SDM listener concept.
172
173While the goals set drive the delivery process, domain specific **listeners** help in goal implementation and allow observation of the process as it unfolds. Listener **registrations** allow selective listener firing, on only particular pushes. A registration includes a name (for diagnostics) and a `PushTest`, narrowing on particular pushes.
174
175For example, the following listener registration causes an automatic fix to be made on every push to a Node project, adding a license file if none is found:
176
177```typescript
178 sdm.addAutofixes({
179 name: "fix me",
180 pushTest: IsNode,
181 action: async cri => {
182 const license = await axios.get("https://www.apache.org/licenses/LICENSE-2.0.txt");
183 return cri.project.addFile("LICENSE", license.data);
184 },
185 })
186
187```
188
189The following listener observes a build, notifying any linked Slack channels of its status:
190
191```typescript
192sdm.addBuildListeners(async br =>
193 br.addressChannels(`Build of ${br.id.repo} has status ${br.build.status}`));
194```
195
196> SDM listeners are a layer above GraphQL subscriptions and event handlers that simplify common scenarios, and enable most functionality to be naturally expressed in terms of the problem domain. Listener implementations are also easily testable.
197
198##### Common Listener Context
199As with all good frameworks, we've tried to make the API consistent. All listener invocations include at least the following generally useful information:
200
201```typescript
202export interface SdmContext {
203
204 /**
205 * Context of the Atomist EventHandler invocation. Use to run GraphQL
206 * queries, use the messageClient directly and find
207 * the team and correlation id
208 */
209 context: HandlerContext;
210
211 /**
212 * If available, provides a way to address the channel(s) related to this event.
213 * This is usually, but not always, the channels linked to a
214 * In some cases, such as repo creation or a push to a repo where there is no linked channel,
215 * addressChannels will go to dev/null without error.
216 */
217 addressChannels: AddressChannels;
218
219 /**
220 * Credentials for use with source control hosts such as GitHub
221 */
222 credentials: ProjectOperationCredentials;
223
224}
225```
226Most events concern a specific repository, and hence most listener invocations extend `RepoContext`:
227
228```typescript
229export interface RepoContext extends SdmContext {
230
231 /**
232 * The repo this relates to
233 */
234 id: RemoteRepoRef;
235
236}
237```
238
239Many repo-specific listeners are given access to the repository source, via the `Project` abstraction:
240
241```typescript
242export interface ProjectListenerInvocation extends RepoListenerInvocation {
243
244 /**
245 * The project to which this event relates. It will have been cloned
246 * prior to this invocation. Modifications made during listener invocation will
247 * not be committed back to the project (although they are acceptable if necessary, for
248 * example to run particular commands against the project).
249 * As well as working with
250 * project files using the Project superinterface, we can use git-related
251 * functionality fro the GitProject subinterface: For example to check
252 * for previous shas.
253 * We can also easily run shell commands against the project using its baseDir.
254 */
255 project: GitProject;
256
257}
258
259```
260The `Project` interface is defined in [@atomist/automation-client](https://github.com/atomist/automation-client-ts). It provides an abstraction to the present repository, with Atomist taking care of Git cloning and (if necessary) writing back any changes via a push. It is abstracted from the file system, making it easy to unit listeners accessing repository contents, using the `InMemoryProject` and `InMemoryFile` classes.
261
262> The Project API and sophisticated parsing functionality available on top of it is a core Atomist capability. Many events can only be understood in the context of the impacted code, and many actions are achieved by modifying code.
263
264Push listeners also have access to the details of the relevant push:
265
266```typescript
267export interface PushListenerInvocation extends ProjectListenerInvocation {
268
269 /**
270 * Information about the push, including repo and commit
271 */
272 readonly push: OnPushToAnyBranch.Push;
273
274}
275```
276
277##### Available Listener Interfaces
278The following listener interfaces are available:
279
280- `ArtifactListener`: Invoked when a new binary has been created
281- `BuildListener`: Invoked when a build is complete.
282- `ChannelLinkListenerInvocation`: Invoked when a channel is linked to a repo
283- `ClosedIssueListener`: Invoked when an issue is closed
284- `PushReactionListener`: Invoked in response to a code change
285- `DeploymentListener`: Invoked when a deployment has succeeded
286- `FingerprintDifferenceListener`: Invoked when a fingerprint has changed
287- `GoalsSetListener`: Invoked when goals are set on a push
288- `Listener`: Superinterface for all listeners
289- `NewIssueListener`: Invoked when an issue has been created
290- `ProjectListener`: Superinterface for all listeners that relate to a project and make the cloned project available
291- `PullRequestListener`: Invoked when a pull request is raised
292- `PushListener`: Superinterface for listeners to push events
293- `RepoCreationListener`: Invoked when a repository has been created
294- `SupersededListener`: Invoked when a commit has been superseded by a subsequent commit
295- `TagListener`: Invoked when a repo is created
296- `UpdatedIssueListener`: Invoked when an issue has been updated
297- `UserJoiningChannelListener`: Invoked when a user joins a channel
298- `VerifiedDeploymentListener`: Invoked when an endpoint has been verified
299
300
301#### Push Mappings
302Let's now return to push mappings and goal setting. The `PushMapping` interface is used to decide how to handle pushes. Normally it is used via the DSL we've seen.
303
304```typescript
305export interface PushMapping<V> {
306
307 /**
308 * Name of the PushMapping. Must be unique
309 */
310 readonly name: string;
311
312 /**
313 * Compute a value for the given push. Return undefined
314 * if we don't find a mapped value.
315 * Return DoNotSetAnyGoals (null) to shortcut evaluation of the present set of rules,
316 * terminating evaluation and guarantee the return of undefined if we've reached this point.
317 * Only do so if you are sure
318 * that this evaluation must be short circuited if it has reached this point.
319 * If a previous rule has matched, it will still be used.
320 * The value may be static
321 * or computed on demand, depending on the implementation.
322 * @param {PushListenerInvocation} p
323 * @return {Promise<V | undefined | NeverMatch>}
324 */
325 valueForPush(p: PushListenerInvocation): Promise<V | undefined | NeverMatch>;
326}
327```
328`PushMapping` is a central interface used in many places.
329
330A `GoalSetter` is a `PushMapping` that returns `Goals`.
331
332A `PushTest` is simply a `PushMapping` that returns `boolean`.
333
334## Code Examples
335Let's look at some examples of listeners.
336
337### Issue Creation
338When a new issue is created, you may want to notify people or perform an action.
339#### Listener interfaces
3401. `NewIssueListener`: [NewIssueListener](src/api/listener/NewIssueListener.ts)
341
342#### Examples
343The following example notifies any user who raises an issue with insufficient detail in the body, via a
344direct message in Slack, and provides them with a helpful
345link to the issue. Note that we make use of the
346person available via the `openedBy` field:
347
348```typescript
349export async function requestDescription(inv: NewIssueInvocation) {
350 if (!inv.issue.body || inv.issue.body.length < 10) {
351 await inv.context.messageClient.addressUsers(
352 `Please add a description for new issue ${inv.issue.number}: _${inv.issue.title}_: ${inv.id.url}/issues/${inv.issue.number}`,
353 inv.issue.openedBy.person.chatId.screenName);
354 }
355}
356```
357This is registed with a `SoftwareDeliveryMachine` instance as follows:
358
359```typescript
360sdm.addNewIssueListeners(requestDescription)
361```
362
363Using the `credentials` on the `NewIssueInvocation`, you can easily use the GitHub API to modify the issue, for example correcting spelling errors.
364
365### Repo Creation
366We frequently want to respond to the creation of a new repository: For example, we may want to notify people, provision infrastructure, or tag it with GitHub topics based on its contents.
367
368#### Listener interfaces
369There are two scenarios to consider:
370
3711. The creation of a new repository. `RepoCreationListener`: [RepoCreationListener](src/api/listener/RepoCreationListener.ts)
3722. The first push to a repository, which uses the more generic [ProjectListener](src/api/listener/PushListener.ts)
373
374The second scenario is usually more important, as it is possible to create a repository without any source code or a master branch, which isn't enough to work with for common actions.
375
376#### Examples
377The following example publishes a message to the `#general` channel in Slack when a new repo has been created:
378
379```typescript
380export const PublishNewRepo: SdmListener = (i: ListenerInvocation) => {
381 return i.context.messageClient.addressChannels(
382 `A new repo was created: \`${i.id.owner}:${i.id.repo}\``, "general");
383};
384
385```
386
387Tagging a repo with topics based on its content is a useful action. `tagRepo` is a convenient function to construct a `ProjectListener` for this. It tags as an argument a `Tagger`, which looks at the project content and returns a `Tags` object. The following example from `atomist.config.ts` tags Spring Boot repos, using a `Tagger` from the `spring-automation` project, in addition to suggesting the addition of a Cloud Foundry manifest, and publishing the repo using the listener previously shown:
388
389```typescript
390sdm.addFirstPushListener(tagRepo(springBootTagger))
391 .addFirstPushListener(suggestAddingCloudFoundryManifest),
392 .addFirstPushListener(PublishNewRepo)
393```
394
395##### ReviewerRegistration
396`ProjectReviewer` is a type defined in `automation-client-ts`. It allows a structured review to be returned. The review comments can localize the file path, line and column if such information is available, and also optionally include a link to a "fix" command to autofix the problem.
397
398The following is a simple project reviewer spots projects without a README, using the `Project` API:
399
400```typescript
401const hasNoReadMe: ReviewerRegistration = {
402 name: "hasNoReadme",
403 action: async cri => ({
404 repoId: cri.project.id,
405 comments: !!(await cri.project.getFile("README.me")) ?
406 [] :
407 [new DefaultReviewComment("info", "readme",
408 "Project has no README",
409 {
410 path: "README.md",
411 lineFrom1: 1,
412 offset: -1,
413 })],
414 }),
415};
416```
417A slightly more complex example uses the `saveFromFiles` utility method to look for and object to YAML files in Maven projects:
418
419```typescript
420const rodHatesYaml: ReviewerRegistration = {
421 name: "rodHatesYaml",
422 pushTest: hasFile("pom.xml"),
423 action: async cri => ({
424 repoId: cri.project.id,
425 comments:
426 await saveFromFiles(cri.project, "**/*.yml", f =>
427 new DefaultReviewComment("info", "yml-reviewer",
428 `Found YML in \`${f.path}\`: Rod regards the format as an insult to computer science`,
429 {
430 path: f.path,
431 lineFrom1: 1,
432 offset: -1,
433 })),
434 }),
435};
436
437```
438
439These reviewers can be added in an SDM definition as follows:
440
441```typescript
442sdm.addProjectReviewers(hasNoReadme, rodHatesYaml);
443```
444##### AutofixRegistration
445
446An `AutofixRegistration` can automatically execute code fixes. An example, which adds a license to file if one isn't found:
447
448```typescript
449export const AddLicenseFile: AutofixRegistration = editorAutofixRegistration({
450 name: "License Fix",
451 pushTest: not(hasFile(LicenseFilename)),
452 editor: async p => {
453 const license = await axios.get("https://www.apache.org/licenses/LICENSE-2.0.txt");
454 return p.addFile("LICENSE", license.data);
455 },
456});
457```
458Note the use of the `addFile` method on `Project`. Atomist takes care of committing the change to the
459branch of the push.
460
461Registration with an SDM is simple:
462
463```typescript
464sdm.addAutofixes(
465 AddAtomistJavaHeader,
466 AddAtomistTypeScriptHeader,
467 AddLicenseFile,
468);
469```
470
471##### CodeActionRegistration interface
472This registration allows you to react to the code, with information about the changes in the given push:
473
474For example, the following function lists changed files to any linked Slack channels for the repo:
475
476```typescript
477export const listChangedFiles: PushReactionRegistration = {
478 action(i: PushImpactListenerInvocation) {
479 return i.addressChannels(`Files changed:\n${i.filesChanged.map(n => "- `" + n + "`").join("\n")}`);
480 },
481 name: "List files changed",
482};
483```
484
485If you don't have a custom name or PushTest, you can use the following shorthand:
486
487
488```typescript
489export const listChangedFiles = i => i.addressChannels(`Files changed:\n${i.filesChanged.map(n => "- `" + n + "`").join("\n")}`);
490
491```
492
493Add in an SDM definition as follows:
494
495```typescript
496sdm.addPushReactions(listChangedFiles)
497```
498> If your reaction is essentially a review--for example, it's associated with a known problem in a particular file location--use a `ReviewerRegistration` rather than a `PushReactionRegistration`.
499>
500> Important note: You must have set a `PushReactionGoal` for push reactions to be invoked
501
502#### Fingerprints
503A special kind of push listener relates to **fingerprints**.
504
505Fingerprints are data computed against a push. Think of them as snapshots. Typically they reflect the state of the repository's source code after the push; they can also take into account other characteristics of the commit. Fingerprinting is valuable because:
506
5071. *It enables us to assess the impact of a particular commit, through providing a semantic diff*. For example, did the commit change dependencies? Did it change some particularly sensitive files that necessitate closer than usual review?
5082. *It enables us to understand the evolution of a code base over time.* Atomist persists fingerprints, so we can trace over time anything we fingerprint, and report against it. For example, what is happening to code quality metrics over time?
509
510Atomist ships some out of the box fingerprints, such as Maven and `npm` dependency fingerprints. But it's easy to write your own. Fingerprint registrations are like other listener registrations, specifying a name and `PushTest`. The following example is the complete code for fingerprinting dependencies specified in a `package-lock.json` file:
511
512```typescript
513export class PackageLockFingerprinter implements FingerprinterRegistration {
514
515 public readonly name = "PackageLockFingerprinter";
516
517 public readonly pushTest: PushTest = IsNode;
518
519 public async action(cri: PushImpactListenerInvocation): Promise<FingerprinterResult> {
520 const lockFile = await cri.project.getFile("package-lock.json");
521 if (!lockFile) {
522 return [];
523 }
524 try {
525 const content = await lockFile.getContent();
526 const json = JSON.parse(content);
527 const deps = json.dependencies;
528 const dstr = JSON.stringify(deps);
529 return {
530 name: "dependencies",
531 abbreviation: "deps",
532 version: "0.1",
533 sha: computeShaOf(dstr),
534 data: json,
535 };
536 } catch (err) {
537 logger.warn("Unable to compute package-lock.json fingerprint: %s", err.message);
538 return [];
539 }
540 }
541}
542```
543
544Fingerprinters can be added to an SDM as follows:
545
546```typescript
547sdm.addFingerprinterRegistrations(new PackageLockFingerprinter());
548```
549
550Fingerprinting will only occur if a `FingerprintGoal` is selected when goals are set.
551
552## Generators
553Another important concern is project creation. Consistent project creation is important to governance and provides a way of sharing knowledge across a team.
554
555Atomist's [unique take on project generation](https://the-composition.com/no-more-copy-paste-bf6c7f96e445) starts from a **seed project**--a kind of golden master, that is version controlled using your regular repository hosting solution. A seed project doesn't need to include template content: It's a regular project in whatever stack, and Atomist transforms it to be a unique, custom project based on the parameters supplied at the time of project creation. This allows freedom to evolve the seed project with regular development tools.
556
557Generators can be registered with an SDM as follows:
558
559```typescript
560sdm.addGenerators(() => springBootGenerator({
561 ...CommonJavaGeneratorConfig,
562 seedRepo: "spring-rest-seed",
563 intent: "create spring",
564}))
565```
566
567The `springBootGenerator` function used here is provided in `sample-sdm`, but it's easy enough to write your own transformation using the `Project` API. Here's most of the code in our real Node generator:
568
569```typescript
570export function nodeGenerator(config: GeneratorConfig,
571 details: Partial<GeneratorCommandDetails<NodeProjectCreationParameters>> = {}): HandleCommand {
572 return generatorHandler<NodeProjectCreationParameters>(
573 transformSeed,
574 () => new NodeProjectCreationParameters(config),
575 `nodeGenerator-${config.seedRepo}`,
576 {
577 tags: ["node", "typescript", "generator"],
578 ...details,
579 intent: config.intent,
580 });
581}
582
583function transformSeed(params: NodeProjectCreationParameters, ctx: HandlerContext) {
584 return chainEditors(
585 updatePackageJsonIdentification(params.appName, params.target.description,
586 params.version,
587 params.screenName,
588 params.target),
589 updateReadmeTitle(params.appName, params.target.description),
590 );
591}
592```
593
594You can invoke such a generator from Slack, like this:
595
596<img src="https://github.com/atomist/github-sdm/blob/master/docs/create_sample1.png?raw=true"/>
597
598Note how the repo was automatically tagged with GitHub topics after creation. This was the work of a listener, specified as follows:
599
600```typescript
601sdm.addFirstPushListener(
602 tagRepo(springBootTagger),
603);
604```
605
606With Atomist ChatOps supports, you can follow along in a linked channel like this:
607
608<img src="https://github.com/atomist/github-sdm/blob/master/docs/sample1_channel.png?raw=true"/>
609
610Note the suggestion to add a Cloud Foundry manifest. This is the work of another listener, which reacts to finding new code in a repo. Listeners and commands such as generators work hand in hand for Atomist.
611
612## Editors
613Another core concept is a project **editor**. An editor is a command that transforms project content. Atomist infrastructure can help persist such transformations through branch commits or pull requests, with clean diffs.
614
615### A Simple Editor
616As you'd expect, editors also use th `Project` API.
617
618Here's an example of a simple editor that takes as a parameter the path of a file to remove from a repository.
619
620```typescript
621@Parameters()
622export class RemoveFileParams {
623
624 @Parameter()
625 public path: string;
626}
627
628export const removeFileEditor: HandleCommand = editorCommand<RemoveFileParams>(
629 () => removeFile,
630 "remove file",
631 RemoveFileParams,
632 {
633 editMode: params => commitToMaster(`You asked me to remove file ${params.path}!`),
634 });
635
636async function removeFile(p: Project, ctx: HandlerContext, params: RemoveFileParams) {
637 return p.deleteFile(params.path);
638}
639```
640
641Editors can be registered with an SDM as follows:
642
643```typescript
644sdm.addEditors(
645 () => removeFileEditor,
646);
647```
648
649### Dry Run Editors
650More elaborate editors use helper APIs on top of the `Project` API such as Atomist's [microgrammar](https://github.com/atomist/microgrammar) API and [ANTLR](https://github.com/atomist/antlr-ts) integration.
651
652There's also an important capability called "dry run editing": Performing an edit on a branch, and then either raising either a PR or an issue, depending on build success or failure. This allows us to safely apply edits across many repositories. There's a simple wrapper function to enable this:
653
654```typescript
655export const tryToUpgradeSpringBootVersion: HandleCommand = dryRunEditor<UpgradeSpringBootParameters>(
656 params => setSpringBootVersionEditor(params.desiredBootVersion),
657 UpgradeSpringBootParameters,
658 "boot-upgrade", {
659 description: `Upgrade Spring Boot version`,
660 intent: "try to upgrade Spring Boot",
661 },
662);
663```
664This editor will upgrade the Spring Boot version in one or more projects, then wait to see if the builds succeed. Output will look like this (in the case of success):
665
666<img src="https://github.com/atomist/github-sdm/blob/master/docs/dry_run_upgrade.png?raw=true"/>
667
668> Dry run editing is another example of how commands and events can work hand in hand with Atomist to provide a uniquely powerful solution.
669
670
671## Arbitrary Commands
672Both generators and editors are special cases of Atomist **command handlers**, which can be invoked via Slack or HTTP. You can write commands to ensure that anything that needs to be repeated gets done the right way each time, and that the solution isn't hidden on someone's machine.
673
674## Pulling it All Together: The `SoftwareDeliveryMachine` class
675
676Your ideal delivery blueprint spans delivery flow, generators, editors and other commands. All we need is something to pull it together.
677
678Your event listeners need to be invoked by Atomist handlers. The `SoftwareDeliveryMachine` takes care of this, ensuring that the correct handlers are emitted for use in `atomist.config.ts`, without you needing to worry about the event handler registrations on underlying GraphQL.
679
680The `SoftwareDeliveryMachine` class offers a fluent builder approach to adding command handlers, generators and editors.
681
682### Example
683For example:
684
685```typescript
686 const sdm = createSoftwareDeliveryMachine(
687 {
688 builder: K8sBuildOnSuccessStatus,
689 deployers: [
690 K8sStagingDeployOnSuccessStatus,
691 K8sProductionDeployOnSuccessStatus,
692 ],
693 artifactStore,
694 },
695 whenPushSatisfies(PushToDefaultBranch, IsMaven, IsSpringBoot, HasK8Spec, PushToPublicRepo)
696 .setGoals(HttpServiceGoals),
697 whenPushSatisfies(not(PushFromAtomist), IsMaven, IsSpringBoot)
698 .setGoals(LocalDeploymentGoals),
699 whenPushSatisfies(IsMaven, MaterialChangeToJavaRepo)
700 .setGoals(LibraryGoals),
701 whenPushSatisfies(IsNode).setGoals(NpmGoals),
702 );
703 sdm.addFirstPushListener(suggestAddingK8sSpec)
704 .addSupportingCommands(() => addK8sSpec)
705 .addSupportingEvents(() => NoticeK8sTestDeployCompletion,
706 () => NoticeK8sProdDeployCompletion)
707 .addEndpointVerificationListeners(
708 lookFor200OnEndpointRootGet({
709 retries: 15,
710 maxTimeout: 5000,
711 minTimeout: 3000,
712 }),
713 );
714 sdm.addNewIssueListeners(requestDescription)
715 .addEditors(() => tryToUpgradeSpringBootVersion)
716 .addGenerators(() => springBootGenerator({
717 seedOwner: "spring-team",
718 seedRepo: "spring-rest-seed",
719 groupId: "myco",
720 }))
721 .addFirstPushListener(tagRepo(springBootTagger))
722 .addProjectReviewers(logReview)
723 .addPushReactions(listChangedFiles)
724 .addFingerprinters(mavenFingerprinter)
725 .addDeploymentListeners(PostToDeploymentsChannel)
726 .addEndpointVerificationListeners(LookFor200OnEndpointRootGet)
727 .addVerifiedDeploymentListeners(presentPromotionButton)
728 .addSupersededListeners(
729 inv => {
730 logger.info("Will undeploy application %j", inv.id);
731 return LocalMavenDeployer.deployer.undeploy(inv.id);
732 })
733 .addSupportingCommands(
734 () => addCloudFoundryManifest,
735 DescribeStagingAndProd,
736 () => disposeProjectHandler,
737 )
738 .addSupportingEvents(OnDryRunBuildComplete);
739```
740The `SoftwareDeliveryMachine` instance will create the necessary Atomist event handlers to export.
741
742In `atomist.config.ts` you can bring them in simply as follows:
743
744```typescript
745commands: assembled.commandHandlers,
746events: assembled.eventHandlers,
747```
748
749## Structure of This Project
750
751- `src/api` is the public user-facing API, including the software delivery machine concept that ties everything together. *This may be extracted into its own Node module in future.*
752- `src/spi` contains interfaces to be extended in integrations with infrastructure,
753such as artifact storage, logging, build and deployment.
754- `src/graphql` contains GraphQL queries. You can add fields to existing queries and subscriptions, and add your own.
755- `src/typings` is where types generated from GraphQL wind up. Refresh these with `npm run gql:gen`
756if you update any GraphQL files in `src/graphql`.
757- `src/util` contains miscellaneous utilities.
758- `src/internal` contains lower level code such as event handlers necessary to support the user API. This is not intended for user use.
759- `src/pack` contains "extension packs." These will ultimately be extracted into their own Node modules.
760- The other directories unders `src` contain useful functionality that may eventually be moved out of this project.
761
762The types from `src/api` can be imported into downstream projects from the `index.ts` barrel.
763
764## Plugging in Third Party Tools
765
766This repo shows the use of Atomist to perform many steps itself. However, each of the goals used by Atomist here is pluggable.
767
768It's also easy to integrate third party tools like Checkstyle.
769
770### Integrating CI tools
771One of the tools you are most likely to integrate is CI. For example, you can integrate Jenkins, Travis or Circle CI with Atomist so that these tools are responsible for build. This has potential advantages in terms of scheduling and repeatability of environments.
772
773Integrating a CI tool with Atomist is simple. Simply invoke Atomist hooks to send events around build and artifact creation.
774
775If integrating CI tools, we recommend the following:
776
777- CI tools are great for building and generating artifacts. They are often abused as a PaaS for `bash`. If you find your CI usage has you programming in `bash` or YML, consider whether invoking such operations from Atomist event handlers might be a better model.
778- Use Atomist generators to create your CI files, and Atomist editors to keep them in synch, minimizing inconsistency.
779
780### Integrating APM tools
781tbd
782
783### Integrating with Static Analysis Tools
784Any tool that runs on code, such as Checkstyle, can easily be integrated.
785
786If the tool doesn't have a Node API (which Checkstyle doesn't as it's written in Java), you can invoke it via Node `spawn`, as Node excels at working with child processes.
787
788## Advanced Push Rules
789
790### Computed Values
791You can use computed `boolean` values or the results of synchronous or asynchronous functions returning `boolean` in the DSL, making it possible to bring in any state you wish. For example:
792
793```typescript
794whenPushSatisfies(IsMaven, HasSpringBootApplicationClass,
795 deploymentsToday < 25)
796 .itMeans("Not tired of deploying Spring apps yet")
797 .setGoals(LocalDeploymentGoals),
798```
799
800### Decision Trees
801You can write decision trees in push rules or other push mappings. These can be nested to arbitrary depth, and can use computed state. For example:
802
803```typescript
804let count = 0;
805const pm: PushMapping<Goals> = given<Goals>(IsNode)
806 // Compute a value we'll use later
807 .init(() => count = 0)
808 .itMeans("node")
809 .then(
810 given<Goals>(IsExpress).itMeans("express")
811 .compute(() => count++)
812 // Go into tree branch rule set
813 .then(
814 whenPushSatisfies(count > 0).itMeans("nope").setGoals(NoGoals),
815 whenPushSatisfies(TruePushTest).itMeans("yes").setGoals(HttpServiceGoals),
816 ),
817 );
818```
819
820### "Contribution" Style
821tbd
822
823## Roadmap
824
825This project is under active development, and still in flux. Some goals:
826
827- Splitting out `sdm-api` project with the contents of the `src/api` directory
828- Extracting the extension packs under `src/pack` into their own Node modules.