From d53de08ef0b7588c5916585916b890e417741dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tute=20Costa=2C=20Lu=C3=ADs=20Ferreira?= Date: Fri, 23 Oct 2015 14:42:44 -0400 Subject: [PATCH] Introduce Heroku Pipelines support 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 --- README.md | 8 ++++ bin/setup | 2 +- lib/suspenders/adapters/heroku.rb | 56 +++++++++++++++++++++- lib/suspenders/app_builder.rb | 47 ++++++------------ lib/suspenders/generators/app_generator.rb | 3 ++ spec/features/heroku_spec.rb | 20 ++++++++ spec/features/new_project_spec.rb | 15 +++++- spec/support/fake_heroku.rb | 8 ++++ templates/app.json.erb | 39 +++++++++++++++ templates/{bin_setup.erb => bin_setup} | 2 +- templates/bin_setup_review_app.erb | 19 ++++++++ 11 files changed, 181 insertions(+), 38 deletions(-) create mode 100644 templates/app.json.erb rename templates/{bin_setup.erb => bin_setup} (96%) create mode 100644 templates/bin_setup_review_app.erb diff --git a/README.md b/README.md index 8f0ddeb96..ff5f2bf6a 100644 --- a/README.md +++ b/README.md @@ -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' @@ -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: diff --git a/bin/setup b/bin/setup index 19471f48c..9e6d7684b 100755 --- a/bin/setup +++ b/bin/setup @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/bin/sh # Run this script immediately after cloning the codebase. diff --git a/lib/suspenders/adapters/heroku.rb b/lib/suspenders/adapters/heroku.rb index 0106c1523..cba333000 100644 --- a/lib/suspenders/adapters/heroku.rb +++ b/lib/suspenders/adapters/heroku.rb @@ -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 @@ -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( @@ -34,6 +47,44 @@ 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 @@ -41,6 +92,7 @@ def set_heroku_rails_secrets 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' diff --git a/lib/suspenders/app_builder.rb b/lib/suspenders/app_builder.rb index 32e9ed7ec..189b863a8 100644 --- a/lib/suspenders/app_builder.rb +++ b/lib/suspenders/app_builder.rb @@ -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' @@ -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 @@ -130,6 +136,10 @@ 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 @@ -137,7 +147,7 @@ def enable_rack_canonical_host inject_into_file( "config/environments/production.rb", config, - after: serve_static_files_line + after: "Rails.application.configure do", ) end @@ -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" @@ -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: @@ -518,10 +507,6 @@ 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 @@ -529,9 +514,5 @@ def heroku_adapter 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 diff --git a/lib/suspenders/generators/app_generator.rb b/lib/suspenders/generators/app_generator.rb index f8adfde80..a3dea96da 100644 --- a/lib/suspenders/generators/app_generator.rb +++ b/lib/suspenders/generators/app_generator.rb @@ -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 diff --git a/spec/features/heroku_spec.rb b/spec/features/heroku_spec.rb index a4f49f055..976b0cef8 100644 --- a/spec/features/heroku_spec.rb +++ b/spec/features/heroku_spec.rb @@ -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) @@ -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) @@ -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 diff --git a/spec/features/new_project_spec.rb b/spec/features/new_project_spec.rb index c077d76c6..03a070f5e 100644 --- a/spec/features/new_project_spec.rb +++ b/spec/features/new_project_spec.rb @@ -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 @@ -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 @@ -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 diff --git a/spec/support/fake_heroku.rb b/spec/support/fake_heroku.rb index 86ad95733..429a1dd38 100644 --- a/spec/support/fake_heroku.rb +++ b/spec/support/fake_heroku.rb @@ -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 @@ -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 diff --git a/templates/app.json.erb b/templates/app.json.erb new file mode 100644 index 000000000..0b28107b7 --- /dev/null +++ b/templates/app.json.erb @@ -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" + ] +} diff --git a/templates/bin_setup.erb b/templates/bin_setup similarity index 96% rename from templates/bin_setup.erb rename to templates/bin_setup index 8340739f2..a11489886 100644 --- a/templates/bin_setup.erb +++ b/templates/bin_setup @@ -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 diff --git a/templates/bin_setup_review_app.erb b/templates/bin_setup_review_app.erb new file mode 100644 index 000000000..7d5abb605 --- /dev/null +++ b/templates/bin_setup_review_app.erb @@ -0,0 +1,19 @@ +#!/bin/sh + +# Run this script to set up a review app's database and worker dyno + +set -e + +if [ -z "$1" ]; then + printf "You must provide a review app (same as the pull request) id.\n" + exit 64 +fi + +heroku pg:backups restore \ + `heroku pg:backups public-url -a <%= app_name.dasherize %>-staging` \ + DATABASE_URL \ + --confirm <%= app_name.dasherize %>-staging-pr-$1 \ + --app <%= app_name.dasherize %>-staging-pr-$1 +heroku run rake db:migrate --app <%= app_name.dasherize %>-staging-pr-$1 +heroku ps:scale worker=1 --app <%= app_name.dasherize %>-staging-pr-$1 +heroku restart --app <%= app_name.dasherize %>-staging-pr-$1