Event Handlers

Events are everywhere in software development and operations processes. Your source code repositories generate events. Your CI systems generate events. Your runtime platforms generate events. Your services generate events. Your issue tracking systems generate events. Everything seems to generate events.

Atomist believes that bringing all these events together so they can be connected and acted upon provides tremendous value for shipping better code faster. The mechanism for realizing this promise is to automate event responses using Rug event handlers.

Event handlers define automated responses to events. Each event handler is a program in a compiles-to-JavaScript, Turing-complete language. Based on the characteristics of the event that occurred, the handler decides whether to act and which actions to take.

Why not try…

A new issue was created? Post that in the repository’s chat channel and also add buttons to the message that let people apply labels or claim the issue without leaving chat.

A developer submits a pull request in a library? Find out whether it will impact a service that uses the library: create a branch in the service, modify the code to update the dependency.

The service build completes successfully? Update the library’s pull request (PR), and tell the developer all the news.

A person in chat asks Atomist “what did I do today?”? Respond by listing the issues they updated, the PRs they reviewed, and the commits they pushed.

Anatomy of an Event Handler

Rug event handlers are where you define how Atomist responds to the events that matter in your system.

Depending on their goal, Event Handler implementations are stored either along side the project they target or in different projects altogether.

Below is the basic structure of any Rug project, except that our handlers live in the .atomist/handlers directory:

~/workspace/team-handlers
    ├── .atomist
    │   ├── .gitignore
    │   ├── handlers
    │   │   ├── command
    │   │   │   └── MergePR.ts
    │   │   └── event
    │   │   │   └── GitHubCommit.ts
    │   ├── package.json
    │   ├── tests
    │   └── tsconfig.json
    ├── CHANGELOG.md
    ├── .gitignore
    ├── LICENSE
    └── README.md

The remaining files and directors of this Rug follows the usual Rug project structure.

Example Event Handler

The following event handler responds to GitHub issue closed events. When this event occurs, the Atomist Bot sends a message to the repository’s Slack channel informing people in the channel about the closing event, whilst also providing a button, that when clicked, will reopen it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { EventHandler, Tags } from "@atomist/rug/operations/Decorators";
import { HandleEvent, LifecycleMessage, Plan } from "@atomist/rug/operations/Handlers";
import { GraphNode, Match, PathExpression } from "@atomist/rug/tree/PathExpression";

import { Comment } from "@atomist/cortex/Comment";
import { Issue } from "@atomist/cortex/Issue";

@EventHandler("ClosedGitHubIssues", "Handles closed issue events",
    new PathExpression<Issue, Issue>(
        `/Issue()[@state='closed']
            [/resolvingCommits::Commit()/author::GitHubId()
                [/person::Person()/chatId::ChatId()]?]?
            [/openedBy::GitHubId()
                [/person::Person()/chatId::ChatId()]?]
            [/closedBy::GitHubId()
                [/person::Person()/chatId::ChatId()]?]?
            [/repo::Repo()/channels::ChatChannel()]
            [/labels::Label()]?`))
@Tags("github", "issue")
class ClosedIssue implements HandleEvent<Issue, Issue> {
    handle(event: Match<Issue, Issue>): Plan {
        const issue = event.root();

        const lifecycleId = "issue/" + issue.repo.owner + "/" + issue.repo.name + "/" + issue.number;
        const message = new LifecycleMessage(issue, lifecycleId);

        message.addAction({
            label: "Reopen",
            instruction: {
                kind: "command",
                name: "ReopenGitHubIssue",
                parameters: {
                    issue: issue.number,
                    owner: issue.repo.owner,
                    repo: issue.repo.name,
                },
            },
        });

        return Plan.ofMessage(message);
    }
}
export const closedIssue = new ClosedIssue();

This event handler follows the same programming model as other Rugs, so it should look familiar. It gets triggered for each GitHub repository Commit event in the team and responds by sending a Lifecycle Message to the Atomist Bot.

Declaration

Declaring an event handler is done using the @EventHandler class decorator which requires a name and a description. Additionally, it requires a Path Expression, which registers the types of events for which the handler should be triggered.

Implementation

Rug event handlers must implemente the HandleEvent<R,M> interface, where the R and M type parameters refer to the expected root node and match types resulting from the execution of a Path Expression respectively.

Discovery

All Rugs should be annotated with one or more @Tags decorators to optimize their discoverability. For example if you were to create an Rug that alters a README file then the following @Tags would be applicable:

@Tags("readme", "documentation")

Tag values should consist of only lower case letters, numbers, and dashes (-).

If possible, try to include at least one of the tags on your Rug maps to an image for a nicer rendering. The following tags currently have images: docker,github, travis-ci, apache, git, spring-boot, spring, clojure, go, java, python, scala, and documentation.

EventPlans

An EventPlan describes the actions to be taken by the Rug runtime on behalf of the handler. EventPlans are composed of Messages and/or respondables. Respondables instruct the rug runtime to automatically perform ordinary Rug operations, whereas messages are sent to chat by the Atomist Bot.

Messages

A Message represents presentable content and/or deferable actions displayed to the user in response to returning an EventPlan from an event handler. Every message will end up being sent to a chat channel or user by the Atomist bot. However, each of the two available message types achieve this in different ways.

Directed Messages

A Directed Message is sent directly to on or more users or channels. No automatic routing is performed by the Atomist Bot. It’s possible to add Directed Messages to a Plan returned by any handler type.

The simplest use of a Response Message just echos back some plain text to the user that invoked the command:

    handle(command: HandlerContext): CommandPlan {
        const bob = new UserAddress("@bob");
        return CommandPlan.ofMessage(new DirectedMessage("Hello world!", bob));
    }

It’s possible to add more channels or usernames:

handle(response: Response<any>): CommandPlan {
    const bob = new UserAddress("@bob");
    let msg  = new DirectedMessage("Hello everyone!", bob);
    msg.addAddress(new ChannelAddress("#general"));
    return CommandPlan.ofMessage(msg);
}

By default, the body of the message is assumed to be "text/plain, and will be displayed verbatim. The message can also be JSON formatted according to Slack’s message standards:

    handle(command: HandlerContext): CommandPlan {
        const user = new UserAddress("@bob");
        const json = {
            "text": "I am a test message https://www.atomist.com",
            "attachments": [{
               "text": "And here’s an attachment!"
             }]
        }
        const msg = new DirectedMessage(JSON.stringify(json), user, MessageMimeTypes.SLACK_JSON);
        return CommandPlan.ofMessage(msg);
    }

Lifecycle Messages

As implied by the name, a LifecycleMessage refers to some pre-defined system lifecycle, such as the flow of a commit from Git, through CI to deployment, monitoring and the like.

Lifecycle messages are automatically routed to appropriate chat channel by the Atomist Bot. Typically, this is the channel associated with the GitHub repository associated with the activity initiating the lifecycle message.

Message styling is also currently performed by the Atomist Bot, though we are working to allow customization in the handlers themselves.

The Atomist Bot needs two pieces of information in order to correctly route and render a lifecycle message, a GraphNode and a lifecycle-id. The GraphNode contains all the data required to render actionable messages in chat, and the lifecycle-id ties associates related messages to one-another. In a Slack context, this means a Commit message could have its associated CI build status updated as the build progresses from say started through succeeded or failed.

So the main responsibility of lifecycle messages is to create associations between related messages using the lifecycle-id.

Messages would not be actionable without some way for the user to initiate related acitivites. Lifecyce messages achieve this by allowing actions in the form of a Presentable to be added to the message before dispatch:

 message.addAction({
            label: "Reopen",
            instruction: {
                kind: "command",
                name: "ReopenGitHubIssue",
                parameters: {
                    issue: issue.number,
                    owner: issue.repo.owner,
                    repo: issue.repo.name,
                },
            },
        });

Here we are saying: render a button with label Reopen, which when clicked, will dispatch a Command Handler ("command") called ReopenGithubIssue, which will execute a Rug Function which attempt to re-open the issue.

The term action here refers to the actions associated with Slack buttons. So by adding actions to a message, we are dispatching optional deferred Rug executions. In this instance a Command Handler bound to a specific GitHub issue, but it could just as easily have been an editor or generator.

Respondables

An EventRespondable is really just a container for an instruction and some optional onError and onSuccess capabilities. The onError and onSuccess properties of an EventRespondable can be messages, EventPlans or response handlers.

const plan = new EventPlan();
plan.add(
    {
        instruction: {
            kind: "edit",
            project: "rugs",
            name: "UpdateReadme",
            parameters: {
                newContent: "Rugs really tie the room together"
            }
        },
        onSuccess: new DirectedMessage("woot", new ChannelAddress("#random")),
        onError: {
            kind: "respond",
            name: "HandleReadmeUpdateErrors"
        }
    }
)

The example above shows how to send woot to the #random channel using a Directed Message if the edit Instruction completes successfully, and invoke the HandleReadmeUpdateErros Response Handler if it fails.

Instructions

Instructions in an EventRespondable have the following properties:

  • kind: "generate" | "edit" | "execute": the kind of instruction
  • name: string: the name of the operation to apply
  • parameters: {}: key/value pairs passed to the operation
  • project?: string: Project name (only for generators & editors)

Instructions can be used to have the rug runtime run Rugs, such as invoking a generator ("generate"), an editor ("edit"), or a Rug Funtion ("execute").

Rug Functions

Rug Functions, as indicated by their name, are functions that can be invoked from within Event or Command Handlers. They can be scheduled for invocation by adding an instruction of kind execute to a plan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
plan.add(
    {
        instruction: {
            kind: "execute",
            name: "http",
            parameters: {
                url: "https://api.github.com/repos/atomist/rug",
                method: "post",
                config: {
                    headers: {
                        "Content-Type": "application/json",
                        "Authorization": `token #{github://user_token?scopes=repo}`,
                    },
                    body: JSON.stringify(createRepoRequest)
                }
            }
        },
        onSuccess: new DirectMessage("Woot!", new ChannelAddress("#random")),
        onError: new DirectMessage("Un oh", new ChannelAddress("#random"))
    }
)

The plan above instructs the Rug Runtime to invoke the http Rug Function, passing all the parameters to it, and sending appropriate messages to the #random channel onSuccess or onError.

To use a Rug Function from an Event or Command Handler, its archive’s coordinates must be present in the .atomist.extensions section of the Rug project’s package.json.

Security

All Rug Function archives must be signed by Atomist and be explicitly whitelisted in the Atomist service. Otherwise, the execution of the invoking handler will be aborted.

Secrets can also be passed to Rug Functions, but currently this is only supported for Command Handlers.

The special #{<secret path>} notation used above is a way of injecting the value of a secret (in this case, a GitHub token with repo scope) in to the parameters to a function via string filtering. Rug Functions can also be annotated so that if the secrets required are known in advance, we don’t need to use paramter filtering.

The following Rug Function archives available:

Creating your own Rug functions

Right now there is no support for user-created Rug functions. Please get in touch by joining our Slack team if you need one that doesn’t already exist.

Secrets

Secrets are pieces of sensitive information stored securely by Atomist. Secrets are used by Rug Functions to provide access to secured systems, such as the GitHub API.

Handlers that invoke Rug Functions that require secrets must use the @Secrets decorator to declare to the Rug runtime that those secrets will be required during the execution of the handler’s CommandPlan:

...
@Secrets("github://user_token?scopes=repo,read:org")
class CloseIssueCommand implements HandleCommand {
    //...
}

The @Secrets decorator takes a comma separate list of secret paths. The decorator provides enough context to the Atomist Bot such that it can initiate the secure collection of the require secret data, such as a GitHub token collected via OAuth flow.

Confidentiality

All sensitive data stored by Atomist are encrypted at rest in Vault.

There are currently two types of secrets:

  • GitHub tokens: automatically collected by the Atomist Bot
    • "github://user_token?scopes=repo" - repo scoped user token
    • "github://team_token?scopes=repo" - repo scoped team token Both user and team GitHub tokens require the scopes needed by the token to be provided as a comma-separated list.
  • Generic Secrets: manual collection
    • "secret://user?path=/some/secret" - generic user secret
    • "secret://team?path=/some/secret" - generic team secret

Generic Secrets

These are currently only available for very specific and mostly internal use cases as we currently have no secure public mechanism for collecting and storing them, though this is something we are hoping to support in the near future. They are mentioned here to avoid any confusion when seen in publically visible Handlers.

Missing

Although it’s possible to invoke Rug Functions from Event Handlers, there is currently no mechanism to populate Secrets (tokens, passwords etc.) as there is for Command Handlers. We are currently working on support for this.

Response Handlers

A Respond object (declared using {kind: "respond"}) indicates the desire to handle the response to an Instruction using a ResponseHandler object.

import {
    Parameter, ParseJson, ResponseHandler, Tags,
} from "@atomist/rug/operations/Decorators";

import {
    ChannelAddress, DirectedMessage, EventPlan, HandlerContext,
    HandleResponse, Response, ResponseMessage,
} from "@atomist/rug/operations/Handlers";

@ResponseHandler("GenericSuccessHandler", "displays a success message in chat")
@Tags("success")
class GenericSuccessHandler implements HandleResponse<any> {

    @Parameter({ description: "Success msg", pattern: "@any" })
    public msg: string;

    public handle( @ParseJson response: Response<any>): EventPlan {
        const rand = new ChannelAddress("#random");
        const result = `${this.msg}: ${response.body.status}`;
        return new EventPlan().add(new DirectedMessage(result, rand));
    }
}

Response handlers are declared with the ResponseHandler decorator and the class must implement the handle(response: Response<T>): EventPlan | CommandPlan method of the HandleResponse<T> interface. When the handler receives a JSON payload, you can benefit from automatic JSON deserialization into an object by decorating the response parameter with the @ParseJson decorator, for example handle(@ParseJson response: Response<T>): CommandPlan.

Response Handlers must also return a plan, just like any handler. However, they must return the appropriate type of plan for the execution chain in which they respond. So for an Event Handler they must return an EventPlan, and for a Command Handler they must return a CommandPlan.

It’s all asynchronous

It is important to appreciate that because handlers return plans, the actions declared in that plan are executed asynchronously. In other words, plans are just data the Rug runtime knows how to interpret but your handler cannot invoke those Rugs directly. So it’s not safe to make assumptions about when instructions in a plan will be run, although it is fair to say that the Atomist platform will do its best to apply them as soon as possible.