Skip to content

Commit

Permalink
Introduce Heroku Pipelines support
Browse files Browse the repository at this point in the history
Why:

* We have observed that running acceptance through Heroku Pipelines is a
  better flow for code reviews, by allowing to show work in progress
  earlier, and fixing issue before merging into master

This change addresses the need by:

* Creating a pipeline for the new app, and adding the staging and
  production environments to it
* Adding a required `app.json` configuration file
* Setting `Rack::CanonicalHost` only in staging and production, and not
  review apps (otherwise review apps redirect to staging)
* Providing script for setting up database and worker in review apps
* FakeHeroku pretends the pipelines plugin is installed
  • Loading branch information
Tute Costa, Luís Ferreira authored and tute committed Dec 30, 2015
1 parent c1acc82 commit d53de08
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 38 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ Then run:

This will create a Rails app in `projectname` using the latest version of Rails.

### Associated services

* Enable [Circle CI](https://circleci.com/) Continuous Integration
* Enable [GitHub auto deploys to Heroku staging and review
apps](https://dashboard.heroku.com/apps/app-name-staging/deploy/github).

## Gemfile

To see the latest and greatest gems, look at Suspenders'
Expand Down Expand Up @@ -128,9 +134,11 @@ This:
* Adds the [Rails Stdout Logging][logging-gem] gem
to configure the app to log to standard out,
which is how [Heroku's logging][heroku-logging] works.
* Creates a [Heroku Pipeline] for review apps

[logging-gem]: https://github.com/heroku/rails_stdout_logging
[heroku-logging]: https://devcenter.heroku.com/articles/logging#writing-to-your-log
[Heroku Pipeline]: https://devcenter.heroku.com/articles/pipelines

You can optionally specify alternate Heroku flags:

Expand Down
2 changes: 1 addition & 1 deletion bin/setup
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/sh

# Run this script immediately after cloning the codebase.

Expand Down
56 changes: 54 additions & 2 deletions lib/suspenders/adapters/heroku.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ def initialize(app_builder)

def set_heroku_remotes
remotes = <<-SHELL.strip_heredoc
# Set up the staging and production apps.
#{command_to_join_heroku_app('staging')}
#{command_to_join_heroku_app('production')}
git config heroku.remote staging
SHELL

Expand All @@ -25,6 +24,20 @@ def set_up_heroku_specific_gems
)
end

def create_staging_heroku_app(flags)
rack_env = "RACK_ENV=staging RAILS_ENV=staging"
app_name = heroku_app_name_for("staging")

run_toolbelt_command "create #{app_name} #{flags}", "staging"
run_toolbelt_command "config:add #{rack_env}", "staging"
end

def create_production_heroku_app(flags)
app_name = heroku_app_name_for("production")

run_toolbelt_command "create #{app_name} #{flags}", "production"
end

def set_heroku_rails_secrets
%w(staging production).each do |environment|
run_toolbelt_command(
Expand All @@ -34,13 +47,52 @@ def set_heroku_rails_secrets
end
end

def provide_review_apps_setup_script
app_builder.template(
"bin_setup_review_app.erb",
"bin/setup_review_app",
force: true,
)
app_builder.run "chmod a+x bin/setup_review_app"
end

def create_heroku_pipelines_config_file
app_builder.template "app.json.erb", "app.json"
end

def create_heroku_pipeline
pipelines_plugin = `heroku plugins | grep pipelines`
if pipelines_plugin.empty?
puts "You need heroku pipelines plugin. Run: heroku plugins:install heroku-pipelines"
exit 1
end

heroku_app_name = app_builder.app_name.dasherize
%w(staging production).each do |environment|
run_toolbelt_command(
"pipelines:create #{heroku_app_name} -a #{heroku_app_name}-#{environment} --stage #{environment}",
environment,
)
end
end

def set_heroku_serve_static_files
%w(staging production).each do |environment|
run_toolbelt_command(
"config:add RAILS_SERVE_STATIC_FILES=true",
environment,
)
end
end

private

attr_reader :app_builder

def command_to_join_heroku_app(environment)
heroku_app_name = heroku_app_name_for(environment)
<<-SHELL
if heroku join --app #{heroku_app_name} &> /dev/null; then
git remote add #{environment} git@heroku.com:#{heroku_app_name}.git || true
printf 'You are a collaborator on the "#{heroku_app_name}" Heroku app\n'
Expand Down
47 changes: 14 additions & 33 deletions lib/suspenders/app_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ class AppBuilder < Rails::AppBuilder
extend Forwardable

def_delegators :heroku_adapter,
:create_heroku_pipelines_config_file,
:create_heroku_pipeline,
:create_production_heroku_app,
:create_staging_heroku_app,
:provide_review_apps_setup_script,
:set_heroku_rails_secrets,
:set_heroku_remotes,
:set_up_heroku_specific_gems,
:set_heroku_rails_secrets
:set_heroku_serve_static_files,
:set_up_heroku_specific_gems

def readme
template 'README.md.erb', 'README.md'
Expand Down Expand Up @@ -69,7 +75,7 @@ def configure_quiet_assets
end

def provide_setup_script
template "bin_setup.erb", "bin/setup", force: true
template "bin_setup", "bin/setup", force: true
run "chmod a+x bin/setup"
end

Expand Down Expand Up @@ -130,14 +136,18 @@ def configure_smtp
def enable_rack_canonical_host
config = <<-RUBY
if ENV.fetch("HEROKU_APP_NAME", "").include?("staging-pr-")
ENV["APPLICATION_HOST"] = ENV["HEROKU_APP_NAME"] + ".herokuapp.com"
end
# Ensure requests are only served from one, canonical host name
config.middleware.use Rack::CanonicalHost, ENV.fetch("APPLICATION_HOST")
RUBY

inject_into_file(
"config/environments/production.rb",
config,
after: serve_static_files_line
after: "Rails.application.configure do",
)
end

Expand Down Expand Up @@ -369,31 +379,11 @@ def init_git
run 'git init'
end

def create_staging_heroku_app(flags)
rack_env = "RACK_ENV=staging RAILS_ENV=staging"
app_name = heroku_app_name_for("staging")

run_heroku "create #{app_name} #{flags}", "staging"
run_heroku "config:add #{rack_env}", "staging"
end

def create_production_heroku_app(flags)
app_name = heroku_app_name_for("production")

run_heroku "create #{app_name} #{flags}", "production"
end

def create_heroku_apps(flags)
create_staging_heroku_app(flags)
create_production_heroku_app(flags)
end

def set_heroku_serve_static_files
%w(staging production).each do |environment|
run_heroku "config:add RAILS_SERVE_STATIC_FILES=true", environment
end
end

def provide_deploy_script
copy_file "bin_deploy", "bin/deploy"

Expand All @@ -413,7 +403,6 @@ def provide_deploy_script
end

def configure_automatic_deployment
staging_remote_name = heroku_app_name_for("staging")
deploy_command = <<-YML.strip_heredoc
deployment:
staging:
Expand Down Expand Up @@ -518,20 +507,12 @@ def raise_on_missing_translations_in(environment)
uncomment_lines("config/environments/#{environment}.rb", config)
end

def run_heroku(command, environment)
run "heroku #{command} --remote #{environment}"
end

def heroku_adapter
@heroku_adapter ||= Adapters::Heroku.new(self)
end

def serve_static_files_line
"config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?\n"
end

def heroku_app_name_for(environment)
"#{app_name.dasherize}-#{environment}"
end
end
end
3 changes: 3 additions & 0 deletions lib/suspenders/generators/app_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,12 @@ def create_heroku_apps
if options[:heroku]
say "Creating Heroku apps"
build :create_heroku_apps, options[:heroku_flags]
build :provide_review_apps_setup_script
build :set_heroku_serve_static_files
build :set_heroku_remotes
build :set_heroku_rails_secrets
build :create_heroku_pipelines_config_file
build :create_heroku_pipeline
build :provide_deploy_script
build :configure_automatic_deployment
end
Expand Down
20 changes: 20 additions & 0 deletions spec/features/heroku_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"production",
"SECRET_KEY_BASE",
)
expect(FakeHeroku).to have_setup_pipeline_for(app_name)

bin_setup_path = "#{project_path}/bin/setup"
bin_setup = IO.read(bin_setup_path)
Expand All @@ -29,6 +30,14 @@
expect(bin_setup).to include("git config heroku.remote staging")
expect(File.stat(bin_setup_path)).to be_executable

bin_setup_path = "#{project_path}/bin/setup_review_app"
bin_setup = IO.read(bin_setup_path)

expect(bin_setup).to include("heroku run rake db:migrate --app #{app_name}-staging-pr-$1")
expect(bin_setup).to include("heroku ps:scale worker=1 --app #{app_name}-staging-pr-$1")
expect(bin_setup).to include("heroku restart --app #{app_name}-staging-pr-$1")
expect(File.stat(bin_setup_path)).to be_executable

bin_deploy_path = "#{project_path}/bin/deploy"
bin_deploy = IO.read(bin_deploy_path)

Expand All @@ -51,6 +60,17 @@
- bin/deploy staging
YML
end

it "adds app.json file" do
expect(File).to exist("#{project_path}/app.json")
end

it "includes application name in app.json file" do
app_json_file = IO.read("#{project_path}/app.json")
app_name = SuspendersTestHelpers::APP_NAME.dasherize

expect(app_json_file).to match(/"name":"#{app_name}"/)
end
end

context "--heroku with region flag" do
Expand Down
15 changes: 14 additions & 1 deletion spec/features/new_project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
expect(secrets_file).to match(/secret_key_base: <%= ENV\["SECRET_KEY_BASE"\] %>/)
end

it "adds bin/setup file" do
expect(File).to exist("#{project_path}/bin/setup")
end

it "makes bin/setup executable" do
bin_setup_path = "#{project_path}/bin/setup"

expect(File.stat(bin_setup_path)).to be_executable
end

it "adds support file for action mailer" do
expect(File).to exist("#{project_path}/spec/support/action_mailer.rb")
end
Expand Down Expand Up @@ -102,7 +112,6 @@

it "evaluates en.yml.erb" do
locales_en_file = IO.read("#{project_path}/config/locales/en.yml")
app_name = SuspendersTestHelpers::APP_NAME

expect(locales_en_file).to match(/application: #{app_name.humanize}/)
end
Expand Down Expand Up @@ -185,6 +194,10 @@
expect(File).to exist("#{project_path}/spec/factories.rb")
end

def app_name
SuspendersTestHelpers::APP_NAME
end

def analytics_partial
IO.read("#{project_path}/app/views/application/_analytics.html.erb")
end
Expand Down
8 changes: 8 additions & 0 deletions spec/support/fake_heroku.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ def initialize(args)
end

def run!
if @args.first == "plugins"
puts "heroku-pipelines@0.29.0"
end
File.open(RECORDER, 'a') do |file|
file.puts @args.join(' ')
end
Expand Down Expand Up @@ -39,6 +42,11 @@ def self.has_configured_vars?(remote_name, var)
commands_ran =~ /^config:add #{var}=.+ --remote #{remote_name}\n/
end

def self.has_setup_pipeline_for?(app_name)
commands_ran =~ /^pipelines:create #{app_name} -a #{app_name}-staging --stage staging/ &&
commands_ran =~ /^pipelines:create #{app_name} -a #{app_name}-production --stage production/
end

def self.commands_ran
@commands_ran ||= File.read(RECORDER)
end
Expand Down
39 changes: 39 additions & 0 deletions templates/app.json.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name":"<%= app_name.dasherize %>",
"scripts":{},
"env":{
"AIRBRAKE_API_KEY":{
"required":true
},
"EMAIL_RECIPIENTS":{
"required":true
},
"RACK_ENV":{
"required":true
},
"RAILS_ENV":{
"required":true
},
"SECRET_KEY_BASE":{
"generator":"secret"
},
"SMTP_ADDRESS":{
"required":true
},
"SMTP_DOMAIN":{
"required":true
},
"SMTP_PASSWORD":{
"required":true
},
"SMTP_USERNAME":{
"required":true
},
"WEB_CONCURRENCY":{
"required":true
}
},
"addons":[
"heroku-postgresql"
]
}
2 changes: 1 addition & 1 deletion templates/bin_setup.erb → templates/bin_setup
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/sh

# Set up Rails app. Run this script immediately after cloning the codebase.
# https://github.com/thoughtbot/guides/tree/master/protocol
Expand Down
Loading

0 comments on commit d53de08

Please sign in to comment.