UNPKG

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