Published by Rahul Mody - December 11, 2018

Building Slack apps to run custom Heroku commands

Enable admin access in Heroku review apps to improve your software delivery process.

Engineering at Calendly

We use Heroku Review Apps to test code before it ships to end users. Each of these review apps is initialized with a new environment and a fresh database. A lot of the time, anyone involved with reviewing the changes—developers, product managers, and QA analysts—need to grant themselves certain admin access in the app. Historically, to do this, they would run a piece of code in their terminal. Over the course of every deploy this proved to be tedious and time-consuming.

Solution: Custom Slack app to run custom Heroku commands

We needed a solution to perform these tasks that was quick to run, did not require any technical knowledge and minimized human error. Slack’s easy-to-use Slash Commands was the best interface to accomplish our goal. In order to build out this functionality, here is everything we did to enable admin access in a review app via Slack Slash Commands:

  1. Set up a Slack App with Slash Commands
  2. Connect Heroku to the Slack App
  3. Accept a Slash Command request
  4. Process a Slash Command request
  5. Return a Slack message

Setting up a Slack app with commands

Visit Slack’s API site to create a Slack application and find documentation.

For the use case at hand—enabling admin access in review apps—we need to create a Slack Slash Command with a uniquely identifiable and easy to remember name, /review_app_access. A Slash Command simply makes a post request to a specified url. We make the “Request URL” the domain that hosts our application and then specify the route that will handle the post request: https://yourhostedslackapp.com/hooks/slack. In our Slack application, we use /hooks/slack to handle all Slash Commands as detailed in a later section below. The last fields let Slack users know what this command does and what parameters it accepts.

Connecting Heroku to your Slack app

In order to run commands on Heroku, we leverage the Heroku PlatformAPI, more specifically the platform-api ruby gem.

We create a self-contained module to instantiate a Heroku client and run commands on the Heroku review app. We create a new one-off dyno that executes a command on the review app’s console. In a later section, Processing a Slash Command Request, we will use run_command to create an admin role for a user.

Diagram of flow between Slack, our Custom Slack app, and Heroku.

  #/services/heroku_service.rb

  module SlackLine::Services
    module HerokuService extend self

      def run_command(app:, command:)
        client.dyno.create(app, {:command => command})
      end

      private
      def client
        @client ||= ::PlatformAPI.connect(ENV['HEROKU_API_KEY'])
      end
    end
  end

Accepting a Slash Command request

When a Slash Command is triggered, we need to be able to handle that post request. We built our custom Slack application using Sinatra.


  # slack_line_app.rb

  class SlackLineApp < Sinatra::Base
    post '/hooks/slack' do
  	  ...
    end
  end

For security measures, we want to check that the request is coming from our Slack workspace using the Slack app we set up. You can find the verification token in the “App Credentials” section of the Slack app you added to your workspace.


  # slack_line_app.rb

  halt 403 unless SlackLine::Services::SlackService.valid_signature?(params)

  # /services/slack_service.rb

  def valid_signature?(request)
    return request['token'] == ENV['VERIFICATION_TOKEN']
  end

Next, we’ll want to process the request event and return the response generated when running our Heroku command. As long as the signature of the request is valid, our response will always return 200. Putting it all together we get:


  # slack_line_app.rb

  class SlackLineApp < Sinatra::Base
    post '/hooks/slack' do
      halt 403 unless SlackLine::Services::SlackService.valid_signature?(params)    
    
      response = SlackLine::Services::SlackService.process_event(params)
      
      status 200
      content_type :json
      response ? response.to_json : nil
    end
  end

Processing a Slash Command request

We use process_event to catch all Slack Slash Commands, so we implemented a case statement for each command. For enabling admin access, we need two things from the end user, the pull request (PR) number assigned by Github and the email of the user that admin access will be enabled for.


  # /services/slack_service.rb

  def process_event(event)
    case event['command']
  	...
    when '/review_app_access'
      arguments = event['text'].split(' ')
      SlackLine::Commands::AdminAccess.review_app_access(pr: arguments[0], email: arguments[1])
    end
  end

When the /review_app_access Slash Command is called, we will eventually call upon review_app_access to validate and enable admin access in the review app. The first argument, PR number, determines which review app we need to work with. At Calendly, we set up our review app names based on the PR number using the HEROKU_APP_NAME config variable, which we dynamically set each time a review app is initialized. The second argument, email, identifies the user we want to enable admin access for on the review app.


  # /commands/admin_access.rb
  
  module SlackLine::Commands
    module AdminAccess extend self
      def review_app_access(pr:, email:)
        review_app = "calendly-qa-pr-#{pr}"
        begin
          heroku.run_command(app: review_app, command: give_access_command(email))
          presenter.access_granted(app: review_app, email: email)
        rescue
          presenter.access_denied(app: review_app, email: email, reason: 'Unknown Error')
        end
      end
      
      def presenter
        SlackLine::Presenters::AdminAccessPresenter
      end
      
      def heroku
        SlackLine::Services::HerokuService
      end
      
      def give_access_command(email)
  	    # rails command to be run on review app console giving admin access
        "rails r \\"User.find_by_email(email).create_admin_role(...)\""
      end 
    end
  end

Returning a Slack message

If the line that executes the Heroku command, heroku.run_command(...), is successful, then we respond with an access granted message, otherwise, we return an access denied message. The presenters build a hash object based on Slack’s docs for creating messages to respond back to the Slack user:


  # /presenters/admin_access_presenter.rb

  module SlackLine::Presenters
    module AdminAccessPresenter extend self

      def access_granted(app:, email:)
        {
          :text => '',
          :attachments => [{
            :title => 'Access Granted',
            :text => 'It will take about 1 minute for the change to be reflected.',
            :fields => [{
              :title => 'User',
              :value => email,
              :short => true
            }, {
              :title => 'App',
              :value => "<https://#{app}.herokuapp.com|#{app}>",
              :short => true
            }],
            :color => 'good'
          }]
        }
      end


      def access_denied(app:, email:, reason: '')
        {
          :text => '',
          :attachments => [{
            :title => 'Access Denied',
            :text => reason,
            :fields => [{
              :title => 'User',
              :value => email,
              :short => true
            }, {
              :title => 'App',
              :value => "<https://#{app}.herokuapp.com|#{app}>",
              :short => true
            }],
            :color => 'danger'
          }]
        }
      end

    end
  end

Here’s how these responses are rendered in Slack:

Wrapping up, this hash object from the presenter is converted to json and returned as the response to the post request, as described in the prior section.


  # slack_line_app.rb

  class SlackLineApp < Sinatra::Base
    post '/hooks/slack' do
      ...
      response = SlackLine::Services::SlackService.process_event(params)
      ...
      response ? response.to_json : nil
    end
  end

Parting thoughts

Although we focused on a particular task here, we can use much of the same code to automate many other tasks when working with Heroku applications. We can create seed data on the fly based on specific test cases or quickly enable sets of feature flags. There are endless use cases with Slack apps that can make everyone at your organization more efficient, especially when managing your software development processes.

Have you used Slack apps to improve your software delivery process? Comment below and tell us about the interesting ways you’ve leveraged Slack’s API.