Streamlining setup for Heroku review apps

For web applications running on Heroku, Heroku’s review app functionality is an invaluable way of testing changes introduced in a pull request before they’re merged and deployed to production.

Review apps are primarily configured via an app.json file that you commit to the root of your project’s GitHub repo. This file tells Heroku how to build any new review apps (in response to a pull request being opened), including any add-ons to provision, environment variables to set, or commands to run once the app has been deployed.

In most cases configuring a new review app via app.json is good enough, but in a recent project I realized I needed more control over the process of spinning up a new review app than I was able to wring out of my app’s app.json file. With the app in question being a Ruby app, I turned to Heroku’s Platform API gem.

Setting up a new Heroku review app for the project required three steps to be completed:

  • Setting the required environment variables
  • Creating, migrating, and seeding the database
  • Setting up a handful of Elasticsearch indices and indexing the contents of the database

For most of the environment variables the app required I could either set these in app.json directly or configure them to be inherited from the parent app in the pipeline. The difficulty I had was that some of these variables needed to reference the name of the app itself, for example, CANONICAL_URL which needed to be set to the full canonical URL at which the app could be accessed (i.e. https://#{heroku_app_name}.herokuapp.com). The obvious issue here is that there’s of course no way to access the name of the app before it has been created, so it wasn’t going to be possible to set these environment variables in app.json. This meant I needed a better approach.

After adding the platform-api gem to the app’s Gemfile, I next needed to generate a token for accessing Heroku’s Platform API by running:

heroku authorizations:create -d "Token for My Awesome App"

I figured the best way of invoking the various commands needed to get a new review app into a usable state was via a rake task, so I added a postdeploy script to my app.json file to run this task:

{
  "name": "my-awesome-app",
  "scripts": {
    "postdeploy": "bundle exec rake review_app:setup"
  },
}

The postdeploy script is automatically run once a newly-created review app is successfully built, and is run only once (i.e. it’s not re-run on subsequent deploys) so utilizing it in this way here was perfect for my needs.

Next was to set up the rake task, starting with the logic to set each of the environment variables that needed access to the name of the Heroku app. To make this process easier, Heroku makes two special environment variables available to review apps (HEROKU_APP_NAME and HEROKU_PARENT_APP_NAME), provided you specify these as required in your app.json like so:

{
  "name": "my-awesome-app",
  "scripts":{
    "postdeploy": "bundle exec rake review_app:setup"
  },
  "env":{
    "HEROKU_APP_NAME": {
      "required": true
    },
    "HEROKU_PARENT_APP_NAME": {
      "required": true
    }
  }
}

Having access to HEROKU_APP_NAME made setting the environment variables I needed a breeze:

# Four env vars (APP_DOMAIN, ASSETS_SERVER_URL, CANONICAL_URL, and ORIGIN_URL) reference the app's URL, so these need
# to be updated to use the correct URL for each review app

task update_heroku_app_env_vars: :system do
  heroku_token = ENV["HEROKU_PLATFORM_TOKEN"]
  heroku = PlatformAPI.connect_oauth(heroku_token)
  heroku_app_name = ENV["HEROKU_APP_NAME"]

  heroku.config_var.update(heroku_app_name, "APP_DOMAIN" => "#{heroku_app_name}.herokuapp.com")
  heroku.config_var.update(heroku_app_name, "ASSETS_SERVER_URL" => "https://#{heroku_app_name}.herokuapp.com")
  heroku.config_var.update(heroku_app_name, "CANONICAL_URL" => "https://#{heroku_app_name}.herokuapp.com")
  heroku.config_var.update(heroku_app_name, "ORIGIN_URL" => "https://#{heroku_app_name}.herokuapp.com")
end

I then fleshed out my review_app:setup rake task with the other logic that I wanted to be run whenever a new review app is created:

namespace :review_app do
  # This task is invoked as a postdeploy script in app.json, which is triggered when a new Heroku review app is created
  # (once the 'release' commands defined in Procfile have been executed)
  #
  # - Updates relevant ENV vars
  # - Seeds the database
  # - Resets the Elasticsearch index

  task :setup do
    Rake::Task["review_app:update_heroku_app_env_vars"].invoke
    Rake::Task["review_app:seed_database"].invoke
    Rake::Task["review_app:reset_search"].invoke
  end

  task update_heroku_app_env_vars: :system do
    heroku_token = ENV["HEROKU_PLATFORM_TOKEN"]
    heroku = PlatformAPI.connect_oauth(heroku_token)
    heroku_app_name = ENV["HEROKU_APP_NAME"]

    heroku.config_var.update(heroku_app_name, "APP_DOMAIN" => "#{heroku_app_name}.herokuapp.com")
    heroku.config_var.update(heroku_app_name, "ASSETS_SERVER_URL" => "https://#{heroku_app_name}.herokuapp.com")
    heroku.config_var.update(heroku_app_name, "CANONICAL_URL" => "https://#{heroku_app_name}.herokuapp.com")
    heroku.config_var.update(heroku_app_name, "ORIGIN_URL" => "https://#{heroku_app_name}.herokuapp.com")
  end

  task :seed_database do
    system "bundle exec rake db:seed"
  end

  task :reset_search do
    system "bundle exec rake search:reset"
  end
end

Now when I open a pull request a new review app is created with all of the required configuration set and setup tasks run automatically, leaving me to get straight into testing my changes rather than having to muck around with setting things up manually in the Heroku dashboard or console.