Are they scripts, modules, packages or plugins?

  • It’s a Hubot Script if it lives in the ./scripts directory, or loaded through the hubot-scripts Node.js package.
  • It’s a Hubot Script Package if it was loaded through external-scripts.json (thereby installing it by adding it to package.json).

I’ll shorten it to “Hubot package” or just “package” for this writing.

1) Create a Hubot Script Package

Doing this requires a handful of things, but it really is the best approach. It offers maximum flexibility and is now the preferred way to create new functionality by Hubot’s creators.

Update 6/11/2015: This has been converted into a Yeoman generator for easier use.

npm install -g yo generator-hubot
mkdir hubot-example
yo hubot:script

2) Test your package locally

  • If you already have a Hubot in development locally, use it. If not, use hubot create to make one.

Update 6/6/2015: It turns out the linking process is easier if you simply use the built-in tools.

  • Run npm install in hubot-your-package repository to grab needed dependencies
  • Run npm link in hubot-your-package repository to create a shim in your node installation
  • Run npm link hubot-your-package in your Hubot directory to tell Hubot to use the shimmed version
  • run bin/hubot -n Hubot to verify that the script still works. (e.g. hubot-hello)

You can now do local development from your original repository.

To make this easier, I also create a local_test.sh script. This also helps with loading in the environment variables for yours and other scripts. It should essentially match what is found in your Procfile or other launcher.

I keep my environment variables in a .env file that is not committed in my repository. Instead, I copy the variables into a .env-dist which is committed into my repository. It is generally a bad idea to put any sensitive credentials (such as API keys) with the repository, even if it is a private one.

This is my launcher script local_test.sh, made executable with chmod +x local_test.sh.

date
echo "Starting Hubot ..."

source ./.env

bin/hubot -n Hubot --alias '!'

So running ./local_test.sh replaces having to type something like source ./.env && bin/hubot -n Hubot every time.

3) Avoid using robot.hear if possible

It seems like a good idea at the time, but it gets annoying fast. For example, if you have a method that looks like:

  robot.hear /where is (.*)\??/i, (msg) ->

It will trigger every time somebody asks Where is the critical issue that everyone is panicking about occurring?. It’s a quick way to get your Hubot process pkill‘d.

Instead, be explicit and use respond:

  robot.respond /where is (.*)\??/i, (msg) ->

This makes the user be required to type something like hubot where is James? or !where is James? to trigger the script.

4) Namespace your commands

There is an odd phenomenon when using a tool like Hubot: Once you add a little functionality, more functionality is desired. Soon, scripts will start to trip over each other.

Take for example a script that has a /show me (.*)/i expression. I have seen at least five different scripts use this exact same expression, so something like this happens:

user> hubot show 123
Hubot> https://github.com/example/example-repo/issues/123
Hubot> https://redmine.example.com/issue/123
Hubot> Error! Could not find an incident for #123

Instead, good namespaces let you declare explicitly your purpose for the script. It’s a handful of extra characters to type, but it has a few main advantages:

  • You don’t get nearly as much noise in your channels
  • Scripts like hubot-help are easier to use to see all the features of a given script if they start with the same namespace
  • Contributors know to follow this pattern when adding new functionality

Now your scripts would work more like:

user> hubot github show 123
Hubot> https://github.com/example/example-repo/issues/123
user> hubot redmine show 123
Hubot> https://redmine.example.com/issue/123
user> hubot pd 123
Hubot> Error! Could not find an incident for "123"

5) In most cases, make ... me ... optional, or not included

Some early example scripts included this pattern, but it does not make much sense in common usage. There are exceptions, however.

user> hubot redmine assign me 123
Hubot> Assigned issue 123 to johndoe@example.com

But things like hubot image me octocat are a bit overkill.

6) Write tests for your scripts

I admit to not doing this much deeper than registering listeners for my methods so that npm test passes and does something somewhat useful.

  it 'registers a respond listener for fitbit', ->
    expect(@robot.respond).to.have.been.calledWith(/fitbit$/i)

  it 'registers a respond listener for fitbit friends', ->
    expect(@robot.respond).to.have.been.calledWith(/fitbit friends/i)

  it 'registers a respond listener for fitbit approve', ->
    expect(@robot.respond).to.have.been.calledWith(/fitbit approve/i)

Update 11/2/2015: Check out this guide on writing tests for your Hubot packages by Andrew Mussey. He has a sample boilerplate repository that is useful for grabbing code snippets.

7) Use robot.brain for mapping users to other identifiers

This is mostly a convenience thing, but it is helpful to let users associate their chat username with an external username.

  # Identify your username with the bot
  robot.respond /(someapi|some_api) ([a-zA-Z0-9]+) as ([0-9]+)/i, (msg) ->
    if msg.match[1] is 'me'
      actor = msg.message.user.name
    else
      actor = msg.match[1].trim()

    user_id = msg.match[2].trim()

    # Save to brain if not set
    if robot.brain.data.someapi[actor]?
      previous = robot.brain.data.someapi[actor]
      msg.send "Cannot save #{actor} as #{user_id} because it already set to '#{previous}'."
      msg.send "Use `#{robot.name} someapi forget #{actor}` to set a new value."
      return;

    robot.brain.data.someapi[actor] = user_id

    msg.send "Ok, I have #{actor} as #{user_id} on SomeAPI."

  # Stop remembering a particular username
  robot.respond /(someapi|some_api) forget ([a-zA-Z0-9]+)/i, (msg) ->

    actor = msg.match[1].trim()

    # Remove from brain if set
    if robot.brain.data.someapi[actor]?
      previous = robot.brain.data.someapi[actor]
      delete robot.brain.data.someapi[actor]
      msg.send "I no longer know #{actor} as #{previous} on SomeAPI."
      return;

    msg.send "I don't know who #{actor} is on SomeAPI."

8) Use Semantic Versioning

Read up on semantic versioning to determine what part of your version number to bump. npm has a handy utility to help with this.

$ git commit -m "Fix bug or other minor issue."
$ npm version patch
$ npm publish
$ git commit -m "Add a feature that is backwards compatible."
$ npm version minor
$ npm publish
$ git commit -m "Introduce a major feature is not backwards compatible."
$ npm version major
$ npm publish

As an additional step, you should create a GitHub Release or equivalent when bumping the version so anyone implementing your script package will be able to see what changed.

9) Handle missing credentials and API errors gracefully

Your script package does not want to be the one that crashes Hubot, particularly if it is not running properly as a service on the hosting provider and needs to be manually restarted.

The first thing to consider is if your script requires an environment variable, such as an API key, you should give useful error messages.

  # Check for required config
  missingEnvironmentForApi = (msg) ->
    missingAnything = false
    unless process.env.SOMEAPI_CLIENT_ID?
      msg.send "SomeAPI Client ID is missing: Ensure that SOMEAPI_CLIENT_ID is set."
      missingAnything |= true
    unless process.env.SOMEAPI_CLIENT_SECRET?
      msg.send "SomeAPI Client Secret is missing: Ensure that SOMEAPI_CLIENT_SECRET is set."
      missingAnything |= true
    unless process.env.SOMEAPI_ACCESS_TOKEN?
      msg.send "SomeAPI Access Token is missing: Ensure that SOMEAPI_ACCESS_TOKEN is set."
      missingAnything |= true
    missingAnything

Then add missingEnvironmentForApi msg to the top of any method that accesses the API.

Beyond that, you should also avoid executing code that may throw a JavaScript error. The most common is trying to access an undefined property. Here is an example of code that is not very fault tolerant:

  api.getActiveCampaigns args, (err, data) ->
    msg.send "#{data.some.key}"

In this case, the API gives an err object if something goes wrong. You could additionally check to make sure the data returned has the desired key.

  api.getActiveCampaigns args, (err, data) ->
    if err?
      msg.send err
      return

    if data.some_key?
      msg.send "#{data.some_key}"
    else
      msg.send "Error: Did not get expected data."

10) Use the robot.logger.* methods

You can declare an environment variable called HUBOT_LOG_LEVEL to one of four values:

  • HUBOT_LOG_LEVEL=info
  • HUBOT_LOG_LEVEL=warning
  • HUBOT_LOG_LEVEL=error
  • HUBOT_LOG_LEVEL=debug

When building your script package, using these will help trace where issues may be occurring and to inspect the full response. In most cases, this can (and should) be used instead of leaving console.log in your final package.

  api.getActiveCampaigns args, (err, data) ->
    robot.logger.debug "Received API response:" 
    robot.logger.debug response