Command Handlers

Lowering the barrier for communication and information flow makes teams more productive. Open Source projects have long relied on chat to help them operate and coordinate contributors and project lifecycle. Modern chat solutions such as Slack, which can integrate deeply in your ecosystem, have improved on those foundations and have proven to be fantastic hubs for teams to drive automation with simple chat commands.

Common scenarios, for instance cutting a new release, or rolling back a broken deployment. Additionally, like other Rugs, command handlers can also dive into the code of project as a result of a bot interaction. In other words, not only can you use Rug commands to list open issues on a project but the team can also query the project’s code or even run other Rugs against the project, all of this from the project’s chat channel.

Anatomy of a Command Handler

Rug commands handlers are the interface to add new skills to the Atomist bot. These handlers are appropriate when you want to either query your project or perform an action where your team communicates about the project.

Chat commands are declared in Rug command handlers. Command handlers handle commands coming from users via the Atomist bot.

Depending on their goal, Rug command handler implementations are stored either alongside 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 Command Handler

Suppose we want to open a new GitHub issue:

 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
44
45
46
import { Issue } from "@atomist/cortex/Issue";
import {
    CommandHandler, Intent, MappedParameter, Parameter, Secrets, Tags,
} from "@atomist/rug/operations/Decorators";
import {
    CommandPlan, CommandRespondable, Execute, HandleCommand, HandlerContext,
    MappedParameters,
} from "@atomist/rug/operations/Handlers";
import { handleErrors } from "@atomist/rugs/operations/CommonHandlers";

@CommandHandler("CreateGitHubIssue", "Create an issue on GitHub")
@Tags("github", "issues")
@Secrets("github://user_token?scopes=repo")
@Intent("create issue")
class CreateIssueCommand implements HandleCommand {

    @Parameter({ description: "The issue title", pattern: "^.*$" })
    public title: string;

    @Parameter({ description: "The issue body", pattern: "^.*(?m)$" })
    public body: string;

    @MappedParameter(MappedParameters.GITHUB_REPOSITORY)
    public repo: string;

    @MappedParameter(MappedParameters.GITHUB_REPO_OWNER)
    public owner: string;

    @MappedParameter("atomist://correlation_id")
    public corrid: string;

    public handle(ctx: HandlerContext): CommandPlan {
        const plan = new CommandPlan();
        const execute: CommandRespondable<Execute> = {
            instruction: {
                kind: "execute",
                name: "create-github-issue",
                parameters: this,
            },
        };
        plan.add(handleErrors(execute, this));
        return plan;
    }
}

export const create = new CreateIssueCommand();

This command handler follows the same programming model as other Rugs, so it should look familiar.

Declaration

The first lines group the Rug typing imports which, provide interfaces and decorators to implement and declare your handlers. The CreateIssueCommand class expresses how the command is invoked, as well as how to handle the error scenario. GenericErrorHandler is a Response Handlers to handle the failure case of the create-github-issue execute instruction.

We declare our handler through decorators. The first argument of the @CommandHandler decorator is the name of the command, the second is its description. These make the handlers visible and discoverable.

Intent

A Rug command handler can have associated intent that users can send when talking with the Atomist bot. The intent is described using the @Intent decorator (line 20). Whenever a user sends the @atomist create issue message to the Atomist bot, the Rug runtime runs the CreateGitHubIssue command handler. Declaring Intent is the way we declare the commands that are made availble to users in chat.

Command Handlers invoking Command Handlers

Most of the time, it makes sense to add the @Intent decorator. However, it’s also possible to invoke Command Handlers from Command Handlers by adding them to the Plan (more on this later), so for these handlers, it might not make sense to expose them directly to chat users as commands.

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.

Implementation

You define the class which implements your command handler (line 21). The class is exported so that it can referenced from unit tests. A command handler implements the HandleCommand interface. This interface requires the handle(command: HandlerContext): CommandPlan method to be implemented. It is a convention for the command handler and the class that defines it to have the same name.

The handle method takes a single argument, a HandlerContext instance. This gives you access to a path expression engine to query your organization’s projects. The method must return a CommandPlan.

Parameters

Rug command handlers can take parameters like other Rugs. Parameters are declared using the @Parameter decorator. The decorated variable names the parameter. If you assign a value to that variable, it becomes the parameter’s default value. The @Parameter decorator adds additional metadata via a single argument: a JavaScript object whose properties are documented in the conventions. Though the only mandatory property is pattern. It is highly recommended to also set description, displayName and validInput in order to help other users when invoking Rugs via the Atomist bot.

Mapped Parameters

Rug command handlers can define what are called Mapped Parameters to receive relevant contextual information when invoked via the Atomist bot. Mapped Parameters, declared using the @MappedParameter decorator, behave much like ordinary Parameters declared by the @Parameter decorator, but are defined and provided by an external system, in most cases, the Atomist Bot itself.

For example, the "atomist://github/repository/owner" Mapped Parameter will be populated with the name of owner (user or organization) of the GitHub repository associated with a particular chat channel.

A TypeScript helper class called MappedParameters exists to aid discovery and use of Mapped Parameters. It lives in an NPM module at @atomist/rug/operations/Handlers.ts:

abstract class MappedParameters {
  static readonly GITHUB_REPO_OWNER: string = "atomist://github/repository/owner"
  static readonly GITHUB_REPOSITORY: string = "atomist://github/repository"
  static readonly SLACK_CHANNEL: string = "atomist://slack/channel"
  static readonly SLACK_TEAM: string = "atomist://slack/team"
  static readonly SLACK_USER: string = "atomist://slack/user"
  static readonly GITHUB_WEBHOOK_URL: string = "atomist://github_webhook_url"
}

CommandPlans

A CommandPlan describes the actions to be taken by the Rug runtime on behalf of the handler. CommandPlans are composed of Messages and/or respondables. Respondables instruct the rug runtime to immediately perform ordinary rug operations, whereas messages are sent to the Atomist Bot for display to the user.

Messages

A Message represents presentable content to be rendered in chat by the Atomist Bot to a chat channel or user. 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);
    }

Response Messages

A Response Message is much like a DirectedMesssage, except that the destination for the message is optional because the Atomist Bot chooses the channel or user to respond to based on where the command was invoked in the chat system. As such, it really only makes sense to return Response Messages in Plans returned from Command Handlers.

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

    handle(command: HandlerContext): CommandPlan {
        return CommandPlan.ofMessage(new ResponseMessage("Hello world!"))
    }

In addition to the default recipient, we can add one or more MessageAddressto the message to send the same message to other chat users or channels.

handle(response: Response<any>): CommandPlan {
    let message  = new ResponseMessage("Hello other world!");
    message.addAddress(new UserAddress("@bob"))
    return CommandPlan.ofMessage(message);
}

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 json = {
            "text": "I am a test message https://www.atomist.com",
            "attachments": [{
               "text": "And here’s an attachment!"
             }]
        }
        const msgg = new ResponseMessage(JSON.stringify(json) , MessageMimeTypes.SLACK_JSON)
        return CommandPlan.ofMessage(message)
    }

Respondables

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

const plan = new CommandPlan();
plan.add(
    {
        instruction: {
            kind: "execute",
            name: "create-github-issue",
            parameters: this
        },
        onError: {
            kind: "respond",
            name: "GenericErrorHandler",
            parameters: this
        },
        onSuccess: new ResponseMesssage("Successfully created issue")
    }
)
plan.add(handleErrors(exec, this))
return plan;

The example above shows how send a message back to the user or channel that invoked the command onSuccess or to invoke the GenericErrorHandler Response Handler if creation fails.

Instructions

Instructions in an CommandRespondable have the following properties:

  • kind: "generate" | "edit" | "execute" | "command": 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"), a Rug Funtion ("execute") or even another Command Handler.

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.

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.